Merge branch 'release/2.6.0' into production
This commit is contained in:
commit
29bfcce9fc
|
@ -5,9 +5,10 @@
|
|||
"indent": 2,
|
||||
"undef": true,
|
||||
"quotmark": "single",
|
||||
"maxlen": 80,
|
||||
"maxlen": 150,
|
||||
"trailing": true,
|
||||
"newcap": true,
|
||||
"nonew": true,
|
||||
"undef": false
|
||||
"laxcomma": true,
|
||||
"laxbreak": true
|
||||
}
|
||||
|
|
19
Gruntfile.js
19
Gruntfile.js
|
@ -30,8 +30,11 @@ module.exports = function(grunt) {
|
|||
gruntfile: {
|
||||
src: 'Gruntfile.js'
|
||||
},
|
||||
lib_test: {
|
||||
src: ['lib/**/*.js', 'test/**/*.js']
|
||||
lib: {
|
||||
src: ['lib/**/*.js']
|
||||
},
|
||||
test: {
|
||||
src: ['test/**/*.js']
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -39,9 +42,13 @@ module.exports = function(grunt) {
|
|||
files: '<%= jshint.gruntfile.src %>',
|
||||
tasks: ['jshint:gruntfile']
|
||||
},
|
||||
lib_test: {
|
||||
files: '<%= jshint.lib_test.src %>',
|
||||
tasks: ['jshint:lib_test']
|
||||
lib: {
|
||||
files: ['<%= jshint.lib.src %>'],
|
||||
tasks: ['jshint:lib']
|
||||
},
|
||||
test: {
|
||||
files: ['<%= jshint.test.src %>'],
|
||||
tasks: ['jshint:test']
|
||||
}
|
||||
},
|
||||
browserify: {
|
||||
|
@ -104,7 +111,7 @@ module.exports = function(grunt) {
|
|||
|
||||
// list of files to exclude
|
||||
exclude: [
|
||||
|
||||
|
||||
],
|
||||
|
||||
// test results reporter to use
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
/*!
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var loopback = require('../../lib/loopback')
|
||||
, assert = require('assert')
|
||||
, uid = require('uid2')
|
||||
, DEFAULT_TOKEN_LEN = 64;
|
||||
|
||||
/**
|
||||
* Token based authentication and access control.
|
||||
*
|
||||
* **Default ACLs**
|
||||
*
|
||||
* - DENY EVERYONE `*`
|
||||
* - ALLOW EVERYONE create
|
||||
*
|
||||
* @property {String} id Generated token ID
|
||||
* @property {Number} ttl Time to live in seconds, 2 weeks by default.
|
||||
* @property {Date} created When the token was created
|
||||
*
|
||||
* @class AccessToken
|
||||
* @inherits {PersistedModel}
|
||||
*/
|
||||
|
||||
module.exports = function(AccessToken) {
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
AccessToken.definition.rawProperties.created.default =
|
||||
AccessToken.definition.properties.created.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
/**
|
||||
* Anonymous Token
|
||||
*
|
||||
* ```js
|
||||
* assert(AccessToken.ANONYMOUS.id === '$anonymous');
|
||||
* ```
|
||||
*/
|
||||
|
||||
AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'});
|
||||
|
||||
/**
|
||||
* Create a cryptographically random access token id.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {String} token
|
||||
*/
|
||||
|
||||
AccessToken.createAccessTokenId = function(fn) {
|
||||
uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) {
|
||||
if (err) {
|
||||
fn(err);
|
||||
} else {
|
||||
fn(null, guid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*!
|
||||
* Hook to create accessToken id.
|
||||
*/
|
||||
|
||||
AccessToken.beforeCreate = function(next, data) {
|
||||
data = data || {};
|
||||
|
||||
AccessToken.createAccessTokenId(function(err, id) {
|
||||
if (err) {
|
||||
next(err);
|
||||
} else {
|
||||
data.id = id;
|
||||
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a token for the given `ServerRequest`.
|
||||
*
|
||||
* @param {ServerRequest} req
|
||||
* @param {Object} [options] Options for finding the token
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {AccessToken} token
|
||||
*/
|
||||
|
||||
AccessToken.findForRequest = function(req, options, cb) {
|
||||
var id = tokenIdForRequest(req, options);
|
||||
|
||||
if (id) {
|
||||
this.findById(id, function(err, token) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else if (token) {
|
||||
token.validate(function(err, isValid) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else if (isValid) {
|
||||
cb(null, token);
|
||||
} else {
|
||||
var e = new Error('Invalid Access Token');
|
||||
e.status = e.statusCode = 401;
|
||||
cb(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the token.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Boolean} isValid
|
||||
*/
|
||||
|
||||
AccessToken.prototype.validate = function(cb) {
|
||||
try {
|
||||
assert(
|
||||
this.created && typeof this.created.getTime === 'function',
|
||||
'token.created must be a valid Date'
|
||||
);
|
||||
assert(this.ttl !== 0, 'token.ttl must be not be 0');
|
||||
assert(this.ttl, 'token.ttl must exist');
|
||||
assert(this.ttl >= -1, 'token.ttl must be >= -1');
|
||||
|
||||
var now = Date.now();
|
||||
var created = this.created.getTime();
|
||||
var elapsedSeconds = (now - created) / 1000;
|
||||
var secondsToLive = this.ttl;
|
||||
var isValid = elapsedSeconds < secondsToLive;
|
||||
|
||||
if (isValid) {
|
||||
cb(null, isValid);
|
||||
} else {
|
||||
this.destroy(function(err) {
|
||||
cb(err, isValid);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
cb(e);
|
||||
}
|
||||
}
|
||||
|
||||
function tokenIdForRequest(req, options) {
|
||||
var params = options.params || [];
|
||||
var headers = options.headers || [];
|
||||
var cookies = options.cookies || [];
|
||||
var i = 0;
|
||||
var length;
|
||||
var id;
|
||||
|
||||
params = params.concat(['access_token']);
|
||||
headers = headers.concat(['X-Access-Token', 'authorization']);
|
||||
cookies = cookies.concat(['access_token', 'authorization']);
|
||||
|
||||
for (length = params.length; i < length; i++) {
|
||||
id = req.param(params[i]);
|
||||
|
||||
if (typeof id === 'string') {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, length = headers.length; i < length; i++) {
|
||||
id = req.header(headers[i]);
|
||||
|
||||
if (typeof id === 'string') {
|
||||
// Add support for oAuth 2.0 bearer token
|
||||
// http://tools.ietf.org/html/rfc6750
|
||||
if (id.indexOf('Bearer ') === 0) {
|
||||
id = id.substring(7);
|
||||
// Decode from base64
|
||||
var buf = new Buffer(id, 'base64');
|
||||
id = buf.toString('utf8');
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.signedCookies) {
|
||||
for (i = 0, length = cookies.length; i < length; i++) {
|
||||
id = req.signedCookies[cookies[i]];
|
||||
|
||||
if (typeof id === 'string') {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "AccessToken",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"id": true
|
||||
},
|
||||
"ttl": {
|
||||
"type": "number",
|
||||
"ttl": true,
|
||||
"default": 1209600,
|
||||
"description": "time to live in seconds (2 weeks by default)"
|
||||
},
|
||||
"created": {
|
||||
"type": "Date"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "User",
|
||||
"foreignKey": "userId"
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"property": "create",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,462 @@
|
|||
/*!
|
||||
Schema ACL options
|
||||
|
||||
Object level permissions, for example, an album owned by a user
|
||||
|
||||
Factors to be authorized against:
|
||||
|
||||
* model name: Album
|
||||
* model instance properties: userId of the album, friends, shared
|
||||
* methods
|
||||
* app and/or user ids/roles
|
||||
** loggedIn
|
||||
** roles
|
||||
** userId
|
||||
** appId
|
||||
** none
|
||||
** everyone
|
||||
** relations: owner/friend/granted
|
||||
|
||||
Class level permissions, for example, Album
|
||||
* model name: Album
|
||||
* methods
|
||||
|
||||
URL/Route level permissions
|
||||
* url pattern
|
||||
* application id
|
||||
* ip addresses
|
||||
* http headers
|
||||
|
||||
Map to oAuth 2.0 scopes
|
||||
|
||||
*/
|
||||
|
||||
var loopback = require('../../lib/loopback');
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
var debug = require('debug')('loopback:security:acl');
|
||||
|
||||
var ctx = require('../../lib/access-context');
|
||||
var AccessContext = ctx.AccessContext;
|
||||
var Principal = ctx.Principal;
|
||||
var AccessRequest = ctx.AccessRequest;
|
||||
|
||||
var Role = loopback.Role;
|
||||
assert(Role, 'Role model must be defined before ACL model');
|
||||
|
||||
/**
|
||||
* A Model for access control meta data.
|
||||
*
|
||||
* System grants permissions to principals (users/applications, can be grouped
|
||||
* into roles).
|
||||
*
|
||||
* Protected resource: the model data and operations
|
||||
* (model/property/method/relation/…)
|
||||
*
|
||||
* For a given principal, such as client application and/or user, is it allowed
|
||||
* to access (read/write/execute)
|
||||
* the protected resource?
|
||||
*
|
||||
* @header ACL
|
||||
* @property {String} model Name of the model.
|
||||
* @property {String} property Name of the property, method, scope, or relation.
|
||||
* @property {String} accessType Type of access being granted: one of READ, WRITE, or EXECUTE.
|
||||
* @property {String} permission Type of permission granted. One of:
|
||||
* - ALARM: Generate an alarm, in a system-dependent way, the access specified in the permissions component of the ACL entry.
|
||||
* - ALLOW: Explicitly grants access to the resource.
|
||||
* - AUDIT: Log, in a system-dependent way, the access specified in the permissions component of the ACL entry.
|
||||
* - DENY: Explicitly denies access to the resource.
|
||||
* @property {String} principalType Type of the principal; one of: Application, Use, Role.
|
||||
* @property {String} principalId ID of the principal - such as appId, userId or roleId
|
||||
*
|
||||
* @class ACL
|
||||
* @inherits PersistedModel
|
||||
*/
|
||||
|
||||
module.exports = function(ACL) {
|
||||
|
||||
ACL.ALL = AccessContext.ALL;
|
||||
|
||||
ACL.DEFAULT = AccessContext.DEFAULT; // Not specified
|
||||
ACL.ALLOW = AccessContext.ALLOW; // Allow
|
||||
ACL.ALARM = AccessContext.ALARM; // Warn - send an alarm
|
||||
ACL.AUDIT = AccessContext.AUDIT; // Audit - record the access
|
||||
ACL.DENY = AccessContext.DENY; // Deny
|
||||
|
||||
ACL.READ = AccessContext.READ; // Read operation
|
||||
ACL.WRITE = AccessContext.WRITE; // Write operation
|
||||
ACL.EXECUTE = AccessContext.EXECUTE; // Execute operation
|
||||
|
||||
ACL.USER = Principal.USER;
|
||||
ACL.APP = ACL.APPLICATION = Principal.APPLICATION;
|
||||
ACL.ROLE = Principal.ROLE;
|
||||
ACL.SCOPE = Principal.SCOPE;
|
||||
|
||||
/**
|
||||
* Calculate the matching score for the given rule and request
|
||||
* @param {ACL} rule The ACL entry
|
||||
* @param {AccessRequest} req The request
|
||||
* @returns {Number}
|
||||
*/
|
||||
ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||
var props = ['model', 'property', 'accessType'];
|
||||
var score = 0;
|
||||
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
// Shift the score by 4 for each of the properties as the weight
|
||||
score = score * 4;
|
||||
var val1 = rule[props[i]] || ACL.ALL;
|
||||
var val2 = req[props[i]] || ACL.ALL;
|
||||
var isMatchingMethodName = props[i] === 'property' && req.methodNames.indexOf(val1) !== -1;
|
||||
|
||||
if (val1 === val2 || isMatchingMethodName) {
|
||||
// Exact match
|
||||
score += 3;
|
||||
} else if (val1 === ACL.ALL) {
|
||||
// Wildcard match
|
||||
score += 2;
|
||||
} else if (val2 === ACL.ALL) {
|
||||
// Doesn't match at all
|
||||
score += 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Weigh against the principal type into 4 levels
|
||||
// - user level (explicitly allow/deny a given user)
|
||||
// - app level (explicitly allow/deny a given app)
|
||||
// - role level (role based authorization)
|
||||
// - other
|
||||
// user > app > role > ...
|
||||
score = score * 4;
|
||||
switch (rule.principalType) {
|
||||
case ACL.USER:
|
||||
score += 4;
|
||||
break;
|
||||
case ACL.APP:
|
||||
score += 3;
|
||||
break;
|
||||
case ACL.ROLE:
|
||||
score += 2;
|
||||
break;
|
||||
default:
|
||||
score += 1;
|
||||
}
|
||||
|
||||
// Weigh against the roles
|
||||
// everyone < authenticated/unauthenticated < related < owner < ...
|
||||
score = score * 8;
|
||||
if (rule.principalType === ACL.ROLE) {
|
||||
switch (rule.principalId) {
|
||||
case Role.OWNER:
|
||||
score += 4;
|
||||
break;
|
||||
case Role.RELATED:
|
||||
score += 3;
|
||||
break;
|
||||
case Role.AUTHENTICATED:
|
||||
case Role.UNAUTHENTICATED:
|
||||
score += 2;
|
||||
break;
|
||||
case Role.EVERYONE:
|
||||
score += 1;
|
||||
break;
|
||||
default:
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
score = score * 4;
|
||||
score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1;
|
||||
return score;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get matching score for the given `AccessRequest`.
|
||||
* @param {AccessRequest} req The request
|
||||
* @returns {Number} score
|
||||
*/
|
||||
|
||||
ACL.prototype.score = function(req) {
|
||||
return this.constructor.getMatchingScore(this, req);
|
||||
}
|
||||
|
||||
/*!
|
||||
* Resolve permission from the ACLs
|
||||
* @param {Object[]) acls The list of ACLs
|
||||
* @param {Object} req The request
|
||||
* @returns {AccessRequest} result The effective ACL
|
||||
*/
|
||||
ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||
if (!(req instanceof AccessRequest)) {
|
||||
req = new AccessRequest(req);
|
||||
}
|
||||
// Sort by the matching score in descending order
|
||||
acls = acls.sort(function(rule1, rule2) {
|
||||
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
|
||||
});
|
||||
var permission = ACL.DEFAULT;
|
||||
var score = 0;
|
||||
|
||||
for (var i = 0; i < acls.length; i++) {
|
||||
score = ACL.getMatchingScore(acls[i], req);
|
||||
if (score < 0) {
|
||||
// the highest scored ACL did not match
|
||||
break;
|
||||
}
|
||||
if (!req.isWildcard()) {
|
||||
// We should stop from the first match for non-wildcard
|
||||
permission = acls[i].permission;
|
||||
break;
|
||||
} else {
|
||||
if (req.exactlyMatches(acls[i])) {
|
||||
permission = acls[i].permission;
|
||||
break;
|
||||
}
|
||||
// For wildcard match, find the strongest permission
|
||||
if (AccessContext.permissionOrder[acls[i].permission]
|
||||
> AccessContext.permissionOrder[permission]) {
|
||||
permission = acls[i].permission;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debug.enabled) {
|
||||
debug('The following ACLs were searched: ');
|
||||
acls.forEach(function(acl) {
|
||||
acl.debug();
|
||||
debug('with score:', acl.score(req));
|
||||
});
|
||||
}
|
||||
|
||||
var res = new AccessRequest(req.model, req.property, req.accessType,
|
||||
permission || ACL.DEFAULT);
|
||||
return res;
|
||||
};
|
||||
|
||||
/*!
|
||||
* Get the static ACLs from the model definition
|
||||
* @param {String} model The model name
|
||||
* @param {String} property The property/method/relation name
|
||||
*
|
||||
* @return {Object[]} An array of ACLs
|
||||
*/
|
||||
ACL.getStaticACLs = function getStaticACLs(model, property) {
|
||||
var modelClass = loopback.findModel(model);
|
||||
var staticACLs = [];
|
||||
if (modelClass && modelClass.settings.acls) {
|
||||
modelClass.settings.acls.forEach(function(acl) {
|
||||
if (!acl.property || acl.property === ACL.ALL
|
||||
|| property === acl.property) {
|
||||
staticACLs.push(new ACL({
|
||||
model: model,
|
||||
property: acl.property || ACL.ALL,
|
||||
principalType: acl.principalType,
|
||||
principalId: acl.principalId, // TODO: Should it be a name?
|
||||
accessType: acl.accessType || ACL.ALL,
|
||||
permission: acl.permission
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
var prop = modelClass &&
|
||||
(modelClass.definition.properties[property] // regular property
|
||||
|| (modelClass._scopeMeta && modelClass._scopeMeta[property]) // relation/scope
|
||||
|| modelClass[property] // static method
|
||||
|| modelClass.prototype[property]); // prototype method
|
||||
if (prop && prop.acls) {
|
||||
prop.acls.forEach(function(acl) {
|
||||
staticACLs.push(new ACL({
|
||||
model: modelClass.modelName,
|
||||
property: property,
|
||||
principalType: acl.principalType,
|
||||
principalId: acl.principalId,
|
||||
accessType: acl.accessType,
|
||||
permission: acl.permission
|
||||
}));
|
||||
});
|
||||
}
|
||||
return staticACLs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given principal is allowed to access the model/property
|
||||
* @param {String} principalType The principal type.
|
||||
* @param {String} principalId The principal ID.
|
||||
* @param {String} model The model name.
|
||||
* @param {String} property The property/method/relation name.
|
||||
* @param {String} accessType The access type.
|
||||
* @callback {Function} callback Callback function.
|
||||
* @param {String|Error} err The error object
|
||||
* @param {AccessRequest} result The access permission
|
||||
*/
|
||||
ACL.checkPermission = function checkPermission(principalType, principalId,
|
||||
model, property, accessType,
|
||||
callback) {
|
||||
if (principalId !== null && principalId !== undefined && (typeof principalId !== 'string')) {
|
||||
principalId = principalId.toString();
|
||||
}
|
||||
property = property || ACL.ALL;
|
||||
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
|
||||
accessType = accessType || ACL.ALL;
|
||||
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
|
||||
|
||||
var req = new AccessRequest(model, property, accessType);
|
||||
|
||||
var acls = this.getStaticACLs(model, property);
|
||||
|
||||
var resolved = this.resolvePermission(acls, req);
|
||||
|
||||
if (resolved && resolved.permission === ACL.DENY) {
|
||||
debug('Permission denied by statically resolved permission');
|
||||
debug(' Resolved Permission: %j', resolved);
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, resolved);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.find({where: {principalType: principalType, principalId: principalId,
|
||||
model: model, property: propertyQuery, accessType: accessTypeQuery}},
|
||||
function(err, dynACLs) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
acls = acls.concat(dynACLs);
|
||||
resolved = self.resolvePermission(acls, req);
|
||||
if (resolved && resolved.permission === ACL.DEFAULT) {
|
||||
var modelClass = loopback.findModel(model);
|
||||
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
|
||||
}
|
||||
callback && callback(null, resolved);
|
||||
});
|
||||
};
|
||||
|
||||
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.
|
||||
* @options {Object} context See below.
|
||||
* @property {Object[]} principals An array of principals.
|
||||
* @property {String|Model} model The model name or model class.
|
||||
* @property {*} id The model instance ID.
|
||||
* @property {String} property The property/method/relation name.
|
||||
* @property {String} accessType The access type: READE, WRITE, or EXECUTE.
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
|
||||
ACL.checkAccessForContext = function(context, callback) {
|
||||
if (!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
|
||||
var model = context.model;
|
||||
var property = context.property;
|
||||
var accessType = context.accessType;
|
||||
var modelName = context.modelName;
|
||||
|
||||
var methodNames = context.methodNames;
|
||||
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: methodNames.concat([ACL.ALL])};
|
||||
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
|
||||
|
||||
var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames);
|
||||
|
||||
var effectiveACLs = [];
|
||||
var staticACLs = this.getStaticACLs(model.modelName, property);
|
||||
|
||||
var self = this;
|
||||
var roleModel = loopback.getModelByType(Role);
|
||||
this.find({where: {model: model.modelName, property: propertyQuery,
|
||||
accessType: accessTypeQuery}}, function(err, acls) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
var inRoleTasks = [];
|
||||
|
||||
acls = acls.concat(staticACLs);
|
||||
|
||||
acls.forEach(function(acl) {
|
||||
// Check exact matches
|
||||
for (var i = 0; i < context.principals.length; i++) {
|
||||
var p = context.principals[i];
|
||||
if (p.type === acl.principalType
|
||||
&& String(p.id) === String(acl.principalId)) {
|
||||
effectiveACLs.push(acl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check role matches
|
||||
if (acl.principalType === ACL.ROLE) {
|
||||
inRoleTasks.push(function(done) {
|
||||
roleModel.isInRole(acl.principalId, context,
|
||||
function(err, inRole) {
|
||||
if (!err && inRole) {
|
||||
effectiveACLs.push(acl);
|
||||
}
|
||||
done(err, acl);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async.parallel(inRoleTasks, function(err, results) {
|
||||
if (err) {
|
||||
callback && callback(err, null);
|
||||
return;
|
||||
}
|
||||
var resolved = self.resolvePermission(effectiveACLs, req);
|
||||
if (resolved && resolved.permission === ACL.DEFAULT) {
|
||||
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
|
||||
}
|
||||
debug('---Resolved---');
|
||||
resolved.debug();
|
||||
callback && callback(null, resolved);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given access token can invoke the method
|
||||
* @param {AccessToken} token The access token
|
||||
* @param {String} model The model name
|
||||
* @param {*} modelId The model id
|
||||
* @param {String} method The method name
|
||||
* @callback {Function} callback Callback function
|
||||
* @param {String|Error} err The error object
|
||||
* @param {Boolean} allowed is the request allowed
|
||||
*/
|
||||
ACL.checkAccessForToken = function(token, model, modelId, method, callback) {
|
||||
assert(token, 'Access token is required');
|
||||
|
||||
var context = new AccessContext({
|
||||
accessToken: token,
|
||||
model: model,
|
||||
property: method,
|
||||
method: method,
|
||||
modelId: modelId
|
||||
});
|
||||
|
||||
this.checkAccessForContext(context, function(err, access) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
callback && callback(null, access.permission !== ACL.DENY);
|
||||
});
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "ACL",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the model"
|
||||
},
|
||||
"property": {
|
||||
"type": "string",
|
||||
"description": "The name of the property, method, scope, or relation"
|
||||
},
|
||||
"accessType": "string",
|
||||
"permission": "string",
|
||||
"principalType": "string",
|
||||
"principalId": "string"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
var assert = require('assert');
|
||||
|
||||
/*!
|
||||
* Application management functions
|
||||
*/
|
||||
|
||||
var crypto = require('crypto');
|
||||
|
||||
function generateKey(hmacKey, algorithm, encoding) {
|
||||
hmacKey = hmacKey || 'loopback';
|
||||
algorithm = algorithm || 'sha1';
|
||||
encoding = encoding || 'hex';
|
||||
var hmac = crypto.createHmac(algorithm, hmacKey);
|
||||
var buf = crypto.randomBytes(32);
|
||||
hmac.update(buf);
|
||||
var key = hmac.digest(encoding);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage client applications and organize their users.
|
||||
*
|
||||
* @property {String} id Generated ID.
|
||||
* @property {String} name Name; required.
|
||||
* @property {String} description Text description
|
||||
* @property {String} icon String Icon image URL.
|
||||
* @property {String} owner User ID of the developer who registers the application.
|
||||
* @property {String} email E-mail address
|
||||
* @property {Boolean} emailVerified Whether the e-mail is verified.
|
||||
* @property {String} url OAuth 2.0 application URL.
|
||||
* @property {String}[] callbackUrls The OAuth 2.0 code/token callback URL.
|
||||
* @property {String} status Status of the application; Either `production`, `sandbox` (default), or `disabled`.
|
||||
* @property {Date} created Date Application object was created. Default: current date.
|
||||
* @property {Date} modified Date Application object was modified. Default: current date.
|
||||
*
|
||||
* @property {Object} pushSettings.apns APNS configuration, see the options
|
||||
* below and also
|
||||
* https://github.com/argon/node-apn/blob/master/doc/apn.markdown
|
||||
* @property {Boolean} pushSettings.apns.production Whether to use production Apple Push Notification Service (APNS) servers to send push notifications.
|
||||
* If true, uses `gateway.push.apple.com:2195` and `feedback.push.apple.com:2196`.
|
||||
* If false, uses `gateway.sandbox.push.apple.com:2195` and `feedback.sandbox.push.apple.com:2196`
|
||||
* @property {String} pushSettings.apns.certData The certificate data loaded from the cert.pem file (APNS).
|
||||
* @property {String} pushSettings.apns.keyData The key data loaded from the key.pem file (APNS).
|
||||
* @property {String} pushSettings.apns.pushOptions.gateway (APNS).
|
||||
* @property {Number} pushSettings.apns.pushOptions.port (APNS).
|
||||
* @property {String} pushSettings.apns.feedbackOptions.gateway (APNS).
|
||||
* @property {Number} pushSettings.apns.feedbackOptions.port (APNS).
|
||||
* @property {Boolean} pushSettings.apns.feedbackOptions.batchFeedback (APNS).
|
||||
* @property {Number} pushSettings.apns.feedbackOptions.interval (APNS).
|
||||
* @property {String} pushSettings.gcm.serverApiKey: Google Cloud Messaging API key.
|
||||
*
|
||||
* @property {Boolean} authenticationEnabled
|
||||
* @property {Boolean} anonymousAllowed
|
||||
* @property {Array} authenticationSchemes List of authentication schemes
|
||||
* (see below).
|
||||
* @property {String} authenticationSchemes.scheme Scheme name.
|
||||
* Supported values: `local`, `facebook`, `google`,
|
||||
* `twitter`, `linkedin`, `github`.
|
||||
* @property {Object} authenticationSchemes.credential
|
||||
* Scheme-specific credentials.
|
||||
*
|
||||
* @class Application
|
||||
* @inherits {PersistedModel}
|
||||
*/
|
||||
|
||||
module.exports = function(Application) {
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
Application.definition.rawProperties.created.default =
|
||||
Application.definition.properties.created.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
Application.definition.rawProperties.modified.default =
|
||||
Application.definition.properties.modified.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
/*!
|
||||
* A hook to generate keys before creation
|
||||
* @param next
|
||||
*/
|
||||
Application.beforeCreate = function(next) {
|
||||
var app = this;
|
||||
app.created = app.modified = new Date();
|
||||
app.id = generateKey('id', 'md5');
|
||||
app.clientKey = generateKey('client');
|
||||
app.javaScriptKey = generateKey('javaScript');
|
||||
app.restApiKey = generateKey('restApi');
|
||||
app.windowsKey = generateKey('windows');
|
||||
app.masterKey = generateKey('master');
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a new application
|
||||
* @param {String} owner Owner's user ID.
|
||||
* @param {String} name Name of the application
|
||||
* @param {Object} options Other options
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
Application.register = function(owner, name, options, cb) {
|
||||
assert(owner, 'owner is required');
|
||||
assert(name, 'name is required');
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
this.create(props, cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset keys for the application instance
|
||||
* @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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset keys for a given application by the appId
|
||||
* @param {Any} appId
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
*/
|
||||
Application.resetKeys = function(appId, cb) {
|
||||
this.findById(appId, function(err, app) {
|
||||
if (err) {
|
||||
cb && cb(err, app);
|
||||
return;
|
||||
}
|
||||
app.resetKeys(cb);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate the application id and key.
|
||||
*
|
||||
* @param {Any} appId
|
||||
* @param {String} key
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {String} matched The matching key; one of:
|
||||
* - clientKey
|
||||
* - javaScriptKey
|
||||
* - restApiKey
|
||||
* - windowsKey
|
||||
* - masterKey
|
||||
*
|
||||
*/
|
||||
Application.authenticate = function(appId, key, cb) {
|
||||
this.findById(appId, function(err, app) {
|
||||
if (err || !app) {
|
||||
cb && cb(err, null);
|
||||
return;
|
||||
}
|
||||
var result = null;
|
||||
var keyNames = ['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'];
|
||||
for (var i = 0; i < keyNames.length; i++) {
|
||||
if (app[keyNames[i]] === key) {
|
||||
result = {
|
||||
application: app,
|
||||
keyType: keyNames[i]
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
cb && cb(null, result);
|
||||
});
|
||||
};
|
||||
};
|
|
@ -0,0 +1,121 @@
|
|||
{
|
||||
"name": "Application",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"id": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"description": "string",
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "The icon image url"
|
||||
},
|
||||
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "The user id of the developer who registers the application"
|
||||
},
|
||||
"collaborators": {
|
||||
"type": ["string"],
|
||||
"description": "A list of users ids who have permissions to work on this app"
|
||||
},
|
||||
|
||||
"email": "string",
|
||||
"emailVerified": "boolean",
|
||||
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The application URL for OAuth 2.0"
|
||||
},
|
||||
"callbackUrls": {
|
||||
"type": ["string"],
|
||||
"description": "OAuth 2.0 code/token callback URLs"
|
||||
},
|
||||
"permissions": {
|
||||
"type": ["string"],
|
||||
"description": "A list of permissions required by the application"
|
||||
},
|
||||
|
||||
"clientKey": "string",
|
||||
"javaScriptKey": "string",
|
||||
"restApiKey": "string",
|
||||
"windowsKey": "string",
|
||||
"masterKey": "string",
|
||||
|
||||
"pushSettings": {
|
||||
"apns": {
|
||||
"production": {
|
||||
"type": "boolean",
|
||||
"description": [
|
||||
"Production or development mode. It denotes what default APNS",
|
||||
"servers to be used to send notifications.",
|
||||
"See API documentation for more details."
|
||||
]
|
||||
},
|
||||
|
||||
"certData": {
|
||||
"type": "string",
|
||||
"description": "The certificate data loaded from the cert.pem file"
|
||||
},
|
||||
"keyData": {
|
||||
"type": "string",
|
||||
"description": "The key data loaded from the key.pem file"
|
||||
},
|
||||
|
||||
"pushOptions": {
|
||||
"type": {
|
||||
"gateway": "string",
|
||||
"port": "number"
|
||||
}
|
||||
},
|
||||
|
||||
"feedbackOptions": {
|
||||
"type": {
|
||||
"gateway": "string",
|
||||
"port": "number",
|
||||
"batchFeedback": "boolean",
|
||||
"interval": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"gcm": {
|
||||
"serverApiKey": "string"
|
||||
}
|
||||
},
|
||||
|
||||
"authenticationEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"anonymousAllowed": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"authenticationSchemes": [
|
||||
{
|
||||
"scheme": {
|
||||
"type": "string",
|
||||
"description": "See the API docs for the list of supported values."
|
||||
},
|
||||
"credential": {
|
||||
"type": "object",
|
||||
"description": "Scheme-specific credentials"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"status": {
|
||||
"type": "string",
|
||||
"default": "sandbox",
|
||||
"description": "Status of the application, production/sandbox/disabled"
|
||||
},
|
||||
|
||||
"created": "date",
|
||||
"modified": "date"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,628 @@
|
|||
/*!
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var PersistedModel = require('../../lib/loopback').PersistedModel
|
||||
, loopback = require('../../lib/loopback')
|
||||
, crypto = require('crypto')
|
||||
, CJSON = {stringify: require('canonical-json')}
|
||||
, async = require('async')
|
||||
, assert = require('assert')
|
||||
, debug = require('debug')('loopback:change');
|
||||
|
||||
|
||||
/**
|
||||
* Change list entry.
|
||||
*
|
||||
* @property {String} id Hash of the modelName and id
|
||||
* @property {String} rev The current model revision
|
||||
* @property {String} prev The previous model revision
|
||||
* @property {Number} checkpoint The current checkpoint at time of the change
|
||||
* @property {String} modelName Model name
|
||||
* @property {String} modelId Model ID
|
||||
*
|
||||
* @class Change
|
||||
* @inherits {PersistedModel}
|
||||
*/
|
||||
|
||||
module.exports = function(Change) {
|
||||
|
||||
/*!
|
||||
* Constants
|
||||
*/
|
||||
|
||||
Change.UPDATE = 'update';
|
||||
Change.CREATE = 'create';
|
||||
Change.DELETE = 'delete';
|
||||
Change.UNKNOWN = 'unknown';
|
||||
|
||||
/*!
|
||||
* Conflict Class
|
||||
*/
|
||||
|
||||
Change.Conflict = Conflict;
|
||||
|
||||
/*!
|
||||
* Setup the extended model.
|
||||
*/
|
||||
|
||||
Change.setup = function() {
|
||||
PersistedModel.setup.call(this);
|
||||
var Change = this;
|
||||
|
||||
Change.getter.id = function() {
|
||||
var hasModel = this.modelName && this.modelId;
|
||||
if (!hasModel) return null;
|
||||
|
||||
return Change.idForModel(this.modelName, this.modelId);
|
||||
}
|
||||
}
|
||||
Change.setup();
|
||||
|
||||
/**
|
||||
* Track the recent change of the given modelIds.
|
||||
*
|
||||
* @param {String} modelName
|
||||
* @param {Array} modelIds
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Array} changes Changes that were tracked
|
||||
*/
|
||||
|
||||
Change.rectifyModelChanges = function(modelName, modelIds, callback) {
|
||||
var tasks = [];
|
||||
var Change = this;
|
||||
|
||||
modelIds.forEach(function(id) {
|
||||
tasks.push(function(cb) {
|
||||
Change.findOrCreateChange(modelName, id, function(err, change) {
|
||||
if (err) return Change.handleError(err, cb);
|
||||
change.rectify(cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
async.parallel(tasks, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an identifier for a given model.
|
||||
*
|
||||
* @param {String} modelName
|
||||
* @param {String} modelId
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
Change.idForModel = function(modelName, modelId) {
|
||||
return this.hash([modelName, modelId].join('-'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a change for the given model.
|
||||
*
|
||||
* @param {String} modelName
|
||||
* @param {String} modelId
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Change} change
|
||||
* @end
|
||||
*/
|
||||
|
||||
Change.findOrCreateChange = function(modelName, modelId, callback) {
|
||||
assert(loopback.findModel(modelName), modelName + ' does not exist');
|
||||
var id = this.idForModel(modelName, modelId);
|
||||
var Change = this;
|
||||
|
||||
this.findById(id, function(err, change) {
|
||||
if (err) return callback(err);
|
||||
if (change) {
|
||||
callback(null, change);
|
||||
} else {
|
||||
var ch = new Change({
|
||||
id: id,
|
||||
modelName: modelName,
|
||||
modelId: modelId
|
||||
});
|
||||
ch.debug('creating change');
|
||||
ch.save(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update (or create) the change with the current revision.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
Change.prototype.rectify = function(cb) {
|
||||
var change = this;
|
||||
var tasks = [
|
||||
updateRevision,
|
||||
updateCheckpoint
|
||||
];
|
||||
var currentRev = this.rev;
|
||||
|
||||
change.debug('rectify change');
|
||||
|
||||
cb = cb || function(err) {
|
||||
if (err) throw new Error(err);
|
||||
}
|
||||
|
||||
async.parallel(tasks, function(err) {
|
||||
if (err) return cb(err);
|
||||
if (change.prev === Change.UNKNOWN) {
|
||||
// this occurs when a record of a change doesn't exist
|
||||
// and its current revision is null (not found)
|
||||
change.remove(cb);
|
||||
} else {
|
||||
change.save(cb);
|
||||
}
|
||||
});
|
||||
|
||||
function updateRevision(cb) {
|
||||
// get the current revision
|
||||
change.currentRevision(function(err, rev) {
|
||||
if (err) return Change.handleError(err, cb);
|
||||
if (rev) {
|
||||
// avoid setting rev and prev to the same value
|
||||
if (currentRev !== rev) {
|
||||
change.rev = rev;
|
||||
change.prev = currentRev;
|
||||
} else {
|
||||
change.debug('rev and prev are equal (not updating rev)');
|
||||
}
|
||||
} else {
|
||||
change.rev = null;
|
||||
if (currentRev) {
|
||||
change.prev = currentRev;
|
||||
} else if (!change.prev) {
|
||||
change.debug('ERROR - could not determing prev');
|
||||
change.prev = Change.UNKNOWN;
|
||||
}
|
||||
}
|
||||
change.debug('updated revision (was ' + currentRev + ')');
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function updateCheckpoint(cb) {
|
||||
change.constructor.getCheckpointModel().current(function(err, checkpoint) {
|
||||
if (err) return Change.handleError(err);
|
||||
change.checkpoint = checkpoint;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a change's current revision based on current data.
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {String} rev The current revision
|
||||
*/
|
||||
|
||||
Change.prototype.currentRevision = function(cb) {
|
||||
var model = this.getModelCtor();
|
||||
var id = this.getModelId();
|
||||
model.findById(id, function(err, inst) {
|
||||
if (err) return Change.handleError(err, cb);
|
||||
if (inst) {
|
||||
cb(null, Change.revisionForInst(inst));
|
||||
} else {
|
||||
cb(null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hash of the given `string` with the `options.hashAlgorithm`.
|
||||
* **Default: `sha1`**
|
||||
*
|
||||
* @param {String} str The string to be hashed
|
||||
* @return {String} The hashed string
|
||||
*/
|
||||
|
||||
Change.hash = function(str) {
|
||||
return crypto
|
||||
.createHash(Change.settings.hashAlgorithm || 'sha1')
|
||||
.update(str)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the revision string for the given object
|
||||
* @param {Object} inst The data to get the revision string for
|
||||
* @return {String} The revision string
|
||||
*/
|
||||
|
||||
Change.revisionForInst = function(inst) {
|
||||
return this.hash(CJSON.stringify(inst));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a change's type. Returns one of:
|
||||
*
|
||||
* - `Change.UPDATE`
|
||||
* - `Change.CREATE`
|
||||
* - `Change.DELETE`
|
||||
* - `Change.UNKNOWN`
|
||||
*
|
||||
* @return {String} the type of change
|
||||
*/
|
||||
|
||||
Change.prototype.type = function() {
|
||||
if (this.rev && this.prev) {
|
||||
return Change.UPDATE;
|
||||
}
|
||||
if (this.rev && !this.prev) {
|
||||
return Change.CREATE;
|
||||
}
|
||||
if (!this.rev && this.prev) {
|
||||
return Change.DELETE;
|
||||
}
|
||||
return Change.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two changes.
|
||||
* @param {Change} change
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
Change.prototype.equals = function(change) {
|
||||
if (!change) return false;
|
||||
var thisRev = this.rev || null;
|
||||
var thatRev = change.rev || null;
|
||||
return thisRev === thatRev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this change conflict with the given change.
|
||||
* @param {Change} change
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
Change.prototype.conflictsWith = function(change) {
|
||||
if (!change) return false;
|
||||
if (this.equals(change)) return false;
|
||||
if (Change.bothDeleted(this, change)) return false;
|
||||
if (this.isBasedOn(change)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are both changes deletes?
|
||||
* @param {Change} a
|
||||
* @param {Change} b
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
Change.bothDeleted = function(a, b) {
|
||||
return a.type() === Change.DELETE
|
||||
&& b.type() === Change.DELETE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the change is based on the given change.
|
||||
* @param {Change} change
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
Change.prototype.isBasedOn = function(change) {
|
||||
return this.prev === change.rev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the differences for a given model since a given checkpoint.
|
||||
*
|
||||
* The callback will contain an error or `result`.
|
||||
*
|
||||
* **result**
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* deltas: Array,
|
||||
* conflicts: Array
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **deltas**
|
||||
*
|
||||
* An array of changes that differ from `remoteChanges`.
|
||||
*
|
||||
* **conflicts**
|
||||
*
|
||||
* An array of changes that conflict with `remoteChanges`.
|
||||
*
|
||||
* @param {String} modelName
|
||||
* @param {Number} since Compare changes after this checkpoint
|
||||
* @param {Change[]} remoteChanges A set of changes to compare
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Object} result See above.
|
||||
*/
|
||||
|
||||
Change.diff = function(modelName, since, remoteChanges, callback) {
|
||||
var remoteChangeIndex = {};
|
||||
var modelIds = [];
|
||||
remoteChanges.forEach(function(ch) {
|
||||
modelIds.push(ch.modelId);
|
||||
remoteChangeIndex[ch.modelId] = new Change(ch);
|
||||
});
|
||||
|
||||
// normalize `since`
|
||||
since = Number(since) || 0;
|
||||
this.find({
|
||||
where: {
|
||||
modelName: modelName,
|
||||
modelId: {inq: modelIds},
|
||||
checkpoint: {gte: since}
|
||||
}
|
||||
}, function(err, localChanges) {
|
||||
if (err) return callback(err);
|
||||
var deltas = [];
|
||||
var conflicts = [];
|
||||
var localModelIds = [];
|
||||
|
||||
localChanges.forEach(function(localChange) {
|
||||
localChange = new Change(localChange);
|
||||
localModelIds.push(localChange.modelId);
|
||||
var remoteChange = remoteChangeIndex[localChange.modelId];
|
||||
if (remoteChange && !localChange.equals(remoteChange)) {
|
||||
if (remoteChange.conflictsWith(localChange)) {
|
||||
remoteChange.debug('remote conflict');
|
||||
localChange.debug('local conflict');
|
||||
conflicts.push(localChange);
|
||||
} else {
|
||||
remoteChange.debug('remote delta');
|
||||
deltas.push(remoteChange);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
modelIds.forEach(function(id) {
|
||||
if (localModelIds.indexOf(id) === -1) {
|
||||
deltas.push(remoteChangeIndex[id]);
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, {
|
||||
deltas: deltas,
|
||||
conflicts: conflicts
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct all change list entries.
|
||||
* @param {Function} callback
|
||||
*/
|
||||
|
||||
Change.rectifyAll = function(cb) {
|
||||
debug('rectify all');
|
||||
var Change = this;
|
||||
// this should be optimized
|
||||
this.find(function(err, changes) {
|
||||
if (err) return cb(err);
|
||||
changes.forEach(function(change) {
|
||||
change = new Change(change);
|
||||
change.rectify();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checkpoint model.
|
||||
* @return {Checkpoint}
|
||||
*/
|
||||
|
||||
Change.getCheckpointModel = function() {
|
||||
var checkpointModel = this.Checkpoint;
|
||||
if (checkpointModel) return checkpointModel;
|
||||
this.checkpoint = checkpointModel = loopback.Checkpoint.extend('checkpoint');
|
||||
assert(this.dataSource, 'Cannot getCheckpointModel(): ' + this.modelName
|
||||
+ ' is not attached to a dataSource');
|
||||
checkpointModel.attachTo(this.dataSource);
|
||||
return checkpointModel;
|
||||
}
|
||||
|
||||
Change.handleError = function(err) {
|
||||
if (!this.settings.ignoreErrors) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
Change.prototype.debug = function() {
|
||||
if (debug.enabled) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
debug.apply(this, args);
|
||||
debug('\tid', this.id);
|
||||
debug('\trev', this.rev);
|
||||
debug('\tprev', this.prev);
|
||||
debug('\tmodelName', this.modelName);
|
||||
debug('\tmodelId', this.modelId);
|
||||
debug('\ttype', this.type());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `Model` class for `change.modelName`.
|
||||
* @return {Model}
|
||||
*/
|
||||
|
||||
Change.prototype.getModelCtor = function() {
|
||||
return this.constructor.settings.trackModel;
|
||||
}
|
||||
|
||||
Change.prototype.getModelId = function() {
|
||||
// TODO(ritch) get rid of the need to create an instance
|
||||
var Model = this.getModelCtor();
|
||||
var id = this.modelId;
|
||||
var m = new Model();
|
||||
m.setId(id);
|
||||
return m.getId();
|
||||
}
|
||||
|
||||
Change.prototype.getModel = function(callback) {
|
||||
var Model = this.constructor.settings.trackModel;
|
||||
var id = this.getModelId();
|
||||
Model.findById(id, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* When two changes conflict a conflict is created.
|
||||
*
|
||||
* **Note**: call `conflict.fetch()` to get the `target` and `source` models.
|
||||
*
|
||||
* @param {*} modelId
|
||||
* @param {PersistedModel} SourceModel
|
||||
* @param {PersistedModel} TargetModel
|
||||
* @property {ModelClass} source The source model instance
|
||||
* @property {ModelClass} target The target model instance
|
||||
* @class Change.Conflict
|
||||
*/
|
||||
|
||||
function Conflict(modelId, SourceModel, TargetModel) {
|
||||
this.SourceModel = SourceModel;
|
||||
this.TargetModel = TargetModel;
|
||||
this.SourceChange = SourceModel.getChangeModel();
|
||||
this.TargetChange = TargetModel.getChangeModel();
|
||||
this.modelId = modelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the conflicting models.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {PersistedModel} source
|
||||
* @param {PersistedModel} target
|
||||
*/
|
||||
|
||||
Conflict.prototype.models = function(cb) {
|
||||
var conflict = this;
|
||||
var SourceModel = this.SourceModel;
|
||||
var TargetModel = this.TargetModel;
|
||||
var source;
|
||||
var target;
|
||||
|
||||
async.parallel([
|
||||
getSourceModel,
|
||||
getTargetModel
|
||||
], done);
|
||||
|
||||
function getSourceModel(cb) {
|
||||
SourceModel.findById(conflict.modelId, function(err, model) {
|
||||
if (err) return cb(err);
|
||||
source = model;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function getTargetModel(cb) {
|
||||
TargetModel.findById(conflict.modelId, function(err, model) {
|
||||
if (err) return cb(err);
|
||||
target = model;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function done(err) {
|
||||
if (err) return cb(err);
|
||||
cb(null, source, target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the conflicting changes.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Change} sourceChange
|
||||
* @param {Change} targetChange
|
||||
*/
|
||||
|
||||
Conflict.prototype.changes = function(cb) {
|
||||
var conflict = this;
|
||||
var sourceChange;
|
||||
var targetChange;
|
||||
|
||||
async.parallel([
|
||||
getSourceChange,
|
||||
getTargetChange
|
||||
], done);
|
||||
|
||||
function getSourceChange(cb) {
|
||||
conflict.SourceChange.findOne({where: {
|
||||
modelId: conflict.modelId
|
||||
}}, function(err, change) {
|
||||
if (err) return cb(err);
|
||||
sourceChange = change;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function getTargetChange(cb) {
|
||||
conflict.TargetChange.findOne({where: {
|
||||
modelId: conflict.modelId
|
||||
}}, function(err, change) {
|
||||
if (err) return cb(err);
|
||||
targetChange = change;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function done(err) {
|
||||
if (err) return cb(err);
|
||||
cb(null, sourceChange, targetChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the conflict.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
*/
|
||||
|
||||
Conflict.prototype.resolve = function(cb) {
|
||||
var conflict = this;
|
||||
conflict.changes(function(err, sourceChange, targetChange) {
|
||||
if (err) return cb(err);
|
||||
sourceChange.prev = targetChange.rev;
|
||||
sourceChange.save(cb);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the conflict type.
|
||||
*
|
||||
* Possible results are
|
||||
*
|
||||
* - `Change.UPDATE`: Source and target models were updated.
|
||||
* - `Change.DELETE`: Source and or target model was deleted.
|
||||
* - `Change.UNKNOWN`: the conflict type is uknown or due to an error.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {String} type The conflict type.
|
||||
*/
|
||||
|
||||
Conflict.prototype.type = function(cb) {
|
||||
var conflict = this;
|
||||
this.changes(function(err, sourceChange, targetChange) {
|
||||
if (err) return cb(err);
|
||||
var sourceChangeType = sourceChange.type();
|
||||
var targetChangeType = targetChange.type();
|
||||
if (sourceChangeType === Change.UPDATE && targetChangeType === Change.UPDATE) {
|
||||
return cb(null, Change.UPDATE);
|
||||
}
|
||||
if (sourceChangeType === Change.DELETE || targetChangeType === Change.DELETE) {
|
||||
return cb(null, Change.DELETE);
|
||||
}
|
||||
return cb(null, Change.UNKNOWN);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "Change",
|
||||
"trackChanges": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"id": true
|
||||
},
|
||||
"rev": {
|
||||
"type": "string"
|
||||
},
|
||||
"prev": {
|
||||
"type": "string"
|
||||
},
|
||||
"checkpoint": {
|
||||
"type": "number"
|
||||
},
|
||||
"modelName": {
|
||||
"type": "string"
|
||||
},
|
||||
"modelId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
/**
|
||||
* Checkpoint list entry.
|
||||
*
|
||||
* @property id {Number} the sequencial identifier of a checkpoint
|
||||
* @property time {Number} the time when the checkpoint was created
|
||||
* @property sourceId {String} the source identifier
|
||||
*
|
||||
* @class Checkpoint
|
||||
* @inherits {PersistedModel}
|
||||
*/
|
||||
|
||||
module.exports = function(Checkpoint) {
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
Checkpoint.definition.rawProperties.time.default =
|
||||
Checkpoint.definition.properties.time.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current checkpoint id
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Number} checkpointId The current checkpoint id
|
||||
*/
|
||||
|
||||
Checkpoint.current = function(cb) {
|
||||
var Checkpoint = this;
|
||||
this.find({
|
||||
limit: 1,
|
||||
order: 'seq DESC'
|
||||
}, function(err, checkpoints) {
|
||||
if (err) return cb(err);
|
||||
var checkpoint = checkpoints[0];
|
||||
if (checkpoint) {
|
||||
cb(null, checkpoint.seq);
|
||||
} else {
|
||||
Checkpoint.create({seq: 0}, function(err, checkpoint) {
|
||||
if (err) return cb(err);
|
||||
cb(null, checkpoint.seq);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Checkpoint.beforeSave = function(next, model) {
|
||||
if (!model.getId() && model.seq === undefined) {
|
||||
model.constructor.current(function(err, seq) {
|
||||
if (err) return next(err);
|
||||
model.seq = seq + 1;
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "Checkpoint",
|
||||
"properties": {
|
||||
"seq": {
|
||||
"type": "number"
|
||||
},
|
||||
"time": {
|
||||
"type": "date"
|
||||
},
|
||||
"sourceId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Email model. Extends LoopBack base [Model](#model-new-model).
|
||||
* @property {String} to Email addressee. Required.
|
||||
* @property {String} from Email sender address. Required.
|
||||
* @property {String} subject Email subject string. Required.
|
||||
* @property {String} text Text body of email.
|
||||
* @property {String} html HTML body of email.
|
||||
*
|
||||
* @class Email
|
||||
* @inherits {Model}
|
||||
*/
|
||||
|
||||
module.exports = function(Email) {
|
||||
|
||||
/**
|
||||
* Send an email with the given `options`.
|
||||
*
|
||||
* Example Options:
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* 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.
|
||||
*
|
||||
* @options {Object} options See below
|
||||
* @prop {String} from Senders's email address
|
||||
* @prop {String} to List of one or more recipient email addresses (comma-delimited)
|
||||
* @prop {String} subject Subject line
|
||||
* @prop {String} text Body text
|
||||
* @prop {String} html Body HTML (optional)
|
||||
* @param {Function} callback Called after the e-mail is sent or the sending failed
|
||||
*/
|
||||
|
||||
Email.send = function() {
|
||||
throw new Error('You must connect the Email Model to a Mail connector');
|
||||
};
|
||||
|
||||
/**
|
||||
* A shortcut for Email.send(this).
|
||||
*/
|
||||
Email.prototype.send = function() {
|
||||
throw new Error('You must connect the Email Model to a Mail connector');
|
||||
};
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "Email",
|
||||
"base": "Model",
|
||||
"properties": {
|
||||
"to": {"type": "String", "required": true},
|
||||
"from": {"type": "String", "required": true},
|
||||
"subject": {"type": "String", "required": true},
|
||||
"text": {"type": "String"},
|
||||
"html": {"type": "String"}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
var loopback = require('../../lib/loopback');
|
||||
|
||||
/**
|
||||
* The `RoleMapping` model extends from the built in `loopback.Model` type.
|
||||
*
|
||||
* @property {String} id Generated ID.
|
||||
* @property {String} name Name of the role.
|
||||
* @property {String} Description Text description.
|
||||
*
|
||||
* @class RoleMapping
|
||||
* @inherits {PersistedModel}
|
||||
*/
|
||||
|
||||
module.exports = function(RoleMapping) {
|
||||
// Principal types
|
||||
RoleMapping.USER = 'USER';
|
||||
RoleMapping.APP = RoleMapping.APPLICATION = 'APP';
|
||||
RoleMapping.ROLE = 'ROLE';
|
||||
|
||||
/**
|
||||
* Get the application principal
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Application} application
|
||||
*/
|
||||
RoleMapping.prototype.application = function (callback) {
|
||||
if (this.principalType === RoleMapping.APPLICATION) {
|
||||
var applicationModel = this.constructor.Application
|
||||
|| loopback.getModelByType(loopback.Application);
|
||||
applicationModel.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the user principal
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {User} user
|
||||
*/
|
||||
RoleMapping.prototype.user = function (callback) {
|
||||
if (this.principalType === RoleMapping.USER) {
|
||||
var userModel = this.constructor.User
|
||||
|| loopback.getModelByType(loopback.User);
|
||||
userModel.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the child role principal
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {User} childUser
|
||||
*/
|
||||
RoleMapping.prototype.childRole = function (callback) {
|
||||
if (this.principalType === RoleMapping.ROLE) {
|
||||
var roleModel = this.constructor.Role ||
|
||||
loopback.getModelByType(loopback.Role);
|
||||
roleModel.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "RoleMapping",
|
||||
"description": "Map principals to roles",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"id": true,
|
||||
"generated": true
|
||||
},
|
||||
"principalType": {
|
||||
"type": "string",
|
||||
"description": "The principal type, such as user, application, or role"
|
||||
},
|
||||
"principalId": "string"
|
||||
},
|
||||
"relations": {
|
||||
"role": {
|
||||
"type": "belongsTo",
|
||||
"model": "Role",
|
||||
"foreignKey": "roleId"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,387 @@
|
|||
var loopback = require('../../lib/loopback');
|
||||
var debug = require('debug')('loopback:security:role');
|
||||
var assert = require('assert');
|
||||
var async = require('async');
|
||||
|
||||
var AccessContext = require('../../lib/access-context').AccessContext;
|
||||
|
||||
var RoleMapping = loopback.RoleMapping;
|
||||
assert(RoleMapping, 'RoleMapping model must be defined before Role model');
|
||||
|
||||
/**
|
||||
* The Role Model
|
||||
* @class Role
|
||||
*/
|
||||
module.exports = function(Role) {
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
Role.definition.rawProperties.created.default =
|
||||
Role.definition.properties.created.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
Role.definition.rawProperties.modified.default =
|
||||
Role.definition.properties.modified.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
// Set up the connection to users/applications/roles once the model
|
||||
Role.once('dataSourceAttached', function() {
|
||||
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
||||
Role.prototype.users = function(callback) {
|
||||
roleMappingModel.find({where: {roleId: this.id,
|
||||
principalType: RoleMapping.USER}}, function(err, mappings) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
return mappings.map(function(m) {
|
||||
return m.principalId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Role.prototype.applications = function(callback) {
|
||||
roleMappingModel.find({where: {roleId: this.id,
|
||||
principalType: RoleMapping.APPLICATION}}, function(err, mappings) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
return mappings.map(function(m) {
|
||||
return m.principalId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Role.prototype.roles = function(callback) {
|
||||
roleMappingModel.find({where: {roleId: this.id,
|
||||
principalType: RoleMapping.ROLE}}, function(err, mappings) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
return mappings.map(function(m) {
|
||||
return m.principalId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// Special roles
|
||||
Role.OWNER = '$owner'; // owner of the object
|
||||
Role.RELATED = "$related"; // any User with a relationship to the object
|
||||
Role.AUTHENTICATED = "$authenticated"; // authenticated user
|
||||
Role.UNAUTHENTICATED = "$unauthenticated"; // authenticated user
|
||||
Role.EVERYONE = "$everyone"; // everyone
|
||||
|
||||
/**
|
||||
* Add custom handler for roles.
|
||||
* @param {String} role Name of role.
|
||||
* @param {Function} resolver Function that determines if a principal is in the specified role.
|
||||
* Signature must be `function(role, context, callback)`
|
||||
*/
|
||||
Role.registerResolver = function(role, resolver) {
|
||||
if (!Role.resolvers) {
|
||||
Role.resolvers = {};
|
||||
}
|
||||
Role.resolvers[role] = resolver;
|
||||
};
|
||||
|
||||
Role.registerResolver(Role.OWNER, function(role, context, callback) {
|
||||
if (!context || !context.model || !context.modelId) {
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var modelClass = context.model;
|
||||
var modelId = context.modelId;
|
||||
var userId = context.getUserId();
|
||||
Role.isOwner(modelClass, modelId, userId, callback);
|
||||
});
|
||||
|
||||
function isUserClass(modelClass) {
|
||||
return modelClass === loopback.User ||
|
||||
modelClass.prototype instanceof loopback.User;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Check if two user ids matches
|
||||
* @param {*} id1
|
||||
* @param {*} id2
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function matches(id1, id2) {
|
||||
if (id1 === undefined || id1 === null || id1 === ''
|
||||
|| id2 === undefined || id2 === null || id2 === '') {
|
||||
return false;
|
||||
}
|
||||
// The id can be a MongoDB ObjectID
|
||||
return id1 === id2 || id1.toString() === id2.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given user ID is the owner the model instance.
|
||||
* @param {Function} modelClass The model class
|
||||
* @param {*} modelId The model ID
|
||||
* @param {*} userId The user ID
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||
assert(modelClass, 'Model class is required');
|
||||
debug('isOwner(): %s %s userId: %s', modelClass && modelClass.modelName, modelId, userId);
|
||||
// No userId is present
|
||||
if (!userId) {
|
||||
process.nextTick(function() {
|
||||
callback(null, false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Is the modelClass User or a subclass of User?
|
||||
if (isUserClass(modelClass)) {
|
||||
process.nextTick(function() {
|
||||
callback(null, matches(modelId, userId));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modelClass.findById(modelId, function(err, inst) {
|
||||
if (err || !inst) {
|
||||
debug('Model not found for id %j', modelId);
|
||||
callback && callback(err, false);
|
||||
return;
|
||||
}
|
||||
debug('Model found: %j', inst);
|
||||
var ownerId = inst.userId || inst.owner;
|
||||
if (ownerId) {
|
||||
callback && callback(null, matches(ownerId, userId));
|
||||
return;
|
||||
} else {
|
||||
// Try to follow belongsTo
|
||||
for (var r in modelClass.relations) {
|
||||
var rel = modelClass.relations[r];
|
||||
if (rel.type === 'belongsTo' && isUserClass(rel.modelTo)) {
|
||||
debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel);
|
||||
inst[r](function(err, user) {
|
||||
if (!err && user) {
|
||||
debug('User found: %j', user.id);
|
||||
callback && callback(null, matches(user.id, userId));
|
||||
} else {
|
||||
callback && callback(err, false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
debug('No matching belongsTo relation found for model %j and user: %j', modelId, userId);
|
||||
callback && callback(null, false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
|
||||
if (!context) {
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
Role.isAuthenticated(context, callback);
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if the user id is authenticated
|
||||
* @param {Object} context The security context
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Boolean} isAuthenticated
|
||||
*/
|
||||
Role.isAuthenticated = function isAuthenticated(context, callback) {
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, context.isAuthenticated());
|
||||
});
|
||||
};
|
||||
|
||||
Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) {
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, !context || !context.isAuthenticated());
|
||||
});
|
||||
});
|
||||
|
||||
Role.registerResolver(Role.EVERYONE, function(role, context, callback) {
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, true); // Always true
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if a given principal is in the role
|
||||
*
|
||||
* @param {String} role The role name
|
||||
* @param {Object} context The context object
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Boolean} isInRole
|
||||
*/
|
||||
Role.isInRole = function(role, context, callback) {
|
||||
if (!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
|
||||
debug('isInRole(): %s', role);
|
||||
context.debug();
|
||||
|
||||
var resolver = Role.resolvers[role];
|
||||
if (resolver) {
|
||||
debug('Custom resolver found for role %s', role);
|
||||
resolver(role, context, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.principals.length === 0) {
|
||||
debug('isInRole() returns: false');
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var inRole = context.principals.some(function(p) {
|
||||
|
||||
var principalType = p.type || undefined;
|
||||
var principalId = p.id || undefined;
|
||||
|
||||
// Check if it's the same role
|
||||
return principalType === RoleMapping.ROLE && principalId === role;
|
||||
});
|
||||
|
||||
if (inRole) {
|
||||
debug('isInRole() returns: %j', inRole);
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
||||
this.findOne({where: {name: role}}, function(err, result) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
if (!result) {
|
||||
callback && callback(null, false);
|
||||
return;
|
||||
}
|
||||
debug('Role found: %j', result);
|
||||
|
||||
// Iterate through the list of principals
|
||||
async.some(context.principals, function(p, done) {
|
||||
var principalType = p.type || undefined;
|
||||
var principalId = p.id || undefined;
|
||||
var roleId = result.id.toString();
|
||||
|
||||
if (principalId !== null && principalId !== undefined && (typeof principalId !== 'string')) {
|
||||
principalId = principalId.toString();
|
||||
}
|
||||
|
||||
if (principalType && principalId) {
|
||||
roleMappingModel.findOne({where: {roleId: roleId,
|
||||
principalType: principalType, principalId: principalId}},
|
||||
function(err, result) {
|
||||
debug('Role mapping found: %j', result);
|
||||
done(!err && result); // The only arg is the result
|
||||
});
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
done(false);
|
||||
});
|
||||
}
|
||||
}, function(inRole) {
|
||||
debug('isInRole() returns: %j', inRole);
|
||||
callback && callback(null, inRole);
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* List roles for a given principal
|
||||
* @param {Object} context The security context
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param err
|
||||
* @param {String[]} An array of role ids
|
||||
*/
|
||||
Role.getRoles = function(context, callback) {
|
||||
if (!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
var roles = [];
|
||||
|
||||
var addRole = function(role) {
|
||||
if (role && roles.indexOf(role) === -1) {
|
||||
roles.push(role);
|
||||
}
|
||||
};
|
||||
|
||||
var self = this;
|
||||
// Check against the smart roles
|
||||
var inRoleTasks = [];
|
||||
Object.keys(Role.resolvers).forEach(function(role) {
|
||||
inRoleTasks.push(function(done) {
|
||||
self.isInRole(role, context, function(err, inRole) {
|
||||
if (debug.enabled) {
|
||||
debug('In role %j: %j', role, inRole);
|
||||
}
|
||||
if (!err && inRole) {
|
||||
addRole(role);
|
||||
done();
|
||||
} else {
|
||||
done(err, null);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
||||
context.principals.forEach(function(p) {
|
||||
// Check against the role mappings
|
||||
var principalType = p.type || undefined;
|
||||
var principalId = p.id || undefined;
|
||||
|
||||
// Add the role itself
|
||||
if (principalType === RoleMapping.ROLE && principalId) {
|
||||
addRole(principalId);
|
||||
}
|
||||
|
||||
if (principalType && principalId) {
|
||||
// Please find() treat undefined matches all values
|
||||
inRoleTasks.push(function(done) {
|
||||
roleMappingModel.find({where: {principalType: principalType,
|
||||
principalId: principalId}}, function(err, mappings) {
|
||||
debug('Role mappings found: %s %j', err, mappings);
|
||||
if (err) {
|
||||
done && done(err);
|
||||
return;
|
||||
}
|
||||
mappings.forEach(function(m) {
|
||||
addRole(m.roleId);
|
||||
});
|
||||
done && done();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async.parallel(inRoleTasks, function(err, results) {
|
||||
debug('getRoles() returns: %j %j', err, roles);
|
||||
callback && callback(err, roles);
|
||||
});
|
||||
};
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "Role",
|
||||
"properties": {
|
||||
|
||||
"id": {
|
||||
"type": "string",
|
||||
"id": true,
|
||||
"generated": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"description": "string",
|
||||
|
||||
"created": "date",
|
||||
"modified": "date"
|
||||
},
|
||||
"relations": {
|
||||
"principals": {
|
||||
"type": "hasMany",
|
||||
"model": "RoleMapping",
|
||||
"foreignKey": "roleId"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
var assert = require('assert');
|
||||
var loopback = require('../../lib/loopback');
|
||||
|
||||
/**
|
||||
* Resource owner grants/delegates permissions to client applications
|
||||
*
|
||||
* For a protected resource, does the client application have the authorization
|
||||
* from the resource owner (user or system)?
|
||||
*
|
||||
* Scope has many resource access entries
|
||||
*
|
||||
* @class Scope
|
||||
*/
|
||||
|
||||
module.exports = function(Scope) {
|
||||
/**
|
||||
* 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) {
|
||||
var ACL = loopback.ACL;
|
||||
assert(ACL,
|
||||
'ACL model must be defined before Scope.checkPermission is called');
|
||||
|
||||
this.findOne({where: {name: scope}}, function (err, scope) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
} else {
|
||||
var aclModel = loopback.getModelByType(ACL);
|
||||
aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "Scope",
|
||||
"description": [
|
||||
"Schema for Scope which represents the permissions that are granted",
|
||||
"to client applications by the resource owner"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"description": "string"
|
||||
}
|
||||
}
|
|
@ -2,109 +2,21 @@
|
|||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var PersistedModel = require('../loopback').PersistedModel
|
||||
, loopback = require('../loopback')
|
||||
var loopback = require('../../lib/loopback')
|
||||
, path = require('path')
|
||||
, SALT_WORK_FACTOR = 10
|
||||
, crypto = require('crypto')
|
||||
, bcrypt = require('bcryptjs')
|
||||
, BaseAccessToken = require('./access-token')
|
||||
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
|
||||
, DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds
|
||||
, DEFAULT_MAX_TTL = 31556926 // 1 year in seconds
|
||||
, Role = require('./role').Role
|
||||
, ACL = require('./acl').ACL
|
||||
, assert = require('assert');
|
||||
|
||||
var debug = require('debug')('loopback:user');
|
||||
|
||||
/*!
|
||||
* Default User properties.
|
||||
*/
|
||||
|
||||
var properties = {
|
||||
realm: {type: String},
|
||||
username: {type: String},
|
||||
password: {type: String, required: true},
|
||||
credentials: Object, // deprecated, to be removed in 2.x
|
||||
challenges: Object, // deprecated, to be removed in 2.x
|
||||
email: {type: String, required: true},
|
||||
emailVerified: Boolean,
|
||||
verificationToken: String,
|
||||
status: String,
|
||||
created: Date,
|
||||
lastUpdated: Date
|
||||
};
|
||||
|
||||
var options = {
|
||||
hidden: ['password'],
|
||||
acls: [
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
principalId: Role.EVERYONE,
|
||||
permission: ACL.DENY
|
||||
},
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
principalId: Role.EVERYONE,
|
||||
permission: ACL.ALLOW,
|
||||
property: 'create'
|
||||
},
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
principalId: Role.OWNER,
|
||||
permission: ACL.ALLOW,
|
||||
property: 'deleteById'
|
||||
},
|
||||
{
|
||||
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"
|
||||
},
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
principalId: Role.EVERYONE,
|
||||
permission: ACL.ALLOW,
|
||||
property: "confirm"
|
||||
},
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
principalId: Role.EVERYONE,
|
||||
permission: ACL.ALLOW,
|
||||
property: "resetPassword",
|
||||
accessType: ACL.EXECUTE
|
||||
}
|
||||
],
|
||||
relations: {
|
||||
accessTokens: {
|
||||
type: 'hasMany',
|
||||
model: 'AccessToken',
|
||||
foreignKey: 'userId'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extends from the built in `loopback.Model` type.
|
||||
* Built-in User model.
|
||||
* Extends LoopBack [PersistedModel](#persistedmodel-new-persistedmodel).
|
||||
*
|
||||
* Default `User` ACLs.
|
||||
*
|
||||
|
@ -122,11 +34,11 @@ var options = {
|
|||
* @property {Boolean} emailVerified Set when a user's email has been verified via `confirm()`
|
||||
* @property {String} verificationToken Set when `verify()` is called
|
||||
*
|
||||
* @class
|
||||
* @inherits {Model}
|
||||
* @class User
|
||||
* @inherits {PersistedModel}
|
||||
*/
|
||||
|
||||
var User = module.exports = PersistedModel.extend('User', properties, options);
|
||||
module.exports = function(User) {
|
||||
|
||||
/**
|
||||
* Create access token for the logged in user. This method can be overridden to
|
||||
|
@ -150,8 +62,8 @@ User.prototype.createAccessToken = function(ttl, cb) {
|
|||
*
|
||||
* ```js
|
||||
* User.login({username: 'foo', password: 'bar'}, function (err, token) {
|
||||
* console.log(token.id);
|
||||
* });
|
||||
* console.log(token.id);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {Object} credentials
|
||||
|
@ -160,7 +72,7 @@ User.prototype.createAccessToken = function(ttl, cb) {
|
|||
* @param {AccessToken} token
|
||||
*/
|
||||
|
||||
User.login = function (credentials, include, fn) {
|
||||
User.login = function(credentials, include, fn) {
|
||||
var self = this;
|
||||
if (typeof include === 'function') {
|
||||
fn = include;
|
||||
|
@ -169,19 +81,18 @@ User.login = function (credentials, include, fn) {
|
|||
|
||||
include = (include || '');
|
||||
if (Array.isArray(include)) {
|
||||
include = include.map(function ( val ) {
|
||||
include = include.map(function(val) {
|
||||
return val.toLowerCase();
|
||||
});
|
||||
}else {
|
||||
} else {
|
||||
include = include.toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
|
||||
var query = {};
|
||||
if(credentials.email) {
|
||||
if (credentials.email) {
|
||||
query.email = credentials.email;
|
||||
} else if(credentials.username) {
|
||||
} else if (credentials.username) {
|
||||
query.username = credentials.username;
|
||||
} else {
|
||||
var err = new Error('username or email is required');
|
||||
|
@ -193,10 +104,10 @@ User.login = function (credentials, include, fn) {
|
|||
var defaultError = new Error('login failed');
|
||||
defaultError.statusCode = 401;
|
||||
|
||||
if(err) {
|
||||
if (err) {
|
||||
debug('An error is reported from User.findOne: %j', err);
|
||||
fn(defaultError);
|
||||
} else if(user) {
|
||||
} else if (user) {
|
||||
if (self.settings.emailVerificationRequired) {
|
||||
if (!user.emailVerified) {
|
||||
// Fail to log in if email verification is not done yet
|
||||
|
@ -207,10 +118,10 @@ User.login = function (credentials, include, fn) {
|
|||
}
|
||||
}
|
||||
user.hasPassword(credentials.password, function(err, isMatch) {
|
||||
if(err) {
|
||||
if (err) {
|
||||
debug('An error is reported from User.hasPassword: %j', err);
|
||||
fn(defaultError);
|
||||
} else if(isMatch) {
|
||||
} else if (isMatch) {
|
||||
user.createAccessToken(credentials.ttl, function(err, token) {
|
||||
if (err) return fn(err);
|
||||
if (Array.isArray(include) ? include.indexOf('user') !== -1 : include === 'user') {
|
||||
|
@ -241,8 +152,8 @@ User.login = function (credentials, include, fn) {
|
|||
*
|
||||
* ```js
|
||||
* User.logout('asd0a9f8dsj9s0s3223mk', function (err) {
|
||||
* console.log(err || 'Logged out');
|
||||
* });
|
||||
* console.log(err || 'Logged out');
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {String} accessTokenID
|
||||
|
@ -250,11 +161,11 @@ User.login = function (credentials, include, fn) {
|
|||
* @param {Error} err
|
||||
*/
|
||||
|
||||
User.logout = function (tokenId, fn) {
|
||||
this.relations.accessTokens.modelTo.findById(tokenId, function (err, accessToken) {
|
||||
if(err) {
|
||||
User.logout = function(tokenId, fn) {
|
||||
this.relations.accessTokens.modelTo.findById(tokenId, function(err, accessToken) {
|
||||
if (err) {
|
||||
fn(err);
|
||||
} else if(accessToken) {
|
||||
} else if (accessToken) {
|
||||
accessToken.destroy(fn);
|
||||
} else {
|
||||
fn(new Error('could not find accessToken'));
|
||||
|
@ -269,10 +180,10 @@ User.logout = function (tokenId, fn) {
|
|||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
User.prototype.hasPassword = function (plain, fn) {
|
||||
if(this.password && plain) {
|
||||
User.prototype.hasPassword = function(plain, fn) {
|
||||
if (this.password && plain) {
|
||||
bcrypt.compare(plain, this.password, function(err, isMatch) {
|
||||
if(err) return fn(err);
|
||||
if (err) return fn(err);
|
||||
fn(null, isMatch);
|
||||
});
|
||||
} else {
|
||||
|
@ -285,19 +196,29 @@ User.prototype.hasPassword = function (plain, fn) {
|
|||
*
|
||||
* ```js
|
||||
* var options = {
|
||||
* type: 'email',
|
||||
* to: user.email,
|
||||
* template: 'verify.ejs',
|
||||
* redirect: '/'
|
||||
* };
|
||||
* type: 'email',
|
||||
* to: user.email,
|
||||
* template: 'verify.ejs',
|
||||
* redirect: '/'
|
||||
* };
|
||||
*
|
||||
* user.verify(options, next);
|
||||
* ```
|
||||
*
|
||||
* @param {Object} options
|
||||
* @options {Object} options
|
||||
* @property {String} type Must be 'email'.
|
||||
* @property {String} to Email address to which verification email is sent.
|
||||
* @property {String} from Sender email addresss, for example
|
||||
* `'noreply@myapp.com'`.
|
||||
* @property {String} subject Subject line text.
|
||||
* @property {String} text Text of email.
|
||||
* @property {String} template Name of template that displays verification
|
||||
* page, for example, `'verify.ejs'.
|
||||
* @property {String} redirect Page to which user will be redirected after
|
||||
* they verify their email, for example `'/'` for root URI.
|
||||
*/
|
||||
|
||||
User.prototype.verify = function (options, fn) {
|
||||
User.prototype.verify = function(options, fn) {
|
||||
var user = this;
|
||||
var userModel = this.constructor;
|
||||
assert(typeof options === 'object', 'options required when calling user.verify()');
|
||||
|
@ -314,32 +235,32 @@ User.prototype.verify = function (options, fn) {
|
|||
var app = userModel.app;
|
||||
options.host = options.host || (app && app.get('host')) || 'localhost';
|
||||
options.port = options.port || (app && app.get('port')) || 3000;
|
||||
options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api';
|
||||
options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api';
|
||||
options.verifyHref = options.verifyHref ||
|
||||
options.protocol
|
||||
+ '://'
|
||||
+ options.host
|
||||
+ ':'
|
||||
+ options.port
|
||||
+ options.restApiRoot
|
||||
+ userModel.http.path
|
||||
+ userModel.confirm.http.path
|
||||
+ '?uid='
|
||||
+ options.user.id
|
||||
+ '&redirect='
|
||||
+ options.redirect;
|
||||
options.protocol
|
||||
+ '://'
|
||||
+ options.host
|
||||
+ ':'
|
||||
+ options.port
|
||||
+ options.restApiRoot
|
||||
+ userModel.http.path
|
||||
+ userModel.confirm.http.path
|
||||
+ '?uid='
|
||||
+ options.user.id
|
||||
+ '&redirect='
|
||||
+ options.redirect;
|
||||
|
||||
|
||||
// Email model
|
||||
var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email);
|
||||
|
||||
crypto.randomBytes(64, function(err, buf) {
|
||||
if(err) {
|
||||
if (err) {
|
||||
fn(err);
|
||||
} else {
|
||||
user.verificationToken = buf.toString('hex');
|
||||
user.save(function (err) {
|
||||
if(err) {
|
||||
user.save(function(err) {
|
||||
if (err) {
|
||||
fn(err);
|
||||
} else {
|
||||
sendEmail(user);
|
||||
|
@ -363,8 +284,8 @@ User.prototype.verify = function (options, fn) {
|
|||
subject: options.subject || 'Thanks for Registering',
|
||||
text: options.text,
|
||||
html: template(options)
|
||||
}, function (err, email) {
|
||||
if(err) {
|
||||
}, function(err, email) {
|
||||
if (err) {
|
||||
fn(err);
|
||||
} else {
|
||||
fn(null, {email: email, token: user.verificationToken, uid: user.id});
|
||||
|
@ -383,16 +304,16 @@ User.prototype.verify = function (options, fn) {
|
|||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
*/
|
||||
User.confirm = function (uid, token, redirect, fn) {
|
||||
this.findById(uid, function (err, user) {
|
||||
if(err) {
|
||||
User.confirm = function(uid, token, redirect, fn) {
|
||||
this.findById(uid, function(err, user) {
|
||||
if (err) {
|
||||
fn(err);
|
||||
} else {
|
||||
if(user && user.verificationToken === token) {
|
||||
if (user && user.verificationToken === token) {
|
||||
user.verificationToken = undefined;
|
||||
user.emailVerified = true;
|
||||
user.save(function (err) {
|
||||
if(err) {
|
||||
user.save(function(err) {
|
||||
if (err) {
|
||||
fn(err);
|
||||
} else {
|
||||
fn();
|
||||
|
@ -427,15 +348,15 @@ User.resetPassword = function(options, cb) {
|
|||
var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
|
||||
|
||||
options = options || {};
|
||||
if(typeof options.email === 'string') {
|
||||
if (typeof options.email === 'string') {
|
||||
UserModel.findOne({ where: {email: options.email} }, function(err, user) {
|
||||
if(err) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else if(user) {
|
||||
} else if (user) {
|
||||
// create a short lived access token for temp login to change password
|
||||
// TODO(ritch) - eventually this should only allow password change
|
||||
user.accessTokens.create({ttl: ttl}, function(err, accessToken) {
|
||||
if(err) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else {
|
||||
cb();
|
||||
|
@ -462,16 +383,16 @@ User.resetPassword = function(options, cb) {
|
|||
* Setup an extended user model.
|
||||
*/
|
||||
|
||||
User.setup = function () {
|
||||
User.setup = function() {
|
||||
// We need to call the base class's setup method
|
||||
PersistedModel.setup.call(this);
|
||||
User.base.setup.call(this);
|
||||
var UserModel = this;
|
||||
|
||||
// max ttl
|
||||
this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL;
|
||||
this.settings.ttl = DEFAULT_TTL;
|
||||
|
||||
UserModel.setter.password = function (plain) {
|
||||
UserModel.setter.password = function(plain) {
|
||||
var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR);
|
||||
this.$password = bcrypt.hashSync(plain, salt);
|
||||
}
|
||||
|
@ -491,16 +412,14 @@ User.setup = function () {
|
|||
description: 'Login a user with username/email and password',
|
||||
accepts: [
|
||||
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
|
||||
{arg: 'include', type: 'string', http: {source: 'query' }, description:
|
||||
'Related objects to include in the response. ' +
|
||||
'See the description of return value for more details.'}
|
||||
{arg: 'include', type: 'string', http: {source: 'query' }, description: 'Related objects to include in the response. ' +
|
||||
'See the description of return value for more details.'}
|
||||
],
|
||||
returns: {
|
||||
arg: 'accessToken', type: 'object', root: true, description:
|
||||
'The response body contains properties of the AccessToken created on login.\n' +
|
||||
'Depending on the value of `include` parameter, the body may contain ' +
|
||||
'additional properties:\n\n' +
|
||||
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n'
|
||||
arg: 'accessToken', type: 'object', root: true, description: 'The response body contains properties of the AccessToken created on login.\n' +
|
||||
'Depending on the value of `include` parameter, the body may contain ' +
|
||||
'additional properties:\n\n' +
|
||||
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n'
|
||||
},
|
||||
http: {verb: 'post'}
|
||||
}
|
||||
|
@ -517,9 +436,8 @@ User.setup = function () {
|
|||
var tokenID = accessToken && accessToken.id;
|
||||
|
||||
return tokenID;
|
||||
}, description:
|
||||
'Do not supply this argument, it is automatically extracted ' +
|
||||
'from request headers.'
|
||||
}, description: 'Do not supply this argument, it is automatically extracted ' +
|
||||
'from request headers.'
|
||||
}
|
||||
],
|
||||
http: {verb: 'all'}
|
||||
|
@ -550,26 +468,29 @@ User.setup = function () {
|
|||
}
|
||||
);
|
||||
|
||||
UserModel.on('attached', function () {
|
||||
UserModel.afterRemote('confirm', function (ctx, inst, next) {
|
||||
if(ctx.req) {
|
||||
UserModel.on('attached', function() {
|
||||
UserModel.afterRemote('confirm', function(ctx, inst, next) {
|
||||
if (ctx.req) {
|
||||
ctx.res.redirect(ctx.req.param('redirect'));
|
||||
} else {
|
||||
fn(new Error('transport unsupported'));
|
||||
next(new Error('transport unsupported'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// default models
|
||||
UserModel.email = require('./email');
|
||||
UserModel.accessToken = require('./access-token');
|
||||
assert(loopback.Email, 'Email model must be defined before User model');
|
||||
UserModel.email = loopback.Email;
|
||||
|
||||
assert(loopback.AccessToken, 'AccessToken model must be defined before User model');
|
||||
UserModel.accessToken = loopback.AccessToken;
|
||||
|
||||
// email validation regex
|
||||
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
|
||||
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
||||
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
|
||||
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
|
||||
|
||||
return UserModel;
|
||||
}
|
||||
|
@ -579,3 +500,5 @@ User.setup = function () {
|
|||
*/
|
||||
|
||||
User.setup();
|
||||
|
||||
};
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"name": "User",
|
||||
"properties": {
|
||||
"realm": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"credentials": {
|
||||
"type": "object",
|
||||
"deprecated": true
|
||||
},
|
||||
"challenges": {
|
||||
"type": "object",
|
||||
"deprecated": true
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"emailVerified": "boolean",
|
||||
"verificationToken": "string",
|
||||
"status": "string",
|
||||
"created": "date",
|
||||
"lastUpdated": "date"
|
||||
},
|
||||
"hidden": ["password"],
|
||||
"acls": [
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "create"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$owner",
|
||||
"permission": "ALLOW",
|
||||
"property": "deleteById"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "login"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "logout"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$owner",
|
||||
"permission": "ALLOW",
|
||||
"property": "findById"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$owner",
|
||||
"permission": "ALLOW",
|
||||
"property": "updateAttributes"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "confirm"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "resetPassword",
|
||||
"accessType": "EXECUTE"
|
||||
}
|
||||
],
|
||||
"relations": {
|
||||
"accessTokens": {
|
||||
"type": "hasMany",
|
||||
"model": "AccessToken",
|
||||
"foreignKey": "userId"
|
||||
}
|
||||
}
|
||||
}
|
29
docs.json
29
docs.json
|
@ -3,24 +3,25 @@
|
|||
"content": [
|
||||
"lib/application.js",
|
||||
"lib/loopback.js",
|
||||
"lib/runtime.js",
|
||||
"lib/registry.js",
|
||||
{ "title": "Base models", "depth": 2 },
|
||||
"lib/models/model.js",
|
||||
"lib/models/persisted-model.js",
|
||||
{ "title": "Middleware", "depth": 2 },
|
||||
{ "title": "Base models", "depth": 2 },
|
||||
"lib/model.js",
|
||||
"lib/persisted-model.js",
|
||||
{ "title": "Middleware", "depth": 2 },
|
||||
"lib/middleware/rest.js",
|
||||
"lib/middleware/status.js",
|
||||
"lib/middleware/token.js",
|
||||
"lib/middleware/token.js",
|
||||
"lib/middleware/urlNotFound.js",
|
||||
{ "title": "Built-in models", "depth": 2 },
|
||||
"lib/models/access-token.js",
|
||||
"lib/models/acl.js",
|
||||
"lib/models/application.js",
|
||||
"lib/models/email.js",
|
||||
"lib/models/role.js",
|
||||
"lib/models/user.js",
|
||||
"lib/models/change.js"
|
||||
{ "title": "Built-in models", "depth": 2 },
|
||||
"common/models/access-token.js",
|
||||
"common/models/acl.js",
|
||||
"common/models/scope.js",
|
||||
"common/models/application.js",
|
||||
"common/models/email.js",
|
||||
"common/models/role-mapping.js",
|
||||
"common/models/role.js",
|
||||
"common/models/user.js",
|
||||
"common/models/change.js"
|
||||
],
|
||||
"assets": "/docs/assets"
|
||||
}
|
||||
|
|
2
index.js
2
index.js
|
@ -19,4 +19,4 @@ loopback.Remote = require('loopback-connector-remote');
|
|||
*/
|
||||
|
||||
loopback.GeoPoint = require('loopback-datasource-juggler/lib/geo').GeoPoint;
|
||||
loopback.ValidationError = datasourceJuggler.ValidationError;
|
||||
loopback.ValidationError = loopback.Model.ValidationError;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var loopback = require('../loopback');
|
||||
var AccessToken = require('./access-token');
|
||||
var assert = require('assert');
|
||||
var loopback = require('./loopback');
|
||||
var debug = require('debug')('loopback:security:access-context');
|
||||
|
||||
/**
|
||||
|
@ -39,17 +39,19 @@ function AccessContext(context) {
|
|||
this.sharedMethod = context.sharedMethod;
|
||||
this.sharedClass = this.sharedMethod && this.sharedMethod.sharedClass;
|
||||
if(this.sharedMethod) {
|
||||
this.methodNames = this.sharedMethod.aliases.concat([this.sharedMethod.name]);
|
||||
this.methodNames = this.sharedMethod.aliases.concat([this.sharedMethod.name]);
|
||||
} else {
|
||||
this.methodNames = [];
|
||||
}
|
||||
|
||||
|
||||
if(this.sharedMethod) {
|
||||
this.accessType = this.model._getAccessTypeForMethod(this.sharedMethod);
|
||||
this.accessType = this.model._getAccessTypeForMethod(this.sharedMethod);
|
||||
}
|
||||
|
||||
|
||||
this.accessType = context.accessType || AccessContext.ALL;
|
||||
this.accessToken = context.accessToken || AccessToken.ANONYMOUS;
|
||||
assert(loopback.AccessToken,
|
||||
'AccessToken model must be defined before AccessContext model');
|
||||
this.accessToken = context.accessToken || loopback.AccessToken.ANONYMOUS;
|
||||
|
||||
var principalType = context.principalType || Principal.USER;
|
||||
var principalId = context.principalId || undefined;
|
||||
|
@ -155,9 +157,9 @@ AccessContext.prototype.debug = function() {
|
|||
if(debug.enabled) {
|
||||
debug('---AccessContext---');
|
||||
if(this.principals && this.principals.length) {
|
||||
debug('principals:')
|
||||
debug('principals:');
|
||||
this.principals.forEach(function(principal) {
|
||||
debug('principal: %j', principal)
|
||||
debug('principal: %j', principal);
|
||||
});
|
||||
} else {
|
||||
debug('principals: %j', this.principals);
|
||||
|
@ -168,14 +170,14 @@ AccessContext.prototype.debug = function() {
|
|||
debug('method %s', this.method);
|
||||
debug('accessType %s', this.accessType);
|
||||
if(this.accessToken) {
|
||||
debug('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
|
||||
|
@ -271,17 +273,17 @@ AccessRequest.prototype.exactlyMatches = function(acl) {
|
|||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the request for access allowed?
|
||||
*
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
AccessRequest.prototype.isAllowed = function() {
|
||||
return this.permission !== require('./acl').ACL.DENY;
|
||||
}
|
||||
return this.permission !== loopback.ACL.DENY;
|
||||
};
|
||||
|
||||
AccessRequest.prototype.debug = function() {
|
||||
if(debug.enabled) {
|
||||
|
@ -293,7 +295,7 @@ AccessRequest.prototype.debug = function() {
|
|||
debug(' isWildcard() %s', this.isWildcard());
|
||||
debug(' isAllowed() %s', this.isAllowed());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.AccessContext = AccessContext;
|
||||
module.exports.Principal = Principal;
|
|
@ -14,23 +14,22 @@ var DataSource = require('loopback-datasource-juggler').DataSource
|
|||
|
||||
/**
|
||||
* The `App` object represents a Loopback application.
|
||||
*
|
||||
*
|
||||
* The App object extends [Express](http://expressjs.com/api.html#express) and
|
||||
* supports
|
||||
* [Express / Connect middleware](http://expressjs.com/api.html#middleware). See
|
||||
* [Express documentation](http://expressjs.com/api.html) for details.
|
||||
*
|
||||
* supports Express middleware. See
|
||||
* [Express documentation](http://expressjs.com/) for details.
|
||||
*
|
||||
* ```js
|
||||
* var loopback = require('loopback');
|
||||
* var app = loopback();
|
||||
*
|
||||
*
|
||||
* app.get('/', function(req, res){
|
||||
* res.send('hello world');
|
||||
* });
|
||||
*
|
||||
*
|
||||
* app.listen(3000);
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @class LoopBackApplication
|
||||
* @header var app = loopback()
|
||||
*/
|
||||
|
@ -60,10 +59,10 @@ app.remotes = function () {
|
|||
if(this.get) {
|
||||
options = this.get('remoting');
|
||||
}
|
||||
|
||||
|
||||
return (this._remotes = RemoteObjects.create(options));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Remove a route by reference.
|
||||
|
@ -77,36 +76,28 @@ app.disuse = function (route) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach a model to the app. The `Model` will be available on the
|
||||
* `app.models` object.
|
||||
*
|
||||
* ```js
|
||||
* // Attach an existing model
|
||||
* Example - Attach an existing model:
|
||||
```js
|
||||
* var User = loopback.User;
|
||||
* app.model(User);
|
||||
*
|
||||
* // Attach an existing model, alter some aspects of the model
|
||||
*```
|
||||
* Example - Attach an existing model, alter some aspects of the model:
|
||||
* ```js
|
||||
* var User = loopback.User;
|
||||
* app.model(User, { dataSource: 'db' });
|
||||
*```
|
||||
*
|
||||
* // LoopBack 1.x way: create and attach a new model (deprecated)
|
||||
* var Widget = app.model('Widget', {
|
||||
* dataSource: 'db',
|
||||
* properties: {
|
||||
* name: 'string'
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {Object|String} Model The model to attach.
|
||||
* @options {Object} config The model's configuration.
|
||||
* @property {String|DataSource} dataSource The `DataSource` to which to
|
||||
* attach the model.
|
||||
* @property {Boolean} [public] whether the model should be exposed via REST API
|
||||
* @property {Object} [relations] relations to add/update
|
||||
* @property {String|DataSource} dataSource The `DataSource` to which to attach the model.
|
||||
* @property {Boolean} [public] Whether the model should be exposed via REST API.
|
||||
* @property {Object} [relations] Relations to add/update.
|
||||
* @end
|
||||
* @returns {ModelConstructor} the model class
|
||||
*/
|
||||
|
@ -180,7 +171,7 @@ app.model = function (Model, config) {
|
|||
* });
|
||||
* ```
|
||||
*
|
||||
* **2. Use `app.model` to access a model by name.
|
||||
* 2. Use `app.model` to access a model by name.
|
||||
* `app.model` has properties for all defined models.
|
||||
*
|
||||
* The following example illustrates accessing the `Product` and `CustomerReceipt` models
|
||||
|
@ -216,14 +207,13 @@ app.model = function (Model, config) {
|
|||
|
||||
app.models = function () {
|
||||
return this._models || (this._models = []);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Define a DataSource.
|
||||
*
|
||||
* @param {String} name The data source name
|
||||
* @param {Object} config The data source config
|
||||
* @param {DataSource} The registered data source
|
||||
*/
|
||||
app.dataSource = function (name, config) {
|
||||
var ds = dataSourcesFromConfig(config, this.connectors);
|
||||
|
@ -231,7 +221,7 @@ app.dataSource = function (name, config) {
|
|||
this.dataSources[classify(name)] =
|
||||
this.dataSources[camelize(name)] = ds;
|
||||
return ds;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a connector.
|
||||
|
@ -264,30 +254,30 @@ app.remoteObjects = function () {
|
|||
this.remotes().classes().forEach(function(sharedClass) {
|
||||
result[sharedClass.name] = sharedClass.ctor;
|
||||
});
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Get a handler of the specified type from the handler cache.
|
||||
* @triggers `mounted` events on shared class constructors (models)
|
||||
* @triggers `mounted` events on shared class constructors (models)
|
||||
*/
|
||||
|
||||
app.handler = function (type) {
|
||||
|
||||
app.handler = function (type, options) {
|
||||
var handlers = this._handlers || (this._handlers = {});
|
||||
if(handlers[type]) {
|
||||
return handlers[type];
|
||||
}
|
||||
|
||||
|
||||
var remotes = this.remotes();
|
||||
var handler = this._handlers[type] = remotes.handler(type);
|
||||
|
||||
var handler = this._handlers[type] = remotes.handler(type, options);
|
||||
|
||||
remotes.classes().forEach(function(sharedClass) {
|
||||
sharedClass.ctor.emit('mounted', app, sharedClass, remotes);
|
||||
});
|
||||
|
||||
|
||||
return handler;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An object to store dataSource instances.
|
||||
|
@ -352,7 +342,7 @@ app.enableAuth = function() {
|
|||
app.boot = function(options) {
|
||||
throw new Error(
|
||||
'`app.boot` was removed, use the new module loopback-boot instead');
|
||||
}
|
||||
};
|
||||
|
||||
function classify(str) {
|
||||
return stringUtils.classify(str);
|
||||
|
@ -365,7 +355,7 @@ function camelize(str) {
|
|||
function dataSourcesFromConfig(config, connectorRegistry) {
|
||||
var connectorPath;
|
||||
|
||||
assert(typeof config === 'object',
|
||||
assert(typeof config === 'object',
|
||||
'cannont create data source without config object');
|
||||
|
||||
if(typeof config.connector === 'string') {
|
||||
|
@ -416,15 +406,15 @@ function clearHandlerCache(app) {
|
|||
* When there are no parameters or there is only one callback parameter,
|
||||
* the server will listen on `app.get('host')` and `app.get('port')`.
|
||||
*
|
||||
* For example, to listen on host/port configured in app config:
|
||||
* ```js
|
||||
* // listen on host/port configured in app config
|
||||
* app.listen();
|
||||
* ```
|
||||
*
|
||||
* Otherwise all arguments are forwarded to `http.Server.listen`.
|
||||
*
|
||||
* For example, to listen on the specified port and all hosts, and ignore app config.
|
||||
* ```js
|
||||
* // listen on the specified port and all hosts, ignore app config
|
||||
* app.listen(80);
|
||||
* ```
|
||||
*
|
||||
|
@ -433,7 +423,7 @@ function clearHandlerCache(app) {
|
|||
* This way the port param contains always the real port number, even when
|
||||
* listen was called with port number 0.
|
||||
*
|
||||
* @param {Function} cb If specified, the callback is added as a listener
|
||||
* @param {Function} [cb] If specified, the callback is added as a listener
|
||||
* for the server's "listening" event.
|
||||
* @returns {http.Server} A node `http.Server` with this application configured
|
||||
* as the request handler.
|
||||
|
@ -445,17 +435,31 @@ app.listen = function(cb) {
|
|||
|
||||
server.on('listening', function() {
|
||||
self.set('port', this.address().port);
|
||||
|
||||
var listeningOnAll = false;
|
||||
var host = self.get('host');
|
||||
if (!host) {
|
||||
listeningOnAll = true;
|
||||
host = this.address().address;
|
||||
self.set('host', host);
|
||||
} else if (host === '0.0.0.0' || host === '::') {
|
||||
listeningOnAll = true;
|
||||
}
|
||||
|
||||
if (!self.get('url')) {
|
||||
// A better default host would be `0.0.0.0`,
|
||||
// but such URL is not supported by Windows
|
||||
var host = self.get('host') || '127.0.0.1';
|
||||
if (process.platform === 'win32' && listeningOnAll) {
|
||||
// Windows browsers don't support `0.0.0.0` host in the URL
|
||||
// We are replacing it with localhost to build a URL
|
||||
// that can be copied and pasted into the browser.
|
||||
host = 'localhost';
|
||||
}
|
||||
var url = 'http://' + host + ':' + self.get('port') + '/';
|
||||
self.set('url', url);
|
||||
}
|
||||
});
|
||||
|
||||
var useAppConfig =
|
||||
arguments.length == 0 ||
|
||||
arguments.length === 0 ||
|
||||
(arguments.length == 1 && typeof arguments[0] == 'function');
|
||||
|
||||
if (useAppConfig) {
|
||||
|
@ -465,4 +469,4 @@ app.listen = function(cb) {
|
|||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
module.exports = function(loopback) {
|
||||
// NOTE(bajtos) we must use static require() due to browserify limitations
|
||||
|
||||
loopback.Email = createModel(
|
||||
require('../common/models/email.json'),
|
||||
require('../common/models/email.js'));
|
||||
|
||||
loopback.Application = createModel(
|
||||
require('../common/models/application.json'),
|
||||
require('../common/models/application.js'));
|
||||
|
||||
loopback.AccessToken = createModel(
|
||||
require('../common/models/access-token.json'),
|
||||
require('../common/models/access-token.js'));
|
||||
|
||||
loopback.RoleMapping = createModel(
|
||||
require('../common/models/role-mapping.json'),
|
||||
require('../common/models/role-mapping.js'));
|
||||
|
||||
loopback.Role = createModel(
|
||||
require('../common/models/role.json'),
|
||||
require('../common/models/role.js'));
|
||||
|
||||
loopback.ACL = createModel(
|
||||
require('../common/models/acl.json'),
|
||||
require('../common/models/acl.js'));
|
||||
|
||||
loopback.Scope = createModel(
|
||||
require('../common/models/scope.json'),
|
||||
require('../common/models/scope.js'));
|
||||
|
||||
loopback.User = createModel(
|
||||
require('../common/models/user.json'),
|
||||
require('../common/models/user.js'));
|
||||
|
||||
loopback.Change = createModel(
|
||||
require('../common/models/change.json'),
|
||||
require('../common/models/change.js'));
|
||||
|
||||
loopback.Checkpoint = createModel(
|
||||
require('../common/models/checkpoint.json'),
|
||||
require('../common/models/checkpoint.js'));
|
||||
|
||||
/*!
|
||||
* Automatically attach these models to dataSources
|
||||
*/
|
||||
|
||||
var dataSourceTypes = {
|
||||
DB: 'db',
|
||||
MAIL: 'mail'
|
||||
};
|
||||
|
||||
loopback.Email.autoAttach = dataSourceTypes.MAIL;
|
||||
loopback.PersistedModel.autoAttach = dataSourceTypes.DB;
|
||||
loopback.User.autoAttach = dataSourceTypes.DB;
|
||||
loopback.AccessToken.autoAttach = dataSourceTypes.DB;
|
||||
loopback.Role.autoAttach = dataSourceTypes.DB;
|
||||
loopback.RoleMapping.autoAttach = dataSourceTypes.DB;
|
||||
loopback.ACL.autoAttach = dataSourceTypes.DB;
|
||||
loopback.Scope.autoAttach = dataSourceTypes.DB;
|
||||
loopback.Application.autoAttach = dataSourceTypes.DB;
|
||||
|
||||
function createModel(definitionJson, customizeFn) {
|
||||
var Model = loopback.createModel(definitionJson);
|
||||
customizeFn(Model);
|
||||
return Model;
|
||||
}
|
||||
};
|
|
@ -7,13 +7,13 @@ module.exports = Connector;
|
|||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
|
||||
var EventEmitter = require('events').EventEmitter
|
||||
, debug = require('debug')('connector')
|
||||
, util = require('util')
|
||||
, inherits = util.inherits
|
||||
, assert = require('assert');
|
||||
|
||||
|
||||
/**
|
||||
* Create a new `Connector` with the given `options`.
|
||||
*
|
||||
|
@ -24,7 +24,7 @@ var EventEmitter = require('events').EventEmitter
|
|||
function Connector(options) {
|
||||
EventEmitter.apply(this, arguments);
|
||||
this.options = options;
|
||||
|
||||
|
||||
debug('created with options', options);
|
||||
}
|
||||
|
||||
|
@ -43,12 +43,12 @@ Connector._createJDBAdapter = function (jdbModule) {
|
|||
jdbModule.initialize(fauxSchema, function () {
|
||||
// connected
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Add default crud operations from a JugglingDB adapter.
|
||||
*/
|
||||
|
||||
Connector.prototype._addCrudOperationsFromJDBAdapter = function (connector) {
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
var mailer = require('nodemailer')
|
||||
, assert = require('assert')
|
||||
, debug = require('debug')('loopback:connector:mail')
|
||||
, loopback = require('../loopback')
|
||||
, loopback = require('../loopback');
|
||||
|
||||
/**
|
||||
* Export the MailConnector class.
|
||||
|
@ -44,7 +44,7 @@ function MailConnector(settings) {
|
|||
MailConnector.initialize = function(dataSource, callback) {
|
||||
dataSource.connector = new MailConnector(dataSource.settings);
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
MailConnector.prototype.DataAccessObject = Mailer;
|
||||
|
||||
|
@ -86,7 +86,7 @@ MailConnector.prototype.setupTransport = function(setting) {
|
|||
|
||||
connector.transportsIndex[setting.type] = transport;
|
||||
connector.transports.push(transport);
|
||||
}
|
||||
};
|
||||
|
||||
function Mailer() {
|
||||
|
||||
|
@ -101,7 +101,7 @@ function Mailer() {
|
|||
|
||||
MailConnector.prototype.transportForName = function(name) {
|
||||
return this.transportsIndex[name];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default transport.
|
||||
|
@ -111,7 +111,7 @@ MailConnector.prototype.transportForName = function(name) {
|
|||
|
||||
MailConnector.prototype.defaultTransport = function() {
|
||||
return this.transports[0] || this.stubTransport;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Send an email with the given `options`.
|
||||
|
@ -166,7 +166,7 @@ Mailer.send = function (options, fn) {
|
|||
fn(null, options);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Send an email instance using `modelInstance.send()`.
|
||||
|
@ -174,7 +174,7 @@ Mailer.send = function (options, fn) {
|
|||
|
||||
Mailer.prototype.send = function (fn) {
|
||||
this.constructor.send(this, fn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Access the node mailer object.
|
||||
|
|
|
@ -7,14 +7,14 @@ module.exports = Memory;
|
|||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
|
||||
var Connector = require('./base-connector')
|
||||
, debug = require('debug')('memory')
|
||||
, util = require('util')
|
||||
, inherits = util.inherits
|
||||
, assert = require('assert')
|
||||
, JdbMemory = require('loopback-datasource-juggler/lib/connectors/memory');
|
||||
|
||||
|
||||
/**
|
||||
* Create a new `Memory` connector with the given `options`.
|
||||
*
|
||||
|
|
|
@ -20,20 +20,20 @@ function createMiddlewareNotInstalled(memberName, moduleName) {
|
|||
}
|
||||
|
||||
var middlewareModules = {
|
||||
"compress": "compression",
|
||||
"timeout": "connect-timeout",
|
||||
"cookieParser": "cookie-parser",
|
||||
"cookieSession": "cookie-session",
|
||||
"csrf": "csurf",
|
||||
"errorHandler": "errorhandler",
|
||||
"session": "express-session",
|
||||
"methodOverride": "method-override",
|
||||
"logger": "morgan",
|
||||
"responseTime": "response-time",
|
||||
"favicon": "serve-favicon",
|
||||
"directory": "serve-index",
|
||||
// "static": "serve-static",
|
||||
"vhost": "vhost"
|
||||
'compress': 'compression',
|
||||
'timeout': 'connect-timeout',
|
||||
'cookieParser': 'cookie-parser',
|
||||
'cookieSession': 'cookie-session',
|
||||
'csrf': 'csurf',
|
||||
'errorHandler': 'errorhandler',
|
||||
'session': 'express-session',
|
||||
'methodOverride': 'method-override',
|
||||
'logger': 'morgan',
|
||||
'responseTime': 'response-time',
|
||||
'favicon': 'serve-favicon',
|
||||
'directory': 'serve-index',
|
||||
// 'static': 'serve-static',
|
||||
'vhost': 'vhost'
|
||||
};
|
||||
|
||||
middlewares.bodyParser = safeRequire('body-parser');
|
||||
|
|
|
@ -11,28 +11,32 @@ var express = require('express')
|
|||
, assert = require('assert');
|
||||
|
||||
/**
|
||||
* Main entry for LoopBack core module. It provides static properties and
|
||||
* LoopBack core module. It provides static properties and
|
||||
* methods to create models and data sources. The module itself is a function
|
||||
* that creates loopback `app`. For example,
|
||||
* that creates loopback `app`. For example:
|
||||
*
|
||||
* ```js
|
||||
* var loopback = require('loopback');
|
||||
* var app = loopback();
|
||||
* ```
|
||||
*
|
||||
* @property {String} version Version of LoopBack framework. Static read-only property.
|
||||
* @property {String} mime
|
||||
* @property {Boolean} isBrowser True if running in a browser environment; false otherwise. Static read-only property.
|
||||
* @property {Boolean} isServer True if running in a server environment; false otherwise. Static read-only property.
|
||||
* @class loopback
|
||||
* @header loopback
|
||||
*/
|
||||
|
||||
var loopback = exports = module.exports = createApplication;
|
||||
|
||||
/**
|
||||
/*!
|
||||
* Framework version.
|
||||
*/
|
||||
|
||||
loopback.version = require('../package.json').version;
|
||||
|
||||
/**
|
||||
/*!
|
||||
* Expose mime.
|
||||
*/
|
||||
|
||||
|
@ -165,31 +169,4 @@ loopback.template = function (file) {
|
|||
* Built in models / services
|
||||
*/
|
||||
|
||||
loopback.Email = require('./models/email');
|
||||
loopback.User = require('./models/user');
|
||||
loopback.Application = require('./models/application');
|
||||
loopback.AccessToken = require('./models/access-token');
|
||||
loopback.Role = require('./models/role').Role;
|
||||
loopback.RoleMapping = require('./models/role').RoleMapping;
|
||||
loopback.ACL = require('./models/acl').ACL;
|
||||
loopback.Scope = require('./models/acl').Scope;
|
||||
loopback.Change = require('./models/change');
|
||||
|
||||
/*!
|
||||
* Automatically attach these models to dataSources
|
||||
*/
|
||||
|
||||
var dataSourceTypes = {
|
||||
DB: 'db',
|
||||
MAIL: 'mail'
|
||||
};
|
||||
|
||||
loopback.Email.autoAttach = dataSourceTypes.MAIL;
|
||||
loopback.PersistedModel.autoAttach = dataSourceTypes.DB;
|
||||
loopback.User.autoAttach = dataSourceTypes.DB;
|
||||
loopback.AccessToken.autoAttach = dataSourceTypes.DB;
|
||||
loopback.Role.autoAttach = dataSourceTypes.DB;
|
||||
loopback.RoleMapping.autoAttach = dataSourceTypes.DB;
|
||||
loopback.ACL.autoAttach = dataSourceTypes.DB;
|
||||
loopback.Scope.autoAttach = dataSourceTypes.DB;
|
||||
loopback.Application.autoAttach = dataSourceTypes.DB;
|
||||
require('./builtin-models')(loopback);
|
||||
|
|
|
@ -14,7 +14,7 @@ module.exports = status;
|
|||
* "uptime": 9.394
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @header loopback.status()
|
||||
*/
|
||||
function status() {
|
||||
|
@ -25,6 +25,6 @@ function status() {
|
|||
started: started,
|
||||
uptime: (Date.now() - Number(started)) / 1000
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,22 +11,22 @@ var assert = require('assert');
|
|||
|
||||
module.exports = token;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Check for an access token in cookies, headers, and query string parameters.
|
||||
* This function always checks for the following:
|
||||
*
|
||||
*
|
||||
* - `access_token` (params only)
|
||||
* - `X-Access-Token` (headers only)
|
||||
* - `authorization` (headers and cookies)
|
||||
*
|
||||
* It checks for these values in cookies, headers, and query string parameters _in addition_ to the items
|
||||
* specified in the options parameter.
|
||||
*
|
||||
*
|
||||
* **NOTE:** This function only checks for [signed cookies](http://expressjs.com/api.html#req.signedCookies).
|
||||
*
|
||||
*
|
||||
* The following example illustrates how to check for an `accessToken` in a custom cookie, query string parameter
|
||||
* and header called `foo-auth`.
|
||||
*
|
||||
*
|
||||
* ```js
|
||||
* app.use(loopback.token({
|
||||
* cookies: ['foo-auth'],
|
||||
|
@ -47,13 +47,13 @@ function token(options) {
|
|||
options = options || {};
|
||||
var TokenModel = options.model || loopback.AccessToken;
|
||||
assert(TokenModel, 'loopback.token() middleware requires a AccessToken model');
|
||||
|
||||
|
||||
return function (req, res, next) {
|
||||
if (req.accessToken !== undefined) return next();
|
||||
TokenModel.findForRequest(req, options, function(err, token) {
|
||||
req.accessToken = token || null;
|
||||
next(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -15,5 +15,5 @@ function urlNotFound() {
|
|||
var error = new Error('Cannot ' + req.method + ' ' + req.url);
|
||||
error.status = 404;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*!
|
||||
* Module Dependencies.
|
||||
*/
|
||||
var registry = require('../registry');
|
||||
var registry = require('./registry');
|
||||
var assert = require('assert');
|
||||
var RemoteObjects = require('strong-remoting');
|
||||
var SharedClass = require('strong-remoting').SharedClass;
|
||||
|
@ -9,7 +9,7 @@ var extend = require('util')._extend;
|
|||
var stringUtils = require('underscore.string');
|
||||
|
||||
/**
|
||||
* The base class for **all models**.
|
||||
* The base class for **all models**.
|
||||
*
|
||||
* **Inheriting from `Model`**
|
||||
*
|
||||
|
@ -18,7 +18,7 @@ var stringUtils = require('underscore.string');
|
|||
* var options = {...};
|
||||
* var MyModel = loopback.Model.extend('MyModel', properties, options);
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* **Options**
|
||||
*
|
||||
* - `trackChanges` - If true, changes to the model will be tracked. **Required
|
||||
|
@ -27,7 +27,7 @@ var stringUtils = require('underscore.string');
|
|||
* **Events**
|
||||
*
|
||||
* #### Event: `changed`
|
||||
*
|
||||
*
|
||||
* Emitted after a model has been successfully created, saved, or updated.
|
||||
* Argument: `inst`, model instance, object
|
||||
*
|
||||
|
@ -37,10 +37,10 @@ var stringUtils = require('underscore.string');
|
|||
* // => model with id 1 has been changed
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* #### Event: `deleted`
|
||||
*
|
||||
* Emitted after an individual model has been deleted.
|
||||
*
|
||||
* Emitted after an individual model has been deleted.
|
||||
* Argument: `id`, model ID (number).
|
||||
*
|
||||
* ```js
|
||||
|
@ -51,7 +51,7 @@ var stringUtils = require('underscore.string');
|
|||
* ```
|
||||
*
|
||||
* #### Event: `deletedAll`
|
||||
*
|
||||
*
|
||||
* Emitted after an individual model has been deleted.
|
||||
* Argument: `where` (optional), where filter, JSON object.
|
||||
*
|
||||
|
@ -65,31 +65,31 @@ var stringUtils = require('underscore.string');
|
|||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* #### Event: `attached`
|
||||
*
|
||||
*
|
||||
* Emitted after a `Model` has been attached to an `app`.
|
||||
*
|
||||
*
|
||||
* #### Event: `dataSourceAttached`
|
||||
*
|
||||
*
|
||||
* Emitted after a `Model` has been attached to a `DataSource`.
|
||||
*
|
||||
*
|
||||
* #### Event: set
|
||||
*
|
||||
*
|
||||
* Emitted when model property is set.
|
||||
* Argument: `inst`, model instance, object
|
||||
*
|
||||
*
|
||||
* ```js
|
||||
* MyModel.on('set', function(inst) {
|
||||
* console.log('model with id %s has been changed', inst.id);
|
||||
* // => model with id 1 has been changed
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @class
|
||||
*
|
||||
* @param {Object} data
|
||||
* @property {String} modelName The name of the model
|
||||
* @property {DataSource} dataSource
|
||||
* @property {String} modelName The name of the model. Static property.
|
||||
* @property {DataSource} dataSource Data source to which the model is connected, if any. Static property.
|
||||
* @class
|
||||
*/
|
||||
|
||||
var Model = module.exports = registry.modelBuilder.define('Model');
|
||||
|
@ -103,11 +103,14 @@ Model.setup = function () {
|
|||
var options = this.settings;
|
||||
var typeName = this.modelName;
|
||||
|
||||
var remotingOptions = {};
|
||||
extend(remotingOptions, options.remoting || {});
|
||||
|
||||
// create a sharedClass
|
||||
var sharedClass = ModelCtor.sharedClass = new SharedClass(
|
||||
ModelCtor.modelName,
|
||||
ModelCtor,
|
||||
options.remoting
|
||||
remotingOptions
|
||||
);
|
||||
|
||||
// setup a remoting type converter for this model
|
||||
|
@ -125,7 +128,7 @@ Model.setup = function () {
|
|||
id = null;
|
||||
} else if (typeof id === 'function') {
|
||||
fn = id;
|
||||
|
||||
|
||||
if(typeof data !== 'object') {
|
||||
id = data;
|
||||
data = null;
|
||||
|
@ -133,7 +136,7 @@ Model.setup = function () {
|
|||
id = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(id && data) {
|
||||
var model = new ModelCtor(data);
|
||||
model.id = id;
|
||||
|
@ -149,14 +152,14 @@ Model.setup = function () {
|
|||
} else {
|
||||
err = new Error('could not find a model with id ' + id);
|
||||
err.statusCode = 404;
|
||||
|
||||
|
||||
fn(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fn(new Error('must specify an id or data'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var idDesc = ModelCtor.modelName + ' id';
|
||||
ModelCtor.sharedCtor.accepts = [
|
||||
|
@ -168,7 +171,7 @@ Model.setup = function () {
|
|||
ModelCtor.sharedCtor.http = [
|
||||
{path: '/:id'}
|
||||
];
|
||||
|
||||
|
||||
ModelCtor.sharedCtor.returns = {root: true};
|
||||
|
||||
// before remote hook
|
||||
|
@ -187,7 +190,7 @@ Model.setup = function () {
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// after remote hook
|
||||
ModelCtor.afterRemote = function (name, fn) {
|
||||
var self = this;
|
||||
|
@ -250,25 +253,25 @@ Model._ACL = function getACL(ACL) {
|
|||
if(_aclModel) {
|
||||
return _aclModel;
|
||||
}
|
||||
var aclModel = require('./acl').ACL;
|
||||
var aclModel = registry.getModel('ACL');
|
||||
_aclModel = registry.getModelByType(aclModel);
|
||||
return _aclModel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given access token can invoke the method
|
||||
* Check if the given access token can invoke the specified method.
|
||||
*
|
||||
* @param {AccessToken} token The access token
|
||||
* @param {AccessToken} token The access token.
|
||||
* @param {*} modelId The model ID.
|
||||
* @param {SharedMethod} sharedMethod The method in question
|
||||
* @param {Object} ctx The remote invocation context
|
||||
* @callback {Function} callback The callback function
|
||||
* @param {String|Error} err The error object
|
||||
* @param {SharedMethod} sharedMethod The method in question.
|
||||
* @param {Object} ctx The remote invocation context.
|
||||
* @callback {Function} callback The callback function.
|
||||
* @param {String|Error} err The error object.
|
||||
* @param {Boolean} allowed True if the request is allowed; false otherwise.
|
||||
*/
|
||||
|
||||
Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) {
|
||||
var ANONYMOUS = require('./access-token').ANONYMOUS;
|
||||
var ANONYMOUS = registry.getModel('AccessToken').ANONYMOUS;
|
||||
token = token || ANONYMOUS;
|
||||
var aclModel = Model._ACL();
|
||||
|
||||
|
@ -277,7 +280,7 @@ Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) {
|
|||
callback = ctx;
|
||||
ctx = {};
|
||||
}
|
||||
|
||||
|
||||
aclModel.checkAccessForContext({
|
||||
accessToken: token,
|
||||
model: this,
|
||||
|
@ -305,7 +308,7 @@ Model._getAccessTypeForMethod = function(method) {
|
|||
method = {name: method};
|
||||
}
|
||||
assert(
|
||||
typeof method === 'object',
|
||||
typeof method === 'object',
|
||||
'method is a required argument and must be a RemoteMethod object'
|
||||
);
|
||||
|
||||
|
@ -337,7 +340,7 @@ Model._getAccessTypeForMethod = function(method) {
|
|||
default:
|
||||
return ACL.EXECUTE;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the `Application` the Model is attached to.
|
||||
|
@ -358,17 +361,18 @@ Model.getApp = function(callback) {
|
|||
callback(null, Model.app);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable remote invocation for the method with the given name.
|
||||
* See [Remote methods and hooks](http://docs.strongloop.com/display/LB/Remote+methods+and+hooks for more information.
|
||||
* See [Defining remote methods](http://docs.strongloop.com/display/LB/Defining+remote+methods) for more information.
|
||||
*
|
||||
* Static method example:
|
||||
* ```js
|
||||
* Model.myMethod();
|
||||
* Model.remoteMethod('myMethod');
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @param {String} name The name of the method.
|
||||
* @param {Object} options The remoting options.
|
||||
*/
|
||||
|
@ -378,7 +382,7 @@ Model.remoteMethod = function(name, options) {
|
|||
options.isStatic = true;
|
||||
}
|
||||
this.sharedClass.defineMethod(name, options);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable remote invocation for the method with the given name.
|
||||
|
@ -391,7 +395,7 @@ Model.remoteMethod = function(name, options) {
|
|||
|
||||
Model.disableRemoteMethod = function(name, isStatic) {
|
||||
this.sharedClass.disableMethod(name, isStatic || false);
|
||||
}
|
||||
};
|
||||
|
||||
Model.belongsToRemoting = function(relationName, relation, define) {
|
||||
var modelName = relation.modelTo && relation.modelTo.modelName;
|
||||
|
@ -405,7 +409,7 @@ Model.belongsToRemoting = function(relationName, relation, define) {
|
|||
description: 'Fetches belongsTo relation ' + relationName,
|
||||
returns: {arg: relationName, type: modelName, root: true}
|
||||
}, fn);
|
||||
}
|
||||
};
|
||||
|
||||
Model.hasOneRemoting = function(relationName, relation, define) {
|
||||
var fn = this.prototype[relationName];
|
||||
|
@ -417,22 +421,22 @@ Model.hasOneRemoting = function(relationName, relation, define) {
|
|||
description: 'Fetches hasOne relation ' + relationName,
|
||||
returns: {arg: relationName, type: relation.modelTo.modelName, root: true}
|
||||
}, fn);
|
||||
}
|
||||
};
|
||||
|
||||
Model.hasManyRemoting = function (relationName, relation, define) {
|
||||
var pathName = (relation.options.http && relation.options.http.path) || relationName;
|
||||
var toModelName = relation.modelTo.modelName;
|
||||
|
||||
|
||||
function convertNullToNotFoundError(ctx, cb) {
|
||||
if (ctx.result !== null) return cb();
|
||||
|
||||
|
||||
var fk = ctx.getArgByName('fk');
|
||||
var msg = 'Unknown "' + toModelName + '" id "' + fk + '".';
|
||||
var error = new Error(msg);
|
||||
error.statusCode = error.status = 404;
|
||||
cb(error);
|
||||
}
|
||||
|
||||
|
||||
var findByIdFunc = this.prototype['__findById__' + relationName];
|
||||
define('__findById__' + relationName, {
|
||||
isStatic: false,
|
||||
|
@ -564,9 +568,9 @@ Model.scopeRemoting = function(scopeName, scope, define) {
|
|||
http: {verb: 'get', path: '/' + pathName + '/count'},
|
||||
accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'},
|
||||
description: 'Counts ' + scopeName + ' of ' + this.modelName + '.',
|
||||
returns: {arg: 'count', type: 'number'}
|
||||
returns: {arg: 'count', type: 'number'}
|
||||
});
|
||||
|
||||
|
||||
};
|
||||
|
||||
Model.nestRemoting = function(relationName, options, cb) {
|
||||
|
@ -575,7 +579,7 @@ Model.nestRemoting = function(relationName, options, cb) {
|
|||
options = {};
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
|
||||
var regExp = /^__([^_]+)__([^_]+)$/;
|
||||
var relation = this.relations[relationName];
|
||||
if (relation && relation.modelTo && relation.modelTo.sharedClass) {
|
||||
|
@ -583,17 +587,17 @@ Model.nestRemoting = function(relationName, options, cb) {
|
|||
var sharedClass = this.sharedClass;
|
||||
var sharedToClass = relation.modelTo.sharedClass;
|
||||
var toModelName = relation.modelTo.modelName;
|
||||
|
||||
|
||||
var pathName = options.pathName || relation.options.path || relationName;
|
||||
var paramName = options.paramName || 'nk';
|
||||
|
||||
|
||||
var http = [].concat(sharedToClass.http || [])[0];
|
||||
|
||||
|
||||
if (relation.multiple) {
|
||||
var httpPath = pathName + '/:' + paramName;
|
||||
var acceptArgs = [
|
||||
{
|
||||
arg: paramName, type: 'any', http: { source: 'path' },
|
||||
{
|
||||
arg: paramName, type: 'any', http: { source: 'path' },
|
||||
description: 'Foreign key for ' + relation.name,
|
||||
required: true
|
||||
}
|
||||
|
@ -602,7 +606,7 @@ Model.nestRemoting = function(relationName, options, cb) {
|
|||
var httpPath = pathName;
|
||||
var acceptArgs = [];
|
||||
}
|
||||
|
||||
|
||||
// A method should return the method name to use, if it is to be
|
||||
// included as a nested method - a falsy return value will skip.
|
||||
var filter = cb || options.filterMethod || function(method, relation) {
|
||||
|
@ -611,31 +615,31 @@ Model.nestRemoting = function(relationName, options, cb) {
|
|||
return '__' + matches[1] + '__' + relation.name + '__' + matches[2];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
sharedToClass.methods().forEach(function(method) {
|
||||
var methodName;
|
||||
if (!method.isStatic && (methodName = filter(method, relation))) {
|
||||
var prefix = relation.multiple ? '__findById__' : '__get__';
|
||||
var getterName = options.getterName || (prefix + relationName);
|
||||
|
||||
|
||||
var getterFn = relation.modelFrom.prototype[getterName];
|
||||
if (typeof getterFn !== 'function') {
|
||||
throw new Error('Invalid remote method: `' + getterName + '`');
|
||||
}
|
||||
|
||||
|
||||
var nestedFn = relation.modelTo.prototype[method.name];
|
||||
if (typeof nestedFn !== 'function') {
|
||||
throw new Error('Invalid remote method: `' + method.name + '`');
|
||||
}
|
||||
|
||||
|
||||
var opts = {};
|
||||
|
||||
|
||||
opts.accepts = acceptArgs.concat(method.accepts || []);
|
||||
opts.returns = [].concat(method.returns || []);
|
||||
opts.description = method.description;
|
||||
opts.rest = extend({}, method.rest || {});
|
||||
opts.rest.delegateTo = method.name;
|
||||
|
||||
|
||||
opts.http = [];
|
||||
var routes = [].concat(method.http || []);
|
||||
routes.forEach(function(route) {
|
||||
|
@ -645,7 +649,7 @@ Model.nestRemoting = function(relationName, options, cb) {
|
|||
opts.http.push(copy);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (relation.multiple) {
|
||||
sharedClass.defineMethod(methodName, opts, function(fkId) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
|
@ -677,17 +681,17 @@ Model.nestRemoting = function(relationName, options, cb) {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (options.hooks === false) return; // don't inherit before/after hooks
|
||||
|
||||
|
||||
self.once('mounted', function(app, sc, remotes) {
|
||||
var listenerTree = extend({}, remotes.listenerTree || {});
|
||||
listenerTree.before = listenerTree.before || {};
|
||||
listenerTree.after = listenerTree.after || {};
|
||||
|
||||
|
||||
var beforeListeners = remotes.listenerTree.before[toModelName] || {};
|
||||
var afterListeners = remotes.listenerTree.after[toModelName] || {};
|
||||
|
||||
|
||||
sharedClass.methods().forEach(function(method) {
|
||||
var delegateTo = method.rest && method.rest.delegateTo;
|
||||
if (delegateTo) {
|
||||
|
@ -707,12 +711,14 @@ Model.nestRemoting = function(relationName, options, cb) {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
throw new Error('Relation `' + relationName + '` does not exist for model `' + this.modelName + '`');
|
||||
}
|
||||
};
|
||||
|
||||
Model.ValidationError = require('loopback-datasource-juggler').ValidationError;
|
||||
|
||||
// setup the initial model
|
||||
Model.setup();
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
/*!
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var loopback = require('../loopback')
|
||||
, assert = require('assert')
|
||||
, crypto = require('crypto')
|
||||
, uid = require('uid2')
|
||||
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
|
||||
, DEFAULT_TOKEN_LEN = 64
|
||||
, Role = require('./role').Role
|
||||
, ACL = require('./acl').ACL;
|
||||
|
||||
/*!
|
||||
* Default AccessToken properties.
|
||||
*/
|
||||
|
||||
var properties = {
|
||||
id: {type: String, id: true},
|
||||
ttl: {type: Number, ttl: true, default: DEFAULT_TTL}, // time to live in seconds
|
||||
created: {type: Date, default: function() {
|
||||
return new Date();
|
||||
}}
|
||||
};
|
||||
|
||||
/**
|
||||
* Token based authentication and access control.
|
||||
*
|
||||
* **Default ACLs**
|
||||
*
|
||||
* - DENY EVERYONE `*`
|
||||
* - ALLOW EVERYONE create
|
||||
*
|
||||
* @property {String} id Generated token ID
|
||||
* @property {Number} ttl Time to live in seconds
|
||||
* @property {Date} created When the token was created
|
||||
*
|
||||
* @class
|
||||
* @inherits {PersistedModel}
|
||||
*/
|
||||
|
||||
var AccessToken = module.exports =
|
||||
loopback.PersistedModel.extend('AccessToken', properties, {
|
||||
acls: [
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
principalId: Role.EVERYONE,
|
||||
permission: 'DENY'
|
||||
},
|
||||
{
|
||||
principalType: ACL.ROLE,
|
||||
principalId: Role.EVERYONE,
|
||||
property: 'create',
|
||||
permission: 'ALLOW'
|
||||
}
|
||||
],
|
||||
relations: {
|
||||
user: {
|
||||
type: 'belongsTo',
|
||||
model: 'User',
|
||||
foreignKey: 'userId'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Anonymous Token
|
||||
*
|
||||
* ```js
|
||||
* assert(AccessToken.ANONYMOUS.id === '$anonymous');
|
||||
* ```
|
||||
*/
|
||||
|
||||
AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'});
|
||||
|
||||
/**
|
||||
* Create a cryptographically random access token id.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {String} token
|
||||
*/
|
||||
|
||||
AccessToken.createAccessTokenId = function (fn) {
|
||||
uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) {
|
||||
if(err) {
|
||||
fn(err);
|
||||
} else {
|
||||
fn(null, guid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*!
|
||||
* Hook to create accessToken id.
|
||||
*/
|
||||
|
||||
AccessToken.beforeCreate = function (next, data) {
|
||||
data = data || {};
|
||||
|
||||
AccessToken.createAccessTokenId(function (err, id) {
|
||||
if(err) {
|
||||
next(err);
|
||||
} else {
|
||||
data.id = id;
|
||||
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a token for the given `ServerRequest`.
|
||||
*
|
||||
* @param {ServerRequest} req
|
||||
* @param {Object} [options] Options for finding the token
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {AccessToken} token
|
||||
*/
|
||||
|
||||
AccessToken.findForRequest = function(req, options, cb) {
|
||||
var id = tokenIdForRequest(req, options);
|
||||
|
||||
if(id) {
|
||||
this.findById(id, function(err, token) {
|
||||
if(err) {
|
||||
cb(err);
|
||||
} else if(token) {
|
||||
token.validate(function(err, isValid) {
|
||||
if(err) {
|
||||
cb(err);
|
||||
} else if(isValid) {
|
||||
cb(null, token);
|
||||
} else {
|
||||
var e = new Error('Invalid Access Token');
|
||||
e.status = e.statusCode = 401;
|
||||
cb(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the token.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Boolean} isValid
|
||||
*/
|
||||
|
||||
AccessToken.prototype.validate = function(cb) {
|
||||
try {
|
||||
assert(
|
||||
this.created && typeof this.created.getTime === 'function',
|
||||
'token.created must be a valid Date'
|
||||
);
|
||||
assert(this.ttl !== 0, 'token.ttl must be not be 0');
|
||||
assert(this.ttl, 'token.ttl must exist');
|
||||
assert(this.ttl >= -1, 'token.ttl must be >= -1');
|
||||
|
||||
var now = Date.now();
|
||||
var created = this.created.getTime();
|
||||
var elapsedSeconds = (now - created) / 1000;
|
||||
var secondsToLive = this.ttl;
|
||||
var isValid = elapsedSeconds < secondsToLive;
|
||||
|
||||
if(isValid) {
|
||||
cb(null, isValid);
|
||||
} else {
|
||||
this.destroy(function(err) {
|
||||
cb(err, isValid);
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
cb(e);
|
||||
}
|
||||
}
|
||||
|
||||
function tokenIdForRequest(req, options) {
|
||||
var params = options.params || [];
|
||||
var headers = options.headers || [];
|
||||
var cookies = options.cookies || [];
|
||||
var i = 0;
|
||||
var length;
|
||||
var id;
|
||||
|
||||
params = params.concat(['access_token']);
|
||||
headers = headers.concat(['X-Access-Token', 'authorization']);
|
||||
cookies = cookies.concat(['access_token', 'authorization']);
|
||||
|
||||
for(length = params.length; i < length; i++) {
|
||||
id = req.param(params[i]);
|
||||
|
||||
if(typeof id === 'string') {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
for(i = 0, length = headers.length; i < length; i++) {
|
||||
id = req.header(headers[i]);
|
||||
|
||||
if(typeof id === 'string') {
|
||||
// Add support for oAuth 2.0 bearer token
|
||||
// http://tools.ietf.org/html/rfc6750
|
||||
if (id.indexOf('Bearer ') === 0) {
|
||||
id = id.substring(7);
|
||||
// Decode from base64
|
||||
var buf = new Buffer(id, 'base64');
|
||||
id = buf.toString('utf8');
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
if(req.signedCookies) {
|
||||
for(i = 0, length = cookies.length; i < length; i++) {
|
||||
id = req.signedCookies[cookies[i]];
|
||||
|
||||
if(typeof id === 'string') {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -1,511 +0,0 @@
|
|||
/*!
|
||||
Schema ACL options
|
||||
|
||||
Object level permissions, for example, an album owned by a user
|
||||
|
||||
Factors to be authorized against:
|
||||
|
||||
* model name: Album
|
||||
* model instance properties: userId of the album, friends, shared
|
||||
* methods
|
||||
* app and/or user ids/roles
|
||||
** loggedIn
|
||||
** roles
|
||||
** userId
|
||||
** appId
|
||||
** none
|
||||
** everyone
|
||||
** relations: owner/friend/granted
|
||||
|
||||
Class level permissions, for example, Album
|
||||
* model name: Album
|
||||
* methods
|
||||
|
||||
URL/Route level permissions
|
||||
* url pattern
|
||||
* application id
|
||||
* ip addresses
|
||||
* http headers
|
||||
|
||||
Map to oAuth 2.0 scopes
|
||||
|
||||
*/
|
||||
|
||||
var loopback = require('../loopback');
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
var debug = require('debug')('loopback:security:acl');
|
||||
|
||||
var ctx = require('./access-context');
|
||||
var AccessContext = ctx.AccessContext;
|
||||
var Principal = ctx.Principal;
|
||||
var AccessRequest = ctx.AccessRequest;
|
||||
|
||||
var role = require('./role');
|
||||
var Role = role.Role;
|
||||
|
||||
/**
|
||||
* System grants permissions to principals (users/applications, can be grouped
|
||||
* into roles).
|
||||
*
|
||||
* Protected resource: the model data and operations
|
||||
* (model/property/method/relation/…)
|
||||
*
|
||||
* 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
|
||||
property: String, // The name of the property, method, scope, or relation
|
||||
accessType: String,
|
||||
permission: String,
|
||||
principalType: String,
|
||||
principalId: String
|
||||
};
|
||||
|
||||
/**
|
||||
* A Model for access control meta data.
|
||||
*
|
||||
* @header ACL
|
||||
* @property {String} model Name of the model.
|
||||
* @property {String} property Name of the property, method, scope, or relation.
|
||||
* @property {String} accessType Type of access being granted: one of READ, WRITE, or EXECUTE.
|
||||
* @property {String} permission Type of permission granted. One of:
|
||||
* - ALARM: Generate an alarm, in a system-dependent way, the access specified in the permissions component of the ACL entry.
|
||||
* - ALLOW: Explicitly grants access to the resource.
|
||||
* - AUDIT: Log, in a system-dependent way, the access specified in the permissions component of the ACL entry.
|
||||
* - DENY: Explicitly denies access to the resource.
|
||||
* @property {String} principalType Type of the principal; one of: Application, Use, Role.
|
||||
* @property {String} principalId ID of the principal - such as appId, userId or roleId
|
||||
* @class
|
||||
* @inherits Model
|
||||
*/
|
||||
|
||||
var ACL = loopback.PersistedModel.extend('ACL', ACLSchema);
|
||||
|
||||
ACL.ALL = AccessContext.ALL;
|
||||
|
||||
ACL.DEFAULT = AccessContext.DEFAULT; // Not specified
|
||||
ACL.ALLOW = AccessContext.ALLOW; // Allow
|
||||
ACL.ALARM = AccessContext.ALARM; // Warn - send an alarm
|
||||
ACL.AUDIT = AccessContext.AUDIT; // Audit - record the access
|
||||
ACL.DENY = AccessContext.DENY; // Deny
|
||||
|
||||
ACL.READ = AccessContext.READ; // Read operation
|
||||
ACL.WRITE = AccessContext.WRITE; // Write operation
|
||||
ACL.EXECUTE = AccessContext.EXECUTE; // Execute operation
|
||||
|
||||
ACL.USER = Principal.USER;
|
||||
ACL.APP = ACL.APPLICATION = Principal.APPLICATION;
|
||||
ACL.ROLE = Principal.ROLE;
|
||||
ACL.SCOPE = Principal.SCOPE;
|
||||
|
||||
/**
|
||||
* Calculate the matching score for the given rule and request
|
||||
* @param {ACL} rule The ACL entry
|
||||
* @param {AccessRequest} req The request
|
||||
* @returns {Number}
|
||||
*/
|
||||
ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||
var props = ['model', 'property', 'accessType'];
|
||||
var score = 0;
|
||||
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
// Shift the score by 4 for each of the properties as the weight
|
||||
score = score * 4;
|
||||
var val1 = rule[props[i]] || ACL.ALL;
|
||||
var val2 = req[props[i]] || ACL.ALL;
|
||||
var isMatchingMethodName = props[i] === 'property' && req.methodNames.indexOf(val1) !== -1;
|
||||
|
||||
if (val1 === val2 || isMatchingMethodName) {
|
||||
// Exact match
|
||||
score += 3;
|
||||
} else if (val1 === ACL.ALL) {
|
||||
// Wildcard match
|
||||
score += 2;
|
||||
} else if (val2 === ACL.ALL) {
|
||||
// Doesn't match at all
|
||||
score += 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Weigh against the principal type into 4 levels
|
||||
// - user level (explicitly allow/deny a given user)
|
||||
// - app level (explicitly allow/deny a given app)
|
||||
// - role level (role based authorization)
|
||||
// - other
|
||||
// user > app > role > ...
|
||||
score = score * 4;
|
||||
switch(rule.principalType) {
|
||||
case ACL.USER:
|
||||
score += 4;
|
||||
break;
|
||||
case ACL.APP:
|
||||
score += 3;
|
||||
break;
|
||||
case ACL.ROLE:
|
||||
score += 2;
|
||||
break;
|
||||
default:
|
||||
score +=1;
|
||||
}
|
||||
|
||||
// Weigh against the roles
|
||||
// everyone < authenticated/unauthenticated < related < owner < ...
|
||||
score = score * 8;
|
||||
if(rule.principalType === ACL.ROLE) {
|
||||
switch(rule.principalId) {
|
||||
case Role.OWNER:
|
||||
score += 4;
|
||||
break;
|
||||
case Role.RELATED:
|
||||
score += 3;
|
||||
break;
|
||||
case Role.AUTHENTICATED:
|
||||
case Role.UNAUTHENTICATED:
|
||||
score += 2;
|
||||
break;
|
||||
case Role.EVERYONE:
|
||||
score += 1;
|
||||
break;
|
||||
default:
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
score = score * 4;
|
||||
score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1;
|
||||
return score;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get matching score for the given `AccessRequest`.
|
||||
* @param {AccessRequest} req The request
|
||||
* @returns {Number} score
|
||||
*/
|
||||
|
||||
ACL.prototype.score = function(req) {
|
||||
return this.constructor.getMatchingScore(this, req);
|
||||
}
|
||||
|
||||
/*!
|
||||
* Resolve permission from the ACLs
|
||||
* @param {Object[]) acls The list of ACLs
|
||||
* @param {Object} req The request
|
||||
* @returns {AccessRequest} result The effective ACL
|
||||
*/
|
||||
ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||
if(!(req instanceof AccessRequest)) {
|
||||
req = new AccessRequest(req);
|
||||
}
|
||||
// Sort by the matching score in descending order
|
||||
acls = acls.sort(function (rule1, rule2) {
|
||||
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
|
||||
});
|
||||
var permission = ACL.DEFAULT;
|
||||
var score = 0;
|
||||
|
||||
for (var i = 0; i < acls.length; i++) {
|
||||
score = ACL.getMatchingScore(acls[i], req);
|
||||
if (score < 0) {
|
||||
// the highest scored ACL did not match
|
||||
break;
|
||||
}
|
||||
if (!req.isWildcard()) {
|
||||
// We should stop from the first match for non-wildcard
|
||||
permission = acls[i].permission;
|
||||
break;
|
||||
} else {
|
||||
if(req.exactlyMatches(acls[i])) {
|
||||
permission = acls[i].permission;
|
||||
break;
|
||||
}
|
||||
// For wildcard match, find the strongest permission
|
||||
if(AccessContext.permissionOrder[acls[i].permission]
|
||||
> AccessContext.permissionOrder[permission]) {
|
||||
permission = acls[i].permission;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(debug.enabled) {
|
||||
debug('The following ACLs were searched: ');
|
||||
acls.forEach(function(acl) {
|
||||
acl.debug();
|
||||
debug('with score:', acl.score(req));
|
||||
});
|
||||
}
|
||||
|
||||
var res = new AccessRequest(req.model, req.property, req.accessType,
|
||||
permission || ACL.DEFAULT);
|
||||
return res;
|
||||
};
|
||||
|
||||
/*!
|
||||
* Get the static ACLs from the model definition
|
||||
* @param {String} model The model name
|
||||
* @param {String} property The property/method/relation name
|
||||
*
|
||||
* @return {Object[]} An array of ACLs
|
||||
*/
|
||||
ACL.getStaticACLs = function getStaticACLs(model, property) {
|
||||
var modelClass = loopback.findModel(model);
|
||||
var staticACLs = [];
|
||||
if (modelClass && modelClass.settings.acls) {
|
||||
modelClass.settings.acls.forEach(function (acl) {
|
||||
staticACLs.push(new ACL({
|
||||
model: model,
|
||||
property: acl.property || ACL.ALL,
|
||||
principalType: acl.principalType,
|
||||
principalId: acl.principalId, // TODO: Should it be a name?
|
||||
accessType: acl.accessType || ACL.ALL,
|
||||
permission: acl.permission
|
||||
}));
|
||||
});
|
||||
}
|
||||
var prop = modelClass &&
|
||||
(modelClass.definition.properties[property] // regular property
|
||||
|| (modelClass._scopeMeta && modelClass._scopeMeta[property]) // relation/scope
|
||||
|| modelClass[property] // static method
|
||||
|| modelClass.prototype[property]); // prototype method
|
||||
if (prop && prop.acls) {
|
||||
prop.acls.forEach(function (acl) {
|
||||
staticACLs.push(new ACL({
|
||||
model: modelClass.modelName,
|
||||
property: property,
|
||||
principalType: acl.principalType,
|
||||
principalId: acl.principalId,
|
||||
accessType: acl.accessType,
|
||||
permission: acl.permission
|
||||
}));
|
||||
});
|
||||
}
|
||||
return staticACLs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given principal is allowed to access the model/property
|
||||
* @param {String} principalType The principal type.
|
||||
* @param {String} principalId The principal ID.
|
||||
* @param {String} model The model name.
|
||||
* @param {String} property The property/method/relation name.
|
||||
* @param {String} accessType The access type.
|
||||
* @callback {Function} callback Callback function.
|
||||
* @param {String|Error} err The error object
|
||||
* @param {AccessRequest} result The access permission
|
||||
*/
|
||||
ACL.checkPermission = function checkPermission(principalType, principalId,
|
||||
model, property, accessType,
|
||||
callback) {
|
||||
if(principalId !== null && principalId !== undefined && (typeof principalId !== 'string') ) {
|
||||
principalId = principalId.toString();
|
||||
}
|
||||
property = property || ACL.ALL;
|
||||
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
|
||||
accessType = accessType || ACL.ALL;
|
||||
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
|
||||
|
||||
var req = new AccessRequest(model, property, accessType);
|
||||
|
||||
var acls = this.getStaticACLs(model, property);
|
||||
|
||||
var resolved = this.resolvePermission(acls, req);
|
||||
|
||||
if(resolved && resolved.permission === ACL.DENY) {
|
||||
debug('Permission denied by statically resolved permission');
|
||||
debug(' Resolved Permission: %j', resolved);
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, resolved);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.find({where: {principalType: principalType, principalId: principalId,
|
||||
model: model, property: propertyQuery, accessType: accessTypeQuery}},
|
||||
function (err, dynACLs) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
acls = acls.concat(dynACLs);
|
||||
resolved = self.resolvePermission(acls, req);
|
||||
if(resolved && resolved.permission === ACL.DEFAULT) {
|
||||
var modelClass = loopback.findModel(model);
|
||||
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
|
||||
}
|
||||
callback && callback(null, resolved);
|
||||
});
|
||||
};
|
||||
|
||||
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.
|
||||
* @options {Object} context See below.
|
||||
* @property {Object[]} principals An array of principals.
|
||||
* @property {String|Model} model The model name or model class.
|
||||
* @property {*} id The model instance ID.
|
||||
* @property {String} property The property/method/relation name.
|
||||
* @property {String} accessType The access type: READE, WRITE, or EXECUTE.
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
|
||||
ACL.checkAccessForContext = function (context, callback) {
|
||||
if(!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
|
||||
var model = context.model;
|
||||
var property = context.property;
|
||||
var accessType = context.accessType;
|
||||
var modelName = context.modelName;
|
||||
|
||||
var methodNames = context.methodNames;
|
||||
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: methodNames.concat([ACL.ALL])};
|
||||
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
|
||||
|
||||
var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames);
|
||||
|
||||
var effectiveACLs = [];
|
||||
var staticACLs = this.getStaticACLs(model.modelName, property);
|
||||
|
||||
var self = this;
|
||||
var roleModel = loopback.getModelByType(Role);
|
||||
this.find({where: {model: model.modelName, property: propertyQuery,
|
||||
accessType: accessTypeQuery}}, function (err, acls) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
var inRoleTasks = [];
|
||||
|
||||
acls = acls.concat(staticACLs);
|
||||
|
||||
acls.forEach(function (acl) {
|
||||
// Check exact matches
|
||||
for (var i = 0; i < context.principals.length; i++) {
|
||||
var p = context.principals[i];
|
||||
if (p.type === acl.principalType
|
||||
&& String(p.id) === String(acl.principalId)) {
|
||||
effectiveACLs.push(acl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check role matches
|
||||
if (acl.principalType === ACL.ROLE) {
|
||||
inRoleTasks.push(function (done) {
|
||||
roleModel.isInRole(acl.principalId, context,
|
||||
function (err, inRole) {
|
||||
if (!err && inRole) {
|
||||
effectiveACLs.push(acl);
|
||||
}
|
||||
done(err, acl);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async.parallel(inRoleTasks, function (err, results) {
|
||||
if(err) {
|
||||
callback && callback(err, null);
|
||||
return;
|
||||
}
|
||||
var resolved = self.resolvePermission(effectiveACLs, req);
|
||||
if(resolved && resolved.permission === ACL.DEFAULT) {
|
||||
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
|
||||
}
|
||||
debug('---Resolved---');
|
||||
resolved.debug();
|
||||
callback && callback(null, resolved);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given access token can invoke the method
|
||||
* @param {AccessToken} token The access token
|
||||
* @param {String} model The model name
|
||||
* @param {*} modelId The model id
|
||||
* @param {String} method The method name
|
||||
* @callback {Function} callback Callback function
|
||||
* @param {String|Error} err The error object
|
||||
* @param {Boolean} allowed is the request allowed
|
||||
*/
|
||||
ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
|
||||
assert(token, 'Access token is required');
|
||||
|
||||
var context = new AccessContext({
|
||||
accessToken: token,
|
||||
model: model,
|
||||
property: method,
|
||||
method: method,
|
||||
modelId: modelId
|
||||
});
|
||||
|
||||
this.checkAccessForContext(context, function (err, access) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
callback && callback(null, access.permission !== ACL.DENY);
|
||||
});
|
||||
};
|
||||
|
||||
/*!
|
||||
* Schema for Scope which represents the permissions that are granted to client
|
||||
* applications by the resource owner
|
||||
*/
|
||||
var ScopeSchema = {
|
||||
name: {type: String, required: true},
|
||||
description: String
|
||||
};
|
||||
|
||||
/**
|
||||
* Resource owner grants/delegates permissions to client applications
|
||||
*
|
||||
* For a protected resource, does the client application have the authorization
|
||||
* from the resource owner (user or system)?
|
||||
*
|
||||
* Scope has many resource access entries
|
||||
* @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) {
|
||||
this.findOne({where: {name: scope}}, function (err, scope) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
} else {
|
||||
var aclModel = loopback.getModelByType(ACL);
|
||||
aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.ACL = ACL;
|
||||
module.exports.Scope = Scope;
|
|
@ -1,254 +0,0 @@
|
|||
var loopback = require('../loopback');
|
||||
var assert = require('assert');
|
||||
|
||||
// Authentication schemes
|
||||
var AuthenticationSchemeSchema = {
|
||||
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 = {
|
||||
/**
|
||||
* 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: {
|
||||
gateway: String,
|
||||
port: Number
|
||||
}},
|
||||
|
||||
feedbackOptions: {type: {
|
||||
gateway: String,
|
||||
port: Number,
|
||||
batchFeedback: Boolean,
|
||||
interval: Number
|
||||
}}
|
||||
};
|
||||
|
||||
var GcmSettingsSchema = {
|
||||
serverApiKey: String
|
||||
};
|
||||
|
||||
// Push notification settings
|
||||
var PushNotificationSettingSchema = {
|
||||
apns: APNSSettingSchema,
|
||||
gcm: GcmSettingsSchema
|
||||
};
|
||||
|
||||
/*!
|
||||
* Data model for Application
|
||||
*/
|
||||
var ApplicationSchema = {
|
||||
id: {type: String, id: 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
|
||||
|
||||
// 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
|
||||
|
||||
// Keys
|
||||
clientKey: String,
|
||||
javaScriptKey: String,
|
||||
restApiKey: String,
|
||||
windowsKey: String,
|
||||
masterKey: String,
|
||||
|
||||
// Push notification
|
||||
pushSettings: PushNotificationSettingSchema,
|
||||
|
||||
// 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
|
||||
|
||||
// Timestamps
|
||||
created: {type: Date, default: Date},
|
||||
modified: {type: Date, default: Date}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Application management functions
|
||||
*/
|
||||
|
||||
var crypto = require('crypto');
|
||||
|
||||
function generateKey(hmacKey, algorithm, encoding) {
|
||||
hmacKey = hmacKey || 'loopback';
|
||||
algorithm = algorithm || 'sha1';
|
||||
encoding = encoding || 'hex';
|
||||
var hmac = crypto.createHmac(algorithm, hmacKey);
|
||||
var buf = crypto.randomBytes(32);
|
||||
hmac.update(buf);
|
||||
var key = hmac.digest(encoding);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage client applications and organize their users.
|
||||
*
|
||||
* @property {String} id Generated ID.
|
||||
* @property {String} name Name; required.
|
||||
* @property {String} description Text description
|
||||
* @property {String} icon String Icon image URL.
|
||||
* @property {String} owner User ID of the developer who registers the application.
|
||||
* @property {String} email E-mail address
|
||||
* @property {Boolean} emailVerified Whether the e-mail is verified.
|
||||
* @property {String} url OAuth 2.0 application URL.
|
||||
* @property {String}[] callbackUrls The OAuth 2.0 code/token callback URL.
|
||||
* @property {String} status Status of the application; Either `production`, `sandbox` (default), or `disabled`.
|
||||
* @property {Date} created Date Application object was created. Default: current date.
|
||||
* @property {Date} modified Date Application object was modified. Default: current date.
|
||||
*
|
||||
* @property {Boolean} pushSettings.apns.production Whether to use production Apple Push Notification Service (APNS) servers to send push notifications.
|
||||
* If true, uses `gateway.push.apple.com:2195` and `feedback.push.apple.com:2196`.
|
||||
* If false, uses `gateway.sandbox.push.apple.com:2195` and `feedback.sandbox.push.apple.com:2196`
|
||||
* @property {String} pushSettings.apns.certData The certificate data loaded from the cert.pem file (APNS).
|
||||
* @property {String} pushSettings.apns.keyData The key data loaded from the key.pem file (APNS).
|
||||
* @property {String} pushSettings.apns.pushOptions.gateway (APNS).
|
||||
* @property {Number} pushSettings.apns.pushOptions.port (APNS).
|
||||
* @property {String} pushSettings.apns.feedbackOptions.gateway (APNS).
|
||||
* @property {Number} pushSettings.apns.feedbackOptions.port (APNS).
|
||||
* @property {Boolean} pushSettings.apns.feedbackOptions.batchFeedback (APNS).
|
||||
* @property {Number} pushSettings.apns.feedbackOptions.interval (APNS).
|
||||
* @property {String} pushSettings.gcm.serverApiKey: Google Cloud Messaging API key.
|
||||
*
|
||||
* @class
|
||||
* @inherits {Model}
|
||||
*/
|
||||
|
||||
var Application = loopback.PersistedModel.extend('Application', ApplicationSchema);
|
||||
|
||||
/*!
|
||||
* A hook to generate keys before creation
|
||||
* @param next
|
||||
*/
|
||||
Application.beforeCreate = function (next) {
|
||||
var app = this;
|
||||
app.created = app.modified = new Date();
|
||||
app.id = generateKey('id', 'md5');
|
||||
app.clientKey = generateKey('client');
|
||||
app.javaScriptKey = generateKey('javaScript');
|
||||
app.restApiKey = generateKey('restApi');
|
||||
app.windowsKey = generateKey('windows');
|
||||
app.masterKey = generateKey('master');
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a new application
|
||||
* @param {String} owner Owner's user ID.
|
||||
* @param {String} name Name of the application
|
||||
* @param {Object} options Other options
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
Application.register = function (owner, name, options, cb) {
|
||||
assert(owner, 'owner is required');
|
||||
assert(name, 'name is required');
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
this.create(props, cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset keys for the application instance
|
||||
* @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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset keys for a given application by the appId
|
||||
* @param {Any} appId
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
*/
|
||||
Application.resetKeys = function (appId, cb) {
|
||||
this.findById(appId, function (err, app) {
|
||||
if (err) {
|
||||
cb && cb(err, app);
|
||||
return;
|
||||
}
|
||||
app.resetKeys(cb);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate the application id and key.
|
||||
*
|
||||
* `matched` parameter is one of:
|
||||
* - 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) {
|
||||
this.findById(appId, function (err, app) {
|
||||
if (err || !app) {
|
||||
cb && cb(err, null);
|
||||
return;
|
||||
}
|
||||
var result = null;
|
||||
var keyNames = ['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'];
|
||||
for (var i = 0; i < keyNames.length; i++) {
|
||||
if (app[keyNames[i]] === key) {
|
||||
result = {
|
||||
application: app,
|
||||
keyType: keyNames[i]
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
cb && cb(null, result);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Application;
|
||||
|
|
@ -1,646 +0,0 @@
|
|||
/*!
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var PersistedModel = require('./persisted-model')
|
||||
, loopback = require('../loopback')
|
||||
, crypto = require('crypto')
|
||||
, CJSON = {stringify: require('canonical-json')}
|
||||
, async = require('async')
|
||||
, assert = require('assert')
|
||||
, debug = require('debug')('loopback:change');
|
||||
|
||||
/*!
|
||||
* Properties
|
||||
*/
|
||||
|
||||
var properties = {
|
||||
id: {type: String, id: true},
|
||||
rev: {type: String},
|
||||
prev: {type: String},
|
||||
checkpoint: {type: Number},
|
||||
modelName: {type: String},
|
||||
modelId: {type: String}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Options
|
||||
*/
|
||||
|
||||
var options = {
|
||||
trackChanges: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Change list entry.
|
||||
*
|
||||
* @property {String} id Hash of the modelName and id
|
||||
* @property {String} rev The current model revision
|
||||
* @property {String} prev The previous model revision
|
||||
* @property {Number} checkpoint The current checkpoint at time of the change
|
||||
* @property {String} modelName Model name
|
||||
* @property {String} modelId Model ID
|
||||
*
|
||||
* @class
|
||||
* @inherits {Model}
|
||||
*/
|
||||
|
||||
var Change = module.exports = PersistedModel.extend('Change', properties, options);
|
||||
|
||||
/*!
|
||||
* Constants
|
||||
*/
|
||||
|
||||
Change.UPDATE = 'update';
|
||||
Change.CREATE = 'create';
|
||||
Change.DELETE = 'delete';
|
||||
Change.UNKNOWN = 'unknown';
|
||||
|
||||
/*!
|
||||
* Conflict Class
|
||||
*/
|
||||
|
||||
Change.Conflict = Conflict;
|
||||
|
||||
/*!
|
||||
* Setup the extended model.
|
||||
*/
|
||||
|
||||
Change.setup = function() {
|
||||
PersistedModel.setup.call(this);
|
||||
var Change = this;
|
||||
|
||||
Change.getter.id = function() {
|
||||
var hasModel = this.modelName && this.modelId;
|
||||
if(!hasModel) return null;
|
||||
|
||||
return Change.idForModel(this.modelName, this.modelId);
|
||||
}
|
||||
}
|
||||
Change.setup();
|
||||
|
||||
/**
|
||||
* Track the recent change of the given modelIds.
|
||||
*
|
||||
* @param {String} modelName
|
||||
* @param {Array} modelIds
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Array} changes Changes that were tracked
|
||||
*/
|
||||
|
||||
Change.rectifyModelChanges = function(modelName, modelIds, callback) {
|
||||
var tasks = [];
|
||||
var Change = this;
|
||||
|
||||
modelIds.forEach(function(id) {
|
||||
tasks.push(function(cb) {
|
||||
Change.findOrCreateChange(modelName, id, function(err, change) {
|
||||
if(err) return Change.handleError(err, cb);
|
||||
change.rectify(cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
async.parallel(tasks, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an identifier for a given model.
|
||||
*
|
||||
* @param {String} modelName
|
||||
* @param {String} modelId
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
Change.idForModel = function(modelName, modelId) {
|
||||
return this.hash([modelName, modelId].join('-'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a change for the given model.
|
||||
*
|
||||
* @param {String} modelName
|
||||
* @param {String} modelId
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Change} change
|
||||
* @end
|
||||
*/
|
||||
|
||||
Change.findOrCreateChange = function(modelName, modelId, callback) {
|
||||
assert(loopback.findModel(modelName), modelName + ' does not exist');
|
||||
var id = this.idForModel(modelName, modelId);
|
||||
var Change = this;
|
||||
|
||||
this.findById(id, function(err, change) {
|
||||
if(err) return callback(err);
|
||||
if(change) {
|
||||
callback(null, change);
|
||||
} else {
|
||||
var ch = new Change({
|
||||
id: id,
|
||||
modelName: modelName,
|
||||
modelId: modelId
|
||||
});
|
||||
ch.debug('creating change');
|
||||
ch.save(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update (or create) the change with the current revision.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
Change.prototype.rectify = function(cb) {
|
||||
var change = this;
|
||||
var tasks = [
|
||||
updateRevision,
|
||||
updateCheckpoint
|
||||
];
|
||||
var currentRev = this.rev;
|
||||
|
||||
change.debug('rectify change');
|
||||
|
||||
cb = cb || function(err) {
|
||||
if(err) throw new Error(err);
|
||||
}
|
||||
|
||||
async.parallel(tasks, function(err) {
|
||||
if(err) return cb(err);
|
||||
if(change.prev === Change.UNKNOWN) {
|
||||
// this occurs when a record of a change doesn't exist
|
||||
// and its current revision is null (not found)
|
||||
change.remove(cb);
|
||||
} else {
|
||||
change.save(cb);
|
||||
}
|
||||
});
|
||||
|
||||
function updateRevision(cb) {
|
||||
// get the current revision
|
||||
change.currentRevision(function(err, rev) {
|
||||
if(err) return Change.handleError(err, cb);
|
||||
if(rev) {
|
||||
// avoid setting rev and prev to the same value
|
||||
if(currentRev !== rev) {
|
||||
change.rev = rev;
|
||||
change.prev = currentRev;
|
||||
} else {
|
||||
change.debug('rev and prev are equal (not updating rev)');
|
||||
}
|
||||
} else {
|
||||
change.rev = null;
|
||||
if(currentRev) {
|
||||
change.prev = currentRev;
|
||||
} else if(!change.prev) {
|
||||
change.debug('ERROR - could not determing prev');
|
||||
change.prev = Change.UNKNOWN;
|
||||
}
|
||||
}
|
||||
change.debug('updated revision (was ' + currentRev + ')');
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function updateCheckpoint(cb) {
|
||||
change.constructor.getCheckpointModel().current(function(err, checkpoint) {
|
||||
if(err) return Change.handleError(err);
|
||||
change.checkpoint = checkpoint;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a change's current revision based on current data.
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {String} rev The current revision
|
||||
*/
|
||||
|
||||
Change.prototype.currentRevision = function(cb) {
|
||||
var model = this.getModelCtor();
|
||||
var id = this.getModelId();
|
||||
model.findById(id, function(err, inst) {
|
||||
if(err) return Change.handleError(err, cb);
|
||||
if(inst) {
|
||||
cb(null, Change.revisionForInst(inst));
|
||||
} else {
|
||||
cb(null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hash of the given `string` with the `options.hashAlgorithm`.
|
||||
* **Default: `sha1`**
|
||||
*
|
||||
* @param {String} str The string to be hashed
|
||||
* @return {String} The hashed string
|
||||
*/
|
||||
|
||||
Change.hash = function(str) {
|
||||
return crypto
|
||||
.createHash(Change.settings.hashAlgorithm || 'sha1')
|
||||
.update(str)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the revision string for the given object
|
||||
* @param {Object} inst The data to get the revision string for
|
||||
* @return {String} The revision string
|
||||
*/
|
||||
|
||||
Change.revisionForInst = function(inst) {
|
||||
return this.hash(CJSON.stringify(inst));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a change's type. Returns one of:
|
||||
*
|
||||
* - `Change.UPDATE`
|
||||
* - `Change.CREATE`
|
||||
* - `Change.DELETE`
|
||||
* - `Change.UNKNOWN`
|
||||
*
|
||||
* @return {String} the type of change
|
||||
*/
|
||||
|
||||
Change.prototype.type = function() {
|
||||
if(this.rev && this.prev) {
|
||||
return Change.UPDATE;
|
||||
}
|
||||
if(this.rev && !this.prev) {
|
||||
return Change.CREATE;
|
||||
}
|
||||
if(!this.rev && this.prev) {
|
||||
return Change.DELETE;
|
||||
}
|
||||
return Change.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two changes.
|
||||
* @param {Change} change
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
Change.prototype.equals = function(change) {
|
||||
if(!change) return false;
|
||||
var thisRev = this.rev || null;
|
||||
var thatRev = change.rev || null;
|
||||
return thisRev === thatRev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this change conflict with the given change.
|
||||
* @param {Change} change
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
Change.prototype.conflictsWith = function(change) {
|
||||
if(!change) return false;
|
||||
if(this.equals(change)) return false;
|
||||
if(Change.bothDeleted(this, change)) return false;
|
||||
if(this.isBasedOn(change)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are both changes deletes?
|
||||
* @param {Change} a
|
||||
* @param {Change} b
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
Change.bothDeleted = function(a, b) {
|
||||
return a.type() === Change.DELETE
|
||||
&& b.type() === Change.DELETE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the change is based on the given change.
|
||||
* @param {Change} change
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
Change.prototype.isBasedOn = function(change) {
|
||||
return this.prev === change.rev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the differences for a given model since a given checkpoint.
|
||||
*
|
||||
* The callback will contain an error or `result`.
|
||||
*
|
||||
* **result**
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* deltas: Array,
|
||||
* conflicts: Array
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **deltas**
|
||||
*
|
||||
* An array of changes that differ from `remoteChanges`.
|
||||
*
|
||||
* **conflicts**
|
||||
*
|
||||
* An array of changes that conflict with `remoteChanges`.
|
||||
*
|
||||
* @param {String} modelName
|
||||
* @param {Number} since Compare changes after this checkpoint
|
||||
* @param {Change[]} remoteChanges A set of changes to compare
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Object} result See above.
|
||||
*/
|
||||
|
||||
Change.diff = function(modelName, since, remoteChanges, callback) {
|
||||
var remoteChangeIndex = {};
|
||||
var modelIds = [];
|
||||
remoteChanges.forEach(function(ch) {
|
||||
modelIds.push(ch.modelId);
|
||||
remoteChangeIndex[ch.modelId] = new Change(ch);
|
||||
});
|
||||
|
||||
// normalize `since`
|
||||
since = Number(since) || 0;
|
||||
this.find({
|
||||
where: {
|
||||
modelName: modelName,
|
||||
modelId: {inq: modelIds},
|
||||
checkpoint: {gte: since}
|
||||
}
|
||||
}, function(err, localChanges) {
|
||||
if(err) return callback(err);
|
||||
var deltas = [];
|
||||
var conflicts = [];
|
||||
var localModelIds = [];
|
||||
|
||||
localChanges.forEach(function(localChange) {
|
||||
localChange = new Change(localChange);
|
||||
localModelIds.push(localChange.modelId);
|
||||
var remoteChange = remoteChangeIndex[localChange.modelId];
|
||||
if(remoteChange && !localChange.equals(remoteChange)) {
|
||||
if(remoteChange.conflictsWith(localChange)) {
|
||||
remoteChange.debug('remote conflict');
|
||||
localChange.debug('local conflict');
|
||||
conflicts.push(localChange);
|
||||
} else {
|
||||
remoteChange.debug('remote delta');
|
||||
deltas.push(remoteChange);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
modelIds.forEach(function(id) {
|
||||
if(localModelIds.indexOf(id) === -1) {
|
||||
deltas.push(remoteChangeIndex[id]);
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, {
|
||||
deltas: deltas,
|
||||
conflicts: conflicts
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct all change list entries.
|
||||
* @param {Function} callback
|
||||
*/
|
||||
|
||||
Change.rectifyAll = function(cb) {
|
||||
debug('rectify all');
|
||||
var Change = this;
|
||||
// this should be optimized
|
||||
this.find(function(err, changes) {
|
||||
if(err) return cb(err);
|
||||
changes.forEach(function(change) {
|
||||
change = new Change(change);
|
||||
change.rectify();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checkpoint model.
|
||||
* @return {Checkpoint}
|
||||
*/
|
||||
|
||||
Change.getCheckpointModel = function() {
|
||||
var checkpointModel = this.Checkpoint;
|
||||
if(checkpointModel) return checkpointModel;
|
||||
this.checkpoint = checkpointModel = require('./checkpoint').extend('checkpoint');
|
||||
assert(this.dataSource, 'Cannot getCheckpointModel(): ' + this.modelName
|
||||
+ ' is not attached to a dataSource');
|
||||
checkpointModel.attachTo(this.dataSource);
|
||||
return checkpointModel;
|
||||
}
|
||||
|
||||
Change.handleError = function(err) {
|
||||
if(!this.settings.ignoreErrors) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
Change.prototype.debug = function() {
|
||||
if(debug.enabled) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
debug.apply(this, args);
|
||||
debug('\tid', this.id);
|
||||
debug('\trev', this.rev);
|
||||
debug('\tprev', this.prev);
|
||||
debug('\tmodelName', this.modelName);
|
||||
debug('\tmodelId', this.modelId);
|
||||
debug('\ttype', this.type());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `Model` class for `change.modelName`.
|
||||
* @return {Model}
|
||||
*/
|
||||
|
||||
Change.prototype.getModelCtor = function() {
|
||||
return this.constructor.settings.trackModel;
|
||||
}
|
||||
|
||||
Change.prototype.getModelId = function() {
|
||||
// TODO(ritch) get rid of the need to create an instance
|
||||
var Model = this.getModelCtor();
|
||||
var id = this.modelId;
|
||||
var m = new Model();
|
||||
m.setId(id);
|
||||
return m.getId();
|
||||
}
|
||||
|
||||
Change.prototype.getModel = function(callback) {
|
||||
var Model = this.constructor.settings.trackModel;
|
||||
var id = this.getModelId();
|
||||
Model.findById(id, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* When two changes conflict a conflict is created.
|
||||
*
|
||||
* **Note: call `conflict.fetch()` to get the `target` and `source` models.
|
||||
*
|
||||
* @param {*} modelId
|
||||
* @param {PersistedModel} SourceModel
|
||||
* @param {PersistedModel} TargetModel
|
||||
* @property {ModelClass} source The source model instance
|
||||
* @property {ModelClass} target The target model instance
|
||||
*/
|
||||
|
||||
function Conflict(modelId, SourceModel, TargetModel) {
|
||||
this.SourceModel = SourceModel;
|
||||
this.TargetModel = TargetModel;
|
||||
this.SourceChange = SourceModel.getChangeModel();
|
||||
this.TargetChange = TargetModel.getChangeModel();
|
||||
this.modelId = modelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the conflicting models.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {PersistedModel} source
|
||||
* @param {PersistedModel} target
|
||||
*/
|
||||
|
||||
Conflict.prototype.models = function(cb) {
|
||||
var conflict = this;
|
||||
var SourceModel = this.SourceModel;
|
||||
var TargetModel = this.TargetModel;
|
||||
var source;
|
||||
var target;
|
||||
|
||||
async.parallel([
|
||||
getSourceModel,
|
||||
getTargetModel
|
||||
], done);
|
||||
|
||||
function getSourceModel(cb) {
|
||||
SourceModel.findById(conflict.modelId, function(err, model) {
|
||||
if(err) return cb(err);
|
||||
source = model;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function getTargetModel(cb) {
|
||||
TargetModel.findById(conflict.modelId, function(err, model) {
|
||||
if(err) return cb(err);
|
||||
target = model;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function done(err) {
|
||||
if(err) return cb(err);
|
||||
cb(null, source, target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the conflicting changes.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Change} sourceChange
|
||||
* @param {Change} targetChange
|
||||
*/
|
||||
|
||||
Conflict.prototype.changes = function(cb) {
|
||||
var conflict = this;
|
||||
var sourceChange;
|
||||
var targetChange;
|
||||
|
||||
async.parallel([
|
||||
getSourceChange,
|
||||
getTargetChange
|
||||
], done);
|
||||
|
||||
function getSourceChange(cb) {
|
||||
conflict.SourceChange.findOne({where: {
|
||||
modelId: conflict.modelId
|
||||
}}, function(err, change) {
|
||||
if(err) return cb(err);
|
||||
sourceChange = change;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function getTargetChange(cb) {
|
||||
conflict.TargetChange.findOne({where: {
|
||||
modelId: conflict.modelId
|
||||
}}, function(err, change) {
|
||||
if(err) return cb(err);
|
||||
targetChange = change;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function done(err) {
|
||||
if(err) return cb(err);
|
||||
cb(null, sourceChange, targetChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the conflict.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
*/
|
||||
|
||||
Conflict.prototype.resolve = function(cb) {
|
||||
var conflict = this;
|
||||
conflict.changes(function(err, sourceChange, targetChange) {
|
||||
if(err) return cb(err);
|
||||
sourceChange.prev = targetChange.rev;
|
||||
sourceChange.save(cb);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the conflict type.
|
||||
*
|
||||
* Possible results are
|
||||
*
|
||||
* - `Change.UPDATE`: Source and target models were updated.
|
||||
* - `Change.DELETE`: Source and or target model was deleted.
|
||||
* - `Change.UNKNOWN`: the conflict type is uknown or due to an error.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {String} type The conflict type.
|
||||
*/
|
||||
|
||||
Conflict.prototype.type = function(cb) {
|
||||
var conflict = this;
|
||||
this.changes(function(err, sourceChange, targetChange) {
|
||||
if(err) return cb(err);
|
||||
var sourceChangeType = sourceChange.type();
|
||||
var targetChangeType = targetChange.type();
|
||||
if(sourceChangeType === Change.UPDATE && targetChangeType === Change.UPDATE) {
|
||||
return cb(null, Change.UPDATE);
|
||||
}
|
||||
if(sourceChangeType === Change.DELETE || targetChangeType === Change.DELETE) {
|
||||
return cb(null, Change.DELETE);
|
||||
}
|
||||
return cb(null, Change.UNKNOWN);
|
||||
});
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
/**
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var PersistedModel = require('../loopback').PersistedModel
|
||||
, loopback = require('../loopback')
|
||||
, assert = require('assert');
|
||||
|
||||
/**
|
||||
* Properties
|
||||
*/
|
||||
|
||||
var properties = {
|
||||
seq: {type: Number},
|
||||
time: {type: Date, default: Date},
|
||||
sourceId: {type: String}
|
||||
};
|
||||
|
||||
/**
|
||||
* Options
|
||||
*/
|
||||
|
||||
var options = {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Checkpoint list entry.
|
||||
*
|
||||
* @property id {Number} the sequencial identifier of a checkpoint
|
||||
* @property time {Number} the time when the checkpoint was created
|
||||
* @property sourceId {String} the source identifier
|
||||
*
|
||||
* @class
|
||||
* @inherits {PersistedModel}
|
||||
*/
|
||||
|
||||
var Checkpoint = module.exports = PersistedModel.extend('Checkpoint', properties, options);
|
||||
|
||||
/**
|
||||
* Get the current checkpoint id
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Number} checkpointId The current checkpoint id
|
||||
*/
|
||||
|
||||
Checkpoint.current = function(cb) {
|
||||
var Checkpoint = this;
|
||||
this.find({
|
||||
limit: 1,
|
||||
order: 'seq DESC'
|
||||
}, function(err, checkpoints) {
|
||||
if(err) return cb(err);
|
||||
var checkpoint = checkpoints[0];
|
||||
if(checkpoint) {
|
||||
cb(null, checkpoint.seq);
|
||||
} else {
|
||||
Checkpoint.create({seq: 0}, function(err, checkpoint) {
|
||||
if(err) return cb(err);
|
||||
cb(null, checkpoint.seq);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Checkpoint.beforeSave = function(next, model) {
|
||||
if(!model.getId() && model.seq === undefined) {
|
||||
model.constructor.current(function(err, seq) {
|
||||
if(err) return next(err);
|
||||
model.seq = seq + 1;
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/*!
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var Model = require('../loopback').Model
|
||||
, loopback = require('../loopback');
|
||||
|
||||
var properties = {
|
||||
to: {type: String, required: true},
|
||||
from: {type: String, required: true},
|
||||
subject: {type: String, required: true},
|
||||
text: {type: String},
|
||||
html: {type: String}
|
||||
};
|
||||
|
||||
/**
|
||||
* @property {String} to Email addressee. Required.
|
||||
* @property {String} from Email sender address. Required.
|
||||
* @property {String} subject Email subject string. Required.
|
||||
* @property {String} text Text body of email.
|
||||
* @property {String} html HTML body of email.
|
||||
*
|
||||
* @class
|
||||
* @inherits {Model}
|
||||
*/
|
||||
|
||||
var Email = module.exports = Model.extend('Email', properties);
|
||||
|
||||
/**
|
||||
* Send an email with the given `options`.
|
||||
*
|
||||
* Example Options:
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* 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.
|
||||
*
|
||||
* @options {Object} options See below
|
||||
* @prop {String} from Senders's email address
|
||||
* @prop {String} to List of one or more recipient email addresses (comma-delimited)
|
||||
* @prop {String} subject Subject line
|
||||
* @prop {String} text Body text
|
||||
* @prop {String} html Body HTML (optional)
|
||||
* @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');
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
exports.Model = require('./model');
|
||||
exports.Email = require('./email');
|
||||
exports.User = require('./user');
|
||||
exports.AccessToken = require('./access-token');
|
||||
|
||||
exports.Application = require('./application');
|
||||
exports.ACL = require('./acl');
|
||||
exports.Role = require('./role');
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,488 +0,0 @@
|
|||
var loopback = require('../loopback');
|
||||
var debug = require('debug')('loopback:security:role');
|
||||
var assert = require('assert');
|
||||
var async = require('async');
|
||||
|
||||
var AccessContext = require('./access-context').AccessContext;
|
||||
|
||||
// Role model
|
||||
var RoleSchema = {
|
||||
id: {type: String, id: true, generated: true}, // Id
|
||||
name: {type: String, required: true}, // The name of a role
|
||||
description: String, // Description
|
||||
|
||||
// Timestamps
|
||||
created: {type: Date, default: Date},
|
||||
modified: {type: Date, default: Date}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Map principals to roles
|
||||
*/
|
||||
var RoleMappingSchema = {
|
||||
id: {type: String, id: true, generated: true}, // Id
|
||||
// roleId: String, // The role id, to be injected by the belongsTo relation
|
||||
principalType: String, // The principal type, such as user, application, or role
|
||||
principalId: String // The principal id
|
||||
};
|
||||
|
||||
/**
|
||||
* The `RoleMapping` model extends from the built in `loopback.Model` type.
|
||||
*
|
||||
* @class
|
||||
* @property {String} id Generated ID.
|
||||
* @property {String} name Name of the role.
|
||||
* @property {String} Description Text description.
|
||||
* @inherits {Model}
|
||||
*/
|
||||
|
||||
var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, {
|
||||
relations: {
|
||||
role: {
|
||||
type: 'belongsTo',
|
||||
model: 'Role',
|
||||
foreignKey: 'roleId'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Principal types
|
||||
RoleMapping.USER = 'USER';
|
||||
RoleMapping.APP = RoleMapping.APPLICATION = 'APP';
|
||||
RoleMapping.ROLE = 'ROLE';
|
||||
|
||||
/**
|
||||
* Get the application principal
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Application} application
|
||||
*/
|
||||
RoleMapping.prototype.application = function (callback) {
|
||||
if (this.principalType === RoleMapping.APPLICATION) {
|
||||
var applicationModel = this.constructor.Application
|
||||
|| loopback.getModelByType(loopback.Application);
|
||||
applicationModel.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the user principal
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {User} user
|
||||
*/
|
||||
RoleMapping.prototype.user = function (callback) {
|
||||
if (this.principalType === RoleMapping.USER) {
|
||||
var userModel = this.constructor.User
|
||||
|| loopback.getModelByType(loopback.User);
|
||||
userModel.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the child role principal
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {User} childUser
|
||||
*/
|
||||
RoleMapping.prototype.childRole = function (callback) {
|
||||
if (this.principalType === RoleMapping.ROLE) {
|
||||
var roleModel = this.constructor.Role || loopback.getModelByType(Role);
|
||||
roleModel.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The Role Model
|
||||
* @class
|
||||
*/
|
||||
var Role = loopback.createModel('Role', RoleSchema, {
|
||||
relations: {
|
||||
principals: {
|
||||
type: 'hasMany',
|
||||
model: 'RoleMapping',
|
||||
foreignKey: 'roleId'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set up the connection to users/applications/roles once the model
|
||||
Role.once('dataSourceAttached', function () {
|
||||
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
||||
Role.prototype.users = function (callback) {
|
||||
roleMappingModel.find({where: {roleId: this.id,
|
||||
principalType: RoleMapping.USER}}, function (err, mappings) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
return mappings.map(function (m) {
|
||||
return m.principalId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Role.prototype.applications = function (callback) {
|
||||
roleMappingModel.find({where: {roleId: this.id,
|
||||
principalType: RoleMapping.APPLICATION}}, function (err, mappings) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
return mappings.map(function (m) {
|
||||
return m.principalId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Role.prototype.roles = function (callback) {
|
||||
roleMappingModel.find({where: {roleId: this.id,
|
||||
principalType: RoleMapping.ROLE}}, function (err, mappings) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
return mappings.map(function (m) {
|
||||
return m.principalId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// Special roles
|
||||
Role.OWNER = '$owner'; // owner of the object
|
||||
Role.RELATED = "$related"; // any User with a relationship to the object
|
||||
Role.AUTHENTICATED = "$authenticated"; // authenticated user
|
||||
Role.UNAUTHENTICATED = "$unauthenticated"; // authenticated user
|
||||
Role.EVERYONE = "$everyone"; // everyone
|
||||
|
||||
/**
|
||||
* Add custom handler for roles
|
||||
* @param role
|
||||
* @param resolver The resolver function decides if a principal is in the role
|
||||
* dynamically
|
||||
*
|
||||
* function(role, context, callback)
|
||||
*/
|
||||
Role.registerResolver = function(role, resolver) {
|
||||
if(!Role.resolvers) {
|
||||
Role.resolvers = {};
|
||||
}
|
||||
Role.resolvers[role] = resolver;
|
||||
};
|
||||
|
||||
Role.registerResolver(Role.OWNER, function(role, context, callback) {
|
||||
if(!context || !context.model || !context.modelId) {
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var modelClass = context.model;
|
||||
var modelId = context.modelId;
|
||||
var userId = context.getUserId();
|
||||
Role.isOwner(modelClass, modelId, userId, callback);
|
||||
});
|
||||
|
||||
function isUserClass(modelClass) {
|
||||
return modelClass === loopback.User ||
|
||||
modelClass.prototype instanceof loopback.User;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Check if two user ids matches
|
||||
* @param {*} id1
|
||||
* @param {*} id2
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function matches(id1, id2) {
|
||||
if (id1 === undefined || id1 === null || id1 ===''
|
||||
|| id2 === undefined || id2 === null || id2 === '') {
|
||||
return false;
|
||||
}
|
||||
// The id can be a MongoDB ObjectID
|
||||
return id1 === id2 || id1.toString() === id2.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given userId is the owner the model instance
|
||||
* @param {Function} modelClass The model class
|
||||
* @param {*} modelId The model id
|
||||
* @param {*) userId The user id
|
||||
* @param {Function} callback
|
||||
*/
|
||||
Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||
assert(modelClass, 'Model class is required');
|
||||
debug('isOwner(): %s %s userId: %s', modelClass && modelClass.modelName, modelId, userId);
|
||||
// No userId is present
|
||||
if(!userId) {
|
||||
process.nextTick(function() {
|
||||
callback(null, false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Is the modelClass User or a subclass of User?
|
||||
if(isUserClass(modelClass)) {
|
||||
process.nextTick(function() {
|
||||
callback(null, matches(modelId, userId));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modelClass.findById(modelId, function(err, inst) {
|
||||
if(err || !inst) {
|
||||
debug('Model not found for id %j', modelId);
|
||||
callback && callback(err, false);
|
||||
return;
|
||||
}
|
||||
debug('Model found: %j', inst);
|
||||
var ownerId = inst.userId || inst.owner;
|
||||
if(ownerId) {
|
||||
callback && callback(null, matches(ownerId, userId));
|
||||
return;
|
||||
} else {
|
||||
// Try to follow belongsTo
|
||||
for(var r in modelClass.relations) {
|
||||
var rel = modelClass.relations[r];
|
||||
if(rel.type === 'belongsTo' && isUserClass(rel.modelTo)) {
|
||||
debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel);
|
||||
inst[r](function(err, user) {
|
||||
if(!err && user) {
|
||||
debug('User found: %j', user.id);
|
||||
callback && callback(null, matches(user.id, userId));
|
||||
} else {
|
||||
callback && callback(err, false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
debug('No matching belongsTo relation found for model %j and user: %j', modelId, userId);
|
||||
callback && callback(null, false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
|
||||
if(!context) {
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
Role.isAuthenticated(context, callback);
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if the user id is authenticated
|
||||
* @param {Object} context The security context
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Boolean} isAuthenticated
|
||||
*/
|
||||
Role.isAuthenticated = function isAuthenticated(context, callback) {
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, context.isAuthenticated());
|
||||
});
|
||||
};
|
||||
|
||||
Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) {
|
||||
process.nextTick(function() {
|
||||
callback && callback(null, !context || !context.isAuthenticated());
|
||||
});
|
||||
});
|
||||
|
||||
Role.registerResolver(Role.EVERYONE, function (role, context, callback) {
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, true); // Always true
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if a given principal is in the role
|
||||
*
|
||||
* @param {String} role The role name
|
||||
* @param {Object} context The context object
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Boolean} isInRole
|
||||
*/
|
||||
Role.isInRole = function (role, context, callback) {
|
||||
if (!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
|
||||
debug('isInRole(): %s', role);
|
||||
context.debug();
|
||||
|
||||
var resolver = Role.resolvers[role];
|
||||
if (resolver) {
|
||||
debug('Custom resolver found for role %s', role);
|
||||
resolver(role, context, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.principals.length === 0) {
|
||||
debug('isInRole() returns: false');
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var inRole = context.principals.some(function (p) {
|
||||
|
||||
var principalType = p.type || undefined;
|
||||
var principalId = p.id || undefined;
|
||||
|
||||
// Check if it's the same role
|
||||
return principalType === RoleMapping.ROLE && principalId === role;
|
||||
});
|
||||
|
||||
if (inRole) {
|
||||
debug('isInRole() returns: %j', inRole);
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
||||
this.findOne({where: {name: role}}, function (err, result) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
if (!result) {
|
||||
callback && callback(null, false);
|
||||
return;
|
||||
}
|
||||
debug('Role found: %j', result);
|
||||
|
||||
// Iterate through the list of principals
|
||||
async.some(context.principals, function (p, done) {
|
||||
var principalType = p.type || undefined;
|
||||
var principalId = p.id || undefined;
|
||||
var roleId = result.id.toString();
|
||||
|
||||
if(principalId !== null && principalId !== undefined && (typeof principalId !== 'string') ) {
|
||||
principalId = principalId.toString();
|
||||
}
|
||||
|
||||
if (principalType && principalId) {
|
||||
roleMappingModel.findOne({where: {roleId: roleId,
|
||||
principalType: principalType, principalId: principalId}},
|
||||
function (err, result) {
|
||||
debug('Role mapping found: %j', result);
|
||||
done(!err && result); // The only arg is the result
|
||||
});
|
||||
} else {
|
||||
process.nextTick(function () {
|
||||
done(false);
|
||||
});
|
||||
}
|
||||
}, function (inRole) {
|
||||
debug('isInRole() returns: %j', inRole);
|
||||
callback && callback(null, inRole);
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* List roles for a given principal
|
||||
* @param {Object} context The security context
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param err
|
||||
* @param {String[]} An array of role ids
|
||||
*/
|
||||
Role.getRoles = function (context, callback) {
|
||||
if(!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
var roles = [];
|
||||
|
||||
var addRole = function (role) {
|
||||
if (role && roles.indexOf(role) === -1) {
|
||||
roles.push(role);
|
||||
}
|
||||
};
|
||||
|
||||
var self = this;
|
||||
// Check against the smart roles
|
||||
var inRoleTasks = [];
|
||||
Object.keys(Role.resolvers).forEach(function (role) {
|
||||
inRoleTasks.push(function (done) {
|
||||
self.isInRole(role, context, function (err, inRole) {
|
||||
if(debug.enabled) {
|
||||
debug('In role %j: %j', role, inRole);
|
||||
}
|
||||
if (!err && inRole) {
|
||||
addRole(role);
|
||||
done();
|
||||
} else {
|
||||
done(err, null);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
||||
context.principals.forEach(function (p) {
|
||||
// Check against the role mappings
|
||||
var principalType = p.type || undefined;
|
||||
var principalId = p.id || undefined;
|
||||
|
||||
// Add the role itself
|
||||
if (principalType === RoleMapping.ROLE && principalId) {
|
||||
addRole(principalId);
|
||||
}
|
||||
|
||||
if (principalType && principalId) {
|
||||
// Please find() treat undefined matches all values
|
||||
inRoleTasks.push(function (done) {
|
||||
roleMappingModel.find({where: {principalType: principalType,
|
||||
principalId: principalId}}, function (err, mappings) {
|
||||
debug('Role mappings found: %s %j', err, mappings);
|
||||
if (err) {
|
||||
done && done(err);
|
||||
return;
|
||||
}
|
||||
mappings.forEach(function (m) {
|
||||
addRole(m.roleId);
|
||||
});
|
||||
done && done();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async.parallel(inRoleTasks, function (err, results) {
|
||||
debug('getRoles() returns: %j %j', err, roles);
|
||||
callback && callback(err, roles);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Role: Role,
|
||||
RoleMapping: RoleMapping
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
*/
|
||||
|
||||
var Model = require('./model');
|
||||
var runtime = require('../runtime');
|
||||
var registry = require('./registry');
|
||||
var runtime = require('./runtime');
|
||||
var assert = require('assert');
|
||||
var async = require('async');
|
||||
|
||||
|
@ -21,8 +22,6 @@ var async = require('async');
|
|||
* ```
|
||||
*
|
||||
* @class PersistedModel
|
||||
* @param {Object} data
|
||||
* @param {Number} data.id The default id property
|
||||
*/
|
||||
|
||||
var PersistedModel = module.exports = Model.extend('PersistedModel');
|
||||
|
@ -44,9 +43,9 @@ PersistedModel.setup = function setupPersistedModel() {
|
|||
PersistedModel.enableChangeTracking();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
PersistedModel.setupRemoting();
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Throw an error telling the user that the method is not available and why.
|
||||
|
@ -80,12 +79,9 @@ function convertNullToNotFoundError(ctx, cb) {
|
|||
/**
|
||||
* Create new instance of Model class, saved in database
|
||||
*
|
||||
* @param data [optional]
|
||||
* @param callback(err, obj)
|
||||
* callback called with arguments:
|
||||
*
|
||||
* - err (null or Error)
|
||||
* - instance (null or Model)
|
||||
* @param {Object} data Optional data object.
|
||||
* @param {Function} cb Callback function with `cb(err, obj)` signature,
|
||||
* where `err` is error object and `obj` is null or Model instance.
|
||||
*/
|
||||
|
||||
PersistedModel.create = function (data, callback) {
|
||||
|
@ -103,12 +99,12 @@ PersistedModel.upsert = PersistedModel.updateOrCreate = function upsert(data, ca
|
|||
};
|
||||
|
||||
/**
|
||||
* Find one record, same as `find`, limited by 1 and return object, not collection,
|
||||
* if not found, create using data provided as second argument
|
||||
* Find one record, same as `find`, but limited to one object. Returns an object, not collection.
|
||||
* If not found, create the object using data provided as second argument.
|
||||
*
|
||||
* @param {Object} query - search conditions: {where: {test: 'me'}}.
|
||||
* @param {Object} data - object to create.
|
||||
* @param {Function} cb - callback called with (err, instance)
|
||||
* @param {Object} query Search conditions: {where: {test: 'me'}}.
|
||||
* @param {Object} data Object to create.
|
||||
* @param {Function} cb Callback called with `cb(err, instance)` signature.
|
||||
*/
|
||||
|
||||
PersistedModel.findOrCreate = function findOrCreate(query, data, callback) {
|
||||
|
@ -118,10 +114,10 @@ PersistedModel.findOrCreate = function findOrCreate(query, data, callback) {
|
|||
PersistedModel.findOrCreate._delegate = true;
|
||||
|
||||
/**
|
||||
* Check whether a model instance exists in database
|
||||
* Check whether a model instance exists in database.
|
||||
*
|
||||
* @param {id} id - identifier of object (primary key value)
|
||||
* @param {Function} cb - callback called with (err, exists: Bool)
|
||||
* @param {id} id Identifier of object (primary key value)
|
||||
* @param {Function} cb Callback function called with (err, exists: Bool)
|
||||
*/
|
||||
|
||||
PersistedModel.exists = function exists(id, cb) {
|
||||
|
@ -129,10 +125,10 @@ PersistedModel.exists = function exists(id, cb) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Find object by id
|
||||
* Find object by ID.
|
||||
*
|
||||
* @param {*} id - primary key value
|
||||
* @param {Function} cb - callback called with (err, instance)
|
||||
* @param {Function} cb Callback function called with `(err, instances)`. Required.
|
||||
*/
|
||||
|
||||
PersistedModel.findById = function find(id, cb) {
|
||||
|
@ -140,21 +136,28 @@ PersistedModel.findById = function find(id, cb) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Find all instances of Model, matched by query
|
||||
* make sure you have marked as `index: true` fields for filter or sort
|
||||
* Find all model instances that match `filter` specification.
|
||||
* See [Querying models](http://docs.strongloop.com/display/LB/Querying+models).
|
||||
*
|
||||
* @param {Object} params (optional)
|
||||
* @options {Object} [filter] Optional Filter JSON object; see below.
|
||||
* @property {String|Object|Array} fields Identify fields to include in return result.
|
||||
* <br/>See [Fields filter](http://docs.strongloop.com/display/LB/Fields+filter).
|
||||
* @property {String|Object|Array} include See PersistedModel.include documentation.
|
||||
* <br/>See [Include filter](http://docs.strongloop.com/display/LB/Include+filter).
|
||||
* @property {Number} limit Maximum number of instances to return.
|
||||
* <br/>See [Limit filter](http://docs.strongloop.com/display/LB/Limit+filter).
|
||||
* @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending.
|
||||
* <br/>See [Order filter](http://docs.strongloop.com/display/LB/Order+filter).
|
||||
* @property {Number} skip Number of results to skip.
|
||||
* <br/>See [Skip filter](http://docs.strongloop.com/display/LB/Skip+filter).
|
||||
* @property {Object} where Where clause, like
|
||||
* ```
|
||||
* { key: val, key2: {gt: 'val2'}, ...}
|
||||
* ```
|
||||
* <br/>See [Where filter](http://docs.strongloop.com/display/LB/Where+filter).
|
||||
*
|
||||
* - where: Object `{ key: val, key2: {gt: 'val2'}}`
|
||||
* - include: String, Object or Array. See PersistedModel.include documentation.
|
||||
* - order: String
|
||||
* - limit: Number
|
||||
* - skip: Number
|
||||
*
|
||||
* @param {Function} callback (required) called with arguments:
|
||||
*
|
||||
* - err (null or Error)
|
||||
* - Array of instances
|
||||
* @param {Function} Callback function called with `(err, returned-instances)`.
|
||||
* @returns {Object} Array of model instances that match the filter; or Error.
|
||||
*/
|
||||
|
||||
PersistedModel.find = function find(params, cb) {
|
||||
|
@ -162,10 +165,29 @@ PersistedModel.find = function find(params, cb) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Find one record, same as `all`, limited by 1 and return object, not collection
|
||||
* Find one model instance that matches `filter` specification.
|
||||
* Same as `find`, but limited to one result;
|
||||
* Returns object, not collection.
|
||||
*
|
||||
* @param {Object} params - search conditions: {where: {test: 'me'}}
|
||||
* @param {Function} cb - callback called with (err, instance)
|
||||
* @options {Object} [filter] Optional Filter JSON object; see below.
|
||||
* @property {String|Object|Array} fields Identify fields to include in return result.
|
||||
* <br/>See [Fields filter](http://docs.strongloop.com/display/LB/Fields+filter).
|
||||
* @property {String|Object|Array} include See PersistedModel.include documentation.
|
||||
* <br/>See [Include filter](http://docs.strongloop.com/display/LB/Include+filter).
|
||||
* @property {Number} limit Maximum number of instances to return.
|
||||
* <br/>See [Limit filter](http://docs.strongloop.com/display/LB/Limit+filter).
|
||||
* @property {String} order Sort order: either "ASC" for ascending or "DESC" for descending.
|
||||
* <br/>See [Order filter](http://docs.strongloop.com/display/LB/Order+filter).
|
||||
* @property {Number} skip Number of results to skip.
|
||||
* <br/>See [Skip filter](http://docs.strongloop.com/display/LB/Skip+filter).
|
||||
* @property {Object} where Where clause, like
|
||||
* ```
|
||||
* { key: val, key2: {gt: 'val2'}, ...}
|
||||
* ```
|
||||
* <br/>See [Where filter](http://docs.strongloop.com/display/LB/Where+filter).
|
||||
*
|
||||
* @param {Function} Callback function called with `(err, returned-instance)`. Required.
|
||||
* @returns {Object} First model instance that matches the filter; or Error.
|
||||
*/
|
||||
|
||||
PersistedModel.findOne = function findOne(params, cb) {
|
||||
|
@ -173,17 +195,31 @@ PersistedModel.findOne = function findOne(params, cb) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Destroy all matching records
|
||||
* @param {Object} [where] An object that defines the criteria
|
||||
* @param {Function} [cb] - callback called with (err)
|
||||
* Destroy all model instances that match the optional `filter` specification.
|
||||
*
|
||||
* @options {Object} [where] Optional where filter JSON object; see below.
|
||||
* @property {Object} where Where clause, like
|
||||
* ```
|
||||
* { key: val, key2: {gt: 'val2'}, ...}
|
||||
* ```
|
||||
*
|
||||
* @param {Function} [cb] - callback called with `(err)`.
|
||||
*/
|
||||
|
||||
PersistedModel.remove =
|
||||
PersistedModel.deleteAll =
|
||||
PersistedModel.destroyAll = function destroyAll(where, cb) {
|
||||
throwNotAttached(this.modelName, 'destroyAll');
|
||||
};
|
||||
|
||||
/**
|
||||
* Alias for `destroyAll`
|
||||
*/
|
||||
PersistedModel.remove = PersistedModel.destroyAll;
|
||||
|
||||
/**
|
||||
* Alias for `destroyAll`
|
||||
*/
|
||||
PersistedModel.deleteAll = PersistedModel.destroyAll;
|
||||
|
||||
/**
|
||||
* Update multiple instances that match the where clause
|
||||
*
|
||||
|
@ -195,32 +231,50 @@ PersistedModel.destroyAll = function destroyAll(where, cb) {
|
|||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {Object} [where] Search conditions (optional)
|
||||
* @options {Object} [where] Optional where filter JSON object; see below.
|
||||
* @property {Object} where Where clause, like
|
||||
* ```
|
||||
* { key: val, key2: {gt: 'val2'}, ...}
|
||||
* ```
|
||||
* @param {Object} data Changes to be made
|
||||
* @param {Function} cb Callback, called with (err, count)
|
||||
* @param {Function} cb Callback function called with (err, count).
|
||||
*/
|
||||
PersistedModel.update =
|
||||
PersistedModel.updateAll = function updateAll(where, data, cb) {
|
||||
throwNotAttached(this.modelName, 'updateAll');
|
||||
};
|
||||
PersistedModel.updateAll = function updateAll(where, data, cb) {
|
||||
throwNotAttached(this.modelName, 'updateAll');
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy a record by id
|
||||
* @param {*} id The id value
|
||||
* @param {Function} cb - callback called with (err)
|
||||
* Alias for updateAll.
|
||||
*/
|
||||
PersistedModel.update = PersistedModel.updateAll;
|
||||
|
||||
PersistedModel.removeById =
|
||||
PersistedModel.deleteById =
|
||||
/**
|
||||
* Destroy model instance with the specified ID.
|
||||
* @param {*} id The ID value of model instance to delete.
|
||||
* @param {Function} cb Callback function called with (err).
|
||||
*/
|
||||
PersistedModel.destroyById = function deleteById(id, cb) {
|
||||
throwNotAttached(this.modelName, 'deleteById');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return count of matched records
|
||||
*
|
||||
* @param {Object} where - search conditions (optional)
|
||||
* @param {Function} cb - callback, called with (err, count)
|
||||
* Alias for destroyById.
|
||||
*/
|
||||
PersistedModel.removeById = PersistedModel.destroyById;
|
||||
|
||||
/**
|
||||
* Alias for destroyById.
|
||||
*/
|
||||
PersistedModel.deleteById = PersistedModel.destroyById;
|
||||
|
||||
/**
|
||||
* Return the number of records that match the optional filter.
|
||||
* @options {Object} [filter] Optional where filter JSON object; see below.
|
||||
* @property {Object} where Where clause, like
|
||||
* ```
|
||||
* { key: val, key2: {gt: 'val2'}, ...}
|
||||
* ```
|
||||
* @param {Function} cb Callback function called with (err, count).
|
||||
*/
|
||||
|
||||
PersistedModel.count = function (where, cb) {
|
||||
|
@ -228,10 +282,12 @@ PersistedModel.count = function (where, cb) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Save instance. When instance haven't id, create method called instead.
|
||||
* Triggers: validate, save, update | create
|
||||
* @param options {validate: true, throws: false} [optional]
|
||||
* @param callback(err, obj)
|
||||
* Save model instance. If the instance doesn't have an ID, then the [create](#persistedmodelcreatedata-cb) method is called instead.
|
||||
* Triggers: validate, save, update, or create.
|
||||
* @options {Object} [options] See below.
|
||||
* @property {Boolean} validate
|
||||
* @property {Boolean} throws
|
||||
* @param {Function} [callback] Callback function called with (err, obj).
|
||||
*/
|
||||
|
||||
PersistedModel.prototype.save = function (options, callback) {
|
||||
|
@ -270,7 +326,7 @@ PersistedModel.prototype.save = function (options, callback) {
|
|||
if (valid) {
|
||||
save();
|
||||
} else {
|
||||
var err = new ValidationError(inst);
|
||||
var err = new Model.ValidationError(inst);
|
||||
// throws option is dangerous for async usage
|
||||
if (options.throws) {
|
||||
throw err;
|
||||
|
@ -282,7 +338,7 @@ PersistedModel.prototype.save = function (options, callback) {
|
|||
// then save
|
||||
function save() {
|
||||
inst.trigger('save', function (saveDone) {
|
||||
inst.trigger('update', function (updateDone) {
|
||||
inst.trigger('update', function (updateDone) {
|
||||
Model.upsert(inst, function(err) {
|
||||
inst._initProperties(data);
|
||||
updateDone.call(inst, function () {
|
||||
|
@ -298,7 +354,7 @@ PersistedModel.prototype.save = function (options, callback) {
|
|||
|
||||
/**
|
||||
* Determine if the data model is new.
|
||||
* @returns {Boolean}
|
||||
* @returns {Boolean} Returns true if the data model is new; false otherwise.
|
||||
*/
|
||||
|
||||
PersistedModel.prototype.isNewRecord = function () {
|
||||
|
@ -306,27 +362,36 @@ PersistedModel.prototype.isNewRecord = function () {
|
|||
};
|
||||
|
||||
/**
|
||||
* Delete object from persistence
|
||||
*
|
||||
* @triggers `destroy` hook (async) before and after destroying object
|
||||
* Deletes the model from persistence.
|
||||
* Triggers `destroy` hook (async) before and after destroying object.
|
||||
* @param {Function} callback Callback function.
|
||||
*/
|
||||
|
||||
PersistedModel.prototype.remove =
|
||||
PersistedModel.prototype.delete =
|
||||
PersistedModel.prototype.destroy = function (cb) {
|
||||
throwNotAttached(this.constructor.modelName, 'destroy');
|
||||
};
|
||||
|
||||
/**
|
||||
* Alias for destroy.
|
||||
* @header PersistedModel.remove
|
||||
*/
|
||||
PersistedModel.prototype.remove = PersistedModel.prototype.destroy;
|
||||
|
||||
/**
|
||||
* Alias for destroy.
|
||||
* @header PersistedModel.delete
|
||||
*/
|
||||
PersistedModel.prototype.delete = PersistedModel.prototype.destroy;
|
||||
|
||||
PersistedModel.prototype.destroy._delegate = true;
|
||||
|
||||
/**
|
||||
* Update single attribute
|
||||
* Update a single attribute.
|
||||
* Equivalent to `updateAttributes({name: 'value'}, cb)`
|
||||
*
|
||||
* equals to `updateAttributes({name: value}, cb)
|
||||
*
|
||||
* @param {String} name - name of property
|
||||
* @param {Mixed} value - value of property
|
||||
* @param {Function} callback - callback called with (err, instance)
|
||||
* @param {String} name Name of property
|
||||
* @param {Mixed} value Value of property
|
||||
* @param {Function} callback Callback function called with (err, instance).
|
||||
*/
|
||||
|
||||
PersistedModel.prototype.updateAttribute = function updateAttribute(name, value, callback) {
|
||||
|
@ -334,13 +399,11 @@ PersistedModel.prototype.updateAttribute = function updateAttribute(name, value,
|
|||
};
|
||||
|
||||
/**
|
||||
* Update set of attributes
|
||||
* Update set of attributes. Performs validation before updating.
|
||||
*
|
||||
* this method performs validation before updating
|
||||
*
|
||||
* @trigger `validation`, `save` and `update` hooks
|
||||
* @param {Object} data - data to update
|
||||
* @param {Function} callback - callback called with (err, instance)
|
||||
* Trigger: `validation`, `save` and `update` hooks
|
||||
* @param {Object} data Dta to update.
|
||||
* @param {Function} callback Callback function called with (err, instance).
|
||||
*/
|
||||
|
||||
PersistedModel.prototype.updateAttributes = function updateAttributes(data, cb) {
|
||||
|
@ -348,10 +411,8 @@ PersistedModel.prototype.updateAttributes = function updateAttributes(data, cb)
|
|||
};
|
||||
|
||||
/**
|
||||
* Reload object from persistence
|
||||
*
|
||||
* @requires `id` member of `object` to be able to call `find`
|
||||
* @param {Function} callback - called with (err, instance) arguments
|
||||
* Reload object from persistence. Requires `id` member of `object` to be able to call `find`.
|
||||
* @param {Function} callback Callback function called with (err, instance) arguments.
|
||||
*/
|
||||
|
||||
PersistedModel.prototype.reload = function reload(callback) {
|
||||
|
@ -360,17 +421,16 @@ PersistedModel.prototype.reload = function reload(callback) {
|
|||
|
||||
/**
|
||||
* Set the correct `id` property for the `PersistedModel`. If a `Connector` defines
|
||||
* a `setId` method it will be used. Otherwise the default lookup is used. You
|
||||
* should override this method to handle complex ids.
|
||||
* a `setId` method it will be used. Otherwise the default lookup is used.
|
||||
* Override this method to handle complex IDs.
|
||||
*
|
||||
* @param {*} val The `id` value. Will be converted to the type the id property
|
||||
* specifies.
|
||||
* @param {*} val The `id` value. Will be converted to the type that the `id` property specifies.
|
||||
*/
|
||||
|
||||
PersistedModel.prototype.setId = function(val) {
|
||||
var ds = this.getDataSource();
|
||||
this[this.getIdName()] = val;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the `id` value for the `PersistedModel`.
|
||||
|
@ -382,7 +442,7 @@ PersistedModel.prototype.getId = function() {
|
|||
var data = this.toObject();
|
||||
if(!data) return;
|
||||
return data[this.getIdName()];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the id property name of the constructor.
|
||||
|
@ -392,7 +452,7 @@ PersistedModel.prototype.getId = function() {
|
|||
|
||||
PersistedModel.prototype.getIdName = function() {
|
||||
return this.constructor.getIdName();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the id property name of the constructor.
|
||||
|
@ -409,7 +469,7 @@ PersistedModel.getIdName = function() {
|
|||
} else {
|
||||
return 'id';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PersistedModel.setupRemoting = function() {
|
||||
var PersistedModel = this;
|
||||
|
@ -537,7 +597,7 @@ PersistedModel.setupRemoting = function() {
|
|||
{arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'},
|
||||
{arg: 'remoteChanges', type: 'array', description: 'an array of change objects',
|
||||
http: {source: 'body'}}
|
||||
],
|
||||
],
|
||||
returns: {arg: 'result', type: 'object', root: true},
|
||||
http: {verb: 'post', path: '/diff'}
|
||||
});
|
||||
|
@ -552,7 +612,7 @@ PersistedModel.setupRemoting = function() {
|
|||
returns: {arg: 'changes', type: 'array', root: true},
|
||||
http: {verb: 'get', path: '/changes'}
|
||||
});
|
||||
|
||||
|
||||
setRemoting(PersistedModel, 'checkpoint', {
|
||||
description: 'Create a checkpoint.',
|
||||
returns: {arg: 'checkpoint', type: 'object', root: true},
|
||||
|
@ -589,22 +649,22 @@ PersistedModel.setupRemoting = function() {
|
|||
http: {verb: 'post', path: '/:id/rectify-change'}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a set of deltas and conflicts since the given checkpoint.
|
||||
*
|
||||
* See `Change.diff()` for details.
|
||||
*
|
||||
*
|
||||
* @param {Number} since Find deltas since this checkpoint
|
||||
* @param {Array} remoteChanges An array of change objects
|
||||
* @param {Function} callback
|
||||
* @param {Function} callback
|
||||
*/
|
||||
|
||||
PersistedModel.diff = function(since, remoteChanges, callback) {
|
||||
var Change = this.getChangeModel();
|
||||
Change.diff(this.modelName, since, remoteChanges, callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the changes to a model since a given checkpoint. Provide a filter object
|
||||
|
@ -644,13 +704,13 @@ PersistedModel.changes = function(since, filter, callback) {
|
|||
checkpoint: {gt: since},
|
||||
modelName: this.modelName
|
||||
}, function(err, changes) {
|
||||
if(err) return cb(err);
|
||||
if(err) return callback(err);
|
||||
var ids = changes.map(function(change) {
|
||||
return change.getModelId();
|
||||
});
|
||||
filter.where[idName] = {inq: ids};
|
||||
model.find(filter, function(err, models) {
|
||||
if(err) return cb(err);
|
||||
if(err) return callback(err);
|
||||
var modelIds = models.map(function(m) {
|
||||
return m[idName].toString();
|
||||
});
|
||||
|
@ -660,11 +720,11 @@ PersistedModel.changes = function(since, filter, callback) {
|
|||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a checkpoint.
|
||||
*
|
||||
*
|
||||
* @param {Function} callback
|
||||
*/
|
||||
|
||||
|
@ -676,11 +736,11 @@ PersistedModel.checkpoint = function(cb) {
|
|||
sourceId: sourceId
|
||||
}, cb);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current checkpoint id.
|
||||
*
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Number} currentCheckpointId
|
||||
|
@ -690,7 +750,7 @@ PersistedModel.checkpoint = function(cb) {
|
|||
PersistedModel.currentCheckpoint = function(cb) {
|
||||
var Checkpoint = this.getChangeModel().getCheckpointModel();
|
||||
Checkpoint.current(cb);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Replicate changes since the given checkpoint to the given target model.
|
||||
|
@ -733,7 +793,7 @@ PersistedModel.replicate = function(since, targetModel, options, callback) {
|
|||
|
||||
callback = callback || function defaultReplicationCallback(err) {
|
||||
if(err) throw err;
|
||||
}
|
||||
};
|
||||
|
||||
var tasks = [
|
||||
getSourceChanges,
|
||||
|
@ -788,13 +848,13 @@ PersistedModel.replicate = function(since, targetModel, options, callback) {
|
|||
|
||||
callback && callback(null, conflicts);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an update list (for `Model.bulkUpdate()`) from a delta list
|
||||
* (result of `Change.diff()`).
|
||||
*
|
||||
* @param {Array} deltas
|
||||
*
|
||||
* @param {Array} deltas
|
||||
* @param {Function} callback
|
||||
*/
|
||||
|
||||
|
@ -839,13 +899,13 @@ PersistedModel.createUpdates = function(deltas, cb) {
|
|||
if(err) return cb(err);
|
||||
cb(null, updates);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply an update list.
|
||||
*
|
||||
* **Note: this is not atomic**
|
||||
*
|
||||
*
|
||||
* @param {Array} updates An updates list (usually from Model.createUpdates())
|
||||
* @param {Function} callback
|
||||
*/
|
||||
|
@ -877,11 +937,11 @@ PersistedModel.bulkUpdate = function(updates, callback) {
|
|||
});
|
||||
|
||||
async.parallel(tasks, callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the `Change` model.
|
||||
*
|
||||
*
|
||||
* @throws {Error} Throws an error if the change model is not correctly setup.
|
||||
* @return {Change}
|
||||
*/
|
||||
|
@ -893,11 +953,11 @@ PersistedModel.getChangeModel = function() {
|
|||
assert(isSetup, 'Cannot get a setup Change model');
|
||||
|
||||
return changeModel;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the source identifier for this model / dataSource.
|
||||
*
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {String} sourceId
|
||||
|
@ -909,12 +969,12 @@ PersistedModel.getSourceId = function(cb) {
|
|||
this.once('dataSourceAttached', this.getSourceId.bind(this, cb));
|
||||
}
|
||||
assert(
|
||||
dataSource.connector.name,
|
||||
dataSource.connector.name,
|
||||
'Model.getSourceId: cannot get id without dataSource.connector.name'
|
||||
);
|
||||
var id = [dataSource.connector.name, this.modelName].join('-');
|
||||
cb(null, id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable the tracking of changes made to the model. Usually for replication.
|
||||
|
@ -933,11 +993,11 @@ PersistedModel.enableChangeTracking = function() {
|
|||
|
||||
Model.afterSave = function afterSave(next) {
|
||||
Model.rectifyChange(this.getId(), next);
|
||||
}
|
||||
};
|
||||
|
||||
Model.afterDestroy = function afterDestroy(next) {
|
||||
Model.rectifyChange(this.getId(), next);
|
||||
}
|
||||
};
|
||||
|
||||
Model.on('deletedAll', cleanup);
|
||||
|
||||
|
@ -957,21 +1017,24 @@ PersistedModel.enableChangeTracking = function() {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PersistedModel._defineChangeModel = function() {
|
||||
var BaseChangeModel = require('./change');
|
||||
var BaseChangeModel = registry.getModel('Change');
|
||||
assert(BaseChangeModel,
|
||||
'Change model must be defined before enabling change replication');
|
||||
|
||||
return this.Change = BaseChangeModel.extend(this.modelName + '-change',
|
||||
{},
|
||||
{
|
||||
trackModel: this
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PersistedModel.rectifyAllChanges = function(callback) {
|
||||
this.getChangeModel().rectifyAll(callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a change error. Override this method in a subclassing model to customize
|
||||
|
@ -985,7 +1048,7 @@ PersistedModel.handleChangeError = function(err) {
|
|||
console.error(Model.modelName + ' Change Tracking Error:');
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tell loopback that a change to the model with the given id has occurred.
|
||||
|
@ -998,6 +1061,6 @@ PersistedModel.handleChangeError = function(err) {
|
|||
PersistedModel.rectifyChange = function(id, callback) {
|
||||
var Change = this.getChangeModel();
|
||||
Change.rectifyModelChanges(this.modelName, [id], callback);
|
||||
}
|
||||
};
|
||||
|
||||
PersistedModel.setup();
|
|
@ -283,7 +283,7 @@ registry.createDataSource = function (name, options) {
|
|||
* @param {String} [name] The name of the data source.
|
||||
* If not provided, the `'default'` is used.
|
||||
*
|
||||
* @header loopback.memory()
|
||||
* @header loopback.memory([name])
|
||||
*/
|
||||
|
||||
registry.memory = function (name) {
|
||||
|
@ -303,9 +303,9 @@ registry.memory = function (name) {
|
|||
|
||||
/**
|
||||
* Set the default `dataSource` for a given `type`.
|
||||
* @param {String} type The datasource type
|
||||
* @param {String} type The datasource type.
|
||||
* @param {Object|DataSource} dataSource The data source settings or instance
|
||||
* @returns {DataSource} The data source instance
|
||||
* @returns {DataSource} The data source instance.
|
||||
*
|
||||
* @header loopback.setDefaultDataSourceForType(type, dataSource)
|
||||
*/
|
||||
|
@ -323,9 +323,9 @@ registry.setDefaultDataSourceForType = function(type, dataSource) {
|
|||
|
||||
/**
|
||||
* Get the default `dataSource` for a given `type`.
|
||||
* @param {String} type The datasource type
|
||||
* @returns {DataSource} The data source instance
|
||||
* @header loopback.getDefaultDataSourceForType()
|
||||
* @param {String} type The datasource type.
|
||||
* @returns {DataSource} The data source instance.
|
||||
* @header loopback.getDefaultDataSourceForType(type)
|
||||
*/
|
||||
|
||||
registry.getDefaultDataSourceForType = function(type) {
|
||||
|
@ -371,8 +371,8 @@ registry.DataSource = DataSource;
|
|||
* @private
|
||||
*/
|
||||
|
||||
registry.Model = require('./models/model');
|
||||
registry.PersistedModel = require('./models/persisted-model');
|
||||
registry.Model = require('./model');
|
||||
registry.PersistedModel = require('./persisted-model');
|
||||
|
||||
// temporary alias to simplify migration of code based on <=2.0.0-beta3
|
||||
Object.defineProperty(registry, 'DataModel', {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "loopback",
|
||||
"version": "2.6.0",
|
||||
"description": "LoopBack: Open Source Framework for Node.js",
|
||||
"homepage": "http://loopback.io",
|
||||
"keywords": [
|
||||
|
@ -27,7 +28,6 @@
|
|||
"mobile",
|
||||
"mBaaS"
|
||||
],
|
||||
"version": "2.4.1",
|
||||
"scripts": {
|
||||
"test": "grunt mocha-and-karma"
|
||||
},
|
||||
|
|
|
@ -109,8 +109,6 @@ describe('AccessToken', function () {
|
|||
});
|
||||
|
||||
describe('app.enableAuth()', function() {
|
||||
this.timeout(0);
|
||||
|
||||
beforeEach(createTestingToken);
|
||||
|
||||
it('prevents remote call with 401 status on denied ACL', function (done) {
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
var assert = require('assert');
|
||||
var loopback = require('../index');
|
||||
var acl = require('../lib/models/acl');
|
||||
var Scope = acl.Scope;
|
||||
var ACL = acl.ACL;
|
||||
var role = require('../lib/models/role');
|
||||
var Role = role.Role;
|
||||
var RoleMapping = role.RoleMapping;
|
||||
var Scope = loopback.Scope;
|
||||
var ACL = loopback.ACL;
|
||||
var Role = loopback.Role;
|
||||
var RoleMapping = loopback.RoleMapping;
|
||||
var User = loopback.User;
|
||||
var testModel;
|
||||
|
||||
|
@ -229,6 +227,33 @@ describe('security ACLs', function () {
|
|||
|
||||
});
|
||||
|
||||
it("should filter static ACLs by model/property", function() {
|
||||
var Model1 = ds.createModel('Model1', {
|
||||
name: {
|
||||
type: String,
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.WRITE, permission: ACL.DENY},
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW}
|
||||
]
|
||||
}
|
||||
}, {
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: 'u001', property: 'name',
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
{principalType: ACL.USER, principalId: 'u002', property: 'findOne',
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW}
|
||||
]
|
||||
});
|
||||
|
||||
var staticACLs = ACL.getStaticACLs('Model1', 'name');
|
||||
assert(staticACLs.length === 3);
|
||||
|
||||
staticACLs = ACL.getStaticACLs('Model1', 'findOne');
|
||||
assert(staticACLs.length === 1);
|
||||
});
|
||||
|
||||
it("should check access against LDL, ACL, and Role", function () {
|
||||
// var log = console.log;
|
||||
var log = function() {};
|
||||
|
|
|
@ -213,8 +213,9 @@ describe('app', function() {
|
|||
app.set('host', undefined);
|
||||
|
||||
app.listen(function() {
|
||||
expect(app.get('url'), 'url')
|
||||
.to.equal('http://127.0.0.1:' + app.get('port') + '/');
|
||||
var host = process.platform === 'win32' ? 'localhost' : app.get('host');
|
||||
var expectedUrl = 'http://' + host + ':' + app.get('port') + '/';
|
||||
expect(app.get('url'), 'url').to.equal(expectedUrl);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -350,4 +351,26 @@ describe('app', function() {
|
|||
var app = loopback();
|
||||
expect(app.loopback).to.equal(loopback);
|
||||
});
|
||||
|
||||
describe('normalizeHttpPath option', function() {
|
||||
var app, db;
|
||||
beforeEach(function() {
|
||||
app = loopback();
|
||||
db = loopback.createDataSource({ connector: loopback.Memory });
|
||||
});
|
||||
|
||||
it.onServer('normalizes the http path', function(done) {
|
||||
var UserAccount = PersistedModel.extend(
|
||||
'UserAccount',
|
||||
{ name: String },
|
||||
{
|
||||
remoting: { normalizeHttpPath: true }
|
||||
});
|
||||
app.model(UserAccount);
|
||||
UserAccount.attachTo(db);
|
||||
|
||||
app.use(loopback.rest());
|
||||
request(app).get('/user-accounts').expect(200, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ var async = require('async');
|
|||
var loopback = require('../');
|
||||
|
||||
// create a unique Checkpoint model
|
||||
var Checkpoint = require('../lib/models/checkpoint').extend('TestCheckpoint');
|
||||
var Checkpoint = loopback.Checkpoint.extend('TestCheckpoint');
|
||||
Checkpoint.attachTo(loopback.memory());
|
||||
|
||||
describe('Checkpoint', function() {
|
||||
|
|
|
@ -16,7 +16,9 @@ module.exports = function(config) {
|
|||
files: [
|
||||
'node_modules/es5-shim/es5-shim.js',
|
||||
'test/support.js',
|
||||
'test/loopback.test.js',
|
||||
'test/model.test.js',
|
||||
'test/model.application.test.js',
|
||||
'test/geo-point.test.js',
|
||||
'test/app.test.js'
|
||||
],
|
||||
|
|
|
@ -241,4 +241,28 @@ describe('loopback', function() {
|
|||
expect(owner._targetClass).to.equal('User');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loopback object', function() {
|
||||
it('exports all built-in models', function() {
|
||||
var expectedModelNames = [
|
||||
'Email',
|
||||
'User',
|
||||
'Application',
|
||||
'AccessToken',
|
||||
'Role',
|
||||
'RoleMapping',
|
||||
'ACL',
|
||||
'Scope',
|
||||
'Change',
|
||||
'Checkpoint'
|
||||
];
|
||||
|
||||
expect(Object.keys(loopback)).to.include.members(expectedModelNames);
|
||||
|
||||
expectedModelNames.forEach(function(name) {
|
||||
expect(loopback[name], name).to.be.a('function');
|
||||
expect(loopback[name].modelName, name + '.modelName').to.eql(name);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ describe('RemoteConnector', function() {
|
|||
remoteApp.use(loopback.rest());
|
||||
remoteApp.listen(0, function() {
|
||||
test.dataSource = loopback.createDataSource({
|
||||
host: remoteApp.get('host'),
|
||||
host: 'localhost',
|
||||
port: remoteApp.get('port'),
|
||||
connector: loopback.Remote
|
||||
});
|
||||
|
@ -38,7 +38,7 @@ describe('RemoteConnector', function() {
|
|||
|
||||
remoteApp.listen(0, function() {
|
||||
test.remote = loopback.createDataSource({
|
||||
host: remoteApp.get('host'),
|
||||
host: 'localhost',
|
||||
port: remoteApp.get('port'),
|
||||
connector: loopback.Remote
|
||||
});
|
||||
|
@ -63,6 +63,7 @@ describe('RemoteConnector', function() {
|
|||
|
||||
var m = new RemoteModel({foo: 'bar'});
|
||||
m.save(function(err, inst) {
|
||||
if (err) return done(err);
|
||||
assert(inst instanceof RemoteModel);
|
||||
assert(calledServerCreate);
|
||||
done();
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
var assert = require('assert');
|
||||
var loopback = require('../index');
|
||||
var role = require('../lib/models/role');
|
||||
var Role = role.Role;
|
||||
var RoleMapping = role.RoleMapping;
|
||||
var Role = loopback.Role;
|
||||
var RoleMapping = loopback.RoleMapping;
|
||||
var User = loopback.User;
|
||||
var ACL = require('../lib/models/acl');
|
||||
var ACL = loopback.ACL;
|
||||
|
||||
function checkResult(err, result) {
|
||||
// console.log(err, result);
|
||||
|
|
|
@ -28,6 +28,7 @@ describe('User', function(){
|
|||
});
|
||||
|
||||
beforeEach(function (done) {
|
||||
app.enableAuth();
|
||||
app.use(loopback.token());
|
||||
app.use(loopback.rest());
|
||||
app.model(User);
|
||||
|
@ -600,4 +601,16 @@ describe('User', function(){
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ctor', function() {
|
||||
it('exports default Email model', function() {
|
||||
expect(User.email, 'User.email').to.be.a('function');
|
||||
expect(User.email.modelName, 'modelName').to.eql('email');
|
||||
});
|
||||
|
||||
it('exports default AccessToken model', function() {
|
||||
expect(User.accessToken, 'User.accessToken').to.be.a('function');
|
||||
expect(User.accessToken.modelName, 'modelName').to.eql('AccessToken');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue