diff --git a/lib/models/user.js b/lib/models/user.js index 355a4d7f..e378f6a6 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -52,6 +52,11 @@ var properties = { var options = { acls: [ + { + principalType: ACL.ROLE, + principalId: Role.EVERYONE, + permission: ACL.DENY, + }, { principalType: ACL.ROLE, principalId: Role.EVERYONE, diff --git a/package.json b/package.json index 3edfa8e7..b689ffd0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "mocha": "~1.14.0", "strong-task-emitter": "0.0.x", "supertest": "~0.8.1", - "chai": "~1.8.1" + "chai": "~1.8.1", + "loopback-testing": "0.0.4" }, "repository": { "type": "git", diff --git a/test/access-control.integration.js b/test/access-control.integration.js new file mode 100644 index 00000000..07cd6a5a --- /dev/null +++ b/test/access-control.integration.js @@ -0,0 +1,178 @@ +var loopback = require('loopback'); +var lt = require('loopback-testing'); +var path = require('path'); +var ACCESS_CONTROL_APP = path.join(__dirname, 'fixtures', 'access-control'); +var app = require(path.join(ACCESS_CONTROL_APP, 'app.js')); +var assert = require('assert'); +var USER = {email: 'test@test.test', password: 'test'}; +var CURRENT_USER = {email: 'current@test.test', password: 'test'}; + +describe('access control - integration', function () { + + lt.beforeEach.withApp(app); + + describe('accessToken', function() { + it('should be a sublcass of AccessToken', function () { + assert(app.models.accessToken.prototype instanceof loopback.AccessToken); + }); + + it('should have a validate method', function () { + var token = new app.models.accessToken; + assert.equal(typeof token.validate, 'function'); + }); + }); + + describe('/accessToken', function() { + + lt.beforeEach.givenModel('accessToken', {}, 'randomToken'); + + lt.it.shouldBeAllowedWhenCalledAnonymously('POST', '/api/accessTokens'); + lt.it.shouldBeAllowedWhenCalledUnauthenticated('POST', '/api/accessTokens'); + lt.it.shouldBeAllowedWhenCalledByUser(USER, 'POST', '/api/accessTokens'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accessTokens'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accessTokens'); + lt.it.shouldBeDeniedWhenCalledByUser(USER, 'GET', '/api/accessTokens'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', '/api/accessTokens'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', '/api/accessTokens'); + lt.it.shouldBeDeniedWhenCalledByUser(USER, 'PUT', '/api/accessTokens'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForToken); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForToken); + lt.it.shouldBeDeniedWhenCalledByUser(USER, 'GET', urlForToken); + + lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForToken); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForToken); + lt.it.shouldBeDeniedWhenCalledByUser(USER, 'PUT', urlForToken); + + lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForToken); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForToken); + lt.it.shouldBeDeniedWhenCalledByUser(USER, 'DELETE', urlForToken); + + function urlForToken() { + return '/api/accessTokens/' + this.randomToken.id; + } + }); + + describe('/users', function () { + + lt.beforeEach.givenModel('user', USER, 'randomUser'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/users'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/users'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/users'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForUser); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForUser); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER,'GET', urlForUser); + + lt.it.shouldBeAllowedWhenCalledAnonymously('POST', '/api/users'); + lt.it.shouldBeAllowedWhenCalledUnauthenticated('POST', '/api/users'); + lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users'); + + lt.describe.whenCalledRemotely('DELETE', '/api/users', function() { + lt.it.shouldNotBeFound(); + }); + + lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForUser); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForUser); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForUser); + + lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForUser); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForUser); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForUser); + + lt.describe.whenLoggedInAsUser(CURRENT_USER, function() { + beforeEach(function() { + this.url = '/api/users/' + this.user.id + '?ok'; + }); + lt.describe.whenCalledRemotely('DELETE', '/api/users/:id', function() { + lt.it.shouldBeAllowed(); + }); + lt.describe.whenCalledRemotely('GET', '/api/users/:id', function() { + lt.it.shouldBeAllowed(); + }); + lt.describe.whenCalledRemotely('PUT', '/api/users/:id', function() { + lt.it.shouldBeAllowed(); + }); + }); + + lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForUser); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForUser); + + function urlForUser() { + return '/api/users/' + this.randomUser.id; + } + }); + + describe('/banks', function () { + lt.beforeEach.givenModel('bank'); + + lt.it.shouldBeAllowedWhenCalledAnonymously('GET', '/api/banks'); + lt.it.shouldBeAllowedWhenCalledUnauthenticated('GET', '/api/banks'); + lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'GET', '/api/banks'); + + lt.it.shouldBeAllowedWhenCalledAnonymously('GET', urlForBank); + lt.it.shouldBeAllowedWhenCalledUnauthenticated('GET', urlForBank); + lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'GET', urlForBank); + + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/banks'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/banks'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/banks'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForBank); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForBank); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForBank); + + lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForBank); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForBank); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForBank); + + function urlForBank() { + return '/api/banks/' + this.bank.id; + } + }); + + describe('/accounts', function () { + lt.beforeEach.givenModel('account'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount); + 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'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount); + + lt.describe.whenLoggedInAsUser(CURRENT_USER, function() { + beforeEach(function() { + this.url = '/api/accounts/' + this.user.accountId; + }); + lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() { + lt.it.shouldBeAllowed(); + }); + lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() { + lt.it.shouldBeDenied(); + }); + }); + + lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForAccount); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount); + + function urlForAccount() { + return '/api/accounts/' + this.account.id; + } + }); + +}); diff --git a/test/acl.test.js b/test/acl.test.js index 7df37a3f..a7dd15ee 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -7,6 +7,7 @@ var role = require('../lib/models/role'); var Role = role.Role; var RoleMapping = role.RoleMapping; var User = loopback.User; +var testModel; function checkResult(err, result) { // console.log(err, result); @@ -15,6 +16,17 @@ function checkResult(err, result) { describe('security scopes', function () { + beforeEach(function() { + var ds = this.ds = loopback.createDataSource({connector: loopback.Memory}); + testModel = loopback.Model.extend('testModel'); + ACL.attachTo(ds); + Role.attachTo(ds); + RoleMapping.attachTo(ds); + User.attachTo(ds); + Scope.attachTo(ds); + testModel.attachTo(ds); + }); + it("should allow access to models for the given scope by wildcard", function () { Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) { ACL.create({principalType: ACL.SCOPE, principalId: scope.id, model: 'User', property: ACL.ALL, @@ -29,27 +41,24 @@ describe('security scopes', function () { }); it("should allow access to models for the given scope", function () { - var ds = loopback.createDataSource({connector: loopback.Memory}); - Scope.attachTo(ds); - ACL.attachTo(ds); - Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) { + Scope.create({name: 'testModelScope', description: 'access testModel information'}, function (err, scope) { ACL.create({principalType: ACL.SCOPE, principalId: scope.id, - model: 'User', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW}, + model: 'testModel', 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}, + model: 'testModel', property: 'name', accessType: ACL.WRITE, permission: ACL.DENY}, function (err, resource) { // console.log(resource); - Scope.checkPermission('userScope', 'User', ACL.ALL, ACL.ALL, function (err, perm) { + Scope.checkPermission('testModelScope', 'testModel', ACL.ALL, ACL.ALL, function (err, perm) { assert(perm.permission === ACL.DENY); // because name.WRITE == DENY }); - Scope.checkPermission('userScope', 'User', 'name', ACL.ALL, function (err, perm) { + Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.ALL, function (err, perm) { assert(perm.permission === ACL.DENY); // because name.WRITE == DENY }); - Scope.checkPermission('userScope', 'User', 'name', ACL.READ, function (err, perm) { + Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.READ, function (err, perm) { assert(perm.permission === ACL.ALLOW); }); - Scope.checkPermission('userScope', 'User', 'name', ACL.WRITE, function (err, perm) { + Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.WRITE, function (err, perm) { assert(perm.permission === ACL.DENY); }); }); @@ -63,9 +72,6 @@ describe('security scopes', function () { 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: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) { @@ -87,28 +93,25 @@ describe('security ACLs', function () { }); it("should allow access to models by exception", function () { - var ds = loopback.createDataSource({connector: loopback.Memory}); - ACL.attachTo(ds); - - ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL, + ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'testModel', property: ACL.ALL, accessType: ACL.ALL, permission: ACL.DENY}, function (err, acl) { - ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL, + ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'testModel', property: ACL.ALL, accessType: ACL.READ, permission: ACL.ALLOW}, function (err, acl) { - ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.READ, function (err, perm) { + ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.READ, function (err, perm) { assert(perm.permission === ACL.ALLOW); }); - ACL.checkPermission(ACL.USER, 'u001', 'User', ACL.ALL, ACL.READ, function (err, perm) { + ACL.checkPermission(ACL.USER, 'u001', 'testModel', ACL.ALL, ACL.READ, function (err, perm) { assert(perm.permission === ACL.ALLOW); }); - ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.WRITE, function (err, perm) { + ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.WRITE, function (err, perm) { assert(perm.permission === ACL.DENY); }); - ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.ALL, function (err, perm) { + ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.ALL, function (err, perm) { assert(perm.permission === ACL.DENY); }); @@ -119,8 +122,7 @@ describe('security ACLs', function () { }); it("should honor defaultPermission from the model", function () { - var ds = loopback.createDataSource({connector: loopback.Memory}); - ACL.attachTo(ds); + var ds = this.ds; var Customer = ds.createModel('Customer', { name: { type: String, @@ -152,7 +154,7 @@ describe('security ACLs', function () { }); it("should honor static ACLs from the model", function () { - var ds = loopback.createDataSource({connector: loopback.Memory}); + var ds = this.ds; var Customer = ds.createModel('Customer', { name: { type: String, @@ -188,14 +190,9 @@ describe('security ACLs', function () { }); it("should check access against LDL, ACL, and Role", function () { - var ds = loopback.createDataSource({connector: loopback.Memory}); - ACL.attachTo(ds); - Role.attachTo(ds); - RoleMapping.attachTo(ds); - User.attachTo(ds); - // var log = console.log; var log = function() {}; + var ds = this.ds; // Create User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) { @@ -246,21 +243,7 @@ describe('security ACLs', function () { }, function(err, access) { assert(!err && access.permission === ACL.ALLOW); }); - - /* - ACL.checkAccess({ - principals: [ - {principalType: ACL.USER, principalId: userId} - ], - model: 'Customer', - accessType: ACL.READ - }, function(err, access) { - assert(!err && access.permission === ACL.DENY); - }); - */ - }); - }); }); }); diff --git a/test/fixtures/access-control/app.js b/test/fixtures/access-control/app.js new file mode 100644 index 00000000..cb47188c --- /dev/null +++ b/test/fixtures/access-control/app.js @@ -0,0 +1,14 @@ +var loopback = require('loopback'); +var path = require('path'); +var app = module.exports = loopback(); + +app.boot(__dirname); + +var apiPath = '/api'; +app.use(loopback.cookieParser('secret')); +app.use(loopback.token({model: app.models.accessToken})); +app.use(apiPath, loopback.rest()); +app.use(app.router); +app.use(loopback.urlNotFound()); +app.use(loopback.errorHandler()); +app.enableAuth(); diff --git a/test/fixtures/access-control/app.json b/test/fixtures/access-control/app.json new file mode 100644 index 00000000..ffc18ffd --- /dev/null +++ b/test/fixtures/access-control/app.json @@ -0,0 +1,4 @@ +{ + "port": 3000, + "host": "0.0.0.0" +} \ No newline at end of file diff --git a/test/fixtures/access-control/datasources.json b/test/fixtures/access-control/datasources.json new file mode 100644 index 00000000..9f97a8d3 --- /dev/null +++ b/test/fixtures/access-control/datasources.json @@ -0,0 +1,10 @@ +{ + "db": { + "defaultForType": "db", + "connector": "memory" + }, + "mail": { + "defaultForType": "mail", + "connector": "mail" + } +} \ No newline at end of file diff --git a/test/fixtures/access-control/models.json b/test/fixtures/access-control/models.json new file mode 100644 index 00000000..f814353a --- /dev/null +++ b/test/fixtures/access-control/models.json @@ -0,0 +1,163 @@ +{ + "email": { + "options": { + "base": "Email", + "acls": [ + { + "accessType": "*", + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$everyone" + } + ] + }, + "dataSource": "mail", + "public": false + }, + "user": { + "options": { + "base": "User", + "relations": { + "accessTokens": { + "model": "accessToken", + "type": "hasMany", + "foreignKey": "userId" + }, + "account": { + "model": "account", + "type": "belongsTo" + }, + "transactions": { + "model": "transaction", + "type": "hasMany" + } + }, + "acls": [ + { + "accessType": "*", + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$everyone" + } + ] + }, + "dataSource": "db", + "public": true + }, + "accessToken": { + "options": { + "base": "AccessToken", + "baseUrl": "access-tokens", + "acls": [ + { + "accessType": "*", + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$everyone" + }, + { + "permission": "ALLOW", + "principalType": "ROLE", + "principalId": "$everyone", + "property": "create" + } + ] + }, + "dataSource": "db", + "public": true + }, + "bank": { + "options": { + "relations": { + "users": { + "model": "user", + "type": "hasMany" + }, + "accounts": { + "model": "account", + "type": "hasMany" + } + }, + "acls": [ + { + "accessType": "*", + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$everyone" + }, + { + "accessType": "READ", + "permission": "ALLOW", + "principalType": "ROLE", + "principalId": "$everyone" + } + ] + }, + "properties": {}, + "public": true, + "dataSource": "db" + }, + "account": { + "options": { + "relations": { + "transactions": { + "model": "transaction", + "type": "hasMany" + } + }, + "acls": [ + { + "accessType": "*", + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$everyone" + }, + { + "accessType": "*", + "permission": "ALLOW", + "principalType": "ROLE", + "principalId": "$owner" + }, + { + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$owner", + "property": "removeById" + } + ] + }, + "properties": {}, + "public": true, + "dataSource": "db" + }, + "transaction": { + "options": { + "acls": [ + { + "accessType": "*", + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$everyone" + } + ] + }, + "properties": {}, + "public": true, + "dataSource": "db" + }, + "alert": { + "options": { + "acls": [ + { + "accessType": "WRITE", + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$everyone" + } + ] + }, + "properties": {}, + "public": true, + "dataSource": "db" + } +}