diff --git a/src/lib/LdapClient.ts b/src/lib/LdapClient.ts new file mode 100644 index 00000000..27638430 --- /dev/null +++ b/src/lib/LdapClient.ts @@ -0,0 +1,169 @@ + +import util = require("util"); +import BluebirdPromise = require("bluebird"); +import exceptions = require("./Exceptions"); +import Dovehash = require("dovehash"); +import ldapjs = require("ldapjs"); + +import { EventEmitter } from "events"; +import { LdapConfiguration } from "./Configuration"; +import { Ldapjs } from "../types/Dependencies"; +import { ILogger } from "./ILogger"; + +interface SearchEntry { + object: any; +} + +export class LdapClient { + options: LdapConfiguration; + ldapjs: Ldapjs; + logger: ILogger; + client: ldapjs.ClientAsync; + + constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: ILogger) { + this.options = options; + this.ldapjs = ldapjs; + this.logger = logger; + + this.connect(); + } + + connect(): void { + const ldap_client = this.ldapjs.createClient({ + url: this.options.url, + reconnect: true + }); + + ldap_client.on("error", function (err: Error) { + console.error("LDAP Error:", err.message); + }); + + this.client = BluebirdPromise.promisifyAll(ldap_client) as ldapjs.ClientAsync; + } + + private build_user_dn(username: string): string { + let user_name_attr = this.options.user_name_attribute; + // if not provided, default to cn + if (!user_name_attr) user_name_attr = "cn"; + + const additional_user_dn = this.options.additional_user_dn; + const base_dn = this.options.base_dn; + + let 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; + } + + bind(username: string, password: string): BluebirdPromise { + const user_dn = this.build_user_dn(username); + + this.logger.debug("LDAP: Bind user %s", user_dn); + return this.client.bindAsync(user_dn, password) + .error(function (err) { + throw new exceptions.LdapBindError(err.message); + }); + } + + private search_in_ldap(base: string, query: ldapjs.SearchOptions): BluebirdPromise { + this.logger.debug("LDAP: Search for %s in %s", JSON.stringify(query), base); + return new BluebirdPromise((resolve, reject) => { + this.client.searchAsync(base, query) + .then(function (res: EventEmitter) { + const doc: SearchEntry[] = []; + res.on("searchEntry", function (entry: SearchEntry) { + doc.push(entry.object); + }); + res.on("error", function (err: Error) { + reject(err); + }); + res.on("end", function () { + resolve(doc); + }); + }) + .catch(function (err) { + reject(err); + }); + }); + } + + get_groups(username: string): BluebirdPromise { + const user_dn = this.build_user_dn(username); + + let group_name_attr = this.options.group_name_attribute; + if (!group_name_attr) group_name_attr = "cn"; + + const additional_group_dn = this.options.additional_group_dn; + const base_dn = this.options.base_dn; + + let group_dn = base_dn; + if (additional_group_dn) + group_dn = util.format("%s,", additional_group_dn) + group_dn; + + const query = { + scope: "sub", + attributes: [group_name_attr], + filter: "member=" + user_dn + }; + + const that = this; + this.logger.debug("LDAP: get groups of user %s", username); + return this.search_in_ldap(group_dn, query) + .then(function (docs) { + const groups = []; + for (let i = 0; i < docs.length; ++i) { + groups.push(docs[i].cn); + } + that.logger.debug("LDAP: got groups %s", groups); + return Promise.resolve(groups); + }); + } + + get_emails(username: string): BluebirdPromise { + const that = this; + const user_dn = this.build_user_dn(username); + + const query = { + scope: "base", + sizeLimit: 1, + attributes: ["mail"] + }; + + this.logger.debug("LDAP: get emails of user %s", username); + return this.search_in_ldap(user_dn, query) + .then(function (docs) { + const emails = []; + for (let 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); + }); + } + + update_password(username: string, new_password: string): BluebirdPromise { + const user_dn = this.build_user_dn(username); + + const encoded_password = Dovehash.encode("SSHA", new_password); + const change = { + operation: "replace", + modification: { + userPassword: encoded_password + } + }; + + const that = this; + this.logger.debug("LDAP: update password of user %s", username); + + this.logger.debug("LDAP: bind admin"); + return this.client.bindAsync(this.options.user, this.options.password) + .then(function () { + that.logger.debug("LDAP: modify password"); + return that.client.modifyAsync(user_dn, change); + }); + } +} diff --git a/src/lib/Server.ts b/src/lib/Server.ts index 4e81fa1b..15c03edd 100644 --- a/src/lib/Server.ts +++ b/src/lib/Server.ts @@ -8,6 +8,7 @@ import { NotifierFactory } from "./notifiers/NotifierFactory"; import TOTPValidator from "./TOTPValidator"; import TOTPGenerator from "./TOTPGenerator"; import RestApi from "./RestApi"; +import { LdapClient } from "./LdapClient"; import * as Express from "express"; import * as BodyParser from "body-parser"; @@ -16,8 +17,6 @@ import * as http from "http"; import AccessController from "./access_control/AccessController"; -const Ldap = require("./ldap"); - export default class Server { private httpServer: http.Server; @@ -58,7 +57,7 @@ export default class Server { const data_store = new UserDataStore(datastore_options); const regulator = new AuthenticationRegulator(data_store, five_minutes); const notifier = NotifierFactory.build(config.notifier, deps.nodemailer); - const ldap = new Ldap(deps, config.ldap); + const ldap = new LdapClient(config.ldap, deps.ldapjs, deps.winston); const accessController = new AccessController(config.access_control, deps.winston); const totpValidator = new TOTPValidator(deps.speakeasy); const totpGenerator = new TOTPGenerator(deps.speakeasy); diff --git a/src/lib/ldap.js b/src/lib/ldap.js deleted file mode 100644 index cc473e98..00000000 --- a/src/lib/ldap.js +++ /dev/null @@ -1,154 +0,0 @@ - -module.exports = Ldap; - -var util = require('util'); -var Promise = require('bluebird'); -var exceptions = require('./Exceptions'); -var Dovehash = require('dovehash'); - -function Ldap(deps, ldap_config) { - this.ldap_config = ldap_config; - - 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) { - throw new exceptions.LdapBindError(err.message); - }); -} - -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) { - that.ldap_client.searchAsync(base, query) - .then(function(res) { - var doc = []; - res.on('searchEntry', function(entry) { - doc.push(entry.object); - }); - res.on('error', function(err) { - reject(new exceptions.LdapSearchError(err)); - }); - res.on('end', function(result) { - resolve(doc); - }); - }) - .catch(function(err) { - reject(err); - }); - }); -} - -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; + searchAsync(base: string, query: ldapjs.SearchOptions): Promise; + modifyAsync(userdn: string, change: ldapjs.Change): Promise; + } +} \ No newline at end of file diff --git a/test/unitary/LdapClient.test.ts b/test/unitary/LdapClient.test.ts new file mode 100644 index 00000000..82f49c7c --- /dev/null +++ b/test/unitary/LdapClient.test.ts @@ -0,0 +1,243 @@ + +import LdapClient = require("../../src/lib/LdapClient"); +import { LdapConfiguration } from "../../src/lib/Configuration"; + +import sinon = require("sinon"); +import BluebirdPromise = require("bluebird"); +import assert = require("assert"); +import ldapjs = require("ldapjs"); +import winston = require("winston"); +import { EventEmitter } from "events"; + +import { LdapjsMock, LdapjsClientMock } from "./mocks/ldapjs"; + + +describe("test ldap validation", function () { + let ldap: LdapClient.LdapClient; + let ldap_client: LdapjsClientMock; + let ldapjs: LdapjsMock; + let ldap_config: LdapConfiguration; + + beforeEach(function () { + ldap_client = { + bind: sinon.stub(), + search: sinon.stub(), + modify: sinon.stub(), + on: sinon.stub() + } as any; + + ldapjs = LdapjsMock(); + ldapjs.createClient.returns(ldap_client); + + ldap_config = { + url: "http://localhost:324", + user: "admin", + password: "password", + base_dn: "dc=example,dc=com", + additional_user_dn: "ou=users" + }; + + ldap = new LdapClient.LdapClient(ldap_config, ldapjs, winston); + return ldap.connect(); + }); + + describe("test binding", test_binding); + 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_bind() { + const username = "username"; + const password = "password"; + return ldap.bind(username, password); + } + + it("should bind the user if good credentials provided", function () { + ldap_client.bind.yields(); + return test_bind(); + }); + + it("should bind the user with correct DN", function () { + ldap_config.user_name_attribute = "uid"; + const username = "user"; + const password = "password"; + 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 () { + const username = "user"; + const password = "password"; + 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"); + const promise = test_bind(); + return promise.catch(function () { + return Promise.resolve(); + }); + }); + } + + function test_get_emails() { + let res_emitter: any; + let expected_doc: any; + + beforeEach(function () { + expected_doc = { + object: { + mail: "user@example.com" + } + }; + + res_emitter = { + on: sinon.spy(function (event: string, fn: (doc: any) => void) { + 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_emails("user") + .then(function (emails) { + assert.deepEqual(emails, [expected_doc.object.mail]); + return Promise.resolve(); + }); + }); + + 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(); + }); + }); + + it("should fail on error with search method", function () { + const expected_doc = { + mail: ["user@example.com"] + }; + ldap_client.search.yields("Error while searching mails"); + + return ldap.get_emails("user") + .catch(function () { + return Promise.resolve(); + }); + }); + } + + function test_get_groups() { + let res_emitter: any; + let expected_doc1: any, expected_doc2: any; + + beforeEach(function () { + expected_doc1 = { + object: { + cn: "group1" + } + }; + + expected_doc2 = { + object: { + cn: "group2" + } + }; + + res_emitter = { + on: sinon.spy(function (event: string, fn: (doc: any) => void) { + 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.yields(undefined, res_emitter); + ldap.get_groups("user") + .then(function() { + assert.equal(ldap_client.search.getCall(0).args[0], "ou=groups,dc=example,dc=com"); + done(); + }); + }); + + it("should use default group_name_attr if not provided", function (done) { + ldap_client.search.yields(undefined, res_emitter); + ldap.get_groups("user") + .then(function() { + assert.equal(ldap_client.search.getCall(0).args[0], "dc=example,dc=com"); + assert.equal(ldap_client.search.getCall(0).args[1].filter, "member=cn=user,ou=users,dc=example,dc=com"); + assert.deepEqual(ldap_client.search.getCall(0).args[1].attributes, ["cn"]); + done(); + }); + }); + + 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 () { + const change = { + operation: "replace", + modification: { + userPassword: "new-password" + } + }; + const userdn = "cn=user,ou=users,dc=example,dc=com"; + + ldap_client.bind.yields(undefined); + ldap_client.modify.yields(undefined); + + return ldap.update_password("user", "new-password") + .then(function () { + assert.deepEqual(ldap_client.modify.getCall(0).args[0], userdn); + assert.deepEqual(ldap_client.modify.getCall(0).args[1].operation, change.operation); + + const userPassword = ldap_client.modify.getCall(0).args[1].modification.userPassword; + assert(/{SSHA}/.test(userPassword)); + return Promise.resolve(); + }); + }); + + it("should fail when ldap throws an error", function () { + ldap_client.bind.yields(undefined); + ldap_client.modify.yields("Error"); + + return ldap.update_password("user", "new-password") + .catch(function () { + return Promise.resolve(); + }); + }); + + 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.withArgs("uid=username,ou=users,dc=example,dc=com").yields(); + return ldap.update_password("username", "newpass"); + }); + } +}); + diff --git a/test/unitary/Server.test.ts b/test/unitary/Server.test.ts index ed698aad..15105b26 100644 --- a/test/unitary/Server.test.ts +++ b/test/unitary/Server.test.ts @@ -1,6 +1,6 @@ import Server from "../../src/lib/Server"; -import Ldap = require("../../src/lib/ldap"); +import LdapClient = require("../../src/lib/LdapClient"); import Promise = require("bluebird"); import speakeasy = require("speakeasy"); diff --git a/test/unitary/mocks/Ldap.ts b/test/unitary/mocks/Ldap.ts deleted file mode 100644 index a44846c9..00000000 --- a/test/unitary/mocks/Ldap.ts +++ /dev/null @@ -1,10 +0,0 @@ - -import sinon = require("sinon"); - -export = function () { - return { - bind: sinon.stub(), - get_emails: sinon.stub(), - get_groups: sinon.stub() - }; -}; diff --git a/test/unitary/mocks/LdapClient.ts b/test/unitary/mocks/LdapClient.ts new file mode 100644 index 00000000..17495c69 --- /dev/null +++ b/test/unitary/mocks/LdapClient.ts @@ -0,0 +1,20 @@ + +import sinon = require("sinon"); + +export interface LdapClientMock { + bind: sinon.SinonStub; + get_emails: sinon.SinonStub; + get_groups: sinon.SinonStub; + search_in_ldap: sinon.SinonStub; + update_password: sinon.SinonStub; +} + +export function LdapClientMock(): LdapClientMock { + return { + bind: sinon.stub(), + get_emails: sinon.stub(), + get_groups: sinon.stub(), + search_in_ldap: sinon.stub(), + update_password: sinon.stub() + }; +} diff --git a/test/unitary/mocks/UserDataStore.ts b/test/unitary/mocks/UserDataStore.ts new file mode 100644 index 00000000..fc2774f1 --- /dev/null +++ b/test/unitary/mocks/UserDataStore.ts @@ -0,0 +1,18 @@ + +import sinon = require("sinon"); + +export interface UserDataStore { + set_u2f_meta: sinon.SinonStub; + get_u2f_meta: sinon.SinonStub; + issue_identity_check_token: sinon.SinonStub; + consume_identity_check_token: sinon.SinonStub; +} + +export function UserDataStore(): UserDataStore { + return { + set_u2f_meta: sinon.stub(), + get_u2f_meta: sinon.stub(), + issue_identity_check_token: sinon.stub(), + consume_identity_check_token: sinon.stub() + }; +} diff --git a/test/unitary/mocks/express.ts b/test/unitary/mocks/express.ts index 1f6712bc..31aec299 100644 --- a/test/unitary/mocks/express.ts +++ b/test/unitary/mocks/express.ts @@ -1,11 +1,30 @@ import sinon = require("sinon"); -export = { - Response: function () { - return { - send: sinon.stub(), - status: sinon.stub() - }; - } -}; \ No newline at end of file +export interface RequestMock { + app?: any; + body?: any; + session?: any; + headers?: any; +} + +export interface ResponseMock { + send: sinon.SinonStub | sinon.SinonSpy; + status: sinon.SinonStub; + json: sinon.SinonStub; +} + +export function RequestMock(): RequestMock { + return { + app: { + get: sinon.stub() + } + }; +} +export function ResponseMock(): ResponseMock { + return { + send: sinon.stub(), + status: sinon.stub(), + json: sinon.stub() + }; +} diff --git a/test/unitary/mocks/ldapjs.ts b/test/unitary/mocks/ldapjs.ts new file mode 100644 index 00000000..957f4a9e --- /dev/null +++ b/test/unitary/mocks/ldapjs.ts @@ -0,0 +1,28 @@ + +import sinon = require("sinon"); + +export interface LdapjsMock { + createClient: sinon.SinonStub; +} + +export interface LdapjsClientMock { + bind: sinon.SinonStub; + search: sinon.SinonStub; + modify: sinon.SinonStub; + on: sinon.SinonStub; +} + +export function LdapjsMock(): LdapjsMock { + return { + createClient: sinon.stub() + }; +} + +export function LdapjsClientMock(): LdapjsClientMock { + return { + bind: sinon.stub(), + search: sinon.stub(), + modify: sinon.stub(), + on: sinon.stub() + }; +} \ No newline at end of file diff --git a/test/unitary/routes/FirstFactor.test.ts b/test/unitary/routes/FirstFactor.test.ts index f949a151..9d0b1af3 100644 --- a/test/unitary/routes/FirstFactor.test.ts +++ b/test/unitary/routes/FirstFactor.test.ts @@ -8,7 +8,7 @@ import FirstFactor = require("../../../src/lib/routes/FirstFactor"); import exceptions = require("../../../src/lib/Exceptions"); import AuthenticationRegulatorMock = require("../mocks/AuthenticationRegulator"); import AccessControllerMock = require("../mocks/AccessController"); -import LdapMock = require("../mocks/Ldap"); +import { LdapClientMock } from "../mocks/LdapClient"; import ExpressMock = require("../mocks/express"); describe("test the first factor validation route", function() { @@ -17,7 +17,7 @@ describe("test the first factor validation route", function() { let emails: string[]; let groups: string[]; let configuration; - let ldapMock: any; + let ldapMock: LdapClientMock; let regulator: any; let accessController: any; @@ -32,7 +32,7 @@ describe("test the first factor validation route", function() { emails = [ "test_ok@example.com" ]; groups = [ "group1", "group2" ]; - ldapMock = LdapMock(); + ldapMock = LdapClientMock(); accessController = AccessControllerMock(); accessController.isDomainAllowedForUser.returns(true); @@ -63,7 +63,7 @@ describe("test the first factor validation route", function() { } } }; - res = ExpressMock.Response(); + res = ExpressMock.ResponseMock(); }); it("should return status code 204 when LDAP binding succeeds", function() { diff --git a/test/unitary/routes/reset_password.test.ts b/test/unitary/routes/reset_password.test.ts new file mode 100644 index 00000000..8ec9cbef --- /dev/null +++ b/test/unitary/routes/reset_password.test.ts @@ -0,0 +1,151 @@ + +import reset_password = require("../../../src/lib/routes/reset_password"); +import LdapClient = require("../../../src/lib/LdapClient"); +import sinon = require("sinon"); +import winston = require("winston"); +import assert = require("assert"); +import BluebirdPromise = require("bluebird"); + +import ExpressMock = require("../mocks/express"); +import { LdapClientMock } from "../mocks/LdapClient"; +import { UserDataStore } from "../mocks/UserDataStore"; + +describe("test reset password", function () { + let req: ExpressMock.RequestMock; + let res: ExpressMock.ResponseMock; + let user_data_store: UserDataStore; + let ldap_client: LdapClientMock; + let configuration: any; + + beforeEach(function () { + req = { + body: { + userid: "user" + }, + app: { + get: sinon.stub() + }, + session: { + auth_session: { + userid: "user", + email: "user@example.com", + first_factor: true, + second_factor: false + } + }, + headers: { + host: "localhost" + } + }; + + const options = { + inMemoryOnly: true + }; + + user_data_store = UserDataStore(); + user_data_store.set_u2f_meta.returns(Promise.resolve({})); + user_data_store.get_u2f_meta.returns(Promise.resolve({})); + user_data_store.issue_identity_check_token.returns(Promise.resolve({})); + user_data_store.consume_identity_check_token.returns(Promise.resolve({})); + req.app.get.withArgs("user data store").returns(user_data_store); + + + configuration = { + ldap: { + base_dn: "dc=example,dc=com", + user_name_attribute: "cn" + } + }; + + req.app.get.withArgs("logger").returns(winston); + req.app.get.withArgs("config").returns(configuration); + + ldap_client = LdapClientMock(); + req.app.get.withArgs("ldap").returns(ldap_client); + + res = ExpressMock.ResponseMock(); + }); + + describe("test reset password identity pre check", test_reset_password_check); + describe("test reset password post", test_reset_password_post); + + function test_reset_password_check() { + it("should fail when no userid is provided", function (done) { + req.body.userid = undefined; + reset_password.icheck_interface.pre_check_callback(req) + .catch(function (err: Error) { + done(); + }); + }); + + it("should fail if ldap fail", function (done) { + ldap_client.get_emails.returns(BluebirdPromise.reject("Internal error")); + reset_password.icheck_interface.pre_check_callback(req) + .catch(function (err: Error) { + done(); + }); + }); + + it("should perform a search in ldap to find email address", function (done) { + configuration.ldap.user_name_attribute = "uid"; + ldap_client.get_emails.returns(BluebirdPromise.resolve([])); + reset_password.icheck_interface.pre_check_callback(req) + .then(function () { + assert.equal("user", ldap_client.get_emails.getCall(0).args[0]); + done(); + }); + }); + + it("should returns identity when ldap replies", function (done) { + ldap_client.get_emails.returns(BluebirdPromise.resolve(["test@example.com"])); + reset_password.icheck_interface.pre_check_callback(req) + .then(function () { + done(); + }); + }); + } + + function test_reset_password_post() { + it("should update the password and reset auth_session for reauthentication", function (done) { + req.session.auth_session.identity_check = {}; + req.session.auth_session.identity_check.userid = "user"; + req.session.auth_session.identity_check.challenge = "reset-password"; + req.body = {}; + req.body.password = "new-password"; + + ldap_client.update_password.returns(BluebirdPromise.resolve()); + ldap_client.bind.returns(BluebirdPromise.resolve()); + res.send = sinon.spy(function () { + assert.equal(res.status.getCall(0).args[0], 204); + assert.equal(req.session.auth_session, undefined); + done(); + }); + reset_password.post(req, res); + }); + + it("should fail if identity_challenge does not exist", function (done) { + req.session.auth_session.identity_check = {}; + req.session.auth_session.identity_check.challenge = undefined; + res.send = sinon.spy(function () { + assert.equal(res.status.getCall(0).args[0], 403); + done(); + }); + reset_password.post(req, res); + }); + + it("should fail when ldap fails", function (done) { + req.session.auth_session.identity_check = {}; + req.session.auth_session.identity_check.challenge = "reset-password"; + req.body = {}; + req.body.password = "new-password"; + + ldap_client.bind.yields(undefined); + ldap_client.update_password.returns(BluebirdPromise.reject("Internal error with LDAP")); + res.send = sinon.spy(function () { + assert.equal(res.status.getCall(0).args[0], 500); + done(); + }); + reset_password.post(req, res); + }); + } +}); diff --git a/test/unitary/routes/test_reset_password.js b/test/unitary/routes/test_reset_password.js deleted file mode 100644 index efac684d..00000000 --- a/test/unitary/routes/test_reset_password.js +++ /dev/null @@ -1,162 +0,0 @@ -var reset_password = require('../../../src/lib/routes/reset_password'); -var Ldap = require('../../../src/lib/ldap'); - -var sinon = require('sinon'); -var winston = require('winston'); -var assert = require('assert'); - -describe('test reset password', function() { - var req, res; - var user_data_store; - var ldap_client; - var ldap; - - beforeEach(function() { - req = {} - req.body = {}; - req.body.userid = 'user'; - req.app = {}; - req.app.get = sinon.stub(); - req.app.get.withArgs('logger').returns(winston); - req.session = {}; - req.session.auth_session = {}; - req.session.auth_session.userid = 'user'; - req.session.auth_session.email = 'user@example.com'; - req.session.auth_session.first_factor = true; - req.session.auth_session.second_factor = false; - req.headers = {}; - req.headers.host = 'localhost'; - - var options = {}; - options.inMemoryOnly = true; - - user_data_store = {}; - user_data_store.set_u2f_meta = sinon.stub().returns(Promise.resolve({})); - user_data_store.get_u2f_meta = sinon.stub().returns(Promise.resolve({})); - user_data_store.issue_identity_check_token = sinon.stub().returns(Promise.resolve({})); - user_data_store.consume_identity_check_token = sinon.stub().returns(Promise.resolve({})); - req.app.get.withArgs('user data store').returns(user_data_store); - - - 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(); - ldap_client.on = sinon.spy(); - - 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(); - res.json = sinon.spy(); - res.status = sinon.spy(); - }); - - describe('test reset password identity pre check', test_reset_password_check); - describe('test reset password post', test_reset_password_post); - - function test_reset_password_check() { - it('should fail when no userid is provided', function(done) { - req.body.userid = undefined; - reset_password.icheck_interface.pre_check_callback(req) - .catch(function(err) { - done(); - }); - }); - - it('should fail if ldap fail', function(done) { - ldap_client.search.yields('Internal error'); - reset_password.icheck_interface.pre_check_callback(req) - .catch(function(err) { - done(); - }); - }); - - it('should perform a search in ldap to find email address', function(done) { - config.ldap.user_name_attribute = 'uid'; - ldap_client.search = sinon.spy(function(dn) { - if(dn == 'uid=user,dc=example,dc=com') done(); - }); - reset_password.icheck_interface.pre_check_callback(req); - }); - - it('should returns identity when ldap replies', function(done) { - var doc = {}; - doc.object = {}; - doc.object.email = ['test@example.com']; - doc.object.userid = 'user'; - - var res = {}; - res.on = sinon.stub(); - res.on.withArgs('searchEntry').yields(doc); - res.on.withArgs('end').yields(); - - ldap_client.search.yields(undefined, res); - reset_password.icheck_interface.pre_check_callback(req) - .then(function() { - done(); - }); - }); - } - - function test_reset_password_post() { - it('should update the password and reset auth_session for reauthentication', function(done) { - req.session.auth_session.identity_check = {}; - req.session.auth_session.identity_check.userid = 'user'; - req.session.auth_session.identity_check.challenge = 'reset-password'; - req.body = {}; - req.body.password = 'new-password'; - - ldap_client.modify.yields(undefined); - ldap_client.bind.yields(undefined); - res.send = sinon.spy(function() { - assert.equal(ldap_client.modify.getCall(0).args[0], 'cn=user,dc=example,dc=com'); - assert.equal(res.status.getCall(0).args[0], 204); - assert.equal(req.session.auth_session, undefined); - done(); - }); - reset_password.post(req, res); - }); - - it('should fail if identity_challenge does not exist', function(done) { - req.session.auth_session.identity_check = {}; - req.session.auth_session.identity_check.challenge = undefined; - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 403); - done(); - }); - reset_password.post(req, res); - }); - - it('should fail when ldap fails', function(done) { - req.session.auth_session.identity_check = {}; - req.session.auth_session.identity_check.challenge = 'reset-password'; - req.body = {}; - req.body.password = 'new-password'; - - ldap_client.bind.yields(undefined); - ldap_client.modify.yields('Internal error with LDAP'); - res.send = sinon.spy(function() { - assert.equal(res.status.getCall(0).args[0], 500); - done(); - }); - reset_password.post(req, res); - }); - } -}); diff --git a/test/unitary/test_ldap.js b/test/unitary/test_ldap.js deleted file mode 100644 index 08085700..00000000 --- a/test/unitary/test_ldap.js +++ /dev/null @@ -1,233 +0,0 @@ - -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(), - 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 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_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_bind(); - }); - - it('should bind the user with correct DN', function() { - ldap_config.user_name_attribute = 'uid'; - var username = 'user'; - var password = 'password'; - 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() { - var username = 'user'; - var password = 'password'; - 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_bind(); - return promise.catch(function() { - return Promise.resolve(); - }); - }); - } - - function test_get_emails() { - var res_emitter; - var expected_doc; - - beforeEach(function() { - expected_doc = {}; - expected_doc.object = {}; - expected_doc.object.mail = 'user@example.com'; - - 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_emails('user') - .then(function(emails) { - assert.deepEqual(emails, [expected_doc.object.mail]); - return Promise.resolve(); - }) - }); - - 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(); - }); - }); - - 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 while searching mails'); - - 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() { - var change = {}; - change.operation = 'replace'; - change.modification = {}; - change.modification.userPassword = 'new-password'; - - var userdn = 'cn=user,ou=users,dc=example,dc=com'; - - ldap_client.bind.yields(undefined); - ldap_client.modify.yields(undefined); - - 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)); - return Promise.resolve(); - }) - }); - - it('should fail when ldap throws an error', function() { - ldap_client.bind.yields(undefined); - ldap_client.modify.yields('Error'); - - return ldap.update_password('user', 'new-password') - .catch(function() { - return Promise.resolve(); - }) - }); - - 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.withArgs('uid=username,ou=users,dc=example,dc=com').yields(); - return ldap.update_password('username', 'newpass'); - }); - } -}); -