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) {
|
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) {
|
ds.createModel = function (name, properties, settings) {
|
||||||
var ModelCtor = loopback.createModel(name, properties, settings);
|
var ModelCtor = loopback.createModel(name, properties, settings);
|
||||||
ModelCtor.attachTo(ds);
|
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 name: Album
|
||||||
* model instance properties: userId of the album, friends, shared
|
* model instance properties: userId of the album, friends, shared
|
||||||
* methods
|
* methods
|
||||||
* app and/or user ids/roles
|
* app and/or user ids/roles
|
||||||
** loggedIn
|
** loggedIn
|
||||||
** roles
|
** roles
|
||||||
** userId
|
** userId
|
||||||
|
@ -17,35 +17,166 @@ Factors to be authorized against:
|
||||||
** everyone
|
** everyone
|
||||||
** relations: owner/friend/granted
|
** relations: owner/friend/granted
|
||||||
|
|
||||||
Class level permissions, for example, Album
|
Class level permissions, for example, Album
|
||||||
* model name: Album
|
* model name: Album
|
||||||
* methods
|
* methods
|
||||||
|
|
||||||
URL/Route level permissions
|
URL/Route level permissions
|
||||||
* url pattern
|
* url pattern
|
||||||
* application id
|
* application id
|
||||||
* ip addresses
|
* ip addresses
|
||||||
* http headers
|
* 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 = {
|
var ACLSchema = {
|
||||||
model: String, // The model name
|
model: String, // The name of the model
|
||||||
properties: [String], // A list of property names
|
property: String, // The name of the property, method, scope, or relation
|
||||||
methods: [String], // A list of methods
|
|
||||||
roles: [String], // A list of roles
|
/**
|
||||||
permission: {type: String, enum: ['Allow', 'Deny']}, // Allow/Deny
|
* Name of the access type - READ/WRITE/EXEC
|
||||||
status: String, // Enabled/disabled
|
*/
|
||||||
created: Date,
|
accessType: String,
|
||||||
modified: Date
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
ACL.find({where: {principalType: principalType, principalId: principalId,
|
||||||
dataSource = dataSource || new require('loopback-datasource-juggler').ModelBuilder();
|
model: model, property: propertyQuery, accessType: accessTypeQuery}},
|
||||||
var ACL = dataSource.define('ACL', ACLSchema);
|
function (err, acls) {
|
||||||
return ACL;
|
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
|
// Role model
|
||||||
var RoleSchema = {
|
var RoleSchema = {
|
||||||
id: {type: String, required: true}, // Id
|
id: {type: String, id: true}, // Id
|
||||||
name: {type: String, required: true}, // The name of a role
|
name: {type: String, required: true}, // The name of a role
|
||||||
description: String, // Description
|
description: String, // Description
|
||||||
roles: [String], // A role can be an aggregate of other roles
|
|
||||||
users: [String], // A role contains a list of user ids
|
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
created: {type: Date, default: Date},
|
created: {type: Date, default: Date},
|
||||||
modified: {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",
|
"debug": "~0.7.2",
|
||||||
"express": "~3.4.0",
|
"express": "~3.4.0",
|
||||||
"loopback-datasource-juggler": "~1.2.0",
|
"loopback-datasource-juggler": "~1.2.0",
|
||||||
"strong-remoting": "~1.0.0",
|
"strong-remoting": "~1.1.0",
|
||||||
"inflection": "~1.2.5",
|
"inflection": "~1.2.5",
|
||||||
"passport": "~0.1.17",
|
"passport": "~0.1.17",
|
||||||
"passport-local": "~0.1.6",
|
"passport-local": "~0.1.6",
|
||||||
"nodemailer": "~0.4.4",
|
"nodemailer": "~0.5.5",
|
||||||
"ejs": "~0.8.4",
|
"ejs": "~0.8.4",
|
||||||
"bcryptjs": "~0.7.10",
|
"bcryptjs": "~0.7.10",
|
||||||
"underscore.string": "~2.3.3",
|
"underscore.string": "~2.3.3",
|
||||||
|
@ -34,9 +34,9 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"blanket": "~1.1.5",
|
"blanket": "~1.1.5",
|
||||||
"mocha": "~1.12.1",
|
"mocha": "~1.14.0",
|
||||||
"strong-task-emitter": "0.0.x",
|
"strong-task-emitter": "0.0.x",
|
||||||
"supertest": "~0.7.1"
|
"supertest": "~0.8.1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"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() {
|
// describe('Model.hasAndBelongsToMany()', function() {
|
||||||
// it("TODO: implement / document", function(done) {
|
// it("TODO: implement / document", function(done) {
|
||||||
// /* example -
|
// /* 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');
|
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();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue