1
0
Fork 0
arangodb/js/server/modules/@arangodb/users.js

438 lines
12 KiB
JavaScript

/*jshint strict: false */
/*global ArangoAgency */
////////////////////////////////////////////////////////////////////////////////
/// @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
////////////////////////////////////////////////////////////////////////////////
var internal = require("internal"); // OK: reloadAuth
var arangodb = require("@arangodb");
var shallowCopy = require("@arangodb/util").shallowCopy;
var crypto = require("@arangodb/crypto");
var db = arangodb.db;
var ArangoError = arangodb.ArangoError;
////////////////////////////////////////////////////////////////////////////////
/// @brief converts a user document to the legacy format
////////////////////////////////////////////////////////////////////////////////
var convertToLegacyFormat = function (doc) {
return {
user: doc.user,
active: doc.authData.active,
extra: doc.userData || {},
changePassword: doc.authData.changePassword
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief encode password using SHA256
////////////////////////////////////////////////////////////////////////////////
var hashPassword = function (password) {
var salt = internal.genRandomAlphaNumbers(16);
return {
hash: crypto.sha256(salt + password),
salt: salt,
method: "sha256"
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief validates a username
////////////////////////////////////////////////////////////////////////////////
var validateName = function (username) {
if (typeof username !== "string" || username === "") {
var err = new ArangoError();
err.errorNum = arangodb.errors.ERROR_USER_INVALID_NAME.code;
err.errorMessage = arangodb.errors.ERROR_USER_INVALID_NAME.message;
throw err;
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief validates password
////////////////////////////////////////////////////////////////////////////////
var validatePassword = function (password) {
if (typeof password !== "string") {
var 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;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief returns the users collection
////////////////////////////////////////////////////////////////////////////////
var getStorage = function () {
var users = db._collection("_users");
if (users === null) {
var err = new ArangoError();
err.errorNum = arangodb.errors.ERROR_ARANGO_COLLECTION_NOT_FOUND.code;
err.errorMessage = "collection _users not found";
throw err;
}
return users;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief creates a new user
////////////////////////////////////////////////////////////////////////////////
exports.save = function (username, password, active, userData, changePassword) {
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
}
var users = getStorage();
var user = users.firstExample({user: username});
if (user !== null) {
var err = new ArangoError();
err.errorNum = arangodb.errors.ERROR_USER_DUPLICATE.code;
err.errorMessage = arangodb.errors.ERROR_USER_DUPLICATE.message;
throw err;
}
var data = {
user: username,
userData: userData || {},
authData: {
simple: hashPassword(password),
active: Boolean(active),
changePassword: Boolean(changePassword)
}
};
var doc = users.save(data);
// not exports.reload() as this is an abstract method...
require("@arangodb/users").reload();
return convertToLegacyFormat(users.document(doc._id));
};
////////////////////////////////////////////////////////////////////////////////
/// @brief replaces an existing user
////////////////////////////////////////////////////////////////////////////////
exports.replace = function (username, password, active, userData, changePassword) {
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
}
var users = getStorage();
var user = users.firstExample({user: username});
if (user === null) {
var err = new ArangoError();
err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code;
err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message;
throw err;
}
var data = {
user: username,
userData: userData || {},
authData: {
simple: hashPassword(password),
active: Boolean(active),
changePassword: Boolean(changePassword)
}
};
var doc = users.replace(user, data);
// not exports.reload() as this is an abstract method...
require("@arangodb/users").reload();
return convertToLegacyFormat(users.document(doc._id));
};
////////////////////////////////////////////////////////////////////////////////
/// @brief updates an existing user
////////////////////////////////////////////////////////////////////////////////
exports.update = function (username, password, active, userData, changePassword) {
// validate input
validateName(username);
if (password !== undefined) {
password = validatePassword(password);
}
var users = getStorage();
var user = users.firstExample({user: username});
if (user === null) {
var err = new ArangoError();
err.errorNum = arangodb.errors.ERROR_USER_NOT_FOUND.code;
err.errorMessage = arangodb.errors.ERROR_USER_NOT_FOUND.message;
throw err;
}
var 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));
};
////////////////////////////////////////////////////////////////////////////////
/// @brief deletes an existing user
////////////////////////////////////////////////////////////////////////////////
exports.remove = function (username) {
// validate input
validateName(username);
var users = getStorage();
var user = users.firstExample({user: username});
if (user === null) {
var 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);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief gets an existing user
////////////////////////////////////////////////////////////////////////////////
exports.document = function (username) {
// validate name
validateName(username);
var users = getStorage();
var user = users.firstExample({user: username});
if (user === null) {
var 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);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief checks whether a combination of username / password is valid.
////////////////////////////////////////////////////////////////////////////////
exports.isValid = function (username, password) {
// validate name
validateName(username);
var users = getStorage();
var user = users.firstExample({user: username});
if (user === null || !user.authData.active) {
return false;
}
// penalize the call
internal.sleep(Math.random());
var hash = crypto[user.authData.simple.method](user.authData.simple.salt + password);
return crypto.constantEquals(user.authData.simple.hash, hash);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief gets all existing users
////////////////////////////////////////////////////////////////////////////////
exports.all = function () {
var users = getStorage();
return users.all().toArray().map(convertToLegacyFormat);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief 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.
var UserVersion;
var done = false;
while (! done) {
try {
UserVersion = ArangoAgency.get("Sync/UserVersion")["Sync/UserVersion"];
// This is now a string!
}
catch (err) {
break;
}
try {
done = ArangoAgency.cas("Sync/UserVersion",UserVersion,
(parseInt(UserVersion,10)+1).toString());
}
catch (err2) {
break;
}
}
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief sets a password-change token
////////////////////////////////////////////////////////////////////////////////
exports.setPasswordToken = function (username, token) {
var users = getStorage();
var 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;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief checks the password-change token
////////////////////////////////////////////////////////////////////////////////
exports.userByToken = function (token) {
var users = getStorage();
return users.firstExample({"authData.passwordToken": token});
};
////////////////////////////////////////////////////////////////////////////////
/// @brief checks the password-change token
////////////////////////////////////////////////////////////////////////////////
exports.changePassword = function (token, password) {
var users = getStorage();
var user = users.firstExample({'authData.passwordToken': token});
if (user === null) {
return false;
}
password = validatePassword(password);
var 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;
};