diff --git a/Documentation/Books/Users/Foxx/FoxxSessions.mdpp b/Documentation/Books/Users/Foxx/FoxxSessions.mdpp
new file mode 100644
index 0000000000..e867e28e27
--- /dev/null
+++ b/Documentation/Books/Users/Foxx/FoxxSessions.mdpp
@@ -0,0 +1,54 @@
+!CHAPTER Foxx Sessions
+
+Foxx provides some convenience methods to make working with sessions easier.
+
+!SUBSECTION Activate sessions
+
+Enables session features for the controller.
+
+`controller.activateSessions(options)`
+
+Once sessions have been activated, a *session* property will be added to the *request* object passed to route handlers defined on the controller, which will be a saved instance of the session model provided by the session storage.
+
+If the option *autoCreateSession* has not explicitly been set to *false*, a new session will be created for users that do not yet have an active session.
+
+If *type* is set to *"cookie"*, the session cookie will be updated after every route.
+
+*Parameter*
+
+* *options* (optional): an object with any of the following properties:
+ * *sessionStorageApp* (optional): mount point of the session storage app to use. Default: *"/_system/sessions"*.
+ * *type* (optional): sessions type, currently only *"cookie"* is supported. Default: *"cookie"*.
+ * *cookieName* (optional): name of the session cookie if using cookie sessions. If a *cookieSecret* is provided, the signature will be stored in a cookie named *cookieName + "_sig"*. Defaults to *"sid"*.
+ * *cookieSecret* (optional): secret string to sign session cookies with if using cookie sessions.
+ * *autoCreateSession* (optional): whether a session should always be created if none exists. Default: *true*.
+
+@EXAMPLES
+
+```js
+var controller = new FoxxController(applicationContext);
+controller.activateSessions({
+ sessionStorageApp: '/_system/sessions',
+ cookieName: 'sid',
+ cookieSecret: 'secret',
+ type: 'cookie'
+});
+```
+
+!SUBSECTION Define a session destruction route
+
+Defines a route that will destroy the session.
+
+`controller.destroySession(path, options)`
+
+Defines a route handler on the controller that destroys the session.
+
+When using cookie sessions, this function will clear the session cookie (if *autoCreateSession* was disabled) or create a new session cookie, before calling the *after* function.
+
+*Parameter*
+
+* *path*: route path as passed to *controller.get*, *controller.post*, etc.
+* *options* (optional): an object with any of the following properties:
+ * *method* (optional): HTTP method to handle. Default: *"post"*.
+ * *before* (optional): function to execute before the session is destroyed. Receives the same arguments as a regular route handler.
+ * *after* (optional): function to execute after the session is destroyed. Receives the same arguments as a regular route handler. Default: a function that sends a *{"message": "logged out"}* JSON response.
diff --git a/Documentation/Books/Users/FoxxBundledApps/README.mdpp b/Documentation/Books/Users/FoxxBundledApps/README.mdpp
new file mode 100644
index 0000000000..16f20ff67d
--- /dev/null
+++ b/Documentation/Books/Users/FoxxBundledApps/README.mdpp
@@ -0,0 +1,3 @@
+!CHAPTER Bundled Foxx Applications
+
+ArangoDB ships with a number of Foxx apps that allow you to implement basic authentication and user management with minimal effort.
\ No newline at end of file
diff --git a/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp
new file mode 100644
index 0000000000..88b014f048
--- /dev/null
+++ b/Documentation/Books/Users/FoxxBundledApps/Sessions.mdpp
@@ -0,0 +1,273 @@
+!CHAPTER The Sessions App
+
+The sessions app provides a session storage JavaScript API that can be used in other Foxx apps.
+
+!SECTION Configuration
+
+This app has the following configuration options:
+
+* *timeToLive* (optional): number of milliseconds until the session expires. Default: *604800000* (one week).
+* *ttlType* (optional): attribute against which the *timeToLive* is enforced. Valid options: *lastAccess*, *lastUpdate*, *created*. Default: *"created"*.
+* *sidTimestamp* (optional): whether to append a timestamp to the random part of generated session IDs. Default: *false*.
+* *sidLength* (optional): number of random characters to use for new session IDs. Default *20*.
+
+!SECTION JavaScript API: *sessionStorage*
+
+This app exposes a session storage via a JavaScript API named *sessionStorage*.
+
+@EXAMPLES
+
+```js
+var sessionStorage = Foxx.requireApp('/_system/sessions').sessionStorage;
+```
+
+!SUBSECTION Exceptions
+
+!SUBSUBSECTION Session Not Found
+
+Indicates a session could not be found in the database.
+
+`new sessionStorage.errors.SessionNotFound(sessionId)`
+
+Thrown by the session storage's *delete* and *get* methods if passed a session ID that does not exist in the database.
+
+@EXAMPLES
+
+```js
+try {
+ sessionStorage.get(invalidSessionId);
+} catch(err) {
+ assertTrue(err instanceof sessionStorage.errors.SessionNotFound);
+}
+```
+
+!SUBSUBSECTION Session Expired
+
+Indicates the session exists in the database but has expired.
+
+`new sessionStorage.errors.SessionExpired(sessionId)`
+
+Thrown by the session storage's *get* method if passed a session ID for a session that has expired. See also this app's configuration options.
+
+@EXAMPLES
+
+```js
+try {
+ sessionStorage.get(expiredSessionId);
+} catch(err) {
+ assertTrue(err instanceof sessionStorage.errors.SessionExpired);
+ assertTrue(err instanceof sessionStorage.errors.SessionNotFound);
+}
+```
+
+!SUBSECTION The session object
+
+Session objects are instances of a Foxx model with the following attributes:
+
+* *sessionData*: volatile session data. This can be an arbitrary object that will be stored with the session in the database. If you want to store session-specific (rather than user-specific) data in the database, this is the right place for that.
+* *uid*: the session's active user's *_key* or *undefined* (no active user).
+* *userData*: the session's active user's *userData* attribute or an empty object.
+* *created*: timestamp the session was created at.
+* *lastAccess*: timestamp of the last time the session was fetched from the database.
+* *lastUpdate*: timestamp of the last time the session was written to the database.
+
+!SUBSECTION Create a session
+
+Creates and saves a new instance of the session model.
+
+`sessionStorage.create(sessionData)`
+
+*Parameter*
+
+* *sessionData* (optional): an arbitrary object that will be stored as the session's *sessionData* attribute when the model is saved to the database.
+
+@EXAMPLES
+
+```js
+var session = sessionStorage.create(sessionData);
+assertEqual(session.get('sessionData'), sessionData);
+```
+
+!SUBSECTION Fetch an existing session
+
+There are two ways to fetch a session via the session storage API:
+
+* resolving a session cookie with the *fromCookie* method
+* calling the session storage's *get* method with a session ID directly
+
+!SUBSUBSECTION Resolve a session cookie
+
+Fetch a session matching a cookie in a Foxx request.
+
+`sessionStorage.fromCookie(request, cookieName, secret)`
+
+Parses a request's cookies and returns the matching instance of the session model.
+
+The method will return *null* instead of a session object in the following cases:
+
+* the request has no session cookie
+* the request's session cookie does not match a known session ID
+* the matching session has expired
+* the cookie's signature is missing (if a *secret* is provided)
+* the cookie's signature does not match (if a *secret* is provided)
+
+*Parameter*
+
+* *request*: a Foxx request object as passed to controller routes.
+* *cookieName*: name of the cookie to parse.
+* *secret* (optional): secret string to validate the cookie's signature with.
+
+@EXAMPLES
+
+```js
+controller.get('/hello', function(request, response) {
+ var session = sessionStorage.fromCookie(request, cookieName, secret);
+ response.json(session.get('sessionData'));
+});
+```
+
+!SUBSUBSECTION Resolve a session ID directly
+
+Fetch a session from the database for a given ID.
+
+`sessionStorage.get(sessionId)`
+
+Attempts to load the session with the given session ID from the database. If the session does not exist, a *SessionNotFound* exception will be thrown. If the session does exist, but has already expired, a *SessionExpired* exception will be thrown instead.
+
+*Parameter*
+
+* *sessionId*: a session *_key*.
+
+@EXAMPLES
+
+```js
+var session = sessionStorage.get(sessionId);
+```
+
+!SUBSECTION Delete a session
+
+There are two ways to delete a session from the database:
+
+* calling the session storage's *delete* method with a session ID directly
+* telling a session to delete itself
+
+!SUBSUBSECTION Delete a session by its ID
+
+Delete a session with a given ID.
+
+`sessionStorage.delete(sessionId)`
+
+Attempts to delete the session with the given session ID from the database. If the session does not exist, a *SessionNotFound* exception will be thrown. The method always returns *null*.
+
+*Parameter*
+
+* *sessionId*: a session *_key*.
+
+@EXAMPLES
+
+```js
+sessionStorage.delete(sessionId);
+```
+
+!SUBSUBSECTION Tell a session to delete itself
+
+Delete a session from the database.
+
+`session.delete()`
+
+Attempts to delete the session from the database.
+
+Returns *true* if the session was deleted successfully.
+
+Returns *false* if the session already didn't exist.
+
+@EXAMPLES
+
+```js
+session.delete();
+```
+
+!SUBSECTION Save a session
+
+Save a session to the database.
+
+`session.save()`
+
+If you made any changes to the session and are not using the sessions app via Foxx Authentication, you must call this method to commit the changes to the database.
+
+@EXAMPLES
+
+```js
+session.setUser(user);
+session.save();
+```
+
+!SUBSECTION Set a session's active user
+
+Set the active user of a session.
+
+`session.setUser(user)`
+
+Expects a Foxx model with a *userData* attribute and sets the session's *uid* attribute to the model's *_key* and the session's *userData* attribute to the model's *userData* attribute.
+
+*Parameter*
+
+* *user*: instance of a Foxx model with a *`userData* attribute.
+
+@EXAMPLES
+
+```js
+session.setUser(user);
+assertEqual(session.get('uid'), user.get('_key'));
+assertEqual(session.get('userData'), user.get('userData'));
+```
+
+!SUBSECTION Add a session cookie to a response
+
+Add a session cookie to a Foxx response.
+
+`session.addCookie(response, cookieName, secret)`
+
+Adds a session cookie to the response.
+
+If a *secret* string is provided, the cookie is signed using that secret (a second cookie with the name *cookieName + '_sig'* containing the cryptographic signature of the cookie value is added to the response).
+
+If you want to use signed cookies, you must make sure to pass the same *secret* to the *fromCookie* method when fetching the session from a cookie later.
+
+*Parameter*
+
+* *response*: a Foxx response object as passed to controller routes.
+* *cookieName*: name of the cookie to parse.
+* *secret* (optional): secret string to sign the cookie with.
+
+@EXAMPLES
+
+```js
+controller.get('/hello', function(request, response) {
+ session.addCookie(response, cookieName, secret);
+});
+```
+
+!SUBSECTION Clear a session cookie
+
+Clear the session cookie of a Foxx response.
+
+`session.clearCookie(response, cookieName, secret)`
+
+Adds a blank expired cookie to clear the user's previously set session cookie.
+
+If the method is passed a *secret* string, a second blank expired cookie is added that overwrites the signature cookie (see above).
+
+*Parameter*
+
+* *response*: a Foxx response object as passed to controller routes.
+* *cookieName*: name of the cookie to parse.
+* *secret* (optional): indicates the signature should be cleared also.
+
+@EXAMPLES
+
+```js
+controller.get('/goodbye', function(request, response) {
+ session.clearCookie(response, cookieName, secret);
+});
+```
\ No newline at end of file
diff --git a/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp
new file mode 100644
index 0000000000..0191ac2b58
--- /dev/null
+++ b/Documentation/Books/Users/FoxxBundledApps/SimpleAuth.mdpp
@@ -0,0 +1,49 @@
+!CHAPTER The Simple Authentication App
+
+The simple auth app provides hashed password-based authentication with automatically generated salts and constant-time password verification.
+
+!SECTION Configuration
+
+This app has the following configuration options:
+
+* *saltLength* (optional): length of newly generated salts. Default: *16*.
+* *hashMethod* (optional): hash algorithm to use. Supported values: *sha1*, *sha224*, *sha256*, *md5*. Default: *"sha256"*.
+
+!SECTION JavaScript API: *auth*
+
+This app exposes its functionality via a JavaScript API named *auth*.
+
+@EXAMPLES
+
+```js
+var auth = Foxx.requireApp('/_system/simple-auth').auth;
+```
+
+!SUBSECTION Generate an authentication object
+
+Generates an authentication object for a given password.
+
+`auth.hashPassword(password)`
+
+Returns an authentication object with the following properties:
+
+* *hash*: the generated hex-encoded hash.
+* *salt*: the salt used to generate the hash.
+* *method*: the algorithm used to generate the hash.
+
+*Parameter*
+
+* *password*: the password to hash.
+
+!SUBSECTION Verify a password
+
+Verifies a password against a given authentication object.
+
+`auth.verifyPassword(authData, password)`
+
+Generates a hash for the given password using the *salt* and *method* stored in the authentication object and performs a constant time string comparison on them. Returns *true* if the password is valid or *false* otherwise.
+
+*Parameter*
+
+* *authData*: an authentication object.
+* *password*: a password to verify.
\ No newline at end of file
diff --git a/Documentation/Books/Users/FoxxBundledApps/Users.mdpp b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp
new file mode 100644
index 0000000000..bda7a3c44a
--- /dev/null
+++ b/Documentation/Books/Users/FoxxBundledApps/Users.mdpp
@@ -0,0 +1,184 @@
+!CHAPTER The Users App
+
+The users app provides a username-based user storage JavaScript API that can be used in other Foxx apps.
+
+!SECTION JavaScript API: *userStorage*
+
+This app exposes a user storage via a JavaScript API named *userStorage*.
+
+@EXAMPLES
+
+```js
+var userStorage = Foxx.requireApp('/_system/users').userStorage;
+```
+
+!SUBSECTION Exceptions
+
+!SUBSUBSECTION User Not Found
+
+Indicates a user could not be found in the database.
+
+`new userStorage.errors.UserNotFound(userId)`
+
+Thrown by the user storage's *delete* and *get* methods if passed a user ID that does not exist in the database.
+
+@EXAMPLES
+
+```js
+try {
+ userStorage.get(invalidUserId);
+} catch(err) {
+ assertTrue(err instanceof userStorage.errors.UserNotFound);
+}
+```
+
+!SUBSUBSECTION Username Not Available
+
+Indicates a username is already in use.
+
+`new userStorage.errors.UsernameNotAvailable(username)`
+
+Thrown by the user storage's *create* method if passed a *userData* object with a *username* that is already in use.
+
+@EXAMPLES
+
+```js
+try {
+ userStorage.create('alreadyTaken', {some: 'data'});
+} catch(err) {
+ assertTrue(err instanceof userStorage.errors.UsernameNotAvailable);
+}
+```
+
+!SUBSECTION The user object
+
+User objects are instances of a Foxx model with the following attributes:
+
+* *user*: the user's unique *username*.
+* *userData*: application-specific user data.
+* *authData*: an arbitrary object used by authentication apps to store sensitive data. For password authentication this could be a hash, for third-party authentication services this could be information about the user's identity. This attribute should never be exposed to the user directly.
+
+!SUBSECTION Create a user
+
+Creates and saves a new instance of the user model.
+
+`userStorage.create(username, userData)`
+
+Throws *UsernameNotAvailable* if a user with the given username already exists.
+
+*Parameter*
+
+* *username*: an arbitrary string that will be used as the user's username
+* *userData*: an arbitrary object that will be stored as the user's *userData* attribute when the model is saved to the database.
+
+@EXAMPLES
+
+```js
+var user = userStorage.create('malaclypse', {hair: 'fuzzy'});
+assertEqual(user.get('userData').hair, 'fuzzy');
+```
+
+!SUBSECTION Fetch an existing user
+
+There are two ways to fetch a user via the user storage API:
+
+* resolving a *username* with the user storage's *resolve* method
+* calling the user storage's *get* method with a user ID directly
+
+!SUBSUBSECTION Resolve a *username*
+
+Fetches a user with a given *username*.
+
+`userStorage.resolve(username)`
+
+If the username can not be resolved, a *UserNotFound* exception will be thrown instead.
+
+*Parameter*
+
+* *username*: an arbitrary string matching the username of the user you are trying to fetch.
+
+@EXAMPLES
+
+```js
+var user = userStorage.resolve('malaclypse');
+assertEqual(user.user, 'malaclypse');
+```
+
+!SUBSUBSECTION Resolve a user ID directly
+
+Fetches a user with a given ID.
+
+`userStorage.get(userId)`
+
+Attempts to fetch the user with the given ID from the database. If the user does not exist, a *UserNotFound* exception will be thrown instead.
+
+*Parameter*
+
+* *userId*: a user *_key*.
+
+@EXAMPLES
+
+```js
+var user = userStorage.get(userId);
+assertEqual(user.get('_key'), userId);
+```
+
+!SUBSECTION Delete a user
+
+There are two ways to delete a user from the database:
+
+* calling the user storage's *delete* method with a user ID directly
+* telling a user to delete itself
+
+!SUBSUBSECTION Delete a user by its ID
+
+Delete a user with a given ID.
+
+`userStorage.delete(userId)`
+
+Attempts to delete the user with the given user ID from the database. If the user does not exist, a *UserNotFound* exception will be thrown. The method always returns *null*.
+
+*Parameter*
+
+* *userId*: a user *_key*.
+
+@EXAMPLES
+
+```js
+userStorage.delete(userId);
+```
+
+!SUBSUBSECTION Tell a user to delete itself
+
+Delete a user from the database.
+
+`user.delete()`
+
+Attempts to delete the user from the database.
+
+Returns *true* if the user was deleted successfully.
+
+Returns *false* if the user already didn't exist.
+
+@EXAMPLES
+
+```js
+var user = userStorage.get(userId);
+user.delete();
+```
+
+!SUBSECTION Save a user
+
+Save a user to the database.
+
+`user.save()`
+
+In order to commit changes made to the user in your code, you need to call this method.
+
+@EXAMPLES
+
+```js
+var userData = user.get('userData');
+userData.counter++;
+user.save();
+```
diff --git a/Documentation/Books/Users/Installing/Linux.mdpp b/Documentation/Books/Users/Installing/Linux.mdpp
index a5fd65236b..5094834664 100644
--- a/Documentation/Books/Users/Installing/Linux.mdpp
+++ b/Documentation/Books/Users/Installing/Linux.mdpp
@@ -32,7 +32,7 @@ provided by @mgiken.
!SUBSECTION Debian sid
To use ArangoDB on Debian sid (the development version of Debian), a different version
-of ICU is required. User basir provided the following instructions for getting ArangoDB 2.0.7
+of ICU is required. User baslr provided the following instructions for getting ArangoDB 2.0.7
to work on an x86_64:
[link to Github issue](https://github.com/triAGENS/ArangoDB/issues/865)
diff --git a/Documentation/Books/Users/SUMMARY.md b/Documentation/Books/Users/SUMMARY.md
index 87376280d2..1823b29570 100644
--- a/Documentation/Books/Users/SUMMARY.md
+++ b/Documentation/Books/Users/SUMMARY.md
@@ -96,7 +96,12 @@
* [Dependency Injection](Foxx/FoxxInjection.md)
* [Foxx Exports](Foxx/FoxxExports.md)
* [Foxx Job Queues](Foxx/FoxxQueues.md)
+ * [Foxx Sessions](Foxx/FoxxSessions.md)
* [Optional Functionality](Foxx/FoxxOptional.md)
+ * [Bundled Applications](FoxxBundledApps/README.md)
+ * [Session Storage](FoxxBundledApps/Sessions.md)
+ * [User Storage](FoxxBundledApps/Users.md)
+ * [Simple Authentication](FoxxBundledApps/SimpleAuth.md)
* [Foxx Manager](FoxxManager/README.md)
* [First Steps](FoxxManager/FirstSteps.md)
@@ -115,6 +120,7 @@
* [Example Setup](Replication/ExampleSetup.md)
* [Replication Limitations](Replication/Limitations.md)
* [Replication Overhead](Replication/Overhead.md)
+ * [Replication Events](Replication/Events.md)
* [Sharding](Sharding/README.md)
* [How to try it out](Sharding/HowTo.md)
diff --git a/js/Makefile.files b/js/Makefile.files
index e8aa030420..b63c1167a5 100644
--- a/js/Makefile.files
+++ b/js/Makefile.files
@@ -59,6 +59,9 @@ JAVASCRIPT_JSLINT = \
\
`find @srcdir@/js/apps/system/cerberus -name "*.js"` \
`find @srcdir@/js/apps/system/gharial -name "*.js"` \
+ `find @srcdir@/js/apps/system/sessions -name "*.js"` \
+ `find @srcdir@/js/apps/system/simple-auth -name "*.js"` \
+ `find @srcdir@/js/apps/system/users -name "*.js"` \
\
`find @srcdir@/js/apps/system/aardvark/frontend/js/models -name "*.js"` \
`find @srcdir@/js/apps/system/aardvark/frontend/js/views -name "*.js"` \
diff --git a/js/apps/system/aardvark/frontend/js/templates/foxxActiveView.ejs b/js/apps/system/aardvark/frontend/js/templates/foxxActiveView.ejs
index 8668442166..14cb4a6ac9 100644
--- a/js/apps/system/aardvark/frontend/js/templates/foxxActiveView.ejs
+++ b/js/apps/system/aardvark/frontend/js/templates/foxxActiveView.ejs
@@ -31,5 +31,5 @@
<%}%>
-
<%= appInfos[1] %><%= attributes.isSystem ? " (system)" : "" %><%= appInfos[0] === "dev" ? " (dev)" : ""%>
+<%= appInfos[1] %><%= attributes.isSystem && attributes.mount.charAt(0) === "/" && attributes.mount.charAt(1) === "_" ? " (system)" : "" %><%= appInfos[0] === "dev" ? " (dev)" : ""%>
diff --git a/js/apps/system/aardvark/frontend/js/views/foxxActiveView.js b/js/apps/system/aardvark/frontend/js/views/foxxActiveView.js
index b8fdfc4866..6f7b53150a 100644
--- a/js/apps/system/aardvark/frontend/js/views/foxxActiveView.js
+++ b/js/apps/system/aardvark/frontend/js/views/foxxActiveView.js
@@ -27,14 +27,24 @@
initialize: function(){
this._show = true;
- this.buttonConfig = [
- window.modalView.createDeleteButton(
- "Uninstall", this.uninstall.bind(this)
- ),
- window.modalView.createSuccessButton(
- "Save", this.changeFoxx.bind(this)
- )
- ];
+ var mount = this.model.get("mount");
+ var isSystem = (
+ this.model.get("isSystem") &&
+ mount.charAt(0) === '/' &&
+ mount.charAt(1) === '_'
+ );
+ if (isSystem) {
+ this.buttonConfig = [];
+ } else {
+ this.buttonConfig = [
+ window.modalView.createDeleteButton(
+ "Uninstall", this.uninstall.bind(this)
+ ),
+ window.modalView.createSuccessButton(
+ "Save", this.changeFoxx.bind(this)
+ )
+ ];
+ }
this.showMod = window.modalView.show.bind(
window.modalView,
"modalTable.ejs",
@@ -72,6 +82,12 @@
isSystem,
active,
modView = window.modalView;
+ var mount = this.model.get("mount");
+ var editable = !(
+ this.model.get("isSystem") &&
+ mount.charAt(0) === '/' &&
+ mount.charAt(1) === '_'
+ );
if (this.model.get("isSystem")) {
isSystem = "Yes";
} else {
@@ -85,18 +101,24 @@
list.push(modView.createReadOnlyEntry(
"id_name", "Name", name
));
- list.push(modView.createTextEntry(
- "change-mount-point", "Mount", this.model.get("mount"),
- "The path where the app can be reached.",
- "mount-path",
- true,
- [
- {
- rule: Joi.string().required(),
- msg: "No mount-path given."
- }
- ]
- ));
+ if (editable) {
+ list.push(modView.createTextEntry(
+ "change-mount-point", "Mount", this.model.get("mount"),
+ "The path where the app can be reached.",
+ "mount-path",
+ true,
+ [
+ {
+ rule: Joi.string().required(),
+ msg: "No mount-path given."
+ }
+ ]
+ ));
+ } else {
+ list.push(modView.createReadOnlyEntry(
+ "change-mount-point", "Mount", this.model.get("mount")
+ ));
+ }
/*
* For the future, update apps to available newer versions
* versOptions.push(modView.createOptionEntry(appInfos[2]));
@@ -116,9 +138,15 @@
editFoxxDialog: function(event) {
event.stopPropagation();
- if (this.model.get("isSystem") || this.model.get("development")) {
+ var mount = this.model.get("mount");
+ var isSystem = (
+ this.model.get("isSystem") &&
+ mount.charAt(0) === '/' &&
+ mount.charAt(1) === '_'
+ );
+ if (this.model.get("development")) {
this.buttonConfig[0].disabled = true;
- } else {
+ } else if (!isSystem) {
delete this.buttonConfig[0].disabled;
}
this.showMod(this.buttonConfig, this.fillValues());
@@ -163,11 +191,9 @@
},
uninstall: function () {
- if (!this.model.get("isSystem")) {
- this.model.destroy({ async: false });
- window.modalView.hide();
- this.appsView.reload();
- }
+ this.model.destroy({ async: false });
+ window.modalView.hide();
+ this.appsView.reload();
},
showDocu: function(event) {
diff --git a/js/apps/system/aardvark/frontend/js/views/foxxInstalledView.js b/js/apps/system/aardvark/frontend/js/views/foxxInstalledView.js
index 5991c6eed1..aaf0884927 100644
--- a/js/apps/system/aardvark/frontend/js/views/foxxInstalledView.js
+++ b/js/apps/system/aardvark/frontend/js/views/foxxInstalledView.js
@@ -91,8 +91,6 @@
"Install", this.installDialog.bind(this)
)
];
- var buttonSystemInfoConfig = [
- ];
this.showMod = window.modalView.show.bind(
window.modalView,
"modalTable.ejs",
@@ -123,12 +121,6 @@
"Application Settings",
buttonInfoMultipleVersionsConfigUpdate
);
- this.showSystemInfoMod = window.modalView.show.bind(
- window.modalView,
- "modalTable.ejs",
- "Application Settings",
- buttonSystemInfoConfig
- );
this.showPurgeMod = window.modalView.show.bind(
window.modalView,
"modalTable.ejs",
@@ -334,38 +326,23 @@
infoDialog: function(event) {
var name = this.model.get("name"),
mountinfo = this.model.collection.gitInfo(name),
- versions, isSystem = false, isGit;
+ isGit;
if (mountinfo.git === true) {
this.model.set("isGit", mountinfo.git);
this.model.set("gitUrl", mountinfo.url);
}
- if (this.model.get("isSystem")) {
- isSystem = true;
- } else {
- isSystem = false;
- }
-
- versions = this.model.get("versions");
isGit = this.model.get("isGit");
event.stopPropagation();
- if (isSystem === false && !versions && !isGit) {
- this.showInfoMod(this.fillInfoValues());
- }
- else if (isSystem === false && !versions && isGit) {
- this.showInfoModUpdate(this.fillInfoValues());
- }
- else if (isSystem === false && versions && !isGit) {
- this.showInfoMultipleVersionsMod(this.fillInfoValues());
- }
- else if (isSystem === false && versions && isGit) {
- this.showInfoMultipleVersionsModUpdate(this.fillInfoValues());
- }
- else {
- this.showSystemInfoMod(this.fillInfoValues());
- }
+
+ this[
+ this.model.get("versions")
+ ? (isGit ? 'showInfoMultipleVersionsModUpdate' : 'showInfoMultipleVersionsMod')
+ : (isGit ? 'showInfoModUpdate' : 'showInfoMod')
+ ](this.fillInfoValues());
+
this.selectHighestVersion();
},
@@ -401,9 +378,12 @@
install: function() {
var mountPoint = $("#mount-point").val(),
version = "",
- regex = /^(\/[^\/\s]+)+$/,
self = this;
- if (!regex.test(mountPoint)){
+ if (/^\/_.+$/.test(mountPoint)) {
+ alert("Sorry, mount paths starting with an underscore are reserved for internal use.");
+ return false;
+ }
+ if (!/^(\/[^\/\s]+)+$/.test(mountPoint)){
alert("Sorry, you have to give a valid mount point, e.g.: /myPath");
return false;
}
diff --git a/js/apps/system/sessions/app.js b/js/apps/system/sessions/app.js
new file mode 100644
index 0000000000..5161f60dbc
--- /dev/null
+++ b/js/apps/system/sessions/app.js
@@ -0,0 +1,68 @@
+/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
+/*global require, applicationContext */
+(function () {
+ 'use strict';
+ var _ = require('underscore'),
+ joi = require('joi'),
+ Foxx = require('org/arangodb/foxx'),
+ errors = require('./errors'),
+ controller = new Foxx.Controller(applicationContext),
+ api = Foxx.requireApp(applicationContext.mount).sessionStorage,
+ sessionId = joi.string().description('Session ID');
+
+ controller.post('/', function (req, res) {
+ var session = api.create(req.body());
+ res.status(201);
+ res.json(session.forClient());
+ })
+ .errorResponse(SyntaxError, 400, 'Malformed or non-JSON session data.')
+ .summary('Create session')
+ .notes('Stores the given sessionData in a new session.');
+
+ controller.get('/:sid', function (req, res) {
+ var session = api.get(req.urlParameters.sid);
+ res.json(session.forClient());
+ })
+ .pathParam('sid', {type: sessionId})
+ .errorResponse(errors.SessionExpired, 404, 'Session has expired')
+ .errorResponse(errors.SessionNotFound, 404, 'Session does not exist')
+ .summary('Read session')
+ .notes('Fetches the session with the given sid.');
+
+ controller.put('/:sid', function (req, res) {
+ var body = JSON.parse(req.rawBody()),
+ session = api.get(req.urlParameters.sid);
+ session.set('sessionData', body);
+ session.save();
+ res.json(session.forClient());
+ })
+ .pathParam('sid', {type: sessionId})
+ .errorResponse(errors.SessionExpired, 404, 'Session has expired')
+ .errorResponse(errors.SessionNotFound, 404, 'Session does not exist')
+ .errorResponse(SyntaxError, 400, 'Malformed or non-JSON session data.')
+ .summary('Update session (replace)')
+ .notes('Updates the session with the given sid by replacing the sessionData.');
+
+ controller.patch('/:sid', function (req, res) {
+ var body = JSON.parse(req.rawBody()),
+ session = api.get(req.urlParameters.sid);
+ _.extend(session.get('sessionData'), body);
+ session.save();
+ res.json(session.forClient());
+ })
+ .pathParam('sid', {type: sessionId})
+ .errorResponse(errors.SessionExpired, 404, 'Session has expired')
+ .errorResponse(errors.SessionNotFound, 404, 'Session does not exist')
+ .errorResponse(SyntaxError, 400, 'Malformed or non-JSON session data.')
+ .summary('Update session')
+ .notes('Updates the session with the given sid by merging its sessionData.');
+
+ controller.delete('/:sid', function (req, res) {
+ api.destroy(req.urlParameters.sid);
+ res.status(204);
+ })
+ .pathParam('sid', {type: sessionId})
+ .errorResponse(errors.SessionNotFound, 404, 'Session does not exist')
+ .summary('Delete session')
+ .notes('Removes the session with the given sid from the database.');
+}());
\ No newline at end of file
diff --git a/js/apps/system/sessions/errors.js b/js/apps/system/sessions/errors.js
new file mode 100644
index 0000000000..b55816c382
--- /dev/null
+++ b/js/apps/system/sessions/errors.js
@@ -0,0 +1,18 @@
+/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
+/*global require, exports */
+(function () {
+ 'use strict';
+
+ function SessionNotFound(sid) {
+ this.message = 'Session with session id ' + sid + ' not found.';
+ }
+ SessionNotFound.prototype = new Error();
+
+ function SessionExpired(sid) {
+ this.message = 'Session with session id ' + sid + ' has expired.';
+ }
+ SessionExpired.prototype = Object.create(SessionNotFound.prototype);
+
+ exports.SessionNotFound = SessionNotFound;
+ exports.SessionExpired = SessionExpired;
+}());
\ No newline at end of file
diff --git a/js/apps/system/sessions/manifest.json b/js/apps/system/sessions/manifest.json
new file mode 100644
index 0000000000..ac6ca75d76
--- /dev/null
+++ b/js/apps/system/sessions/manifest.json
@@ -0,0 +1,44 @@
+{
+ "name": "sessions",
+ "author": "Alan Plum",
+ "version": "0.1",
+ "isSystem": true,
+ "description": "Session storage for Foxx.",
+
+ "controllers": {
+ "/": "app.js"
+ },
+
+ "exports": {
+ "sessionStorage": "storage.js"
+ },
+
+ "defaultDocument": "",
+
+ "lib": ".",
+
+ "setup": "setup.js",
+
+ "configuration": {
+ "timeToLive": {
+ "description": "Session expiry timeout in milliseconds.",
+ "type": "integer",
+ "default": 604800000
+ },
+ "ttlType": {
+ "description": "Timestamp session expiry should be checked against.",
+ "type": "string",
+ "default": "created"
+ },
+ "sidTimestamp": {
+ "description": "Append a timestamp to the session id.",
+ "type": "boolean",
+ "default": false
+ },
+ "sidLength": {
+ "description": "Length of the random part of the session id",
+ "type": "integer",
+ "default": 20
+ }
+ }
+}
diff --git a/js/apps/system/sessions/setup.js b/js/apps/system/sessions/setup.js
new file mode 100644
index 0000000000..593d6a228f
--- /dev/null
+++ b/js/apps/system/sessions/setup.js
@@ -0,0 +1,11 @@
+/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
+/*global require, applicationContext */
+(function () {
+ 'use strict';
+ var db = require('org/arangodb').db,
+ sessionsName = applicationContext.collectionName('sessions');
+
+ if (db._collection(sessionsName) === null) {
+ db._create(sessionsName);
+ }
+}());
\ No newline at end of file
diff --git a/js/apps/system/sessions/storage.js b/js/apps/system/sessions/storage.js
new file mode 100644
index 0000000000..77794310a6
--- /dev/null
+++ b/js/apps/system/sessions/storage.js
@@ -0,0 +1,200 @@
+/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
+/*global require, exports, applicationContext */
+(function () {
+ 'use strict';
+ var _ = require('underscore'),
+ joi = require('joi'),
+ internal = require('internal'),
+ arangodb = require('org/arangodb'),
+ db = arangodb.db,
+ addCookie = require('org/arangodb/actions').addCookie,
+ crypto = require('org/arangodb/crypto'),
+ Foxx = require('org/arangodb/foxx'),
+ errors = require('./errors'),
+ cfg = applicationContext.configuration,
+ Session = Foxx.Model.extend({
+ schema: {
+ _key: joi.string().required(),
+ uid: joi.string().optional(),
+ sessionData: joi.object().required(),
+ userData: joi.object().required(),
+ created: joi.number().integer().required(),
+ lastAccess: joi.number().integer().required(),
+ lastUpdate: joi.number().integer().required()
+ }
+ }),
+ sessions = new Foxx.Repository(
+ applicationContext.collection('sessions'),
+ {model: Session}
+ );
+
+ function generateSessionId() {
+ var sid = '';
+ if (cfg.sidTimestamp) {
+ sid = internal.base64Encode(Number(new Date()));
+ if (cfg.sidLength === 0) {
+ return sid;
+ }
+ sid += '-';
+ }
+ return sid + internal.genRandomAlphaNumbers(cfg.sidLength || 10);
+ }
+
+ function createSession(sessionData) {
+ var sid = generateSessionId(cfg),
+ now = Number(new Date()),
+ session = new Session({
+ _key: sid,
+ sid: sid,
+ sessionData: sessionData || {},
+ userData: {},
+ created: now,
+ lastAccess: now,
+ lastUpdate: now
+ });
+ sessions.save(session);
+ return session;
+ }
+
+ function getSession(sid) {
+ var session;
+ db._executeTransaction({
+ collections: {
+ read: [sessions.collection.name()],
+ write: [sessions.collection.name()]
+ },
+ action: function () {
+ try {
+ session = sessions.byId(sid);
+ session.enforceTimeout();
+ } catch (err) {
+ if (
+ err instanceof arangodb.ArangoError
+ && err.errorNum === arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND
+ ) {
+ throw new errors.SessionNotFound(sid);
+ } else {
+ throw err;
+ }
+ }
+ var now = Number(new Date());
+ sessions.collection.update(session.forDB(), {
+ lastAccess: now
+ });
+ session.set('lastAccess', now);
+ }
+ });
+ return session;
+ }
+
+ function deleteSession(sid) {
+ try {
+ sessions.removeById(sid);
+ } catch (err) {
+ if (
+ err instanceof arangodb.ArangoError
+ && err.errorNum === arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND
+ ) {
+ throw new errors.SessionNotFound(sid);
+ } else {
+ throw err;
+ }
+ }
+ return null;
+ }
+
+ function fromCookie(req, cookieName, secret) {
+ var session = null,
+ value = req.cookies[cookieName],
+ signature;
+ if (value) {
+ if (secret) {
+ signature = req.cookies[cookieName + '_sig'] || '';
+ if (!crypto.constantEquals(signature, crypto.hmac(secret, value))) {
+ return null;
+ }
+ }
+ try {
+ session = getSession(value);
+ } catch (e) {
+ if (!(e instanceof errors.SessionNotFound)) {
+ throw e;
+ }
+ }
+ }
+ return session;
+ }
+
+ _.extend(Session.prototype, {
+ enforceTimeout: function () {
+ if (!cfg.timeToLive) {
+ return;
+ }
+ var now = Number(new Date()),
+ prop = cfg.ttlType;
+ if (!prop || !this.get(prop)) {
+ prop = 'created';
+ }
+ if (cfg.timeToLive < (now - this.get(prop))) {
+ throw new errors.SessionExpired(this.get('_key'));
+ }
+ },
+ addCookie: function (res, cookieName, secret) {
+ var value = this.get('_key'),
+ ttl = cfg.timeToLive;
+ ttl = ttl ? Math.floor(ttl / 1000) : undefined;
+ addCookie(res, cookieName, value, ttl);
+ if (secret) {
+ addCookie(res, cookieName + '_sig', crypto.hmac(secret, value), ttl);
+ }
+ },
+ clearCookie: function (res, cookieName, secret) {
+ addCookie(res, cookieName, '', -(7 * 24 * 60 * 60));
+ if (secret) {
+ addCookie(res, cookieName + '_sig', '', -(7 * 24 * 60 * 60));
+ }
+ },
+ setUser: function (user) {
+ var session = this;
+ if (user) {
+ session.set('uid', user.get('_key'));
+ session.set('userData', user.get('userData'));
+ } else {
+ delete session.attributes.uid;
+ session.set('userData', {});
+ }
+ return session;
+ },
+ save: function () {
+ var session = this,
+ now = Number(new Date());
+ session.set('lastAccess', now);
+ session.set('lastUpdate', now);
+ sessions.replace(session);
+ return session;
+ },
+ delete: function () {
+ var session = this,
+ now = Number(new Date());
+ session.set('lastAccess', now);
+ session.set('lastUpdate', now);
+ try {
+ deleteSession(session.get('_key'));
+ return true;
+ } catch (e) {
+ if (e instanceof errors.SessionNotFound) {
+ return false;
+ }
+ throw e;
+ }
+ }
+ });
+
+ exports.fromCookie = fromCookie;
+ exports.create = createSession;
+ exports.get = getSession;
+ exports.delete = deleteSession;
+ exports.errors = errors;
+ exports.repository = sessions;
+ exports._generateSessionId = generateSessionId;
+}());
\ No newline at end of file
diff --git a/js/apps/system/simple-auth/auth.js b/js/apps/system/simple-auth/auth.js
new file mode 100644
index 0000000000..725a7daef0
--- /dev/null
+++ b/js/apps/system/simple-auth/auth.js
@@ -0,0 +1,29 @@
+/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
+/*global require, exports, applicationContext */
+(function () {
+ 'use strict';
+ var crypto = require('org/arangodb/crypto'),
+ cfg = applicationContext.configuration;
+
+ function verifyPassword(authData, password) {
+ if (!authData) {
+ authData = {};
+ }
+ var hashMethod = authData.method || cfg.hashMethod,
+ salt = authData.salt || '',
+ storedHash = authData.hash || '',
+ generatedHash = crypto[hashMethod](salt + password);
+ // non-lazy comparison to avoid timing attacks
+ return crypto.constantEquals(storedHash, generatedHash);
+ }
+
+ function hashPassword(password) {
+ var hashMethod = cfg.hashMethod,
+ salt = crypto.genRandomAlphaNumbers(cfg.saltLength),
+ hash = crypto[hashMethod](salt + password);
+ return {method: hashMethod, salt: salt, hash: hash};
+ }
+
+ exports.verifyPassword = verifyPassword;
+ exports.hashPassword = hashPassword;
+}());
\ No newline at end of file
diff --git a/js/apps/system/simple-auth/manifest.json b/js/apps/system/simple-auth/manifest.json
new file mode 100644
index 0000000000..1f803ba56a
--- /dev/null
+++ b/js/apps/system/simple-auth/manifest.json
@@ -0,0 +1,28 @@
+{
+ "name": "simple-auth",
+ "author": "Alan Plum",
+ "version": "0.1",
+ "isSystem": true,
+ "description": "Simple password-based authentication for Foxx.",
+
+ "exports": {
+ "auth": "auth.js"
+ },
+
+ "defaultDocument": "",
+
+ "lib": ".",
+
+ "configuration": {
+ "hashMethod": {
+ "description": "Cryptographic hash function to use for new passwords.",
+ "type": "string",
+ "default": "sha256"
+ },
+ "saltLength": {
+ "description": "Length of new salts.",
+ "type": "integer",
+ "default": 16
+ }
+ }
+}
diff --git a/js/apps/system/users/errors.js b/js/apps/system/users/errors.js
new file mode 100644
index 0000000000..59a268dd22
--- /dev/null
+++ b/js/apps/system/users/errors.js
@@ -0,0 +1,18 @@
+/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
+/*global require, exports */
+(function () {
+ 'use strict';
+
+ function UserNotFound(uid) {
+ this.message = 'User with user id ' + uid + ' not found.';
+ }
+ UserNotFound.prototype = new Error();
+
+ function UsernameNotAvailable(username) {
+ this.message = 'The username ' + username + ' is not available or already taken.';
+ }
+ UsernameNotAvailable.prototype = new Error();
+
+ exports.UserNotFound = UserNotFound;
+ exports.UsernameNotAvailable = UsernameNotAvailable;
+}());
\ No newline at end of file
diff --git a/js/apps/system/users/manifest.json b/js/apps/system/users/manifest.json
new file mode 100644
index 0000000000..b0e437bd65
--- /dev/null
+++ b/js/apps/system/users/manifest.json
@@ -0,0 +1,17 @@
+{
+ "name": "users",
+ "author": "Alan Plum",
+ "version": "0.1",
+ "isSystem": true,
+ "description": "Username-based user storage for Foxx.",
+
+ "exports": {
+ "userStorage": "storage.js"
+ },
+
+ "defaultDocument": "",
+
+ "lib": ".",
+
+ "setup": "setup.js"
+}
diff --git a/js/apps/system/users/setup.js b/js/apps/system/users/setup.js
new file mode 100644
index 0000000000..564bda8c74
--- /dev/null
+++ b/js/apps/system/users/setup.js
@@ -0,0 +1,11 @@
+/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
+/*global require, applicationContext */
+(function () {
+ 'use strict';
+ var db = require('org/arangodb').db,
+ usersName = applicationContext.collectionName('users');
+
+ if (db._collection(usersName) === null) {
+ db._create(usersName);
+ }
+}());
\ No newline at end of file
diff --git a/js/apps/system/users/storage.js b/js/apps/system/users/storage.js
new file mode 100644
index 0000000000..0a02c83569
--- /dev/null
+++ b/js/apps/system/users/storage.js
@@ -0,0 +1,122 @@
+/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
+/*global require, exports, applicationContext */
+(function () {
+ 'use strict';
+ var _ = require('underscore'),
+ joi = require('joi'),
+ arangodb = require('org/arangodb'),
+ db = arangodb.db,
+ Foxx = require('org/arangodb/foxx'),
+ errors = require('./errors'),
+ User = Foxx.Model.extend({
+ schema: {
+ user: joi.string().required(),
+ authData: joi.object().required(),
+ userData: joi.object().required()
+ }
+ }),
+ users = new Foxx.Repository(
+ applicationContext.collection('users'),
+ {model: User}
+ );
+
+ function resolve(username) {
+ var user = users.firstExample({user: username});
+ if (!user.get('_key')) {
+ return null;
+ }
+ return user;
+ }
+
+ function listUsers() {
+ return users.collection.all().toArray().map(function (user) {
+ return user.user;
+ }).filter(Boolean);
+ }
+
+ function createUser(username, userData) {
+ if (!userData) {
+ userData = {};
+ }
+ if (!username) {
+ throw new Error('Must provide username!');
+ }
+ var user;
+ db._executeTransaction({
+ collections: {
+ read: [users.collection.name()],
+ write: [users.collection.name()]
+ },
+ action: function () {
+ if (resolve(username)) {
+ throw new errors.UsernameNotAvailable(username);
+ }
+ user = new User({
+ user: username,
+ userData: userData,
+ authData: {}
+ });
+ users.save(user);
+ }
+ });
+ return user;
+ }
+
+ function getUser(uid) {
+ var user;
+ try {
+ user = users.byId(uid);
+ } catch (err) {
+ if (
+ err instanceof arangodb.ArangoError
+ && err.errorNum === arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND
+ ) {
+ throw new errors.UserNotFound(uid);
+ }
+ throw err;
+ }
+ return user;
+ }
+
+ function deleteUser(uid) {
+ try {
+ users.removeById(uid);
+ } catch (err) {
+ if (
+ err instanceof arangodb.ArangoError
+ && err.errorNum === arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND
+ ) {
+ throw new errors.UserNotFound(uid);
+ }
+ throw err;
+ }
+ return null;
+ }
+
+ _.extend(User.prototype, {
+ save: function () {
+ var user = this;
+ users.replace(user);
+ return user;
+ },
+ delete: function () {
+ try {
+ deleteUser(this.get('_key'));
+ return true;
+ } catch (e) {
+ if (e instanceof errors.UserNotFound) {
+ return false;
+ }
+ throw e;
+ }
+ }
+ });
+
+ exports.resolve = resolve;
+ exports.list = listUsers;
+ exports.create = createUser;
+ exports.get = getUser;
+ exports.delete = deleteUser;
+ exports.errors = errors;
+ exports.repository = users;
+}());
\ No newline at end of file
diff --git a/js/common/tests/shell-database.js b/js/common/tests/shell-database.js
index 22ac7d5ae6..72caa2ccea 100644
--- a/js/common/tests/shell-database.js
+++ b/js/common/tests/shell-database.js
@@ -251,14 +251,14 @@ function DatabaseSuite () {
assertTrue(internal.db._createDatabase("UnitTestsDatabase0", { }, users));
internal.db._useDatabase("UnitTestsDatabase0");
- var m = require("org/arangodb/users");
- var user = m.document("admin");
+ var userManager = require("org/arangodb/users");
+ var user = userManager.document("admin");
assertEqual("admin", user.user);
assertTrue(user.active);
assertEqual("m", user.extra.gender);
- user = m.document("foo");
+ user = userManager.document("foo");
assertEqual("foo", user.user);
assertFalse(user.active);
assertEqual("f", user.extra.gender);
@@ -288,8 +288,8 @@ function DatabaseSuite () {
assertTrue(internal.db._createDatabase("UnitTestsDatabase0", { }, users));
internal.db._useDatabase("UnitTestsDatabase0");
- var m = require("org/arangodb/users");
- var user = m.document("admin");
+ var userManager = require("org/arangodb/users");
+ var user = userManager.document("admin");
assertEqual("admin", user.user);
assertTrue(user.active);
assertEqual("m", user.extra.gender);
diff --git a/js/common/tests/shell-users.js b/js/common/tests/shell-users.js
index a060a90a06..a32e7cb914 100644
--- a/js/common/tests/shell-users.js
+++ b/js/common/tests/shell-users.js
@@ -129,8 +129,8 @@ function UsersSuite () {
users.save(username, passwd);
assertEqual(username, c.firstExample({ user: username }).user);
-
- try {
+
+ try {
users.save(username, passwd);
fail();
}
diff --git a/js/server/modules/org/arangodb/foxx/authentication.js b/js/server/modules/org/arangodb/foxx/authentication.js
index dd43b87ff9..57563c5626 100644
--- a/js/server/modules/org/arangodb/foxx/authentication.js
+++ b/js/server/modules/org/arangodb/foxx/authentication.js
@@ -52,7 +52,7 @@ var db = require("org/arangodb").db,
CookieAuthentication,
Authentication,
UserAlreadyExistsError,
- UnauthorizedError;
+ UnauthorizedError = require("./sessions").UnauthorizedError;
// -----------------------------------------------------------------------------
// --SECTION-- helper functions
@@ -711,7 +711,7 @@ Users.prototype.exists = function (identifier) {
};
////////////////////////////////////////////////////////////////////////////////
-/// @brief check whether a user is valid
+/// @brief check whether a user is valid
////////////////////////////////////////////////////////////////////////////////
Users.prototype.isValid = function (identifier, password) {
diff --git a/js/server/modules/org/arangodb/foxx/controller.js b/js/server/modules/org/arangodb/foxx/controller.js
index b6ac6f87d0..cf96fb65f0 100644
--- a/js/server/modules/org/arangodb/foxx/controller.js
+++ b/js/server/modules/org/arangodb/foxx/controller.js
@@ -1,4 +1,4 @@
-/*jslint indent: 2, nomen: true, maxlen: 120, regexp: true */
+/*jslint indent: 2, nomen: true, maxlen: 120, regexp: true, vars: true */
/*global module, require, exports */
////////////////////////////////////////////////////////////////////////////////
@@ -43,8 +43,8 @@ var Controller,
// -----------------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////////////
-/// @brief Create a new Controller
/// @startDocuBlock JSF_foxx_controller_initializer
+///
/// `new FoxxController(applicationContext, options)`
///
/// This creates a new Controller. The first argument is the controller
@@ -60,6 +60,7 @@ var Controller,
/// urlPrefix: "/meadow"
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
@@ -221,6 +222,7 @@ extend(Controller.prototype, {
/// // Take this request and deal with it!
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
@@ -244,6 +246,7 @@ extend(Controller.prototype, {
/// // Take this request and deal with it!
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
@@ -267,6 +270,7 @@ extend(Controller.prototype, {
/// // Take this request and deal with it!
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
@@ -290,6 +294,7 @@ extend(Controller.prototype, {
/// // Take this request and deal with it!
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
@@ -321,6 +326,7 @@ extend(Controller.prototype, {
/// // Take this request and deal with it!
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
@@ -351,6 +357,7 @@ extend(Controller.prototype, {
/// //Do some crazy request logging
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
@@ -388,6 +395,7 @@ extend(Controller.prototype, {
/// //Do some crazy response logging
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
@@ -448,13 +456,14 @@ extend(Controller.prototype, {
/// @EXAMPLES
///
/// ```js
-/// app.activateAuthentication({
-/// type: "cookie",
-/// cookieLifetime: 360000,
-/// cookieName: "my_cookie",
-/// sessionLifetime: 400,
-/// });
+/// app.activateAuthentication({
+/// type: "cookie",
+/// cookieLifetime: 360000,
+/// cookieName: "my_cookie",
+/// sessionLifetime: 400,
+/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
activateAuthentication: function (opts) {
@@ -496,6 +505,7 @@ extend(Controller.prototype, {
/// }
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
login: function (route, opts) {
@@ -533,6 +543,7 @@ extend(Controller.prototype, {
/// }
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
logout: function (route, opts) {
@@ -581,6 +592,7 @@ extend(Controller.prototype, {
/// }
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
register: function (route, opts) {
@@ -622,14 +634,98 @@ extend(Controller.prototype, {
/// }
/// });
/// ```
+///
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
changePassword: function (route, opts) {
'use strict';
var authentication = require("org/arangodb/foxx/authentication");
return this.post(route, authentication.createStandardChangePasswordHandler(this.getUsers(), opts));
- }
+ },
+////////////////////////////////////////////////////////////////////////////////
+/// @fn JSF_foxx_controller_getSessions
+/// @brief Get the sessions object of this controller
+////////////////////////////////////////////////////////////////////////////////
+ getSessions: function () {
+ 'use strict';
+ return this.sessions;
+ },
+
+////////////////////////////////////////////////////////////////////////////////
+/// @startDocuBlock JSF_foxx_controller_activateSessions
+///
+/// `FoxxController#activateAuthentication(opts)`
+///
+/// To activate sessions for this sessions, first call this function.
+/// Provide the following arguments:
+///
+/// * *type*: Currently we only support *cookie*, but this will change in the future. Defaults to *"cookie"*.
+/// * *cookieName*: A string used as the name of the cookie. Defaults to *"sid"*.
+/// * *cookieSecret*: A secret string used to sign the cookie (as "*cookieName*_sig"). Optional.
+/// * *autoCreateSession*: Whether to always create a session if none exists. Defaults to *true*.
+/// * *sessionStorageApp*: Mount path of the app to use for sessions. Defaults to */_system/sessions*
+///
+///
+/// @EXAMPLES
+///
+/// ```js
+/// app.activateSessions({
+/// type: "cookie",
+/// cookieName: "my_cookie",
+/// autoCreateSession: true,
+/// sessionStorageApp: "/my-sessions"
+/// });
+/// ```
+///
+/// @endDocuBlock
+////////////////////////////////////////////////////////////////////////////////
+ activateSessions: function (opts) {
+ 'use strict';
+ var sessions = require("org/arangodb/foxx/sessions");
+
+ this.sessions = new sessions.Sessions(opts);
+ sessions.decorateController(this.sessions, this);
+ },
+
+////////////////////////////////////////////////////////////////////////////////
+/// @startDocuBlock JSF_foxx_controller_destroySession
+///
+/// `FoxxController#logout(path, opts)`
+///
+/// This adds a path to your app for the destroySession functionality.
+/// You can customize it with custom *before* and *after* function:
+/// *before* is a function that you can define to do something before
+/// the session is destroyed.
+/// *after* is a function that you can define to do something after the
+/// session is destroyed. This defaults to a function that returns a
+/// JSON object with *message* set to "logged out".
+/// Both *before* and *after* should take request and result as arguments.
+/// If you only want to provide an *after* function, you can pass the
+/// function directly instead of an object.
+///
+/// @EXAMPLES
+///
+/// ```js
+/// app.destroySession('/logout', function (req, res) {
+/// res.json({"message": "Bye, Bye"});
+/// });
+/// ```
+///
+/// @endDocuBlock
+////////////////////////////////////////////////////////////////////////////////
+ destroySession: function (route, opts) {
+ 'use strict';
+ var method = opts.method;
+ if (typeof method === 'string') {
+ method = method.toLowerCase();
+ }
+ if (!method || typeof this[method] !== 'function') {
+ method = 'post';
+ }
+ var sessions = require("org/arangodb/foxx/sessions");
+ return this[method](route, sessions.createDestroySessionHandler(this.getSessions(), opts));
+ }
});
exports.Controller = Controller;
diff --git a/js/server/modules/org/arangodb/foxx/manager.js b/js/server/modules/org/arangodb/foxx/manager.js
index 74ed9e3f7f..ff320c7e3f 100644
--- a/js/server/modules/org/arangodb/foxx/manager.js
+++ b/js/server/modules/org/arangodb/foxx/manager.js
@@ -192,14 +192,13 @@ function extendContext (context, app, root) {
'use strict';
var cp = context.collectionPrefix;
- var cname = "";
- if (cp !== "") {
- cname = cp + "_";
+ if (cp !== "" && cp !== "_") {
+ cp += "_";
}
context.collectionName = function (name) {
- var replaced = (cname + name).replace(/[^a-zA-Z0-9]/g, '_').replace(/(^_+|_+$)/g, '').substr(0, 64);
+ var replaced = (cp + name.replace(/[^a-zA-Z0-9]/g, '_').replace(/(^_+|_+$)/g, '')).substr(0, 64);
if (replaced.length === 0) {
throw new Error("Cannot derive collection name from '" + name + "'");
@@ -1023,6 +1022,24 @@ function checkConfiguration (app, options) {
return false;
}
+////////////////////////////////////////////////////////////////////////////////
+/// @brief returns collection prefix for system apps
+////////////////////////////////////////////////////////////////////////////////
+
+ function systemCollectionPrefix (appName) {
+ 'use strict';
+
+ if (appName === "sessions") {
+ return "_";
+ }
+
+ if (appName === "users") {
+ return "_";
+ }
+
+ return false;
+ }
+
// -----------------------------------------------------------------------------
// --SECTION-- public functions
// -----------------------------------------------------------------------------
@@ -1236,7 +1253,7 @@ exports.unmount = function (mount) {
var doc = mountFromId(mount);
- if (doc.isSystem) {
+ if (doc.isSystem && mount.charAt(1) === '_') {
throw new Error("Cannot unmount system application");
}
@@ -1764,7 +1781,12 @@ exports.initializeFoxx = function () {
var found = aal.firstExample({ type: "mount", mount: mount });
if (found === null) {
- exports.mount(appName, mount, {reload: false});
+ var opts = {reload: false};
+ var prefix = systemCollectionPrefix(appName);
+ if (prefix) {
+ opts.collectionPrefix = prefix;
+ }
+ exports.mount(appName, mount, opts);
var doc = mountFromId(mount);
var app = appFromAppId(doc.app);
diff --git a/js/server/modules/org/arangodb/foxx/request_context.js b/js/server/modules/org/arangodb/foxx/request_context.js
index 464b792828..4db76f7417 100644
--- a/js/server/modules/org/arangodb/foxx/request_context.js
+++ b/js/server/modules/org/arangodb/foxx/request_context.js
@@ -35,9 +35,9 @@ var RequestContext,
extend = _.extend,
internal = require("org/arangodb/foxx/internals"),
is = require("org/arangodb/is"),
- UnauthorizedError = require("org/arangodb/foxx/authentication").UnauthorizedError,
elementExtractFactory,
bubbleWrapFactory,
+ UnauthorizedError = require("org/arangodb/foxx/sessions").UnauthorizedError,
createErrorBubbleWrap,
createBodyParamBubbleWrap,
addCheck;
@@ -524,7 +524,8 @@ extend(RequestContext.prototype, {
///
/// `FoxxController#onlyIf(code, reason)`
///
-/// Please activate authentification for this app if you want to use this function.
+/// Please activate sessions for this app if you want to use this function.
+/// Or activate authentication (deprecated).
/// If the user is logged in, it will do nothing. Otherwise it will respond with
/// the status code and the reason you provided (the route handler won't be called).
/// This will also add the according documentation for this route.
@@ -543,7 +544,10 @@ extend(RequestContext.prototype, {
var check;
check = function (req) {
- if (!(req.user && req.currentSession)) {
+ if (
+ !(req.session && req.session.get('uid')) // new and shiny
+ && !(req.user && req.currentSession) // old and busted
+ ) {
throw new UnauthorizedError();
}
};
diff --git a/js/server/modules/org/arangodb/foxx/sessions.js b/js/server/modules/org/arangodb/foxx/sessions.js
new file mode 100644
index 0000000000..37a8d132c3
--- /dev/null
+++ b/js/server/modules/org/arangodb/foxx/sessions.js
@@ -0,0 +1,237 @@
+/*jslint indent: 2, nomen: true, maxlen: 120, es5: true */
+/*global require, exports */
+
+////////////////////////////////////////////////////////////////////////////////
+/// @brief Foxx Sessions
+///
+/// @file
+///
+/// DISCLAIMER
+///
+/// Copyright 2013 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, Lucas Dohmen
+/// @author Copyright 2013, triAGENS GmbH, Cologne, Germany
+////////////////////////////////////////////////////////////////////////////////
+
+var Foxx = require('org/arangodb/foxx');
+
+// -----------------------------------------------------------------------------
+// --SECTION-- helper functions
+// -----------------------------------------------------------------------------
+
+////////////////////////////////////////////////////////////////////////////////
+/// @addtogroup Foxx
+/// @{
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+/// @brief decorates the controller with session logic
+////////////////////////////////////////////////////////////////////////////////
+
+function decorateController(auth, controller) {
+ 'use strict';
+ var cfg = auth.configuration;
+
+ controller.before('/*', function (req) {
+ var sessions = auth.getSessionStorage();
+ if (cfg.type === 'cookie') {
+ req.session = sessions.fromCookie(req, cfg.cookieName, cfg.cookieSecret);
+ if (!req.session && cfg.autoCreateSession) {
+ req.session = sessions.create();
+ }
+ }
+ });
+
+ controller.after('/*', function (req, res) {
+ if (req.session) {
+ if (cfg.type === 'cookie') {
+ req.session.addCookie(res, cfg.cookieName, cfg.cookieSecret);
+ }
+ }
+ });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+/// @brief convenience wrapper for session destruction logic
+////////////////////////////////////////////////////////////////////////////////
+
+function createDestroySessionHandler(auth, opts) {
+ 'use strict';
+ if (!opts) {
+ opts = {};
+ }
+ if (typeof opts === 'function') {
+ opts = {after: opts};
+ }
+ if (!opts.after) {
+ opts.after = function (req, res) {
+ res.json({message: 'logged out'});
+ };
+ }
+ var cfg = auth.configuration;
+ return function (req, res, injected) {
+ if (typeof opts.before === 'function') {
+ opts.before(req, res, injected);
+ }
+ if (req.session) {
+ req.session.delete();
+ }
+ if (cfg.autoCreateSession) {
+ req.session = auth.getSessionStorage().create();
+ } else {
+ if (cfg.type === 'cookie') {
+ req.session.clearCookie(res, cfg.cookieName, cfg.cookieSecret);
+ }
+ delete req.session;
+ }
+ if (typeof opts.after === 'function') {
+ opts.after(req, res, injected);
+ }
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+/// @}
+////////////////////////////////////////////////////////////////////////////////
+
+// -----------------------------------------------------------------------------
+// --SECTION-- FOXX SESSIONS
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
+// --SECTION-- constructors and destructors
+// -----------------------------------------------------------------------------
+
+////////////////////////////////////////////////////////////////////////////////
+/// @addtogroup Foxx
+/// @{
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+/// @brief constructor
+////////////////////////////////////////////////////////////////////////////////
+
+function Sessions(opts) {
+ 'use strict';
+ if (!opts) {
+ opts = {};
+ }
+ if (opts.type !== 'cookie') {
+ throw new Error('Only "cookie" type sessions are supported at this time.');
+ }
+ if (opts.cookieSecret && typeof opts.cookieSecret !== 'string') {
+ throw new Error('Cookie secret must be a string or empty.');
+ }
+ if (opts.cookieName && typeof opts.cookieName !== 'string') {
+ throw new Error('Cookie name must be a string or empty.');
+ }
+
+ if (!opts.type) {
+ opts.type = 'cookie';
+ }
+ if (!opts.cookieName) {
+ opts.cookieName = 'sid';
+ }
+ if (opts.autoCreateSession !== false) {
+ opts.autoCreateSession = true;
+ }
+ if (!opts.sessionStorageApp) {
+ opts.sessionStorageApp = '/_system/sessions';
+ }
+ this.configuration = opts;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+/// @}
+////////////////////////////////////////////////////////////////////////////////
+
+// -----------------------------------------------------------------------------
+// --SECTION-- public functions
+// -----------------------------------------------------------------------------
+
+////////////////////////////////////////////////////////////////////////////////
+/// @addtogroup Foxx
+/// @{
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+/// @brief fetches the session storage
+////////////////////////////////////////////////////////////////////////////////
+
+Sessions.prototype.getSessionStorage = function () {
+ 'use strict';
+ return Foxx.requireApp(this.configuration.sessionStorageApp).sessionStorage;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+/// @}
+////////////////////////////////////////////////////////////////////////////////
+
+// -----------------------------------------------------------------------------
+// --SECTION-- custom errors
+// -----------------------------------------------------------------------------
+
+// http://stackoverflow.com/questions/783818/how-do-i-create-a-custom-error-in-javascript
+
+////////////////////////////////////////////////////////////////////////////////
+/// @addtogroup Foxx
+/// @{
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+/// @brief constructor
+////////////////////////////////////////////////////////////////////////////////
+
+function UnauthorizedError(message) {
+ 'use strict';
+ this.message = message;
+ this.statusCode = 401;
+}
+
+UnauthorizedError.prototype = new Error();
+
+////////////////////////////////////////////////////////////////////////////////
+/// @}
+////////////////////////////////////////////////////////////////////////////////
+
+// -----------------------------------------------------------------------------
+// --SECTION-- module exports
+// -----------------------------------------------------------------------------
+
+////////////////////////////////////////////////////////////////////////////////
+/// @addtogroup Foxx
+/// @{
+////////////////////////////////////////////////////////////////////////////////
+
+exports.UnauthorizedError = UnauthorizedError;
+exports.Sessions = Sessions;
+exports.decorateController = decorateController;
+exports.createDestroySessionHandler = createDestroySessionHandler;
+
+////////////////////////////////////////////////////////////////////////////////
+/// @}
+////////////////////////////////////////////////////////////////////////////////
+
+// -----------------------------------------------------------------------------
+// --SECTION-- END-OF-FILE
+// -----------------------------------------------------------------------------
+
+/// Local Variables:
+/// mode: outline-minor
+/// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint"
+/// End:
diff --git a/js/server/modules/org/arangodb/users.js b/js/server/modules/org/arangodb/users.js
index 57dbb6e56a..2e891b7803 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: 120, sloppy: true, vars: true, white: true, plusplus: true */
+/*jslint indent: 2, nomen: true, maxlen: 120, sloppy: true, vars: true, white: true, plusplus: true, es5: true */
/*global require, exports, ArangoAgency */
////////////////////////////////////////////////////////////////////////////////
@@ -43,28 +43,31 @@ var ArangoError = arangodb.ArangoError;
// --SECTION-- private functions
// -----------------------------------------------------------------------------
+////////////////////////////////////////////////////////////////////////////////
+/// @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 encodePassword = function (password) {
- var salt;
- var encoded;
+var hashPassword = function (password) {
+ var salt = internal.genRandomAlphaNumbers(16);
- 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;
+ return {
+ hash: crypto.sha256(salt + password),
+ salt: salt,
+ method: "sha256"
+ };
};
////////////////////////////////////////////////////////////////////////////////
@@ -72,7 +75,7 @@ var encodePassword = function (password) {
////////////////////////////////////////////////////////////////////////////////
var validateName = function (username) {
- if (typeof username !== 'string' || 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;
@@ -85,8 +88,8 @@ var validateName = function (username) {
/// @brief validates password
////////////////////////////////////////////////////////////////////////////////
-var validatePassword = function (passwd) {
- if (typeof passwd !== 'string') {
+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;
@@ -106,7 +109,6 @@ var getStorage = function () {
var err = new ArangoError();
err.errorNum = arangodb.errors.ERROR_ARANGO_COLLECTION_NOT_FOUND.code;
err.errorMessage = "collection _users not found";
-
throw err;
}
@@ -121,14 +123,14 @@ var getStorage = function () {
/// @brief creates a new user
////////////////////////////////////////////////////////////////////////////////
-exports.save = function (user, passwd, active, extra, changePassword) {
- if (passwd === null || passwd === undefined) {
- passwd = "";
+exports.save = function (username, password, active, userData, changePassword) {
+ if (password === null || password === undefined) {
+ password = "";
}
// validate input
- validateName(user);
- validatePassword(passwd);
+ validateName(username);
+ validatePassword(password);
if (active === undefined || active === null) {
active = true; // this is the default value
@@ -143,47 +145,45 @@ exports.save = function (user, passwd, active, extra, changePassword) {
}
var users = getStorage();
- var previous = users.firstExample({ user: user });
+ var user = users.firstExample({user: username});
- 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);
+ 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 err = new ArangoError();
- err.errorNum = arangodb.errors.ERROR_USER_DUPLICATE.code;
- err.errorMessage = arangodb.errors.ERROR_USER_DUPLICATE.message;
+ var data = {
+ user: username,
+ userData: userData || {},
+ authData: {
+ simple: hashPassword(password),
+ active: Boolean(active),
+ changePassword: Boolean(changePassword)
+ }
+ };
- throw err;
+ var doc = users.save(data);
+
+ // not exports.reload() as this is an abstract method...
+ require("org/arangodb/users").reload();
+
+ return convertToLegacyFormat(users.document(doc._id));
};
////////////////////////////////////////////////////////////////////////////////
/// @brief replaces an existing user
////////////////////////////////////////////////////////////////////////////////
-exports.replace = function (user, passwd, active, extra, changePassword) {
- if (passwd === null || passwd === undefined) {
- passwd = "";
+exports.replace = function (username, password, active, userData, changePassword) {
+ if (password === null || password === undefined) {
+ password = "";
}
// validate input
- validateName(user);
- validatePassword(passwd);
+ validateName(username);
+ validatePassword(password);
if (active === undefined || active === null) {
active = true; // this is the default
@@ -194,159 +194,146 @@ exports.replace = function (user, passwd, active, extra, changePassword) {
}
var users = getStorage();
- var previous = users.firstExample({ user: user });
+ var user = users.firstExample({user: username});
- if (previous === null) {
+ 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 hash = encodePassword(passwd);
var data = {
- user: user,
- password: hash,
- active: active,
- changePassword: changePassword
+ user: username,
+ userData: userData || {},
+ authData: {
+ simple: hashPassword(password),
+ active: Boolean(active),
+ changePassword: Boolean(changePassword)
+ }
};
- if (extra !== undefined) {
- data.extra = extra;
- }
-
- users.replace(previous, data);
+ var doc = users.replace(user, data);
// not exports.reload() as this is an abstract method...
require("org/arangodb/users").reload();
- return users.document(previous._id);
+ return convertToLegacyFormat(users.document(doc._id));
};
////////////////////////////////////////////////////////////////////////////////
/// @brief updates an existing user
////////////////////////////////////////////////////////////////////////////////
-exports.update = function (user, passwd, active, extra, changePassword) {
-
+exports.update = function (username, password, active, userData, changePassword) {
// validate input
- validateName(user);
+ validateName(username);
- if (passwd !== undefined) {
- validatePassword(passwd);
+ if (password !== undefined) {
+ validatePassword(password);
}
var users = getStorage();
- var previous = users.firstExample({ user: user });
+ var user = users.firstExample({user: username});
- if (previous === null) {
+ 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 = previous._shallowCopy;
+ var data = user._shallowCopy;
- if (passwd !== undefined) {
- var hash = encodePassword(passwd);
- data.password = hash;
+ if (password !== undefined) {
+ data.authData.simple = hashPassword(password);
}
if (active !== undefined && active !== null) {
- data.active = active;
+ data.authData.active = active;
}
- if (extra !== undefined) {
- data.extra = extra;
+ if (userData !== undefined) {
+ data.userData = userData;
}
if (changePassword !== undefined && changePassword !== null) {
- data.changePassword = changePassword;
+ data.authData.changePassword = changePassword;
}
- users.update(previous, data);
+ users.update(user, data);
// not exports.reload() as this is an abstract method...
require("org/arangodb/users").reload();
- return users.document(previous._id);
+ return convertToLegacyFormat(users.document(user._id));
};
////////////////////////////////////////////////////////////////////////////////
/// @brief deletes an existing user
////////////////////////////////////////////////////////////////////////////////
-exports.remove = function (user) {
+exports.remove = function (username) {
// validate input
- validateName(user);
+ validateName(username);
var users = getStorage();
- var previous = users.firstExample({ user: user });
+ var user = users.firstExample({user: username});
- if (previous === null) {
+ 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;
}
- users.remove(previous);
-
// not exports.reload() as this is an abstract method...
require("org/arangodb/users").reload();
+
+ users.remove(user);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief gets an existing user
////////////////////////////////////////////////////////////////////////////////
-exports.document = function (user) {
-
+exports.document = function (username) {
// validate name
- validateName(user);
+ validateName(username);
var users = getStorage();
- var doc = users.firstExample({ user: user });
+ var user = users.firstExample({user: username});
- if (doc === null) {
+ 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 {
- user: doc.user,
- active: doc.active,
- extra: doc.extra || {},
- changePassword: doc.changePassword
- };
+ return convertToLegacyFormat(user);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief checks whether a combination of username / password is valid.
////////////////////////////////////////////////////////////////////////////////
-exports.isValid = function (user, password) {
- var users = getStorage();
- var previous = users.firstExample({ user: user });
+exports.isValid = function (username, password) {
+ // validate name
+ validateName(username);
- if (previous === null || ! previous.active) {
+ var users = getStorage();
+ var user = users.firstExample({user: username});
+
+ if (user === null || !user.authData.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);
+ var hash = crypto[user.authData.simple.method](user.authData.simple.salt + password);
+ return crypto.constantEquals(user.authData.simple.hash, hash);
};
////////////////////////////////////////////////////////////////////////////////
@@ -354,22 +341,9 @@ exports.isValid = function (user, password) {
////////////////////////////////////////////////////////////////////////////////
exports.all = function () {
- var cursor = getStorage().all();
- var result = [ ];
+ var users = getStorage();
- 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;
+ return users.all().toArray().map(convertToLegacyFormat);
};
////////////////////////////////////////////////////////////////////////////////
@@ -409,11 +383,10 @@ exports.reload = function () {
/// @brief sets a password-change token
////////////////////////////////////////////////////////////////////////////////
-exports.setPasswordToken = function (user, token) {
+exports.setPasswordToken = function (username, token) {
var users = getStorage();
- var current = users.firstExample({ user: user });
-
- if (current === null) {
+ var user = users.firstExample({user: username});
+ if (user === null) {
return null;
}
@@ -421,7 +394,7 @@ exports.setPasswordToken = function (user, token) {
token = internal.genRandomAlphaNumbers(50);
}
- users.update(current, { passwordToken: token });
+ users.update(user, {authData: {passwordToken: token}});
return token;
};
@@ -432,13 +405,7 @@ exports.setPasswordToken = function (user, token) {
exports.userByToken = function (token) {
var users = getStorage();
- var current = users.firstExample({ passwordToken: token });
-
- if (current === null) {
- return null;
- }
-
- return current.user;
+ return users.firstExample({"authData.passwordToken": token});
};
////////////////////////////////////////////////////////////////////////////////
@@ -447,18 +414,24 @@ exports.userByToken = function (token) {
exports.changePassword = function (token, password) {
var users = getStorage();
- var current = users.firstExample({ passwordToken: token });
+ var user = users.firstExample({'authData.passwordToken': token});
- if (current === null) {
+ if (user === null) {
return false;
}
validatePassword(password);
- var hash = encodePassword(password);
+ var authData = user._shallowCopy.authData;
- users.update(current, { passwordToken: null, password: hash, changePassword: false });
- exports.reload();
+ 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("org/arangodb/users").reload();
return true;
};
diff --git a/js/server/tests/shell-foxx.js b/js/server/tests/shell-foxx.js
index f80b658b26..a5365a3652 100644
--- a/js/server/tests/shell-foxx.js
+++ b/js/server/tests/shell-foxx.js
@@ -324,6 +324,36 @@ function SetRoutesFoxxControllerSpec () {
assertEqual(error, new Error("Setup authentication first"));
},
+
+ testAddADestroySessionRoute: function () {
+ var myFunc = function () {},
+ routes = app.routingInfo.routes;
+
+ app.activateSessions({
+ sessionStorageApp: 'sessions',
+ cookieName: 'sid',
+ cookieSecret: 'secret',
+ type: 'cookie'
+ });
+ app.destroySession('/simple/route', myFunc);
+ assertEqual(routes[0].docs.httpMethod, 'POST');
+ assertEqual(routes[0].url.methods, ["post"]);
+ },
+
+ testRefuseDestroySessionWhenSessionsAreNotSetUp: function () {
+ var myFunc = function () {},
+ error;
+
+ try {
+ app.destroySession('/simple/route', myFunc);
+ } catch(e) {
+ error = e;
+ }
+
+ assertEqual(error, new Error("Setup sessions first"));
+ }
+
+
};
}
@@ -1370,6 +1400,50 @@ function SetupAuthorization () {
};
}
+function SetupSessions () {
+ var app;
+
+ return {
+ testWorksWithAllParameters: function () {
+ var err;
+
+ app = new FoxxController(fakeContext);
+
+ try {
+ app.activateSessions({
+ sessionStorageApp: 'sessions',
+ cookieName: 'sid',
+ cookieSecret: 'secret',
+ type: 'cookie'
+ });
+ } catch (e) {
+ err = e;
+ }
+
+ assertUndefined(err);
+ },
+
+ testRefusesUnknownSessionsTypes: function () {
+ var err;
+
+ app = new FoxxController(fakeContext);
+
+ try {
+ app.activateSessions({
+ sessionStorageApp: 'sessions',
+ cookieName: 'sid',
+ cookieSecret: 'secret',
+ type: 'magic'
+ });
+ } catch (e) {
+ err = e;
+ }
+
+ assertEqual(err.message, 'Only "cookie" type sessions are supported at this time.');
+ }
+ };
+}
+
function FoxxControllerWithRootElement () {
var app;
@@ -1444,6 +1518,7 @@ jsunity.run(DocumentationAndConstraintsSpec);
jsunity.run(AddMiddlewareFoxxControllerSpec);
jsunity.run(CommentDrivenDocumentationSpec);
jsunity.run(SetupAuthorization);
+jsunity.run(SetupSessions);
jsunity.run(FoxxControllerWithRootElement);
return jsunity.done();
diff --git a/js/server/upgrade-database.js b/js/server/upgrade-database.js
index 62c108087e..53502bf315 100644
--- a/js/server/upgrade-database.js
+++ b/js/server/upgrade-database.js
@@ -726,6 +726,89 @@
}
});
+////////////////////////////////////////////////////////////////////////////////
+/// @brief upgradeUserModel
+////////////////////////////////////////////////////////////////////////////////
+
+ // create a unique index on "user" attribute in _users
+ addTask({
+ name: "updateUserModel",
+ description: "convert documents in _users collection to new format",
+
+ mode: [ MODE_PRODUCTION, MODE_DEVELOPMENT ],
+ cluster: [ CLUSTER_NONE, CLUSTER_COORDINATOR_GLOBAL ],
+ database: [ DATABASE_INIT, DATABASE_UPGRADE ],
+
+ task: function () {
+ var users = getCollection("_users");
+ if (! users) {
+ return false;
+ }
+
+ var results = users.all().toArray().map(function (oldDoc) {
+ if (!oldDoc.hasOwnProperty('userData')) {
+ if (typeof oldDoc.user !== 'string') {
+ logger.error("user with _key " + oldDoc._key + " has no username");
+ return false;
+ }
+ if (typeof oldDoc.password !== 'string') {
+ logger.error("user with username " + oldDoc.user + " has no password");
+ return false;
+ }
+ var newDoc = {
+ user: oldDoc.user,
+ userData: oldDoc.extra || {},
+ authData: {
+ active: Boolean(oldDoc.active),
+ changePassword: Boolean(oldDoc.changePassword)
+ }
+ };
+ if (oldDoc.passwordToken) {
+ newDoc.authData.passwordToken = oldDoc.passwordToken;
+ }
+ var passwd = oldDoc.password.split('$');
+ if (passwd[0] !== '' || passwd.length !== 4) {
+ logger.error("user with username " + oldDoc.user + " has unexpected password format");
+ return false;
+ }
+ newDoc.authData.simple = {
+ method: 'sha256',
+ salt: passwd[2],
+ hash: passwd[3]
+ };
+ var result = users.replace(oldDoc, newDoc);
+ return !result.errors;
+ }
+ if (!oldDoc.hasOwnProperty('authData')) {
+ logger.error("user with _key " + oldDoc._key + " has no authData");
+ return false;
+ }
+ return true;
+ });
+
+ return results.every(Boolean);
+ }
+ });
+
+////////////////////////////////////////////////////////////////////////////////
+/// @brief setupSessions
+///
+/// set up the collection _sessions
+////////////////////////////////////////////////////////////////////////////////
+
+ addTask({
+ name: "setupSessions",
+ description: "setup _sessions collection",
+
+ mode: [ MODE_PRODUCTION, MODE_DEVELOPMENT ],
+ cluster: [ CLUSTER_NONE, CLUSTER_COORDINATOR_GLOBAL ],
+ database: [ DATABASE_INIT, DATABASE_UPGRADE ],
+
+ task: function () {
+ return createSystemCollection("_sessions", { waitForSync : true });
+ }
+ });
+
////////////////////////////////////////////////////////////////////////////////
/// @brief setupGraphs
///