Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions common/models/access-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,16 @@ module.exports = function(AccessToken) {
var userRelation = AccessToken.relations.user; // may not be set up
var User = userRelation && userRelation.modelTo;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajtos : I think we need to honour AccessToken.relations.user.modelTo as the default User model. Consider the case where app has custom User and AccessToken models, e.g. in order to customise some of built-in methods and/or settings.
In that case, principalType is not set. Before your change, the code will look up the related user model. After your change, the built-in User model would be incorrectly used.

i moved back to the original implementation following your comment.

although: if I correctly get the docs for Registry.getModelByType(...) we could use getModelByType('User') instead of AccessToken.relations.user.modelTo as the method allows to

to find configured models in models.json over the base model.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although: if I correctly get the docs for Registry.getModelByType(...) we could use getModelByType('User') instead of AccessToken.relations.user.modelTo as the method allows to
to find configured models in models.json over the base model.

That may work in most cases, but I would be concerned about changes in edge cases, e.g. when the app has a custom User-based model that's actually not used for authentication, i.e. AccessToken is attached to the base User model. (That's just an example.)

Let's play it safe and keep the current implementation.


// redefine user model if accessToken's principalType is available
if (this.principalType) {
User = AccessToken.registry.findModel(this.principalType);
if (!User) {
process.nextTick(function() {
return cb(null, false);
});
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callback false if a valid User can't be found with the accessToken's principalType

var now = Date.now();
var created = this.created.getTime();
var elapsedSeconds = (now - created) / 1000;
Expand All @@ -157,14 +167,18 @@ module.exports = function(AccessToken) {
elapsedSeconds < secondsToLive;

if (isValid) {
cb(null, isValid);
process.nextTick(function() {
cb(null, isValid);
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the original implem did not respect the async api

} else {
this.destroy(function(err) {
cb(err, isValid);
});
}
} catch (e) {
cb(e);
process.nextTick(function() {
cb(e);
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

}
};

Expand Down
22 changes: 17 additions & 5 deletions common/models/acl.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ module.exports = function(ACL) {
self.resolveRelatedModels();
var roleModel = self.roleModel;

context.registry = this.registry;
if (!(context instanceof AccessContext)) {
context = new AccessContext(context);
}
Expand Down Expand Up @@ -480,6 +481,7 @@ module.exports = function(ACL) {
assert(token, 'Access token is required');
if (!callback) callback = utils.createPromiseCallback();
var context = new AccessContext({
registry: this.registry,
accessToken: token,
model: model,
property: method,
Expand Down Expand Up @@ -514,6 +516,7 @@ module.exports = function(ACL) {
cb = cb || utils.createPromiseCallback();
type = type || ACL.ROLE;
this.resolveRelatedModels();

switch (type) {
case ACL.ROLE:
this.roleModel.findOne({where: {or: [{name: id}, {id: id}]}}, cb);
Expand All @@ -527,11 +530,20 @@ module.exports = function(ACL) {
{where: {or: [{name: id}, {email: id}, {id: id}]}}, cb);
break;
default:
process.nextTick(function() {
var err = new Error(g.f('Invalid principal type: %s', type));
err.statusCode = 400;
cb(err);
});
// try resolving a user model that matches principalType
var userModel = this.registry.findModel(type);
if (userModel) {
userModel.findOne(
{where: {or: [{username: id}, {email: id}, {id: id}]}},
cb);
} else {
process.nextTick(function() {
var err = new Error(g.f('Invalid principal type: %s', type));
err.statusCode = 400;
err.code = 'INVALID_PRINCIPAL_TYPE';
cb(err);
});
}
}
return cb.promise;
};
Expand Down
10 changes: 9 additions & 1 deletion common/models/role-mapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,17 @@ module.exports = function(RoleMapping) {
RoleMapping.prototype.user = function(callback) {
callback = callback || utils.createPromiseCallback();
this.constructor.resolveRelatedModels();
var userModel;

if (this.principalType === RoleMapping.USER) {
var userModel = this.constructor.userModel;
userModel = this.constructor.userModel;
userModel.findById(this.principalId, callback);
return callback.promise;
}

// try resolving a user model that matches principalType
userModel = this.constructor.registry.findModel(this.principalType);
if (userModel) {
userModel.findById(this.principalId, callback);
} else {
process.nextTick(function() {
Expand Down
2 changes: 1 addition & 1 deletion common/models/role-mapping.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"principalType": {
"type": "string",
"description": "The principal type, such as user, application, or role"
"description": "The principal type, such as USER, APPLICATION, ROLE, or user model name in case of multiple user models"
},
"principalId": {
"type": "string",
Expand Down
40 changes: 32 additions & 8 deletions common/models/role.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ var debug = require('debug')('loopback:security:role');
var assert = require('assert');
var async = require('async');
var utils = require('../../lib/utils');

var AccessContext = require('../../lib/access-context').AccessContext;

var ctx = require('../../lib/access-context');
var AccessContext = ctx.AccessContext;
var Principal = ctx.Principal;
var RoleMapping = loopback.RoleMapping;

assert(RoleMapping, 'RoleMapping model must be defined before Role model');
Expand Down Expand Up @@ -70,7 +70,8 @@ module.exports = function(Role) {
callback = utils.createPromiseCallback();
}
}
if (!query) query = {};
query = query || {};
query.where = query.where || {};

roleModel.resolveRelatedModels();
var relsToModels = {
Expand All @@ -86,8 +87,29 @@ module.exports = function(Role) {
roles: ACL.ROLE,
};

var model = relsToModels[rel];
listByPrincipalType(this, model, relsToTypes[rel], query, callback);
var principalModel = relsToModels[rel];
var principalType = relsToTypes[rel];

// redefine user model and user type if user principalType is custom (available and not "USER")
var isCustomUserPrincipalType = rel === 'users' &&
query.where.principalType &&
query.where.principalType !== RoleMapping.USER;

if (isCustomUserPrincipalType) {
var registry = this.constructor.registry;
principalModel = registry.findModel(query.where.principalType);
principalType = query.where.principalType;
}
// make sure we don't keep principalType in userModel query
delete query.where.principalType;

if (principalModel) {
listByPrincipalType(this, principalModel, principalType, query, callback);
} else {
process.nextTick(function() {
callback(null, []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

});
}
return callback.promise;
};
});
Expand All @@ -102,10 +124,11 @@ module.exports = function(Role) {
* @param {Function} [callback] callback function called with `(err, models)` arguments.
*/
function listByPrincipalType(context, model, principalType, query, callback) {
if (callback === undefined) {
if (callback === undefined && typeof query === 'function') {
callback = query;
query = {};
}
query = query || {};

roleModel.roleMappingModel.find({
where: {roleId: context.id, principalType: principalType},
Expand Down Expand Up @@ -303,6 +326,7 @@ module.exports = function(Role) {
* @promise
*/
Role.isInRole = function(role, context, callback) {
context.registry = this.registry;
if (!(context instanceof AccessContext)) {
context = new AccessContext(context);
}
Expand Down Expand Up @@ -421,9 +445,9 @@ module.exports = function(Role) {
callback = utils.createPromiseCallback();
}
}

if (!options) options = {};

context.registry = this.registry;
if (!(context instanceof AccessContext)) {
context = new AccessContext(context);
}
Expand Down
9 changes: 7 additions & 2 deletions common/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -683,13 +683,18 @@ module.exports = function(User) {
return process.nextTick(cb);

var AccessToken = accessTokenRelation.modelTo;

var query = {userId: {inq: userIds}};
var tokenPK = AccessToken.definition.idName() || 'id';
if (options.accessToken && tokenPK in options.accessToken) {
query[tokenPK] = {neq: options.accessToken[tokenPK]};
}

// add principalType in AccessToken.query if using polymorphic relations
// between AccessToken and User
var relatedUser = AccessToken.relations.user;
var isRelationPolymorphic = relatedUser.polymorphic && !relatedUser.modelTo;
if (isRelationPolymorphic) {
query.principalType = this.modelName;
}
AccessToken.deleteAll(query, options, cb);
};

Expand Down
23 changes: 21 additions & 2 deletions lib/access-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ function AccessContext(context) {
}
context = context || {};

assert(context.registry,
'Application registry is mandatory in AccessContext but missing in provided context');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we fall back to loopback.registry when context.registry is not provided, in order to preserve backwards compatibility? loopback.registry is the global singleton where models like loopback.User are defined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should only fallback to loopback.registry in case localRegistry is set to false, or not set and running a loopback version where it defaults to false. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should only fallback to loopback.registry in case localRegistry is set to false, or not set and running a loopback version where it defaults to false.

Sure, that sounds good too. It may be best to leave this change out of scope of this pull request, so that this patch can be finally landed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure !!!
I will take care of this in a second step

this.registry = context.registry;
this.principals = context.principals || [];
var model = context.model;
model = ('string' === typeof model) ? loopback.getModel(model) : model;
model = ('string' === typeof model) ? this.registry.getModel(model) : model;
this.model = model;
this.modelName = model && model.modelName;

Expand Down Expand Up @@ -62,6 +65,7 @@ function AccessContext(context) {
var principalType = context.principalType || Principal.USER;
var principalId = context.principalId || undefined;
var principalName = context.principalName || undefined;

if (principalId) {
this.addPrincipal(principalType, principalId, principalName);
}
Expand Down Expand Up @@ -124,11 +128,25 @@ AccessContext.prototype.addPrincipal = function(principalType, principalId, prin
* @returns {*}
*/
AccessContext.prototype.getUserId = function() {
var BaseUser = this.registry.getModel('User');
for (var i = 0; i < this.principals.length; i++) {
var p = this.principals[i];
var isBuiltinPrincipal = p.type === Principal.APP ||
p.type === Principal.ROLE ||
p.type == Principal.SCOPE;
if (isBuiltinPrincipal) continue;

// the principalType must either be 'USER'
if (p.type === Principal.USER) {
return p.id;
}

// or permit to resolve a valid user model
var userModel = this.registry.findModel(p.type);
if (!userModel) continue;
if (userModel.prototype instanceof BaseUser) {
return p.id;
}
}
return null;
};
Expand Down Expand Up @@ -189,8 +207,9 @@ AccessContext.prototype.debug = function() {
* This class represents the abstract notion of a principal, which can be used
* to represent any entity, such as an individual, a corporation, and a login id
* @param {String} type The principal type
* @param {*} id The princiapl id
* @param {*} id The principal id
* @param {String} [name] The principal name
* @param {String} modelName The principal model name
* @returns {Principal}
* @class
*/
Expand Down
Loading