/* jshint strict: false */ /* global ArangoAgency */ 'use strict'; //////////////////////////////////////////////////////////////////////////////// // @brief User management // // @file // // DISCLAIMER // // Copyright 2004-2014 triAGENS GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Copyright holder is triAGENS GmbH, Cologne, Germany // // @author Jan Steemann // @author Copyright 2012-2014, triAGENS GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// const internal = require('internal'); // OK: reloadAuth const arangodb = require('@arangodb'); const shallowCopy = require('@arangodb/util').shallowCopy; const crypto = require('@arangodb/crypto'); const db = arangodb.db; const ArangoError = arangodb.ArangoError; // converts a user document to the legacy format const convertToLegacyFormat = function (doc) { let ad = doc.authData || {}; return { user: doc.user, active: ad.active, extra: doc.userData || {}, changePassword: ad.changePassword }; }; // encode password using SHA256 const hashPassword = function (password) { const salt = internal.genRandomAlphaNumbers(16); return { hash: crypto.sha256(salt + password), salt: salt, method: 'sha256' }; }; // validates a username const validateName = function (username) { if (typeof username !== 'string' || username === '') { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_INVALID_NAME.code; err.errorMessage = arangodb.errors.ERROR_USER_INVALID_NAME.message; throw err; } }; // validates password const validatePassword = function (password) { if (typeof password !== 'string') { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_INVALID_PASSWORD.code; err.errorMessage = arangodb.errors.ERROR_USER_INVALID_PASSWORD.message; throw err; } if (password === 'ARANGODB_DEFAULT_ROOT_PASSWORD') { password = require('process').env.ARANGODB_DEFAULT_ROOT_PASSWORD || ''; } return password; }; // returns the users collection const getStorage = function () { if (db._name() !== '_system') { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_FORBIDDEN.code; err.errorMessage = 'users can only be used in _system database'; throw err; } const users = db._collection('_users'); if (users === null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_ARANGO_COLLECTION_NOT_FOUND.code; err.errorMessage = 'collection _users not found'; throw err; } return users; }; // creates a new user exports.save = function (username, password, active, userData, changePassword) { const users = getStorage(); if (password === null || password === undefined) { password = ''; } // validate input validateName(username); password = validatePassword(password); if (active === undefined || active === null) { active = true; // this is the default value } if (active === undefined || active === null) { active = true; // this is the default value } if (changePassword === undefined || changePassword === null) { changePassword = false; // this is the default } const user = users.firstExample({ user: username }); if (user !== null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_DUPLICATE.code; err.errorMessage = arangodb.errors.ERROR_USER_DUPLICATE.message; throw err; } const data = { user: username, databases: {}, configData: {}, userData: userData || {}, authData: { simple: hashPassword(password), active: Boolean(active), changePassword: Boolean(changePassword) } }; const doc = users.save(data); // not exports.reload() as this is an abstract method... require('@arangodb/users').reload(); return convertToLegacyFormat(users.document(doc._id)); }; // replaces an existing user exports.replace = function (username, password, active, userData, changePassword) { const users = getStorage(); if (password === null || password === undefined) { password = ''; } // validate input validateName(username); password = validatePassword(password); if (active === undefined || active === null) { active = true; // this is the default } if (changePassword === undefined || changePassword === null) { changePassword = false; // this is the default } const user = users.firstExample({ user: username }); if (user === null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; throw err; } const data = { user: username, databases: user.databases, configData: user.configData, userData: userData || {}, authData: { simple: hashPassword(password), active: Boolean(active), changePassword: Boolean(changePassword) } }; const doc = users.replace(user, data); // not exports.reload() as this is an abstract method... require('@arangodb/users').reload(); return convertToLegacyFormat(users.document(doc._id)); }; // updates an existing user exports.update = function (username, password, active, userData, changePassword) { const users = getStorage(); // validate input validateName(username); if (password !== undefined) { password = validatePassword(password); } const user = users.firstExample({ user: username }); if (user === null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; throw err; } const data = shallowCopy(user); if (password !== undefined) { data.authData.simple = hashPassword(password); } if (active !== undefined && active !== null) { data.authData.active = active; } if (userData !== undefined) { data.userData = userData; } if (changePassword !== undefined && changePassword !== null) { data.authData.changePassword = changePassword; } users.update(user, data); // not exports.reload() as this is an abstract method... require('@arangodb/users').reload(); return convertToLegacyFormat(users.document(user._id)); }; // deletes an existing user exports.remove = function (username) { const users = getStorage(); // validate input validateName(username); const user = users.firstExample({ user: username }); if (user === null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; throw err; } // not exports.reload() as this is an abstract method... require('@arangodb/users').reload(); users.remove(user); }; // gets an existing user exports.document = function (username) { const users = getStorage(); // validate name validateName(username); const user = users.firstExample({ user: username }); if (user === null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; throw err; } return convertToLegacyFormat(user); }; exports.exists = function (username) { try { exports.document(username); return true; } catch (e) { if (e.errorNum === arangodb.errors.ERROR_USER_NOT_FOUND.code) { return false; } throw e; } }; // checks whether a combination of username / password is valid. exports.isValid = function (username, password) { const users = getStorage(); // validate name validateName(username); const user = users.firstExample({ user: username }); if (user === null || !user.authData.active) { return false; } // penalize the call internal.sleep(Math.random()); const hash = crypto[user.authData.simple.method](user.authData.simple.salt + password); return crypto.constantEquals(user.authData.simple.hash, hash); }; // gets all existing users exports.all = function () { const users = getStorage(); return users.all().toArray().map(convertToLegacyFormat); }; // reloads the user authentication data exports.reload = function () { internal.reloadAuth(); if (require('@arangodb/cluster').isCoordinator()) { // Tell the agency about this reload, such that all other coordinators // reload as well. This is important because most calls to this // function here come from actual changes in the collection _users. let UserVersion; let done = false; while (!done) { try { UserVersion = ArangoAgency.get('Sync/UserVersion'); UserVersion = UserVersion.arango.Sync.UserVersion; } catch (err) { break; } try { done = ArangoAgency.cas('Sync/UserVersion', UserVersion, UserVersion + 1); } catch (err2) { break; } } } }; // sets a password-change token exports.setPasswordToken = function (username, token) { const users = getStorage(); const user = users.firstExample({ user: username }); if (user === null) { return null; } if (token === null || token === undefined) { token = internal.genRandomAlphaNumbers(50); } users.update(user, { authData: { passwordToken: token } }); return token; }; // checks the password-change token exports.userByToken = function (token) { const users = getStorage(); return users.firstExample({ 'authData.passwordToken': token }); }; // checks the password-change token exports.changePassword = function (token, password) { const users = getStorage(); const user = users.firstExample({ 'authData.passwordToken': token }); if (user === null) { return false; } password = validatePassword(password); const authData = shallowCopy(user).authData; delete authData.passwordToken; authData.simple = hashPassword(password); authData.changePassword = false; users.update(user, { authData: authData }); // not exports.reload() as this is an abstract method... require('@arangodb/users').reload(); return true; }; // changes the allowed databases exports.grantDatabase = function (username, database, type) { const users = getStorage(); if (type === undefined) { type = 'rw'; } else if (type !== 'rw' && type !== 'ro') { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; err.errorMessage = "expecting access type 'rw' or 'ro'"; throw err; } // validate name validateName(username); const user = users.firstExample({ user: username }); if (user === null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; throw err; } let databases = user.databases || {}; databases[database] = type; users.update(user, { databases: databases }); // not exports.reload() as this is an abstract method... require('@arangodb/users').reload(); return databases; }; // changes the allowed databases exports.revokeDatabase = function (username, database) { const users = getStorage(); // validate name validateName(username); const user = users.firstExample({ user: username }); if (user === null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; throw err; } let databases = user.databases || {}; databases[database] = undefined; delete databases[database]; users.update(user, { databases: databases }, { keepNull: false, mergeObjects: false }); // not exports.reload() as this is an abstract method... require('@arangodb/users').reload(); return databases; }; // create/update (value != null) or delete (value == null) exports.updateConfigData = function (username, key, value) { const users = getStorage(); validateName(username); const user = users.firstExample({ user: username }); if (user === null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; throw err; } if (value === undefined) { value = null; } var options = user.configData; if (key === undefined || key === null) { var data = shallowCopy(user); data.configData = {}; users.replace(user, data); } else { options[key] = value; users.update(user, { configData: options }, false, false); } }; // one config data (key != null) or all (key == null) exports.configData = function (username, key) { const users = getStorage(); validateName(username); const user = users.firstExample({ user: username }); if (user === null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; throw err; } if (key === undefined || key === null) { return user.configData; } else { return user.configData[key]; } }; // one db permission data (key != null) or all (key == null) exports.permission = function (username, key) { const users = getStorage(); validateName(username); const user = users.firstExample({ user: username }); if (user === null) { const err = new ArangoError(); err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code; err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message; throw err; } if (key === undefined || key === null) { let databases = user.databases; let result = {}; if (databases.hasOwnProperty('*')) { const p = databases['*']; const l = db._databases(); for (let k = 0; k < l.length; ++k) { var dbname = l[k]; result[dbname] = p; } } for (let k in databases) { if (k !== '*' && databases.hasOwnProperty(k)) { result[k] = databases[k]; } } return result; } else { if (key === '*') { return user.databases[key]; } else { if (user.databases.hasOwnProperty(key)) { return user.databases[key]; } if (user.databases.hasOwnProperty('*')) { return user.databases['*']; } return ''; } } };