Move access-control feature to typescript

pull/33/head
Clement Michaud 2017-05-20 17:30:42 +02:00
parent 57278a7306
commit 40e02d23bf
13 changed files with 290 additions and 333 deletions

View File

@ -14,9 +14,9 @@ type UserName = string;
type GroupName = string;
type DomainPattern = string;
type ACLDefaultRules = Array<DomainPattern>;
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;

View File

@ -0,0 +1,7 @@
import * as winston from "winston";
export interface ILogger {
debug: winston.LeveledLogMethod;
}

View File

@ -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<void>((resolve, reject) => {

View File

@ -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<groups.length; ++i) {
var group = groups[i];
if(group in groups_policy) {
allowed_domains = allowed_domains.concat(groups_policy[group]);
}
}
  }
return allowed_domains;
}
AccessControlBuilder.prototype.extract_per_user = function(user) {
var allowed_domains = [];
var users_policy = objectPath.get(this.config, 'users');
if(users_policy) {
if(user in users_policy) {
allowed_domains = allowed_domains.concat(users_policy[user]);
}
  }
return allowed_domains;
}
AccessControlBuilder.prototype.get_allowed_domains = function(user, groups) {
var allowed_domains = [];
var default_policy = objectPath.get(this.config, 'default');
if(default_policy) {
allowed_domains = allowed_domains.concat(default_policy);
}
allowed_domains = allowed_domains.concat(this.extract_per_group(groups));
allowed_domains = allowed_domains.concat(this.extract_per_user(user));
this.logger.debug('ACL: user \'%s\' is allowed access to %s', user,
JSON.stringify(allowed_domains));
return allowed_domains;
}
AccessControlBuilder.prototype.get_any_domain = function() {
return ['*'];
}

View File

@ -0,0 +1,35 @@
import { ACLConfiguration } from "../Configuration";
import PatternBuilder from "./PatternBuilder";
import { ILogger } from "../ILogger";
export default class AccessController {
private logger: ILogger;
private patternBuilder: PatternBuilder;
constructor(configuration: ACLConfiguration, logger_: ILogger) {
this.logger = logger_;
this.patternBuilder = new PatternBuilder(configuration, logger_);
}
isDomainAllowedForUser(domain: string, user: string, groups: string[]): boolean {
const allowed_domains = this.patternBuilder.getAllowedDomains(user, groups);
// 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 (let i = 0; i < allowed_domains.length; ++i) {
const 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;
}
}

View File

@ -0,0 +1,61 @@
import { ILogger } from "../ILogger";
import { ACLConfiguration, ACLGroupsRules, ACLUsersRules, ACLDefaultRules } from "../Configuration";
import objectPath = require("object-path");
export default class AccessControlPatternBuilder {
logger: ILogger;
configuration: ACLConfiguration;
constructor(configuration: ACLConfiguration | undefined, logger_: ILogger) {
this.configuration = configuration;
this.logger = logger_;
}
private buildFromGroups(groups: string[]): string[] {
let allowed_domains: string[] = [];
const groups_policy = objectPath.get<ACLConfiguration, ACLGroupsRules>(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<ACLConfiguration, ACLUsersRules>(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<ACLConfiguration, ACLDefaultRules>(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;
}
}

View File

@ -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);

View File

@ -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)

View File

@ -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", []));
});
});
});

View File

@ -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",
]);
});
});
});

View File

@ -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());

View File

@ -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 });
});

View File

@ -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));
});
});
});