diff --git a/api/accounts/add.js b/api/accounts/add.js index 47765eb..d063580 100644 --- a/api/accounts/add.js +++ b/api/accounts/add.js @@ -4,8 +4,7 @@ var createAccount = require('../../utils/account/create') function addAccount (state, options) { return new Promise(function (resolve, reject) { - createAccount({ - couchUrl: state.url, + createAccount(state, { username: options.username, password: options.password, includeProfile: options.include === 'account.profile' diff --git a/api/accounts/find-all.js b/api/accounts/find-all.js new file mode 100644 index 0000000..85ba297 --- /dev/null +++ b/api/accounts/find-all.js @@ -0,0 +1,31 @@ +module.exports = findAccount + +var findIdInRoles = require('../../utils/find-id-in-roles') +var getAllAccounts = require('../../utils/account/get-all') + +function findAccount (state, options) { + return new Promise(function (resolve, reject) { + getAllAccounts({ + couchUrl: state.url + }, function (error, response) { + if (error) { + return reject(error) + } + + resolve(response.rows.map(toAccount.bind(null, options))) + }) + }) +} + +function toAccount (options, row) { + var account = { + username: row.doc.name, + id: findIdInRoles(row.doc.roles) + } + + if (options.include === 'profile') { + account.profile = row.doc.profile + } + + return account +} diff --git a/api/accounts/find.js b/api/accounts/find.js index 0eb5803..9e8220f 100644 --- a/api/accounts/find.js +++ b/api/accounts/find.js @@ -8,7 +8,7 @@ function findAccount (state, username, options) { getAccount({ couchUrl: state.url, username: username, - includeProfile: options.include === 'profile' + bearerToken: options.bearerToken }, function (error, doc) { if (error) { return reject(error) @@ -18,6 +18,11 @@ function findAccount (state, username, options) { username: doc.name, id: findIdInRoles(doc.roles) } + + if (options.include === 'profile') { + account.profile = account.profile + } + resolve(account) }) }) diff --git a/api/index.js b/api/index.js index 7ae81a8..5aa8efb 100644 --- a/api/index.js +++ b/api/index.js @@ -8,6 +8,7 @@ var removeSession = require('./sessions/remove') var addAccount = require('./accounts/add') var findAccount = require('./accounts/find') +var findAllAccounts = require('./accounts/find-all') var removeAccount = require('./accounts/remove') function accountApi (options) { @@ -22,6 +23,7 @@ function accountApi (options) { accounts: { add: addAccount.bind(null, state), find: findAccount.bind(null, state), + findAll: findAllAccounts.bind(null, state), remove: removeAccount.bind(null, state) } } diff --git a/plugin/index.js b/plugin/index.js index e6876ad..99a1f35 100644 --- a/plugin/index.js +++ b/plugin/index.js @@ -5,7 +5,8 @@ hapiAccount.attributes = { var routePlugins = [ require('../routes/session'), - require('../routes/account') + require('../routes/account'), + require('../routes/accounts') ] function hapiAccount (server, options, next) { diff --git a/routes/account.js b/routes/account.js index b496481..f58646d 100644 --- a/routes/account.js +++ b/routes/account.js @@ -3,6 +3,8 @@ module.exports.attributes = { name: 'account-routes-account' } +var Boom = require('boom') + var getApi = require('../api') var joiFailAction = require('../utils/joi-fail-action') var serialiseAccount = require('../utils/account/serialise') @@ -12,7 +14,7 @@ var validations = require('../utils/validations') function accountRoutes (server, options, next) { var couchUrl = options.couchdb.url var prefix = options.prefix || '' - var api = getApi({ url: couchUrl }) + var api = getApi({ url: couchUrl, admin: options.admin }) var sessions = api.sessions var accounts = api.accounts var serialise = serialiseAccount.bind(null, { @@ -23,6 +25,7 @@ function accountRoutes (server, options, next) { method: 'PUT', path: prefix + '/session/account', config: { + auth: false, validate: { headers: validations.bearerTokenHeaderForbidden, query: validations.accountQuery, @@ -53,6 +56,9 @@ function accountRoutes (server, options, next) { var getAccountRoute = { method: 'GET', path: prefix + '/session/account', + config: { + auth: false + }, handler: function (request, reply) { var sessionId = toBearerToken(request) @@ -61,6 +67,10 @@ function accountRoutes (server, options, next) { }) .then(function (session) { + if (session.account.isAdmin) { + throw Boom.forbidden('Admin users have no account') + } + return accounts.find(session.account.username, { bearerToken: sessionId, include: request.query.include @@ -78,6 +88,9 @@ function accountRoutes (server, options, next) { var destroyAccountRoute = { method: 'DELETE', path: prefix + '/session/account', + config: { + auth: false + }, handler: function (request, reply) { var sessionId = toBearerToken(request) diff --git a/routes/accounts.js b/routes/accounts.js new file mode 100644 index 0000000..6efe7e5 --- /dev/null +++ b/routes/accounts.js @@ -0,0 +1,54 @@ +module.exports = accountRoutes +module.exports.attributes = { + name: 'account-routes-accounts' +} + +var getApi = require('../api') +var joiFailAction = require('../utils/joi-fail-action') +var serialiseAccount = require('../utils/account/serialise') +var toBearerToken = require('../utils/to-bearer-token') +var validations = require('../utils/validations') + +function accountRoutes (server, options, next) { + var couchUrl = options.couchdb.url + var prefix = options.prefix || '' + var api = getApi({ url: couchUrl }) + var accounts = api.accounts + var serialise = serialiseAccount.bind(null, { + baseUrl: server.info.uri + prefix + }) + + var getAccountsRoute = { + method: 'GET', + path: prefix + '/accounts', + config: { + auth: false, + validate: { + headers: validations.bearerTokenHeader, + failAction: joiFailAction + } + }, + handler: function (request, reply) { + var sessionId = toBearerToken(request) + + return accounts.findAll({ + bearerToken: sessionId, + include: request.query.include + }) + + .then(function (accounts) { + return accounts.map(serialise) + }) + + .then(reply) + + .catch(reply) + } + } + + server.route([ + getAccountsRoute + ]) + + next() +} diff --git a/routes/session.js b/routes/session.js index d4b11b0..f44eef0 100644 --- a/routes/session.js +++ b/routes/session.js @@ -12,7 +12,7 @@ var validations = require('../utils/validations') function sessionRoutes (server, options, next) { var couchUrl = options.couchdb.url var prefix = options.prefix || '' - var sessions = getApi({ url: couchUrl }).sessions + var sessions = getApi({ url: couchUrl, admin: options.admin }).sessions var serialise = serialiseSession.bind(null, { baseUrl: server.info.uri + prefix }) @@ -21,6 +21,7 @@ function sessionRoutes (server, options, next) { method: 'PUT', path: prefix + '/session', config: { + auth: false, validate: { headers: validations.bearerTokenHeaderForbidden, query: validations.sessionQuery, @@ -52,6 +53,7 @@ function sessionRoutes (server, options, next) { method: 'GET', path: prefix + '/session', config: { + auth: false, validate: { headers: validations.bearerTokenHeader, query: validations.sessionQuery, @@ -77,6 +79,7 @@ function sessionRoutes (server, options, next) { method: 'DELETE', path: prefix + '/session', config: { + auth: false, validate: { headers: validations.bearerTokenHeader, query: validations.sessionQuery, diff --git a/utils/account/create.js b/utils/account/create.js index 80d058d..29ba0ec 100644 --- a/utils/account/create.js +++ b/utils/account/create.js @@ -3,10 +3,10 @@ module.exports = createAccount var Boom = require('boom') var randomstring = require('randomstring') -function createAccount (options, callback) { +function createAccount (state, options, callback) { var request = require('request').defaults({ json: true, - baseUrl: options.couchUrl, + baseUrl: state.url, timeout: 10000 // 10 seconds }) @@ -16,7 +16,7 @@ function createAccount (options, callback) { charset: 'hex' }) - request.put({ + var requestOptions = { url: '/_users/' + accountKey, body: { type: 'user', @@ -26,7 +26,15 @@ function createAccount (options, callback) { name: options.username, password: options.password } - }, function (error, response, body) { + } + + if (state.admin) { + requestOptions.auth = { + user: state.admin.username, + pass: state.admin.password + } + } + request.put(requestOptions, function (error, response, body) { if (error) { return callback(Boom.wrap(error)) } diff --git a/utils/account/get-all.js b/utils/account/get-all.js new file mode 100644 index 0000000..d883af1 --- /dev/null +++ b/utils/account/get-all.js @@ -0,0 +1,35 @@ +module.exports = getAllAccounts + +var Boom = require('boom') + +function getAllAccounts (options, callback) { + var request = require('request').defaults({ + json: true, + baseUrl: options.couchUrl, + timeout: 10000 // 10 seconds + }) + + request.get({ + url: '/_users/_all_docs?startkey=%22org.couchdb.user%3A%22&enkey=%22org.couchdb.user%3A%E9%A6%99%22', + headers: { + cookie: 'AuthSession=' + options.bearerToken + } + }, function (error, response, body) { + if (error) { + return callback(Boom.wrap(error)) + } + + if (response.statusCode >= 400) { + return callback(Boom.create(response.statusCode, fixErrorMessage(body.reason))) + } + callback(null, body) + }) +} + +function fixErrorMessage (message) { + if (message === 'Only admins can access _all_docs of system databases.') { + return 'Only admins can access /users' + } + + return message +} diff --git a/utils/find-custom-roles.js b/utils/find-custom-roles.js new file mode 100644 index 0000000..d544a8b --- /dev/null +++ b/utils/find-custom-roles.js @@ -0,0 +1,17 @@ +module.exports = findCustomRoles + +function findCustomRoles (roles) { + return roles.filter(isntInteralRole) +} + +function isntInteralRole (role) { + if (role === '_admin') { + return false + } + + if (role.substr(0, 3) === 'id:') { + return false + } + + return true +} diff --git a/utils/has-admin-role.js b/utils/has-admin-role.js new file mode 100644 index 0000000..c727260 --- /dev/null +++ b/utils/has-admin-role.js @@ -0,0 +1,5 @@ +module.exports = hasAdminRole + +function hasAdminRole (roles) { + return roles.indexOf('_admin') !== -1 +} diff --git a/utils/session/create.js b/utils/session/create.js index 72c6fc2..27a97cc 100644 --- a/utils/session/create.js +++ b/utils/session/create.js @@ -2,8 +2,10 @@ module.exports = createSession var Boom = require('boom') +var findcustomRoles = require('../find-custom-roles') var findIdInRoles = require('../find-id-in-roles') var getAccount = require('../account/get') +var hasAdminRole = require('../has-admin-role') function createSession (options, callback) { var request = require('request').defaults({ @@ -36,14 +38,21 @@ function createSession (options, callback) { bearerToken = bearerToken.pop() var accountId = findIdInRoles(body.roles) + var isAdmin = hasAdminRole(body.roles) var session = { id: bearerToken, account: { id: accountId, - username: options.username + username: options.username, + isAdmin: isAdmin, + roles: findcustomRoles(body.roles) } } + if (isAdmin && options.includeProfile) { + return callback(Boom.forbidden('Admin accounts have no profile')) + } + if (!options.includeProfile) { return callback(null, session) } diff --git a/utils/session/get.js b/utils/session/get.js index c3904d0..1d409f6 100644 --- a/utils/session/get.js +++ b/utils/session/get.js @@ -2,8 +2,10 @@ module.exports = getSession var Boom = require('boom') +var findcustomRoles = require('../find-custom-roles') var findIdInRoles = require('../find-id-in-roles') var getAccount = require('../account/get') +var hasAdminRole = require('../has-admin-role') function getSession (options, callback) { var request = require('request').defaults({ @@ -32,14 +34,21 @@ function getSession (options, callback) { var username = body.userCtx.name var accountId = findIdInRoles(body.userCtx.roles) + var isAdmin = hasAdminRole(body.userCtx.roles) var session = { id: options.bearerToken, account: { id: accountId, - username: username + username: username, + isAdmin: isAdmin, + roles: findcustomRoles(body.userCtx.roles) } } + if (isAdmin && options.includeProfile) { + return callback(Boom.frobidden('Admin accounts have no profile')) + } + if (!options.includeProfile) { return callback(null, session) } diff --git a/utils/session/serialise.js b/utils/session/serialise.js index 9b5c88d..e3cfcb5 100644 --- a/utils/session/serialise.js +++ b/utils/session/serialise.js @@ -1,6 +1,17 @@ module.exports = serialiseSession function serialiseSession (options, session) { + if (session.account.isAdmin) { + return { + links: { + self: options.baseUrl + '/session' + }, + data: { + id: session.id, + type: 'session' + } + } + } var json = { links: { self: options.baseUrl + '/session' @@ -25,7 +36,8 @@ function serialiseSession (options, session) { id: session.account.id, type: 'account', attributes: { - username: session.account.username + username: session.account.username, + roles: session.account.roles }, relationships: { profile: { diff --git a/utils/to-bearer-token.js b/utils/to-bearer-token.js index 20f9bcb..2e4d7f5 100644 --- a/utils/to-bearer-token.js +++ b/utils/to-bearer-token.js @@ -1,5 +1,8 @@ module.exports = toBearerToken function toBearerToken (request) { + if (!request.headers.authorization) { + return '' + } return request.headers.authorization.substr(7) }