Rework the configuration of the access control to allow default policy for certain domains

This commit is contained in:
Clement Michaud 2017-03-25 18:38:14 +01:00
parent 38a4570b24
commit b403cfe2f8
11 changed files with 387 additions and 60 deletions

View File

@ -43,17 +43,19 @@ ldap:
# is allowed to everyone. # is allowed to everyone.
# Otherwise, the default policy is denied for any user and any subdomain. # Otherwise, the default policy is denied for any user and any subdomain.
access_control: access_control:
- group: admin default:
allowed_domains: - home.test.local
groups:
admin:
- '*.test.local'
dev:
- secret.test.local - secret.test.local
- secret1.test.local
- secret2.test.local - secret2.test.local
- group: dev users:
allowed_domains: harry:
- secret2.test.local
- user: harry
allowed_domains:
- secret1.test.local - secret1.test.local
bob:
- '*.mail.test.local'
# Configuration of session cookies # Configuration of session cookies

View File

@ -3,8 +3,81 @@
<title>Home page</title> <title>Home page</title>
</head> </head>
<body> <body>
You need to <a href="https://auth.test.local:8080/authentication/login?redirect=https://secret.test.local:8080/">log in</a> to access the <a href="/secret.html">secret</a>!<br/><br/> <h1>Access the secret</h1>
But you can also access it from another <a href="https://secret1.test.local:8080/secret.html">domain</a> or still <a href="https://secret2.test.local:8080/secret.html">another one</a>.<br/><br/> You need to log in to access the secret!<br/><br/>
You can also log off by visiting the following <a href="https://auth.test.local:8080/authentication/logout?redirect=https://secret.test.local:8080/">link</a>. Try to access it via one of the following links.<br/>
<ul>
<li>
<a href="https://secret.test.local:8080/secret.html">secret.test.local</a>
</li>
<li>
<a href="https://secret1.test.local:8080/secret.html">secret1.test.local</a>
</li>
<li>
<a href="https://secret2.test.local:8080/secret.html">secret2.test.local</a>
</li>
<li>
<a href="https://home.test.local:8080/secret.html">home.test.local</a>
</li>
<li>
<a href="https://mx1.mail.test.local:8080/secret.html">mx1.mail.test.local</a>
</li>
<li>
<a href="https://mx2.mail.test.local:8080/secret.html">mx2.mail.test.local</a>
</li>
</ul>
You can also log off by visiting the following <a href="https://auth.test.local:8080/authentication/logout?redirect=https://home.test.local:8080/">link</a>.
<h1>List of users</h1>
Here is the list of credentials you can log in with to test access control.
<ul>
<li><strong>john / password</strong>: belongs to <em>admin</em> and <em>dev</em> groups.</li>
<li><strong>bob / password</strong>: belongs to <em>dev</em> group only.</li>
<li><strong>harry / password</strong>: does not belong to any group.</li>
</ul>
<h1>Access control rules</h1>
<ul>
<li><strong>Default policy</strong>
<ul>
<li>home.test.local</li>
</ul>
</li>
<li><strong>Groups policy</strong>
<ul>
<li>admin
<ul>
<li>*.test.local</li>
</ul>
</li>
<li>dev
<ul>
<li>secret.test.local</li>
<li>secret2.test.local</li>
</ul>
</li>
</ul>
</li>
<li><strong>Users policy</strong>
<ul>
<li>harry
<ul>
<li>secret1.test.local</li>
</ul>
</li>
<li>bob
<ul>
<li>*.mail.test.local</li>
</ul>
</li>
</ul>
</li>
</ul>
</body> </body>
</html> </html>

View File

