diff --git a/docs.json b/docs.json index 52acd1f2..36765298 100644 --- a/docs.json +++ b/docs.json @@ -3,6 +3,14 @@ "content": [ "docs/api.md", "docs/api-app.md", + "lib/models/access-token.js", + "lib/models/access-context.js", + "lib/models/acl.js", + "lib/models/application.js", + "lib/models/email.js", + "lib/models/model.js", + "lib/models/role.js", + "lib/models/user.js", "docs/api-datasource.md", "docs/api-geopoint.md", "docs/api-model.md", diff --git a/lib/models/access-context.js b/lib/models/access-context.js index c3730094..fa3c8e72 100644 --- a/lib/models/access-context.js +++ b/lib/models/access-context.js @@ -6,7 +6,7 @@ var debug = require('debug')('loopback:security:access-context'); * Access context represents the context for a request to access protected * resources * - * The AccessContext instance contains the following properties: + * @class * @property {Principal[]} principals An array of principals * @property {Function} model The model class * @property {String} modelName The model name @@ -175,7 +175,7 @@ AccessContext.prototype.debug = function() { * @param {*} id The princiapl id * @param {String} [name] The principal name * @returns {Principal} - * @constructor + * @class */ function Principal(type, id, name) { if (!(this instanceof Principal)) { @@ -211,7 +211,7 @@ Principal.prototype.equals = function (p) { * @param {String} accessType The access type * @param {String} permission The permission * @returns {AccessRequest} - * @constructor + * @class */ function AccessRequest(model, property, accessType, permission) { if (!(this instanceof AccessRequest)) { diff --git a/lib/models/access-token.js b/lib/models/access-token.js index 3ea22d35..fba53357 100644 --- a/lib/models/access-token.js +++ b/lib/models/access-token.js @@ -1,4 +1,4 @@ -/** +/*! * Module Dependencies. */ @@ -14,6 +14,10 @@ var Model = require('../loopback').Model /** * Default AccessToken properties. + * + * @property id {String} - Generated token ID + * @property ttl {Number} - Time to live + * @property created {Date} - When the token was created */ var properties = { @@ -25,7 +29,15 @@ var properties = { }; /** - * Extends from the built in `loopback.Model` type. + * Token based authentication and access control. + * + * **Default ACLs** + * + * - DENY EVERYONE `*` + * - ALLOW EVERYONE create + * + * @class + * @inherits {Model} */ var AccessToken = module.exports = Model.extend('AccessToken', properties, { @@ -44,12 +56,22 @@ var AccessToken = module.exports = Model.extend('AccessToken', properties, { ] }); +/** + * Anonymous Token + * + * ```js + * assert(AccessToken.ANONYMOUS.id === '$anonymous'); + * ``` + */ + AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'}); /** * Create a cryptographically random access token id. * - * @param {Function} callback + * @callback {Function} callback + * @param {Error} err + * @param {String} token */ AccessToken.createAccessTokenId = function (fn) { @@ -85,7 +107,9 @@ AccessToken.beforeCreate = function (next, data) { * * @param {ServerRequest} req * @param {Object} [options] Options for finding the token - * @param {Function} callback Calls back with a token if one exists otherwise null or an error. + * @callback {Function} callback + * @param {Error} err + * @param {AccessToken} token */ AccessToken.findForRequest = function(req, options, cb) { @@ -116,6 +140,14 @@ AccessToken.findForRequest = function(req, options, cb) { } } +/** + * Validate the token. + * + * @callback {Function} callback + * @param {Error} err + * @param {Boolean} isValid + */ + AccessToken.prototype.validate = function(cb) { try { assert( diff --git a/lib/models/acl.js b/lib/models/acl.js index 4814c11e..772b0171 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -1,4 +1,4 @@ -/** +/*! Schema ACL options Object level permissions, for example, an album owned by a user @@ -44,7 +44,7 @@ var AccessRequest = ctx.AccessRequest; var role = require('./role'); var Role = role.Role; -/** +/*! * Schema for Scope which represents the permissions that are granted to client * applications by the resource owner */ @@ -53,7 +53,6 @@ var ScopeSchema = { description: String }; - /** * Resource owner grants/delegates permissions to client applications * @@ -61,10 +60,31 @@ var ScopeSchema = { * from the resource owner (user or system)? * * Scope has many resource access entries - * @type {createModel|*} + * @class */ var Scope = loopback.createModel('Scope', ScopeSchema); + +/** + * Check if the given scope is allowed to access the model/property + * @param {String} scope The scope name + * @param {String} model The model name + * @param {String} property The property/method/relation name + * @param {String} accessType The access type + * @callback {Function} callback + * @param {String|Error} err The error object + * @param {AccessRequest} result The access permission + */ +Scope.checkPermission = function (scope, model, property, accessType, callback) { + Scope.findOne({where: {name: scope}}, function (err, scope) { + if (err) { + callback && callback(err); + } else { + ACL.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback); + } + }); +}; + /** * System grants permissions to principals (users/applications, can be grouped * into roles). @@ -75,7 +95,6 @@ var Scope = loopback.createModel('Scope', ScopeSchema); * For a given principal, such as client application and/or user, is it allowed * to access (read/write/execute) * the protected resource? - * */ var ACLSchema = { model: String, // The name of the model @@ -105,6 +124,14 @@ var ACLSchema = { principalId: String }; +/** + * A Model for access control meta data. + * + * @header ACL + * @class + * @inherits Model + */ + var ACL = loopback.createModel('ACL', ACLSchema); ACL.ALL = AccessContext.ALL; @@ -160,7 +187,7 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) { * Resolve permission from the ACLs * @param {Object[]) acls The list of ACLs * @param {Object} req The request - * @returns {Object} The effective ACL + * @returns {AccessRequest} result The effective ACL */ ACL.resolvePermission = function resolvePermission(acls, req) { // Sort by the matching score in descending order @@ -255,7 +282,7 @@ ACL.getStaticACLs = function getStaticACLs(model, property) { * * @callback callback * @param {String|Error} err The error object - * @param {Object} the access permission + * @param {AccessRequest} result The access permission */ ACL.checkPermission = function checkPermission(principalType, principalId, model, property, accessType, @@ -309,28 +336,6 @@ ACL.prototype.debug = function() { } } -/** - * Check if the given scope is allowed to access the model/property - * @param {String} scope The scope name - * @param {String} model The model name - * @param {String} property The property/method/relation name - * @param {String} accessType The access type - * @param {Function} callback The callback function - * - * @callback callback - * @param {String|Error} err The error object - * @param {Object} the access permission - */ -Scope.checkPermission = function (scope, model, property, accessType, callback) { - Scope.findOne({where: {name: scope}}, function (err, scope) { - if (err) { - callback && callback(err); - } else { - ACL.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback); - } - }); -}; - /** * Check if the request has the permission to access * @param {Object} context @@ -412,9 +417,8 @@ ACL.checkAccess = function (context, callback) { * @param {String} model The model name * @param {*} modelId The model id * @param {String} method The method name - * @param callback The callback function - * - * @callback callback + * @end + * @callback {Function} callback * @param {String|Error} err The error object * @param {Boolean} allowed is the request allowed */ diff --git a/lib/models/application.js b/lib/models/application.js index 7568bd78..d80736fc 100644 --- a/lib/models/application.js +++ b/lib/models/application.js @@ -106,6 +106,12 @@ function generateKey(hmacKey, algorithm, encoding) { return hmac.digest('base64'); } +/** + * Manage client applications and organize their users. + * @class + * @inherits {Model} + */ + var Application = loopback.createModel('Application', ApplicationSchema); /*! @@ -150,7 +156,8 @@ Application.register = function (owner, name, options, cb) { /** * Reset keys for the application instance - * @param cb + * @callback {Function} callback + * @param {Error} err */ Application.prototype.resetKeys = function (cb) { this.clientKey = generateKey('client'); @@ -164,8 +171,9 @@ Application.prototype.resetKeys = function (cb) { /** * Reset keys for a given application by the appId - * @param appId - * @param cb + * @param {Any} appId + * @callback {Function} callback + * @param {Error} err */ Application.resetKeys = function (appId, cb) { Application.findById(appId, function (err, app) { @@ -178,10 +186,21 @@ Application.resetKeys = function (appId, cb) { }; /** + * Authenticate the application id and key. + * + * `matched` will be one of + * + * - clientKey + * - javaScriptKey + * - restApiKey + * - windowsKey + * - masterKey * - * @param appId - * @param key - * @param cb + * @param {Any} appId + * @param {String} key + * @callback {Function} callback + * @param {Error} err + * @param {String} matched - The matching key */ Application.authenticate = function (appId, key, cb) { Application.findById(appId, function (err, app) { diff --git a/lib/models/email.js b/lib/models/email.js index eb079d67..cc351c92 100644 --- a/lib/models/email.js +++ b/lib/models/email.js @@ -1,14 +1,10 @@ -/** +/*! * Module Dependencies. */ var Model = require('../loopback').Model , loopback = require('../loopback'); -/** - * Default Email properties. - */ - var properties = { to: {type: String, required: true}, from: {type: String, required: true}, @@ -18,7 +14,43 @@ var properties = { }; /** - * Extends from the built in `loopback.Model` type. + * The Email Model. + * + * **Properties** + * + * - `to` - **{ String }** **required** + * - `from` - **{ String }** **required** + * - `subject` - **{ String }** **required** + * - `text` - **{ String }** + * - `html` - **{ String }** + * + * @class + * @inherits {Model} */ var Email = module.exports = Model.extend('Email', properties); + +/** + * Send an email with the given `options`. + * + * Example Options: + * + * ```json + * { + * from: "Fred Foo ✔ ", // sender address + * to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers + * subject: "Hello ✔", // Subject line + * text: "Hello world ✔", // plaintext body + * html: "Hello world ✔" // html body + * } + * ``` + * + * See https://github.com/andris9/Nodemailer for other supported options. + * + * @param {Object} options + * @param {Function} callback Called after the e-mail is sent or the sending failed + */ + +Email.prototype.send = function() { + throw new Error('You must connect the Email Model to a Mail connector'); +} \ No newline at end of file diff --git a/lib/models/model.js b/lib/models/model.js index c0a98817..ce2bb5f9 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -1,4 +1,4 @@ -/** +/*! * Module Dependencies. */ var loopback = require('../loopback'); @@ -7,7 +7,10 @@ var modeler = new ModelBuilder(); var assert = require('assert'); /** - * Define the built in loopback.Model. + * The built in loopback.Model. + * + * @class + * @param {Object} data */ var Model = module.exports = modeler.define('Model'); @@ -124,7 +127,7 @@ function getACL() { * @param {String} method The method name * @param callback The callback function * - * @callback callback + * @callback {Function} callback * @param {String|Error} err The error object * @param {Boolean} allowed is the request allowed */ @@ -136,7 +139,7 @@ Model.checkAccess = function(token, modelId, method, callback) { ACL.checkAccessForToken(token, this.modelName, modelId, methodName, callback); }; -/** +/*! * Determine the access type for the given `RemoteMethod`. * * @api private diff --git a/lib/models/role.js b/lib/models/role.js index adc544c6..a76c585a 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -26,6 +26,10 @@ var RoleMappingSchema = { principalId: String // The principal id }; +/** + * Map Roles to + */ + var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, { relations: { role: { @@ -43,7 +47,9 @@ RoleMapping.ROLE = 'ROLE'; /** * Get the application principal - * @param callback + * @callback {Function} callback + * @param {Error} err + * @param {Application} application */ RoleMapping.prototype.application = function (callback) { if (this.principalType === RoleMapping.APPLICATION) { @@ -57,7 +63,9 @@ RoleMapping.prototype.application = function (callback) { /** * Get the user principal - * @param callback + * @callback {Function} callback + * @param {Error} err + * @param {User} user */ RoleMapping.prototype.user = function (callback) { if (this.principalType === RoleMapping.USER) { @@ -71,7 +79,9 @@ RoleMapping.prototype.user = function (callback) { /** * Get the child role principal - * @param callback + * @callback {Function} callback + * @param {Error} err + * @param {User} childUser */ RoleMapping.prototype.childRole = function (callback) { if (this.principalType === RoleMapping.ROLE) { @@ -84,7 +94,8 @@ RoleMapping.prototype.childRole = function (callback) { }; /** - * Define the Role model with `hasMany` relation to RoleMapping + * The Role Model + * @class */ var Role = loopback.createModel('Role', RoleSchema, { relations: { @@ -249,7 +260,9 @@ Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) { /** * Check if the user id is authenticated * @param {Object} context The security context - * @param {Function} callback The callback function + * @callback {Function} callback + * @param {Error} err + * @param {Boolean} isAuthenticated */ Role.isAuthenticated = function isAuthenticated(context, callback) { process.nextTick(function() { @@ -274,7 +287,9 @@ Role.registerResolver(Role.EVERYONE, function (role, context, callback) { * * @param {String} role The role name * @param {Object} context The context object - * @param {Function} callback + * @callback {Function} callback + * @param {Error} err + * @param {Boolean} isInRole */ Role.isInRole = function (role, context, callback) { debug('isInRole(): %s %j', role, context); @@ -355,7 +370,7 @@ Role.isInRole = function (role, context, callback) { * @param {Object} context The security context * @param {Function} callback * - * @callback callback + * @callback {Function} callback * @param err * @param {String[]} An array of role ids */ diff --git a/lib/models/user.js b/lib/models/user.js index 4762c0c8..286b5b98 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -44,11 +44,7 @@ var properties = { status: String, created: Date, lastUpdated: Date -} - -/** - * Default User options. - */ +}; var options = { acls: [ @@ -98,6 +94,19 @@ var options = { /** * Extends from the built in `loopback.Model` type. + * + * Default `User` ACLs. + * + * - DENY EVERYONE `*` + * - ALLOW EVERYONE `create` + * - ALLOW OWNER `removeById` + * - ALLOW EVERYONE `login` + * - ALLOW EVERYONE `logout` + * - ALLOW EVERYONE `findById` + * - ALLOW OWNER `updateAttributes` + * + * @class + * @inherits {Model} */ var User = module.exports = Model.extend('User', properties, options); @@ -105,11 +114,16 @@ var User = module.exports = Model.extend('User', properties, options); /** * Login a user by with the given `credentials`. * + * ```js * User.login({username: 'foo', password: 'bar'}, function (err, token) { * console.log(token.id); * }); + * ``` * * @param {Object} credentials + * @callback {Function} callback + * @param {Error} err + * @param {AccessToken} token */ User.login = function (credentials, fn) { @@ -150,11 +164,15 @@ User.login = function (credentials, fn) { /** * Logout a user with the given accessToken id. * + * ```js * User.logout('asd0a9f8dsj9s0s3223mk', function (err) { * console.log(err || 'Logged out'); * }); + * ``` * * @param {String} accessTokenID + * @callback {Function} callback + * @param {Error} err */ User.logout = function (tokenId, fn) { @@ -188,8 +206,9 @@ User.prototype.hasPassword = function (plain, fn) { } /** - * Verify a user's identity. + * Verify a user's identity by sending them a confirmation email. * + * ```js * var options = { * type: 'email', * to: user.email, @@ -198,6 +217,7 @@ User.prototype.hasPassword = function (plain, fn) { * }; * * user.verify(options, next); + * ``` * * @param {Object} options */ @@ -266,6 +286,16 @@ User.prototype.verify = function (options, fn) { } } + +/** + * Confirm the user's identity. + * + * @param {Any} userId + * @param {String} token The validation token + * @param {String} redirect URL to redirect the user to once confirmed + * @callback {Function} callback + * @param {Error} err + */ User.confirm = function (uid, token, redirect, fn) { this.findById(uid, function (err, user) { if(err) { @@ -288,6 +318,16 @@ User.confirm = function (uid, token, redirect, fn) { }); } +/** + * Create a short lived acess token for temporary login. Allows users + * to change passwords if forgotten. + * + * @options {Object} options + * @prop {String} email The user's email address + * @callback {Function} callback + * @param {Error} err + */ + User.resetPassword = function(options, cb) { var UserModel = this; var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; @@ -323,7 +363,7 @@ User.resetPassword = function(options, cb) { } } -/** +/*! * Setup an extended user model. */