From 20536abf8bf8634599b2424d5f1083be6ac2418f Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 2 Sep 2017 22:38:26 +0200 Subject: [PATCH] Introduce LDAP filters to search users and groups for more flexibility. --- config.template.yml | 20 +- scripts/dc-dev.sh | 1 + src/server/lib/Server.ts | 4 +- src/server/lib/ServerVariablesHandler.ts | 24 ++- .../lib/configuration/Configuration.d.ts | 33 +++- .../lib/configuration/ConfigurationAdapter.ts | 37 +++- src/server/lib/ldap/Authenticator.ts | 46 ++--- src/server/lib/ldap/Client.ts | 106 ++++++----- src/server/lib/ldap/ClientFactory.ts | 27 +++ src/server/lib/ldap/EmailsRetriever.ts | 23 +-- src/server/lib/ldap/IAuthenticator.ts | 6 + src/server/lib/ldap/IClient.ts | 16 ++ src/server/lib/ldap/IClientFactory.ts | 6 + src/server/lib/ldap/IEmailsRetriever.ts | 5 + src/server/lib/ldap/IPasswordUpdater.ts | 5 + src/server/lib/ldap/PasswordUpdater.ts | 23 +-- src/server/lib/ldap/common.ts | 18 -- src/server/lib/routes/firstfactor/post.ts | 11 +- test/features/resilience.feature | 18 +- test/unit/server/DataPersistence.test.ts | 179 ------------------ .../SessionConfigurationBuilder.test.ts | 18 +- .../ConfigurationAdapter.test.ts | 42 ++-- .../LdapConfigurationAdaptation.test.ts | 93 +++++++++ test/unit/server/ldap/Authenticator.test.ts | 176 ++++++++--------- test/unit/server/ldap/EmailsRetriever.test.ts | 113 +++++------ test/unit/server/ldap/PasswordUpdater.test.ts | 96 +++++----- test/unit/server/mocks/ServerVariablesMock.ts | 2 +- .../server/mocks/ldap/ClientFactoryStub.ts | 16 ++ test/unit/server/mocks/ldap/ClientStub.ts | 46 +++++ test/unit/server/server/Server.test.ts | 94 --------- 30 files changed, 639 insertions(+), 665 deletions(-) create mode 100644 src/server/lib/ldap/ClientFactory.ts create mode 100644 src/server/lib/ldap/IAuthenticator.ts create mode 100644 src/server/lib/ldap/IClient.ts create mode 100644 src/server/lib/ldap/IClientFactory.ts create mode 100644 src/server/lib/ldap/IEmailsRetriever.ts create mode 100644 src/server/lib/ldap/IPasswordUpdater.ts delete mode 100644 src/server/lib/ldap/common.ts delete mode 100644 test/unit/server/DataPersistence.test.ts rename test/unit/server/{ => configuration}/ConfigurationAdapter.test.ts (65%) create mode 100644 test/unit/server/configuration/LdapConfigurationAdaptation.test.ts create mode 100644 test/unit/server/mocks/ldap/ClientFactoryStub.ts create mode 100644 test/unit/server/mocks/ldap/ClientStub.ts diff --git a/config.template.yml b/config.template.yml index 638b59202..1f8bf0160 100644 --- a/config.template.yml +++ b/config.template.yml @@ -18,17 +18,27 @@ ldap: base_dn: dc=example,dc=com # An additional dn to define the scope to all users - additional_user_dn: ou=users + additional_users_dn: ou=users - # The user name attribute of users. Might uid for FreeIPA. 'cn' by default. - user_name_attribute: cn + # The users filter. + # {0} is the matcher replaced by username. + # 'cn={0}' by default. + users_filter: cn={0} # An additional dn to define the scope of groups - additional_group_dn: ou=groups + additional_groups_dn: ou=groups - # The group name attribute of group. 'cn' by default. + # The groups filter. + # {0} is the matcher replaced by user dn. + # 'member={0}' by default. + groups_filter: (&(member={0})(objectclass=groupOfNames)) + + # The attribute holding the name of the group group_name_attribute: cn + # The attribute holding the mail address of the user + mail_attribute: mail + # The username and password of the admin user. user: cn=admin,dc=example,dc=com password: password diff --git a/scripts/dc-dev.sh b/scripts/dc-dev.sh index b9cd0630b..416a3ba2c 100755 --- a/scripts/dc-dev.sh +++ b/scripts/dc-dev.sh @@ -9,4 +9,5 @@ docker-compose \ -f example/mongo/docker-compose.yml \ -f example/redis/docker-compose.yml \ -f example/nginx/docker-compose.yml \ + -f example/ldap/docker-compose.admin.yml \ -f example/ldap/docker-compose.yml $* diff --git a/src/server/lib/Server.ts b/src/server/lib/Server.ts index bc42e883b..ecfa152a3 100644 --- a/src/server/lib/Server.ts +++ b/src/server/lib/Server.ts @@ -47,7 +47,7 @@ export default class Server { RestApi.setup(app); } - private transformConfiguration(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): AppConfiguration { + private adaptConfiguration(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): AppConfiguration { const config = ConfigurationAdapter.adapt(yamlConfiguration); // by default the level of logs is info @@ -76,7 +76,7 @@ export default class Server { start(yamlConfiguration: UserConfiguration, deps: GlobalDependencies): BluebirdPromise { const that = this; const app = Express(); - const config = this.transformConfiguration(yamlConfiguration, deps); + const config = this.adaptConfiguration(yamlConfiguration, deps); return this.setup(config, app, deps) .then(function () { return that.startServer(app, config.port); diff --git a/src/server/lib/ServerVariablesHandler.ts b/src/server/lib/ServerVariablesHandler.ts index a7d6df7b3..49fde6fa5 100644 --- a/src/server/lib/ServerVariablesHandler.ts +++ b/src/server/lib/ServerVariablesHandler.ts @@ -1,9 +1,13 @@ import winston = require("winston"); import BluebirdPromise = require("bluebird"); +import { IAuthenticator } from "./ldap/IAuthenticator"; +import { IPasswordUpdater } from "./ldap/IPasswordUpdater"; +import { IEmailsRetriever } from "./ldap/IEmailsRetriever"; import { Authenticator } from "./ldap/Authenticator"; import { PasswordUpdater } from "./ldap/PasswordUpdater"; import { EmailsRetriever } from "./ldap/EmailsRetriever"; +import { ClientFactory } from "./ldap/ClientFactory"; import { TOTPValidator } from "./TOTPValidator"; import { TOTPGenerator } from "./TOTPGenerator"; @@ -29,9 +33,9 @@ export const VARIABLES_KEY = "authelia-variables"; export interface ServerVariables { logger: typeof winston; - ldapAuthenticator: Authenticator; - ldapPasswordUpdater: PasswordUpdater; - ldapEmailsRetriever: EmailsRetriever; + ldapAuthenticator: IAuthenticator; + ldapPasswordUpdater: IPasswordUpdater; + ldapEmailsRetriever: IEmailsRetriever; totpValidator: TOTPValidator; totpGenerator: TOTPGenerator; u2f: typeof U2F; @@ -71,9 +75,11 @@ export class ServerVariablesHandler { const five_minutes = 5 * 60; const notifier = NotifierFactory.build(config.notifier, deps.nodemailer); - const ldapAuthenticator = new Authenticator(config.ldap, deps.ldapjs, deps.winston); - const ldapPasswordUpdater = new PasswordUpdater(config.ldap, deps.ldapjs, deps.dovehash, deps.winston); - const ldapEmailsRetriever = new EmailsRetriever(config.ldap, deps.ldapjs, deps.winston); + const ldapClientFactory = new ClientFactory(config.ldap, deps.ldapjs, deps.dovehash, deps.winston); + + const ldapAuthenticator = new Authenticator(config.ldap, ldapClientFactory); + const ldapPasswordUpdater = new PasswordUpdater(config.ldap, ldapClientFactory); + const ldapEmailsRetriever = new EmailsRetriever(config.ldap, ldapClientFactory); const accessController = new AccessController(config.access_control, deps.winston); const totpValidator = new TOTPValidator(deps.speakeasy); const totpGenerator = new TOTPGenerator(deps.speakeasy); @@ -113,15 +119,15 @@ export class ServerVariablesHandler { return (app.get(VARIABLES_KEY) as ServerVariables).notifier; } - static getLdapAuthenticator(app: express.Application): Authenticator { + static getLdapAuthenticator(app: express.Application): IAuthenticator { return (app.get(VARIABLES_KEY) as ServerVariables).ldapAuthenticator; } - static getLdapPasswordUpdater(app: express.Application): PasswordUpdater { + static getLdapPasswordUpdater(app: express.Application): IPasswordUpdater { return (app.get(VARIABLES_KEY) as ServerVariables).ldapPasswordUpdater; } - static getLdapEmailsRetriever(app: express.Application): EmailsRetriever { + static getLdapEmailsRetriever(app: express.Application): IEmailsRetriever { return (app.get(VARIABLES_KEY) as ServerVariables).ldapEmailsRetriever; } diff --git a/src/server/lib/configuration/Configuration.d.ts b/src/server/lib/configuration/Configuration.d.ts index ec0608736..8a98280e7 100644 --- a/src/server/lib/configuration/Configuration.d.ts +++ b/src/server/lib/configuration/Configuration.d.ts @@ -1,11 +1,32 @@ +export interface UserLdapConfiguration { + url: string; + base_dn: string; + + additional_users_dn?: string; + users_filter?: string; + + additional_groups_dn?: string; + groups_filter?: string; + + group_name_attribute?: string; + mail_attribute?: string; + + user: string; // admin username + password: string; // admin password +} export interface LdapConfiguration { url: string; - base_dn: string; - additional_user_dn?: string; - user_name_attribute?: string; // cn by default - additional_group_dn?: string; - group_name_attribute?: string; // cn by default + + users_dn: string; + users_filter: string; + + groups_dn: string; + groups_filter: string; + + group_name_attribute: string; + mail_attribute: string; + user: string; // admin username password: string; // admin password } @@ -67,7 +88,7 @@ export interface StorageConfiguration { export interface UserConfiguration { port?: number; logs_level?: string; - ldap: LdapConfiguration; + ldap: UserLdapConfiguration; session: SessionCookieConfiguration; storage: StorageConfiguration; notifier: NotifierConfiguration; diff --git a/src/server/lib/configuration/ConfigurationAdapter.ts b/src/server/lib/configuration/ConfigurationAdapter.ts index fe4c33eb6..cc5de6ef7 100644 --- a/src/server/lib/configuration/ConfigurationAdapter.ts +++ b/src/server/lib/configuration/ConfigurationAdapter.ts @@ -3,7 +3,8 @@ import * as ObjectPath from "object-path"; import { AppConfiguration, UserConfiguration, NotifierConfiguration, ACLConfiguration, LdapConfiguration, SessionRedisOptions, - MongoStorageConfiguration, LocalStorageConfiguration + MongoStorageConfiguration, LocalStorageConfiguration, + UserLdapConfiguration } from "./Configuration"; const LDAP_URL_ENV_VARIABLE = "LDAP_URL"; @@ -23,15 +24,45 @@ function ensure_key_existence(config: object, path: string): void { } } +function adaptLdapConfiguration(userConfig: UserLdapConfiguration): LdapConfiguration { + const DEFAULT_USERS_FILTER = "cn={0}"; + const DEFAULT_GROUPS_FILTER = "member={0}"; + const DEFAULT_GROUP_NAME_ATTRIBUTE = "cn"; + const DEFAULT_MAIL_ATTRIBUTE = "mail"; + + let usersDN = userConfig.base_dn; + if (userConfig.additional_users_dn) + usersDN = userConfig.additional_users_dn + "," + usersDN; + + let groupsDN = userConfig.base_dn; + if (userConfig.additional_groups_dn) + groupsDN = userConfig.additional_groups_dn + "," + groupsDN; + + return { + url: userConfig.url, + users_dn: usersDN, + users_filter: userConfig.users_filter || DEFAULT_USERS_FILTER, + groups_dn: groupsDN, + groups_filter: userConfig.groups_filter || DEFAULT_GROUPS_FILTER, + group_name_attribute: userConfig.group_name_attribute || DEFAULT_GROUP_NAME_ATTRIBUTE, + mail_attribute: userConfig.mail_attribute || DEFAULT_MAIL_ATTRIBUTE, + password: userConfig.password, + user: userConfig.user + }; +} + function adaptFromUserConfiguration(userConfiguration: UserConfiguration): AppConfiguration { ensure_key_existence(userConfiguration, "ldap"); + // ensure_key_existence(userConfiguration, "ldap.url"); + // ensure_key_existence(userConfiguration, "ldap.base_dn"); ensure_key_existence(userConfiguration, "session.secret"); - const port = ObjectPath.get(userConfiguration, "port", 8080); + const port = userConfiguration.port || 8080; + const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap); return { port: port, - ldap: ObjectPath.get(userConfiguration, "ldap"), + ldap: ldapConfiguration, session: { domain: ObjectPath.get(userConfiguration, "session.domain"), secret: ObjectPath.get(userConfiguration, "session.secret"), diff --git a/src/server/lib/ldap/Authenticator.ts b/src/server/lib/ldap/Authenticator.ts index 26d9b5a09..22c59520c 100644 --- a/src/server/lib/ldap/Authenticator.ts +++ b/src/server/lib/ldap/Authenticator.ts @@ -1,36 +1,38 @@ import BluebirdPromise = require("bluebird"); import exceptions = require("../Exceptions"); import ldapjs = require("ldapjs"); -import { Client, Attributes } from "./Client"; -import { buildUserDN } from "./common"; +import { IClient } from "./IClient"; +import { IClientFactory } from "./IClientFactory"; +import { GroupsAndEmails } from "./IClient"; +import { IAuthenticator } from "./IAuthenticator"; import { LdapConfiguration } from "../configuration/Configuration"; import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; -export class Authenticator { +export class Authenticator implements IAuthenticator { private options: LdapConfiguration; - private ldapjs: Ldapjs; - private logger: Winston; + private clientFactory: IClientFactory; - constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) { + constructor(options: LdapConfiguration, clientFactory: IClientFactory) { this.options = options; - this.ldapjs = ldapjs; - this.logger = logger; + this.clientFactory = clientFactory; } - private createClient(userDN: string, password: string): Client { - return new Client(userDN, password, this.options, this.ldapjs, undefined, this.logger); - } + authenticate(username: string, password: string): BluebirdPromise { + const that = this; + let userClient: IClient; + const adminClient = this.clientFactory.create(this.options.user, this.options.password); + let groupsAndEmails: GroupsAndEmails; - authenticate(username: string, password: string): BluebirdPromise { - const self = this; - const userDN = buildUserDN(username, this.options); - const userClient = this.createClient(userDN, password); - const adminClient = this.createClient(this.options.user, this.options.password); - let attributes: Attributes; - - return userClient.open() + return adminClient.open() + .then(function () { + return adminClient.searchUserDn(username); + }) + .then(function (userDN: string) { + userClient = that.clientFactory.create(userDN, password); + return userClient.open(); + }) .then(function () { return userClient.close(); }) @@ -40,12 +42,12 @@ export class Authenticator { .then(function () { return adminClient.searchEmailsAndGroups(username); }) - .then(function (attr: Attributes) { - attributes = attr; + .then(function (gae: GroupsAndEmails) { + groupsAndEmails = gae; return adminClient.close(); }) .then(function () { - return BluebirdPromise.resolve(attributes); + return BluebirdPromise.resolve(groupsAndEmails); }) .error(function (err: Error) { return BluebirdPromise.reject(new exceptions.LdapError(err.message)); diff --git a/src/server/lib/ldap/Client.ts b/src/server/lib/ldap/Client.ts index d2a6fc6b0..4d457ec08 100644 --- a/src/server/lib/ldap/Client.ts +++ b/src/server/lib/ldap/Client.ts @@ -2,33 +2,30 @@ import util = require("util"); import BluebirdPromise = require("bluebird"); import exceptions = require("../Exceptions"); -import ldapjs = require("ldapjs"); -import { buildUserDN } from "./common"; +import Ldapjs = require("ldapjs"); +import Dovehash = require("dovehash"); import { EventEmitter } from "events"; +import { IClient, GroupsAndEmails } from "./IClient"; import { LdapConfiguration } from "../configuration/Configuration"; -import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; +import { Winston } from "../../../types/Dependencies"; interface SearchEntry { object: any; } -export interface Attributes { - groups: string[]; - emails: string[]; -} - -export class Client { +export class Client implements IClient { private userDN: string; private password: string; - private client: ldapjs.ClientAsync; + private client: Ldapjs.ClientAsync; - private ldapjs: Ldapjs; + private ldapjs: typeof Ldapjs; private logger: Winston; - private dovehash: Dovehash; + private dovehash: typeof Dovehash; private options: LdapConfiguration; - constructor(userDN: string, password: string, options: LdapConfiguration, ldapjs: Ldapjs, dovehash: Dovehash, logger: Winston) { + constructor(userDN: string, password: string, options: LdapConfiguration, + ldapjs: typeof Ldapjs, dovehash: typeof Dovehash, logger: Winston) { this.options = options; this.ldapjs = ldapjs; this.dovehash = dovehash; @@ -46,7 +43,7 @@ export class Client { clientLogger.level("trace"); }*/ - this.client = BluebirdPromise.promisifyAll(ldapClient) as ldapjs.ClientAsync; + this.client = BluebirdPromise.promisifyAll(ldapClient) as Ldapjs.ClientAsync; } open(): BluebirdPromise { @@ -65,7 +62,7 @@ export class Client { }); } - private search(base: string, query: ldapjs.SearchOptions): BluebirdPromise { + private search(base: string, query: Ldapjs.SearchOptions): BluebirdPromise { const that = this; that.logger.debug("LDAP: Search for '%s' in '%s'", JSON.stringify(query), base); @@ -95,27 +92,18 @@ export class Client { private searchGroups(username: string): BluebirdPromise { const that = this; - const userDN = buildUserDN(username, this.options); - const password = this.options.password; - - let groupNameAttribute = this.options.group_name_attribute; - if (!groupNameAttribute) groupNameAttribute = "cn"; - - const additionalGroupDN = this.options.additional_group_dn; - const base_dn = this.options.base_dn; - - let groupDN = base_dn; - if (additionalGroupDN) - groupDN = util.format("%s,", additionalGroupDN) + groupDN; - - const query = { - scope: "sub", - attributes: [groupNameAttribute], - filter: "member=" + userDN - }; const groups: string[] = []; - return that.search(groupDN, query) + return that.searchUserDn(username) + .then(function (userDN: string) { + const filter = that.options.groups_filter.replace("{0}", userDN); + const query = { + scope: "sub", + attributes: [that.options.group_name_attribute], + filter: filter + }; + return that.search(that.options.groups_dn, query); + }) .then(function (docs) { for (let i = 0; i < docs.length; ++i) { groups.push(docs[i].cn); @@ -127,32 +115,49 @@ export class Client { }); } + searchUserDn(username: string): BluebirdPromise { + const that = this; + const filter = this.options.users_filter.replace("{0}", username); + const query = { + scope: "sub", + sizeLimit: 1, + attributes: ["dn"], + filter: filter + }; + + that.logger.debug("LDAP: searching for user dn of %s", username); + return that.search(this.options.users_dn, query) + .then(function (users: { dn: string }[]) { + that.logger.debug("LDAP: retrieved user dn is %s", users[0].dn); + return BluebirdPromise.resolve(users[0].dn); + }); + } + searchEmails(username: string): BluebirdPromise { const that = this; - const userDN = buildUserDN(username, this.options); - const query = { scope: "base", sizeLimit: 1, - attributes: ["mail"] + attributes: [this.options.mail_attribute] }; - return this.search(userDN, 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); - } + return this.searchUserDn(username) + .then(function (userDN) { + return that.search(userDN, query); + }) + .then(function (docs: { mail: string }[]) { + const emails: string[] = []; + if (typeof docs[0].mail === "string") + emails.push(docs[0].mail); + else { + emails.concat(docs[0].mail); } that.logger.debug("LDAP: emails of user '%s' are %s", username, emails); return BluebirdPromise.resolve(emails); }); } - searchEmailsAndGroups(username: string): BluebirdPromise { + searchEmailsAndGroups(username: string): BluebirdPromise { const that = this; let retrievedEmails: string[], retrievedGroups: string[]; @@ -172,8 +177,6 @@ export class Client { modifyPassword(username: string, newPassword: string): BluebirdPromise { const that = this; - const userDN = buildUserDN(username, this.options); - const encodedPassword = this.dovehash.encode("SSHA", newPassword); const change = { operation: "replace", @@ -183,7 +186,10 @@ export class Client { }; this.logger.debug("LDAP: update password of user '%s'", username); - return this.client.modifyAsync(userDN, change) + return this.searchUserDn(username) + .then(function (userDN: string) { + this.client.modifyAsync(userDN, change); + }) .then(function () { return that.client.unbindAsync(); }); diff --git a/src/server/lib/ldap/ClientFactory.ts b/src/server/lib/ldap/ClientFactory.ts new file mode 100644 index 000000000..850227e25 --- /dev/null +++ b/src/server/lib/ldap/ClientFactory.ts @@ -0,0 +1,27 @@ +import { IClientFactory } from "./IClientFactory"; +import { IClient } from "./IClient"; +import { Client } from "./Client"; +import { LdapConfiguration } from "../configuration/Configuration"; + +import Ldapjs = require("ldapjs"); +import Dovehash = require("dovehash"); +import Winston = require("winston"); + +export class ClientFactory implements IClientFactory { + private config: LdapConfiguration; + private ldapjs: typeof Ldapjs; + private dovehash: typeof Dovehash; + private logger: typeof Winston; + + constructor(ldapConfiguration: LdapConfiguration, ldapjs: typeof Ldapjs, + dovehash: typeof Dovehash, logger: typeof Winston) { + this.config = ldapConfiguration; + this.ldapjs = ldapjs; + this.dovehash = dovehash; + this.logger = logger; + } + + create(userDN: string, password: string): IClient { + return new Client(userDN, password, this.config, this.ldapjs, this.dovehash, this.logger); + } +} \ No newline at end of file diff --git a/src/server/lib/ldap/EmailsRetriever.ts b/src/server/lib/ldap/EmailsRetriever.ts index fe4c3e78a..9cc5ff33d 100644 --- a/src/server/lib/ldap/EmailsRetriever.ts +++ b/src/server/lib/ldap/EmailsRetriever.ts @@ -2,30 +2,23 @@ import BluebirdPromise = require("bluebird"); import exceptions = require("../Exceptions"); import ldapjs = require("ldapjs"); import { Client } from "./Client"; -import { buildUserDN } from "./common"; +import { IClientFactory } from "./IClientFactory"; +import { IEmailsRetriever } from "./IEmailsRetriever"; import { LdapConfiguration } from "../configuration/Configuration"; -import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; -export class EmailsRetriever { +export class EmailsRetriever implements IEmailsRetriever { private options: LdapConfiguration; - private ldapjs: Ldapjs; - private logger: Winston; + private clientFactory: IClientFactory; - constructor(options: LdapConfiguration, ldapjs: Ldapjs, logger: Winston) { + constructor(options: LdapConfiguration, clientFactory: IClientFactory) { this.options = options; - this.ldapjs = ldapjs; - this.logger = logger; - } - - private createClient(userDN: string, password: string): Client { - return new Client(userDN, password, this.options, this.ldapjs, undefined, this.logger); + this.clientFactory = clientFactory; } retrieve(username: string): BluebirdPromise { - const userDN = buildUserDN(username, this.options); - const adminClient = this.createClient(this.options.user, this.options.password); + const adminClient = this.clientFactory.create(this.options.user, this.options.password); let emails: string[]; return adminClient.open() @@ -36,7 +29,7 @@ export class EmailsRetriever { emails = emails_; return adminClient.close(); }) - .then(function() { + .then(function () { return BluebirdPromise.resolve(emails); }) .error(function (err: Error) { diff --git a/src/server/lib/ldap/IAuthenticator.ts b/src/server/lib/ldap/IAuthenticator.ts new file mode 100644 index 000000000..b1813ac2e --- /dev/null +++ b/src/server/lib/ldap/IAuthenticator.ts @@ -0,0 +1,6 @@ +import BluebirdPromise = require("bluebird"); +import { GroupsAndEmails } from "./IClient"; + +export interface IAuthenticator { + authenticate(username: string, password: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/ldap/IClient.ts b/src/server/lib/ldap/IClient.ts new file mode 100644 index 000000000..741ebaf61 --- /dev/null +++ b/src/server/lib/ldap/IClient.ts @@ -0,0 +1,16 @@ + +import BluebirdPromise = require("bluebird"); + +export interface GroupsAndEmails { + groups: string[]; + emails: string[]; +} + +export interface IClient { + open(): BluebirdPromise; + close(): BluebirdPromise; + searchUserDn(username: string): BluebirdPromise; + searchEmails(username: string): BluebirdPromise; + searchEmailsAndGroups(username: string): BluebirdPromise; + modifyPassword(username: string, newPassword: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/ldap/IClientFactory.ts b/src/server/lib/ldap/IClientFactory.ts new file mode 100644 index 000000000..19a6a656b --- /dev/null +++ b/src/server/lib/ldap/IClientFactory.ts @@ -0,0 +1,6 @@ + +import { IClient } from "./IClient"; + +export interface IClientFactory { + create(userDN: string, password: string): IClient; +} \ No newline at end of file diff --git a/src/server/lib/ldap/IEmailsRetriever.ts b/src/server/lib/ldap/IEmailsRetriever.ts new file mode 100644 index 000000000..608a2883f --- /dev/null +++ b/src/server/lib/ldap/IEmailsRetriever.ts @@ -0,0 +1,5 @@ +import BluebirdPromise = require("bluebird"); + +export interface IEmailsRetriever { + retrieve(username: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/ldap/IPasswordUpdater.ts b/src/server/lib/ldap/IPasswordUpdater.ts new file mode 100644 index 000000000..ff8f3d2c9 --- /dev/null +++ b/src/server/lib/ldap/IPasswordUpdater.ts @@ -0,0 +1,5 @@ +import BluebirdPromise = require("bluebird"); + +export interface IPasswordUpdater { + updatePassword(username: string, newPassword: string): BluebirdPromise; +} \ No newline at end of file diff --git a/src/server/lib/ldap/PasswordUpdater.ts b/src/server/lib/ldap/PasswordUpdater.ts index c4e834e2b..4e00034db 100644 --- a/src/server/lib/ldap/PasswordUpdater.ts +++ b/src/server/lib/ldap/PasswordUpdater.ts @@ -2,32 +2,23 @@ import BluebirdPromise = require("bluebird"); import exceptions = require("../Exceptions"); import ldapjs = require("ldapjs"); import { Client } from "./Client"; -import { buildUserDN } from "./common"; +import { IPasswordUpdater } from "./IPasswordUpdater"; import { LdapConfiguration } from "../configuration/Configuration"; -import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; +import { IClientFactory } from "./IClientFactory"; -export class PasswordUpdater { +export class PasswordUpdater implements IPasswordUpdater { private options: LdapConfiguration; - private ldapjs: Ldapjs; - private logger: Winston; - private dovehash: Dovehash; + private clientFactory: IClientFactory; - constructor(options: LdapConfiguration, ldapjs: Ldapjs, dovehash: Dovehash, logger: Winston) { + constructor(options: LdapConfiguration, clientFactory: IClientFactory) { this.options = options; - this.ldapjs = ldapjs; - this.logger = logger; - this.dovehash = dovehash; - } - - private createClient(userDN: string, password: string): Client { - return new Client(userDN, password, this.options, this.ldapjs, this.dovehash, this.logger); + this.clientFactory = clientFactory; } updatePassword(username: string, newPassword: string): BluebirdPromise { - const userDN = buildUserDN(username, this.options); - const adminClient = this.createClient(this.options.user, this.options.password); + const adminClient = this.clientFactory.create(this.options.user, this.options.password); return adminClient.open() .then(function () { diff --git a/src/server/lib/ldap/common.ts b/src/server/lib/ldap/common.ts deleted file mode 100644 index 6bc618333..000000000 --- a/src/server/lib/ldap/common.ts +++ /dev/null @@ -1,18 +0,0 @@ -import util = require("util"); - -import { LdapConfiguration } from "../configuration/Configuration"; - - -export function buildUserDN(username: string, options: LdapConfiguration): string { - let userNameAttribute = options.user_name_attribute; - // if not provided, default to cn - if (!userNameAttribute) userNameAttribute = "cn"; - - const additionalUserDN = options.additional_user_dn; - const base_dn = options.base_dn; - - let userDN = util.format("%s=%s", userNameAttribute, username); - if (additionalUserDN) userDN += util.format(",%s", additionalUserDN); - userDN += util.format(",%s", base_dn); - return userDN; -} \ No newline at end of file diff --git a/src/server/lib/routes/firstfactor/post.ts b/src/server/lib/routes/firstfactor/post.ts index 770c1c959..b5fa2df8b 100644 --- a/src/server/lib/routes/firstfactor/post.ts +++ b/src/server/lib/routes/firstfactor/post.ts @@ -5,7 +5,7 @@ import BluebirdPromise = require("bluebird"); import express = require("express"); import { AccessController } from "../../access_control/AccessController"; import { AuthenticationRegulator } from "../../AuthenticationRegulator"; -import { Client, Attributes } from "../../ldap/Client"; +import { GroupsAndEmails } from "../../ldap/IClient"; import Endpoint = require("../../../endpoints"); import ErrorReplies = require("../../ErrorReplies"); import { ServerVariablesHandler } from "../../ServerVariablesHandler"; @@ -38,13 +38,14 @@ export default function (req: express.Request, res: express.Response): BluebirdP logger.info("1st factor: No regulation applied."); return ldap.authenticate(username, password); }) - .then(function (attributes: Attributes) { - logger.info("1st factor: LDAP binding successful. Retrieved information about user are %s", JSON.stringify(attributes)); + .then(function (groupsAndEmails: GroupsAndEmails) { + logger.info("1st factor: LDAP binding successful. Retrieved information about user are %s", + JSON.stringify(groupsAndEmails)); authSession.userid = username; authSession.first_factor = true; - const emails: string[] = attributes.emails; - const groups: string[] = attributes.groups; + const emails: string[] = groupsAndEmails.emails; + const groups: string[] = groupsAndEmails.groups; if (!emails || emails.length <= 0) { const errMessage = "No emails found. The user should have at least one email address to reset password."; diff --git a/test/features/resilience.feature b/test/features/resilience.feature index a580db4e1..800ad0154 100644 --- a/test/features/resilience.feature +++ b/test/features/resilience.feature @@ -5,9 +5,17 @@ Feature: Authelia keeps user sessions despite the application restart And the application restarts Then I have access to: | url | - | https://public.test.local:8080/secret.html | | https://secret.test.local:8080/secret.html | - | https://secret1.test.local:8080/secret.html | - | https://secret2.test.local:8080/secret.html | - | https://mx1.mail.test.local:8080/secret.html | - | https://mx2.mail.test.local:8080/secret.html | \ No newline at end of file + + Scenario: Secrets are stored even when Authelia restarts + Given I visit "https://auth.test.local:8080/" + And I login with user "john" and password "password" + And I register a TOTP secret called "Sec0" + When the application restarts + And I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/" + And I login with user "john" and password "password" + And I use "Sec0" as TOTP token handle + And I click on "TOTP" + Then I have access to: + | url | + | https://secret.test.local:8080/secret.html | \ No newline at end of file diff --git a/test/unit/server/DataPersistence.test.ts b/test/unit/server/DataPersistence.test.ts deleted file mode 100644 index 0737fd402..000000000 --- a/test/unit/server/DataPersistence.test.ts +++ /dev/null @@ -1,179 +0,0 @@ - -import * as BluebirdPromise from "bluebird"; -import * as request from "request"; - -import Server from "../../../src/server/lib/Server"; -import { UserConfiguration } from "../../../src/server/lib/configuration/Configuration"; -import { GlobalDependencies } from "../../../src/types/Dependencies"; -import * as tmp from "tmp"; -import U2FMock = require("./mocks/u2f"); -import { LdapjsClientMock } from "./mocks/ldapjs"; - - -const requestp = BluebirdPromise.promisifyAll(request) as request.Request; -const assert = require("assert"); -const speakeasy = require("speakeasy"); -const sinon = require("sinon"); -const nedb = require("nedb"); -const session = require("express-session"); -const winston = require("winston"); - -const PORT = 8050; -const requests = require("./requests")(PORT); - -describe("test data persistence", function () { - let u2f: U2FMock.U2FMock; - let tmpDir: tmp.SynchrounousResult; - const ldapClient = LdapjsClientMock(); - const ldap = { - createClient: sinon.spy(function () { - return ldapClient; - }) - }; - - let config: UserConfiguration; - - before(function () { - u2f = U2FMock.U2FMock(); - - const search_doc = { - object: { - mail: "test_ok@example.com" - } - }; - - const search_res = { - on: sinon.spy(function (event: string, fn: (s: object) => void) { - if (event != "error") fn(search_doc); - }) - }; - - ldapClient.bind.withArgs("cn=admin,dc=example,dc=com", - "password").yields(); - ldapClient.bind.withArgs("cn=test_ok,ou=users,dc=example,dc=com", - "password").yields(); - ldapClient.bind.withArgs("cn=test_nok,ou=users,dc=example,dc=com", - "password").yields("error"); - ldapClient.search.yields(undefined, search_res); - ldapClient.unbind.yields(); - - tmpDir = tmp.dirSync({ unsafeCleanup: true }); - config = { - port: PORT, - ldap: { - url: "ldap://127.0.0.1:389", - base_dn: "ou=users,dc=example,dc=com", - user: "cn=admin,dc=example,dc=com", - password: "password" - }, - session: { - secret: "session_secret", - expiration: 50000, - }, - storage: { - local: { - path: tmpDir.name - } - }, - notifier: { - gmail: { - username: "user@example.com", - password: "password" - } - } - }; - }); - - after(function () { - tmpDir.removeCallback(); - }); - - it("should save a u2f meta and reload it after a restart of the server", function () { - let server: Server; - const sign_request = {}; - const sign_status = {}; - const registration_status = {}; - u2f.request.returns(sign_request); - u2f.checkRegistration.returns(sign_status); - u2f.checkSignature.returns(registration_status); - - const nodemailer = { - createTransport: sinon.spy(function () { - return transporter; - }) - }; - const transporter = { - sendMail: sinon.stub().yields() - }; - - const deps: GlobalDependencies = { - u2f: u2f, - nedb: nedb, - nodemailer: nodemailer, - session: session, - winston: winston, - ldapjs: ldap, - speakeasy: speakeasy, - ConnectRedis: sinon.spy(), - dovehash: sinon.spy() - }; - - const j1 = request.jar(); - const j2 = request.jar(); - - return start_server(config, deps) - .then(function (s) { - server = s; - return requests.login(j1); - }) - .then(function (res) { - return requests.first_factor(j1); - }) - .then(function () { - return requests.u2f_registration(j1, transporter); - }) - .then(function () { - return requests.u2f_authentication(j1); - }) - .then(function () { - return stop_server(server); - }) - .then(function () { - return start_server(config, deps); - }) - .then(function (s) { - server = s; - return requests.login(j2); - }) - .then(function () { - return requests.first_factor(j2); - }) - .then(function () { - return requests.u2f_authentication(j2); - }) - .then(function (res) { - assert.equal(200, res.statusCode); - server.stop(); - return BluebirdPromise.resolve(); - }) - .catch(function (err) { - console.error(err); - return BluebirdPromise.reject(err); - }); - }); - - function start_server(config: UserConfiguration, deps: GlobalDependencies): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - const s = new Server(); - s.start(config, deps); - resolve(s); - }); - } - - function stop_server(s: Server) { - return new BluebirdPromise(function (resolve, reject) { - s.stop(); - resolve(); - }); - } -}); diff --git a/test/unit/server/SessionConfigurationBuilder.test.ts b/test/unit/server/SessionConfigurationBuilder.test.ts index 9cf071a45..73271695f 100644 --- a/test/unit/server/SessionConfigurationBuilder.test.ts +++ b/test/unit/server/SessionConfigurationBuilder.test.ts @@ -17,9 +17,14 @@ describe("test session configuration builder", function () { }, ldap: { url: "ldap://ldap", - base_dn: "dc=example,dc=com", user: "user", - password: "password" + password: "password", + groups_dn: "ou=groups,dc=example,dc=com", + users_dn: "ou=users,dc=example,dc=com", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" }, logs_level: "debug", notifier: { @@ -77,9 +82,14 @@ describe("test session configuration builder", function () { }, ldap: { url: "ldap://ldap", - base_dn: "dc=example,dc=com", user: "user", - password: "password" + password: "password", + groups_dn: "ou=groups,dc=example,dc=com", + users_dn: "ou=users,dc=example,dc=com", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" }, logs_level: "debug", notifier: { diff --git a/test/unit/server/ConfigurationAdapter.test.ts b/test/unit/server/configuration/ConfigurationAdapter.test.ts similarity index 65% rename from test/unit/server/ConfigurationAdapter.test.ts rename to test/unit/server/configuration/ConfigurationAdapter.test.ts index 7e4b6b6c3..e699a7b71 100644 --- a/test/unit/server/ConfigurationAdapter.test.ts +++ b/test/unit/server/configuration/ConfigurationAdapter.test.ts @@ -1,14 +1,16 @@ import * as Assert from "assert"; -import { UserConfiguration } from "../../../src/server/lib/configuration/Configuration"; -import { ConfigurationAdapter } from "../../../src/server/lib/configuration/ConfigurationAdapter"; +import { UserConfiguration, LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; +import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/ConfigurationAdapter"; -describe("test config adapter", function() { +describe("test config adapter", function () { function build_yaml_config(): UserConfiguration { const yaml_config = { port: 8080, ldap: { url: "http://ldap", - base_dn: "cn=test,dc=example,dc=com", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", user: "user", password: "pass" }, @@ -33,41 +35,21 @@ describe("test config adapter", function() { return yaml_config; } - it("should read the port from the yaml file", function() { + it("should read the port from the yaml file", function () { const yaml_config = build_yaml_config(); yaml_config.port = 7070; const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.port, 7070); }); - it("should default the port to 8080 if not provided", function() { + it("should default the port to 8080 if not provided", function () { const yaml_config = build_yaml_config(); delete yaml_config.port; const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.port, 8080); }); - it("should get the ldap attributes", function() { - const yaml_config = build_yaml_config(); - yaml_config.ldap = { - url: "http://ldap", - base_dn: "cn=test,dc=example,dc=com", - additional_user_dn: "ou=users", - user_name_attribute: "uid", - user: "admin", - password: "pass" - }; - - const config = ConfigurationAdapter.adapt(yaml_config); - - Assert.equal(config.ldap.url, "http://ldap"); - Assert.equal(config.ldap.additional_user_dn, "ou=users"); - Assert.equal(config.ldap.user_name_attribute, "uid"); - Assert.equal(config.ldap.user, "admin"); - Assert.equal(config.ldap.password, "pass"); - }); - - it("should get the session attributes", function() { + it("should get the session attributes", function () { const yaml_config = build_yaml_config(); yaml_config.session = { domain: "example.com", @@ -80,14 +62,14 @@ describe("test config adapter", function() { Assert.equal(config.session.expiration, 3600); }); - it("should get the log level", function() { + it("should get the log level", function () { const yaml_config = build_yaml_config(); yaml_config.logs_level = "debug"; const config = ConfigurationAdapter.adapt(yaml_config); Assert.equal(config.logs_level, "debug"); }); - it("should get the notifier config", function() { + it("should get the notifier config", function () { const yaml_config = build_yaml_config(); yaml_config.notifier = { gmail: { @@ -104,7 +86,7 @@ describe("test config adapter", function() { }); }); - it("should get the access_control config", function() { + it("should get the access_control config", function () { const yaml_config = build_yaml_config(); yaml_config.access_control = { default: [], diff --git a/test/unit/server/configuration/LdapConfigurationAdaptation.test.ts b/test/unit/server/configuration/LdapConfigurationAdaptation.test.ts new file mode 100644 index 000000000..6a4a375f1 --- /dev/null +++ b/test/unit/server/configuration/LdapConfigurationAdaptation.test.ts @@ -0,0 +1,93 @@ +import * as Assert from "assert"; +import { UserConfiguration, LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; +import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/ConfigurationAdapter"; + +describe("test ldap configuration adaptation", function () { + function build_yaml_config(): UserConfiguration { + const yaml_config = { + port: 8080, + ldap: { + url: "http://ldap", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + user: "user", + password: "pass" + }, + session: { + domain: "example.com", + secret: "secret", + max_age: 40000 + }, + storage: { + local: { + path: "/mydirectory" + } + }, + logs_level: "debug", + notifier: { + gmail: { + username: "user", + password: "password" + } + } + }; + return yaml_config; + } + + it("should adapt correctly while user only specify mandatory fields", function () { + const yaml_config = build_yaml_config(); + yaml_config.ldap = { + url: "http://ldap", + base_dn: "dc=example,dc=com", + user: "admin", + password: "password" + }; + + const config = ConfigurationAdapter.adapt(yaml_config); + const expectedConfig: LdapConfiguration = { + url: "http://ldap", + users_dn: "dc=example,dc=com", + users_filter: "cn={0}", + groups_dn: "dc=example,dc=com", + groups_filter: "member={0}", + group_name_attribute: "cn", + mail_attribute: "mail", + user: "admin", + password: "password" + }; + + Assert.deepEqual(config.ldap, expectedConfig); + }); + + it("should adapt correctly while user specify every fields", function () { + const yaml_config = build_yaml_config(); + yaml_config.ldap = { + url: "http://ldap-server", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + users_filter: "uid={0}", + additional_groups_dn: "ou=groups", + groups_filter: "uniqueMember={0}", + mail_attribute: "email", + group_name_attribute: "groupName", + user: "admin2", + password: "password2" + }; + + const config = ConfigurationAdapter.adapt(yaml_config); + const expectedConfig: LdapConfiguration = { + url: "http://ldap-server", + users_dn: "ou=users,dc=example,dc=com", + users_filter: "uid={0}", + groups_dn: "ou=groups,dc=example,dc=com", + groups_filter: "uniqueMember={0}", + mail_attribute: "email", + group_name_attribute: "groupName", + user: "admin2", + password: "password2" + }; + + Assert.deepEqual(config.ldap, expectedConfig); + }); +}); diff --git a/test/unit/server/ldap/Authenticator.test.ts b/test/unit/server/ldap/Authenticator.test.ts index 9753163c5..08373d4c3 100644 --- a/test/unit/server/ldap/Authenticator.test.ts +++ b/test/unit/server/ldap/Authenticator.test.ts @@ -2,121 +2,127 @@ import { Authenticator } from "../../../../src/server/lib/ldap/Authenticator"; import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; -import sinon = require("sinon"); +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 Assert = require("assert"); -import { LdapjsMock, LdapjsClientMock } from "../mocks/ldapjs"; +import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub"; +import { ClientStub } from "../mocks/ldap/ClientStub"; describe("test ldap authentication", function () { + const USERNAME = "username"; + const PASSWORD = "password"; + + const ADMIN_USER_DN = "cn=admin,dc=example,dc=com"; + const ADMIN_PASSWORD = "admin_password"; + + let clientFactoryStub: ClientFactoryStub; + let adminClientStub: ClientStub; + let userClientStub: ClientStub; + let authenticator: Authenticator; - let ldapClient: LdapjsClientMock; - let ldapjs: LdapjsMock; let ldapConfig: LdapConfiguration; - let adminUserDN: string; - let adminPassword: string; - - function retrieveEmailsAndGroups(ldapClient: LdapjsClientMock) { - const email0 = { - object: { - mail: "user@example.com" - } - }; - - const email1 = { - object: { - mail: "user@example1.com" - } - }; - - const group0 = { - object: { - group: "group0" - } - }; - - const emailsEmitter = { - on: sinon.spy(function (event: string, fn: (doc: any) => void) { - if (event != "error") fn(email0); - if (event != "error") fn(email1); - }) - }; - - const groupsEmitter = { - on: sinon.spy(function (event: string, fn: (doc: any) => void) { - if (event != "error") fn(group0); - }) - }; - - ldapClient.search.onCall(0).yields(undefined, emailsEmitter); - ldapClient.search.onCall(1).yields(undefined, groupsEmitter); - } beforeEach(function () { - ldapClient = LdapjsClientMock(); - ldapjs = LdapjsMock(); - ldapjs.createClient.returns(ldapClient); - - // winston.level = "debug"; - - adminUserDN = "cn=admin,dc=example,dc=com"; - adminPassword = "password"; + clientFactoryStub = new ClientFactoryStub(); + adminClientStub = new ClientStub(); + userClientStub = new ClientStub(); ldapConfig = { url: "http://localhost:324", - user: adminUserDN, - password: adminPassword, - base_dn: "dc=example,dc=com", - additional_user_dn: "ou=users" + users_dn: "ou=users,dc=example,dc=com", + users_filter: "cn={0}", + groups_dn: "ou=groups,dc=example,dc=com", + groups_filter: "member={0}", + mail_attribute: "mail", + group_name_attribute: "cn", + user: ADMIN_USER_DN, + password: ADMIN_PASSWORD }; - authenticator = new Authenticator(ldapConfig, ldapjs, winston); + authenticator = new Authenticator(ldapConfig, clientFactoryStub); }); - function test_check_password_internal() { - const username = "username"; - const password = "password"; - return authenticator.authenticate(username, password); - } - describe("success", function () { - beforeEach(function () { - retrieveEmailsAndGroups(ldapClient); - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.unbind.yields(); - }); - it("should bind the user if good credentials provided", function () { - ldapClient.bind.withArgs("cn=username,ou=users,dc=example,dc=com", "password").yields(); - return test_check_password_internal(); - }); + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + clientFactoryStub.createStub.withArgs("cn=" + USERNAME + ",ou=users,dc=example,dc=com", PASSWORD) + .returns(userClientStub); - it("should bind the user with correct DN", function () { - ldapConfig.user_name_attribute = "uid"; - ldapClient.bind.withArgs("uid=username,ou=users,dc=example,dc=com", "password").yields(); - return test_check_password_internal(); + // admin connects successfully + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + // admin search for user dn of user + adminClientStub.searchUserDnStub.withArgs(USERNAME) + .returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com")); + + // user connects successfully + userClientStub.openStub.returns(BluebirdPromise.resolve()); + userClientStub.closeStub.returns(BluebirdPromise.resolve()); + + // admin retrieves emails and groups of user + adminClientStub.searchEmailsAndGroupsStub.returns(BluebirdPromise.resolve({ + groups: ["group1"], + emails: ["user@example.com"] + })); + + return authenticator.authenticate(USERNAME, PASSWORD); }); }); describe("failure", function () { it("should not bind the user if wrong credentials provided", function () { - ldapClient.bind.yields("wrong credentials"); - return test_check_password_internal() + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + clientFactoryStub.createStub.withArgs("cn=" + USERNAME + ",ou=users,dc=example,dc=com", PASSWORD) + .returns(userClientStub); + + // admin connects successfully + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + // admin search for user dn of user + adminClientStub.searchUserDnStub.withArgs(USERNAME) + .returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com")); + + // user connects successfully + userClientStub.openStub.returns(BluebirdPromise.reject(new Error("Error while binding"))); + userClientStub.closeStub.returns(BluebirdPromise.resolve()); + + return authenticator.authenticate(USERNAME, PASSWORD) + .then(function () { return BluebirdPromise.reject("Should not be here!"); }) .catch(function () { return BluebirdPromise.resolve(); }); }); it("should not bind the user if search of emails or group fails", function () { - ldapClient.bind.withArgs("cn=username,ou=users,dc=example,dc=com", "password").yields(); - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.unbind.yields(); - ldapClient.search.yields("wrong credentials"); - return test_check_password_internal() + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + clientFactoryStub.createStub.withArgs("cn=" + USERNAME + ",ou=users,dc=example,dc=com", PASSWORD) + .returns(userClientStub); + + // admin connects successfully + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + // admin search for user dn of user + adminClientStub.searchUserDnStub.withArgs(USERNAME) + .returns(BluebirdPromise.resolve("cn=" + USERNAME + ",ou=users,dc=example,dc=com")); + + // user connects successfully + userClientStub.openStub.returns(BluebirdPromise.resolve()); + userClientStub.closeStub.returns(BluebirdPromise.resolve()); + + // admin retrieves emails and groups of user + adminClientStub.searchEmailsAndGroupsStub + .returns(BluebirdPromise.reject(new Error("Error while retrieving emails and groups"))); + + return authenticator.authenticate(USERNAME, PASSWORD) + .then(function () { return BluebirdPromise.reject("Should not be here!"); }) .catch(function () { return BluebirdPromise.resolve(); }); diff --git a/test/unit/server/ldap/EmailsRetriever.test.ts b/test/unit/server/ldap/EmailsRetriever.test.ts index 1ad687682..2cfb7ad17 100644 --- a/test/unit/server/ldap/EmailsRetriever.test.ts +++ b/test/unit/server/ldap/EmailsRetriever.test.ts @@ -2,93 +2,74 @@ import { EmailsRetriever } from "../../../../src/server/lib/ldap/EmailsRetriever"; import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; -import sinon = require("sinon"); +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"; +import Assert = require("assert"); +import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub"; +import { ClientStub } from "../mocks/ldap/ClientStub"; describe("test emails retriever", function () { + const USERNAME = "username"; + const ADMIN_USER_DN = "cn=admin,dc=example,dc=com"; + const ADMIN_PASSWORD = "password"; + + let clientFactoryStub: ClientFactoryStub; + let adminClientStub: ClientStub; + let emailsRetriever: EmailsRetriever; - let ldapClient: LdapjsClientMock; - let ldapjs: LdapjsMock; let ldapConfig: LdapConfiguration; - let adminUserDN: string; - let adminPassword: string; beforeEach(function () { - ldapClient = LdapjsClientMock(); - ldapjs = LdapjsMock(); - ldapjs.createClient.returns(ldapClient); - - // winston.level = "debug"; - - adminUserDN = "cn=admin,dc=example,dc=com"; - adminPassword = "password"; + clientFactoryStub = new ClientFactoryStub(); + adminClientStub = new ClientStub(); ldapConfig = { - url: "http://localhost:324", - user: adminUserDN, - password: adminPassword, - base_dn: "dc=example,dc=com", - additional_user_dn: "ou=users" + url: "http://ldap", + user: ADMIN_USER_DN, + password: ADMIN_PASSWORD, + users_dn: "ou=users,dc=example,dc=com", + groups_dn: "ou=groups,dc=example,dc=com", + group_name_attribute: "cn", + groups_filter: "cn={0}", + mail_attribute: "mail", + users_filter: "cn={0}" }; - emailsRetriever = new EmailsRetriever(ldapConfig, ldapjs, winston); + emailsRetriever = new EmailsRetriever(ldapConfig, clientFactoryStub); }); - function retrieveEmails(ldapClient: LdapjsClientMock) { - const email0 = { - object: { - mail: "user@example.com" - } - }; - - const email1 = { - object: { - mail: "user@example1.com" - } - }; - - const emailsEmitter = { - on: sinon.spy(function (event: string, fn: (doc: any) => void) { - if (event != "error") fn(email0); - if (event != "error") fn(email1); - }) - }; - - ldapClient.search.onCall(0).yields(undefined, emailsEmitter); - } - - function test_emails_retrieval() { - const username = "username"; - return emailsRetriever.retrieve(username); - } - describe("success", function () { - beforeEach(function () { - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.unbind.yields(); - }); + it("should retrieve emails successfully", function () { + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); - it("should update the password successfully", function () { - retrieveEmails(ldapClient); - return test_emails_retrieval(); + // admin connects successfully + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + adminClientStub.searchEmailsStub.withArgs(USERNAME) + .returns(BluebirdPromise.resolve(["user@example.com"])); + + return emailsRetriever.retrieve(USERNAME); }); }); describe("failure", function () { it("should fail retrieving emails when search operation fails", function () { - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.search.yields("wrong credentials"); - return test_emails_retrieval() - .catch(function () { - return BluebirdPromise.resolve(); - }); + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + + // admin connects successfully + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + adminClientStub.searchEmailsStub.withArgs(USERNAME) + .returns(BluebirdPromise.reject(new Error("Error while searching emails"))); + + return emailsRetriever.retrieve(USERNAME) + .then(function () { return BluebirdPromise.reject(new Error("Should not be here")); }) + .catch(function () { return BluebirdPromise.resolve(); }); }); }); }); \ No newline at end of file diff --git a/test/unit/server/ldap/PasswordUpdater.test.ts b/test/unit/server/ldap/PasswordUpdater.test.ts index 9cf318aa2..514bf6011 100644 --- a/test/unit/server/ldap/PasswordUpdater.test.ts +++ b/test/unit/server/ldap/PasswordUpdater.test.ts @@ -2,82 +2,78 @@ import { PasswordUpdater } from "../../../../src/server/lib/ldap/PasswordUpdater"; import { LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration"; -import sinon = require("sinon"); +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"; +import Assert = require("assert"); +import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub"; +import { ClientStub } from "../mocks/ldap/ClientStub"; describe("test password update", function () { + const USERNAME = "username"; + const NEW_PASSWORD = "new-password"; + + const ADMIN_USER_DN = "cn=admin,dc=example,dc=com"; + const ADMIN_PASSWORD = "password"; + + let clientFactoryStub: ClientFactoryStub; + let adminClientStub: ClientStub; + let passwordUpdater: PasswordUpdater; - let ldapClient: LdapjsClientMock; - let ldapjs: LdapjsMock; let ldapConfig: LdapConfiguration; - let adminUserDN: string; - let adminPassword: string; let dovehash: any; beforeEach(function () { - ldapClient = LdapjsClientMock(); - ldapjs = LdapjsMock(); - ldapjs.createClient.returns(ldapClient); - - // winston.level = "debug"; - - adminUserDN = "cn=admin,dc=example,dc=com"; - adminPassword = "password"; + clientFactoryStub = new ClientFactoryStub(); + adminClientStub = new ClientStub(); ldapConfig = { - url: "http://localhost:324", - user: adminUserDN, - password: adminPassword, - base_dn: "dc=example,dc=com", - additional_user_dn: "ou=users" + url: "http://ldap", + user: ADMIN_USER_DN, + password: ADMIN_PASSWORD, + users_dn: "ou=users,dc=example,dc=com", + groups_dn: "ou=groups,dc=example,dc=com", + group_name_attribute: "cn", + groups_filter: "cn={0}", + mail_attribute: "mail", + users_filter: "cn={0}" }; dovehash = { - encode: sinon.stub() + encode: Sinon.stub() }; - passwordUpdater = new PasswordUpdater(ldapConfig, ldapjs, dovehash, winston); + passwordUpdater = new PasswordUpdater(ldapConfig, clientFactoryStub); }); - function test_update_password() { - const username = "username"; - const newpassword = "newpassword"; - return passwordUpdater.updatePassword(username, newpassword); - } - describe("success", function () { - beforeEach(function () { - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.unbind.yields(); - }); - it("should update the password successfully", function () { + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn"); - ldapClient.modify.withArgs("cn=username,ou=users,dc=example,dc=com", { - operation: "replace", - modification: { - userPassword: "{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn" - } - }).yields(); - return test_update_password(); + adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD).returns(BluebirdPromise.resolve()); + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + return passwordUpdater.updatePassword(USERNAME, NEW_PASSWORD); }); }); describe("failure", function () { it("should fail updating password when modify operation fails", function () { - ldapClient.bind.withArgs(adminUserDN, adminPassword).yields(); - ldapClient.modify.yields("wrong credentials"); - return test_update_password() - .catch(function () { - return BluebirdPromise.resolve(); - }); + clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD) + .returns(adminClientStub); + + dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn"); + adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD) + .returns(BluebirdPromise.reject(new Error("Error while updating password"))); + adminClientStub.openStub.returns(BluebirdPromise.resolve()); + adminClientStub.closeStub.returns(BluebirdPromise.resolve()); + + return passwordUpdater.updatePassword(USERNAME, NEW_PASSWORD) + .then(function () { return BluebirdPromise.reject(new Error("should not be here")); }) + .catch(function () { return BluebirdPromise.resolve(); }); }); }); }); \ No newline at end of file diff --git a/test/unit/server/mocks/ServerVariablesMock.ts b/test/unit/server/mocks/ServerVariablesMock.ts index 6281b3337..a59ab1580 100644 --- a/test/unit/server/mocks/ServerVariablesMock.ts +++ b/test/unit/server/mocks/ServerVariablesMock.ts @@ -2,7 +2,7 @@ import sinon = require("sinon"); import express = require("express"); import winston = require("winston"); import { UserDataStoreStub } from "./storage/UserDataStoreStub"; -import { ServerVariables, VARIABLES_KEY }  from "../../../../src/server/lib/ServerVariablesHandler"; +import { VARIABLES_KEY }  from "../../../../src/server/lib/ServerVariablesHandler"; export interface ServerVariablesMock { logger: any; diff --git a/test/unit/server/mocks/ldap/ClientFactoryStub.ts b/test/unit/server/mocks/ldap/ClientFactoryStub.ts new file mode 100644 index 000000000..26d3909ef --- /dev/null +++ b/test/unit/server/mocks/ldap/ClientFactoryStub.ts @@ -0,0 +1,16 @@ + +import { IClient } from "../../../../../src/server/lib/ldap/IClient"; +import { IClientFactory } from "../../../../../src/server/lib/ldap/IClientFactory"; +import Sinon = require("sinon"); + +export class ClientFactoryStub implements IClientFactory { + createStub: Sinon.SinonStub; + + constructor() { + this.createStub = Sinon.stub(); + } + + create(userDN: string, password: string): IClient { + return this.createStub(userDN, password); + } +} \ No newline at end of file diff --git a/test/unit/server/mocks/ldap/ClientStub.ts b/test/unit/server/mocks/ldap/ClientStub.ts new file mode 100644 index 000000000..faf3b74a0 --- /dev/null +++ b/test/unit/server/mocks/ldap/ClientStub.ts @@ -0,0 +1,46 @@ + +import BluebirdPromise = require("bluebird"); +import { IClient, GroupsAndEmails } from "../../../../../src/server/lib/ldap/IClient"; +import Sinon = require("sinon"); + +export class ClientStub implements IClient { + openStub: Sinon.SinonStub; + closeStub: Sinon.SinonStub; + searchUserDnStub: Sinon.SinonStub; + searchEmailsStub: Sinon.SinonStub; + searchEmailsAndGroupsStub: Sinon.SinonStub; + modifyPasswordStub: Sinon.SinonStub; + + constructor() { + this.openStub = Sinon.stub(); + this.closeStub = Sinon.stub(); + this.searchUserDnStub = Sinon.stub(); + this.searchEmailsStub = Sinon.stub(); + this.searchEmailsAndGroupsStub = Sinon.stub(); + this.modifyPasswordStub = Sinon.stub(); + } + + open(): BluebirdPromise { + return this.openStub(); + } + + close(): BluebirdPromise { + return this.closeStub(); + } + + searchUserDn(username: string): BluebirdPromise { + return this.searchUserDnStub(username); + } + + searchEmails(username: string): BluebirdPromise { + return this.searchEmailsStub(username); + } + + searchEmailsAndGroups(username: string): BluebirdPromise { + return this.searchEmailsAndGroupsStub(username); + } + + modifyPassword(username: string, newPassword: string): BluebirdPromise { + return this.modifyPasswordStub(username, newPassword); + } +} \ No newline at end of file diff --git a/test/unit/server/server/Server.test.ts b/test/unit/server/server/Server.test.ts index fcaf757ae..f8d2c315d 100644 --- a/test/unit/server/server/Server.test.ts +++ b/test/unit/server/server/Server.test.ts @@ -1,5 +1,3 @@ - - import Server from "../../../../src/server/lib/Server"; import { LdapjsClientMock } from "./../mocks/ldapjs"; @@ -36,7 +34,6 @@ describe("test the server", function () { 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", }, @@ -125,81 +122,10 @@ describe("test the server", function () { describe("test authentication and verification", function () { test_authentication(); - test_reset_password(); test_regulation(); }); function test_authentication() { - it("should return status code 401 when user is not authenticated", function () { - return requestp.getAsync({ url: BASE_URL + Endpoints.VERIFY_GET }) - .then(function (response: request.RequestResponse) { - Assert.equal(response.statusCode, 401); - return BluebirdPromise.resolve(); - }); - }); - - it("should return status code 204 when user is authenticated using totp", function () { - const j = requestp.jar(); - return requests.login(j) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 200, "get login page failed"); - return requests.first_factor(j); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 302, "first factor failed"); - return requests.register_totp(j, transporter); - }) - .then(function (base32_secret: string) { - const realToken = speakeasy.totp({ - secret: base32_secret, - encoding: "base32" - }); - return requests.totp(j, realToken); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 200, "second factor failed"); - return requests.verify(j); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 204, "verify failed"); - return BluebirdPromise.resolve(); - }) - .catch(function (err: Error) { return BluebirdPromise.reject(err); }); - }); - - it("should keep session variables when login page is reloaded", function () { - const j = requestp.jar(); - return requests.login(j) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 200, "get login page failed"); - return requests.first_factor(j); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 302, "first factor failed"); - return requests.register_totp(j, transporter); - }) - .then(function (base32_secret: string) { - const realToken = speakeasy.totp({ - secret: base32_secret, - encoding: "base32" - }); - return requests.totp(j, realToken); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 200, "second factor failed"); - return requests.login(j); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 200, "login page loading failed"); - return requests.verify(j); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 204, "verify failed"); - return BluebirdPromise.resolve(); - }) - .catch(function (err: Error) { return BluebirdPromise.reject(err); }); - }); - it("should return status code 204 when user is authenticated using u2f", function () { const sign_request = {}; const sign_status = {}; @@ -236,26 +162,6 @@ describe("test the server", function () { }); } - function test_reset_password() { - it("should reset the password", function () { - const j = requestp.jar(); - return requests.login(j) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 200, "get login page failed"); - return requests.first_factor(j); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.headers.location, Endpoints.SECOND_FACTOR_GET); - Assert.equal(res.statusCode, 302, "first factor failed"); - return requests.reset_password(j, transporter, "user", "new-password"); - }) - .then(function (res: request.RequestResponse) { - Assert.equal(res.statusCode, 204, "second factor, finish register failed"); - return BluebirdPromise.resolve(); - }); - }); - } - function test_regulation() { it("should regulate authentication", function () { const j = requestp.jar();