Add the access_control entry in the config file to allow the user to define per group rules to access the subdomains

pull/21/head
Clement Michaud 2017-03-25 15:17:21 +01:00
parent 4b93338bae
commit 2a73b1a431
19 changed files with 620 additions and 290 deletions

View File

@ -1,20 +1,57 @@
# The port to listen on
port: 8080
port: 80
# Log level
#
# Level of verbosity for logs
logs_level: info
# Configuration of LDAP
# LDAP configuration
#
# Example: for user john, the DN will be cn=john,ou=users,dc=example,dc=com
ldap:
# The url of the ldap server
url: ldap://ldap
user_search_base: ou=users,dc=example,dc=com
user_search_filter: cn
# The base dn for every entries
base_dn: dc=example,dc=com
# An additional dn to define the scope to all users
additional_user_dn: ou=users
# The user name attribute of users. Might uid for FreeIPA. 'cn' by default.
user_name_attribute: cn
# An additional dn to define the scope of groups
additional_group_dn: ou=groups
# The group name attribute of group. 'cn' by default.
group_name_attribute: cn
# The username and password of the admin user.
user: cn=admin,dc=example,dc=com
password: password
# Access Control
#
# Access control is a set of rules where you can specify a group-based
# subdomain restrictions.
#
# If access_control is not defined, ACL rules are disabled and default policy
# is allowed to everyone.
# Otherwise, the default policy is denied for any user and any subdomain.
access_control:
- group: admin
allowed_domains:
- secret.test.local
- secret1.test.local
- group: dev
allowed_domains:
- secret2.test.local
# Configuration of session cookies
#
# _secret_ the secret to encrypt session cookies

View File

@ -16,6 +16,9 @@ services:
- SLAPD_ORGANISATION=MyCompany
- SLAPD_DOMAIN=example.com
- SLAPD_PASSWORD=password
- SLAPD_ADDITIONAL_MODULES=memberof
- SLAPD_ADDITIONAL_SCHEMAS=openldap
- SLAPD_FORCE_RECONFIGURE=true
expose:
- "389"
volumes:

View File

@ -8,39 +8,55 @@ objectclass: organizationalUnit
objectclass: top
ou: users
dn: cn=user,ou=groups,dc=example,dc=com
cn: user
gidnumber: 502
objectclass: posixGroup
dn: cn=dev,ou=groups,dc=example,dc=com
cn: dev
member: cn=john,ou=users,dc=example,dc=com
member: cn=bob,ou=users,dc=example,dc=com
objectclass: groupOfNames
objectclass: top
dn: cn=user,ou=users,dc=example,dc=com
cn: user
gidnumber: 500
givenname: user
homedirectory: /home/user1
loginshell: /bin/sh
objectclass: inetOrgPerson
objectclass: posixAccount
dn: cn=admin,ou=groups,dc=example,dc=com
cn: admin
member: cn=john,ou=users,dc=example,dc=com
objectclass: groupOfNames
objectclass: top
mail: user@example.com
sn: User
uid: user
uidnumber: 1000
dn: cn=john,ou=users,dc=example,dc=com
cn: john
objectclass: inetOrgPerson
objectclass: top
mail: john.doe@example.com
sn: John Doe
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
dn: uid=useruid,ou=users,dc=example,dc=com
cn: useruid
gidnumber: 500
givenname: user
homedirectory: /home/user1
loginshell: /bin/sh
dn: cn=harry,ou=users,dc=example,dc=com
cn: harry
objectclass: inetOrgPerson
objectclass: posixAccount
objectclass: top
mail: useruid@example.com
sn: User
uid: useruid
uidnumber: 1001
mail: harry.potter@example.com
sn: Harry Potter
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
dn: cn=bob,ou=users,dc=example,dc=com
cn: bob
objectclass: inetOrgPerson
objectclass: top
mail: bob.dylan@example.com
sn: Bob Dylan
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
# dn: uid=jack,ou=users,dc=example,dc=com
# cn: jack
# gidnumber: 501
# givenname: Jack
# homedirectory: /home/jack
# loginshell: /bin/sh
# objectclass: inetOrgPerson
# objectclass: posixAccount
# objectclass: top
# mail: jack.daniels@example.com
# sn: Jack Daniels
# uid: jack
# uidnumber: 1001
# userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
#

View File

