diff --git a/CHANGELOG b/CHANGELOG index 0a450d974c..f2712bc94f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,8 @@ v2.1.0 (XXXX-XX-XX) v2.0.1 (XXXX-XX-XX) ------------------- +* added `changePassword` attribute for users + * fixed non-working "save" button in collection edit view of web interface clicking the save button did nothing. one had to press enter in one of the input fields to send modified form data diff --git a/Documentation/DbaManual/Authentication.md b/Documentation/DbaManual/Authentication.md index 6eee86a1b9..12656c151a 100644 --- a/Documentation/DbaManual/Authentication.md +++ b/Documentation/DbaManual/Authentication.md @@ -29,39 +29,162 @@ Command-Line Options for the Authentication and Authorisation{#DbaManualAuthenti Introduction to User Management{#UserManagementIntro} ===================================================== -ArangoDB provides basic functionality to add, modify and remove -database users programmatically. The following functionality is -provided by the `users` module and can be used from inside arangosh -and arangod. +ArangoDB provides basic functionality to add, modify and remove database users +programmatically. The following functionality is provided by the `users` module +and can be used from inside arangosh and arangod. -Please note that this functionality is not available from within the -web interface. +Please note that this functionality is not available from within the web +interface. +@COMMENT{######################################################################} @anchor UserManagementSave -@copydetails JSF_saveUser +@FUN{users.save(@FA{user}, @FA{passwd}, @FA{active}, @FA{extra}, @FA{changePassword})} + +This will create a new ArangoDB user. The username must be specified in +@FA{user} and must not be empty. + +The password must be given as a string, too, but can be left empty if required. + +If the @FA{active} attribute is not specified, it defaults to `true`. The +@FA{extra} attribute can be used to save custom data with the user. + +If the @FA{changePassword} attribute is not specified, it defaults to `false`. +The @FA{changePassword} attribute can be used to indicate that the user must +change has password before logging in. + +This method will fail if either the username or the passwords are not specified +or given in a wrong format, or there already exists a user with the specified +name. + +The new user account can only be used after the server is either restarted or +the server authentication cache is @ref UserManagementReload "reloaded". + +Note: this function will not work from within the web interface + +@EXAMPLES + + arangosh> require("org/arangodb/users").save("my-user", "my-secret-password"); + +@COMMENT{######################################################################} @CLEARPAGE @anchor UserManagementDocument -@copydetails JSF_documentUser +@FUN{users.document(@FA{user})} +Fetches an existing ArangoDB user from the database. + +The username must be specified in @FA{user}. + +This method will fail if the user cannot be found in the database. + +Note: this function will not work from within the web interface + +@COMMENT{######################################################################} @CLEARPAGE @anchor UserManagementReplace -@copydetails JSF_replaceUser +@FUN{users.replace(@FA{user}, @FA{passwd}, @FA{active}, @FA{extra}, @FA{changePassword})} +This will look up an existing ArangoDB user and replace its user data. + +The username must be specified in @FA{user}, and a user with the specified name +must already exist in the database. + +The password must be given as a string, too, but can be left empty if required. + +If the @FA{active} attribute is not specified, it defaults to `true`. The +@FA{extra} attribute can be used to save custom data with the user. + +If the @FA{changePassword} attribute is not specified, it defaults to `false`. +The @FA{changePassword} attribute can be used to indicate that the user must +change has password before logging in. + +This method will fail if either the username or the passwords are not specified +or given in a wrong format, or if the specified user cannot be found in the +database. + +Note: this function will not work from within the web interface + +@EXAMPLES + + arangosh> require("org/arangodb/users").replace("my-user", "my-changed-password"); + +@COMMENT{######################################################################} @CLEARPAGE @anchor UserManagementUpdate -@copydetails JSF_updateUser +@FUN{@FA{users}.update(@FA{user}, @FA{passwd}, @FA{active}, @FA{extra}, @FA{changePassword})} +This will update an existing ArangoDB user with a new password and other data. + +The username must be specified in @FA{user} and the user must already exist in +the database. + +The password must be given as a string, too, but can be left empty if required. + +If the @FA{active} attribute is not specified, the current value saved for the +user will not be changed. The same is true for the @FA{extra} and the +@FA{changePassword} attribute. + +This method will fail if either the username or the passwords are not specified +or given in a wrong format, or if the specified user cannot be found in the +database. + +Note: this function will not work from within the web interface + +@EXAMPLES + + arangosh> require("org/arangodb/users").update("my-user", "my-secret-password"); + +@COMMENT{######################################################################} @CLEARPAGE @anchor UserManagementRemove -@copydetails JSF_removeUser +@FUN{users.remove(@FA{user})} +Removes an existing ArangoDB user from the database. + +The username must be specified in @FA{user} and the specified user must exist in +the database. + +This method will fail if the user cannot be found in the database. + +Note: this function will not work from within the web interface + +@EXAMPLES + + arangosh> require("org/arangodb/users").remove("my-user"); + +@COMMENT{######################################################################} @CLEARPAGE @anchor UserManagementReload -@copydetails JSF_reloadUsers +@FUN{users.reload()} +Reloads the user authentication data on the server + +All user authentication data is loaded by the server once on startup only and is +cached after that. When users get added or deleted, a cache flush is required, +and this can be performed by called this method. + +Note: this function will not work from within the web interface + +@COMMENT{######################################################################} @CLEARPAGE @anchor UserManagementIsValid -@copydetails JSF_isValidUsers +@FUN{users.isvalid(@FA{user}, @FA{password})} +Checks whether the given combination of username and password is valid. The +function will return a boolean value if the combination of username and password +is valid. + +Each call to this function is penalized by the server sleeping a random +amount of time. + +Note: this function will not work from within the web interface + +@COMMENT{######################################################################} +@CLEARPAGE +@anchor UserManagementAll +@FUN{users.all()} + +Fetches all existing ArangoDB users from the database. + +@COMMENT{######################################################################} @BNAVIGATE_DbaManualAuthentication diff --git a/Documentation/DbaManual/AuthenticationTOC.md b/Documentation/DbaManual/AuthenticationTOC.md index 5153cd33fa..a7c651a8b1 100644 --- a/Documentation/DbaManual/AuthenticationTOC.md +++ b/Documentation/DbaManual/AuthenticationTOC.md @@ -8,6 +8,8 @@ TOC {#DbaManualAuthenticationTOC} - @ref UserManagementSave "users.save" - @ref UserManagementDocument "users.document" - @ref UserManagementReplace "users.replace" + - @ref UserManagementUpdate "users.update" - @ref UserManagementRemove "users.remove" - @ref UserManagementReload "users.reload" - @ref UserManagementIsValid "users.isValid" + - @ref UserManagementAll "users.all" diff --git a/Documentation/ImplementorManual/HttpUser.md b/Documentation/ImplementorManual/HttpUser.md index d0edfda62d..1dab35bcf5 100644 --- a/Documentation/ImplementorManual/HttpUser.md +++ b/Documentation/ImplementorManual/HttpUser.md @@ -1,44 +1,300 @@ -HTTP Interface for User Management {#HttpUser} -============================================== +HTTP Interface for User Management{#HttpUser} +============================================= @NAVIGATE_HttpUser @EMBEDTOC{HttpUserTOC} -User Management {#HttpUserIntro} -================================ +User Management{#HttpUserIntro} +=============================== This is an introduction to ArangoDB's Http interface for managing users. -The interface provides a simple means to add, update, and remove users. -All users managed through this interface will be stored in the system -collection `_users`. +The interface provides a simple means to add, update, and remove users. All +users managed through this interface will be stored in the system collection +`_users`. -This specialised interface intentionally does not provide all functionality -that is available in the regular document REST API. +This specialised interface intentionally does not provide all functionality that +is available in the regular document REST API. -Operations on users may become more restricted than regular document operations, -and extra privileges and security security checks may be introduced in the +Operations on users may become more restricted than regular document operations, +and extra privileges and security security checks may be introduced in the future for this interface. Please note that user operations are not included in ArangoDB's replication. +@COMMENT{######################################################################} @anchor HttpUserSave -@copydetails JSF_post_api_user +@RESTHEADER{POST /_api/user,creates user} +@RESTBODYPARAM{body,json,required} + +@RESTDESCRIPTION + +The following data need to be passed in a JSON representation in the body of the +POST request: + +- `user`: The name of the user as a string. This is mandatory. + +- `passwd`: The user password as a string. If no password is specified, the + empty string will be used. + +- `active`: an optional flag that specifies whether the user is active. If not + specified, this will default to `true`. + +- `extra`: an optional JSON object with arbitrary extra data about the user. + +- `changePassword`: an optional flag that specifies whethers the user must + change the password or not. If not specified, this will default to `false`. + If set to `true`, the only operations allowed are `PUT /_api/user` or + `PATCH /_api/user`. All other operations will result in a `HTTP 403`. + +If the user can be added by the server, the server will respond with `HTTP 201`. + +In case of success, the returned JSON object has the following properties: + +- `error`: boolean flag to indicate that an error occurred (`false` in this + case) + +- `code`: the HTTP status code + +If the JSON representation is malformed or mandatory data is missing from the +request, the server will respond with `HTTP 400`. + +The body of the response will contain a JSON object with additional error +details. The object has the following attributes: + +- `error`: boolean flag to indicate that an error occurred (`true` in this case) + +- `code`: the HTTP status code + +- `errorNum`: the server error number + +- `errorMessage`: a descriptive error message + +@RESTRETURNCODES + +@RESTRETURNCODE{201} +returned if the user can be added by the server. + +@RESTRETURNCODE{400} +If the JSON representation is malformed or mandatory data is missing from the +request. + +@COMMENT{######################################################################} @CLEARPAGE @anchor HttpUserReplace -@copydetails JSF_put_api_user +@RESTHEADER{PUT /_api/user/{user},replaces user} +@RESTURLPARAMETERS + +@RESTURLPARAM{user,string,required} +The name of the user. + +@RESTBODYPARAM{body,json,required} + +@RESTDESCRIPTION + +Replaces the data of an existing user. The name of an existing user must be +specified in `user`. + +The following data can to be passed in a JSON representation in the body of the +POST request: + +- `passwd`: The user password as a string. Specifying a password is mandatory, + but the empty string is allowed for passwords. + +- `active`: an optional flag that specifies whether the user is active. If not + specified, this will default to `true`. + +- `extra`: an optional JSON object with arbitrary extra data about the user. + +- `changePassword`: an optional flag that specifies whether the user must change + the password or not. If not specified, this will default to `false`. + +If the user can be replaced by the server, the server will respond with `HTTP +200`. + +In case of success, the returned JSON object has the following properties: + +- `error`: boolean flag to indicate that an error occurred (`false` in this + case) + +- `code`: the HTTP status code + +If the JSON representation is malformed or mandatory data is missing from the +request, the server will respond with `HTTP 400`. If the specified user does not +exist, the server will respond with `HTTP 404`. + +The body of the response will contain a JSON object with additional error +details. The object has the following attributes: + +- `error`: boolean flag to indicate that an error occurred (`true` in this case) + +- `code`: the HTTP status code + +- `errorNum`: the server error number + +- `errorMessage`: a descriptive error message + +@RESTRETURNCODES + +@RESTRETURNCODE{200} +Is returned if the user data can be replaced by the server. + +@RESTRETURNCODE{400} +The JSON representation is malformed or mandatory data is missing from the +request. + +@RESTRETURNCODE{404} +The specified user does not exist. + +@COMMENT{######################################################################} @CLEARPAGE @anchor HttpUserUpdate -@copydetails JSF_patch_api_user +@RESTHEADER{PATCH /_api/user/{user},updates user} +@RESTURLPARAMETERS + +@RESTURLPARAM{user,string,required} +The name of the user. + +@RESTBODYPARAM{body,json,required} + +@RESTDESCRIPTION + +Partially updates the data of an existing user. The name of an existing user +must be specified in `user`. + +The following data can be passed in a JSON representation in the body of the +POST request: + +- `passwd`: The user password as a string. Specifying a password is optional. If + not specified, the previously existing value will not be modified. + +- `active`: an optional flag that specifies whether the user is active. If not + specified, the previously existing value will not be modified. + +- `extra`: an optional JSON object with arbitrary extra data about the user. If + not specified, the previously existing value will not be modified. + +- `changePassword`: an optional flag that specifies whether the user must change + the password or not. If not specified, the previously existing value will not + be modified. + +If the user can be updated by the server, the server will respond with `HTTP +200`. + +In case of success, the returned JSON object has the following properties: + +- `error`: boolean flag to indicate that an error occurred (`false` in this + case) + +- `code`: the HTTP status code + +If the JSON representation is malformed or mandatory data is missing from the +request, the server will respond with `HTTP 400`. If the specified user does not +exist, the server will respond with `HTTP 404`. + +The body of the response will contain a JSON object with additional error +details. The object has the following attributes: + +- `error`: boolean flag to indicate that an error occurred (`true` in this case) + +- `code`: the HTTP status code + +- `errorNum`: the server error number + +- `errorMessage`: a descriptive error message + +@RESTRETURNCODES + +@RESTRETURNCODE{200} +Is returned if the user data can be replaced by the server. + +@RESTRETURNCODE{400} +The JSON representation is malformed or mandatory data is missing from the +request. + +@RESTRETURNCODE{404} +The specified user does not exist. + +@COMMENT{######################################################################} @CLEARPAGE @anchor HttpUserRemove -@copydetails JSF_delete_api_user +@RESTHEADER{DELETE /_api/user/{user},removes a user} +@RESTURLPARAMETERS + +@RESTURLPARAM{user,string,required} +The name of the user. + +@RESTDESCRIPTION + +Removes an existing user, identified by `user`. + +If the user can be removed, the server will respond with `HTTP 202`. + +In case of success, the returned JSON object has the following properties: + +- `error`: boolean flag to indicate that an error occurred (`false` in this + case) + +- `code`: the HTTP status code + +If the specified user does not exist, the server will respond with `HTTP 404`. + +The body of the response will contain a JSON object with additional error +details. The object has the following attributes: + +- `error`: boolean flag to indicate that an error occurred (`true` in this case) + +- `code`: the HTTP status code + +- `errorNum`: the server error number + +- `errorMessage`: a descriptive error message + +@RESTRETURNCODES + +@RESTRETURNCODE{202} +Is returned if the user was removed by the server. + +@RESTRETURNCODE{404} +The specified user does not exist. + +@COMMENT{######################################################################} @CLEARPAGE @anchor HttpUserDocument -@copydetails JSF_get_api_user +@RESTHEADER{GET /_api/user/{user},fetches a user} +@RESTURLPARAMETERS + +@RESTURLPARAM{user,string,required} +The name of the user. + +@RESTDESCRIPTION + +Fetches data about the specified user. + +The call will return a JSON document with at least the following attributes on +success: + +- `user`: The name of the user as a string. + +- `active`: an optional flag that specifies whether the user is active. + +- `extra`: an optional JSON object with arbitrary extra data about the user. + +- `changePassword`: an optional flag that specifies whether the user must change + the password or not. + +@RESTRETURNCODES + +@RESTRETURNCODE{200} +The user was found. + +@RESTRETURNCODE{404} +The user with `user` does not exist. + +@COMMENT{######################################################################} @BNAVIGATE_HttpUser diff --git a/Documentation/arango.template.in b/Documentation/arango.template.in index 30f6e38950..fb25910430 100644 --- a/Documentation/arango.template.in +++ b/Documentation/arango.template.in @@ -270,6 +270,7 @@ ALIASES += \ # other aliases ALIASES += \ "VERSION=@PACKAGE_VERSION@" \ + "COMMENT{1}=" \ "LIT{1}=\1" \ "LIT{2}=\1, \2" \ "LIT{3}=\1, \2, \3" \ diff --git a/arangod/RestServer/VocbaseContext.cpp b/arangod/RestServer/VocbaseContext.cpp index 3d40849894..0374bb739c 100644 --- a/arangod/RestServer/VocbaseContext.cpp +++ b/arangod/RestServer/VocbaseContext.cpp @@ -42,18 +42,13 @@ using namespace triagens::arango; using namespace triagens::rest; // ----------------------------------------------------------------------------- -// --SECTION-- class ArangoServer +// --SECTION-- class VocbaseContext // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // --SECTION-- constructors and destructors // ----------------------------------------------------------------------------- -//////////////////////////////////////////////////////////////////////////////// -/// @addtogroup ArangoDB -/// @{ -//////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// /// @brief constructor //////////////////////////////////////////////////////////////////////////////// @@ -77,19 +72,10 @@ VocbaseContext::~VocbaseContext () { TRI_ReleaseVocBase(_vocbase); } -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// - // ----------------------------------------------------------------------------- // --SECTION-- public methods // ----------------------------------------------------------------------------- -//////////////////////////////////////////////////////////////////////////////// -/// @addtogroup ArangoDB -/// @{ -//////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// /// @brief whether or not to use special cluster authentication //////////////////////////////////////////////////////////////////////////////// @@ -175,6 +161,7 @@ HttpResponse::HttpResponseCode VocbaseContext::authenticate () { string const up = StringUtils::decodeBase64(auth); std::string::size_type n = up.find(':', 0); + if (n == std::string::npos || n == 0 || n + 1 > up.size()) { LOG_TRACE("invalid authentication data found, cannot extract username/password"); @@ -188,50 +175,54 @@ HttpResponse::HttpResponseCode VocbaseContext::authenticate () { } #endif - // look up the info in the cache first - char* cached = TRI_CheckCacheAuthInfo(_vocbase, auth); - + bool mustChange; + char* cached = TRI_CheckCacheAuthInfo(_vocbase, auth, &mustChange); + string username; + // found a cached entry, access must be granted if (cached != 0) { - // found a cached entry, access must be granted - _request->setUser(string(cached)); + username = string(cached); TRI_Free(TRI_CORE_MEM_ZONE, cached); - - return HttpResponse::OK; } // no entry found in cache, decode the basic auth info and look it up + else { + string const up = StringUtils::decodeBase64(auth); + std::string::size_type n = up.find(':', 0); - string const up = StringUtils::decodeBase64(auth); - - std::string::size_type n = up.find(':', 0); - if (n == std::string::npos || n == 0 || n + 1 > up.size()) { - LOG_TRACE("invalid authentication data found, cannot extract username/password"); - - return HttpResponse::BAD; - } + if (n == std::string::npos || n == 0 || n + 1 > up.size()) { + LOG_TRACE("invalid authentication data found, cannot extract username/password"); + return HttpResponse::BAD; + } - string const username = up.substr(0, n); + username = up.substr(0, n); - LOG_TRACE("checking authentication for user '%s'", username.c_str()); - - bool res = TRI_CheckAuthenticationAuthInfo(_vocbase, auth, username.c_str(), up.substr(n + 1).c_str()); + LOG_TRACE("checking authentication for user '%s'", username.c_str()); + bool res = TRI_CheckAuthenticationAuthInfo( + _vocbase, auth, username.c_str(), up.substr(n + 1).c_str(), &mustChange); - if (! res) { - return HttpResponse::UNAUTHORIZED; + if (! res) { + return HttpResponse::UNAUTHORIZED; + } } // TODO: create a user object for the VocbaseContext _request->setUser(username); + if (mustChange) { + if ((_request->requestType() == HttpRequest::HTTP_REQUEST_PUT + || _request->requestType() == HttpRequest::HTTP_REQUEST_PATCH) + && TRI_EqualString2(_request->requestPath(), "/_api/user/", 11)) { + return HttpResponse::OK; + } + + return HttpResponse::FORBIDDEN; + } + return HttpResponse::OK; } -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// - // ----------------------------------------------------------------------------- // --SECTION-- END-OF-FILE // ----------------------------------------------------------------------------- diff --git a/arangod/VocBase/auth.c b/arangod/VocBase/auth.c index 70cce9861b..05ceb1f322 100644 --- a/arangod/VocBase/auth.c +++ b/arangod/VocBase/auth.c @@ -40,11 +40,6 @@ // --SECTION-- private functions // ----------------------------------------------------------------------------- -//////////////////////////////////////////////////////////////////////////////// -/// @addtogroup VocBase -/// @{ -//////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// /// @brief hashes a string //////////////////////////////////////////////////////////////////////////////// @@ -242,6 +237,7 @@ static TRI_vocbase_auth_t* ConvertAuthInfo (TRI_vocbase_t* vocbase, char* password; bool active; bool found; + bool mustChange; TRI_vocbase_auth_t* result; shaper = primary->_shaper; @@ -273,6 +269,13 @@ static TRI_vocbase_auth_t* ConvertAuthInfo (TRI_vocbase_t* vocbase, return NULL; } + // extract must-change-password flag + mustChange = ExtractBooleanShapedJson(shaper, document, "changePassword", &found); + + if (! found) { + mustChange = false; + } + result = TRI_Allocate(TRI_UNKNOWN_MEM_ZONE, sizeof(TRI_vocbase_auth_t), true); if (result == NULL) { @@ -286,13 +289,15 @@ static TRI_vocbase_auth_t* ConvertAuthInfo (TRI_vocbase_t* vocbase, result->_username = user; result->_password = password; result->_active = active; + result->_mustChange = mustChange; return result; } //////////////////////////////////////////////////////////////////////////////// /// @brief clears the authentication info -/// the caller must acquire the lock itself +/// +/// @note the caller must acquire the lock itself //////////////////////////////////////////////////////////////////////////////// static void ClearAuthInfo (TRI_vocbase_t* vocbase) { @@ -333,19 +338,10 @@ static void ClearAuthInfo (TRI_vocbase_t* vocbase) { vocbase->_authCache._nrUsed = 0; } -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// - // ----------------------------------------------------------------------------- // --SECTION-- public functions // ----------------------------------------------------------------------------- -//////////////////////////////////////////////////////////////////////////////// -/// @addtogroup VocBase -/// @{ -//////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// /// @brief initialises the authentication info //////////////////////////////////////////////////////////////////////////////// @@ -596,7 +592,8 @@ void TRI_ClearAuthInfo (TRI_vocbase_t* vocbase) { //////////////////////////////////////////////////////////////////////////////// char* TRI_CheckCacheAuthInfo (TRI_vocbase_t* vocbase, - char const* hash) { + char const* hash, + bool* mustChange) { TRI_vocbase_auth_cache_t* cached; char* username; @@ -607,6 +604,7 @@ char* TRI_CheckCacheAuthInfo (TRI_vocbase_t* vocbase, if (cached != NULL) { username = TRI_DuplicateStringZ(TRI_CORE_MEM_ZONE, cached->_username); + *mustChange = cached->_mustChange; } TRI_ReadUnlockReadWriteLock(&vocbase->_authInfoLock); @@ -622,7 +620,8 @@ char* TRI_CheckCacheAuthInfo (TRI_vocbase_t* vocbase, bool TRI_CheckAuthenticationAuthInfo (TRI_vocbase_t* vocbase, char const* hash, char const* username, - char const* password) { + char const* password, + bool* mustChange) { TRI_vocbase_auth_t* auth; bool res; char* hex; @@ -642,9 +641,12 @@ bool TRI_CheckAuthenticationAuthInfo (TRI_vocbase_t* vocbase, return false; } + *mustChange = auth->_mustChange; + // convert password res = false; + // salted password if (TRI_IsPrefixString(auth->_password, "$1$")) { if (strlen(auth->_password) < 12 || auth->_password[11] != '$') { LOG_WARNING("found corrupted password for user '%s'", username); @@ -673,6 +675,8 @@ bool TRI_CheckAuthenticationAuthInfo (TRI_vocbase_t* vocbase, TRI_FreeString(TRI_CORE_MEM_ZONE, hex); } } + + // unsalted password else { len = strlen(password); sha256 = TRI_SHA256String(password, len, &sha256Len); @@ -700,8 +704,9 @@ bool TRI_CheckAuthenticationAuthInfo (TRI_vocbase_t* vocbase, if (cached != NULL) { void* old; - cached->_hash = TRI_DuplicateStringZ(TRI_CORE_MEM_ZONE, hash); - cached->_username = TRI_DuplicateStringZ(TRI_CORE_MEM_ZONE, username); + cached->_hash = TRI_DuplicateStringZ(TRI_CORE_MEM_ZONE, hash); + cached->_username = TRI_DuplicateStringZ(TRI_CORE_MEM_ZONE, username); + cached->_mustChange = auth->_mustChange; if (cached->_hash == NULL || cached->_username == NULL) { FreeAuthCacheInfo(cached); @@ -723,10 +728,6 @@ bool TRI_CheckAuthenticationAuthInfo (TRI_vocbase_t* vocbase, return res; } -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// - // ----------------------------------------------------------------------------- // --SECTION-- END-OF-FILE // ----------------------------------------------------------------------------- diff --git a/arangod/VocBase/auth.h b/arangod/VocBase/auth.h index c964af2de4..8c35dc91c7 100644 --- a/arangod/VocBase/auth.h +++ b/arangod/VocBase/auth.h @@ -45,11 +45,6 @@ struct TRI_vocbase_s; // --SECTION-- public types // ----------------------------------------------------------------------------- -//////////////////////////////////////////////////////////////////////////////// -/// @addtogroup VocBase -/// @{ -//////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// /// @brief authentication and authorisation //////////////////////////////////////////////////////////////////////////////// @@ -58,6 +53,7 @@ typedef struct TRI_vocbase_auth_s { char* _username; char* _password; bool _active; + bool _mustChange; } TRI_vocbase_auth_t; @@ -67,23 +63,15 @@ TRI_vocbase_auth_t; typedef struct TRI_vocbase_auth_cache_s { char* _hash; - char* _username; + char* _username; + bool _mustChange; } TRI_vocbase_auth_cache_t; -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// - // ----------------------------------------------------------------------------- // --SECTION-- public functions // ----------------------------------------------------------------------------- -//////////////////////////////////////////////////////////////////////////////// -/// @addtogroup VocBase -/// @{ -//////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// /// @brief initialises the authentication info //////////////////////////////////////////////////////////////////////////////// @@ -133,20 +121,18 @@ void TRI_ClearAuthInfo (struct TRI_vocbase_s*); //////////////////////////////////////////////////////////////////////////////// char* TRI_CheckCacheAuthInfo (struct TRI_vocbase_s*, - char const*); + char const* hash, + bool* mustChange); //////////////////////////////////////////////////////////////////////////////// /// @brief checks the authentication //////////////////////////////////////////////////////////////////////////////// bool TRI_CheckAuthenticationAuthInfo (struct TRI_vocbase_s*, - char const*, - char const*, - char const*); - -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// + char const* hash, + char const* username, + char const* password, + bool* mustChange); #ifdef __cplusplus } diff --git a/arangod/VocBase/server.c b/arangod/VocBase/server.c index 266fc93904..a65c701a11 100644 --- a/arangod/VocBase/server.c +++ b/arangod/VocBase/server.c @@ -459,12 +459,14 @@ static int WriteShutdownInfo (TRI_server_t* server) { static bool CanUseDatabase (TRI_vocbase_t* vocbase, char const* username, char const* password) { + bool mustChange; + if (! vocbase->_settings.requireAuthentication) { // authentication is turned off return true; } - return TRI_CheckAuthenticationAuthInfo(vocbase, NULL, username, password); + return TRI_CheckAuthenticationAuthInfo(vocbase, NULL, username, password, &mustChange); } //////////////////////////////////////////////////////////////////////////////// diff --git a/js/actions/api-user.js b/js/actions/api-user.js index 80cd7d8722..5d892c85a3 100644 --- a/js/actions/api-user.js +++ b/js/actions/api-user.js @@ -1,4 +1,4 @@ -/*jslint indent: 2, nomen: true, maxlen: 100, sloppy: true, vars: true, white: true, plusplus: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, sloppy: true, vars: true, white: true, plusplus: true */ /*global require */ //////////////////////////////////////////////////////////////////////////////// @@ -8,7 +8,7 @@ /// /// DISCLAIMER /// -/// Copyright 2012 triagens GmbH, Cologne, Germany +/// 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. @@ -25,7 +25,7 @@ /// Copyright holder is triAGENS GmbH, Cologne, Germany /// /// @author Jan Steemann -/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany +/// @author Copyright 2012-2014, triAGENS GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// var arangodb = require("org/arangodb"); @@ -37,42 +37,7 @@ var users = require("org/arangodb/users"); // ----------------------------------------------------------------------------- //////////////////////////////////////////////////////////////////////////////// -/// @addtogroup ArangoAPI -/// @{ -//////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////////// -/// @brief fetch a user -/// -/// @RESTHEADER{GET /_api/user/{user},fetches a user} -/// -/// @RESTURLPARAMETERS -/// -/// @RESTURLPARAM{user,string,required} -/// The name of the user. -/// -/// @RESTDESCRIPTION -/// -/// Fetches data about the specified user. -/// -/// The call will return a JSON document with at least the following attributes -/// on success: -/// -/// - `user`: The name of the user as a string. -/// -/// - `active`: an optional flag that specifies whether the user is active. -/// -/// - `extra`: an optional JSON object with arbitrary extra data about the -/// user. -/// -/// @RESTRETURNCODES -/// -/// @RESTRETURNCODE{200} -/// The user was found. -/// -/// @RESTRETURNCODE{404} -/// The user with `user` does not exist. -/// +/// @brief fetches a user //////////////////////////////////////////////////////////////////////////////// function get_api_user (req, res) { @@ -89,8 +54,7 @@ function get_api_user (req, res) { var user = decodeURIComponent(req.suffix[0]); try { - var result = users.document(user); - actions.resultOk(req, res, actions.HTTP_OK, result); + actions.resultOk(req, res, actions.HTTP_OK, users.document(user)); } catch (err) { if (err.errorNum === arangodb.errors.ERROR_USER_NOT_FOUND.code) { @@ -103,61 +67,7 @@ function get_api_user (req, res) { } //////////////////////////////////////////////////////////////////////////////// -/// @brief create a new user -/// -/// @RESTHEADER{POST /_api/user,creates user} -/// -/// @RESTBODYPARAM{body,json,required} -/// -/// @RESTDESCRIPTION -/// -/// The following data need to be passed in a JSON representation in the body of -/// the POST request: -/// -/// - `user`: The name of the user as a string. This is mandatory. -/// -/// - `passwd`: The user password as a string. If no password is specified, -/// the empty string will be used. -/// -/// - `active`: an optional flag that specifies whether the user is active. -/// If not specified, this will default to `true`. -/// -/// - `extra`: an optional JSON object with arbitrary extra data about the -/// user. -/// -/// If the user can be added by the server, the server will respond with -/// `HTTP 201`. -/// -/// In case of success, the returned JSON object has the following properties: -/// -/// - `error`: boolean flag to indicate that an error occurred (`false` -/// in this case) -/// -/// - `code`: the HTTP status code -/// -/// If the JSON representation is malformed or mandatory data is missing from the -/// request, the server will respond with `HTTP 400`. -/// -/// The body of the response will contain a JSON object with additional error -/// details. The object has the following attributes: -/// -/// - `error`: boolean flag to indicate that an error occurred (`true` in this case) -/// -/// - `code`: the HTTP status code -/// -/// - `errorNum`: the server error number -/// -/// - `errorMessage`: a descriptive error message -/// -/// @RESTRETURNCODES -/// -/// @RESTRETURNCODE{201} -/// returned if the user can be added by the server. -/// -/// @RESTRETURNCODE{400} -/// If the JSON representation is malformed or mandatory data is missing from the -/// request. -/// +/// @brief creates a new user //////////////////////////////////////////////////////////////////////////////// function post_api_user (req, res) { @@ -166,8 +76,9 @@ function post_api_user (req, res) { if (json === undefined) { return; } - - var user; + + var user; + if (req.suffix.length === 1) { // validate if a combination or username / password is valid user = decodeURIComponent(req.suffix[0]); @@ -189,82 +100,20 @@ function post_api_user (req, res) { } user = json.user; + if (user === undefined && json.hasOwnProperty("username")) { // deprecated usage user = json.username; } - users.save(user, json.passwd, json.active, json.extra); + var doc = users.save(user, json.passwd, json.active, json.extra, json.changePassword); users.reload(); - actions.resultOk(req, res, actions.HTTP_CREATED, { }); + actions.resultOk(req, res, actions.HTTP_CREATED, doc); } //////////////////////////////////////////////////////////////////////////////// -/// @brief replace an existing user -/// -/// @RESTHEADER{PUT /_api/user/{user},replaces user} -/// -/// @RESTURLPARAMETERS -/// -/// @RESTURLPARAM{user,string,required} -/// The name of the user. -/// -/// @RESTBODYPARAM{body,json,required} -/// -/// @RESTDESCRIPTION -/// -/// Replaces the data of an existing user. The name of an existing user must -/// be specified in `user`. -/// -/// The following data can to be passed in a JSON representation in the body of -/// the POST request: -/// -/// - `passwd`: The user password as a string. Specifying a password is -/// mandatory, but the empty string is allowed for passwords. -/// -/// - `active`: an optional flag that specifies whether the user is active. -/// If not specified, this will default to `true`. -/// -/// - `extra`: an optional JSON object with arbitrary extra data about the -/// user. -/// -/// If the user can be replaced by the server, the server will respond with -/// `HTTP 200`. -/// -/// In case of success, the returned JSON object has the following properties: -/// -/// - `error`: boolean flag to indicate that an error occurred (`false` -/// in this case) -/// -/// - `code`: the HTTP status code -/// -/// If the JSON representation is malformed or mandatory data is missing from the -/// request, the server will respond with `HTTP 400`. If the specified user -/// does not exist, the server will respond with `HTTP 404`. -/// -/// The body of the response will contain a JSON object with additional error -/// details. The object has the following attributes: -/// -/// - `error`: boolean flag to indicate that an error occurred (`true` in this case) -/// -/// - `code`: the HTTP status code -/// -/// - `errorNum`: the server error number -/// -/// - `errorMessage`: a descriptive error message -/// -/// @RESTRETURNCODES -/// -/// @RESTRETURNCODE{200} -/// Is returned if the user data can be replaced by the server. -/// -/// @RESTRETURNCODE{400} -/// The JSON representation is malformed or mandatory data is missing from the -/// request. -/// -/// @RESTRETURNCODE{404} -/// The specified user does not exist. +/// @brief replaces an existing user //////////////////////////////////////////////////////////////////////////////// function put_api_user (req, res) { @@ -276,16 +125,16 @@ function put_api_user (req, res) { var user = decodeURIComponent(req.suffix[0]); var json = actions.getJsonBody(req, res, actions.HTTP_BAD); - + if (json === undefined) { return; } - + try { - users.replace(user, json.passwd, json.active, json.extra); + var doc = users.replace(user, json.passwd, json.active, json.extra, json.changePassword); users.reload(); - - actions.resultOk(req, res, actions.HTTP_OK, { }); + + actions.resultOk(req, res, actions.HTTP_OK, doc); } catch (err) { if (err.errorNum === arangodb.errors.ERROR_USER_NOT_FOUND.code) { @@ -298,72 +147,7 @@ function put_api_user (req, res) { } //////////////////////////////////////////////////////////////////////////////// -/// @brief partially update an existing user -/// -/// @RESTHEADER{PATCH /_api/user/{user},updates user} -/// -/// @RESTURLPARAMETERS -/// -/// @RESTURLPARAM{user,string,required} -/// The name of the user. -/// -/// @RESTBODYPARAM{body,json,required} -/// -/// @RESTDESCRIPTION -/// -/// Partially updates the data of an existing user. The name of an existing user -/// must be specified in `user`. -/// -/// The following data can be passed in a JSON representation in the body of -/// the POST request: -/// -/// - `passwd`: The user password as a string. Specifying a password is -/// optional. If not specified, the previously existing value will not be -/// modified. -/// -/// - `active`: an optional flag that specifies whether the user is active. -/// If not specified, the previously existing value will not be modified. -/// -/// - `extra`: an optional JSON object with arbitrary extra data about the -/// user. If not specified, the previously existing value will not be modified. -/// -/// If the user can be updated by the server, the server will respond with -/// `HTTP 200`. -/// -/// In case of success, the returned JSON object has the following properties: -/// -/// - `error`: boolean flag to indicate that an error occurred (`false` -/// in this case) -/// -/// - `code`: the HTTP status code -/// -/// If the JSON representation is malformed or mandatory data is missing from the -/// request, the server will respond with `HTTP 400`. If the specified user -/// does not exist, the server will respond with `HTTP 404`. -/// -/// The body of the response will contain a JSON object with additional error -/// details. The object has the following attributes: -/// -/// - `error`: boolean flag to indicate that an error occurred (`true` in this case) -/// -/// - `code`: the HTTP status code -/// -/// - `errorNum`: the server error number -/// -/// - `errorMessage`: a descriptive error message -/// -/// @RESTRETURNCODES -/// -/// @RESTRETURNCODE{200} -/// Is returned if the user data can be replaced by the server. -/// -/// @RESTRETURNCODE{400} -/// The JSON representation is malformed or mandatory data is missing from the -/// request. -/// -/// @RESTRETURNCODE{404} -/// The specified user does not exist. -/// +/// @brief partially updates an existing user //////////////////////////////////////////////////////////////////////////////// function patch_api_user (req, res) { @@ -374,15 +158,16 @@ function patch_api_user (req, res) { var user = decodeURIComponent(req.suffix[0]); var json = actions.getJsonBody(req, res, actions.HTTP_BAD); - + if (json === undefined) { return; } - + try { - users.update(user, json.passwd, json.active, json.extra); + var doc = users.update(user, json.passwd, json.active, json.extra, json.changePassword); users.reload(); - actions.resultOk(req, res, actions.HTTP_OK, { }); + + actions.resultOk(req, res, actions.HTTP_OK, doc); } catch (err) { if (err.errorNum === arangodb.errors.ERROR_USER_NOT_FOUND.code) { @@ -395,50 +180,7 @@ function patch_api_user (req, res) { } //////////////////////////////////////////////////////////////////////////////// -/// @brief remove an existing user -/// -/// @RESTHEADER{DELETE /_api/user/{user},removes a user} -/// -/// @RESTURLPARAMETERS -/// -/// @RESTURLPARAM{user,string,required} -/// The name of the user. -/// -/// @RESTDESCRIPTION -/// -/// Removes an existing user, identified by `user`. -/// -/// If the user can be removed, the server will respond with `HTTP 202`. -/// -/// In case of success, the returned JSON object has the following properties: -/// -/// - `error`: boolean flag to indicate that an error occurred (`false` -/// in this case) -/// -/// - `code`: the HTTP status code -/// -/// If the specified user does not exist, the server will respond with -/// `HTTP 404`. -/// -/// The body of the response will contain a JSON object with additional error -/// details. The object has the following attributes: -/// -/// - `error`: boolean flag to indicate that an error occurred (`true` in this case) -/// -/// - `code`: the HTTP status code -/// -/// - `errorNum`: the server error number -/// -/// - `errorMessage`: a descriptive error message -/// -/// @RESTRETURNCODES -/// -/// @RESTRETURNCODE{202} -/// Is returned if the user was removed by the server. -/// -/// @RESTRETURNCODE{404} -/// The specified user does not exist. -/// +/// @brief removes an existing user //////////////////////////////////////////////////////////////////////////////// function delete_api_user (req, res) { @@ -449,9 +191,10 @@ function delete_api_user (req, res) { var user = decodeURIComponent(req.suffix[0]); try { - users.remove(user); + var doc = users.remove(user); users.reload(); - actions.resultOk(req, res, actions.HTTP_ACCEPTED, { }); + + actions.resultOk(req, res, actions.HTTP_ACCEPTED, doc); } catch (err) { if (err.errorNum === arangodb.errors.ERROR_USER_NOT_FOUND.code) { @@ -468,7 +211,7 @@ function delete_api_user (req, res) { // ----------------------------------------------------------------------------- //////////////////////////////////////////////////////////////////////////////// -/// @brief user actions gateway +/// @brief user actions gateway //////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ @@ -478,24 +221,24 @@ actions.defineHttp({ callback : function (req, res) { try { switch (req.requestType) { - case actions.GET: - get_api_user(req, res); + case actions.GET: + get_api_user(req, res); break; - case actions.POST: - post_api_user(req, res); + case actions.POST: + post_api_user(req, res); break; - case actions.PUT: - put_api_user(req, res); - break; - - case actions.PATCH: - patch_api_user(req, res); + case actions.PUT: + put_api_user(req, res); break; - case actions.DELETE: - delete_api_user(req, res); + case actions.PATCH: + patch_api_user(req, res); + break; + + case actions.DELETE: + delete_api_user(req, res); break; default: @@ -508,11 +251,11 @@ actions.defineHttp({ } }); -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// +// ----------------------------------------------------------------------------- +// --SECTION-- END-OF-FILE +// ----------------------------------------------------------------------------- // Local Variables: // mode: outline-minor -// outline-regexp: "^\\(/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @}\\)" +// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint" // End: diff --git a/js/apps/system/aardvark/frontend/js/bootstrap/errors.js b/js/apps/system/aardvark/frontend/js/bootstrap/errors.js index 256a82c7a9..d9cdca168b 100644 --- a/js/apps/system/aardvark/frontend/js/bootstrap/errors.js +++ b/js/apps/system/aardvark/frontend/js/bootstrap/errors.js @@ -172,6 +172,7 @@ "ERROR_USER_INVALID_PASSWORD" : { "code" : 1701, "message" : "invalid password" }, "ERROR_USER_DUPLICATE" : { "code" : 1702, "message" : "duplicate user" }, "ERROR_USER_NOT_FOUND" : { "code" : 1703, "message" : "user not found" }, + "ERROR_USER_CHANGE_PASSWORD" : { "code" : 1704, "message" : "user must change his password" }, "ERROR_APPLICATION_INVALID_NAME" : { "code" : 1750, "message" : "invalid application name" }, "ERROR_APPLICATION_INVALID_MOUNT" : { "code" : 1751, "message" : "invalid mount" }, "ERROR_APPLICATION_DOWNLOAD_FAILED" : { "code" : 1752, "message" : "application download failed" }, diff --git a/js/client/modules/org/arangodb/users.js b/js/client/modules/org/arangodb/users.js index 513af5d8ec..d9ef7bb9bf 100644 --- a/js/client/modules/org/arangodb/users.js +++ b/js/client/modules/org/arangodb/users.js @@ -1,4 +1,4 @@ -/*jslint indent: 2, nomen: true, maxlen: 100, sloppy: true, vars: true, white: true, plusplus: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, sloppy: true, vars: true, white: true, plusplus: true */ /*global require, exports */ //////////////////////////////////////////////////////////////////////////////// @@ -8,7 +8,7 @@ /// /// DISCLAIMER /// -/// Copyright 2012 triagens GmbH, Cologne, Germany +/// 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. @@ -25,19 +25,12 @@ /// Copyright holder is triAGENS GmbH, Cologne, Germany /// /// @author Jan Steemann -/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany +/// @author Copyright 2012-2014, triAGENS GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// -var internal = require("internal"); +var internal = require("internal"); +var arangodb = require("org/arangodb"); var arangosh = require("org/arangodb/arangosh"); -var base = require("org/arangodb/users-common"); - -var i; -for (i in base) { - if (base.hasOwnProperty(i)) { - exports[i] = base[i]; - } -} // ----------------------------------------------------------------------------- // --SECTION-- module "org/arangodb/users" @@ -48,14 +41,150 @@ for (i in base) { // ----------------------------------------------------------------------------- //////////////////////////////////////////////////////////////////////////////// -/// @addtogroup ArangoShell -/// @{ +/// @brief creates a new user //////////////////////////////////////////////////////////////////////////////// +exports.save = function (user, passwd, active, extra, changePassword) { + var db = internal.db; + + var uri = "_api/user/"; + var data = {user: user}; + + if (passwd !== undefined) { + data.passwd = passwd; + } + + if (active !== undefined) { + data.active = active; + } + + if (extra !== undefined) { + data.extra = extra; + } + + if (changePassword !== undefined) { + data.changePassword = changePassword; + } + + var requestResult = db._connection.POST(uri, JSON.stringify(data)); + return arangosh.checkRequestResult(requestResult); +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief replaces an existing user +//////////////////////////////////////////////////////////////////////////////// + +exports.replace = function (user, passwd, active, extra, changePassword) { + var db = internal.db; + + var uri = "_api/user/" + encodeURIComponent(user); + var data = { + passwd: passwd, + active: active, + extra: extra, + changePassword: changePassword + }; + + var requestResult = db._connection.PUT(uri, JSON.stringify(data)); + return arangosh.checkRequestResult(requestResult); +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief updates an existing user +//////////////////////////////////////////////////////////////////////////////// + +exports.update = function (user, passwd, active, extra, changePassword) { + var db = internal.db; + + var uri = "_api/user/" + encodeURIComponent(user); + var data = {}; + + if (passwd !== undefined) { + data.passwd = passwd; + } + + if (active !== undefined) { + data.active = active; + } + + if (extra !== undefined) { + data.extra = extra; + } + + if (changePassword !== undefined) { + data.changePassword = changePassword; + } + + var requestResult = db._connection.PATCH(uri, JSON.stringify(data)); + return arangosh.checkRequestResult(requestResult); +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief deletes an existing user +//////////////////////////////////////////////////////////////////////////////// + +exports.remove = function (user) { + var db = internal.db; + + var uri = "_api/user/" + encodeURIComponent(user); + + var requestResult = db._connection.DELETE(uri); + return arangosh.checkRequestResult(requestResult); +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief gets an existing user +//////////////////////////////////////////////////////////////////////////////// + +exports.document = function (user) { + var db = internal.db; + + var uri = "_api/user/" + encodeURIComponent(user); + + var requestResult = db._connection.GET(uri); + return arangosh.checkRequestResult(requestResult); +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief checks whether a combination of username / password is valid. +//////////////////////////////////////////////////////////////////////////////// + +exports.isValid = function (user, password) { + var db = internal.db; + + var uri = "_api/user/" + encodeURIComponent(user); + var data = { passwd: password }; + + var requestResult = db._connection.POST(uri, JSON.stringify(data)); + + if (requestResult.error !== undefined && requestResult.error) { + if (requestResult.errorNum === arangodb.errors.ERROR_USER_NOT_FOUND.code) { + return false; + } + + return arangosh.checkRequestResult(requestResult); + } + + return requestResult.result; +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief gets all existing users +//////////////////////////////////////////////////////////////////////////////// + +exports.all = function () { + var db = internal.db; + + var uri = "_api/user"; + + var requestResult = db._connection.GET(uri); + return arangosh.checkRequestResult(requestResult).result; +}; + //////////////////////////////////////////////////////////////////////////////// /// @brief reloads the user authentication data //////////////////////////////////////////////////////////////////////////////// - + exports.reload = function () { var db = internal.db; @@ -63,16 +192,12 @@ exports.reload = function () { arangosh.checkRequestResult(requestResult); }; -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// - // ----------------------------------------------------------------------------- // --SECTION-- END-OF-FILE // ----------------------------------------------------------------------------- // Local Variables: // mode: outline-minor -// outline-regexp: "/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @}\\|/\\*jslint" +// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint" // End: diff --git a/js/common/bootstrap/errors.js b/js/common/bootstrap/errors.js index 256a82c7a9..d9cdca168b 100644 --- a/js/common/bootstrap/errors.js +++ b/js/common/bootstrap/errors.js @@ -172,6 +172,7 @@ "ERROR_USER_INVALID_PASSWORD" : { "code" : 1701, "message" : "invalid password" }, "ERROR_USER_DUPLICATE" : { "code" : 1702, "message" : "duplicate user" }, "ERROR_USER_NOT_FOUND" : { "code" : 1703, "message" : "user not found" }, + "ERROR_USER_CHANGE_PASSWORD" : { "code" : 1704, "message" : "user must change his password" }, "ERROR_APPLICATION_INVALID_NAME" : { "code" : 1750, "message" : "invalid application name" }, "ERROR_APPLICATION_INVALID_MOUNT" : { "code" : 1751, "message" : "invalid mount" }, "ERROR_APPLICATION_DOWNLOAD_FAILED" : { "code" : 1752, "message" : "application download failed" }, diff --git a/js/common/modules/org/arangodb/users-common.js b/js/common/modules/org/arangodb/users-common.js deleted file mode 100644 index 65ce0e5a54..0000000000 --- a/js/common/modules/org/arangodb/users-common.js +++ /dev/null @@ -1,534 +0,0 @@ -/*jslint indent: 2, nomen: true, maxlen: 100, sloppy: true, vars: true, white: true, plusplus: true */ -/*global require, exports */ - -//////////////////////////////////////////////////////////////////////////////// -/// @brief User management -/// -/// @file -/// -/// DISCLAIMER -/// -/// Copyright 2012 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, triAGENS GmbH, Cologne, Germany -//////////////////////////////////////////////////////////////////////////////// - -var internal = require("internal"); // OK: time -var arangodb = require("org/arangodb"); -var crypto = require("org/arangodb/crypto"); -var _ = require("underscore"); - -var db = arangodb.db; -var ArangoError = arangodb.ArangoError; - -// ----------------------------------------------------------------------------- -// --SECTION-- module "org/arangodb/users" -// ----------------------------------------------------------------------------- - -// ----------------------------------------------------------------------------- -// --SECTION-- private functions -// ----------------------------------------------------------------------------- - -//////////////////////////////////////////////////////////////////////////////// -/// @addtogroup ArangoShell -/// @{ -//////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////////// -/// @brief encode password using SHA256 -//////////////////////////////////////////////////////////////////////////////// - -var encodePassword = function (password) { - var salt; - var encoded; - - var random = crypto.rand(); - if (random === undefined) { - random = "time:" + internal.time(); - } - else { - random = "random:" + random; - } - - salt = crypto.sha256(random); - salt = salt.substr(0,8); - - encoded = "$1$" + salt + "$" + crypto.sha256(salt + password); - - return encoded; -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @brief validate 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 validate password -//////////////////////////////////////////////////////////////////////////////// - -var validatePassword = function (passwd) { - if (typeof passwd !== '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; - } -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @brief return 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; -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// - -// ----------------------------------------------------------------------------- -// --SECTION-- public functions -// ----------------------------------------------------------------------------- - -//////////////////////////////////////////////////////////////////////////////// -/// @addtogroup ArangoShell -/// @{ -//////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_saveUser -/// @brief create a new user -/// -/// @FUN{users.save(@FA{user}, @FA{passwd}, @FA{active}, @FA{extra})} -/// -/// This will create a new ArangoDB user. The username must be specified in -/// @FA{user} and must not be empty. -/// -/// The password must be given as a string, too, but can be left empty if -/// required. -/// -/// If the @FA{active} attribute is not specified, it defaults to @LIT{true}. -/// The @FA{extra} attribute can be used to save custom data with the user. -/// -/// This method will fail if either the username or the passwords are not -/// specified or given in a wrong format, or there already exists a user with -/// the specified name. -/// -/// The new user account can only be used after the server is either restarted -/// or the server authentication cache is @ref UserManagementReload "reloaded". -/// -/// Note: this function will not work from within the web interface -/// -/// @EXAMPLES -/// -/// @code -/// arangosh> require("org/arangodb/users").save("my-user", "my-secret-password"); -/// arangosh> require("org/arangodb/users").reload(); -/// @endcode -//////////////////////////////////////////////////////////////////////////////// - -exports.save = function (user, passwd, active, extra) { - if (passwd === null || passwd === undefined) { - passwd = ""; - } - - // validate input - validateName(user); - validatePassword(passwd); - - if (active === undefined || active === null) { - // this is the default value for active - active = true; - } - - var users = getStorage(); - var previous = users.firstExample({ user: user }); - - if (previous === null) { - var hash = encodePassword(passwd); - var data = { - user: user, - password: hash, - active: active - }; - - if (extra !== undefined) { - data.extra = extra; - } - - var doc = users.save(data); - // not exports.reload() as this is an abstract method... - require("org/arangodb/users").reload(); - return doc; - } - - var err = new ArangoError(); - err.errorNum = arangodb.errors.ERROR_USER_DUPLICATE.code; - err.errorMessage = arangodb.errors.ERROR_USER_DUPLICATE.message; - - throw err; -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_replaceUser -/// @brief replace an existing user -/// -/// @FUN{users.replace(@FA{user}, @FA{passwd}, @FA{active}, @FA{extra})} -/// -/// This will look up an existing ArangoDB user and replace its user data. -/// -/// The username must be specified in @FA{user}, and a user with the specified -/// name must already exist in the database. -/// -/// The password must be given as a string, too, but can be left empty if -/// required. -/// -/// If the @FA{active} attribute is not specified, it defaults to @LIT{true}. -/// The @FA{extra} attribute can be used to save custom data with the user. -/// -/// This method will fail if either the username or the passwords are not -/// specified or given in a wrong format, or if the specified user cannot be -/// found in the database. -/// -/// The replace is effective only after the server is either restarted -/// or the server authentication cache is reloaded (see @ref JSF_reloadUsers). -/// -/// Note: this function will not work from within the web interface -/// -/// @EXAMPLES -/// -/// @code -/// arangosh> require("org/arangodb/users").replace("my-user", "my-changed-password"); -/// arangosh> require("org/arangodb/users").reload(); -/// @endcode -//////////////////////////////////////////////////////////////////////////////// - -exports.replace = function (user, passwd, active, extra) { - if (passwd === null || passwd === undefined) { - passwd = ""; - } - - // validate input - validateName(user); - validatePassword(passwd); - - if (active === undefined || active === null) { - // this is the default - active = true; - } - - var users = getStorage(); - var previous = users.firstExample({ user: user }); - - if (previous === 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 hash = encodePassword(passwd); - var data = { - user: user, - password: hash, - active: active - }; - if (extra !== undefined) { - data.extra = extra; - } - - var doc = users.replace(previous, data); - - // not exports.reload() as this is an abstract method... - require("org/arangodb/users").reload(); - return doc; -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_updateUser -/// @brief update an existing user -/// -/// @FUN{@FA{users}.update(@FA{user}, @FA{passwd}, @FA{active}, @FA{extra})} -/// -/// This will update an existing ArangoDB user with a new password and other -/// data. -/// -/// The username must be specified in @FA{user} and the user must already exist -/// in the database. -/// -/// The password must be given as a string, too, but can be left empty if -/// required. -/// -/// If the @FA{active} attribute is not specified, the current value saved for -/// the user will not be changed. The same is true for the @FA{extra} attribute. -/// -/// This method will fail if either the username or the passwords are not -/// specified or given in a wrong format, or if the specified user cannot be -/// found in the database. -/// -/// The update is effective only after the server is either restarted -/// or the server authentication cache is reloaded (see @ref JSF_reloadUsers). -/// -/// Note: this function will not work from within the web interface -/// -/// @EXAMPLES -/// -/// @code -/// arangosh> require("org/arangodb/users").replace("my-user", "my-secret-password"); -/// arangosh> require("org/arangodb/users").reload(); -/// @endcode -//////////////////////////////////////////////////////////////////////////////// - -exports.update = function (user, passwd, active, extra) { - // validate input - validateName(user); - if (passwd !== undefined) { - validatePassword(passwd); - } - - var users = getStorage(); - var previous = users.firstExample({ user: user }); - - if (previous === 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 = previous._shallowCopy; - - if (passwd !== undefined) { - var hash = encodePassword(passwd); - data.password = hash; - } - if (active !== undefined && active !== null) { - data.active = active; - } - if (extra !== undefined) { - data.extra = extra; - } - - var doc = users.update(previous, data); - - // not exports.reload() as this is an abstract method... - require("org/arangodb/users").reload(); - - return doc; -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_removeUser -/// @brief delete an existing user -/// -/// @FUN{users.remove(@FA{user})} -/// -/// Removes an existing ArangoDB user from the database. -/// -/// The username must be specified in @FA{user} and the specified user must -/// exist in the database. -/// -/// This method will fail if the user cannot be found in the database. -/// -/// The deletion is effective only after the server is either restarted -/// or the server authentication cache is @ref UserManagementReload "reloaded". -/// -/// Note: this function will not work from within the web interface -/// -/// @EXAMPLES -/// -/// @code -/// arangosh> require("org/arangodb/users").remove("my-user"); -/// arangosh> require("org/arangodb/users").reload(); -/// @endcode -//////////////////////////////////////////////////////////////////////////////// - -exports.remove = function (user) { - // validate input - validateName(user); - - var users = getStorage(); - var previous = users.firstExample({ user: user }); - - if (previous === 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 doc = users.remove(previous); - - // not exports.reload() as this is an abstract method... - require("org/arangodb/users").reload(); - - return doc; -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_documentUser -/// @brief get an existing user -/// -/// @FUN{users.document(@FA{user})} -/// -/// Fetches an existing ArangoDB user from the database. -/// -/// The username must be specified in @FA{user}. -/// This method will fail if the user cannot be found in the database. -/// -/// Note: this function will not work from within the web interface -//////////////////////////////////////////////////////////////////////////////// - -exports.document = function (user) { - // validate name - validateName(user); - - var users = getStorage(); - var previous = users.firstExample({ user: user }); - - if (previous === 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 { - user: previous.user, - active: previous.active, - extra: previous.extra || { } - }; -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_allUser -/// @brief gets all existing users -/// -/// @FUN{users.all()} -/// -/// Fetches all existing ArangoDB users from the database. -//////////////////////////////////////////////////////////////////////////////// - -exports.all = function () { - var cursor = getStorage().all(); - var result = [ ]; - - while (cursor.hasNext()) { - var doc = cursor.next(); - var user = { - user: doc.user, - active: doc.active, - extra: doc.extra || { } - }; - result.push(user); - } - - return result; -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_isValidUsers -/// @brief checks whether a combination of username / password is valid. -/// -/// @FUN{users.isvalid(@FA{user}, @FA{password})} -/// -/// Checks whether the given combination of username and password is valid. -/// The function will return a boolean value if the combination of username -/// and password is valid. -/// -/// Each call to this function is penalized by the server sleeping a random -/// amount of time. -/// -/// Note: this function will not work from within the web interface -//////////////////////////////////////////////////////////////////////////////// - -exports.isValid = function (user, password) { - var users = getStorage(); - var previous = users.firstExample({ user: user }); - - if (previous === null || ! previous.active) { - return false; - } - - var salted = previous.password.substr(3, 8) + password; - var hex = crypto.sha256(salted); - - // penalize the call - internal.sleep(Math.random()); - - return (previous.password.substr(12) === hex); -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @fn JSF_reloadUsers -/// @brief reloads the user authentication data -/// -/// @FUN{users.reload()} -/// -/// Reloads the user authentication data on the server -/// -/// All user authentication data is loaded by the server once on startup only -/// and is cached after that. When users get added or deleted, a cache flush is -/// required, and this can be performed by called this method. -/// -/// Note: this function will not work from within the web interface -//////////////////////////////////////////////////////////////////////////////// - -exports.reload = function () { - throw "cannot use abstract reload function"; -}; - -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// - -// ----------------------------------------------------------------------------- -// --SECTION-- END-OF-FILE -// ----------------------------------------------------------------------------- - -// Local Variables: -// mode: outline-minor -// outline-regexp: "/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @}\\|/\\*jslint" -// End: - diff --git a/js/server/modules/org/arangodb/users.js b/js/server/modules/org/arangodb/users.js index 2384e2ca9c..ea2fe7b4a5 100644 --- a/js/server/modules/org/arangodb/users.js +++ b/js/server/modules/org/arangodb/users.js @@ -1,4 +1,4 @@ -/*jslint indent: 2, nomen: true, maxlen: 100, sloppy: true, vars: true, white: true, plusplus: true */ +/*jslint indent: 2, nomen: true, maxlen: 120, sloppy: true, vars: true, white: true, plusplus: true */ /*global require, exports, ArangoAgency */ //////////////////////////////////////////////////////////////////////////////// @@ -8,7 +8,7 @@ /// /// DISCLAIMER /// -/// Copyright 2012 triagens GmbH, Cologne, Germany +/// 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. @@ -25,37 +25,355 @@ /// Copyright holder is triAGENS GmbH, Cologne, Germany /// /// @author Jan Steemann -/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany +/// @author Copyright 2012-2014, triAGENS GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// var internal = require("internal"); // OK: reloadAuth -var base = require("org/arangodb/users-common"); +var arangodb = require("org/arangodb"); +var crypto = require("org/arangodb/crypto"); -// copy exports from base -var i; -for (i in base) { - if (base.hasOwnProperty(i)) { - exports[i] = base[i]; - } -} +var db = arangodb.db; +var ArangoError = arangodb.ArangoError; // ----------------------------------------------------------------------------- // --SECTION-- module "org/arangodb/users" // ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// --SECTION-- private functions +// ----------------------------------------------------------------------------- + +//////////////////////////////////////////////////////////////////////////////// +/// @brief encode password using SHA256 +//////////////////////////////////////////////////////////////////////////////// + +var encodePassword = function (password) { + var salt; + var encoded; + + var random = crypto.rand(); + if (random === undefined) { + random = "time:" + internal.time(); + } + else { + random = "random:" + random; + } + + salt = crypto.sha256(random); + salt = salt.substr(0,8); + + encoded = "$1$" + salt + "$" + crypto.sha256(salt + password); + + return encoded; +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @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 (passwd) { + if (typeof passwd !== '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; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @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; +}; + // ----------------------------------------------------------------------------- // --SECTION-- public functions // ----------------------------------------------------------------------------- //////////////////////////////////////////////////////////////////////////////// -/// @addtogroup ArangoShell -/// @{ +/// @brief creates a new user //////////////////////////////////////////////////////////////////////////////// +exports.save = function (user, passwd, active, extra, changePassword) { + if (passwd === null || passwd === undefined) { + passwd = ""; + } + + // validate input + validateName(user); + validatePassword(passwd); + + if (active === undefined || active === null) { + active = true; // this is the default value + } + + if (active === undefined || active === null) { + active = true; // this is the default value + } + + var users = getStorage(); + var previous = users.firstExample({ user: user }); + + if (previous === null) { + var hash = encodePassword(passwd); + var data = { + user: user, + password: hash, + active: active, + changePassword: changePassword + }; + + if (extra !== undefined) { + data.extra = extra; + } + + var doc = users.save(data); + + // not exports.reload() as this is an abstract method... + require("org/arangodb/users").reload(); + return users.document(doc._id); + } + + var err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_USER_DUPLICATE.code; + err.errorMessage = arangodb.errors.ERROR_USER_DUPLICATE.message; + + throw err; +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief replaces an existing user +//////////////////////////////////////////////////////////////////////////////// + +exports.replace = function (user, passwd, active, extra, changePassword) { + if (passwd === null || passwd === undefined) { + passwd = ""; + } + + // validate input + validateName(user); + validatePassword(passwd); + + 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 previous = users.firstExample({ user: user }); + + if (previous === 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 hash = encodePassword(passwd); + var data = { + user: user, + password: hash, + active: active, + changePassword: changePassword + }; + + if (extra !== undefined) { + data.extra = extra; + } + + users.replace(previous, data); + + // not exports.reload() as this is an abstract method... + require("org/arangodb/users").reload(); + + return users.document(previous._id); +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief updates an existing user +//////////////////////////////////////////////////////////////////////////////// + +exports.update = function (user, passwd, active, extra, changePassword) { + + // validate input + validateName(user); + + if (passwd !== undefined) { + validatePassword(passwd); + } + + var users = getStorage(); + var previous = users.firstExample({ user: user }); + + if (previous === 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 = previous._shallowCopy; + + if (passwd !== undefined) { + var hash = encodePassword(passwd); + data.password = hash; + } + + if (active !== undefined && active !== null) { + data.active = active; + } + + if (extra !== undefined) { + data.extra = extra; + } + + if (changePassword !== undefined && changePassword !== null) { + data.changePassword = changePassword; + } + + users.update(previous, data); + + // not exports.reload() as this is an abstract method... + require("org/arangodb/users").reload(); + + return users.document(previous._id); +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief deletes an existing user +//////////////////////////////////////////////////////////////////////////////// + +exports.remove = function (user) { + // validate input + validateName(user); + + var users = getStorage(); + var previous = users.firstExample({ user: user }); + + if (previous === 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 doc = users.remove(previous); + + // not exports.reload() as this is an abstract method... + require("org/arangodb/users").reload(); + + return doc; +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief gets an existing user +//////////////////////////////////////////////////////////////////////////////// + +exports.document = function (user) { + + // validate name + validateName(user); + + var users = getStorage(); + var doc = users.firstExample({ user: user }); + + if (doc === 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 { + user: doc.user, + active: doc.active, + extra: doc.extra || {}, + changePassword: doc.changePassword + }; +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief checks whether a combination of username / password is valid. +//////////////////////////////////////////////////////////////////////////////// + +exports.isValid = function (user, password) { + var users = getStorage(); + var previous = users.firstExample({ user: user }); + + if (previous === null || ! previous.active) { + return false; + } + + var salted = previous.password.substr(3, 8) + password; + var hex = crypto.sha256(salted); + + // penalize the call + internal.sleep(Math.random()); + + return (previous.password.substr(12) === hex); +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief gets all existing users +//////////////////////////////////////////////////////////////////////////////// + +exports.all = function () { + var cursor = getStorage().all(); + var result = [ ]; + + while (cursor.hasNext()) { + var doc = cursor.next(); + var user = { + user: doc.user, + active: doc.active, + extra: doc.extra || { }, + changePassword: doc.changePassword + }; + + result.push(user); + } + + return result; +}; + //////////////////////////////////////////////////////////////////////////////// /// @brief reloads the user authentication data //////////////////////////////////////////////////////////////////////////////// - + exports.reload = function () { internal.reloadAuth(); if (require("org/arangodb/cluster").isCoordinator()) { @@ -83,16 +401,12 @@ exports.reload = function () { } }; -//////////////////////////////////////////////////////////////////////////////// -/// @} -//////////////////////////////////////////////////////////////////////////////// - // ----------------------------------------------------------------------------- // --SECTION-- END-OF-FILE // ----------------------------------------------------------------------------- // Local Variables: // mode: outline-minor -// outline-regexp: "/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @}\\|/\\*jslint" +// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint" // End: diff --git a/lib/BasicsC/errors.dat b/lib/BasicsC/errors.dat index 1e68b8275d..5215282920 100755 --- a/lib/BasicsC/errors.dat +++ b/lib/BasicsC/errors.dat @@ -228,10 +228,11 @@ ERROR_TRANSACTION_DISALLOWED_OPERATION,1653,"disallowed operation inside transac ## User management ################################################################################ -ERROR_USER_INVALID_NAME,1700,"invalid user name","Will be raised when an invalid user name is used" -ERROR_USER_INVALID_PASSWORD,1701,"invalid password","Will be raised when an invalid password is used" -ERROR_USER_DUPLICATE,1702,"duplicate user","Will be raised when a user name already exists" -ERROR_USER_NOT_FOUND,1703,"user not found","Will be raised when a user name is updated that does not exist" +ERROR_USER_INVALID_NAME,1700,"invalid user name","Will be raised when an invalid user name is used." +ERROR_USER_INVALID_PASSWORD,1701,"invalid password","Will be raised when an invalid password is used." +ERROR_USER_DUPLICATE,1702,"duplicate user","Will be raised when a user name already exists." +ERROR_USER_NOT_FOUND,1703,"user not found","Will be raised when a user name is updated that does not exist." +ERROR_USER_CHANGE_PASSWORD,1704,"user must change his password","Will be raised when the user must change his password." ################################################################################ ## Application management diff --git a/lib/BasicsC/voc-errors.c b/lib/BasicsC/voc-errors.c index 8c4318fec4..81b5f12360 100644 --- a/lib/BasicsC/voc-errors.c +++ b/lib/BasicsC/voc-errors.c @@ -168,6 +168,7 @@ void TRI_InitialiseErrorMessages (void) { REG_ERROR(ERROR_USER_INVALID_PASSWORD, "invalid password"); REG_ERROR(ERROR_USER_DUPLICATE, "duplicate user"); REG_ERROR(ERROR_USER_NOT_FOUND, "user not found"); + REG_ERROR(ERROR_USER_CHANGE_PASSWORD, "user must change his password"); REG_ERROR(ERROR_APPLICATION_INVALID_NAME, "invalid application name"); REG_ERROR(ERROR_APPLICATION_INVALID_MOUNT, "invalid mount"); REG_ERROR(ERROR_APPLICATION_DOWNLOAD_FAILED, "application download failed"); diff --git a/lib/BasicsC/voc-errors.h b/lib/BasicsC/voc-errors.h index 07acf49742..d3625f2883 100644 --- a/lib/BasicsC/voc-errors.h +++ b/lib/BasicsC/voc-errors.h @@ -393,13 +393,15 @@ extern "C" { /// Will be raised when a disallowed operation is carried out in a /// transaction. /// - 1700: @LIT{invalid user name} -/// Will be raised when an invalid user name is used +/// Will be raised when an invalid user name is used. /// - 1701: @LIT{invalid password} -/// Will be raised when an invalid password is used +/// Will be raised when an invalid password is used. /// - 1702: @LIT{duplicate user} -/// Will be raised when a user name already exists +/// Will be raised when a user name already exists. /// - 1703: @LIT{user not found} -/// Will be raised when a user name is updated that does not exist +/// Will be raised when a user name is updated that does not exist. +/// - 1704: @LIT{user must change his password} +/// Will be raised when the user must change his password. /// - 1750: @LIT{invalid application name} /// Will be raised when an invalid application name is specified. /// - 1751: @LIT{invalid mount} @@ -2102,7 +2104,7 @@ void TRI_InitialiseErrorMessages (void); /// /// invalid user name /// -/// Will be raised when an invalid user name is used +/// Will be raised when an invalid user name is used. //////////////////////////////////////////////////////////////////////////////// #define TRI_ERROR_USER_INVALID_NAME (1700) @@ -2112,7 +2114,7 @@ void TRI_InitialiseErrorMessages (void); /// /// invalid password /// -/// Will be raised when an invalid password is used +/// Will be raised when an invalid password is used. //////////////////////////////////////////////////////////////////////////////// #define TRI_ERROR_USER_INVALID_PASSWORD (1701) @@ -2122,7 +2124,7 @@ void TRI_InitialiseErrorMessages (void); /// /// duplicate user /// -/// Will be raised when a user name already exists +/// Will be raised when a user name already exists. //////////////////////////////////////////////////////////////////////////////// #define TRI_ERROR_USER_DUPLICATE (1702) @@ -2132,11 +2134,21 @@ void TRI_InitialiseErrorMessages (void); /// /// user not found /// -/// Will be raised when a user name is updated that does not exist +/// Will be raised when a user name is updated that does not exist. //////////////////////////////////////////////////////////////////////////////// #define TRI_ERROR_USER_NOT_FOUND (1703) +//////////////////////////////////////////////////////////////////////////////// +/// @brief 1704: ERROR_USER_CHANGE_PASSWORD +/// +/// user must change his password +/// +/// Will be raised when the user must change his password. +//////////////////////////////////////////////////////////////////////////////// + +#define TRI_ERROR_USER_CHANGE_PASSWORD (1704) + //////////////////////////////////////////////////////////////////////////////// /// @brief 1750: ERROR_APPLICATION_INVALID_NAME /// diff --git a/lib/HttpServer/HttpCommTask.h b/lib/HttpServer/HttpCommTask.h index f75f26c650..180746dbc3 100644 --- a/lib/HttpServer/HttpCommTask.h +++ b/lib/HttpServer/HttpCommTask.h @@ -592,7 +592,7 @@ namespace triagens { // not found else if (authResult == HttpResponse::NOT_FOUND) { - HttpResponse response(HttpResponse::NOT_FOUND); + HttpResponse response(authResult); response.setContentType("application/json; charset=utf-8"); response.body().appendText("{\"error\":true,\"errorMessage\":\"") @@ -607,6 +607,21 @@ namespace triagens { this->resetState(); } + // forbidden + else if (authResult == HttpResponse::FORBIDDEN) { + HttpResponse response(authResult); + response.setContentType("application/json; charset=utf-8"); + + response.body().appendText("{\"error\":true,\"errorMessage\":\"change password\",\"code\":") + .appendInteger((int) authResult) + .appendText(",\"errorNum\":") + .appendInteger(TRI_ERROR_USER_CHANGE_PASSWORD) + .appendText("}"); + + this->handleResponse(&response); + this->resetState(); + } + // not authenticated else { const string realm = "basic realm=\"" + this->_server->getHandlerFactory()->authenticationRealm(this->_request) + "\"";