diff --git a/lib/application.js b/lib/application.js index 3d448d67..b25a79b0 100644 --- a/lib/application.js +++ b/lib/application.js @@ -301,18 +301,16 @@ app.enableAuth = function() { var Model = method.ctor; var modelInstance = ctx.instance; - var modelId = modelInstance && modelInstance.id || - // replacement for deprecated req.param() - (req.params && req.params.id !== undefined ? req.params.id : - req.body && req.body.id !== undefined ? req.body.id : - req.query && req.query.id !== undefined ? req.query.id : - undefined); + var modelId = modelInstance && modelInstance.id || ctx.args.id; + ctx.modelId = modelId; var modelName = Model.modelName; var modelSettings = Model.settings || {}; var errStatusCode = modelSettings.aclErrorStatus || app.get('aclErrorStatus') || 401; - if (!req.accessToken) { + if (req.accessToken) { + ctx.accessToken = req.accessToken; + } else { errStatusCode = 401; } diff --git a/lib/model.js b/lib/model.js index 1eb67052..06c0e0fb 100644 --- a/lib/model.js +++ b/lib/model.js @@ -5,6 +5,8 @@ var assert = require('assert'); var RemoteObjects = require('strong-remoting'); var SharedClass = require('strong-remoting').SharedClass; var extend = require('util')._extend; +var normalizeInclude = require('loopback-datasource-juggler/lib/include').normalizeInclude; +var async = require('async'); module.exports = function(registry) { @@ -289,9 +291,10 @@ module.exports = function(registry) { */ Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) { + var Model = this; + var aclModel = Model._ACL(); var ANONYMOUS = registry.getModel('AccessToken').ANONYMOUS; token = token || ANONYMOUS; - var aclModel = Model._ACL(); ctx = ctx || {}; if (typeof ctx === 'function' && callback === undefined) { @@ -310,7 +313,11 @@ module.exports = function(registry) { remotingContext: ctx }, function(err, accessRequest) { if (err) return callback(err); - callback(null, accessRequest.isAllowed()); + if(typeof sharedMethod.authorization === 'function') { + sharedMethod.authorization(ctx, accessRequest, callback); + } else { + callback(null, accessRequest.isAllowed()); + } }); }; @@ -436,6 +443,7 @@ module.exports = function(registry) { var modelName = relation.modelTo && relation.modelTo.modelName; modelName = modelName || 'PersistedModel'; var fn = this.prototype[relationName]; + var registry = this.registry; var pathName = (relation.options.http && relation.options.http.path) || relationName; define('__get__' + relationName, { isStatic: false, @@ -443,7 +451,11 @@ module.exports = function(registry) { accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, accessType: 'READ', description: 'Fetches belongsTo relation ' + relationName + '.', - returns: {arg: relationName, type: modelName, root: true} + returns: {arg: relationName, type: modelName, root: true}, + authorization: function(ctx, done) { + var targetSharedMethod = relation.modelTo.sharedClass.find('findOne', true); + relation.modelTo.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, done); + } }, fn); }; @@ -461,6 +473,9 @@ module.exports = function(registry) { Model.hasOneRemoting = function(relationName, relation, define) { var pathName = (relation.options.http && relation.options.http.path) || relationName; var toModelName = relation.modelTo.modelName; + var Model = this; + var registry = Model.registry; + var TargetModel = registry.getModel(toModelName); define('__get__' + relationName, { isStatic: false, @@ -469,7 +484,11 @@ module.exports = function(registry) { description: 'Fetches hasOne relation ' + relationName + '.', accessType: 'READ', returns: {arg: relationName, type: relation.modelTo.modelName, root: true}, - rest: {after: convertNullToNotFoundError.bind(null, toModelName)} + rest: {after: convertNullToNotFoundError.bind(null, toModelName)}, + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('findById', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }); define('__create__' + relationName, { @@ -478,7 +497,11 @@ module.exports = function(registry) { accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, description: 'Creates a new instance in ' + relationName + ' of this model.', accessType: 'WRITE', - returns: {arg: 'data', type: toModelName, root: true} + returns: {arg: 'data', type: toModelName, root: true}, + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('create', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }); define('__update__' + relationName, { @@ -487,20 +510,29 @@ module.exports = function(registry) { accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, description: 'Update ' + relationName + ' of this model.', accessType: 'WRITE', - returns: {arg: 'data', type: toModelName, root: true} + returns: {arg: 'data', type: toModelName, root: true}, + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('update', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }); define('__destroy__' + relationName, { isStatic: false, http: {verb: 'delete', path: '/' + pathName}, description: 'Deletes ' + relationName + ' of this model.', - accessType: 'WRITE' + accessType: 'WRITE', + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('delete', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }); }; Model.hasManyRemoting = function(relationName, relation, define) { var pathName = (relation.options.http && relation.options.http.path) || relationName; var toModelName = relation.modelTo.modelName; + var TargetModel = relation.modelTo; var findByIdFunc = this.prototype['__findById__' + relationName]; define('__findById__' + relationName, { @@ -512,7 +544,11 @@ module.exports = function(registry) { description: 'Find a related item by id for ' + relationName + '.', accessType: 'READ', returns: {arg: 'result', type: toModelName, root: true}, - rest: {after: convertNullToNotFoundError.bind(null, toModelName)} + rest: {after: convertNullToNotFoundError.bind(null, toModelName)}, + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('findById', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }, findByIdFunc); var destroyByIdFunc = this.prototype['__destroyById__' + relationName]; @@ -524,7 +560,11 @@ module.exports = function(registry) { http: {source: 'path'}}, description: 'Delete a related item by id for ' + relationName + '.', accessType: 'WRITE', - returns: [] + returns: [], + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('destroyById', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }, destroyByIdFunc); var updateByIdFunc = this.prototype['__updateById__' + relationName]; @@ -539,7 +579,11 @@ module.exports = function(registry) { ], description: 'Update a related item by id for ' + relationName + '.', accessType: 'WRITE', - returns: {arg: 'result', type: toModelName, root: true} + returns: {arg: 'result', type: toModelName, root: true}, + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('updateById', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }, updateByIdFunc); if (relation.modelThrough || relation.type === 'referencesMany') { @@ -560,7 +604,11 @@ module.exports = function(registry) { http: {source: 'path'}}].concat(accepts), description: 'Add a related item by id for ' + relationName + '.', accessType: 'WRITE', - returns: {arg: relationName, type: modelThrough.modelName, root: true} + returns: {arg: relationName, type: modelThrough.modelName, root: true}, + authorization: function(ctx, next) { + var targetSharedMethod = relation.modelThrough.sharedClass.find('create', true); + relation.modelThrough.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }, addFunc); var removeFunc = this.prototype['__unlink__' + relationName]; @@ -572,7 +620,11 @@ module.exports = function(registry) { http: {source: 'path'}}, description: 'Remove the ' + relationName + ' relation to an item by id.', accessType: 'WRITE', - returns: [] + returns: [], + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('updateById', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }, removeFunc); // FIXME: [rfeng] How to map a function with callback(err, true|false) to HEAD? @@ -602,6 +654,10 @@ module.exports = function(registry) { cb(); } } + }, + authorization: function(ctx, done) { + var targetSharedMethod = TargetModel.sharedClass.find('exists', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); } }, existsFunc); } @@ -613,6 +669,8 @@ module.exports = function(registry) { var isStatic = scope.isStatic; var toModelName = scope.modelTo.modelName; + var registry = this.registry; + var TargetModel = registry.getModel(toModelName); // https://github.com/strongloop/loopback/issues/811 // Check if the scope is for a hasMany relation @@ -629,7 +687,24 @@ module.exports = function(registry) { accepts: {arg: 'filter', type: 'object'}, description: 'Queries ' + scopeName + ' of ' + this.modelName + '.', accessType: 'READ', - returns: {arg: scopeName, type: [toModelName], root: true} + returns: {arg: scopeName, type: [toModelName], root: true}, + authorization: function(ctx, done) { + var modelsToCheck = [toModelName]; + var include = ctx.args.filter && ctx.args.filter.include; + if (include) { + modelsToCheck = modelsToCheck.concat(normalizeInclude(include)); + } + + async.map(modelsToCheck, function(modelName, cb) { + var TargetModel = registry.get(modelName); + var targetSharedMethod = ModelToCheck.sharedClass.find('find', true); + ModelToCheck.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + }, function(err, results) { + if (err) return done(err); + // if false is in the results, the result is false + done(null, results.indexOf(false) === -1); + }); + } }); define('__create__' + scopeName, { @@ -638,14 +713,22 @@ module.exports = function(registry) { accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, description: 'Creates a new instance in ' + scopeName + ' of this model.', accessType: 'WRITE', - returns: {arg: 'data', type: toModelName, root: true} + returns: {arg: 'data', type: toModelName, root: true}, + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('create', true); + ModelToCheck.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }); define('__delete__' + scopeName, { isStatic: isStatic, http: {verb: 'delete', path: '/' + pathName}, description: 'Deletes all ' + scopeName + ' of this model.', - accessType: 'WRITE' + accessType: 'WRITE', + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('delete', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }); define('__count__' + scopeName, { @@ -654,7 +737,11 @@ module.exports = function(registry) { accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, description: 'Counts ' + scopeName + ' of ' + this.modelName + '.', accessType: 'READ', - returns: {arg: 'count', type: 'number'} + returns: {arg: 'count', type: 'number'}, + authorization: function(ctx, next) { + var targetSharedMethod = TargetModel.sharedClass.find('count', true); + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, targetSharedMethod, ctx, cb); + } }); }; @@ -680,6 +767,7 @@ module.exports = function(registry) { var http = [].concat(sharedToClass.http || [])[0]; var httpPath; var acceptArgs; + var TargetModel = relation.modelTo; if (relation.multiple) { httpPath = pathName + '/:' + paramName; @@ -732,6 +820,9 @@ module.exports = function(registry) { opts.accessType = method.accessType; opts.rest = extend({}, method.rest || {}); opts.rest.delegateTo = method; + options.authorization = function(ctx, next) { + TargetModel.checkAccess(ctx.accessToken, ctx.modelId, method, ctx, cb); + } opts.http = []; var routes = [].concat(method.http || []);