Merge pull request #61 from strongloop/acl
Start to add ACL related models to LoopBack
This commit is contained in:
commit
965ff4e350
|
@ -90,7 +90,7 @@ loopback.errorHandler.title = 'Loopback';
|
|||
*/
|
||||
|
||||
loopback.createDataSource = function (name, options) {
|
||||
var ds = new DataSource(name, options);
|
||||
var ds = new DataSource(name, options, loopback.Model.dataSource);
|
||||
ds.createModel = function (name, properties, settings) {
|
||||
var ModelCtor = loopback.createModel(name, properties, settings);
|
||||
ModelCtor.attachTo(ds);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
/**
|
||||
Schema ACL options
|
||||
Schema ACL options
|
||||
|
||||
Object level permissions, for example, an album owned by a user
|
||||
Object level permissions, for example, an album owned by a user
|
||||
|
||||
Factors to be authorized against:
|
||||
Factors to be authorized against:
|
||||
|
||||
* model name: Album
|
||||
* model instance properties: userId of the album, friends, shared
|
||||
* methods
|
||||
* app and/or user ids/roles
|
||||
* model name: Album
|
||||
* model instance properties: userId of the album, friends, shared
|
||||
* methods
|
||||
* app and/or user ids/roles
|
||||
** loggedIn
|
||||
** roles
|
||||
** userId
|
||||
|
@ -17,35 +17,166 @@ Factors to be authorized against:
|
|||
** everyone
|
||||
** relations: owner/friend/granted
|
||||
|
||||
Class level permissions, for example, Album
|
||||
Class level permissions, for example, Album
|
||||
* model name: Album
|
||||
* methods
|
||||
|
||||
URL/Route level permissions
|
||||
URL/Route level permissions
|
||||
* url pattern
|
||||
* application id
|
||||
* ip addresses
|
||||
* http headers
|
||||
|
||||
Map to oAuth 2.0 scopes
|
||||
Map to oAuth 2.0 scopes
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
var loopback = require('../loopback');
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @type {createModel|*}
|
||||
*/
|
||||
var Scope = loopback.createModel('Scope', ScopeSchema);
|
||||
|
||||
/**
|
||||
* 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 model name
|
||||
properties: [String], // A list of property names
|
||||
methods: [String], // A list of methods
|
||||
roles: [String], // A list of roles
|
||||
permission: {type: String, enum: ['Allow', 'Deny']}, // Allow/Deny
|
||||
status: String, // Enabled/disabled
|
||||
created: Date,
|
||||
modified: Date
|
||||
model: String, // The name of the model
|
||||
property: String, // The name of the property, method, scope, or relation
|
||||
|
||||
/**
|
||||
* Name of the access type - READ/WRITE/EXEC
|
||||
*/
|
||||
accessType: String,
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
permission: String,
|
||||
/**
|
||||
* Type of the principal - Application/User/Role
|
||||
*/
|
||||
principalType: String,
|
||||
/**
|
||||
* Id of the principal - such as appId, userId or roleId
|
||||
*/
|
||||
principalId: String
|
||||
};
|
||||
|
||||
var ACL = loopback.createModel('ACL', ACLSchema);
|
||||
|
||||
ACL.ALL = '*';
|
||||
|
||||
ACL.ALLOW = 'ALLOW';
|
||||
ACL.ALARM = 'ALARM';
|
||||
ACL.AUDIT = 'AUDIT';
|
||||
ACL.DENY = 'DENY';
|
||||
|
||||
ACL.READ = 'READ';
|
||||
ACL.WRITE = 'WRITE';
|
||||
ACL.EXECUTE = 'EXECUTE';
|
||||
|
||||
ACL.USER = 'USER';
|
||||
ACL.APP = ACL.APPLICATION = 'APP';
|
||||
ACL.ROLE = 'ROLE';
|
||||
ACL.SCOPE = 'SCOPE';
|
||||
|
||||
var permissionOrder = {
|
||||
ALLOW: 1,
|
||||
ALARM: 2,
|
||||
AUDIT: 3,
|
||||
DENY: 4
|
||||
};
|
||||
|
||||
function overridePermission(p1, p2) {
|
||||
p1 = permissionOrder[p1] ? p1 : ACL.ALLOW;
|
||||
p2 = permissionOrder[p2] ? p2 : ACL.ALLOW;
|
||||
var i1 = permissionOrder[p1];
|
||||
var i2 = permissionOrder[p2];
|
||||
return i1 > i2 ? p1 : p2;
|
||||
}
|
||||
|
||||
// readAccess, writeAccess --> public, userId, role
|
||||
/**
|
||||
* Check if the given principal is allowed to access the model/property
|
||||
* @param principalType
|
||||
* @param principalId
|
||||
* @param model
|
||||
* @param property
|
||||
* @param accessType
|
||||
* @param callback
|
||||
*/
|
||||
ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) {
|
||||
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]};
|
||||
|
||||
module.exports = function(dataSource) {
|
||||
dataSource = dataSource || new require('loopback-datasource-juggler').ModelBuilder();
|
||||
var ACL = dataSource.define('ACL', ACLSchema);
|
||||
return ACL;
|
||||
}
|
||||
ACL.find({where: {principalType: principalType, principalId: principalId,
|
||||
model: model, property: propertyQuery, accessType: accessTypeQuery}},
|
||||
function (err, acls) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
var resolvedPermission = acls.reduce(function (previousValue, currentValue, index, array) {
|
||||
// If the property is the same or the previous one is ACL.ALL (ALL)
|
||||
if (previousValue.property === currentValue.property || (previousValue.property === ACL.ALL && currentValue.property)) {
|
||||
previousValue.property = currentValue.property;
|
||||
if (previousValue.accessType === currentValue.accessType || (previousValue.accessType === ACL.ALL && currentValue.accessType)) {
|
||||
previousValue.accessType = currentValue.accessType;
|
||||
}
|
||||
previousValue.permission = overridePermission(previousValue.permission, currentValue.permission);
|
||||
}
|
||||
return previousValue;
|
||||
}, {principalType: principalType, principalId: principalId, model: model, property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW});
|
||||
callback && callback(null, resolvedPermission);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given scope is allowed to access the model/property
|
||||
* @param scope
|
||||
* @param model
|
||||
* @param property
|
||||
* @param accessType
|
||||
* @param callback
|
||||
*/
|
||||
Scope.checkPermission = function (scope, model, property, accessType, callback) {
|
||||
Scope.findOne({where: {name: scope}}, function (err, scope) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
} else {
|
||||
ACL.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
ACL: ACL,
|
||||
Scope: Scope
|
||||
};
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
var loopback = require('../loopback');
|
||||
|
||||
// "OAuth token"
|
||||
var OAuthToken = loopback.createModel({
|
||||
// "access token"
|
||||
accessToken: {
|
||||
type: String,
|
||||
index: {
|
||||
unique: true
|
||||
}
|
||||
}, // key, The string token
|
||||
clientId: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The client id
|
||||
resourceOwner: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The resource owner (user) id
|
||||
realm: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The resource owner realm
|
||||
issuedAt: {
|
||||
type: Date,
|
||||
index: true
|
||||
}, // The timestamp when the token is issued
|
||||
expiresIn: Number, // Expiration time in seconds
|
||||
expiredAt: {
|
||||
type: Date,
|
||||
index: {
|
||||
expires: "1d"
|
||||
}
|
||||
}, // The timestamp when the token is expired
|
||||
scopes: [ String ], // oAuth scopes
|
||||
parameters: [
|
||||
{
|
||||
name: String,
|
||||
value: String
|
||||
}
|
||||
],
|
||||
|
||||
authorizationCode: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The corresponding authorization code that is used to request the
|
||||
// access token
|
||||
refreshToken: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The corresponding refresh token if issued
|
||||
|
||||
tokenType: {
|
||||
type: String,
|
||||
enum: [ "Bearer", "MAC" ]
|
||||
}, // The token type, such as Bearer:
|
||||
// http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16
|
||||
// or MAC: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
|
||||
authenticationScheme: String, // HTTP authenticationScheme
|
||||
hash: String // The SHA-1 hash for
|
||||
// client-secret/resource-owner-secret-key
|
||||
});
|
||||
|
||||
// "OAuth authorization code"
|
||||
var OAuthAuthorizationCode = loopback.createModel({
|
||||
code: {
|
||||
type: String,
|
||||
index: {
|
||||
unique: true
|
||||
}
|
||||
}, // key // The string code
|
||||
clientId: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The client id
|
||||
resourceOwner: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The resource owner (user) id
|
||||
realm: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The resource owner realm
|
||||
|
||||
issuedAt: {
|
||||
type: Date,
|
||||
index: true
|
||||
}, // The timestamp when the token is issued
|
||||
expiresIn: Number, // Expiration time in seconds
|
||||
expiredAt: {
|
||||
type: Date,
|
||||
index: {
|
||||
expires: "1d"
|
||||
}
|
||||
}, // The timestamp when the token is expired
|
||||
|
||||
scopes: [ String ], // oAuth scopes
|
||||
parameters: [
|
||||
{
|
||||
name: String,
|
||||
value: String
|
||||
}
|
||||
],
|
||||
|
||||
used: Boolean, // Is it ever used
|
||||
redirectURI: String, // The redirectURI from the request, we need to
|
||||
// check if it's identical to the one used for
|
||||
// access token
|
||||
hash: String // The SHA-1 hash for
|
||||
// client-secret/resource-owner-secret-key
|
||||
});
|
||||
|
||||
// "OAuth client registration record"
|
||||
var ClientRegistration = loopback.createModel({
|
||||
id: {
|
||||
type: String,
|
||||
index: {
|
||||
unique: true
|
||||
}
|
||||
},
|
||||
clientId: {
|
||||
type: String,
|
||||
index: {
|
||||
unique: true
|
||||
}
|
||||
}, // key; // The client id
|
||||
clientSecret: String, // The generated client secret
|
||||
|
||||
defaultTokenType: String,
|
||||
accessLevel: Number, // The access level to scopes, -1: disabled, 0:
|
||||
// basic, 1..N
|
||||
disabled: Boolean,
|
||||
|
||||
name: {
|
||||
type: String,
|
||||
index: true
|
||||
},
|
||||
email: String,
|
||||
description: String,
|
||||
url: String,
|
||||
iconURL: String,
|
||||
redirectURIs: [ String ],
|
||||
type: {
|
||||
type: String,
|
||||
enum: [ "CONFIDENTIAL", "PUBLIC" ]
|
||||
},
|
||||
|
||||
userId: {
|
||||
type: String,
|
||||
index: true
|
||||
} // The registered developer
|
||||
|
||||
});
|
||||
|
||||
// "OAuth permission"
|
||||
var OAuthPermission = loopback.createModel({
|
||||
clientId: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The client id
|
||||
resourceOwner: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The resource owner (user) id
|
||||
realm: {
|
||||
type: String,
|
||||
index: true
|
||||
}, // The resource owner realm
|
||||
|
||||
issuedAt: {
|
||||
type: Date,
|
||||
index: true
|
||||
}, // The timestamp when the permission is issued
|
||||
expiresIn: Number, // Expiration time in seconds
|
||||
expiredAt: {
|
||||
type: Date,
|
||||
index: {
|
||||
expires: "1d"
|
||||
}
|
||||
}, // The timestamp when the permission is expired
|
||||
|
||||
scopes: [ String ]
|
||||
});
|
||||
|
||||
// "OAuth scope"
|
||||
var OAuthScope = loopback.createModel({
|
||||
scope: {
|
||||
type: String,
|
||||
index: {
|
||||
unique: true
|
||||
}
|
||||
}, // key; // The scope name
|
||||
description: String, // Description of the scope
|
||||
iconURL: String, // The icon to be displayed on the "Request Permission"
|
||||
// dialog
|
||||
expiresIn: Number, // The default maximum lifetime of access token that
|
||||
// carries the scope
|
||||
requiredAccessLevel: Number, // The minimum access level required
|
||||
resourceOwnerAuthorizationRequired: Boolean
|
||||
// The scope requires authorization from the resource owner
|
||||
});
|
||||
|
||||
// "OAuth protected resource"
|
||||
var OAuthResource = loopback.createModel({
|
||||
operations: [
|
||||
{
|
||||
type: String,
|
||||
enum: [ "ALL", "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH" ]
|
||||
}
|
||||
], // A list of operations, by default ALL
|
||||
path: String, // The resource URI path
|
||||
scopes: [ String ]
|
||||
// Allowd scopes
|
||||
});
|
||||
|
||||
// Use the schema to register a model
|
||||
exports.OAuthToken = OAuthToken;
|
||||
exports.OAuthAuthorizationCode = OAuthAuthorizationCode;
|
||||
exports.ClientRegistration = ClientRegistration;
|
||||
exports.OAuthPermission = OAuthPermission;
|
||||
exports.OAuthScope = OAuthScope;
|
||||
exports.OAuthResource = OAuthResource;
|
|
@ -1,18 +1,208 @@
|
|||
var loopback = require('../loopback');
|
||||
|
||||
// Role model
|
||||
var RoleSchema = {
|
||||
id: {type: String, required: true}, // Id
|
||||
name: {type: String, required: true}, // The name of a role
|
||||
description: String, // Description
|
||||
roles: [String], // A role can be an aggregate of other roles
|
||||
users: [String], // A role contains a list of user ids
|
||||
id: {type: String, id: 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}
|
||||
}
|
||||
// Timestamps
|
||||
created: {type: Date, default: Date},
|
||||
modified: {type: Date, default: Date}
|
||||
};
|
||||
|
||||
/**
|
||||
* Map principals to roles
|
||||
*/
|
||||
var RoleMappingSchema = {
|
||||
id: {type: String, id: true}, // Id
|
||||
roleId: String, // The role id
|
||||
principalType: String, // The principal type, such as user, application, or role
|
||||
principalId: String // The principal id
|
||||
};
|
||||
|
||||
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
|
||||
* @param callback
|
||||
*/
|
||||
RoleMapping.prototype.application = function (callback) {
|
||||
if (this.principalType === RoleMapping.APPLICATION) {
|
||||
loopback.Application.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the user principal
|
||||
* @param callback
|
||||
*/
|
||||
RoleMapping.prototype.user = function (callback) {
|
||||
if (this.principalType === RoleMapping.USER) {
|
||||
loopback.User.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the child role principal
|
||||
* @param callback
|
||||
*/
|
||||
RoleMapping.prototype.childRole = function (callback) {
|
||||
if (this.principalType === RoleMapping.ROLE) {
|
||||
loopback.User.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function () {
|
||||
callback && callback(null, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Define the Role model with `hasMany` relation to RoleMapping
|
||||
*/
|
||||
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 () {
|
||||
Role.prototype.users = function (callback) {
|
||||
RoleMapping.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) {
|
||||
RoleMapping.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) {
|
||||
RoleMapping.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.EVERYONE = "$everyone"; // everyone
|
||||
|
||||
/**
|
||||
* Add custom handler for roles
|
||||
* @param role
|
||||
* @param resolver The resolver function decides if a principal is in the role dynamically
|
||||
*
|
||||
* isInRole(role, context, callback)
|
||||
*/
|
||||
Role.registerResolver = function(role, resolver) {
|
||||
Role.resolvers[role] = resolver;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a given principal is in the role
|
||||
*
|
||||
* @param role
|
||||
* @param principalType
|
||||
* @param principalId
|
||||
* @param callback
|
||||
*/
|
||||
Role.isInRole = function (role, principalType, principalId, callback) {
|
||||
Role.findOne({where: {name: role}}, function (err, result) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
if(!result) {
|
||||
callback && callback(null, false);
|
||||
return;
|
||||
}
|
||||
RoleMapping.findOne({where: {roleId: result.id, principalType: principalType, principalId: principalId}},
|
||||
function (err, result) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
callback && callback(null, !!result);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* List roles for a given principal
|
||||
* @param {String} principalType
|
||||
* @param {String|Number} principalId
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback callback
|
||||
* @param err
|
||||
* @param {String[]} An array of role ids
|
||||
*/
|
||||
Role.getRoles = function (principalType, principalId, callback) {
|
||||
RoleMapping.find({where: {principalType: principalType, principalId: principalId}}, function (err, mappings) {
|
||||
if (err) {
|
||||
callback && callback(err);
|
||||
return;
|
||||
}
|
||||
var roles = [];
|
||||
mappings.forEach(function (m) {
|
||||
roles.push(m.roleId);
|
||||
});
|
||||
callback && callback(null, roles);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Role: Role,
|
||||
RoleMapping: RoleMapping
|
||||
};
|
||||
|
||||
module.exports = function(dataSource) {
|
||||
dataSource = dataSource || new require('loopback-datasource-juggler').ModelBuilder();
|
||||
var Role = dataSource.define('Role', RoleSchema);
|
||||
return Role;
|
||||
}
|
||||
|
|
|
@ -21,11 +21,11 @@
|
|||
"debug": "~0.7.2",
|
||||
"express": "~3.4.0",
|
||||
"loopback-datasource-juggler": "~1.2.0",
|
||||
"strong-remoting": "~1.0.0",
|
||||
"strong-remoting": "~1.1.0",
|
||||
"inflection": "~1.2.5",
|
||||
"passport": "~0.1.17",
|
||||
"passport-local": "~0.1.6",
|
||||
"nodemailer": "~0.4.4",
|
||||
"nodemailer": "~0.5.5",
|
||||
"ejs": "~0.8.4",
|
||||
"bcryptjs": "~0.7.10",
|
||||
"underscore.string": "~2.3.3",
|
||||
|
@ -34,9 +34,9 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"blanket": "~1.1.5",
|
||||
"mocha": "~1.12.1",
|
||||
"mocha": "~1.14.0",
|
||||
"strong-task-emitter": "0.0.x",
|
||||
"supertest": "~0.7.1"
|
||||
"supertest": "~0.8.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
var assert = require('assert');
|
||||
var loopback = require('../index');
|
||||
var acl = require('../lib/models/acl');
|
||||
var Scope = acl.Scope;
|
||||
var ACL = acl.ACL;
|
||||
var ScopeACL = acl.ScopeACL;
|
||||
var User = loopback.User;
|
||||
|
||||
function checkResult(err, result) {
|
||||
// console.log(err, result);
|
||||
assert(!err);
|
||||
}
|
||||
|
||||
describe('security scopes', function () {
|
||||
|
||||
it("should allow access to models for the given scope by wildcard", function () {
|
||||
var ds = loopback.createDataSource({connector: loopback.Memory});
|
||||
Scope.attachTo(ds);
|
||||
ACL.attachTo(ds);
|
||||
|
||||
// console.log(Scope.relations);
|
||||
|
||||
Scope.create({name: 'user', description: 'access user information'}, function (err, scope) {
|
||||
// console.log(scope);
|
||||
ACL.create({principalType: ACL.SCOPE, principalId: scope.id, model: 'user', property: ACL.ALL,
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
function (err, resource) {
|
||||
// console.log(resource);
|
||||
Scope.checkPermission('user', 'user', ACL.ALL, ACL.ALL, checkResult);
|
||||
Scope.checkPermission('user', 'user', 'name', ACL.ALL, checkResult);
|
||||
Scope.checkPermission('user', 'user', 'name', ACL.READ, checkResult);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("should allow access to models for the given scope", function () {
|
||||
var ds = loopback.createDataSource({connector: loopback.Memory});
|
||||
Scope.attachTo(ds);
|
||||
ACL.attachTo(ds);
|
||||
|
||||
// console.log(Scope.relations);
|
||||
|
||||
Scope.create({name: 'user', description: 'access user information'}, function (err, scope) {
|
||||
// console.log(scope);
|
||||
ACL.create({principalType: ACL.SCOPE, principalId: scope.id,
|
||||
model: 'user', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW},
|
||||
function (err, resource) {
|
||||
ACL.create({principalType: ACL.SCOPE, principalId: scope.id,
|
||||
model: 'user', property: 'name', accessType: ACL.WRITE, permission: ACL.DENY},
|
||||
function (err, resource) {
|
||||
// console.log(resource);
|
||||
Scope.checkPermission('user', 'user', ACL.ALL, ACL.ALL, function (err, perm) {
|
||||
assert(perm.permission === ACL.DENY); // because name.WRITE == DENY
|
||||
});
|
||||
Scope.checkPermission('user', 'user', 'name', ACL.ALL, function (err, perm) {
|
||||
assert(perm.permission === ACL.DENY); // because name.WRITE == DENY
|
||||
});
|
||||
Scope.checkPermission('user', 'user', 'name', ACL.READ, function (err, perm) {
|
||||
assert(perm.permission === ACL.ALLOW);
|
||||
});
|
||||
Scope.checkPermission('user', 'user', 'name', ACL.WRITE, function (err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('security ACLs', function () {
|
||||
|
||||
it("should allow access to models for the given principal by wildcard", function () {
|
||||
var ds = loopback.createDataSource({connector: loopback.Memory});
|
||||
ACL.attachTo(ds);
|
||||
|
||||
ACL.create({principalType: 'user', principalId: 'u001', model: 'user', property: ACL.ALL,
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) {
|
||||
|
||||
ACL.create({principalType: 'user', principalId: 'u001', model: 'user', property: ACL.ALL,
|
||||
accessType: ACL.READ, permission: ACL.DENY}, function (err, acl) {
|
||||
|
||||
ACL.checkPermission('user', 'u001', 'user', 'name', ACL.READ, function (err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
});
|
||||
|
||||
ACL.checkPermission('user', 'u001', 'user', 'name', ACL.ALL, function (err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -512,6 +512,39 @@ describe('Model', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Model.extend() events', function() {
|
||||
it('create isolated emitters for subclasses', function() {
|
||||
var User1 = loopback.createModel('User1', {
|
||||
'first': String,
|
||||
'last': String
|
||||
});
|
||||
|
||||
var User2 = loopback.createModel('User2', {
|
||||
'name': String
|
||||
});
|
||||
|
||||
var user1Triggered = false;
|
||||
User1.once('x', function(event) {
|
||||
user1Triggered = true;
|
||||
});
|
||||
|
||||
|
||||
var user2Triggered = false;
|
||||
User2.once('x', function(event) {
|
||||
user2Triggered = true;
|
||||
});
|
||||
|
||||
assert(User1.once !== User2.once);
|
||||
assert(User1.once !== loopback.Model.once);
|
||||
|
||||
User1.emit('x', User1);
|
||||
|
||||
assert(user1Triggered);
|
||||
assert(!user2Triggered);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// describe('Model.hasAndBelongsToMany()', function() {
|
||||
// it("TODO: implement / document", function(done) {
|
||||
// /* example -
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
var assert = require('assert');
|
||||
var loopback = require('../index');
|
||||
var role = require('../lib/models/role');
|
||||
var Role = role.Role;
|
||||
var RoleMapping = role.RoleMapping;
|
||||
var User = loopback.User;
|
||||
|
||||
function checkResult(err, result) {
|
||||
// console.log(err, result);
|
||||
assert(!err);
|
||||
}
|
||||
|
||||
describe('role model', function () {
|
||||
|
||||
it("should define role/role relations", function () {
|
||||
var ds = loopback.createDataSource({connector: 'memory'});
|
||||
Role.attachTo(ds);
|
||||
RoleMapping.attachTo(ds);
|
||||
|
||||
Role.create({name: 'user'}, function (err, userRole) {
|
||||
Role.create({name: 'admin'}, function (err, adminRole) {
|
||||
userRole.principals.create({principalType: RoleMapping.ROLE, principalId: adminRole.id}, function (err, mapping) {
|
||||
Role.find(function (err, roles) {
|
||||
assert.equal(roles.length, 2);
|
||||
});
|
||||
RoleMapping.find(function (err, mappings) {
|
||||
assert.equal(mappings.length, 1);
|
||||
assert.equal(mappings[0].principalType, RoleMapping.ROLE);
|
||||
assert.equal(mappings[0].principalId, adminRole.id);
|
||||
});
|
||||
userRole.principals(function (err, principals) {
|
||||
assert.equal(principals.length, 1);
|
||||
});
|
||||
userRole.roles(function (err, roles) {
|
||||
assert.equal(roles.length, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("should define role/user relations", function () {
|
||||
var ds = loopback.createDataSource({connector: 'memory'});
|
||||
User.attachTo(ds);
|
||||
Role.attachTo(ds);
|
||||
RoleMapping.attachTo(ds);
|
||||
|
||||
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
|
||||
// console.log('User: ', user.id);
|
||||
Role.create({name: 'userRole'}, function (err, role) {
|
||||
role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function (err, p) {
|
||||
Role.find(function(err, roles) {
|
||||
assert(!err);
|
||||
assert.equal(roles.length, 1);
|
||||
assert.equal(roles[0].name, 'userRole');
|
||||
});
|
||||
role.principals(function(err, principals) {
|
||||
assert(!err);
|
||||
// console.log(principals);
|
||||
assert.equal(principals.length, 1);
|
||||
assert.equal(principals[0].principalType, RoleMapping.USER);
|
||||
assert.equal(principals[0].principalId, user.id);
|
||||
});
|
||||
role.users(function(err, users) {
|
||||
assert(!err);
|
||||
assert.equal(users.length, 1);
|
||||
assert.equal(users[0].principalType, RoleMapping.USER);
|
||||
assert.equal(users[0].principalId, user.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("should support getRoles() and isInRole()", function () {
|
||||
var ds = loopback.createDataSource({connector: 'memory'});
|
||||
User.attachTo(ds);
|
||||
Role.attachTo(ds);
|
||||
RoleMapping.attachTo(ds);
|
||||
|
||||
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
|
||||
// console.log('User: ', user.id);
|
||||
Role.create({name: 'userRole'}, function (err, role) {
|
||||
role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function (err, p) {
|
||||
// Role.find(console.log);
|
||||
// role.principals(console.log);
|
||||
Role.isInRole('userRole', RoleMapping.USER, user.id, function(err, exists) {
|
||||
assert(!err && exists === true);
|
||||
});
|
||||
|
||||
Role.isInRole('userRole', RoleMapping.APP, user.id, function(err, exists) {
|
||||
assert(!err && exists === false);
|
||||
});
|
||||
|
||||
Role.isInRole('userRole', RoleMapping.USER, 100, function(err, exists) {
|
||||
assert(!err && exists === false);
|
||||
});
|
||||
|
||||
Role.getRoles(RoleMapping.USER, user.id, function(err, roles) {
|
||||
assert.equal(roles.length, 1);
|
||||
assert.equal(roles[0], role.id);
|
||||
});
|
||||
Role.getRoles(RoleMapping.APP, user.id, function(err, roles) {
|
||||
assert.equal(roles.length, 0);
|
||||
});
|
||||
Role.getRoles(RoleMapping.USER, 100, function(err, roles) {
|
||||
assert.equal(roles.length, 0);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ describe('User', function(){
|
|||
|
||||
|
||||
var lines = result.email.message.split('\n');
|
||||
assert(lines[4].indexOf('To: bar@bat.com') === 0);
|
||||
assert(lines[3].indexOf('To: bar@bat.com') === 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue