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": [
"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",

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
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()`.
4. Any JavaScript files in the `./boot` directory are loaded with `require()`.
**Options**

View File

@ -310,10 +310,18 @@ User.find({
**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.
```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)
Find instance by id.

View File

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

View File

@ -1,6 +1,7 @@
# 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:
@ -25,7 +26,8 @@ Application model captures the metadata for a loopback application.
## 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
* javaScriptKey: Secret for JavaScript clients
@ -40,49 +42,51 @@ The application can be configured to support multiple methods of push notificati
* pushSettings
{
pushSettings: [
{ "platform": "apns",
"apns": {
"pushOptions": {
"gateway": "gateway.sandbox.push.apple.com",
"cert": "credentials/apns_cert_dev.pem",
"key": "credentials/apns_key_dev.pem"
},
"feedbackOptions": {
"gateway": "feedback.sandbox.push.apple.com",
"cert": "credentials/apns_cert_dev.pem",
"key": "credentials/apns_key_dev.pem",
"batchFeedback": true,
"interval": 300
}
}}
]}
pushSettings: {
apns: {
certData: config.apnsCertData,
keyData: config.apnsKeyData,
production: false, // Development mode
pushOptions: {
// Extra options can go here for APN
},
feedbackOptions: {
batchFeedback: true,
interval: 300
}
},
gcm: {
serverApiKey: config.gcmServerApiKey
}
}
## Authentication schemes
* authenticationEnabled
* anonymousAllowed
* authenticationSchemes
### 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
## 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
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) {
var app = result;
Application.register('rfeng', 'MyApp1',
{description: 'My first loopback application'},
function (err, result) {
var app = result;
...
});
@ -97,7 +101,9 @@ You can reset keys for a given application by id.
### 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) {
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 AccessToken = require('./access-token');
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
@ -95,6 +96,8 @@ AccessContext.prototype.addPrincipal = function (principalType, principalId, pri
}
}
this.principals.push(principal);
debug('adding principal %j', principal);
return true;
};
@ -135,6 +138,36 @@ AccessContext.prototype.isAuthenticated = function() {
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
* 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 {String} [name] The principal name
* @returns {Principal}
* @constructor
* @class
*/
function Principal(type, id, name) {
if (!(this instanceof Principal)) {
@ -178,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)) {
@ -188,6 +221,15 @@ function AccessRequest(model, property, accessType, permission) {
this.property = property || AccessContext.ALL;
this.accessType = accessType || AccessContext.ALL;
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.
*/
@ -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(

View File

@ -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,10 +187,9 @@ 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) {
debug('resolvePermission(): %j %j', acls, req);
// Sort by the matching score in descending order
acls = acls.sort(function (rule1, rule2) {
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,
permission || ACL.DEFAULT);
debug('resolvePermission() returns: %j', res);
return res;
};
@ -210,7 +235,6 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
* @return {Object[]} An array of ACLs
*/
ACL.getStaticACLs = function getStaticACLs(model, property) {
debug('getStaticACLs(): %s %s', model, property);
var modelClass = loopback.getModel(model);
var staticACLs = [];
if (modelClass && modelClass.settings.acls) {
@ -223,6 +247,8 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
accessType: acl.accessType,
permission: acl.permission
}));
staticACLs[staticACLs.length - 1].debug('Adding ACL');
});
}
var prop = modelClass &&
@ -242,7 +268,6 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
}));
});
}
debug('getStaticACLs() returns: %j', staticACLs);
return staticACLs;
};
@ -257,13 +282,11 @@ 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,
callback) {
debug('checkPermission(): %s %s %s %s %s', principalType, principalId, model,
property, accessType);
property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL;
@ -276,8 +299,8 @@ ACL.checkPermission = function checkPermission(principalType, principalId,
var resolved = ACL.resolvePermission(acls, req);
if(resolved && resolved.permission === ACL.DENY) {
// Fail fast
debug('checkPermission(): %j', resolved);
debug('Permission denied by statically resolved permission');
debug(' Resolved Permission: %j', resolved);
process.nextTick(function() {
callback && callback(null, resolved);
});
@ -297,32 +320,21 @@ ACL.checkPermission = function checkPermission(principalType, principalId,
var modelClass = loopback.getModel(model);
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
}
debug('checkPermission(): %j', resolved);
callback && callback(null, resolved);
});
};
/**
* 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);
}
});
};
ACL.prototype.debug = function() {
if(debug.enabled) {
debug('---ACL---')
debug('model %s', this.model);
debug('property %s', this.property);
debug('principalType %s', this.principalType);
debug('principalId %s', this.principalId);
debug('accessType %s', this.accessType);
debug('permission %s', this.permission);
}
}
/**
* Check if the request has the permission to access
@ -335,8 +347,6 @@ Scope.checkPermission = function (scope, model, property, accessType, callback)
* @param {Function} callback
*/
ACL.checkAccess = function (context, callback) {
debug('checkAccess(): %j', context);
if(!(context instanceof AccessContext)) {
context = new AccessContext(context);
}
@ -407,14 +417,12 @@ 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
*/
ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
debug('checkAccessForToken(): %j %s %s %s', token, model, modelId, method);
assert(token, 'Access token is required');
var context = new AccessContext({
@ -427,12 +435,13 @@ ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
context.accessType = context.model._getAccessTypeForMethod(method);
context.debug();
ACL.checkAccess(context, function (err, access) {
if (err) {
callback && callback(err);
return;
}
debug('checkAccessForToken(): %j', access);
callback && callback(null, access.permission !== ACL.DENY);
});
};

View File

@ -3,78 +3,93 @@ var assert = require('assert');
// Authentication schemes
var AuthenticationSchemeSchema = {
scheme: String, // local, facebook, google, twitter, linkedin, github
credential: Object // Scheme-specific credentials
scheme: String, // local, facebook, google, twitter, linkedin, github
credential: Object // Scheme-specific credentials
};
// See https://github.com/argon/node-apn/blob/master/doc/apn.markdown
var APNSSettingSchema = {
pushOptions: {type: {
gateway: String,
cert: String,
key: String
}},
/**
* 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
feedbackOptions: {type: {
gateway: String,
cert: String,
key: String,
batchFeedback: Boolean,
interval: Number
}}
pushOptions: {type: {
gateway: String,
port: Number
}},
feedbackOptions: {type: {
gateway: String,
port: Number,
batchFeedback: Boolean,
interval: Number
}}
};
var GcmSettingsSchema = {
serverApiKey: String
};
// Push notification settings
var PushNotificationSettingSchema = {
platform: {type: String, required: true}, // apns, gcm, mpns
// configuration: {type: Object} // platform-specific configurations
apns: APNSSettingSchema
apns: APNSSettingSchema,
gcm: GcmSettingsSchema
};
/**
* Data model for Application
*/
var ApplicationSchema = {
id: {type: String, id: true, generated: true},
// Basic information
name: {type: String, required: true}, // The name
description: String, // The description
icon: String, // The icon image url
id: {type: String, id: true, generated: true},
// Basic information
name: {type: String, required: true}, // The name
description: String, // The description
icon: String, // The icon image url
owner: String, // The user id of the developer who registers the application
collaborators: [String], // A list of users ids who have permissions to work on this app
owner: String, // The user id of the developer who registers the application
collaborators: [String], // A list of users ids who have permissions to work on this app
// EMail
email: String, // e-mail address
emailVerified: Boolean, // Is the e-mail verified
// EMail
email: String, // e-mail address
emailVerified: Boolean, // Is the e-mail verified
// oAuth 2.0 settings
url: String, // The application url
callbackUrls: [String], // oAuth 2.0 code/token callback url
permissions: [String], // A list of permissions required by the application
// oAuth 2.0 settings
url: String, // The application url
callbackUrls: [String], // oAuth 2.0 code/token callback url
permissions: [String], // A list of permissions required by the application
// Keys
clientKey: String,
javaScriptKey: String,
restApiKey: String,
windowsKey: String,
masterKey: String,
// Keys
clientKey: String,
javaScriptKey: String,
restApiKey: String,
windowsKey: String,
masterKey: String,
// Push notification
pushSettings: PushNotificationSettingSchema,
// Push notification
pushSettings: PushNotificationSettingSchema,
// User Authentication
authenticationEnabled: {type: Boolean, default: true},
anonymousAllowed: {type: Boolean, default: true},
authenticationSchemes: [AuthenticationSchemeSchema],
// User Authentication
authenticationEnabled: {type: Boolean, default: true},
anonymousAllowed: {type: Boolean, default: true},
authenticationSchemes: [AuthenticationSchemeSchema],
status: {type: String, default: 'sandbox'}, // Status of the application, production/sandbox/disabled
status: {type: String, default: 'sandbox'}, // Status of the application, production/sandbox/disabled
// Timestamps
created: {type: Date, default: Date},
modified: {type: Date, default: Date}
// Timestamps
created: {type: Date, default: Date},
modified: {type: Date, default: Date}
};
/**
* Application management functions
*/
@ -82,15 +97,21 @@ var ApplicationSchema = {
var crypto = require('crypto');
function generateKey(hmacKey, algorithm, encoding) {
hmacKey = hmacKey || 'loopback';
algorithm = algorithm || 'sha256';
encoding = encoding || 'base64';
var hmac = crypto.createHmac(algorithm, hmacKey);
var buf = crypto.randomBytes(64);
hmac.update(buf);
return hmac.digest('base64');
hmacKey = hmacKey || 'loopback';
algorithm = algorithm || 'sha256';
encoding = encoding || 'base64';
var hmac = crypto.createHmac(algorithm, hmacKey);
var buf = crypto.randomBytes(64);
hmac.update(buf);
return hmac.digest('base64');
}
/**
* Manage client applications and organize their users.
* @class
* @inherits {Model}
*/
var Application = loopback.createModel('Application', ApplicationSchema);
/*!
@ -98,15 +119,15 @@ var Application = loopback.createModel('Application', ApplicationSchema);
* @param next
*/
Application.beforeCreate = function (next) {
var app = this;
app.created = app.modified = new Date();
app.id = generateKey('id', 'sha1');
app.clientKey = generateKey('client');
app.javaScriptKey = generateKey('javaScript');
app.restApiKey = generateKey('restApi');
app.windowsKey = generateKey('windows');
app.masterKey = generateKey('master');
next();
var app = this;
app.created = app.modified = new Date();
app.id = generateKey('id', 'sha1');
app.clientKey = generateKey('client');
app.javaScriptKey = generateKey('javaScript');
app.restApiKey = generateKey('restApi');
app.windowsKey = generateKey('windows');
app.masterKey = generateKey('master');
next();
};
/**
@ -117,71 +138,84 @@ Application.beforeCreate = function (next) {
* @param cb Callback function
*/
Application.register = function (owner, name, options, cb) {
assert(owner, 'owner is required');
assert(name, 'name is required');
assert(owner, 'owner is required');
assert(name, 'name is required');
if(typeof options === 'function' && !cb) {
cb = options;
options = {};
if (typeof options === 'function' && !cb) {
cb = options;
options = {};
}
var props = {owner: owner, name: name};
for (var p in options) {
if (!(p in props)) {
props[p] = options[p];
}
var props = {owner: owner, name: name};
for(var p in options) {
if(!(p in props)) {
props[p] = options[p];
}
}
Application.create(props, cb);
}
Application.create(props, cb);
};
/**
* Reset keys for the application instance
* @param cb
* @callback {Function} callback
* @param {Error} err
*/
Application.prototype.resetKeys = function(cb) {
this.clientKey = generateKey('client');
this.javaScriptKey = generateKey('javaScript');
this.restApiKey = generateKey('restApi');
this.windowsKey = generateKey('windows');
this.masterKey = generateKey('master');
this.modified = new Date();
this.save(cb);
Application.prototype.resetKeys = function (cb) {
this.clientKey = generateKey('client');
this.javaScriptKey = generateKey('javaScript');
this.restApiKey = generateKey('restApi');
this.windowsKey = generateKey('windows');
this.masterKey = generateKey('master');
this.modified = new Date();
this.save(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) {
if(err) {
cb && cb(err, app);
return;
}
app.resetKeys(cb);
});
Application.resetKeys = function (appId, cb) {
Application.findById(appId, function (err, app) {
if (err) {
cb && cb(err, app);
return;
}
app.resetKeys(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) {
if(err || !app) {
cb && cb(err, null);
return;
}
var matched = null;
['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'].forEach(function(k) {
if(app[k] === key) {
matched = k;
}
});
cb && cb(null, matched);
Application.authenticate = function (appId, key, cb) {
Application.findById(appId, function (err, app) {
if (err || !app) {
cb && cb(err, null);
return;
}
var matched = null;
['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'].forEach(function (k) {
if (app[k] === key) {
matched = k;
}
});
cb && cb(null, matched);
});
};
module.exports = Application;

View File

@ -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 ✔ <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.
*/
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

View File

@ -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
*/

View File

@ -44,14 +44,15 @@ var properties = {
status: String,
created: Date,
lastUpdated: Date
}
/**
* Default User options.
*/
};
var options = {
acls: [
{
principalType: ACL.ROLE,
principalId: Role.EVERYONE,
permission: ACL.DENY,
},
{
principalType: ACL.ROLE,
principalId: Role.EVERYONE,
@ -63,12 +64,49 @@ var options = {
principalId: Role.OWNER,
permission: ACL.ALLOW,
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.
*
* 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);
@ -76,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) {
@ -121,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) {
@ -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 = {
* type: 'email',
* to: user.email,
@ -169,6 +217,7 @@ User.prototype.hasPassword = function (plain, fn) {
* };
*
* user.verify(options, next);
* ```
*
* @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) {
this.findById(uid, function (err, user) {
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) {
var UserModel = this;
var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
@ -294,7 +363,7 @@ User.resetPassword = function(options, cb) {
}
}
/**
/*!
* Setup an extended user model.
*/
@ -327,7 +396,13 @@ User.setup = function () {
UserModel.logout,
{
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'}
}

View File

@ -9,7 +9,7 @@
"Platform",
"mBaaS"
],
"version": "1.3.4",
"version": "1.4.1",
"scripts": {
"test": "mocha -R spec"
},
@ -29,14 +29,15 @@
"async": "~0.2.9"
},
"peerDependencies": {
"loopback-datasource-juggler": "~1.2.0"
"loopback-datasource-juggler": "~1.2.11"
},
"devDependencies": {
"loopback-datasource-juggler": "~1.2.0",
"loopback-datasource-juggler": "~1.2.11",
"mocha": "~1.14.0",
"strong-task-emitter": "0.0.x",
"supertest": "~0.8.1",
"chai": "~1.8.1"
"chai": "~1.8.1",
"loopback-testing": "~0.1.0"
},
"repository": {
"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 RoleMapping = role.RoleMapping;
var User = loopback.User;
var testModel;
function checkResult(err, result) {
// console.log(err, result);
@ -15,6 +16,17 @@ function checkResult(err, result) {
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 () {
Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) {
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 () {
var ds = loopback.createDataSource({connector: loopback.Memory});
Scope.attachTo(ds);
ACL.attachTo(ds);
Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) {
Scope.create({name: 'testModelScope', description: 'access testModel information'}, function (err, scope) {
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) {
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) {
// 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
});
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
});
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);
});
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);
});
});
@ -63,9 +72,6 @@ describe('security scopes', function () {
describe('security ACLs', 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,
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 () {
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: 'testModel', property: ACL.ALL,
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) {
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);
});
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);
});
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);
});
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);
});
@ -119,8 +122,7 @@ describe('security ACLs', function () {
});
it("should honor defaultPermission from the model", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
var ds = this.ds;
var Customer = ds.createModel('Customer', {
name: {
type: String,
@ -152,7 +154,7 @@ describe('security ACLs', 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', {
name: {
type: String,
@ -188,14 +190,9 @@ describe('security ACLs', 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 = function() {};
var ds = this.ds;
// Create
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
@ -246,21 +243,7 @@ describe('security ACLs', function () {
}, function(err, access) {
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.model(Model)', function() {
@ -75,6 +78,23 @@ describe('app', function() {
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() {
beforeEach(function() {
this.boot = function () {
@ -162,7 +182,7 @@ describe('app', function() {
it('Load config files', function () {
var app = loopback();
app.boot(require('path').join(__dirname, 'fixtures', 'simple-app'));
app.boot(SIMPLE_APP);
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

@ -3,109 +3,167 @@ var assert = require('assert');
var Application = loopback.Application;
describe('Application', function () {
var registeredApp = null;
var registeredApp = null;
it('Create a new application', function (done) {
Application.create({owner: 'rfeng', name: 'MyApp1', description: 'My first mobile application'}, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp1');
assert.equal(app.description, 'My first mobile application');
assert(app.clientKey);
assert(app.javaScriptKey);
assert(app.restApiKey);
assert(app.windowsKey);
assert(app.masterKey);
assert(app.created);
assert(app.modified);
done(err, result);
});
it('Create a new application', function (done) {
Application.create({owner: 'rfeng',
name: 'MyApp1',
description: 'My first mobile application'}, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp1');
assert.equal(app.description, 'My first mobile application');
assert(app.clientKey);
assert(app.javaScriptKey);
assert(app.restApiKey);
assert(app.windowsKey);
assert(app.masterKey);
assert(app.created);
assert(app.modified);
done(err, result);
});
});
beforeEach(function (done) {
Application.register('rfeng', 'MyApp2', {description: 'My second mobile application'}, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp2');
assert.equal(app.description, 'My second mobile application');
assert(app.clientKey);
assert(app.javaScriptKey);
assert(app.restApiKey);
assert(app.windowsKey);
assert(app.masterKey);
assert(app.created);
assert(app.modified);
registeredApp = app;
done(err, result);
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) {
Application.register('rfeng', 'MyApp2',
{description: 'My second mobile application'}, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp2');
assert.equal(app.description, 'My second mobile application');
assert(app.clientKey);
assert(app.javaScriptKey);
assert(app.restApiKey);
assert(app.windowsKey);
assert(app.masterKey);
assert(app.created);
assert(app.modified);
registeredApp = app;
done(err, result);
});
});
it('Reset keys', function (done) {
Application.resetKeys(registeredApp.id, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp2');
assert.equal(app.description, 'My second mobile application');
assert(app.clientKey);
assert(app.javaScriptKey);
assert(app.restApiKey);
assert(app.windowsKey);
assert(app.masterKey);
assert(app.clientKey !== registeredApp.clientKey);
assert(app.javaScriptKey !== registeredApp.javaScriptKey);
assert(app.restApiKey !== registeredApp.restApiKey);
assert(app.windowsKey !== registeredApp.windowsKey);
assert(app.masterKey !== registeredApp.masterKey);
assert(app.created);
assert(app.modified);
registeredApp = app;
done(err, result);
});
});
it('Reset keys', function (done) {
Application.resetKeys(registeredApp.id, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp2');
assert.equal(app.description, 'My second mobile application');
assert(app.clientKey);
assert(app.javaScriptKey);
assert(app.restApiKey);
assert(app.windowsKey);
assert(app.masterKey);
it('Authenticate with application id & clientKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.clientKey,
function (err, result) {
assert.equal(result, 'clientKey');
done(err, result);
});
});
assert(app.clientKey !== registeredApp.clientKey);
assert(app.javaScriptKey !== registeredApp.javaScriptKey);
assert(app.restApiKey !== registeredApp.restApiKey);
assert(app.windowsKey !== registeredApp.windowsKey);
assert(app.masterKey !== registeredApp.masterKey);
it('Authenticate with application id & javaScriptKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.javaScriptKey,
function (err, result) {
assert.equal(result, 'javaScriptKey');
done(err, result);
});
});
assert(app.created);
assert(app.modified);
registeredApp = app;
done(err, result);
});
});
it('Authenticate with application id & restApiKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.restApiKey,
function (err, result) {
assert.equal(result, 'restApiKey');
done(err, result);
});
});
it('Authenticate with application id & clientKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.clientKey, function (err, result) {
assert.equal(result, 'clientKey');
done(err, result);
});
});
it('Authenticate with application id & masterKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.masterKey,
function (err, result) {
assert.equal(result, 'masterKey');
done(err, result);
});
});
it('Authenticate with application id & javaScriptKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.javaScriptKey, function (err, result) {
assert.equal(result, 'javaScriptKey');
done(err, result);
});
});
it('Authenticate with application id & windowsKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.windowsKey,
function (err, result) {
assert.equal(result, 'windowsKey');
done(err, result);
});
});
it('Authenticate with application id & restApiKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.restApiKey, function (err, result) {
assert.equal(result, 'restApiKey');
done(err, result);
});
});
it('Authenticate with application id & masterKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.masterKey, function (err, result) {
assert.equal(result, 'masterKey');
done(err, result);
});
});
it('Authenticate with application id & windowsKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.windowsKey, function (err, result) {
assert.equal(result, 'windowsKey');
done(err, result);
});
});
it('Fail to authenticate with application id & invalid key', function (done) {
Application.authenticate(registeredApp.id, 'invalid-key', function (err, result) {
assert(!result);
done(err, result);
});
});
it('Fail to authenticate with application id & invalid key', function (done) {
Application.authenticate(registeredApp.id, 'invalid-key',
function (err, result) {
assert(!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) {
app.use(loopback.token());
app.use(loopback.rest());
app.model(User);
@ -153,7 +154,6 @@ describe('User', function(){
it('Logout a user by providing the current accessToken id (over rest)', function(done) {
login(logout);
function login(fn) {
request(app)
.post('/users/login')
@ -171,22 +171,22 @@ describe('User', function(){
});
}
function logout(err, sid) {
function logout(err, token) {
request(app)
.post('/users/logout')
.post('/users/logout')
.set('Authorization', token)
.expect(204)
.send({sid: sid})
.end(verify(sid, done));
.end(verify(token, done));
}
});
function verify(sid, done) {
assert(sid);
function verify(token, done) {
assert(token);
return function (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');
done(err);
});