Merge pull request #21 from clems4ever/access-control

Adding access control in config to allow per-user and per-group subdomain restrictions
This commit is contained in:
Clément Michaud 2017-03-25 19:23:43 +01:00 committed by GitHub
commit 1910ad520d
24 changed files with 1112 additions and 311 deletions

View File

@ -40,6 +40,9 @@ Add the following lines to your /etc/hosts to simulate multiple subdomains
127.0.0.1 secret.test.local
127.0.0.1 secret1.test.local
127.0.0.1 secret2.test.local
127.0.0.1 home.test.local
127.0.0.1 mx1.mail.test.local
127.0.0.1 mx2.mail.test.local
127.0.0.1 auth.test.local
Then, type the following command to build and deploy the services:
@ -48,15 +51,28 @@ Then, type the following command to build and deploy the services:
docker-compose up -d
After few seconds the services should be running and you should be able to visit
[https://secret.test.local:8080/](https://secret.test.local:8080/).
[https://home.test.local:8080/](https://home.test.local:8080/).
Normally, a self-signed certificate exception should appear, it has to be
accepted before getting to the login page:
![first-factor-page](https://raw.githubusercontent.com/clems4ever/authelia/master/images/first_factor.png)
### 1st factor: LDAP
An LDAP server has been deployed for you with the following credentials: **user/password**.
### 1st factor: LDAP and ACL
An LDAP server has been deployed for you with the following credentials and
access control list:
- **john / password** is in the admin group and has access to the secret from
any subdomain.
- **bob / password** is in the dev group and has access to the secret from
- [secret.test.local](https://secret.test.local:8080/secret.html)
- [secret2.test.local](https://secret2.test.local:8080/secret.html)
- [home.test.local](https://home.test.local:8080/secret.html)
- [\*.mail.test.local](https://mx1.mail.test.local:8080/secret.html)
- **harry / password** is not in a group but has rules giving him has access to
the secret from
- [secret1.test.local](https://secret1.test.local:8080/secret.html)
- [home.test.local](https://home.test.local:8080/secret.html)
Type them in the login page and validate. Then, the second factor page should
have appeared as shown below.
@ -99,6 +115,12 @@ email address. For the sake of the example, the email is delivered in the file
./notifications/notification.txt.
Paste the link in your browser and you should be able to reset the password.
### Access Control
With **Authelia**, you can define your own access control rules for restricting
the access to certain subdomains to your users. Those rules are defined in the
configuration file and can be either default, per-user or per-group policies.
Check out the *config.template.yml* to see how they are defined.
## Documentation
### Configuration
The configuration of the server is defined in the file

View File

@ -1,17 +1,67 @@
# The port to listen on
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 you can use to restrict the user access.
# Default (anyone), per-user or per-group rules can be defined.
#
# If 'access_control' is not defined, ACL rules are disabled and default policy
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined below.
# If no rule is provided, all domains are denied.
#
# '*' means 'any' subdomains and matches any string. It must stand at the
# beginning of the pattern.
access_control:
default:
- home.test.local
groups:
admin:
- '*.test.local'
dev:
- secret.test.local
- secret2.test.local
users:
harry:
- secret1.test.local
bob:
- '*.mail.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

@ -3,8 +3,81 @@
<title>Home page</title>
</head>
<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/>
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 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>.
<h1>Access the secret</h1>
You need to log in to access the secret!<br/><br/>
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>
</html>

View File

@ -60,7 +60,9 @@ http {
listen 443 ssl;
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_certificate /etc/ssl/server.crt;
@ -73,8 +75,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

@ -3,6 +3,7 @@
<title>Secret</title>
</head>
<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>
</html>

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,35 +24,13 @@ console.log('Parse configuration file: %s', config_path);
var yaml_config = YAML.load(config_path);
var config = {
port: process.env.PORT || 8080,
ldap_url: yaml_config.ldap.url || 'ldap://127.0.0.1:389',
ldap_user_search_base: yaml_config.ldap.user_search_base,
ldap_user_search_filter: yaml_config.ldap.user_search_filter,
ldap_user: yaml_config.ldap.user,
ldap_password: yaml_config.ldap.password,
session_domain: yaml_config.session.domain,
session_secret: yaml_config.session.secret,
session_max_age: yaml_config.session.expiration || 3600000, // in ms
store_directory: yaml_config.store_directory,
logs_level: yaml_config.logs_level,
notifier: yaml_config.notifier,
}
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(config, ldap_client, deps);
server.run(yaml_config, deps);

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 ['*'];
}

17
src/lib/config_adapter.js Normal file
View File

@ -0,0 +1,17 @@
var objectPath = require('object-path');
module.exports = function(yaml_config) {
return {
port: objectPath.get(yaml_config, 'port', 8080),
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,29 @@
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, username, groups) {
var allowed_domains = [];
for(var i = 0; i<access_control.length; ++i) {
var rule = access_control[i];
if('allowed_domains' in rule) {
if('group' in rule && groups.indexOf(rule['group']) >= 0) {
var domains = rule.allowed_domains;
allowed_domains = allowed_domains.concat(domains);
}
else if('user' in rule && username == rule['user']) {
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 +33,70 @@ 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');
var acl_builder = req.app.get('access control').builder;
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];
var allowed_domains;
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) {
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);
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,21 @@ 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');
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)
return Promise.reject('First or second factor not validated');
@ -28,6 +45,7 @@ function verify(req, res) {
res.send();
})
.catch(function(err) {
req.app.get('logger').error(err);
res.status(401);
res.send();
});

View File

@ -5,15 +5,18 @@ 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');
var AccessControl = require('./access_control');
function run(yaml_config, deps, fn) {
var config = config_adapter(yaml_config);
function run(config, ldap_client, deps, fn) {
var view_directory = path.resolve(__dirname, '../views');
var public_html_directory = path.resolve(__dirname, '../public_html');
var datastore_options = {};
@ -42,22 +45,24 @@ function run(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);
var access_control = AccessControl(deps.winston, config.access_control);
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);
app.set('authentication regulator', regulator);
app.set('config', config);
app.set('access control', access_control);
setup_endpoints(app);

View File

@ -5,34 +5,29 @@ 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');
var AccessControl = require('../../../src/lib/access_control');
describe('test the first factor validation route', function() {
var req, res;
var ldap_interface_mock;
var emails;
var search_res_ok;
var regulator;
var access_control;
var config;
beforeEach(function() {
ldap_interface_mock = {
bind: sinon.stub(),
search: sinon.stub()
}
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'
ldap_interface_mock = sinon.createStubInstance(Ldap);
config = {
ldap: {
base_dn: 'ou=users,dc=example,dc=com',
user_name_attribute: 'uid'
}
};
}
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();
@ -41,11 +36,22 @@ 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()
}
};
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);
app_get.withArgs('access control').returns(access_control);
req = {
app: {
@ -75,43 +81,83 @@ 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();
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);
});
});
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 +168,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,41 @@
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;
var acl_matcher;
beforeEach(function() {
acl_matcher = {
is_domain_allowed: sinon.stub().returns(true)
};
var access_control = {
matcher: acl_matcher
}
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);
req.app.get.withArgs('access control').returns(access_control);
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',
allowed_domains: ['*']
};
res.send = sinon.spy(function() {
assert.equal(204, res.status.getCall(0).args[0]);
@ -25,13 +47,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 +61,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 +77,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 +93,25 @@ 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

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

View File

@ -0,0 +1,76 @@
var assert = require('assert');
var config_adapter = require('../../src/lib/config_adapter');
describe('test config adapter', function() {
it('should read the port from the yaml file', function() {
yaml_config = {};
yaml_config.port = 7070;
var config = config_adapter(yaml_config);
assert.equal(config.port, 7070);
});
it('should default the port to 8080 if not provided', function() {
yaml_config = {};
var config = config_adapter(yaml_config);
assert.equal(config.port, 8080);
});
it('should get the ldap attributes', function() {
yaml_config = {};
yaml_config.ldap = {};
yaml_config.ldap.url = 'http://ldap';
yaml_config.ldap.user_search_base = 'ou=groups,dc=example,dc=com';
yaml_config.ldap.user_search_filter = 'uid';
yaml_config.ldap.user = 'admin';
yaml_config.ldap.password = 'pass';
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');
});
it('should get the session attributes', function() {
yaml_config = {};
yaml_config.session = {};
yaml_config.session.domain = 'example.com';
yaml_config.session.secret = 'secret';
yaml_config.session.expiration = 3600;
var config = config_adapter(yaml_config);
assert.equal(config.session_domain, 'example.com');
assert.equal(config.session_secret, 'secret');
assert.equal(config.session_max_age, 3600);
});
it('should get the log level', function() {
yaml_config = {};
yaml_config.logs_level = 'debug';
var config = config_adapter(yaml_config);
assert.equal(config.logs_level, 'debug');
});
it('should get the notifier config', function() {
yaml_config = {};
yaml_config.notifier = 'notifier';
var config = config_adapter(yaml_config);
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() {
@ -53,10 +60,14 @@ describe('test data persistence', function() {
config = {
port: PORT,
totp_secret: 'totp_secret',
ldap_url: 'ldap://127.0.0.1:389',
ldap_user_search_base: 'ou=users,dc=example,dc=com',
session_secret: 'session_secret',
session_max_age: 50000,
ldap: {
url: 'ldap://127.0.0.1:389',
base_dn: 'ou=users,dc=example,dc=com',
},
session: {
secret: 'session_secret',
expiration: 50000,
},
store_directory: tmpDir.name,
notifier: { gmail: { user: 'user@example.com', pass: 'password' } }
};
@ -90,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);
@ -112,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;
@ -135,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,26 +23,22 @@ 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 = {
port: PORT,
totp_secret: 'totp_secret',
ldap_url: 'ldap://127.0.0.1:389',
ldap_user_search_base: 'ou=users,dc=example,dc=com',
ldap_user_search_filter: 'cn',
ldap_user: 'cn=admin,dc=example,dc=com',
ldap_password: 'password',
session_secret: 'session_secret',
session_max_age: 50000,
ldap: {
url: 'ldap://127.0.0.1:389',
base_dn: 'ou=users,dc=example,dc=com',
user_name_attribute: 'cn',
user: 'cn=admin,dc=example,dc=com',
password: 'password',
},
session: {
secret: 'session_secret',
expiration: 50000,
},
store_in_memory: true,
notifier: {
gmail: {
@ -48,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();
@ -64,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',
@ -90,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();
});
});
@ -348,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(); };
});
@ -34,8 +39,11 @@ describe('test server configuration', function() {
it('should set cookie scope to domain set in the config', function() {
config.session_domain = 'example.com';
server.run(config, undefined, deps);
config.session = {};
config.session.domain = 'example.com';
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');