diff --git a/lib/application.js b/lib/application.js index fdb56884..6fda62f3 100644 --- a/lib/application.js +++ b/lib/application.js @@ -323,6 +323,7 @@ app.enableAuth = function() { req.accessToken, modelId, method, + ctx, function(err, allowed) { // Emit any cached data events that fired while checking access. req.resume(); diff --git a/lib/models/access-context.js b/lib/models/access-context.js index 681c366c..2bf03733 100644 --- a/lib/models/access-context.js +++ b/lib/models/access-context.js @@ -66,6 +66,7 @@ function AccessContext(context) { if (token.appId) { this.addPrincipal(Principal.APPLICATION, token.appId); } + this.remotingContext = context.remotingContext; } // Define constant for the wildcard diff --git a/lib/models/model.js b/lib/models/model.js index ae079e79..99d899b4 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -185,10 +185,9 @@ Model.setup = function () { // resolve relation functions sharedClass.resolve(function resolver(define) { - var relations = ModelCtor.relations; - if (!relations) { - return; - } + + var relations = ModelCtor.relations || {}; + // get the relations for (var relationName in relations) { var relation = relations[relationName]; @@ -201,13 +200,16 @@ Model.setup = function () { relation.type === 'embedsMany' || relation.type === 'referencesMany') { ModelCtor.hasManyRemoting(relationName, relation, define); - ModelCtor.scopeRemoting(relationName, relation, define); - } else { - ModelCtor.scopeRemoting(relationName, relation, define); } } + + // handle scopes + var scopes = ModelCtor.scopes || {}; + for (var scopeName in scopes) { + ModelCtor.scopeRemoting(scopeName, scopes[scopeName], define); + } }); - + return ModelCtor; }; @@ -234,15 +236,22 @@ Model._ACL = function getACL(ACL) { * @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 {Boolean} allowed True if the request is allowed; false otherwise. */ -Model.checkAccess = function(token, modelId, sharedMethod, callback) { +Model.checkAccess = function(token, modelId, sharedMethod, ctx, callback) { var ANONYMOUS = require('./access-token').ANONYMOUS; token = token || ANONYMOUS; var aclModel = Model._ACL(); + + ctx = ctx || {}; + if(typeof ctx === 'function' && callback === undefined) { + callback = ctx; + ctx = {}; + } aclModel.checkAccessForContext({ accessToken: token, @@ -251,7 +260,8 @@ Model.checkAccess = function(token, modelId, sharedMethod, callback) { method: sharedMethod.name, sharedMethod: sharedMethod, modelId: modelId, - accessType: this._getAccessTypeForMethod(sharedMethod) + accessType: this._getAccessTypeForMethod(sharedMethod), + remotingContext: ctx }, function(err, accessRequest) { if(err) return callback(err); callback(null, accessRequest.isAllowed()); @@ -346,6 +356,8 @@ Model.remoteMethod = function(name, options) { } Model.belongsToRemoting = function(relationName, relation, define) { + var modelName = relation.modelTo && relation.modelTo.modelName; + modelName = modelName || 'PersistedModel'; var fn = this.prototype[relationName]; var pathName = (relation.options.http && relation.options.http.path) || relationName; define('__get__' + relationName, { @@ -353,7 +365,7 @@ Model.belongsToRemoting = function(relationName, relation, define) { http: {verb: 'get', path: '/' + pathName}, accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, description: 'Fetches belongsTo relation ' + relationName, - returns: {arg: relationName, type: relation.modelTo.modelName, root: true} + returns: {arg: relationName, type: modelName, root: true} }, fn); } @@ -464,30 +476,32 @@ Model.hasManyRemoting = function (relationName, relation, define) { } }; -Model.scopeRemoting = function(relationName, relation, define) { - var pathName = (relation.options.http && relation.options.http.path) || relationName; - var toModelName = relation.modelTo.modelName; +Model.scopeRemoting = function(scopeName, scope, define) { + var pathName = (scope.options && scope.options.http && scope.options.http.path) + || scopeName; + var isStatic = scope.isStatic; + var toModelName = scope.modelTo.modelName; - define('__get__' + relationName, { - isStatic: false, + define('__get__' + scopeName, { + isStatic: isStatic, http: {verb: 'get', path: '/' + pathName}, accepts: {arg: 'filter', type: 'object'}, - description: 'Queries ' + relationName + ' of ' + this.modelName + '.', - returns: {arg: relationName, type: [toModelName], root: true} + description: 'Queries ' + scopeName + ' of ' + this.modelName + '.', + returns: {arg: scopeName, type: [toModelName], root: true} }); - define('__create__' + relationName, { - isStatic: false, + define('__create__' + scopeName, { + isStatic: isStatic, http: {verb: 'post', path: '/' + pathName}, accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, - description: 'Creates a new instance in ' + relationName + ' of this model.', + description: 'Creates a new instance in ' + scopeName + ' of this model.', returns: {arg: 'data', type: toModelName, root: true} }); - define('__delete__' + relationName, { - isStatic: false, + define('__delete__' + scopeName, { + isStatic: isStatic, http: {verb: 'delete', path: '/' + pathName}, - description: 'Deletes all ' + relationName + ' of this model.' + description: 'Deletes all ' + scopeName + ' of this model.' }); }; diff --git a/lib/models/user.js b/lib/models/user.js index 09e3281a..c43d557f 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -289,7 +289,7 @@ User.prototype.verify = function (options, fn) { options.user = this; options.protocol = options.protocol || 'http'; - var app = this.app; + 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'; diff --git a/package.json b/package.json index 7446a950..037be4c2 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "mobile", "mBaaS" ], - "version": "2.1.0", + "version": "2.1.1", "scripts": { "test": "grunt mocha-and-karma" }, diff --git a/test/access-control.integration.js b/test/access-control.integration.js index 575baebd..28e48031 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -160,6 +160,19 @@ describe('access control - integration', function () { }); describe('/accounts', function () { + var count = 0; + before(function() { + var roleModel = loopback.getModelByType(loopback.Role); + roleModel.registerResolver('$dummy', function (role, context, callback) { + process.nextTick(function () { + if(context.remotingContext) { + count++; + } + callback && callback(null, false); // Always true + }); + }); + }); + lt.beforeEach.givenModel('account'); lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts'); @@ -170,7 +183,6 @@ describe('access control - integration', function () { lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount); - lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts'); lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts'); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts'); diff --git a/test/fixtures/access-control/models.json b/test/fixtures/access-control/models.json index a1385443..e6b1c292 100644 --- a/test/fixtures/access-control/models.json +++ b/test/fixtures/access-control/models.json @@ -124,6 +124,13 @@ "principalType": "ROLE", "principalId": "$owner", "property": "deleteById" + }, + { + "accessType": "*", + "permission": "DENY", + "property": "find", + "principalType": "ROLE", + "principalId": "$dummy" } ] }, diff --git a/test/fixtures/simple-integration-app/models.json b/test/fixtures/simple-integration-app/models.json index 41acde0c..6f4e7535 100644 --- a/test/fixtures/simple-integration-app/models.json +++ b/test/fixtures/simple-integration-app/models.json @@ -45,6 +45,13 @@ "public": true, "dataSource": "db", "options": { + "scopes": { + "superStores": { + "where": { + "size": "super" + } + } + }, "relations": { "widgets": { "model": "widget", diff --git a/test/relations.integration.js b/test/relations.integration.js index 92e4e9bb..c0e2d28c 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -25,6 +25,16 @@ describe('relations - integration', function () { this.app.models.widget.destroyAll(done); }); + describe('/store/superStores', function() { + it('should invoke scoped methods remotely', function(done) { + this.get('/api/stores/superStores') + .expect(200, function(err, res) { + expect(res.body).to.be.array; + done(); + }); + }); + }); + describe('/store/:id/widgets', function () { beforeEach(function() { this.url = '/api/stores/' + this.store.id + '/widgets';