@ -73,8 +73,8 @@ http {
location /authentication/verify {
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://auth/authentication/verify;
}

View File

@ -4,12 +4,14 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
var server = require('./lib/server');
var ldap = require('ldapjs');
var ldapjs = require('ldapjs');
var u2f = require('authdog');
var nodemailer = require('nodemailer');
var nedb = require('nedb');
var YAML = require('yamljs');
var session = require('express-session');
var winston = require('winston');
var speakeasy = require('speakeasy');
var config_path = process.argv[2];
if(!config_path) {
@ -22,20 +24,13 @@ console.log('Parse configuration file: %s', config_path);
var yaml_config = YAML.load(config_path);
var ldap_client = ldap.createClient({
url: config.ldap_url,
reconnect: true
});
ldap_client.on('error', function(err) {
console.error('LDAP Error:', err.message)
})
var deps = {};
deps.u2f = u2f;
deps.nedb = nedb;
deps.nodemailer = nodemailer;
deps.ldap = ldap;
deps.ldapjs = ldapjs;
deps.session = session;
deps.winston = winston;
deps.speakeasy = speakeasy;
server.run(yaml_config, ldap_client, deps);
server.run(yaml_config, deps);

View File

@ -4,17 +4,14 @@ var objectPath = require('object-path');
module.exports = function(yaml_config) {
return {
port: objectPath.get(yaml_config, 'port', 8080),
ldap_url: objectPath.get(yaml_config, 'ldap.url', 'ldap://127.0.0.1:389'),
ldap_user_search_base: objectPath.get(yaml_config, 'ldap.user_search_base'),
ldap_user_search_filter: objectPath.get(yaml_config, 'ldap.user_search_filter'),
ldap_user: objectPath.get(yaml_config, 'ldap.user'),
ldap_password: objectPath.get(yaml_config, 'ldap.password'),
ldap: objectPath.get(yaml_config, 'ldap', 'ldap://127.0.0.1:389'),
session_domain: objectPath.get(yaml_config, 'session.domain'),
session_secret: objectPath.get(yaml_config, 'session.secret'),
session_max_age: objectPath.get(yaml_config, 'session.expiration', 3600000), // in ms
store_directory: objectPath.get(yaml_config, 'store_directory'),
logs_level: objectPath.get(yaml_config, 'logs_level'),
notifier: objectPath.get(yaml_config, 'notifier'),
access_control: objectPath.get(yaml_config, 'access_control')
}
};

View File

@ -1,46 +1,66 @@
module.exports = {
validate: validate_credentials,
get_email: retrieve_email,
update_password: update_password
}
module.exports = Ldap;
var util = require('util');
var Promise = require('bluebird');
var exceptions = require('./exceptions');
var Dovehash = require('dovehash');
function validate_credentials(ldap_client, username, password, user_base, user_filter) {
// if not provided, default to cn
if(!user_filter) user_filter = 'cn';
function Ldap(deps, ldap_config) {
this.ldap_config = ldap_config;
var userDN = util.format("%s=%s,%s", user_filter, username, user_base);
console.log(userDN);
var bind_promised = Promise.promisify(ldap_client.bind, { context: ldap_client });
return bind_promised(userDN, password)
this.ldapjs = deps.ldapjs;
this.logger = deps.winston;
this.connect();
}
Ldap.prototype.connect = function() {
var ldap_client = this.ldapjs.createClient({
url: this.ldap_config.url,
reconnect: true
});
ldap_client.on('error', function(err) {
console.error('LDAP Error:', err.message)
});
this.ldap_client = Promise.promisifyAll(ldap_client);
}
Ldap.prototype._build_user_dn = function(username) {
var user_name_attr = this.ldap_config.user_name_attribute;
// if not provided, default to cn
if(!user_name_attr) user_name_attr = 'cn';
var additional_user_dn = this.ldap_config.additional_user_dn;
var base_dn = this.ldap_config.base_dn;
var user_dn = util.format("%s=%s", user_name_attr, username);
if(additional_user_dn) user_dn += util.format(",%s", additional_user_dn);
user_dn += util.format(',%s', base_dn);
return user_dn;
}
Ldap.prototype.bind = function(username, password) {
var user_dn = this._build_user_dn(username);
this.logger.debug('LDAP: Bind user %s', user_dn);
return this.ldap_client.bindAsync(user_dn, password)
.error(function(err) {
console.error(err);
throw new exceptions.LdapBindError(err.message);
});
}
function retrieve_email(ldap_client, username, user_base, user_filter) {
// if not provided, default to cn
if(!user_filter) user_filter = 'cn';
var userDN = util.format("%s=%s,%s", user_filter, username, user_base);
console.log(userDN);
var search_promised = Promise.promisify(ldap_client.search, { context: ldap_client });
var query = {};
query.sizeLimit = 1;
query.attributes = ['mail'];
Ldap.prototype._search_in_ldap = function(base, query) {
var that = this;
this.logger.debug('LDAP: Search for %s in %s', JSON.stringify(query), base);
return new Promise(function(resolve, reject) {
search_promised(userDN, query)
that.ldap_client.searchAsync(base, query)
.then(function(res) {
var doc;
var doc = [];
res.on('searchEntry', function(entry) {
doc = entry.object;
doc.push(entry.object);
});
res.on('error', function(err) {
reject(new exceptions.LdapSearchError(err));
@ -55,26 +75,80 @@ function retrieve_email(ldap_client, username, user_base, user_filter) {
});
}
function update_password(ldap_client, ldap, username, new_password, config) {
var user_filter = config.ldap_user_search_filter;
// if not provided, default to cn
if(!user_filter) user_filter = 'cn';
Ldap.prototype.get_groups = function(username) {
var user_dn = this._build_user_dn(username);
var group_name_attr = this.ldap_config.group_name_attribute;
if(!group_name_attr) group_name_attr = 'cn';
var additional_group_dn = this.ldap_config.additional_group_dn;
var base_dn = this.ldap_config.base_dn;
var group_dn = base_dn;
if(additional_group_dn)
group_dn = util.format('%s,', additional_group_dn) + group_dn;
var query = {};
query.scope = 'sub';
query.attributes = [group_name_attr];
query.filter = 'member=' + user_dn ;
var that = this;
this.logger.debug('LDAP: get groups of user %s', username);
return this._search_in_ldap(group_dn, query)
.then(function(docs) {
var groups = [];
for(var i = 0; i<docs.length; ++i) {
groups.push(docs[i].cn);
}
that.logger.debug('LDAP: got groups %s', groups);
return Promise.resolve(groups);
});
}
Ldap.prototype.get_emails = function(username) {
var that = this;
var user_dn = this._build_user_dn(username);
var query = {};
query.scope = 'base';
query.sizeLimit = 1;
query.attributes = ['mail'];
this.logger.debug('LDAP: get emails of user %s', username);
return this._search_in_ldap(user_dn, query)
.then(function(docs) {
var emails = [];
for(var i = 0; i<docs.length; ++i) {
if(typeof docs[i].mail === 'string')
emails.push(docs[i].mail);
else {
emails.concat(docs[i].mail);
}
}
that.logger.debug('LDAP: got emails %s', emails);
return Promise.resolve(emails);
});
}
Ldap.prototype.update_password = function(username, new_password) {
var user_dn = this._build_user_dn(username);
var userDN = util.format("%s=%s,%s", user_filter, username,
config.ldap_user_search_base);
var encoded_password = Dovehash.encode('SSHA', new_password);
var change = new ldap.Change({
var change = new this.ldapjs.Change({
operation: 'replace',
modification: {
userPassword: encoded_password
}
});
var modify_promised = Promise.promisify(ldap_client.modify, { context: ldap_client });
var bind_promised = Promise.promisify(ldap_client.bind, { context: ldap_client });
var that = this;
this.logger.debug('LDAP: update password of user %s', username);
return bind_promised(config.ldap_user, config.ldap_password)
this.logger.debug('LDAP: bind admin');
return this.ldap_client.bindAsync(this.ldap_config.user, this.ldap_config.password)
.then(function() {
return modify_promised(userDN, change);
that.logger.debug('LDAP: modify password');
return that.ldap_client.modifyAsync(user_dn, change);
});
}

View File

@ -2,11 +2,25 @@
module.exports = first_factor;
var exceptions = require('../exceptions');
var ldap = require('../ldap');
var objectPath = require('object-path');
var Promise = require('bluebird');
function get_allowed_domains(access_control, groups) {
var allowed_domains = [];
for(var i = 0; i<access_control.length; ++i) {
var rule = access_control[i];
if('group' in rule && 'allowed_domains' in rule) {
if(groups.indexOf(rule['group']) >= 0) {
var domains = rule.allowed_domains;
allowed_domains = allowed_domains.concat(domains);
}
}
}
return allowed_domains;
}
function first_factor(req, res) {
var logger = req.app.get('logger');
var username = req.body.username;
var password = req.body.password;
if(!username || !password) {
@ -15,59 +29,69 @@ function first_factor(req, res) {
return;
}
logger.info('1st factor: Starting authentication of user "%s"', username);
var ldap_client = req.app.get('ldap client');
var logger = req.app.get('logger');
var ldap = req.app.get('ldap');
var config = req.app.get('config');
var regulator = req.app.get('authentication regulator');
logger.info('1st factor: Starting authentication of user "%s"', username);
logger.debug('1st factor: Start bind operation against LDAP');
logger.debug('1st factor: username=%s', username);
logger.debug('1st factor: base_dn=%s', config.ldap_user_search_base);
logger.debug('1st factor: user_filter=%s', config.ldap_user_search_filter);
regulator.regulate(username)
.then(function() {
return ldap.validate(ldap_client, username, password, config.ldap_user_search_base, config.ldap_user_search_filter);
return ldap.bind(username, password);
})
.then(function() {
objectPath.set(req, 'session.auth_session.userid', username);
objectPath.set(req, 'session.auth_session.first_factor', true);
logger.info('1st factor: LDAP binding successful');
logger.debug('1st factor: Retrieve email from LDAP');
return ldap.get_email(ldap_client, username, config.ldap_user_search_base,
config.ldap_user_search_filter)
return Promise.join(ldap.get_emails(username), ldap.get_groups(username));
})
.then(function(doc) {
var email = objectPath.get(doc, 'mail');
logger.debug('1st factor: document=%s', JSON.stringify(doc));
logger.debug('1st factor: Retrieved email is %s', email);
.then(function(data) {
var emails = data[0];
var groups = data[1];
if(!emails && emails.length <= 0) throw new Error('No email found');
logger.debug('1st factor: Retrieved email are %s', emails);
objectPath.set(req, 'session.auth_session.email', emails[0]);
if(config.access_control) {
var allowed_domains = get_allowed_domains(config.access_control, groups);
logger.debug('1st factor: allowed domains are %s', allowed_domains);
objectPath.set(req, 'session.auth_session.allowed_domains',
allowed_domains);
}
else {
logger.debug('1st factor: no access control rules found.' +
'Default policy to allow all.');
}
objectPath.set(req, 'session.auth_session.email', email);
regulator.mark(username, true);
res.status(204);
res.send();
})
.catch(exceptions.LdapSearchError, function(err) {
logger.info('1st factor: Unable to retrieve email from LDAP', err);
logger.error('1st factor: Unable to retrieve email from LDAP', err);
res.status(500);
res.send();
})
.catch(exceptions.LdapBindError, function(err) {
logger.info('1st factor: LDAP binding failed');
logger.error('1st factor: LDAP binding failed');
logger.debug('1st factor: LDAP binding failed due to ', err);
regulator.mark(username, false);
res.status(401);
res.send('Bad credentials');
})
.catch(exceptions.AuthenticationRegulationError, function(err) {
logger.info('1st factor: the regulator rejected the authentication of user %s', username);
logger.error('1st factor: the regulator rejected the authentication of user %s', username);
logger.debug('1st factor: authentication rejected due to %s', err);
res.status(403);
res.send('Access has been restricted for a few minutes...');
})
.catch(function(err) {
logger.debug('1st factor: Unhandled error %s', err);
logger.error('1st factor: Unhandled error %s', err);
res.status(500);
res.send('Internal error');
});

View File

@ -1,7 +1,6 @@
var Promise = require('bluebird');
var objectPath = require('object-path');
var ldap = require('../ldap');
var exceptions = require('../exceptions');
var CHALLENGE = 'reset-password';
@ -24,16 +23,14 @@ function pre_check(req) {
return Promise.reject(err);
}
var ldap_client = req.app.get('ldap client');
var config = req.app.get('config');
var ldap = req.app.get('ldap');
return ldap.get_email(ldap_client, userid, config.ldap_user_search_base,
config.ldap_user_search_filter)
.then(function(doc) {
var email = objectPath.get(doc, 'mail');
return ldap.get_emails(userid)
.then(function(emails) {
if(!emails && emails.length <= 0) throw new Error('No email found');
var identity = {}
identity.email = email;
identity.email = emails[0];
identity.userid = userid;
return Promise.resolve(identity);
});
@ -53,15 +50,13 @@ function protect(fn) {
function post(req, res) {
var logger = req.app.get('logger');
var ldapjs = req.app.get('ldap');
var ldap_client = req.app.get('ldap client');
var ldap = req.app.get('ldap');
var new_password = objectPath.get(req, 'body.password');
var userid = objectPath.get(req, 'session.auth_session.identity_check.userid');
var config = req.app.get('config');
logger.info('POST reset-password: User %s wants to reset his/her password', userid);
ldap.update_password(ldap_client, ldapjs, userid, new_password, config)
ldap.update_password(userid, new_password)
.then(function() {
logger.info('POST reset-password: Password reset for user %s', userid);
objectPath.set(req, 'session.auth_session', undefined);

View File

@ -5,6 +5,8 @@ var objectPath = require('object-path');
var Promise = require('bluebird');
function verify_filter(req, res) {
var logger = req.app.get('logger');
if(!objectPath.has(req, 'session.auth_session'))
return Promise.reject('No auth_session variable');
@ -14,6 +16,23 @@ function verify_filter(req, res) {
if(!objectPath.has(req, 'session.auth_session.second_factor'))
return Promise.reject('No second factor variable');
if(!objectPath.has(req, 'session.auth_session.userid'))
return Promise.reject('No userid variable');
var config = req.app.get('config');
var access_control = config.access_control;
if(access_control) {
var allowed_domains = objectPath.get(req, 'session.auth_session.allowed_domains');
var host = objectPath.get(req, 'headers.host');
var domain = host.split(':')[0];
logger.debug('Trying to access domain: %s', domain);
logger.debug('User has access to %s', JSON.stringify(allowed_domains));
if(allowed_domains.indexOf(domain) < 0)
return Promise.reject('Access restricted by ACL rules');
}
if(!req.session.auth_session.first_factor ||
!req.session.auth_session.second_factor)
return Promise.reject('First or second factor not validated');
@ -28,6 +47,7 @@ function verify(req, res) {
res.send();
})
.catch(function(err) {
req.app.get('logger').error(err);
res.status(401);
res.send();
});

View File

@ -5,16 +5,15 @@ module.exports = {
var express = require('express');
var bodyParser = require('body-parser');
var speakeasy = require('speakeasy');
var path = require('path');
var winston = require('winston');
var UserDataStore = require('./user_data_store');
var Notifier = require('./notifier');
var AuthenticationRegulator = require('./authentication_regulator');
var setup_endpoints = require('./setup_endpoints');
var config_adapter = require('./config_adapter');
var Ldap = require('./ldap');
function run(yaml_config, ldap_client, deps, fn) {
function run(yaml_config, deps, fn) {
var config = config_adapter(yaml_config);
var view_directory = path.resolve(__dirname, '../views');
@ -45,17 +44,17 @@ function run(yaml_config, ldap_client, deps, fn) {
app.set('view engine', 'ejs');
// by default the level of logs is info
winston.level = config.logs_level || 'info';
deps.winston.level = config.logs_level || 'info';
var five_minutes = 5 * 60;
var data_store = new UserDataStore(deps.nedb, datastore_options);
var regulator = new AuthenticationRegulator(data_store, five_minutes);
var notifier = new Notifier(config.notifier, deps);
var ldap = new Ldap(deps, config.ldap);
app.set('logger', winston);
app.set('ldap', deps.ldap);
app.set('ldap client', ldap_client);
app.set('totp engine', speakeasy);
app.set('logger', deps.winston);
app.set('ldap', ldap);
app.set('totp engine', deps.speakeasy);
app.set('u2f', deps.u2f);
app.set('user data store', data_store);
app.set('notifier', notifier);

View File

@ -5,34 +5,27 @@ var assert = require('assert');
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');
describe('test the first factor validation route', function() {
var req, res;
var ldap_interface_mock;
var emails;
var search_res_ok;
var regulator;
var config;
beforeEach(function() {
ldap_interface_mock = {
bind: sinon.stub(),
search: sinon.stub()
ldap_interface_mock = sinon.createStubInstance(Ldap);
config = {
ldap: {
base_dn: 'ou=users,dc=example,dc=com',
user_name_attribute: 'uid'
}
var config = {
ldap_user_search_base: 'ou=users,dc=example,dc=com',
ldap_user_search_filter: 'uid'
}
var search_doc = {
object: {
mail: 'test_ok@example.com'
}
};
var search_res_ok = {};
search_res_ok.on = sinon.spy(function(event, fn) {
if(event != 'error') fn(search_doc);
});
ldap_interface_mock.search.yields(undefined, search_res_ok);
emails = [ 'test_ok@example.com' ];
groups = [ 'group1', 'group2' ];
regulator = {};
regulator.mark = sinon.stub();
@ -42,7 +35,7 @@ describe('test the first factor validation route', function() {
regulator.regulate.returns(Promise.resolve());
var app_get = sinon.stub();
app_get.withArgs('ldap client').returns(ldap_interface_mock);
app_get.withArgs('ldap').returns(ldap_interface_mock);
app_get.withArgs('config').returns(config);
app_get.withArgs('logger').returns(winston);
app_get.withArgs('authentication regulator').returns(regulator);
@ -75,43 +68,69 @@ describe('test the first factor validation route', function() {
assert.equal(204, res.status.getCall(0).args[0]);
resolve();
});
ldap_interface_mock.bind.yields(undefined);
ldap_interface_mock.bind.withArgs('username').returns(Promise.resolve());
ldap_interface_mock.get_emails.returns(Promise.resolve(emails));
first_factor(req, res);
});
});
it('should bind user based on LDAP DN', function(done) {
ldap_interface_mock.bind = sinon.spy(function(dn) {
if(dn == 'uid=username,ou=users,dc=example,dc=com') done();
it('should store the allowed domains in the auth session', function() {
config.access_control = [];
config.access_control.push({
group: 'group1',
allowed_domains: ['domain1.example.com', 'domain2.example.com']
});
return new Promise(function(resolve, reject) {
res.send = sinon.spy(function(data) {
assert.deepEqual(['domain1.example.com', 'domain2.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 retrieve email from LDAP', function(done) {
ldap_interface_mock.bind.yields(undefined);
ldap_interface_mock.search = sinon.spy(function(dn) {
if(dn == 'uid=username,ou=users,dc=example,dc=com') done();
});
res.send = sinon.spy(function(data) { done(); });
ldap_interface_mock.bind.returns(Promise.resolve());
ldap_interface_mock.get_emails = sinon.stub().withArgs('usernam').returns(Promise.resolve([{mail: ['test@example.com'] }]));
first_factor(req, res);
});
it('should return status code 401 when LDAP binding fails', function(done) {
it('should set email as session variables', function() {
return new Promise(function(resolve, reject) {
res.send = sinon.spy(function(data) {
assert.equal('test_ok@example.com', req.session.auth_session.email);
resolve();
});
var emails = [ 'test_ok@example.com' ];
ldap_interface_mock.bind.returns(Promise.resolve());
ldap_interface_mock.get_emails.returns(Promise.resolve(emails));
first_factor(req, res);
});
});
it('should return status code 401 when LDAP binding throws', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(401, res.status.getCall(0).args[0]);
assert.equal(regulator.mark.getCall(0).args[0], 'username');
done();
});
ldap_interface_mock.bind.yields('Bad credentials');
ldap_interface_mock.bind.throws(new exceptions.LdapBindError('Bad credentials'));
first_factor(req, res);
});
it('should return status code 500 when LDAP binding throws', function(done) {
it('should return status code 500 when LDAP search throws', function(done) {
res.send = sinon.spy(function(data) {
assert.equal(500, res.status.getCall(0).args[0]);
done();
});
ldap_interface_mock.bind.yields(undefined);
ldap_interface_mock.search.yields('error');
ldap_interface_mock.bind.returns(Promise.resolve());
ldap_interface_mock.get_emails.throws(new exceptions.LdapSearchError('err'));
first_factor(req, res);
});
@ -122,8 +141,8 @@ describe('test the first factor validation route', function() {
assert.equal(403, res.status.getCall(0).args[0]);
done();
});
ldap_interface_mock.bind.yields(undefined);
ldap_interface_mock.search.yields(undefined);
ldap_interface_mock.bind.returns(Promise.resolve());
ldap_interface_mock.get_emails.returns(Promise.resolve());
first_factor(req, res);
});
});

View File

@ -1,6 +1,8 @@
var reset_password = require('../../../src/lib/routes/reset_password');
var Ldap = require('../../../src/lib/ldap');
var sinon = require('sinon');
var winston = require('winston');
var reset_password = require('../../../src/lib/routes/reset_password');
var assert = require('assert');
describe('test reset password', function() {
@ -35,20 +37,30 @@ describe('test reset password', function() {
user_data_store.consume_identity_check_token = sinon.stub().returns(Promise.resolve({}));
req.app.get.withArgs('user data store').returns(user_data_store);
ldap = {};
ldap.Change = sinon.spy();
req.app.get.withArgs('ldap').returns(ldap);
config = {};
config.ldap = {};
config.ldap.base_dn = 'dc=example,dc=com';
config.ldap.user_name_attribute = 'cn';
req.app.get.withArgs('config').returns(config);
ldap_client = {};
ldap_client.bind = sinon.stub();
ldap_client.search = sinon.stub();
ldap_client.modify = sinon.stub();
req.app.get.withArgs('ldap client').returns(ldap_client);
ldap_client.on = sinon.spy();
config = {};
config.ldap_user_search_base = 'dc=example,dc=com';
config.ldap_user_search_filter = 'cn';
req.app.get.withArgs('config').returns(config);
ldapjs = {};
ldapjs.Change = sinon.spy();
ldapjs.createClient = sinon.spy(function() {
return ldap_client;
});
deps = {
ldapjs: ldapjs,
winston: winston
};
req.app.get.withArgs('ldap').returns(new Ldap(deps, config.ldap));
res = {};
res.send = sinon.spy();
@ -77,9 +89,8 @@ describe('test reset password', function() {
});
it('should perform a search in ldap to find email address', function(done) {
config.ldap_user_search_filter = 'uid';
config.ldap.user_name_attribute = 'uid';
ldap_client.search = sinon.spy(function(dn) {
console.log(dn);
if(dn == 'uid=user,dc=example,dc=com') done();
});
reset_password.icheck_interface.pre_check_callback(req);
@ -88,7 +99,7 @@ describe('test reset password', function() {
it('should returns identity when ldap replies', function(done) {
var doc = {};
doc.object = {};
doc.object.email = 'test@example.com';
doc.object.email = ['test@example.com'];
doc.object.userid = 'user';
var res = {};

View File

@ -2,19 +2,33 @@
var assert = require('assert');
var verify = require('../../../src/lib/routes/verify');
var sinon = require('sinon');
var winston = require('winston');
describe('test authentication token verification', function() {
var req, res;
var config_mock;
beforeEach(function() {
config_mock = {};
req = {};
res = {};
req.headers = {};
req.headers.host = 'secret.example.com';
req.app = {};
req.app.get = sinon.stub();
req.app.get.withArgs('config').returns(config_mock);
req.app.get.withArgs('logger').returns(winston);
res.status = sinon.spy();
});
it('should be already authenticated', function(done) {
req.session = {};
req.session.auth_session = {first_factor: true, second_factor: true};
req.session.auth_session = {
first_factor: true,
second_factor: true,
userid: 'myuser',
group: 'mygroup'
};
res.send = sinon.spy(function() {
assert.equal(204, res.status.getCall(0).args[0]);
@ -25,13 +39,13 @@ describe('test authentication token verification', function() {
});
describe('given different cases of session', function() {
function test_unauthorized(auth_session) {
function test_session(auth_session, status_code) {
return new Promise(function(resolve, reject) {
req.session = {};
req.session.auth_session = auth_session;
res.send = sinon.spy(function() {
assert.equal(401, res.status.getCall(0).args[0]);
assert.equal(status_code, res.status.getCall(0).args[0]);
resolve();
});
@ -39,6 +53,14 @@ describe('test authentication token verification', function() {
});
}
function test_unauthorized(auth_session) {
return test_session(auth_session, 401);
}
function test_authorized(auth_session) {
return test_session(auth_session, 204);
}
it('should not be authenticated when second factor is missing', function() {
return test_unauthorized({ first_factor: true, second_factor: false });
});
@ -47,6 +69,14 @@ describe('test authentication token verification', function() {
return test_unauthorized({ first_factor: false, second_factor: true });
});
it('should not be authenticated when userid is missing', function() {
return test_unauthorized({
first_factor: true,
second_factor: true,
group: 'mygroup',
});
});
it('should not be authenticated when first and second factor are missing', function() {
return test_unauthorized({ first_factor: false, second_factor: false });
});
@ -55,6 +85,34 @@ describe('test authentication token verification', function() {
return test_unauthorized(undefined);
});
it('should reply unauthorized when the domain is restricted', function() {
config_mock.access_control = [];
config_mock.access_control.push({
group: 'abc',
allowed_domains: ['secret.example.com']
});
return test_unauthorized({
first_factor: true,
second_factor: true,
userid: 'user',
allowed_domains: ['restricted.example.com']
});
});
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({
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

@ -27,11 +27,11 @@ describe('test config adapter', function() {
var config = config_adapter(yaml_config);
assert.equal(config.ldap_url, 'http://ldap');
assert.equal(config.ldap_user_search_base, 'ou=groups,dc=example,dc=com');
assert.equal(config.ldap_user_search_filter, 'uid');
assert.equal(config.ldap_user, 'admin');
assert.equal(config.ldap_password, 'pass');
assert.equal(config.ldap.url, 'http://ldap');
assert.equal(config.ldap.user_search_base, 'ou=groups,dc=example,dc=com');
assert.equal(config.ldap.user_search_filter, 'uid');
assert.equal(config.ldap.user, 'admin');
assert.equal(config.ldap.password, 'pass');
});
it('should get the session attributes', function() {
@ -64,4 +64,13 @@ describe('test config adapter', function() {
assert.equal(config.notifier, 'notifier');
});
it('should get the access_control config', function() {
yaml_config = {};
yaml_config.access_control = 'access_control';
var config = config_adapter(yaml_config);
assert.equal(config.access_control, 'access_control');
});
});

View File

@ -9,6 +9,7 @@ var sinon = require('sinon');
var tmp = require('tmp');
var nedb = require('nedb');
var session = require('express-session');
var winston = require('winston');
var PORT = 8050;
var BASE_URL = 'http://localhost:' + PORT;
@ -21,8 +22,14 @@ describe('test data persistence', function() {
var tmpDir;
var ldap_client = {
bind: sinon.stub(),
search: sinon.stub()
search: sinon.stub(),
on: sinon.spy()
};
var ldap = {
createClient: sinon.spy(function() {
return ldap_client;
})
}
var config;
before(function() {
@ -55,7 +62,7 @@ describe('test data persistence', function() {
totp_secret: 'totp_secret',
ldap: {
url: 'ldap://127.0.0.1:389',
user_search_base: 'ou=users,dc=example,dc=com',
base_dn: 'ou=users,dc=example,dc=com',
},
session: {
secret: 'session_secret',
@ -94,11 +101,13 @@ describe('test data persistence', function() {
deps.nedb = nedb;
deps.nodemailer = nodemailer;
deps.session = session;
deps.winston = winston;
deps.ldapjs = ldap;
var j1 = request.jar();
var j2 = request.jar();
return start_server(config, ldap_client, deps)
return start_server(config, deps)
.then(function(s) {
server = s;
return requests.login(j1);
@ -116,7 +125,7 @@ describe('test data persistence', function() {
return stop_server(server);
})
.then(function() {
return start_server(config, ldap_client, deps)
return start_server(config, deps)
})
.then(function(s) {
server = s;
@ -139,9 +148,9 @@ describe('test data persistence', function() {
});
});
function start_server(config, ldap_client, deps) {
function start_server(config, deps) {
return new Promise(function(resolve, reject) {
var s = server.run(config, ldap_client, deps);
var s = server.run(config, deps);
resolve(s);
});
}

View File

@ -1,185 +1,232 @@
var ldap = require('../../src/lib/ldap');
var Ldap = require('../../src/lib/ldap');
var sinon = require('sinon');
var Promise = require('bluebird');
var assert = require('assert');
var ldapjs = require('ldapjs');
var winston = require('winston');
describe('test ldap validation', function() {
var ldap_client;
var ldap, ldapjs;
var ldap_config;
beforeEach(function() {
ldap_client = {
bind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
Change: sinon.spy()
on: sinon.stub()
};
ldapjs = {
Change: sinon.spy(),
createClient: sinon.spy(function() {
return ldap_client;
  })
}
ldap_config = {
url: 'http://localhost:324',
user: 'admin',
password: 'password',
base_dn: 'dc=example,dc=com',
additional_user_dn: 'ou=users'
};
var deps = {};
deps.ldapjs = ldapjs;
deps.winston = winston;
ldap = new Ldap(deps, ldap_config);
return ldap.connect();
});
describe('test binding', test_binding);
describe('test get email', test_get_email);
describe('test get emails from username', test_get_emails);
describe('test get groups from username', test_get_groups);
describe('test update password', test_update_password);
function test_binding() {
function test_validate() {
var username = 'user';
var password = 'password';
var users_dn = 'dc=example,dc=com';
return ldap.validate(ldap_client, username, password, users_dn);
function test_bind() {
var username = "username";
var password = "password";
return ldap.bind(username, password);
}
it('should bind the user if good credentials provided', function() {
ldap_client.bind.yields();
return test_validate();
return test_bind();
});
it('should bind the user with correct DN', function(done) {
it('should bind the user with correct DN', function() {
ldap_config.user_name_attribute = 'uid';
var username = 'user';
var password = 'password';
var user_search_base = 'dc=example,dc=com';
var user_search_filter = 'uid';
ldap_client.bind = sinon.spy(function(dn) {
if(dn == 'uid=user,dc=example,dc=com') done();
});
ldap.validate(ldap_client, username, password, user_search_base,
user_search_filter);
ldap_client.bind.withArgs('uid=user,ou=users,dc=example,dc=com').yields();
return ldap.bind(username, password);
});
it('should default to cn user search filter if no filter provided', function(done) {
it('should default to cn user search filter if no filter provided', function() {
var username = 'user';
var password = 'password';
var user_search_base = 'dc=example,dc=com';
ldap_client.bind = sinon.spy(function(dn) {
if(dn == 'cn=user,dc=example,dc=com') done();
});
ldap.validate(ldap_client, username, password, user_search_base,
undefined);
});
// cover an issue with promisify context
it('should promisify correctly', function() {
function LdapClient() {
this.test = 'abc';
}
LdapClient.prototype.bind = function(username, password, fn) {
assert.equal('abc', this.test);
fn();
}
ldap_client = new LdapClient();
return test_validate();
ldap_client.bind.withArgs('cn=user,ou=users,dc=example,dc=com').yields();
return ldap.bind(username, password);
});
it('should not bind the user if wrong credentials provided', function() {
ldap_client.bind.yields('wrong credentials');
var promise = test_validate();
var promise = test_bind();
return promise.catch(function() {
return Promise.resolve();
});
});
}
function test_get_email() {
it('should retrieve the email of an existing user', function() {
var expected_doc = {};
function test_get_emails() {
var res_emitter;
var expected_doc;
beforeEach(function() {
expected_doc = {};
expected_doc.object = {};
expected_doc.object.mail = 'user@example.com';
var res_emitter = {};
res_emitter = {};
res_emitter.on = sinon.spy(function(event, fn) {
if(event != 'error') fn(expected_doc)
});
});
it('should retrieve the email of an existing user', function() {
ldap_client.search.yields(undefined, res_emitter);
return ldap.get_email(ldap_client, 'user', 'dc=example,dc=com')
.then(function(doc) {
assert.deepEqual(doc, expected_doc.object);
return ldap.get_emails('user')
.then(function(emails) {
assert.deepEqual(emails, [expected_doc.object.mail]);
return Promise.resolve();
})
});
it('should use the user filter', function(done) {
ldap_client.search = sinon.spy(function(dn) {
if(dn == 'uid=username,ou=users,dc=example,dc=com') done();
it('should retrieve email for user with uid name attribute', function() {
ldap_config.user_name_attribute = 'uid';
ldap_client.search.withArgs('uid=username,ou=users,dc=example,dc=com').yields(undefined, res_emitter);
return ldap.get_emails('username')
.then(function(emails) {
assert.deepEqual(emails, ['user@example.com']);
return Promise.resolve();
});
ldap.get_email(ldap_client, 'username', 'ou=users,dc=example,dc=com',
'uid')
});
it('should fail on error with search method', function(done) {
it('should fail on error with search method', function() {
var expected_doc = {};
expected_doc.mail = [];
expected_doc.mail.push('user@example.com');
ldap_client.search.yields('error');
ldap.get_email(ldap_client, 'user', 'dc=example,dc=com')
return ldap.get_emails('user')
.catch(function() {
return Promise.resolve();
})
});
}
function test_get_groups() {
var res_emitter;
var expected_doc1, expected_doc2;
beforeEach(function() {
expected_doc1 = {};
expected_doc1.object = {};
expected_doc1.object.cn = 'group1';
expected_doc2 = {};
expected_doc2.object = {};
expected_doc2.object.cn = 'group2';
res_emitter = {};
res_emitter.on = sinon.spy(function(event, fn) {
if(event != 'error') fn(expected_doc1);
if(event != 'error') fn(expected_doc2);
});
});
it('should retrieve the groups of an existing user', function() {
ldap_client.search.yields(undefined, res_emitter);
return ldap.get_groups('user')
.then(function(groups) {
assert.deepEqual(groups, ['group1', 'group2']);
return Promise.resolve();
});
});
it('should reduce the scope to additional_group_dn', function(done) {
ldap_config.additional_group_dn = 'ou=groups';
ldap_client.search = sinon.spy(function(base_dn) {
assert.equal(base_dn, 'ou=groups,dc=example,dc=com');
done();
});
ldap.get_groups('user');
});
it('should use default group_name_attr if not provided', function(done) {
ldap_client.search = sinon.spy(function(base_dn, query) {
assert.equal(base_dn, 'dc=example,dc=com');
assert.equal(query.filter, 'member=cn=user,ou=users,dc=example,dc=com');
assert.deepEqual(query.attributes, ['cn']);
done();
});
ldap.get_groups('user');
});
it('should fail on error with search method', function() {
ldap_client.search.yields('error');
return ldap.get_groups('user')
.catch(function() {
return Promise.resolve();
})
});
}
function test_update_password() {
it('should update the password successfully', function(done) {
it('should update the password successfully', function() {
var change = {};
change.operation = 'replace';
change.modification = {};
change.modification.userPassword = 'new-password';
var config = {};
config.ldap_user_search_base = 'dc=example,dc=com';
config.ldap_user = 'admin';
var userdn = 'cn=user,dc=example,dc=com';
var ldapjs = {};
ldapjs.Change = sinon.spy();
var userdn = 'cn=user,ou=users,dc=example,dc=com';
ldap_client.bind.yields(undefined);
ldap_client.modify.yields(undefined);
ldap.update_password(ldap_client, ldapjs, 'user', 'new-password', config)
return ldap.update_password('user', 'new-password')
.then(function() {
assert.deepEqual(ldap_client.modify.getCall(0).args[0], userdn);
assert.deepEqual(ldapjs.Change.getCall(0).args[0].operation, change.operation);
var userPassword = ldapjs.Change.getCall(0).args[0].modification.userPassword;
assert(/{SSHA}/.test(userPassword));
done();
return Promise.resolve();
})
});
it('should fail when ldap throws an error', function(done) {
it('should fail when ldap throws an error', function() {
ldap_client.bind.yields(undefined);
ldap_client.modify.yields('Error');
var config = {};
config.ldap_users_dn = 'dc=example,dc=com';
config.ldap_user = 'admin';
var ldapjs = {};
ldapjs.Change = sinon.spy();
ldap.update_password(ldap_client, ldapjs, 'user', 'new-password', config)
return ldap.update_password('user', 'new-password')
.catch(function() {
done();
return Promise.resolve();
})
});
it('should use the user filter', function(done) {
var ldapjs = {};
ldapjs.Change = sinon.spy();
var config = {};
config.ldap_user_search_base = 'ou=users,dc=example,dc=com';
config.ldap_user_search_filter = 'uid';
config.ldap_user = 'admin';
it('should update password of user using particular user name attribute', function() {
ldap_config.user_name_attribute = 'uid';
ldap_client.bind.yields(undefined);
ldap_client.modify = sinon.spy(function(dn) {
if(dn == 'uid=username,ou=users,dc=example,dc=com') done();
});
ldap.update_password(ldap_client, ldapjs, 'username', 'newpass', config)
ldap_client.modify.withArgs('uid=username,ou=users,dc=example,dc=com').yields();
return ldap.update_password('username', 'newpass');
});
}
});

View File

@ -1,5 +1,6 @@
var server = require('../../src/lib/server');
var Ldap = require('../../src/lib/ldap');
var Promise = require('bluebird');
var request = Promise.promisifyAll(require('request'));
@ -8,6 +9,9 @@ var speakeasy = require('speakeasy');
var sinon = require('sinon');
var MockDate = require('mockdate');
var session = require('express-session');
var winston = require('winston');
var speakeasy = require('speakeasy');
var ldapjs = require('ldapjs');
var PORT = 8090;
var BASE_URL = 'http://localhost:' + PORT;
@ -19,14 +23,6 @@ describe('test the server', function() {
var u2f, nedb;
var transporter;
var collection;
var ldap_client = {
bind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
};
var ldap = {
Change: sinon.spy()
}
beforeEach(function(done) {
var config = {
@ -34,8 +30,8 @@ describe('test the server', function() {
totp_secret: 'totp_secret',
ldap: {
url: 'ldap://127.0.0.1:389',
user_search_base: 'ou=users,dc=example,dc=com',
user_search_filter: 'cn',
base_dn: 'ou=users,dc=example,dc=com',
user_name_attribute: 'cn',
user: 'cn=admin,dc=example,dc=com',
password: 'password',
},
@ -52,6 +48,19 @@ describe('test the server', function() {
}
};
var ldap_client = {
bind: sinon.stub(),
search: sinon.stub(),
modify: sinon.stub(),
on: sinon.spy()
};
var ldap = {
Change: sinon.spy(),
createClient: sinon.spy(function() {
return ldap_client;
})
};
u2f = {};
u2f.startRegistration = sinon.stub();
u2f.finishRegistration = sinon.stub();
@ -68,15 +77,15 @@ describe('test the server', function() {
return transporter;
  });
var search_doc = {
ldap_document = {
object: {
mail: 'test_ok@example.com'
mail: 'test_ok@example.com',
}
};
var search_res = {};
search_res.on = sinon.spy(function(event, fn) {
if(event != 'error') fn(search_doc);
if(event != 'error') fn(ldap_document);
});
ldap_client.bind.withArgs('cn=test_ok,ou=users,dc=example,dc=com',
@ -94,10 +103,12 @@ describe('test the server', function() {
deps.u2f = u2f;
deps.nedb = nedb;
deps.nodemailer = nodemailer;
deps.ldap = ldap;
deps.ldapjs = ldap;
deps.session = session;
deps.winston = winston;
deps.speakeasy = speakeasy;
_server = server.run(config, ldap_client, deps, function() {
_server = server.run(config, deps, function() {
done();
});
});
@ -352,7 +363,6 @@ describe('test the server', function() {
return requests.failing_first_factor(j);
})
.then(function(res) {
console.log('coucou');
assert.equal(res.statusCode, 401, 'first factor failed');
return requests.failing_first_factor(j);
})

View File

@ -26,7 +26,12 @@ describe('test server configuration', function() {
deps = {};
deps.nedb = require('nedb');
deps.winston = sinon.spy();
deps.nodemailer = nodemailer;
deps.ldapjs = {};
deps.ldapjs.createClient = sinon.spy(function() {
return { on: sinon.spy() };
});
deps.session = sinon.spy(function() {
return function(req, res, next) { next(); };
});
@ -36,7 +41,9 @@ describe('test server configuration', function() {
it('should set cookie scope to domain set in the config', function() {
config.session = {};
config.session.domain = 'example.com';
server.run(config, undefined, deps);
config.ldap = {};
config.ldap.url = 'http://ldap';
server.run(config, deps);
assert(deps.session.calledOnce);
assert.equal(deps.session.getCall(0).args[0].cookie.domain, 'example.com');