Merge remote-tracking branch 'origin/production' into production

This commit is contained in:
Miroslav Bajtoš 2014-01-06 18:52:31 +01:00
commit 05dd7bc84b
31 changed files with 1242 additions and 385 deletions

View File

@ -3,6 +3,14 @@
"content": [ "content": [
"docs/api.md", "docs/api.md",
"docs/api-app.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-datasource.md",
"docs/api-geopoint.md", "docs/api-geopoint.md",
"docs/api-model.md", "docs/api-model.md",

View File

@ -58,6 +58,7 @@ Initialize an application from an options object or a set of JSON and JavaScript
1. **DataSources** are created from an `options.dataSources` object or `datasources.json` in the current directory 1. **DataSources** are created from an `options.dataSources` object or `datasources.json` in the current directory
2. **Models** are created from an `options.models` object or `models.json` in the current directory 2. **Models** are created from an `options.models` object or `models.json` in the current directory
3. Any JavaScript files in the `./models` directory are loaded with `require()`. 3. Any JavaScript files in the `./models` directory are loaded with `require()`.
4. Any JavaScript files in the `./boot` directory are loaded with `require()`.
**Options** **Options**

View File

@ -310,10 +310,18 @@ User.find({
**Note:** See the specific connector's [docs](#connectors) for more info. **Note:** See the specific connector's [docs](#connectors) for more info.
### Model.destroyAll(callback) ### Model.destroyAll([where], callback)
Delete all Model instances from data source. **Note:** destroyAll method does not perform destroy hooks. Delete all Model instances from data source. **Note:** destroyAll method does not perform destroy hooks.
```js
Product.destroyAll({price: {gt: 99}}, function(err) {
// removed matching products
});
```
> **NOTE:* `where` is optional and a where object... do NOT pass a filter object
### Model.findById(id, callback) ### Model.findById(id, callback)
Find instance by id. Find instance by id.

View File

@ -290,6 +290,7 @@ app.boot = function(options) {
// require directories // require directories
var requiredModels = requireDir(path.join(appRootDir, 'models')); var requiredModels = requireDir(path.join(appRootDir, 'models'));
var requiredBootScripts = requireDir(path.join(appRootDir, 'boot'));
} }
function assertIsValidConfig(name, config) { function assertIsValidConfig(name, config) {

View File

@ -1,6 +1,7 @@
# Application # Application
Application model captures the metadata for a loopback application. Application model represents the metadata for a client application that has its
own identity and associated configuration with the LoopBack server.
## Each application has the following basic properties: ## Each application has the following basic properties:
@ -25,7 +26,8 @@ Application model captures the metadata for a loopback application.
## Security keys ## Security keys
The following keys are automatically generated by the application creation process. They can be reset upon request. The following keys are automatically generated by the application creation
process. They can be reset upon request.
* clientKey: Secret for mobile clients * clientKey: Secret for mobile clients
* javaScriptKey: Secret for JavaScript clients * javaScriptKey: Secret for JavaScript clients
@ -40,48 +42,50 @@ The application can be configured to support multiple methods of push notificati
* pushSettings * pushSettings
{ pushSettings: {
pushSettings: [ apns: {
{ "platform": "apns", certData: config.apnsCertData,
"apns": { keyData: config.apnsKeyData,
"pushOptions": { production: false, // Development mode
"gateway": "gateway.sandbox.push.apple.com", pushOptions: {
"cert": "credentials/apns_cert_dev.pem", // Extra options can go here for APN
"key": "credentials/apns_key_dev.pem"
}, },
feedbackOptions: {
"feedbackOptions": { batchFeedback: true,
"gateway": "feedback.sandbox.push.apple.com", interval: 300
"cert": "credentials/apns_cert_dev.pem", }
"key": "credentials/apns_key_dev.pem", },
"batchFeedback": true, gcm: {
"interval": 300 serverApiKey: config.gcmServerApiKey
}
} }
}}
]}
## Authentication schemes ## Authentication schemes
* authenticationEnabled * authenticationEnabled
* anonymousAllowed * anonymousAllowed
* authenticationSchemes * authenticationSchemes
### Authentication scheme settings ### Authentication scheme settings
* scheme: Name of the authentication scheme, such as local, facebook, google, twitter, linkedin, github * scheme: Name of the authentication scheme, such as local, facebook, google,
twitter, linkedin, github
* credential: Scheme-specific credentials * credential: Scheme-specific credentials
## APIs for Application model ## APIs for Application model
In addition to the CRUD methods, the Application model also has the following apis: In addition to the CRUD methods, the Application model also has the following
apis:
### Register a new application ### Register a new application
You can register a new application by providing the owner user id, applicaiton name, and other properties in the options object. You can register a new application by providing the owner user id, application
name, and other properties in the options object.
Application.register('rfeng', 'MyApp1', {description: 'My first loopback application'}, function (err, result) { Application.register('rfeng', 'MyApp1',
{description: 'My first loopback application'},
function (err, result) {
var app = result; var app = result;
... ...
}); });
@ -97,7 +101,9 @@ You can reset keys for a given application by id.
### Authenticate by appId and key ### Authenticate by appId and key
You can authenticate an application by id and one of the keys. If successful, it calls back with the key name in the result argument. Otherwise, the keyName is null. You can authenticate an application by id and one of the keys. If successful,
it calls back with the key name in the result argument. Otherwise, the
keyName is null.
Application.authenticate(appId, clientKey, function (err, keyName) { Application.authenticate(appId, clientKey, function (err, keyName) {
assert.equal(keyName, 'clientKey'); assert.equal(keyName, 'clientKey');
@ -105,21 +111,3 @@ You can authenticate an application by id and one of the keys. If successful, it
}); });
# Installation
Installation captures the installation of the application on devices.
Each record of installation has the following properties:
* id: Generated id that uniquely identifies the installation
* appId: Application id
* appVersion: Application version
* userId: The current user id that logs into the application
* deviceToken: Device token
* deviceType: Device type, such as apns
* subscriptions: An Array of tags that represents subscriptions of notifications
* status: Status of the application, production/sandbox/disabled
* created: Timestamp of the recored being created
* modified: Timestamp of the recored being modified

View File

@ -1,11 +1,12 @@
var loopback = require('../loopback'); var loopback = require('../loopback');
var AccessToken = require('./access-token'); var AccessToken = require('./access-token');
var debug = require('debug')('loopback:security:access-context');
/** /**
* Access context represents the context for a request to access protected * Access context represents the context for a request to access protected
* resources * resources
* *
* The AccessContext instance contains the following properties: * @class
* @property {Principal[]} principals An array of principals * @property {Principal[]} principals An array of principals
* @property {Function} model The model class * @property {Function} model The model class
* @property {String} modelName The model name * @property {String} modelName The model name
@ -95,6 +96,8 @@ AccessContext.prototype.addPrincipal = function (principalType, principalId, pri
} }
} }
this.principals.push(principal); this.principals.push(principal);
debug('adding principal %j', principal);
return true; return true;
}; };
@ -135,6 +138,36 @@ AccessContext.prototype.isAuthenticated = function() {
return !!(this.getUserId() || this.getAppId()); return !!(this.getUserId() || this.getAppId());
}; };
/**
* Print debug info for access context.
*/
AccessContext.prototype.debug = function() {
if(debug.enabled) {
debug('---AccessContext---');
if(this.principals && this.principals.length) {
debug('principals:')
this.principals.forEach(function(principal) {
debug('principal: %j', principal)
});
} else {
debug('principals: %j', this.principals);
}
debug('modelName %s', this.modelName);
debug('modelId %s', this.modelId);
debug('property %s', this.property);
debug('method %s', this.method);
debug('accessType %s', this.accessType);
if(this.accessToken) {
debug('accessToken:')
debug(' id %j', this.accessToken.id);
debug(' ttl %j', this.accessToken.ttl);
}
debug('getUserId() %s', this.getUserId());
debug('isAuthenticated() %s', this.isAuthenticated());
}
}
/** /**
* This class represents the abstract notion of a principal, which can be used * This class represents the abstract notion of a principal, which can be used
* to represent any entity, such as an individual, a corporation, and a login id * to represent any entity, such as an individual, a corporation, and a login id
@ -142,7 +175,7 @@ AccessContext.prototype.isAuthenticated = function() {
* @param {*} id The princiapl id * @param {*} id The princiapl id
* @param {String} [name] The principal name * @param {String} [name] The principal name
* @returns {Principal} * @returns {Principal}
* @constructor * @class
*/ */
function Principal(type, id, name) { function Principal(type, id, name) {
if (!(this instanceof Principal)) { if (!(this instanceof Principal)) {
@ -178,7 +211,7 @@ Principal.prototype.equals = function (p) {
* @param {String} accessType The access type * @param {String} accessType The access type
* @param {String} permission The permission * @param {String} permission The permission
* @returns {AccessRequest} * @returns {AccessRequest}
* @constructor * @class
*/ */
function AccessRequest(model, property, accessType, permission) { function AccessRequest(model, property, accessType, permission) {
if (!(this instanceof AccessRequest)) { if (!(this instanceof AccessRequest)) {
@ -188,6 +221,15 @@ function AccessRequest(model, property, accessType, permission) {
this.property = property || AccessContext.ALL; this.property = property || AccessContext.ALL;
this.accessType = accessType || AccessContext.ALL; this.accessType = accessType || AccessContext.ALL;
this.permission = permission || AccessContext.DEFAULT; this.permission = permission || AccessContext.DEFAULT;
if(debug.enabled) {
debug('---AccessRequest---');
debug(' model %s', this.model);
debug(' property %s', this.property);
debug(' accessType %s', this.accessType);
debug(' permission %s', this.permission);
debug(' isWildcard() %s', this.isWildcard());
}
} }
/** /**

View File

@ -1,4 +1,4 @@
/** /*!
* Module Dependencies. * Module Dependencies.
*/ */
@ -14,6 +14,10 @@ var Model = require('../loopback').Model
/** /**
* Default AccessToken properties. * 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 = { 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, { 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'}); AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'});
/** /**
* Create a cryptographically random access token id. * Create a cryptographically random access token id.
* *
* @param {Function} callback * @callback {Function} callback
* @param {Error} err
* @param {String} token
*/ */
AccessToken.createAccessTokenId = function (fn) { AccessToken.createAccessTokenId = function (fn) {
@ -85,7 +107,9 @@ AccessToken.beforeCreate = function (next, data) {
* *
* @param {ServerRequest} req * @param {ServerRequest} req
* @param {Object} [options] Options for finding the token * @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) { 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) { AccessToken.prototype.validate = function(cb) {
try { try {
assert( assert(

View File

@ -1,4 +1,4 @@
/** /*!
Schema ACL options Schema ACL options
Object level permissions, for example, an album owned by a user 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 = require('./role');
var Role = role.Role; var Role = role.Role;
/** /*!
* Schema for Scope which represents the permissions that are granted to client * Schema for Scope which represents the permissions that are granted to client
* applications by the resource owner * applications by the resource owner
*/ */
@ -53,7 +53,6 @@ var ScopeSchema = {
description: String description: String
}; };
/** /**
* Resource owner grants/delegates permissions to client applications * Resource owner grants/delegates permissions to client applications
* *
@ -61,10 +60,31 @@ var ScopeSchema = {
* from the resource owner (user or system)? * from the resource owner (user or system)?
* *
* Scope has many resource access entries * Scope has many resource access entries
* @type {createModel|*} * @class
*/ */
var Scope = loopback.createModel('Scope', ScopeSchema); 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 * System grants permissions to principals (users/applications, can be grouped
* into roles). * 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 * For a given principal, such as client application and/or user, is it allowed
* to access (read/write/execute) * to access (read/write/execute)
* the protected resource? * the protected resource?
*
*/ */
var ACLSchema = { var ACLSchema = {
model: String, // The name of the model model: String, // The name of the model
@ -105,6 +124,14 @@ var ACLSchema = {
principalId: String principalId: String
}; };
/**
* A Model for access control meta data.
*
* @header ACL
* @class
* @inherits Model
*/
var ACL = loopback.createModel('ACL', ACLSchema); var ACL = loopback.createModel('ACL', ACLSchema);
ACL.ALL = AccessContext.ALL; ACL.ALL = AccessContext.ALL;
@ -160,10 +187,9 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
* Resolve permission from the ACLs * Resolve permission from the ACLs
* @param {Object[]) acls The list of ACLs * @param {Object[]) acls The list of ACLs
* @param {Object} req The request * @param {Object} req The request
* @returns {Object} The effective ACL * @returns {AccessRequest} result The effective ACL
*/ */
ACL.resolvePermission = function resolvePermission(acls, req) { ACL.resolvePermission = function resolvePermission(acls, req) {
debug('resolvePermission(): %j %j', acls, req);
// Sort by the matching score in descending order // Sort by the matching score in descending order
acls = acls.sort(function (rule1, rule2) { acls = acls.sort(function (rule1, rule2) {
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req); return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
@ -198,7 +224,6 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
var res = new AccessRequest(req.model, req.property, req.accessType, var res = new AccessRequest(req.model, req.property, req.accessType,
permission || ACL.DEFAULT); permission || ACL.DEFAULT);
debug('resolvePermission() returns: %j', res);
return res; return res;
}; };
@ -210,7 +235,6 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
* @return {Object[]} An array of ACLs * @return {Object[]} An array of ACLs
*/ */
ACL.getStaticACLs = function getStaticACLs(model, property) { ACL.getStaticACLs = function getStaticACLs(model, property) {
debug('getStaticACLs(): %s %s', model, property);
var modelClass = loopback.getModel(model); var modelClass = loopback.getModel(model);
var staticACLs = []; var staticACLs = [];
if (modelClass && modelClass.settings.acls) { if (modelClass && modelClass.settings.acls) {
@ -223,6 +247,8 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
accessType: acl.accessType, accessType: acl.accessType,
permission: acl.permission permission: acl.permission
})); }));
staticACLs[staticACLs.length - 1].debug('Adding ACL');
}); });
} }
var prop = modelClass && var prop = modelClass &&
@ -242,7 +268,6 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
})); }));
}); });
} }
debug('getStaticACLs() returns: %j', staticACLs);
return staticACLs; return staticACLs;
}; };
@ -257,13 +282,11 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
* *
* @callback callback * @callback callback
* @param {String|Error} err The error object * @param {String|Error} err The error object
* @param {Object} the access permission * @param {AccessRequest} result The access permission
*/ */
ACL.checkPermission = function checkPermission(principalType, principalId, ACL.checkPermission = function checkPermission(principalType, principalId,
model, property, accessType, model, property, accessType,
callback) { callback) {
debug('checkPermission(): %s %s %s %s %s', principalType, principalId, model,
property, accessType);
property = property || ACL.ALL; property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]}; var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL; accessType = accessType || ACL.ALL;
@ -276,8 +299,8 @@ ACL.checkPermission = function checkPermission(principalType, principalId,
var resolved = ACL.resolvePermission(acls, req); var resolved = ACL.resolvePermission(acls, req);
if(resolved && resolved.permission === ACL.DENY) { if(resolved && resolved.permission === ACL.DENY) {
// Fail fast debug('Permission denied by statically resolved permission');
debug('checkPermission(): %j', resolved); debug(' Resolved Permission: %j', resolved);
process.nextTick(function() { process.nextTick(function() {
callback && callback(null, resolved); callback && callback(null, resolved);
}); });
@ -297,32 +320,21 @@ ACL.checkPermission = function checkPermission(principalType, principalId,
var modelClass = loopback.getModel(model); var modelClass = loopback.getModel(model);
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW; resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
} }
debug('checkPermission(): %j', resolved);
callback && callback(null, resolved); callback && callback(null, resolved);
}); });
}; };
/** ACL.prototype.debug = function() {
* Check if the given scope is allowed to access the model/property if(debug.enabled) {
* @param {String} scope The scope name debug('---ACL---')
* @param {String} model The model name debug('model %s', this.model);
* @param {String} property The property/method/relation name debug('property %s', this.property);
* @param {String} accessType The access type debug('principalType %s', this.principalType);
* @param {Function} callback The callback function debug('principalId %s', this.principalId);
* debug('accessType %s', this.accessType);
* @callback callback debug('permission %s', this.permission);
* @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 * Check if the request has the permission to access
@ -335,8 +347,6 @@ Scope.checkPermission = function (scope, model, property, accessType, callback)
* @param {Function} callback * @param {Function} callback
*/ */
ACL.checkAccess = function (context, callback) { ACL.checkAccess = function (context, callback) {
debug('checkAccess(): %j', context);
if(!(context instanceof AccessContext)) { if(!(context instanceof AccessContext)) {
context = new AccessContext(context); context = new AccessContext(context);
} }
@ -407,14 +417,12 @@ ACL.checkAccess = function (context, callback) {
* @param {String} model The model name * @param {String} model The model name
* @param {*} modelId The model id * @param {*} modelId The model id
* @param {String} method The method name * @param {String} method The method name
* @param callback The callback function * @end
* * @callback {Function} callback
* @callback callback
* @param {String|Error} err The error object * @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed * @param {Boolean} allowed is the request allowed
*/ */
ACL.checkAccessForToken = function (token, model, modelId, method, callback) { ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
debug('checkAccessForToken(): %j %s %s %s', token, model, modelId, method);
assert(token, 'Access token is required'); assert(token, 'Access token is required');
var context = new AccessContext({ var context = new AccessContext({
@ -427,12 +435,13 @@ ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
context.accessType = context.model._getAccessTypeForMethod(method); context.accessType = context.model._getAccessTypeForMethod(method);
context.debug();
ACL.checkAccess(context, function (err, access) { ACL.checkAccess(context, function (err, access) {
if (err) { if (err) {
callback && callback(err); callback && callback(err);
return; return;
} }
debug('checkAccessForToken(): %j', access);
callback && callback(null, access.permission !== ACL.DENY); callback && callback(null, access.permission !== ACL.DENY);
}); });
}; };

View File

@ -7,27 +7,43 @@ var AuthenticationSchemeSchema = {
credential: Object // Scheme-specific credentials credential: Object // Scheme-specific credentials
}; };
// See https://github.com/argon/node-apn/blob/master/doc/apn.markdown
var APNSSettingSchema = { var APNSSettingSchema = {
/**
* production or development mode. It denotes what default APNS servers to be
* used to send notifications
* - true (production mode)
* - push: gateway.push.apple.com:2195
* - feedback: feedback.push.apple.com:2196
* - false (development mode, the default)
* - push: gateway.sandbox.push.apple.com:2195
* - feedback: feedback.sandbox.push.apple.com:2196
*/
production: Boolean,
certData: String, // The certificate data loaded from the cert.pem file
keyData: String, // The key data loaded from the key.pem file
pushOptions: {type: { pushOptions: {type: {
gateway: String, gateway: String,
cert: String, port: Number
key: String
}}, }},
feedbackOptions: {type: { feedbackOptions: {type: {
gateway: String, gateway: String,
cert: String, port: Number,
key: String,
batchFeedback: Boolean, batchFeedback: Boolean,
interval: Number interval: Number
}} }}
}; };
var GcmSettingsSchema = {
serverApiKey: String
};
// Push notification settings // Push notification settings
var PushNotificationSettingSchema = { var PushNotificationSettingSchema = {
platform: {type: String, required: true}, // apns, gcm, mpns apns: APNSSettingSchema,
// configuration: {type: Object} // platform-specific configurations gcm: GcmSettingsSchema
apns: APNSSettingSchema
}; };
/** /**
@ -74,7 +90,6 @@ var ApplicationSchema = {
modified: {type: Date, default: Date} modified: {type: Date, default: Date}
}; };
/** /**
* Application management functions * Application management functions
*/ */
@ -91,6 +106,12 @@ function generateKey(hmacKey, algorithm, encoding) {
return hmac.digest('base64'); return hmac.digest('base64');
} }
/**
* Manage client applications and organize their users.
* @class
* @inherits {Model}
*/
var Application = loopback.createModel('Application', ApplicationSchema); var Application = loopback.createModel('Application', ApplicationSchema);
/*! /*!
@ -135,7 +156,8 @@ Application.register = function (owner, name, options, cb) {
/** /**
* Reset keys for the application instance * Reset keys for the application instance
* @param cb * @callback {Function} callback
* @param {Error} err
*/ */
Application.prototype.resetKeys = function (cb) { Application.prototype.resetKeys = function (cb) {
this.clientKey = generateKey('client'); this.clientKey = generateKey('client');
@ -149,8 +171,9 @@ Application.prototype.resetKeys = function(cb) {
/** /**
* Reset keys for a given application by the appId * Reset keys for a given application by the appId
* @param appId * @param {Any} appId
* @param cb * @callback {Function} callback
* @param {Error} err
*/ */
Application.resetKeys = function (appId, cb) { Application.resetKeys = function (appId, cb) {
Application.findById(appId, function (err, app) { Application.findById(appId, function (err, app) {
@ -163,10 +186,21 @@ Application.resetKeys = function(appId, cb) {
}; };
/** /**
* Authenticate the application id and key.
* *
* @param appId * `matched` will be one of
* @param key *
* @param cb * - clientKey
* - javaScriptKey
* - restApiKey
* - windowsKey
* - masterKey
*
* @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.authenticate = function (appId, key, cb) {
Application.findById(appId, function (err, app) { Application.findById(appId, function (err, app) {

View File

@ -1,14 +1,10 @@
/** /*!
* Module Dependencies. * Module Dependencies.
*/ */
var Model = require('../loopback').Model var Model = require('../loopback').Model
, loopback = require('../loopback'); , loopback = require('../loopback');
/**
* Default Email properties.
*/
var properties = { var properties = {
to: {type: String, required: true}, to: {type: String, required: true},
from: {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); var Email = module.exports = Model.extend('Email', properties);
/**
* Send an email with the given `options`.
*
* Example Options:
*
* ```json
* {
* from: "Fred Foo ✔ <foo@blurdybloop.com>", // sender address
* to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers
* subject: "Hello ✔", // Subject line
* text: "Hello world ✔", // plaintext body
* html: "<b>Hello world ✔</b>" // 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');
}

View File

@ -1,4 +1,4 @@
/** /*!
* Module Dependencies. * Module Dependencies.
*/ */
var loopback = require('../loopback'); var loopback = require('../loopback');
@ -7,7 +7,10 @@ var modeler = new ModelBuilder();
var assert = require('assert'); 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'); var Model = module.exports = modeler.define('Model');
@ -124,7 +127,7 @@ function getACL() {
* @param {String} method The method name * @param {String} method The method name
* @param callback The callback function * @param callback The callback function
* *
* @callback callback * @callback {Function} callback
* @param {String|Error} err The error object * @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed * @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); ACL.checkAccessForToken(token, this.modelName, modelId, methodName, callback);
}; };
/** /*!
* Determine the access type for the given `RemoteMethod`. * Determine the access type for the given `RemoteMethod`.
* *
* @api private * @api private

View File

@ -26,6 +26,10 @@ var RoleMappingSchema = {
principalId: String // The principal id principalId: String // The principal id
}; };
/**
* Map Roles to
*/
var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, { var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, {
relations: { relations: {
role: { role: {
@ -43,7 +47,9 @@ RoleMapping.ROLE = 'ROLE';
/** /**
* Get the application principal * Get the application principal
* @param callback * @callback {Function} callback
* @param {Error} err
* @param {Application} application
*/ */
RoleMapping.prototype.application = function (callback) { RoleMapping.prototype.application = function (callback) {
if (this.principalType === RoleMapping.APPLICATION) { if (this.principalType === RoleMapping.APPLICATION) {
@ -57,7 +63,9 @@ RoleMapping.prototype.application = function (callback) {
/** /**
* Get the user principal * Get the user principal
* @param callback * @callback {Function} callback
* @param {Error} err
* @param {User} user
*/ */
RoleMapping.prototype.user = function (callback) { RoleMapping.prototype.user = function (callback) {
if (this.principalType === RoleMapping.USER) { if (this.principalType === RoleMapping.USER) {
@ -71,7 +79,9 @@ RoleMapping.prototype.user = function (callback) {
/** /**
* Get the child role principal * Get the child role principal
* @param callback * @callback {Function} callback
* @param {Error} err
* @param {User} childUser
*/ */
RoleMapping.prototype.childRole = function (callback) { RoleMapping.prototype.childRole = function (callback) {
if (this.principalType === RoleMapping.ROLE) { 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, { var Role = loopback.createModel('Role', RoleSchema, {
relations: { relations: {
@ -249,7 +260,9 @@ Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
/** /**
* Check if the user id is authenticated * Check if the user id is authenticated
* @param {Object} context The security context * @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) { Role.isAuthenticated = function isAuthenticated(context, callback) {
process.nextTick(function() { process.nextTick(function() {
@ -274,7 +287,9 @@ Role.registerResolver(Role.EVERYONE, function (role, context, callback) {
* *
* @param {String} role The role name * @param {String} role The role name
* @param {Object} context The context object * @param {Object} context The context object
* @param {Function} callback * @callback {Function} callback
* @param {Error} err
* @param {Boolean} isInRole
*/ */
Role.isInRole = function (role, context, callback) { Role.isInRole = function (role, context, callback) {
debug('isInRole(): %s %j', role, context); debug('isInRole(): %s %j', role, context);
@ -355,7 +370,7 @@ Role.isInRole = function (role, context, callback) {
* @param {Object} context The security context * @param {Object} context The security context
* @param {Function} callback * @param {Function} callback
* *
* @callback callback * @callback {Function} callback
* @param err * @param err
* @param {String[]} An array of role ids * @param {String[]} An array of role ids
*/ */

View File

@ -44,14 +44,15 @@ var properties = {
status: String, status: String,
created: Date, created: Date,
lastUpdated: Date lastUpdated: Date
} };
/**
* Default User options.
*/
var options = { var options = {
acls: [ acls: [
{
principalType: ACL.ROLE,
principalId: Role.EVERYONE,
permission: ACL.DENY,
},
{ {
principalType: ACL.ROLE, principalType: ACL.ROLE,
principalId: Role.EVERYONE, principalId: Role.EVERYONE,
@ -63,12 +64,49 @@ var options = {
principalId: Role.OWNER, principalId: Role.OWNER,
permission: ACL.ALLOW, permission: ACL.ALLOW,
property: 'removeById' property: 'removeById'
},
{
principalType: ACL.ROLE,
principalId: Role.EVERYONE,
permission: ACL.ALLOW,
property: "login"
},
{
principalType: ACL.ROLE,
principalId: Role.EVERYONE,
permission: ACL.ALLOW,
property: "logout"
},
{
principalType: ACL.ROLE,
principalId: Role.OWNER,
permission: ACL.ALLOW,
property: "findById"
},
{
principalType: ACL.ROLE,
principalId: Role.OWNER,
permission: ACL.ALLOW,
property: "updateAttributes"
} }
] ]
}; };
/** /**
* Extends from the built in `loopback.Model` type. * 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); var User = module.exports = Model.extend('User', properties, options);
@ -76,11 +114,16 @@ var User = module.exports = Model.extend('User', properties, options);
/** /**
* Login a user by with the given `credentials`. * Login a user by with the given `credentials`.
* *
* ```js
* User.login({username: 'foo', password: 'bar'}, function (err, token) { * User.login({username: 'foo', password: 'bar'}, function (err, token) {
* console.log(token.id); * console.log(token.id);
* }); * });
* ```
* *
* @param {Object} credentials * @param {Object} credentials
* @callback {Function} callback
* @param {Error} err
* @param {AccessToken} token
*/ */
User.login = function (credentials, fn) { User.login = function (credentials, fn) {
@ -121,11 +164,15 @@ User.login = function (credentials, fn) {
/** /**
* Logout a user with the given accessToken id. * Logout a user with the given accessToken id.
* *
* ```js
* User.logout('asd0a9f8dsj9s0s3223mk', function (err) { * User.logout('asd0a9f8dsj9s0s3223mk', function (err) {
* console.log(err || 'Logged out'); * console.log(err || 'Logged out');
* }); * });
* ```
* *
* @param {String} accessTokenID * @param {String} accessTokenID
* @callback {Function} callback
* @param {Error} err
*/ */
User.logout = function (tokenId, fn) { User.logout = function (tokenId, fn) {
@ -159,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 = { * var options = {
* type: 'email', * type: 'email',
* to: user.email, * to: user.email,
@ -169,6 +217,7 @@ User.prototype.hasPassword = function (plain, fn) {
* }; * };
* *
* user.verify(options, next); * user.verify(options, next);
* ```
* *
* @param {Object} options * @param {Object} options
*/ */
@ -237,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) { User.confirm = function (uid, token, redirect, fn) {
this.findById(uid, function (err, user) { this.findById(uid, function (err, user) {
if(err) { if(err) {
@ -259,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) { User.resetPassword = function(options, cb) {
var UserModel = this; var UserModel = this;
var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
@ -294,7 +363,7 @@ User.resetPassword = function(options, cb) {
} }
} }
/** /*!
* Setup an extended user model. * Setup an extended user model.
*/ */
@ -327,7 +396,13 @@ User.setup = function () {
UserModel.logout, UserModel.logout,
{ {
accepts: [ accepts: [
{arg: 'sid', type: 'string', required: true} {arg: 'access_token', type: 'string', required: true, http: function(ctx) {
var req = ctx && ctx.req;
var accessToken = req && req.accessToken;
var tokenID = accessToken && accessToken.id;
return tokenID;
}}
], ],
http: {verb: 'all'} http: {verb: 'all'}
} }

View File

@ -9,7 +9,7 @@
"Platform", "Platform",
"mBaaS" "mBaaS"
], ],
"version": "1.3.4", "version": "1.4.1",
"scripts": { "scripts": {
"test": "mocha -R spec" "test": "mocha -R spec"
}, },
@ -29,14 +29,15 @@
"async": "~0.2.9" "async": "~0.2.9"
}, },
"peerDependencies": { "peerDependencies": {
"loopback-datasource-juggler": "~1.2.0" "loopback-datasource-juggler": "~1.2.11"
}, },
"devDependencies": { "devDependencies": {
"loopback-datasource-juggler": "~1.2.0", "loopback-datasource-juggler": "~1.2.11",
"mocha": "~1.14.0", "mocha": "~1.14.0",
"strong-task-emitter": "0.0.x", "strong-task-emitter": "0.0.x",
"supertest": "~0.8.1", "supertest": "~0.8.1",
"chai": "~1.8.1" "chai": "~1.8.1",
"loopback-testing": "~0.1.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -0,0 +1,179 @@
var loopback = require('../');
var lt = require('loopback-testing');
var path = require('path');
var ACCESS_CONTROL_APP = path.join(__dirname, 'fixtures', 'access-control');
var app = require(path.join(ACCESS_CONTROL_APP, 'app.js'));
var assert = require('assert');
var USER = {email: 'test@test.test', password: 'test'};
var CURRENT_USER = {email: 'current@test.test', password: 'test'};
describe('access control - integration', function () {
lt.beforeEach.withApp(app);
describe('accessToken', function() {
// it('should be a sublcass of AccessToken', function () {
// assert(app.models.accessToken.prototype instanceof loopback.AccessToken);
// });
it('should have a validate method', function () {
var token = new app.models.accessToken;
assert.equal(typeof token.validate, 'function');
});
});
describe('/accessToken', function() {
lt.beforeEach.givenModel('accessToken', {}, 'randomToken');
lt.it.shouldBeAllowedWhenCalledAnonymously('POST', '/api/accessTokens');
lt.it.shouldBeAllowedWhenCalledUnauthenticated('POST', '/api/accessTokens');
lt.it.shouldBeAllowedWhenCalledByUser(USER, 'POST', '/api/accessTokens');
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accessTokens');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accessTokens');
lt.it.shouldBeDeniedWhenCalledByUser(USER, 'GET', '/api/accessTokens');
lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', '/api/accessTokens');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', '/api/accessTokens');
lt.it.shouldBeDeniedWhenCalledByUser(USER, 'PUT', '/api/accessTokens');
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForToken);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForToken);
lt.it.shouldBeDeniedWhenCalledByUser(USER, 'GET', urlForToken);
lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForToken);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForToken);
lt.it.shouldBeDeniedWhenCalledByUser(USER, 'PUT', urlForToken);
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForToken);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForToken);
lt.it.shouldBeDeniedWhenCalledByUser(USER, 'DELETE', urlForToken);
function urlForToken() {
return '/api/accessTokens/' + this.randomToken.id;
}
});
describe('/users', function () {
lt.beforeEach.givenModel('user', USER, 'randomUser');
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/users');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/users');
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/users');
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForUser);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForUser);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER,'GET', urlForUser);
lt.it.shouldBeAllowedWhenCalledAnonymously('POST', '/api/users');
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users');
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/logout');
lt.describe.whenCalledRemotely('DELETE', '/api/users', function() {
lt.it.shouldNotBeFound();
});
lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForUser);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForUser);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForUser);
lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForUser);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForUser);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForUser);
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
beforeEach(function() {
this.url = '/api/users/' + this.user.id + '?ok';
});
lt.describe.whenCalledRemotely('DELETE', '/api/users/:id', function() {
lt.it.shouldBeAllowed();
});
lt.describe.whenCalledRemotely('GET', '/api/users/:id', function() {
lt.it.shouldBeAllowed();
});
lt.describe.whenCalledRemotely('PUT', '/api/users/:id', function() {
lt.it.shouldBeAllowed();
});
});
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForUser);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForUser);
function urlForUser() {
return '/api/users/' + this.randomUser.id;
}
});
describe('/banks', function () {
lt.beforeEach.givenModel('bank');
lt.it.shouldBeAllowedWhenCalledAnonymously('GET', '/api/banks');
lt.it.shouldBeAllowedWhenCalledUnauthenticated('GET', '/api/banks');
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'GET', '/api/banks');
lt.it.shouldBeAllowedWhenCalledAnonymously('GET', urlForBank);
lt.it.shouldBeAllowedWhenCalledUnauthenticated('GET', urlForBank);
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'GET', urlForBank);
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/banks');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/banks');
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/banks');
lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForBank);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForBank);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForBank);
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForBank);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForBank);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForBank);
function urlForBank() {
return '/api/banks/' + this.bank.id;
}
});
describe('/accounts', function () {
lt.beforeEach.givenModel('account');
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount);
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts');
lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount);
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
beforeEach(function() {
this.url = '/api/accounts/' + this.user.accountId;
});
lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() {
lt.it.shouldBeAllowed();
});
lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() {
lt.it.shouldBeDenied();
});
});
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount);
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForAccount);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);
function urlForAccount() {
return '/api/accounts/' + this.account.id;
}
});
});

View File

@ -7,6 +7,7 @@ var role = require('../lib/models/role');
var Role = role.Role; var Role = role.Role;
var RoleMapping = role.RoleMapping; var RoleMapping = role.RoleMapping;
var User = loopback.User; var User = loopback.User;
var testModel;
function checkResult(err, result) { function checkResult(err, result) {
// console.log(err, result); // console.log(err, result);
@ -15,6 +16,17 @@ function checkResult(err, result) {
describe('security scopes', function () { describe('security scopes', function () {
beforeEach(function() {
var ds = this.ds = loopback.createDataSource({connector: loopback.Memory});
testModel = loopback.Model.extend('testModel');
ACL.attachTo(ds);
Role.attachTo(ds);
RoleMapping.attachTo(ds);
User.attachTo(ds);
Scope.attachTo(ds);
testModel.attachTo(ds);
});
it("should allow access to models for the given scope by wildcard", function () { it("should allow access to models for the given scope by wildcard", function () {
Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) { Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) {
ACL.create({principalType: ACL.SCOPE, principalId: scope.id, model: 'User', property: ACL.ALL, ACL.create({principalType: ACL.SCOPE, principalId: scope.id, model: 'User', property: ACL.ALL,
@ -29,27 +41,24 @@ describe('security scopes', function () {
}); });
it("should allow access to models for the given scope", function () { it("should allow access to models for the given scope", function () {
var ds = loopback.createDataSource({connector: loopback.Memory}); Scope.create({name: 'testModelScope', description: 'access testModel information'}, function (err, scope) {
Scope.attachTo(ds);
ACL.attachTo(ds);
Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) {
ACL.create({principalType: ACL.SCOPE, principalId: scope.id, ACL.create({principalType: ACL.SCOPE, principalId: scope.id,
model: 'User', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW}, model: 'testModel', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW},
function (err, resource) { function (err, resource) {
ACL.create({principalType: ACL.SCOPE, principalId: scope.id, ACL.create({principalType: ACL.SCOPE, principalId: scope.id,
model: 'User', property: 'name', accessType: ACL.WRITE, permission: ACL.DENY}, model: 'testModel', property: 'name', accessType: ACL.WRITE, permission: ACL.DENY},
function (err, resource) { function (err, resource) {
// console.log(resource); // console.log(resource);
Scope.checkPermission('userScope', 'User', ACL.ALL, ACL.ALL, function (err, perm) { Scope.checkPermission('testModelScope', 'testModel', ACL.ALL, ACL.ALL, function (err, perm) {
assert(perm.permission === ACL.DENY); // because name.WRITE == DENY assert(perm.permission === ACL.DENY); // because name.WRITE == DENY
}); });
Scope.checkPermission('userScope', 'User', 'name', ACL.ALL, function (err, perm) { Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.ALL, function (err, perm) {
assert(perm.permission === ACL.DENY); // because name.WRITE == DENY assert(perm.permission === ACL.DENY); // because name.WRITE == DENY
}); });
Scope.checkPermission('userScope', 'User', 'name', ACL.READ, function (err, perm) { Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.READ, function (err, perm) {
assert(perm.permission === ACL.ALLOW); assert(perm.permission === ACL.ALLOW);
}); });
Scope.checkPermission('userScope', 'User', 'name', ACL.WRITE, function (err, perm) { Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.WRITE, function (err, perm) {
assert(perm.permission === ACL.DENY); assert(perm.permission === ACL.DENY);
}); });
}); });
@ -63,9 +72,6 @@ describe('security scopes', function () {
describe('security ACLs', function () { describe('security ACLs', function () {
it("should allow access to models for the given principal by wildcard", function () { it("should allow access to models for the given principal by wildcard", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL, ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) { accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) {
@ -87,28 +93,25 @@ describe('security ACLs', function () {
}); });
it("should allow access to models by exception", function () { it("should allow access to models by exception", function () {
var ds = loopback.createDataSource({connector: loopback.Memory}); ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'testModel', property: ACL.ALL,
ACL.attachTo(ds);
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
accessType: ACL.ALL, permission: ACL.DENY}, function (err, acl) { accessType: ACL.ALL, permission: ACL.DENY}, function (err, acl) {
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL, ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'testModel', property: ACL.ALL,
accessType: ACL.READ, permission: ACL.ALLOW}, function (err, acl) { accessType: ACL.READ, permission: ACL.ALLOW}, function (err, acl) {
ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.READ, function (err, perm) { ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.READ, function (err, perm) {
assert(perm.permission === ACL.ALLOW); assert(perm.permission === ACL.ALLOW);
}); });
ACL.checkPermission(ACL.USER, 'u001', 'User', ACL.ALL, ACL.READ, function (err, perm) { ACL.checkPermission(ACL.USER, 'u001', 'testModel', ACL.ALL, ACL.READ, function (err, perm) {
assert(perm.permission === ACL.ALLOW); assert(perm.permission === ACL.ALLOW);
}); });
ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.WRITE, function (err, perm) { ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.WRITE, function (err, perm) {
assert(perm.permission === ACL.DENY); assert(perm.permission === ACL.DENY);
}); });
ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.ALL, function (err, perm) { ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.ALL, function (err, perm) {
assert(perm.permission === ACL.DENY); assert(perm.permission === ACL.DENY);
}); });
@ -119,8 +122,7 @@ describe('security ACLs', function () {
}); });
it("should honor defaultPermission from the model", function () { it("should honor defaultPermission from the model", function () {
var ds = loopback.createDataSource({connector: loopback.Memory}); var ds = this.ds;
ACL.attachTo(ds);
var Customer = ds.createModel('Customer', { var Customer = ds.createModel('Customer', {
name: { name: {
type: String, type: String,
@ -152,7 +154,7 @@ describe('security ACLs', function () {
}); });
it("should honor static ACLs from the model", function () { it("should honor static ACLs from the model", function () {
var ds = loopback.createDataSource({connector: loopback.Memory}); var ds = this.ds;
var Customer = ds.createModel('Customer', { var Customer = ds.createModel('Customer', {
name: { name: {
type: String, type: String,
@ -188,14 +190,9 @@ describe('security ACLs', function () {
}); });
it("should check access against LDL, ACL, and Role", function () { it("should check access against LDL, ACL, and Role", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
Role.attachTo(ds);
RoleMapping.attachTo(ds);
User.attachTo(ds);
// var log = console.log; // var log = console.log;
var log = function() {}; var log = function() {};
var ds = this.ds;
// Create // Create
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) { User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
@ -246,21 +243,7 @@ describe('security ACLs', function () {
}, function(err, access) { }, function(err, access) {
assert(!err && access.permission === ACL.ALLOW); assert(!err && access.permission === ACL.ALLOW);
}); });
/*
ACL.checkAccess({
principals: [
{principalType: ACL.USER, principalId: userId}
],
model: 'Customer',
accessType: ACL.READ
}, function(err, access) {
assert(!err && access.permission === ACL.DENY);
}); });
*/
});
}); });
}); });
}); });

View File

@ -1,3 +1,6 @@
var path = require('path');
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
describe('app', function() { describe('app', function() {
describe('app.model(Model)', function() { describe('app.model(Model)', function() {
@ -75,6 +78,23 @@ describe('app', function() {
expect(this.app.get('baz')).to.eql(true); expect(this.app.get('baz')).to.eql(true);
}); });
describe('boot and models directories', function() {
beforeEach(function() {
var app = this.app = loopback();
app.boot(SIMPLE_APP);
});
it('should run all modules in the boot directory', function () {
assert(process.loadedFooJS);
delete process.loadedFooJS;
});
it('should run all modules in the models directory', function () {
assert(process.loadedBarJS);
delete process.loadedBarJS;
});
});
describe('PaaS and npm env variables', function() { describe('PaaS and npm env variables', function() {
beforeEach(function() { beforeEach(function() {
this.boot = function () { this.boot = function () {
@ -162,7 +182,7 @@ describe('app', function() {
it('Load config files', function () { it('Load config files', function () {
var app = loopback(); var app = loopback();
app.boot(require('path').join(__dirname, 'fixtures', 'simple-app')); app.boot(SIMPLE_APP);
assert(app.models.foo); assert(app.models.foo);
assert(app.models.Foo); assert(app.models.Foo);

14
test/fixtures/access-control/app.js vendored Normal file
View File

@ -0,0 +1,14 @@
var loopback = require('../../../');
var path = require('path');
var app = module.exports = loopback();
app.boot(__dirname);
var apiPath = '/api';
app.use(loopback.cookieParser('secret'));
app.use(loopback.token({model: app.models.accessToken}));
app.use(apiPath, loopback.rest());
app.use(app.router);
app.use(loopback.urlNotFound());
app.use(loopback.errorHandler());
app.enableAuth();

4
test/fixtures/access-control/app.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"port": 3000,
"host": "0.0.0.0"
}

View File

@ -0,0 +1,10 @@
{
"db": {
"defaultForType": "db",
"connector": "memory"
},
"mail": {
"defaultForType": "mail",
"connector": "mail"
}
}

163
test/fixtures/access-control/models.json vendored Normal file
View File

@ -0,0 +1,163 @@
{
"email": {
"options": {
"base": "Email",
"acls": [
{
"accessType": "*",
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
}
]
},
"dataSource": "mail",
"public": false
},
"user": {
"options": {
"base": "User",
"relations": {
"accessTokens": {
"model": "accessToken",
"type": "hasMany",
"foreignKey": "userId"
},
"account": {
"model": "account",
"type": "belongsTo"
},
"transactions": {
"model": "transaction",
"type": "hasMany"
}
},
"acls": [
{
"accessType": "*",
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
}
]
},
"dataSource": "db",
"public": true
},
"accessToken": {
"options": {
"base": "AccessToken",
"baseUrl": "access-tokens",
"acls": [
{
"accessType": "*",
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
},
{
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "$everyone",
"property": "create"
}
]
},
"dataSource": "db",
"public": true
},
"bank": {
"options": {
"relations": {
"users": {
"model": "user",
"type": "hasMany"
},
"accounts": {
"model": "account",
"type": "hasMany"
}
},
"acls": [
{
"accessType": "*",
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
},
{
"accessType": "READ",
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "$everyone"
}
]
},
"properties": {},
"public": true,
"dataSource": "db"
},
"account": {
"options": {
"relations": {
"transactions": {
"model": "transaction",
"type": "hasMany"
}
},
"acls": [
{
"accessType": "*",
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
},
{
"accessType": "*",
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "$owner"
},
{
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$owner",
"property": "removeById"
}
]
},
"properties": {},
"public": true,
"dataSource": "db"
},
"transaction": {
"options": {
"acls": [
{
"accessType": "*",
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
}
]
},
"properties": {},
"public": true,
"dataSource": "db"
},
"alert": {
"options": {
"acls": [
{
"accessType": "WRITE",
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
}
]
},
"properties": {},
"public": true,
"dataSource": "db"
}
}

1
test/fixtures/simple-app/boot/bad.txt vendored Normal file
View File

@ -0,0 +1 @@
this is not a js file!

1
test/fixtures/simple-app/boot/foo.js vendored Normal file
View File

@ -0,0 +1 @@
process.loadedFooJS = true;

View File

@ -0,0 +1 @@
process.loadedBarJS = true;

View File

@ -0,0 +1,13 @@
var loopback = require('../../../');
var path = require('path');
var app = module.exports = loopback();
app.boot(__dirname);
app.use(loopback.favicon());
app.use(loopback.cookieParser({secret: app.get('cookieSecret')}));
var apiPath = '/api';
app.use(apiPath, loopback.rest());
app.use(app.router);
app.use(loopback.static(path.join(__dirname, 'public')));
app.use(loopback.urlNotFound());
app.use(loopback.errorHandler());

View File

@ -0,0 +1,5 @@
{
"port": 3000,
"host": "0.0.0.0",
"cookieSecret": "2d13a01d-44fb-455c-80cb-db9cb3cd3cd0"
}

View File

@ -0,0 +1,10 @@
{
"db": {
"defaultForType": "db",
"connector": "memory"
},
"mail": {
"defaultForType": "mail",
"connector": "mail"
}
}

View File

@ -0,0 +1,48 @@
{
"email": {
"dataSource": "mail",
"public": false,
"options": {
"base": "Email"
}
},
"user": {
"dataSource": "db",
"public": true,
"options": {
"base": "User",
"relations": {
"accessTokens": {
"model": "accessToken",
"type": "hasMany",
"foreignKey": "userId"
}
}
}
},
"accessToken": {
"dataSource": "db",
"public": true,
"options": {
"base": "AccessToken"
}
},
"widget": {
"properties": {},
"public": true,
"dataSource": "db"
},
"store": {
"properties": {},
"public": true,
"dataSource": "db",
"options": {
"relations": {
"widgets": {
"model": "widget",
"type": "hasMany"
}
}
}
}
}

View File

@ -6,7 +6,9 @@ describe('Application', function () {
var registeredApp = null; var registeredApp = null;
it('Create a new application', function (done) { it('Create a new application', function (done) {
Application.create({owner: 'rfeng', name: 'MyApp1', description: 'My first mobile application'}, function (err, result) { Application.create({owner: 'rfeng',
name: 'MyApp1',
description: 'My first mobile application'}, function (err, result) {
var app = result; var app = result;
assert.equal(app.owner, 'rfeng'); assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp1'); assert.equal(app.name, 'MyApp1');
@ -22,8 +24,59 @@ describe('Application', function () {
}); });
}); });
it('Create a new application with push settings', function (done) {
Application.create({owner: 'rfeng',
name: 'MyAppWithPush',
description: 'My push mobile application',
pushSettings: {
apns: {
production: false,
certData: 'cert',
keyData: 'key',
pushOptions: {
gateway: 'gateway.sandbox.push.apple.com',
port: 2195
},
feedbackOptions: {
gateway: 'feedback.sandbox.push.apple.com',
port: 2196,
interval: 300,
batchFeedback: true
}
},
gcm: {
serverApiKey: 'serverKey'
}
}},
function (err, result) {
var app = result;
assert.deepEqual(app.pushSettings.toObject(), {
apns: {
production: false,
certData: 'cert',
keyData: 'key',
pushOptions: {
gateway: 'gateway.sandbox.push.apple.com',
port: 2195
},
feedbackOptions: {
gateway: 'feedback.sandbox.push.apple.com',
port: 2196,
interval: 300,
batchFeedback: true
}
},
gcm: {
serverApiKey: 'serverKey'
}
});
done(err, result);
});
});
beforeEach(function (done) { beforeEach(function (done) {
Application.register('rfeng', 'MyApp2', {description: 'My second mobile application'}, function (err, result) { Application.register('rfeng', 'MyApp2',
{description: 'My second mobile application'}, function (err, result) {
var app = result; var app = result;
assert.equal(app.owner, 'rfeng'); assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp2'); assert.equal(app.name, 'MyApp2');
@ -66,43 +119,48 @@ describe('Application', function () {
}); });
it('Authenticate with application id & clientKey', function (done) { it('Authenticate with application id & clientKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.clientKey, function (err, result) { Application.authenticate(registeredApp.id, registeredApp.clientKey,
function (err, result) {
assert.equal(result, 'clientKey'); assert.equal(result, 'clientKey');
done(err, result); done(err, result);
}); });
}); });
it('Authenticate with application id & javaScriptKey', function (done) { it('Authenticate with application id & javaScriptKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.javaScriptKey, function (err, result) { Application.authenticate(registeredApp.id, registeredApp.javaScriptKey,
function (err, result) {
assert.equal(result, 'javaScriptKey'); assert.equal(result, 'javaScriptKey');
done(err, result); done(err, result);
}); });
}); });
it('Authenticate with application id & restApiKey', function (done) { it('Authenticate with application id & restApiKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.restApiKey, function (err, result) { Application.authenticate(registeredApp.id, registeredApp.restApiKey,
function (err, result) {
assert.equal(result, 'restApiKey'); assert.equal(result, 'restApiKey');
done(err, result); done(err, result);
}); });
}); });
it('Authenticate with application id & masterKey', function (done) { it('Authenticate with application id & masterKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.masterKey, function (err, result) { Application.authenticate(registeredApp.id, registeredApp.masterKey,
function (err, result) {
assert.equal(result, 'masterKey'); assert.equal(result, 'masterKey');
done(err, result); done(err, result);
}); });
}); });
it('Authenticate with application id & windowsKey', function (done) { it('Authenticate with application id & windowsKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.windowsKey, function (err, result) { Application.authenticate(registeredApp.id, registeredApp.windowsKey,
function (err, result) {
assert.equal(result, 'windowsKey'); assert.equal(result, 'windowsKey');
done(err, result); done(err, result);
}); });
}); });
it('Fail to authenticate with application id & invalid key', function (done) { it('Fail to authenticate with application id & invalid key', function (done) {
Application.authenticate(registeredApp.id, 'invalid-key', function (err, result) { Application.authenticate(registeredApp.id, 'invalid-key',
function (err, result) {
assert(!result); assert(!result);
done(err, result); done(err, result);
}); });

View File

@ -0,0 +1,98 @@
var loopback = require('../');
var lt = require('loopback-testing');
var path = require('path');
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-integration-app');
var app = require(path.join(SIMPLE_APP, 'app.js'));
var assert = require('assert');
describe('relations - integration', function () {
lt.beforeEach.withApp(app);
lt.beforeEach.givenModel('store');
beforeEach(function(done) {
this.widgetName = 'foo';
this.store.widgets.create({
name: this.widgetName
}, function() {
done();
});
});
afterEach(function(done) {
this.app.models.widget.destroyAll(done);
});
describe('/store/:id/widgets', function () {
beforeEach(function() {
this.url = '/api/stores/' + this.store.id + '/widgets';
});
lt.describe.whenCalledRemotely('GET', '/api/stores/:id/widgets', function() {
it('should succeed with statusCode 200', function() {
assert.equal(this.res.statusCode, 200);
});
describe('widgets (response.body)', function() {
beforeEach(function() {
this.widgets = this.res.body;
this.widget = this.res.body[0];
});
it('should be an array', function() {
assert(Array.isArray(this.widgets));
});
it('should include a single widget', function() {
assert(this.widgets.length === 1);
assert(this.widget);
});
it('should be a valid widget', function() {
assert(this.widget.id);
assert.equal(this.widget.storeId, this.store.id);
assert.equal(this.widget.name, this.widgetName);
});
});
});
describe('POST /api/store/:id/widgets', function() {
beforeEach(function() {
this.newWidgetName = 'baz';
this.newWidget = {
name: this.newWidgetName
};
});
beforeEach(function(done) {
this.http = this.post(this.url, this.newWidget);
this.http.send(this.newWidget);
this.http.end(function(err) {
if(err) return done(err);
this.req = this.http.req;
this.res = this.http.res;
done();
}.bind(this));
});
it('should succeed with statusCode 200', function() {
assert.equal(this.res.statusCode, 200);
});
describe('widget (response.body)', function() {
beforeEach(function() {
this.widget = this.res.body;
});
it('should be an object', function() {
assert(typeof this.widget === 'object');
assert(!Array.isArray(this.widget));
});
it('should be a valid widget', function() {
assert(this.widget.id);
assert.equal(this.widget.storeId, this.store.id);
assert.equal(this.widget.name, this.newWidgetName);
});
});
it('should have a single widget with storeId', function (done) {
this.app.models.widget.count({
storeId: this.store.id
}, function(err, count) {
if(err) return done(err);
assert.equal(count, 2);
done();
});
});
});
});
});

View File

@ -17,6 +17,7 @@ describe('User', function(){
}); });
beforeEach(function (done) { beforeEach(function (done) {
app.use(loopback.token());
app.use(loopback.rest()); app.use(loopback.rest());
app.model(User); app.model(User);
@ -153,7 +154,6 @@ describe('User', function(){
it('Logout a user by providing the current accessToken id (over rest)', function(done) { it('Logout a user by providing the current accessToken id (over rest)', function(done) {
login(logout); login(logout);
function login(fn) { function login(fn) {
request(app) request(app)
.post('/users/login') .post('/users/login')
@ -171,22 +171,22 @@ describe('User', function(){
}); });
} }
function logout(err, sid) { function logout(err, token) {
request(app) request(app)
.post('/users/logout') .post('/users/logout')
.set('Authorization', token)
.expect(204) .expect(204)
.send({sid: sid}) .end(verify(token, done));
.end(verify(sid, done));
} }
}); });
function verify(sid, done) { function verify(token, done) {
assert(sid); assert(token);
return function (err) { return function (err) {
if(err) return done(err); if(err) return done(err);
AccessToken.findById(sid, function (err, accessToken) { AccessToken.findById(token, function (err, accessToken) {
assert(!accessToken, 'accessToken should not exist after logging out'); assert(!accessToken, 'accessToken should not exist after logging out');
done(err); done(err);
}); });