diff --git a/README.md b/README.md index 584b6f8c..52d8e0a3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.template.yml b/config.template.yml index e1ab5244..a70c8d3c 100644 --- a/config.template.yml +++ b/config.template.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index bf30b905..bfaaeb33 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/example/ldap/base.ldif b/example/ldap/base.ldif index 1e46c0ef..07d4e5a8 100644 --- a/example/ldap/base.ldif +++ b/example/ldap/base.ldif @@ -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= +# diff --git a/example/nginx_conf/index.html b/example/nginx_conf/index.html index c59b1e20..6eb9a534 100644 --- a/example/nginx_conf/index.html +++ b/example/nginx_conf/index.html @@ -3,8 +3,81 @@ Home page - You need to log in to access the secret!

- But you can also access it from another domain or still another one.

- You can also log off by visiting the following link. +

Access the secret

+ You need to log in to access the secret!

+ Try to access it via one of the following links.
+ + + You can also log off by visiting the following link. + +

List of users

+ Here is the list of credentials you can log in with to test access control. + + + +

Access control rules

+ + + diff --git a/example/nginx_conf/nginx.conf b/example/nginx_conf/nginx.conf index 5ffafad6..4cce9207 100644 --- a/example/nginx_conf/nginx.conf +++ b/example/nginx_conf/nginx.conf @@ -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; } diff --git a/example/nginx_conf/secret.html b/example/nginx_conf/secret.html index b0f43b63..8b44155a 100644 --- a/example/nginx_conf/secret.html +++ b/example/nginx_conf/secret.html @@ -3,6 +3,7 @@ Secret - This is a very important secret! + This is a very important secret!
+ Go back to home page. diff --git a/src/index.js b/src/index.js index 0719b0ab..e593e48b 100755 --- a/src/index.js +++ b/src/index.js @@ -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); diff --git a/src/lib/access_control.js b/src/lib/access_control.js new file mode 100644 index 00000000..e185eb7a --- /dev/null +++ b/src/lib/access_control.js @@ -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= 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'); }); diff --git a/src/lib/routes/reset_password.js b/src/lib/routes/reset_password.js index 8b46cf15..dd26bf5c 100644 --- a/src/lib/routes/reset_password.js +++ b/src/lib/routes/reset_password.js @@ -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); diff --git a/src/lib/routes/verify.js b/src/lib/routes/verify.js index 06332827..0bea86e2 100644 --- a/src/lib/routes/verify.js +++ b/src/lib/routes/verify.js @@ -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(); }); diff --git a/src/lib/server.js b/src/lib/server.js index a828d670..f4c1ec83 100644 --- a/src/lib/server.js +++ b/src/lib/server.js @@ -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); diff --git a/test/unitary/routes/test_first_factor.js b/test/unitary/routes/test_first_factor.js index 548b6dd7..7f500fc8 100644 --- a/test/unitary/routes/test_first_factor.js +++ b/test/unitary/routes/test_first_factor.js @@ -5,35 +5,30 @@ 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(); regulator.regulate = 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); }); }); diff --git a/test/unitary/routes/test_reset_password.js b/test/unitary/routes/test_reset_password.js index 14bff5a3..efac684d 100644 --- a/test/unitary/routes/test_reset_password.js +++ b/test/unitary/routes/test_reset_password.js @@ -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 = {}; diff --git a/test/unitary/routes/test_verify.js b/test/unitary/routes/test_verify.js index 2e057a1d..e4987540 100644 --- a/test/unitary/routes/test_verify.js +++ b/test/unitary/routes/test_verify.js @@ -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 }); }); diff --git a/test/unitary/test_access_control.js b/test/unitary/test_access_control.js new file mode 100644 index 00000000..c0a496c7 --- /dev/null +++ b/test/unitary/test_access_control.js @@ -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)); + }); + }); +}); diff --git a/test/unitary/test_config_adapter.js b/test/unitary/test_config_adapter.js new file mode 100644 index 00000000..5ffcc84a --- /dev/null +++ b/test/unitary/test_config_adapter.js @@ -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'); + }); +}); diff --git a/test/unitary/test_data_persistence.js b/test/unitary/test_data_persistence.js index a20b323a..35c2c980 100644 --- a/test/unitary/test_data_persistence.js +++ b/test/unitary/test_data_persistence.js @@ -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); }); } diff --git a/test/unitary/test_ldap.js b/test/unitary/test_ldap.js index 9d301d23..c7fff8f6 100644 --- a/test/unitary/test_ldap.js +++ b/test/unitary/test_ldap.js @@ -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'); }); } }); diff --git a/test/unitary/test_server.js b/test/unitary/test_server.js index f9252b84..33bf25c9 100644 --- a/test/unitary/test_server.js +++ b/test/unitary/test_server.js @@ -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); }) diff --git a/test/unitary/test_server_config.js b/test/unitary/test_server_config.js index 4d6f9212..b5c26b5e 100644 --- a/test/unitary/test_server_config.js +++ b/test/unitary/test_server_config.js @@ -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');