From 2f9403016cb30cabdc24fd24d0c243a1bc506813 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Thu, 14 Nov 2013 20:19:46 -0800 Subject: [PATCH] Initial auth implementation --- lib/application.js | 65 ++++++++++++++++++++++++++++---------- lib/loopback.js | 1 - lib/models/access-token.js | 3 +- lib/models/acl.js | 6 +++- test/access-token.test.js | 39 +++++++++++++++++++++++ test/user.test.js | 4 +-- 6 files changed, 95 insertions(+), 23 deletions(-) diff --git a/lib/application.js b/lib/application.js index 8a93d8ff..347004db 100644 --- a/lib/application.js +++ b/lib/application.js @@ -43,12 +43,6 @@ app.disuse = function (route) { } } -/** - * App models. - */ - -app._models = []; - /** * Expose a model. * @@ -60,7 +54,7 @@ app.model = function (Model, config) { assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor'); assert(Model.pluralModelName, 'Model must have a "pluralModelName" property'); this.remotes().exports[Model.pluralModelName] = Model; - this._models.push(Model); + this.models().push(Model); Model.shared = true; Model.app = this; Model.emit('attached', this); @@ -82,20 +76,12 @@ app.model = function (Model, config) { return Model; } -/** - * Get a Model by name. - */ - -app.getModel = function (modelName) { - this.models -}; - /** * Get all exposed models. */ app.models = function () { - return this._models; + return this._models || (this._models = []); } /** @@ -144,7 +130,6 @@ app.docs = function (options) { swagger(remotes, options); } - /*! * Get a handler of the specified type from the handler cache. */ @@ -166,6 +151,52 @@ app.handler = function (type) { app.dataSources = app.datasources = {}; +/** + * Enable app wide authentication. + */ + +app.enableAuth = function() { + var remotes = this.remotes(); + + remotes.before('**', function(ctx, next, method) { + var req = ctx.req; + var Model = method.ctor; + var modelInstance = ctx.instance; + var modelId = modelInstance && modelInstance.id; + + // TODO(ritch) - this fallback could be less express dependent + if(modelInstance && !modelId) { + modelId = req.param('id'); + } + + if(req.accessToken) { + Model.checkAccess( + req.accessToken, + modelId, + method.name, + function(err, allowed) { + if(err) { + next(err); + } else if(allowed) { + next(); + } else { + var e = new Error('Access Denied'); + e.statusCode = 401; + next(e); + } + } + ); + } else if(method.fn && method.fn.requireToken === false) { + next(); + } else { + var e = new Error('Access Denied'); + e.statusCode = 401; + + next(e); + } + }); +} + /** * Initialize the app using JSON and JavaScript files. * diff --git a/lib/loopback.js b/lib/loopback.js index a8b77c42..e519b05b 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -252,7 +252,6 @@ loopback.RoleMapping = require('./models/role').RoleMapping; loopback.ACL = require('./models/acl').ACL; loopback.Scope = require('./models/acl').Scope; - /** * Automatically attach these models to dataSources */ diff --git a/lib/models/access-token.js b/lib/models/access-token.js index ec2f0187..e15e2f17 100644 --- a/lib/models/access-token.js +++ b/lib/models/access-token.js @@ -8,7 +8,8 @@ var Model = require('../loopback').Model , crypto = require('crypto') , uid = require('uid2') , DEFAULT_TTL = 1209600 // 2 weeks in seconds - , DEFAULT_TOKEN_LEN = 64; + , DEFAULT_TOKEN_LEN = 64 + , ACL = require('./acl').ACL; /** * Default AccessToken properties. diff --git a/lib/models/acl.js b/lib/models/acl.js index d2b99be4..3c6661f4 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -274,6 +274,10 @@ Scope.checkPermission = function (scope, model, property, accessType, callback) ACL.checkAccess = function (context, callback) { context = context || {}; var principals = context.principals || []; + + // add ROLE.EVERYONE + principals.unshift({principalType: ACL.ROLE, principalId: Role.EVERYONE}); + var model = context.model; model = ('string' === typeof model) ? loopback.getModel(model) : model; var id = context.id; @@ -369,7 +373,7 @@ ACL.checkAccessForToken = function(token, model, modelId, method, callback) { callback && callback(err); return; } - callback && callback(access.permission !== ACL.DENY); + callback && callback(null, access.permission !== ACL.DENY); }); }; diff --git a/test/access-token.test.js b/test/access-token.test.js index ae48721f..de497fde 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -1,5 +1,6 @@ var loopback = require('../'); var Token = loopback.AccessToken.extend('MyToken'); +var ACL = loopback.ACL; describe('loopback.token(options)', function() { beforeEach(createTestingToken); @@ -54,6 +55,27 @@ describe('AccessToken', function () { }); }); +describe('app.enableAuth()', function() { + this.timeout(0); + + beforeEach(createTestingToken); + + it('should prevent all remote method calls without an accessToken', function (done) { + createTestAppAndRequest(this.token, done) + .get('/tests') + .expect(401) + .end(done); + }); + + it('should prevent remote method calls if the accessToken doesnt have access', function (done) { + createTestAppAndRequest(this.token, done) + .del('/tests/123') + .expect(401) + .set('authorization', this.token.id) + .end(done); + }); +}); + function createTestingToken(done) { var test = this; Token.create({}, function (err, token) { @@ -86,6 +108,23 @@ function createTestApp(testToken, done) { } res.send('ok'); }); + app.use(loopback.rest()); + app.enableAuth(); + + var TestModel = loopback.Model.extend('test', {}, { + acls: [ + { + principalType: "ROLE", + principalId: "$everyone", + accessType: ACL.ALL, + permission: ACL.DENY, + property: 'removeById' + } + ] + }); + + TestModel.attachTo(loopback.memory()); + app.model(TestModel); return app; } diff --git a/test/user.test.js b/test/user.test.js index 26b3ef90..ce0b8b21 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -12,7 +12,6 @@ describe('User', function(){ User.setMaxListeners(0); before(function () { - debugger; User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'}); }); @@ -257,8 +256,7 @@ describe('User', function(){ assert(result.token); - var lines = result.email.message.split('\n'); - assert(lines[3].indexOf('To: bar@bat.com') === 0); + assert(~result.email.message.indexOf('To: bar@bat.com')); done(); }); });