diff --git a/lib/gatekeeper/middleware/api_settings.js b/lib/gatekeeper/middleware/api_settings.js index e1a30e46a..3729302a0 100644 --- a/lib/gatekeeper/middleware/api_settings.js +++ b/lib/gatekeeper/middleware/api_settings.js @@ -26,9 +26,24 @@ _.extend(ApiSettings.prototype, { var subSettings = api.sub_settings[i]; if(subSettings.http_method === 'any' || subSettings.http_method === request.method) { if(subSettings.regex.test(request.url)) { + var originalRequiredRoles; + if(!subSettings.settings.required_roles_override) { + originalRequiredRoles = settings.required_roles; + } + // Merge the matching sub-settings in. mergeOverwriteArrays(settings, subSettings.settings); + if(!subSettings.settings.required_roles_override) { + if(originalRequiredRoles) { + settings.required_roles = _.uniq((settings.required_roles || []).concat(originalRequiredRoles)); + } + } else { + if(!subSettings.settings.required_roles) { + settings.required_roles = []; + } + } + // We've deep-merged the root settings and the sub-settings // together, but cached attributes are a special case, where we // want to perform a non-deep merge. diff --git a/lib/gatekeeper/middleware/role_validator.js b/lib/gatekeeper/middleware/role_validator.js index 32594eed5..62129d784 100644 --- a/lib/gatekeeper/middleware/role_validator.js +++ b/lib/gatekeeper/middleware/role_validator.js @@ -12,21 +12,20 @@ _.extend(RoleValidator.prototype, { }, handleRequest: function(request, response, next) { - var requiredRoles = request.apiUmbrellaGatekeeper.settings.required_roles; - var authenticated = true; + var requiredRoles = request.apiUmbrellaGatekeeper.settings.required_roles; if(requiredRoles && requiredRoles.length > 0) { authenticated = false; - var userRoles = request.apiUmbrellaGatekeeper.user.roles; - if(userRoles && userRoles.length > 0) { - if(userRoles.indexOf('admin') !== -1) { - authenticated = true; - } else { + if(request.apiUmbrellaGatekeeper.user) { + var userRoles = request.apiUmbrellaGatekeeper.user.roles; + if(userRoles && userRoles.length > 0) { for(var i = 0, len = requiredRoles.length; i < len; i++) { - if(userRoles.indexOf(requiredRoles[i]) !== -1) { - authenticated = true; + if(userRoles.indexOf(requiredRoles[i]) === -1) { + authenticated = false; break; + } else { + authenticated = true; } } } diff --git a/test/server/role_validation.js b/test/server/role_validation.js index 57b92b41f..180b0ce93 100644 --- a/test/server/role_validation.js +++ b/test/server/role_validation.js @@ -7,6 +7,39 @@ var Factory = require('factory-lady'); describe('ApiUmbrellaGatekeper', function() { shared.runServer({ apis: [ + { + frontend_host: 'localhost', + backend_host: 'example.com', + url_matches: [ + { + frontend_prefix: '/info/no-key/', + backend_prefix: '/info/no-key/', + } + ], + settings: { + disable_api_key: true, + required_roles: ['restricted'], + }, + }, + { + frontend_host: 'localhost', + backend_host: 'example.com', + url_matches: [ + { + frontend_prefix: '/info/no-parent-roles/', + backend_prefix: '/info/no-parent-roles/', + } + ], + sub_settings: [ + { + http_method: 'any', + regex: '^/info/no-parent-roles/sub/', + settings: { + required_roles: ['sub'], + }, + }, + ], + }, { frontend_host: 'localhost', backend_host: 'example.com', @@ -22,9 +55,75 @@ describe('ApiUmbrellaGatekeper', function() { sub_settings: [ { http_method: 'any', - regex: '^/info/sub', + regex: '^/info/sub/', + settings: { + required_roles: ['sub'], + }, + }, + { + http_method: 'any', + regex: '^/info/sub-null-roles/', + settings: { + required_roles: null, + }, + }, + { + http_method: 'any', + regex: '^/info/sub-empty-roles/', + settings: { + required_roles: [], + }, + }, + { + http_method: 'any', + regex: '^/info/sub-unset-roles/', + settings: { + }, + }, + { + http_method: 'any', + regex: '^/info/sub-override-true/', + settings: { + required_roles: ['sub'], + required_roles_override: true, + }, + }, + { + http_method: 'any', + regex: '^/info/sub-override-false/', settings: { required_roles: ['sub'], + required_roles_override: false, + }, + }, + { + http_method: 'any', + regex: '^/info/sub-null-roles-override/', + settings: { + required_roles: null, + required_roles_override: true, + }, + }, + { + http_method: 'any', + regex: '^/info/sub-empty-roles-override/', + settings: { + required_roles: [], + required_roles_override: true, + }, + }, + { + http_method: 'any', + regex: '^/info/sub-unset-roles-override/', + settings: { + required_roles_override: true, + }, + }, + { + http_method: 'any', + regex: '^/info/sub-no-key-required/', + settings: { + disable_api_key: true, }, }, ], @@ -56,7 +155,7 @@ describe('ApiUmbrellaGatekeper', function() { describe('unauthorized api_key with empty roles', function() { beforeEach(function setupApiUser(done) { - Factory.create('api_user', { roles: null }, function(user) { + Factory.create('api_user', { roles: [] }, function(user) { this.apiKey = user.api_key; done(); }.bind(this)); @@ -76,7 +175,7 @@ describe('ApiUmbrellaGatekeper', function() { shared.itBehavesLikeGatekeeperBlocked('/info/', 403, 'API_KEY_UNAUTHORIZED'); }); - describe('authorized api_key with one of the appropriate role', function() { + describe('unauthorized api_key with only one of the required roles', function() { beforeEach(function setupApiUser(done) { Factory.create('api_user', { roles: ['private'] }, function(user) { this.apiKey = user.api_key; @@ -84,10 +183,21 @@ describe('ApiUmbrellaGatekeper', function() { }.bind(this)); }); + shared.itBehavesLikeGatekeeperBlocked('/info/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('authorized api_key with all of the required role', function() { + beforeEach(function setupApiUser(done) { + Factory.create('api_user', { roles: ['restricted', 'private'] }, function(user) { + this.apiKey = user.api_key; + done(); + }.bind(this)); + }); + shared.itBehavesLikeGatekeeperAllowed('/info/'); }); - describe('api_key with admin roles is authorized automatically', function() { + describe('api_key with admin roles is not authorized automatically', function() { beforeEach(function setupApiUser(done) { Factory.create('api_user', { roles: ['admin'] }, function(user) { this.apiKey = user.api_key; @@ -95,22 +205,22 @@ describe('ApiUmbrellaGatekeper', function() { }.bind(this)); }); - shared.itBehavesLikeGatekeeperAllowed('/info/'); + shared.itBehavesLikeGatekeeperBlocked('/info/', 403, 'API_KEY_UNAUTHORIZED'); }); - describe('sub-url with different role requirements', function() { - describe('unauthorized api_key with other roles', function() { + describe('sub-url with additional role requirements', function() { + describe('unauthorized api_key with only the parent roles', function() { beforeEach(function setupApiUser(done) { - Factory.create('api_user', { roles: ['restricted'] }, function(user) { + Factory.create('api_user', { roles: ['private', 'restricted'] }, function(user) { this.apiKey = user.api_key; done(); }.bind(this)); }); - shared.itBehavesLikeGatekeeperBlocked('/info/sub', 403, 'API_KEY_UNAUTHORIZED'); + shared.itBehavesLikeGatekeeperBlocked('/info/sub/', 403, 'API_KEY_UNAUTHORIZED'); }); - describe('authorized api_key with the appropriate role', function() { + describe('unauthorized api_key with only the sub role', function() { beforeEach(function setupApiUser(done) { Factory.create('api_user', { roles: ['sub'] }, function(user) { this.apiKey = user.api_key; @@ -118,7 +228,156 @@ describe('ApiUmbrellaGatekeper', function() { }.bind(this)); }); - shared.itBehavesLikeGatekeeperAllowed('/info/sub'); + shared.itBehavesLikeGatekeeperBlocked('/info/sub/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('authorized api_key with all the parent and sub roles', function() { + beforeEach(function setupApiUser(done) { + Factory.create('api_user', { roles: ['sub', 'private', 'restricted'] }, function(user) { + this.apiKey = user.api_key; + done(); + }.bind(this)); + }); + + shared.itBehavesLikeGatekeeperAllowed('/info/sub/'); + }); + }); + + describe('sub-url with null role requirements', function() { + describe('unauthorized api_key with no roles', function() { + shared.itBehavesLikeGatekeeperBlocked('/info/sub-null-roles/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('authorized api_key with all the parent roles', function() { + beforeEach(function setupApiUser(done) { + Factory.create('api_user', { roles: ['private', 'restricted'] }, function(user) { + this.apiKey = user.api_key; + done(); + }.bind(this)); + }); + + shared.itBehavesLikeGatekeeperAllowed('/info/sub-null-roles/'); + }); + }); + + describe('sub-url with empty role requirements', function() { + describe('unauthorized api_key with no roles', function() { + shared.itBehavesLikeGatekeeperBlocked('/info/sub-empty-roles/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('authorized api_key with all the parent roles', function() { + beforeEach(function setupApiUser(done) { + Factory.create('api_user', { roles: ['private', 'restricted'] }, function(user) { + this.apiKey = user.api_key; + done(); + }.bind(this)); + }); + + shared.itBehavesLikeGatekeeperAllowed('/info/sub-empty-roles/'); + }); + }); + + describe('sub-url with unset role requirements', function() { + describe('unauthorized api_key with no roles', function() { + shared.itBehavesLikeGatekeeperBlocked('/info/sub-unset-roles/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('authorized api_key with all the parent roles', function() { + beforeEach(function setupApiUser(done) { + Factory.create('api_user', { roles: ['private', 'restricted'] }, function(user) { + this.apiKey = user.api_key; + done(); + }.bind(this)); + }); + + shared.itBehavesLikeGatekeeperAllowed('/info/sub-unset-roles/'); + }); + }); + + describe('sub-url with overriding role requirements', function() { + describe('unauthorized api_key with no roles', function() { + shared.itBehavesLikeGatekeeperBlocked('/info/sub-override-true/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('authorized api_key with only the sub role', function() { + beforeEach(function setupApiUser(done) { + Factory.create('api_user', { roles: ['sub'] }, function(user) { + this.apiKey = user.api_key; + done(); + }.bind(this)); + }); + + shared.itBehavesLikeGatekeeperAllowed('/info/sub-override-true/'); + }); + }); + + describe('sub-url with overriding explicitly false role requirements', function() { + describe('unauthorized api_key with only the parent roles', function() { + beforeEach(function setupApiUser(done) { + Factory.create('api_user', { roles: ['private', 'restricted'] }, function(user) { + this.apiKey = user.api_key; + done(); + }.bind(this)); + }); + + shared.itBehavesLikeGatekeeperBlocked('/info/sub-override-false/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('unauthorized api_key with only the sub role', function() { + beforeEach(function setupApiUser(done) { + Factory.create('api_user', { roles: ['sub'] }, function(user) { + this.apiKey = user.api_key; + done(); + }.bind(this)); + }); + + shared.itBehavesLikeGatekeeperBlocked('/info/sub-override-false/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('authorized api_key with all the parent and sub roles', function() { + beforeEach(function setupApiUser(done) { + Factory.create('api_user', { roles: ['sub', 'private', 'restricted'] }, function(user) { + this.apiKey = user.api_key; + done(); + }.bind(this)); + }); + + shared.itBehavesLikeGatekeeperAllowed('/info/sub-override-false/'); + }); + }); + + describe('sub-url with overriding null role requirements', function() { + describe('authorized api_key with no roles', function() { + shared.itBehavesLikeGatekeeperAllowed('/info/sub-null-roles-override/'); + }); + }); + + describe('sub-url with overriding empty role requirements', function() { + describe('authorized api_key with no roles', function() { + shared.itBehavesLikeGatekeeperAllowed('/info/sub-empty-roles-override/'); + }); + }); + + describe('sub-url with overriding unset role requirements', function() { + describe('authorized api_key with no roles', function() { + shared.itBehavesLikeGatekeeperAllowed('/info/sub-unset-roles-override/'); + }); + }); + + describe('sub-url with role requirements but a parent with no requirements', function() { + describe('unauthorized api_key with only the sub role', function() { + shared.itBehavesLikeGatekeeperBlocked('/info/no-parent-roles/sub/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('authorized api_key with all the parent and sub roles', function() { + beforeEach(function setupApiUser(done) { + Factory.create('api_user', { roles: ['sub'] }, function(user) { + this.apiKey = user.api_key; + done(); + }.bind(this)); + }); + + shared.itBehavesLikeGatekeeperAllowed('/info/no-parent-roles/sub/'); }); }); @@ -132,5 +391,41 @@ describe('ApiUmbrellaGatekeper', function() { shared.itBehavesLikeGatekeeperAllowed('/not/restricted'); }); + + describe('no api key for an api that requires keys and roles', function() { + beforeEach(function setupApiUser() { + this.apiKey = ''; + }); + + shared.itBehavesLikeGatekeeperBlocked('/info/', 403, 'API_KEY_MISSING'); + }); + + describe('api requires roles but no api key actually required', function() { + describe('no api key given', function() { + beforeEach(function setupApiUser() { + this.apiKey = ''; + }); + + shared.itBehavesLikeGatekeeperBlocked('/info/no-key/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('api key without roles given', function() { + shared.itBehavesLikeGatekeeperBlocked('/info/no-key/', 403, 'API_KEY_UNAUTHORIZED'); + }); + }); + + describe('parent api requires roles but sub settings disable api key requirements', function() { + describe('no api key given', function() { + beforeEach(function setupApiUser() { + this.apiKey = ''; + }); + + shared.itBehavesLikeGatekeeperBlocked('/info/sub-no-key-required/', 403, 'API_KEY_UNAUTHORIZED'); + }); + + describe('api key without roles given', function() { + shared.itBehavesLikeGatekeeperBlocked('/info/sub-no-key-required/', 403, 'API_KEY_UNAUTHORIZED'); + }); + }); }); });