@ -60,7 +60,9 @@ http {
listen 443 ssl; listen 443 ssl;
root /usr/share/nginx/html; root /usr/share/nginx/html;
server_name secret1.test.local secret2.test.local secret.test.local localhost; server_name secret1.test.local secret2.test.local secret.test.local
home.test.local mx1.mail.test.local mx2.mail.test.local
localhost;
ssl on; ssl on;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.crt;

View File

@ -3,6 +3,7 @@
<title>Secret</title> <title>Secret</title>
</head> </head>
<body> <body>
This is a very important secret! This is a very important secret!<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
</body> </body>
</html> </html>

84
src/lib/access_control.js Normal file
View File

@ -0,0 +1,84 @@
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

@ -37,6 +37,7 @@ function first_factor(req, res) {
var ldap = req.app.get('ldap'); var ldap = req.app.get('ldap');
var config = req.app.get('config'); var config = req.app.get('config');
var regulator = req.app.get('authentication regulator'); var regulator = req.app.get('authentication regulator');
var acl_builder = req.app.get('access control').builder;
logger.info('1st factor: Starting authentication of user "%s"', username); logger.info('1st factor: Starting authentication of user "%s"', username);
logger.debug('1st factor: Start bind operation against LDAP'); logger.debug('1st factor: Start bind operation against LDAP');
@ -56,22 +57,21 @@ function first_factor(req, res) {
.then(function(data) { .then(function(data) {
var emails = data[0]; var emails = data[0];
var groups = data[1]; var groups = data[1];
var allowed_domains;
if(!emails && emails.length <= 0) throw new Error('No email found'); if(!emails && emails.length <= 0) throw new Error('No email found');
logger.debug('1st factor: Retrieved email are %s', emails); logger.debug('1st factor: Retrieved email are %s', emails);
objectPath.set(req, 'session.auth_session.email', emails[0]); objectPath.set(req, 'session.auth_session.email', emails[0]);
if(config.access_control) { if(config.access_control) {
var allowed_domains = get_allowed_domains(config.access_control, allowed_domains = acl_builder.get_allowed_domains(username, groups);
username, groups);
logger.debug('1st factor: allowed domains are %s', allowed_domains);
objectPath.set(req, 'session.auth_session.allowed_domains',
allowed_domains);
} }
else { else {
allowed_domains = acl_builder.get_any_domain();
logger.debug('1st factor: no access control rules found.' + logger.debug('1st factor: no access control rules found.' +
'Default policy to allow all.'); 'Default policy to allow all.');
} }
objectPath.set(req, 'session.auth_session.allowed_domains', allowed_domains);
regulator.mark(username, true); regulator.mark(username, true);
res.status(204); res.status(204);

View File

@ -19,19 +19,17 @@ function verify_filter(req, res) {
if(!objectPath.has(req, 'session.auth_session.userid')) if(!objectPath.has(req, 'session.auth_session.userid'))
return Promise.reject('No userid variable'); return Promise.reject('No userid variable');
var config = req.app.get('config'); if(!objectPath.has(req, 'session.auth_session.allowed_domains'))
var access_control = config.access_control; return Promise.reject('No allowed_domains variable');
if(access_control) { // Get the session ACL matcher
var allowed_domains = objectPath.get(req, 'session.auth_session.allowed_domains'); var allowed_domains = objectPath.get(req, 'session.auth_session.allowed_domains');
var host = objectPath.get(req, 'headers.host'); var host = objectPath.get(req, 'headers.host');
var domain = host.split(':')[0]; var domain = host.split(':')[0];
logger.debug('Trying to access domain: %s', domain); var acl_matcher = req.app.get('access control').matcher;
logger.debug('User has access to %s', JSON.stringify(allowed_domains));
if(allowed_domains.indexOf(domain) < 0) if(!acl_matcher.is_domain_allowed(domain, allowed_domains))
return Promise.reject('Access restricted by ACL rules'); return Promise.reject('Access restricted by ACL rules');
}
if(!req.session.auth_session.first_factor || if(!req.session.auth_session.first_factor ||
!req.session.auth_session.second_factor) !req.session.auth_session.second_factor)

View File

@ -12,6 +12,7 @@ var AuthenticationRegulator = require('./authentication_regulator');
var setup_endpoints = require('./setup_endpoints'); var setup_endpoints = require('./setup_endpoints');
var config_adapter = require('./config_adapter'); var config_adapter = require('./config_adapter');
var Ldap = require('./ldap'); var Ldap = require('./ldap');
var AccessControl = require('./access_control');
function run(yaml_config, deps, fn) { function run(yaml_config, deps, fn) {
var config = config_adapter(yaml_config); var config = config_adapter(yaml_config);
@ -51,6 +52,7 @@ function run(yaml_config, deps, fn) {
var regulator = new AuthenticationRegulator(data_store, five_minutes); var regulator = new AuthenticationRegulator(data_store, five_minutes);
var notifier = new Notifier(config.notifier, deps); var notifier = new Notifier(config.notifier, deps);
var ldap = new Ldap(deps, config.ldap); var ldap = new Ldap(deps, config.ldap);
var access_control = AccessControl(deps.winston, config.access_control);
app.set('logger', deps.winston); app.set('logger', deps.winston);
app.set('ldap', ldap); app.set('ldap', ldap);
@ -60,6 +62,7 @@ function run(yaml_config, deps, fn) {
app.set('notifier', notifier); app.set('notifier', notifier);
app.set('authentication regulator', regulator); app.set('authentication regulator', regulator);
app.set('config', config); app.set('config', config);
app.set('access control', access_control);
setup_endpoints(app); setup_endpoints(app);

View File

@ -6,6 +6,7 @@ var winston = require('winston');
var first_factor = require('../../../src/lib/routes/first_factor'); var first_factor = require('../../../src/lib/routes/first_factor');
var exceptions = require('../../../src/lib/exceptions'); var exceptions = require('../../../src/lib/exceptions');
var Ldap = require('../../../src/lib/ldap'); var Ldap = require('../../../src/lib/ldap');
var AccessControl = require('../../../src/lib/access_control');
describe('test the first factor validation route', function() { describe('test the first factor validation route', function() {
var req, res; var req, res;
@ -13,6 +14,7 @@ describe('test the first factor validation route', function() {
var emails; var emails;
var search_res_ok; var search_res_ok;
var regulator; var regulator;
var access_control;
var config; var config;
beforeEach(function() { beforeEach(function() {
@ -34,11 +36,22 @@ describe('test the first factor validation route', function() {
regulator.mark.returns(Promise.resolve()); regulator.mark.returns(Promise.resolve());
regulator.regulate.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()
}
};
var app_get = sinon.stub(); var app_get = sinon.stub();
app_get.withArgs('ldap').returns(ldap_interface_mock); app_get.withArgs('ldap').returns(ldap_interface_mock);
app_get.withArgs('config').returns(config); app_get.withArgs('config').returns(config);
app_get.withArgs('logger').returns(winston); app_get.withArgs('logger').returns(winston);
app_get.withArgs('authentication regulator').returns(regulator); app_get.withArgs('authentication regulator').returns(regulator);
app_get.withArgs('access control').returns(access_control);
req = { req = {
app: { app: {
@ -74,16 +87,13 @@ describe('test the first factor validation route', function() {
}); });
}); });
describe('store the allowed domains in the auth session', function() { describe('store the ACL matcher in the auth session', function() {
it('should store the per group allowed domains', function() { it('should store the allowed domains in the auth session', function() {
config.access_control = []; config.access_control = {};
config.access_control.push({ access_control.builder.get_allowed_domains.returns(['example.com', 'test.example.com']);
group: 'group1',
allowed_domains: ['domain1.example.com', 'domain2.example.com']
});
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
res.send = sinon.spy(function(data) { res.send = sinon.spy(function(data) {
assert.deepEqual(['domain1.example.com', 'domain2.example.com'], assert.deepEqual(['example.com', 'test.example.com'],
req.session.auth_session.allowed_domains); req.session.auth_session.allowed_domains);
assert.equal(204, res.status.getCall(0).args[0]); assert.equal(204, res.status.getCall(0).args[0]);
resolve(); resolve();
@ -95,16 +105,11 @@ describe('test the first factor validation route', function() {
}); });
}); });
it('should store the per group allowed domains', function() { it('should store the allow all ACL matcher in the auth session', function() {
config.access_control = []; access_control.builder.get_any_domain.returns(['*']);
config.access_control.push({
user: 'username',
allowed_domains: ['domain1.example.com', 'domain2.example.com']
});
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
res.send = sinon.spy(function(data) { res.send = sinon.spy(function(data) {
assert.deepEqual(['domain1.example.com', 'domain2.example.com'], assert(req.session.auth_session.allowed_domains);
req.session.auth_session.allowed_domains);
assert.equal(204, res.status.getCall(0).args[0]); assert.equal(204, res.status.getCall(0).args[0]);
resolve(); resolve();
}); });

View File

@ -7,8 +7,15 @@ var winston = require('winston');
describe('test authentication token verification', function() { describe('test authentication token verification', function() {
var req, res; var req, res;
var config_mock; var config_mock;
var acl_matcher;
beforeEach(function() { beforeEach(function() {
acl_matcher = {
is_domain_allowed: sinon.stub().returns(true)
};
var access_control = {
matcher: acl_matcher
}
config_mock = {}; config_mock = {};
req = {}; req = {};
res = {}; res = {};
@ -18,6 +25,7 @@ describe('test authentication token verification', function() {
req.app.get = sinon.stub(); req.app.get = sinon.stub();
req.app.get.withArgs('config').returns(config_mock); req.app.get.withArgs('config').returns(config_mock);
req.app.get.withArgs('logger').returns(winston); req.app.get.withArgs('logger').returns(winston);
req.app.get.withArgs('access control').returns(access_control);
res.status = sinon.spy(); res.status = sinon.spy();
}); });
@ -27,7 +35,7 @@ describe('test authentication token verification', function() {
first_factor: true, first_factor: true,
second_factor: true, second_factor: true,
userid: 'myuser', userid: 'myuser',
group: 'mygroup' allowed_domains: ['*']
}; };
res.send = sinon.spy(function() { res.send = sinon.spy(function() {
@ -86,25 +94,16 @@ describe('test authentication token verification', function() {
}); });
it('should reply unauthorized when the domain is restricted', function() { it('should reply unauthorized when the domain is restricted', function() {
config_mock.access_control = []; acl_matcher.is_domain_allowed.returns(false);
config_mock.access_control.push({
group: 'abc',
allowed_domains: ['secret.example.com']
});
return test_unauthorized({ return test_unauthorized({
first_factor: true, first_factor: true,
second_factor: true, second_factor: true,
userid: 'user', userid: 'user',
allowed_domains: ['restricted.example.com'] allowed_domains: []
}); });
}); });
it('should reply authorized when the domain is allowed', function() { it('should reply authorized when the domain is allowed', function() {
config_mock.access_control = [];
config_mock.access_control.push({
group: 'abc',
allowed_domains: ['secret.example.com']
});
return test_authorized({ return test_authorized({
first_factor: true, first_factor: true,
second_factor: true, second_factor: true,

View File

@ -0,0 +1,160 @@
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));
});
});
});