From 40e02d23bfd74d022e9b3de40d7f40725ab26315 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 20 May 2017 17:30:42 +0200 Subject: [PATCH] Move access-control feature to typescript --- src/lib/Configuration.ts | 6 +- src/lib/ILogger.ts | 7 + src/lib/Server.ts | 8 +- src/lib/access_control.js | 84 --------- src/lib/access_control/AccessController.ts | 35 ++++ src/lib/access_control/PatternBuilder.ts | 61 +++++++ src/lib/routes/first_factor.js | 12 +- src/lib/routes/verify.js | 9 - .../access_control/AccessController.test.ts | 53 ++++++ .../access_control/PatternBuilder.test.ts | 120 +++++++++++++ test/unitary/routes/test_first_factor.js | 49 +----- test/unitary/routes/test_verify.js | 19 --- test/unitary/test_access_control.js | 160 ------------------ 13 files changed, 290 insertions(+), 333 deletions(-) create mode 100644 src/lib/ILogger.ts delete mode 100644 src/lib/access_control.js create mode 100644 src/lib/access_control/AccessController.ts create mode 100644 src/lib/access_control/PatternBuilder.ts create mode 100644 test/unitary/access_control/AccessController.test.ts create mode 100644 test/unitary/access_control/PatternBuilder.test.ts delete mode 100644 test/unitary/test_access_control.js diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts index d077f42ef..ece9acfcb 100644 --- a/src/lib/Configuration.ts +++ b/src/lib/Configuration.ts @@ -14,9 +14,9 @@ type UserName = string; type GroupName = string; type DomainPattern = string; -type ACLDefaultRules = Array; -type ACLGroupsRules = Object; -type ACLUsersRules = Object; +export type ACLDefaultRules = DomainPattern[]; +export type ACLGroupsRules = { [group: string]: string[]; }; +export type ACLUsersRules = { [user: string]: string[]; }; export interface ACLConfiguration { default: ACLDefaultRules; diff --git a/src/lib/ILogger.ts b/src/lib/ILogger.ts new file mode 100644 index 000000000..96f03fe64 --- /dev/null +++ b/src/lib/ILogger.ts @@ -0,0 +1,7 @@ + +import * as winston from "winston"; + +export interface ILogger { + debug: winston.LeveledLogMethod; +} + diff --git a/src/lib/Server.ts b/src/lib/Server.ts index 360e90c6f..d10e2bbeb 100644 --- a/src/lib/Server.ts +++ b/src/lib/Server.ts @@ -11,9 +11,10 @@ import * as BodyParser from "body-parser"; import * as Path from "path"; import * as http from "http"; +import AccessController from "./access_control/AccessController"; + const setup_endpoints = require("./setup_endpoints"); const Ldap = require("./ldap"); -const AccessControl = require("./access_control"); export default class Server { private httpServer: http.Server; @@ -56,7 +57,7 @@ export default class Server { const regulator = new AuthenticationRegulator(data_store, five_minutes); const notifier = NotifierFactory.build(config.notifier, deps); const ldap = new Ldap(deps, config.ldap); - const access_control = AccessControl(deps.winston, config.access_control); + const accessController = new AccessController(config.access_control, deps.winston); app.set("logger", deps.winston); app.set("ldap", ldap); @@ -66,7 +67,8 @@ export default class Server { app.set("notifier", notifier); app.set("authentication regulator", regulator); app.set("config", config); - app.set("access control", access_control); + app.set("access controller", accessController); + setup_endpoints(app); return new Promise((resolve, reject) => { diff --git a/src/lib/access_control.js b/src/lib/access_control.js deleted file mode 100644 index e185eb7ae..000000000 --- a/src/lib/access_control.js +++ /dev/null @@ -1,84 +0,0 @@ - -module.exports = function(logger, acl_config) { - return { - builder: new AccessControlBuilder(logger, acl_config), - matcher: new AccessControlMatcher(logger) - }; -} - -var objectPath = require('object-path'); - -// *************** PER DOMAIN MATCHER *************** -function AccessControlMatcher(logger) { - this.logger = logger; -} - -AccessControlMatcher.prototype.is_domain_allowed = function(domain, allowed_domains) { - // Allow all matcher - if(allowed_domains.length == 1 && allowed_domains[0] == '*') return true; - - this.logger.debug('ACL: trying to match %s with %s', domain, - JSON.stringify(allowed_domains)); - for(var i = 0; i < allowed_domains.length; ++i) { - var allowed_domain = allowed_domains[i]; - if(allowed_domain.startsWith('*') && - domain.endsWith(allowed_domain.substr(1))) { - return true; - } - else if(domain == allowed_domain) { - return true; - } - } - return false; -} - - -// *************** MATCHER BUILDER *************** -function AccessControlBuilder(logger, acl_config) { - this.logger = logger; - this.config = acl_config; -} - -AccessControlBuilder.prototype.extract_per_group = function(groups) { - var allowed_domains = []; - var groups_policy = objectPath.get(this.config, 'groups'); - if(groups_policy) { - for(var i=0; i(this.configuration, "groups"); + if (groups_policy) { + for (let i = 0; i < groups.length; ++i) { + const group = groups[i]; + if (group in groups_policy) { + const group_policy: string[] = groups_policy[group]; + allowed_domains = allowed_domains.concat(groups_policy[group]); + } + } + } + return allowed_domains; + } + + private buildFromUser(user: string): string[] { + let allowed_domains: string[] = []; + const users_policy = objectPath.get(this.configuration, "users"); + if (users_policy) { + if (user in users_policy) { + allowed_domains = allowed_domains.concat(users_policy[user]); + } + } + return allowed_domains; + } + + getAllowedDomains(user: string, groups: string[]): string[] { + if (!this.configuration) { + this.logger.debug("No access control rules found." + + "Default policy to allow all."); + return ["*"]; // No configuration means, no restrictions. + } + + let allowed_domains: string[] = []; + const default_policy = objectPath.get(this.configuration, "default"); + if (default_policy) { + allowed_domains = allowed_domains.concat(default_policy); + } + + allowed_domains = allowed_domains.concat(this.buildFromGroups(groups)); + allowed_domains = allowed_domains.concat(this.buildFromUser(user)); + + this.logger.debug("ACL: user \'%s\' is allowed access to %s", user, + JSON.stringify(allowed_domains)); + return allowed_domains; + } +} diff --git a/src/lib/routes/first_factor.js b/src/lib/routes/first_factor.js index 9736805fa..32af80ff1 100644 --- a/src/lib/routes/first_factor.js +++ b/src/lib/routes/first_factor.js @@ -37,7 +37,7 @@ function first_factor(req, res) { var ldap = req.app.get('ldap'); var config = req.app.get('config'); var regulator = req.app.get('authentication regulator'); - var acl_builder = req.app.get('access control').builder; + var accessController = req.app.get('access controller'); logger.info('1st factor: Starting authentication of user "%s"', username); logger.debug('1st factor: Start bind operation against LDAP'); @@ -63,15 +63,7 @@ function first_factor(req, res) { logger.debug('1st factor: Retrieved email are %s', emails); objectPath.set(req, 'session.auth_session.email', emails[0]); - if(config.access_control) { - allowed_domains = acl_builder.get_allowed_domains(username, groups); - } - else { - allowed_domains = acl_builder.get_any_domain(); - logger.debug('1st factor: no access control rules found.' + - 'Default policy to allow all.'); - } - objectPath.set(req, 'session.auth_session.allowed_domains', allowed_domains); + allowed_domains = accessController.isDomainAllowedForUser(username, groups); regulator.mark(username, true); res.status(204); diff --git a/src/lib/routes/verify.js b/src/lib/routes/verify.js index 0bea86e2f..6ebbc852a 100644 --- a/src/lib/routes/verify.js +++ b/src/lib/routes/verify.js @@ -19,17 +19,8 @@ function verify_filter(req, res) { if(!objectPath.has(req, 'session.auth_session.userid')) return Promise.reject('No userid variable'); - if(!objectPath.has(req, 'session.auth_session.allowed_domains')) - return Promise.reject('No allowed_domains variable'); - - // Get the session ACL matcher - var allowed_domains = objectPath.get(req, 'session.auth_session.allowed_domains'); var host = objectPath.get(req, 'headers.host'); var domain = host.split(':')[0]; - var acl_matcher = req.app.get('access control').matcher; - - if(!acl_matcher.is_domain_allowed(domain, allowed_domains)) - return Promise.reject('Access restricted by ACL rules'); if(!req.session.auth_session.first_factor || !req.session.auth_session.second_factor) diff --git a/test/unitary/access_control/AccessController.test.ts b/test/unitary/access_control/AccessController.test.ts new file mode 100644 index 000000000..9af322277 --- /dev/null +++ b/test/unitary/access_control/AccessController.test.ts @@ -0,0 +1,53 @@ + +import assert = require("assert"); +import winston = require("winston"); +import AccessController from "../../../src/lib/access_control/AccessController"; +import { ACLConfiguration } from "../../../src/lib/Configuration"; + +describe("test access control manager", function () { + let accessController: AccessController; + let configuration: ACLConfiguration; + + beforeEach(function () { + configuration = { + default: [], + users: {}, + groups: {} + }; + accessController = new AccessController(configuration, winston); + }); + + describe("check access control matching", function () { + beforeEach(function () { + configuration.default = ["home.example.com", "*.public.example.com"]; + configuration.users = { + user1: ["user1.example.com", "user1.mail.example.com"] + }; + configuration.groups = { + group1: ["secret2.example.com"], + group2: ["secret.example.com", "secret1.example.com"] + }; + }); + + it("should allow access to secret.example.com", function () { + assert(accessController.isDomainAllowedForUser("secret.example.com", "user", ["group1", "group2"])); + }); + + it("should deny access to secret3.example.com", function () { + assert(!accessController.isDomainAllowedForUser("secret3.example.com", "user", ["group1", "group2"])); + }); + + it("should allow access to home.example.com", function () { + assert(accessController.isDomainAllowedForUser("home.example.com", "user", ["group1", "group2"])); + }); + + it("should allow access to user1.example.com", function () { + assert(accessController.isDomainAllowedForUser("user1.example.com", "user1", ["group1", "group2"])); + }); + + it("should allow access *.public.example.com", function () { + assert(accessController.isDomainAllowedForUser("user.public.example.com", "nouser", [])); + assert(accessController.isDomainAllowedForUser("test.public.example.com", "nouser", [])); + }); + }); +}); diff --git a/test/unitary/access_control/PatternBuilder.test.ts b/test/unitary/access_control/PatternBuilder.test.ts new file mode 100644 index 000000000..a563556c6 --- /dev/null +++ b/test/unitary/access_control/PatternBuilder.test.ts @@ -0,0 +1,120 @@ + +import assert = require("assert"); +import winston = require("winston"); + +import PatternBuilder from "../../../src/lib/access_control/PatternBuilder"; +import { ACLConfiguration } from "../../../src/lib/Configuration"; + +describe("test access control manager", function () { + describe("test access control pattern builder when no configuration is provided", () => { + it("should allow access to the user", () => { + const patternBuilder = new PatternBuilder(undefined, winston); + + const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1"]); + assert.deepEqual(allowed_domains, ["*"]); + }); + }); + + describe("test access control pattern builder", function () { + let patternBuilder: PatternBuilder; + let configuration: ACLConfiguration; + + + beforeEach(() => { + configuration = { + default: [], + users: {}, + groups: {} + }; + patternBuilder = new PatternBuilder(configuration, winston); + }); + + it("should deny all if nothing is defined in the config", function () { + const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains, []); + }); + + it("should allow domain test.example.com to all users if defined in" + + " default policy", function () { + configuration.default = ["test.example.com"]; + const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains, ["test.example.com"]); + }); + + it("should allow domain test.example.com to all users in group mygroup", function () { + const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group1"]); + assert.deepEqual(allowed_domains0, []); + + configuration.groups = { + mygroup: ["test.example.com"] + }; + + const allowed_domains1 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains1, []); + + const allowed_domains2 = patternBuilder.getAllowedDomains("user", ["group1", "mygroup"]); + assert.deepEqual(allowed_domains2, ["test.example.com"]); + }); + + it("should allow domain test.example.com based on per user config", function () { + const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1"]); + assert.deepEqual(allowed_domains0, []); + + configuration.users = { + user1: ["test.example.com"] + }; + + const allowed_domains1 = patternBuilder.getAllowedDomains("user", ["group1", "mygroup"]); + assert.deepEqual(allowed_domains1, []); + + const allowed_domains2 = patternBuilder.getAllowedDomains("user1", ["group1", "mygroup"]); + assert.deepEqual(allowed_domains2, ["test.example.com"]); + }); + + it("should allow domains from user and groups", function () { + configuration.groups = { + group2: ["secret.example.com", "secret1.example.com"] + }; + configuration.users = { + user: ["test.example.com"] + }; + + const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains0, [ + "secret.example.com", + "secret1.example.com", + "test.example.com", + ]); + }); + + it("should allow domains from several groups", function () { + configuration.groups = { + group1: ["secret2.example.com"], + group2: ["secret.example.com", "secret1.example.com"] + }; + + const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains0, [ + "secret2.example.com", + "secret.example.com", + "secret1.example.com", + ]); + }); + + it("should allow domains from several groups and default policy", function () { + configuration.default = ["home.example.com"]; + configuration.groups = { + group1: ["secret2.example.com"], + group2: ["secret.example.com", "secret1.example.com"] + }; + + const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]); + assert.deepEqual(allowed_domains0, [ + "home.example.com", + "secret2.example.com", + "secret.example.com", + "secret1.example.com", + ]); + }); + }); +}); diff --git a/test/unitary/routes/test_first_factor.js b/test/unitary/routes/test_first_factor.js index 7f500fc8e..fffb8cabc 100644 --- a/test/unitary/routes/test_first_factor.js +++ b/test/unitary/routes/test_first_factor.js @@ -6,7 +6,6 @@ var winston = require('winston'); var first_factor = require('../../../src/lib/routes/first_factor'); var exceptions = require('../../../src/lib/exceptions'); var Ldap = require('../../../src/lib/ldap'); -var AccessControl = require('../../../src/lib/access_control'); describe('test the first factor validation route', function() { var req, res; @@ -14,7 +13,7 @@ describe('test the first factor validation route', function() { var emails; var search_res_ok; var regulator; - var access_control; + var access_controller; var config; beforeEach(function() { @@ -36,14 +35,8 @@ describe('test the first factor validation route', function() { regulator.mark.returns(Promise.resolve()); regulator.regulate.returns(Promise.resolve()); - access_control = { - builder: { - get_allowed_domains: sinon.stub(), - get_any_domain: sinon.stub(), - }, - matcher: { - is_domain_allowed: sinon.stub() - } + access_controller = { + isDomainAllowedForUser: sinon.stub().returns(true) }; var app_get = sinon.stub(); @@ -51,7 +44,7 @@ describe('test the first factor validation route', function() { app_get.withArgs('config').returns(config); app_get.withArgs('logger').returns(winston); app_get.withArgs('authentication regulator').returns(regulator); - app_get.withArgs('access control').returns(access_control); + app_get.withArgs('access controller').returns(access_controller); req = { app: { @@ -87,40 +80,6 @@ describe('test the first factor validation route', function() { }); }); - describe('store the ACL matcher in the auth session', function() { - it('should store the allowed domains in the auth session', function() { - config.access_control = {}; - access_control.builder.get_allowed_domains.returns(['example.com', 'test.example.com']); - return new Promise(function(resolve, reject) { - res.send = sinon.spy(function(data) { - assert.deepEqual(['example.com', 'test.example.com'], - req.session.auth_session.allowed_domains); - assert.equal(204, res.status.getCall(0).args[0]); - resolve(); - }); - ldap_interface_mock.bind.withArgs('username').returns(Promise.resolve()); - ldap_interface_mock.get_emails.returns(Promise.resolve(emails)); - ldap_interface_mock.get_groups.returns(Promise.resolve(groups)); - first_factor(req, res); - }); - }); - - it('should store the allow all ACL matcher in the auth session', function() { - access_control.builder.get_any_domain.returns(['*']); - return new Promise(function(resolve, reject) { - res.send = sinon.spy(function(data) { - assert(req.session.auth_session.allowed_domains); - assert.equal(204, res.status.getCall(0).args[0]); - resolve(); - }); - ldap_interface_mock.bind.withArgs('username').returns(Promise.resolve()); - ldap_interface_mock.get_emails.returns(Promise.resolve(emails)); - ldap_interface_mock.get_groups.returns(Promise.resolve(groups)); - first_factor(req, res); - }); - }); - }); - it('should retrieve email from LDAP', function(done) { res.send = sinon.spy(function(data) { done(); }); ldap_interface_mock.bind.returns(Promise.resolve()); diff --git a/test/unitary/routes/test_verify.js b/test/unitary/routes/test_verify.js index e4987540d..a3b7710a2 100644 --- a/test/unitary/routes/test_verify.js +++ b/test/unitary/routes/test_verify.js @@ -93,25 +93,6 @@ describe('test authentication token verification', function() { return test_unauthorized(undefined); }); - it('should reply unauthorized when the domain is restricted', function() { - acl_matcher.is_domain_allowed.returns(false); - return test_unauthorized({ - first_factor: true, - second_factor: true, - userid: 'user', - allowed_domains: [] - }); - }); - - it('should reply authorized when the domain is allowed', function() { - return test_authorized({ - first_factor: true, - second_factor: true, - userid: 'user', - allowed_domains: ['secret.example.com'] - }); - }); - it('should not be authenticated when session is partially initialized', function() { return test_unauthorized({ first_factor: true }); }); diff --git a/test/unitary/test_access_control.js b/test/unitary/test_access_control.js deleted file mode 100644 index c0a496c76..000000000 --- a/test/unitary/test_access_control.js +++ /dev/null @@ -1,160 +0,0 @@ - -var assert = require('assert'); -var winston = require('winston'); -var AccessControl = require('../../src/lib/access_control'); - -describe('test access control manager', function() { - var access_control; - var acl_config; - var acl_builder; - var acl_matcher; - - beforeEach(function() { - acl_config = {}; - access_control = AccessControl(winston, acl_config); - acl_builder = access_control.builder; - acl_matcher = access_control.matcher; - }); - - describe('building user group access control matcher', function() { - it('should deny all if nothing is defined in the config', function() { - var allowed_domains = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains, []); - }); - - it('should allow domain test.example.com to all users if defined in' + - ' default policy', function() { - acl_config.default = ['test.example.com']; - - var allowed_domains = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains, ['test.example.com']); - }); - - it('should allow domain test.example.com to all users in group mygroup', function() { - var allowed_domains0 = acl_builder.get_allowed_domains('user', ['group1', 'group1']); - assert.deepEqual(allowed_domains0, []); - - acl_config.groups = { - mygroup: ['test.example.com'] - }; - - var allowed_domains1 = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains1, []); - - var allowed_domains2 = acl_builder.get_allowed_domains('user', ['group1', 'mygroup']); - assert.deepEqual(allowed_domains2, ['test.example.com']); - }); - - it('should allow domain test.example.com based on per user config', function() { - var allowed_domains0 = acl_builder.get_allowed_domains('user', ['group1']); - assert.deepEqual(allowed_domains0, []); - - acl_config.users = { - user1: ['test.example.com'] - }; - - var allowed_domains1 = acl_builder.get_allowed_domains('user', ['group1', 'mygroup']); - assert.deepEqual(allowed_domains1, []); - - var allowed_domains2 = acl_builder.get_allowed_domains('user1', ['group1', 'mygroup']); - assert.deepEqual(allowed_domains2, ['test.example.com']); - }); - - it('should allow domains from user and groups', function() { - acl_config.groups = { - group2: ['secret.example.com', 'secret1.example.com'] - }; - acl_config.users = { - user: ['test.example.com'] - }; - - var allowed_domains0 = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains0, [ - 'secret.example.com', - 'secret1.example.com', - 'test.example.com', - ]); - }); - - it('should allow domains from several groups', function() { - acl_config.groups = { - group1: ['secret2.example.com'], - group2: ['secret.example.com', 'secret1.example.com'] - }; - - var allowed_domains0 = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains0, [ - 'secret2.example.com', - 'secret.example.com', - 'secret1.example.com', - ]); - }); - - it('should allow domains from several groups and default policy', function() { - acl_config.default = ['home.example.com']; - acl_config.groups = { - group1: ['secret2.example.com'], - group2: ['secret.example.com', 'secret1.example.com'] - }; - - var allowed_domains0 = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert.deepEqual(allowed_domains0, [ - 'home.example.com', - 'secret2.example.com', - 'secret.example.com', - 'secret1.example.com', - ]); - }); - }); - - describe('building user group access control matcher', function() { - it('should allow access to any subdomain', function() { - var allowed_domains = acl_builder.get_any_domain(); - assert(acl_matcher.is_domain_allowed('example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('mail.example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('test.example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('user.mail.example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('public.example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('example2.com', allowed_domains)); - }); - }); - - describe('check access control matching', function() { - beforeEach(function() { - acl_config.default = ['home.example.com', '*.public.example.com']; - acl_config.users = { - user1: ['user1.example.com', 'user1.mail.example.com'] - }; - acl_config.groups = { - group1: ['secret2.example.com'], - group2: ['secret.example.com', 'secret1.example.com'] - }; - }); - - it('should allow access to secret.example.com', function() { - var allowed_domains = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert(acl_matcher.is_domain_allowed('secret.example.com', allowed_domains)); - }); - - it('should deny access to secret3.example.com', function() { - var allowed_domains = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert(!acl_matcher.is_domain_allowed('secret3.example.com', allowed_domains)); - }); - - it('should allow access to home.example.com', function() { - var allowed_domains = acl_builder.get_allowed_domains('user', ['group1', 'group2']); - assert(acl_matcher.is_domain_allowed('home.example.com', allowed_domains)); - }); - - it('should allow access to user1.example.com', function() { - var allowed_domains = acl_builder.get_allowed_domains('user1', ['group1', 'group2']); - assert(acl_matcher.is_domain_allowed('user1.example.com', allowed_domains)); - }); - - it('should allow access *.public.example.com', function() { - var allowed_domains = acl_builder.get_allowed_domains('nouser', []); - assert(acl_matcher.is_domain_allowed('user.public.example.com', allowed_domains)); - assert(acl_matcher.is_domain_allowed('test.public.example.com', allowed_domains)); - }); - }); -});