Use username matcher instead of user dn in group filter

Previously, string "{0}" was replaced by the user dn in the groups_filter
attributes of the LDAP configuration.
However, if the groups children only have a memberUid attribute, one would
like to use the username instead of the user dn.

Since the user dn can be built from the username, "{0}" is now replaced
by the username instead of the user dn so that an LDAP relying on attribute
'memberUid' can be used.
pull/106/head
Clement Michaud 2017-10-07 13:46:19 +02:00
parent be81f04248
commit 66449eedb0
25 changed files with 444 additions and 154 deletions

View File

@ -23,18 +23,18 @@ ldap:
# An additional dn to define the scope to all users # An additional dn to define the scope to all users
additional_users_dn: ou=users additional_users_dn: ou=users
# The users filter. # The users filter used to find the user DN
# {0} is the matcher replaced by username. # {0} is a matcher replaced by username.
# 'cn={0}' by default. # 'cn={0}' by default.
users_filter: cn={0} users_filter: cn={0}
# An additional dn to define the scope of groups # An additional dn to define the scope of groups
additional_groups_dn: ou=groups additional_groups_dn: ou=groups
# The groups filter. # The groups filter used for retrieving groups of a given user.
# {0} is the matcher replaced by user dn. # {0} is a matcher replaced by username.
# 'member={0}' by default. # 'member=cn={0},<additional_users_dn>,<base_dn>' by default.
groups_filter: (&(member={0})(objectclass=groupOfNames)) groups_filter: (&(member=cn={0},ou=users,dc=example,dc=com)(objectclass=groupOfNames))
# The attribute holding the name of the group # The attribute holding the name of the group
group_name_attribute: cn group_name_attribute: cn

View File

@ -31,10 +31,10 @@ ldap:
# An additional dn to define the scope of groups # An additional dn to define the scope of groups
additional_groups_dn: ou=groups additional_groups_dn: ou=groups
# The groups filter. # The groups filter used for retrieving groups of a given user.
# {0} is the matcher replaced by user dn. # {0} is a matcher replaced by username.
# 'member={0}' by default. # 'member=cn={0},<additional_users_dn>,<base_dn>' by default.
groups_filter: (&(member={0})(objectclass=groupOfNames)) groups_filter: (&(member=cn={0},ou=users,dc=example,dc=com)(objectclass=groupOfNames))
# The attribute holding the name of the group # The attribute holding the name of the group
group_name_attribute: cn group_name_attribute: cn

View File

@ -1,6 +1,8 @@
import winston = require("winston"); import winston = require("winston");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import U2F = require("u2f");
import { IAuthenticator } from "./ldap/IAuthenticator"; import { IAuthenticator } from "./ldap/IAuthenticator";
import { IPasswordUpdater } from "./ldap/IPasswordUpdater"; import { IPasswordUpdater } from "./ldap/IPasswordUpdater";
import { IEmailsRetriever } from "./ldap/IEmailsRetriever"; import { IEmailsRetriever } from "./ldap/IEmailsRetriever";
@ -8,10 +10,10 @@ import { Authenticator } from "./ldap/Authenticator";
import { PasswordUpdater } from "./ldap/PasswordUpdater"; import { PasswordUpdater } from "./ldap/PasswordUpdater";
import { EmailsRetriever } from "./ldap/EmailsRetriever"; import { EmailsRetriever } from "./ldap/EmailsRetriever";
import { ClientFactory } from "./ldap/ClientFactory"; import { ClientFactory } from "./ldap/ClientFactory";
import { LdapClientFactory } from "./ldap/LdapClientFactory";
import { TOTPValidator } from "./TOTPValidator"; import { TOTPValidator } from "./TOTPValidator";
import { TOTPGenerator } from "./TOTPGenerator"; import { TOTPGenerator } from "./TOTPGenerator";
import U2F = require("u2f");
import { IUserDataStore } from "./storage/IUserDataStore"; import { IUserDataStore } from "./storage/IUserDataStore";
import { UserDataStore } from "./storage/UserDataStore"; import { UserDataStore } from "./storage/UserDataStore";
import { INotifier } from "./notifiers/INotifier"; import { INotifier } from "./notifiers/INotifier";
@ -73,11 +75,12 @@ class UserDataStoreFactory {
export class ServerVariablesHandler { export class ServerVariablesHandler {
static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise<void> { static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
const notifier = NotifierFactory.build(config.notifier, deps.nodemailer); const notifier = NotifierFactory.build(config.notifier, deps.nodemailer);
const ldapClientFactory = new ClientFactory(config.ldap, deps.ldapjs, deps.dovehash, deps.winston); const ldapClientFactory = new LdapClientFactory(config.ldap, deps.ldapjs);
const clientFactory = new ClientFactory(config.ldap, ldapClientFactory, deps.dovehash, deps.winston);
const ldapAuthenticator = new Authenticator(config.ldap, ldapClientFactory); const ldapAuthenticator = new Authenticator(config.ldap, clientFactory);
const ldapPasswordUpdater = new PasswordUpdater(config.ldap, ldapClientFactory); const ldapPasswordUpdater = new PasswordUpdater(config.ldap, clientFactory);
const ldapEmailsRetriever = new EmailsRetriever(config.ldap, ldapClientFactory); const ldapEmailsRetriever = new EmailsRetriever(config.ldap, clientFactory);
const accessController = new AccessController(config.access_control, deps.winston); const accessController = new AccessController(config.access_control, deps.winston);
const totpValidator = new TOTPValidator(deps.speakeasy); const totpValidator = new TOTPValidator(deps.speakeasy);
const totpGenerator = new TOTPGenerator(deps.speakeasy); const totpGenerator = new TOTPGenerator(deps.speakeasy);

View File

@ -6,6 +6,7 @@ import {
MongoStorageConfiguration, LocalStorageConfiguration, MongoStorageConfiguration, LocalStorageConfiguration,
UserLdapConfiguration UserLdapConfiguration
} from "./Configuration"; } from "./Configuration";
import Util = require("util");
const LDAP_URL_ENV_VARIABLE = "LDAP_URL"; const LDAP_URL_ENV_VARIABLE = "LDAP_URL";
@ -26,7 +27,10 @@ function ensure_key_existence(config: object, path: string): void {
function adaptLdapConfiguration(userConfig: UserLdapConfiguration): LdapConfiguration { function adaptLdapConfiguration(userConfig: UserLdapConfiguration): LdapConfiguration {
const DEFAULT_USERS_FILTER = "cn={0}"; const DEFAULT_USERS_FILTER = "cn={0}";
const DEFAULT_GROUPS_FILTER = "member={0}"; const DEFAULT_GROUPS_FILTER =
userConfig.additional_users_dn
? Util.format("member=cn={0},%s,%s", userConfig.additional_groups_dn, userConfig.base_dn)
: Util.format("member=cn={0},%s", userConfig.base_dn);
const DEFAULT_GROUP_NAME_ATTRIBUTE = "cn"; const DEFAULT_GROUP_NAME_ATTRIBUTE = "cn";
const DEFAULT_MAIL_ATTRIBUTE = "mail"; const DEFAULT_MAIL_ATTRIBUTE = "mail";

View File

@ -7,7 +7,7 @@ import { GroupsAndEmails } from "./IClient";
import { IAuthenticator } from "./IAuthenticator"; import { IAuthenticator } from "./IAuthenticator";
import { LdapConfiguration } from "../configuration/Configuration"; import { LdapConfiguration } from "../configuration/Configuration";
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies"; import { EmailsAndGroupsRetriever } from "./EmailsAndGroupsRetriever";
export class Authenticator implements IAuthenticator { export class Authenticator implements IAuthenticator {
@ -23,7 +23,7 @@ export class Authenticator implements IAuthenticator {
const that = this; const that = this;
let userClient: IClient; let userClient: IClient;
const adminClient = this.clientFactory.create(this.options.user, this.options.password); const adminClient = this.clientFactory.create(this.options.user, this.options.password);
let groupsAndEmails: GroupsAndEmails; const emailsAndGroupsRetriever = new EmailsAndGroupsRetriever(this.options, this.clientFactory);
return adminClient.open() return adminClient.open()
.then(function () { .then(function () {
@ -37,16 +37,9 @@ export class Authenticator implements IAuthenticator {
return userClient.close(); return userClient.close();
}) })
.then(function () { .then(function () {
return adminClient.open(); return emailsAndGroupsRetriever.retrieve(username);
}) })
.then(function () { .then(function (groupsAndEmails: GroupsAndEmails) {
return adminClient.searchEmailsAndGroups(username);
})
.then(function (gae: GroupsAndEmails) {
groupsAndEmails = gae;
return adminClient.close();
})
.then(function () {
return BluebirdPromise.resolve(groupsAndEmails); return BluebirdPromise.resolve(groupsAndEmails);
}) })
.error(function (err: Error) { .error(function (err: Error) {

View File

@ -2,63 +2,37 @@
import util = require("util"); import util = require("util");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import exceptions = require("../Exceptions"); import exceptions = require("../Exceptions");
import Ldapjs = require("ldapjs");
import Dovehash = require("dovehash"); import Dovehash = require("dovehash");
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { IClient, GroupsAndEmails } from "./IClient"; import { IClient, GroupsAndEmails } from "./IClient";
import { ILdapClient } from "./ILdapClient";
import { ILdapClientFactory } from "./ILdapClientFactory";
import { LdapConfiguration } from "../configuration/Configuration"; import { LdapConfiguration } from "../configuration/Configuration";
import { Winston } from "../../../types/Dependencies"; import { Winston } from "../../../types/Dependencies";
interface SearchEntry {
object: any;
}
declare module "ldapjs" {
export interface ClientAsync {
on(event: string, callback: (data?: any) => void): void;
bindAsync(username: string, password: string): BluebirdPromise<void>;
unbindAsync(): BluebirdPromise<void>;
searchAsync(base: string, query: Ldapjs.SearchOptions): BluebirdPromise<EventEmitter>;
modifyAsync(userdn: string, change: Ldapjs.Change): BluebirdPromise<void>;
}
}
export class Client implements IClient { export class Client implements IClient {
private userDN: string; private userDN: string;
private password: string; private password: string;
private client: Ldapjs.ClientAsync; private ldapClient: ILdapClient;
private ldapjs: typeof Ldapjs;
private logger: Winston; private logger: Winston;
private dovehash: typeof Dovehash; private dovehash: typeof Dovehash;
private options: LdapConfiguration; private options: LdapConfiguration;
constructor(userDN: string, password: string, options: LdapConfiguration, constructor(userDN: string, password: string, options: LdapConfiguration,
ldapjs: typeof Ldapjs, dovehash: typeof Dovehash, logger: Winston) { ldapClientFactory: ILdapClientFactory, dovehash: typeof Dovehash, logger: Winston) {
this.options = options; this.options = options;
this.ldapjs = ldapjs;
this.dovehash = dovehash; this.dovehash = dovehash;
this.logger = logger; this.logger = logger;
this.userDN = userDN; this.userDN = userDN;
this.password = password; this.password = password;
this.ldapClient = ldapClientFactory.create();
const ldapClient = ldapjs.createClient({
url: this.options.url,
reconnect: true
});
/*const clientLogger = (ldapClient as any).log;
if (clientLogger) {
clientLogger.level("trace");
}*/
this.client = BluebirdPromise.promisifyAll(ldapClient) as Ldapjs.ClientAsync;
} }
open(): BluebirdPromise<void> { open(): BluebirdPromise<void> {
this.logger.debug("LDAP: Bind user '%s'", this.userDN); this.logger.debug("LDAP: Bind user '%s'", this.userDN);
return this.client.bindAsync(this.userDN, this.password) return this.ldapClient.bindAsync(this.userDN, this.password)
.error(function (err: Error) { .error(function (err: Error) {
return BluebirdPromise.reject(new exceptions.LdapBindError(err.message)); return BluebirdPromise.reject(new exceptions.LdapBindError(err.message));
}); });
@ -66,61 +40,24 @@ export class Client implements IClient {
close(): BluebirdPromise<void> { close(): BluebirdPromise<void> {
this.logger.debug("LDAP: Unbind user '%s'", this.userDN); this.logger.debug("LDAP: Unbind user '%s'", this.userDN);
return this.client.unbindAsync() return this.ldapClient.unbindAsync()
.error(function (err: Error) { .error(function (err: Error) {
return BluebirdPromise.reject(new exceptions.LdapBindError(err.message)); return BluebirdPromise.reject(new exceptions.LdapBindError(err.message));
}); });
} }
private search(base: string, query: Ldapjs.SearchOptions): BluebirdPromise<any> { searchGroups(username: string): BluebirdPromise<string[]> {
const that = this; const that = this;
const filter = that.options.groups_filter.replace("{0}", username);
that.logger.debug("LDAP: Search for '%s' in '%s'", JSON.stringify(query), base);
return that.client.searchAsync(base, query)
.then(function (res: EventEmitter) {
const doc: SearchEntry[] = [];
return new BluebirdPromise((resolve, reject) => {
res.on("searchEntry", function (entry: SearchEntry) {
that.logger.debug("Entry retrieved from LDAP is '%s'", JSON.stringify(entry.object));
doc.push(entry.object);
});
res.on("error", function (err: Error) {
that.logger.error("LDAP: Error received during search '%s'.", JSON.stringify(err));
reject(new exceptions.LdapSearchError(err.message));
});
res.on("end", function () {
that.logger.debug("LDAP: Search ended and results are '%s'.", JSON.stringify(doc));
resolve(doc);
});
});
})
.catch(function (err: Error) {
return BluebirdPromise.reject(new exceptions.LdapSearchError(err.message));
});
}
private searchGroups(username: string): BluebirdPromise<string[]> {
const that = this;
const groups: string[] = [];
return that.searchUserDn(username)
.then(function (userDN: string) {
const filter = that.options.groups_filter.replace("{0}", userDN);
const query = { const query = {
scope: "sub", scope: "sub",
attributes: [that.options.group_name_attribute], attributes: [that.options.group_name_attribute],
filter: filter filter: filter
}; };
return that.search(that.options.groups_dn, query); return this.ldapClient.searchAsync(that.options.groups_dn, query)
}) .then(function (docs: { cn: string }[]) {
.then(function (docs) { const groups = docs.map((doc: any) => { return doc.cn; });
for (let i = 0; i < docs.length; ++i) {
groups.push(docs[i].cn);
}
that.logger.debug("LDAP: groups of user %s are %s", username, groups); that.logger.debug("LDAP: groups of user %s are %s", username, groups);
})
.then(function () {
return BluebirdPromise.resolve(groups); return BluebirdPromise.resolve(groups);
}); });
} }
@ -136,7 +73,7 @@ export class Client implements IClient {
}; };
that.logger.debug("LDAP: searching for user dn of %s", username); that.logger.debug("LDAP: searching for user dn of %s", username);
return that.search(this.options.users_dn, query) return that.ldapClient.searchAsync(this.options.users_dn, query)
.then(function (users: { dn: string }[]) { .then(function (users: { dn: string }[]) {
that.logger.debug("LDAP: retrieved user dn is %s", users[0].dn); that.logger.debug("LDAP: retrieved user dn is %s", users[0].dn);
return BluebirdPromise.resolve(users[0].dn); return BluebirdPromise.resolve(users[0].dn);
@ -153,35 +90,17 @@ export class Client implements IClient {
return this.searchUserDn(username) return this.searchUserDn(username)
.then(function (userDN) { .then(function (userDN) {
return that.search(userDN, query); return that.ldapClient.searchAsync(userDN, query);
}) })
.then(function (docs: { mail: string }[]) { .then(function (docs: { mail: string }[]) {
const emails: string[] = []; const emails: string[] = docs
if (typeof docs[0].mail === "string") .filter((d) => { return typeof d.mail === "string"; })
emails.push(docs[0].mail); .map((d) => { return d.mail; });
else {
emails.concat(docs[0].mail);
}
that.logger.debug("LDAP: emails of user '%s' are %s", username, emails); that.logger.debug("LDAP: emails of user '%s' are %s", username, emails);
return BluebirdPromise.resolve(emails); return BluebirdPromise.resolve(emails);
});
}
searchEmailsAndGroups(username: string): BluebirdPromise<GroupsAndEmails> {
const that = this;
let retrievedEmails: string[], retrievedGroups: string[];
return this.searchEmails(username)
.then(function (emails: string[]) {
retrievedEmails = emails;
return that.searchGroups(username);
}) })
.then(function (groups: string[]) { .catch(function (err: Error) {
retrievedGroups = groups; return BluebirdPromise.reject(new exceptions.LdapError("Error while searching emails. " + err.stack));
return BluebirdPromise.resolve({
emails: retrievedEmails,
groups: retrievedGroups
});
}); });
} }
@ -198,10 +117,10 @@ export class Client implements IClient {
this.logger.debug("LDAP: update password of user '%s'", username); this.logger.debug("LDAP: update password of user '%s'", username);
return this.searchUserDn(username) return this.searchUserDn(username)
.then(function (userDN: string) { .then(function (userDN: string) {
that.client.modifyAsync(userDN, change); that.ldapClient.modifyAsync(userDN, change);
}) })
.then(function () { .then(function () {
return that.client.unbindAsync(); return that.ldapClient.unbindAsync();
}); });
} }
} }

View File

@ -1,6 +1,7 @@
import { IClientFactory } from "./IClientFactory"; import { IClientFactory } from "./IClientFactory";
import { IClient } from "./IClient"; import { IClient } from "./IClient";
import { Client } from "./Client"; import { Client } from "./Client";
import { ILdapClientFactory } from "./ILdapClientFactory";
import { LdapConfiguration } from "../configuration/Configuration"; import { LdapConfiguration } from "../configuration/Configuration";
import Ldapjs = require("ldapjs"); import Ldapjs = require("ldapjs");
@ -9,19 +10,20 @@ import Winston = require("winston");
export class ClientFactory implements IClientFactory { export class ClientFactory implements IClientFactory {
private config: LdapConfiguration; private config: LdapConfiguration;
private ldapjs: typeof Ldapjs; private ldapClientFactory: ILdapClientFactory;
private dovehash: typeof Dovehash; private dovehash: typeof Dovehash;
private logger: typeof Winston; private logger: typeof Winston;
constructor(ldapConfiguration: LdapConfiguration, ldapjs: typeof Ldapjs, constructor(ldapConfiguration: LdapConfiguration, ldapClientFactory: ILdapClientFactory,
dovehash: typeof Dovehash, logger: typeof Winston) { dovehash: typeof Dovehash, logger: typeof Winston) {
this.config = ldapConfiguration; this.config = ldapConfiguration;
this.ldapjs = ldapjs; this.ldapClientFactory = ldapClientFactory;
this.dovehash = dovehash; this.dovehash = dovehash;
this.logger = logger; this.logger = logger;
} }
create(userDN: string, password: string): IClient { create(userDN: string, password: string): IClient {
return new Client(userDN, password, this.config, this.ldapjs, this.dovehash, this.logger); return new Client(userDN, password, this.config, this.ldapClientFactory,
this.dovehash, this.logger);
} }
} }

View File

@ -0,0 +1,46 @@
import BluebirdPromise = require("bluebird");
import exceptions = require("../Exceptions");
import ldapjs = require("ldapjs");
import { Client } from "./Client";
import { IClientFactory } from "./IClientFactory";
import { LdapConfiguration } from "../configuration/Configuration";
import { GroupsAndEmails } from "./IClient";
export class EmailsAndGroupsRetriever {
private options: LdapConfiguration;
private clientFactory: IClientFactory;
constructor(options: LdapConfiguration, clientFactory: IClientFactory) {
this.options = options;
this.clientFactory = clientFactory;
}
retrieve(username: string): BluebirdPromise<GroupsAndEmails> {
const adminClient = this.clientFactory.create(this.options.user, this.options.password);
let emails: string[];
let groups: string[];
return adminClient.open()
.then(function () {
return adminClient.searchEmails(username);
})
.then(function (emails_: string[]) {
emails = emails_;
return adminClient.searchGroups(username);
})
.then(function (groups_: string[]) {
groups = groups_;
return adminClient.close();
})
.then(function () {
return BluebirdPromise.resolve({
emails: emails,
groups: groups
});
})
.error(function (err: Error) {
return BluebirdPromise.reject(new exceptions.LdapError("Failed during emails and groups retrieval: " + err.message));
});
}
}

View File

@ -32,8 +32,8 @@ export class EmailsRetriever implements IEmailsRetriever {
.then(function () { .then(function () {
return BluebirdPromise.resolve(emails); return BluebirdPromise.resolve(emails);
}) })
.error(function (err: Error) { .catch(function (err: Error) {
return BluebirdPromise.reject(new exceptions.LdapError("Failed during password update: " + err.message)); return BluebirdPromise.reject(new exceptions.LdapError("Failed during email retrieval: " + err.message));
}); });
} }
} }

View File

@ -0,0 +1,39 @@
import BluebirdPromise = require("bluebird");
import exceptions = require("../Exceptions");
import ldapjs = require("ldapjs");
import { IClient } from "./IClient";
import { IClientFactory } from "./IClientFactory";
import { IGroupsRetriever } from "./IGroupsRetriever";
import { LdapConfiguration } from "../configuration/Configuration";
export class GroupsRetriever implements IGroupsRetriever {
private options: LdapConfiguration;
private clientFactory: IClientFactory;
constructor(options: LdapConfiguration, clientFactory: IClientFactory) {
this.options = options;
this.clientFactory = clientFactory;
}
retrieve(username: string, client?: IClient): BluebirdPromise<string[]> {
client = this.clientFactory.create(this.options.user, this.options.password);
let groups: string[];
return client.open()
.then(function () {
return client.searchGroups(username);
})
.then(function (groups_: string[]) {
groups = groups_;
return client.close();
})
.then(function () {
return BluebirdPromise.resolve(groups);
})
.catch(function (err: Error) {
return BluebirdPromise.reject(new exceptions.LdapError("Failed during groups retrieval: " + err.message));
});
}
}

View File

@ -11,6 +11,6 @@ export interface IClient {
close(): BluebirdPromise<void>; close(): BluebirdPromise<void>;
searchUserDn(username: string): BluebirdPromise<string>; searchUserDn(username: string): BluebirdPromise<string>;
searchEmails(username: string): BluebirdPromise<string[]>; searchEmails(username: string): BluebirdPromise<string[]>;
searchEmailsAndGroups(username: string): BluebirdPromise<GroupsAndEmails>; searchGroups(username: string): BluebirdPromise<string[]>;
modifyPassword(username: string, newPassword: string): BluebirdPromise<void>; modifyPassword(username: string, newPassword: string): BluebirdPromise<void>;
} }

View File

@ -1,5 +1,6 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import { IClient } from "./IClient";
export interface IEmailsRetriever { export interface IEmailsRetriever {
retrieve(username: string): BluebirdPromise<string[]>; retrieve(username: string, client?: IClient): BluebirdPromise<string[]>;
} }

View File

@ -0,0 +1,6 @@
import BluebirdPromise = require("bluebird");
import { IClient } from "./IClient";
export interface IGroupsRetriever {
retrieve(username: string): BluebirdPromise<string[]>;
}

View File

@ -0,0 +1,10 @@
import BluebirdPromise = require("bluebird");
import EventEmitter = require("events");
export interface ILdapClient {
bindAsync(username: string, password: string): BluebirdPromise<void>;
unbindAsync(): BluebirdPromise<void>;
searchAsync(base: string, query: any): BluebirdPromise<any[]>;
modifyAsync(dn: string, changeRequest: any): BluebirdPromise<void>;
}

View File

@ -0,0 +1,6 @@
import { ILdapClient } from "./ILdapClient";
export interface ILdapClientFactory {
create(): ILdapClient;
}

View File

@ -0,0 +1,71 @@
import LdapJs = require("ldapjs");
import EventEmitter = require("events");
import BluebirdPromise = require("bluebird");
import { ILdapClient } from "./ILdapClient";
import Exceptions = require("../Exceptions");
declare module "ldapjs" {
export interface ClientAsync {
on(event: string, callback: (data?: any) => void): void;
bindAsync(username: string, password: string): BluebirdPromise<void>;
unbindAsync(): BluebirdPromise<void>;
searchAsync(base: string, query: LdapJs.SearchOptions): BluebirdPromise<EventEmitter>;
modifyAsync(userdn: string, change: LdapJs.Change): BluebirdPromise<void>;
}
}
interface SearchEntry {
object: any;
}
export class LdapClient implements ILdapClient {
private client: LdapJs.ClientAsync;
constructor(url: string, ldapjs: typeof LdapJs) {
const ldapClient = ldapjs.createClient({
url: url,
reconnect: true
});
/*const clientLogger = (ldapClient as any).log;
if (clientLogger) {
clientLogger.level("trace");
}*/
this.client = BluebirdPromise.promisifyAll(ldapClient) as LdapJs.ClientAsync;
}
bindAsync(username: string, password: string): BluebirdPromise<void> {
return this.client.bindAsync(username, password);
}
unbindAsync(): BluebirdPromise<void> {
return this.client.unbindAsync();
}
searchAsync(base: string, query: any): BluebirdPromise<any[]> {
const that = this;
return this.client.searchAsync(base, query)
.then(function (res: EventEmitter) {
const doc: SearchEntry[] = [];
return new BluebirdPromise<any[]>((resolve, reject) => {
res.on("searchEntry", function (entry: SearchEntry) {
doc.push(entry.object);
});
res.on("error", function (err: Error) {
reject(new Exceptions.LdapSearchError(err.message));
});
res.on("end", function () {
resolve(doc);
});
});
})
.catch(function (err: Error) {
return BluebirdPromise.reject(new Exceptions.LdapSearchError(err.message));
});
}
modifyAsync(dn: string, changeRequest: any): BluebirdPromise<void> {
return this.client.modifyAsync(dn, changeRequest);
}
}

View File

@ -0,0 +1,20 @@
import { ILdapClientFactory } from "./ILdapClientFactory";
import { ILdapClient } from "./ILdapClient";
import { LdapClient } from "./LdapClient";
import { LdapConfiguration } from "../configuration/Configuration";
import Ldapjs = require("ldapjs");
export class LdapClientFactory implements ILdapClientFactory {
private config: LdapConfiguration;
private ldapjs: typeof Ldapjs;
constructor(ldapConfiguration: LdapConfiguration, ldapjs: typeof Ldapjs) {
this.config = ldapConfiguration;
this.ldapjs = ldapjs;
}
create(): ILdapClient {
return new LdapClient(this.config.url, this.ldapjs);
}
}

View File

@ -55,7 +55,7 @@ describe("test ldap configuration adaptation", function () {
users_dn: "dc=example,dc=com", users_dn: "dc=example,dc=com",
users_filter: "cn={0}", users_filter: "cn={0}",
groups_dn: "dc=example,dc=com", groups_dn: "dc=example,dc=com",
groups_filter: "member={0}", groups_filter: "member=cn={0},dc=example,dc=com",
group_name_attribute: "cn", group_name_attribute: "cn",
mail_attribute: "mail", mail_attribute: "mail",
user: "admin", user: "admin",

View File

@ -64,10 +64,8 @@ describe("test ldap authentication", function () {
userClientStub.closeStub.returns(BluebirdPromise.resolve()); userClientStub.closeStub.returns(BluebirdPromise.resolve());
// admin retrieves emails and groups of user // admin retrieves emails and groups of user
adminClientStub.searchEmailsAndGroupsStub.returns(BluebirdPromise.resolve({ adminClientStub.searchEmailsStub.returns(BluebirdPromise.resolve(["group1"]));
groups: ["group1"], adminClientStub.searchGroupsStub.returns(BluebirdPromise.resolve(["user@example.com"]));
emails: ["user@example.com"]
}));
return authenticator.authenticate(USERNAME, PASSWORD); return authenticator.authenticate(USERNAME, PASSWORD);
}); });
@ -117,8 +115,9 @@ describe("test ldap authentication", function () {
userClientStub.openStub.returns(BluebirdPromise.resolve()); userClientStub.openStub.returns(BluebirdPromise.resolve());
userClientStub.closeStub.returns(BluebirdPromise.resolve()); userClientStub.closeStub.returns(BluebirdPromise.resolve());
adminClientStub.searchEmailsStub.returns(BluebirdPromise.resolve(["group1"]));
// admin retrieves emails and groups of user // admin retrieves emails and groups of user
adminClientStub.searchEmailsAndGroupsStub adminClientStub.searchGroupsStub
.returns(BluebirdPromise.reject(new Error("Error while retrieving emails and groups"))); .returns(BluebirdPromise.reject(new Error("Error while retrieving emails and groups")));
return authenticator.authenticate(USERNAME, PASSWORD) return authenticator.authenticate(USERNAME, PASSWORD)

View File

@ -0,0 +1,45 @@
import { LdapConfiguration } from "../../src/lib/configuration/Configuration";
import { Client } from "../../src/lib/ldap/Client";
import { LdapClientFactoryStub } from "../mocks/ldap/LdapClientFactoryStub";
import { LdapClientStub } from "../mocks/ldap/LdapClientStub";
import Sinon = require("sinon");
import BluebirdPromise = require("bluebird");
import Assert = require("assert");
import Dovehash = require("dovehash");
import Winston = require("winston");
describe("test authelia ldap client", function () {
const USERNAME = "username";
const ADMIN_USER_DN = "cn=admin,dc=example,dc=com";
const ADMIN_PASSWORD = "password";
it("should replace {0} by username when searching for groups in LDAP", function () {
const options: LdapConfiguration = {
url: "ldap://ldap",
users_dn: "ou=users,dc=example,dc=com",
users_filter: "cn={0}",
groups_dn: "ou=groups,dc=example,dc=com",
groups_filter: "member=cn={0},ou=users,dc=example,dc=com",
group_name_attribute: "cn",
mail_attribute: "mail",
user: "cn=admin,dc=example,dc=com",
password: "password"
};
const factory = new LdapClientFactoryStub();
const ldapClient = new LdapClientStub();
factory.createStub.returns(ldapClient);
ldapClient.searchAsyncStub.returns(BluebirdPromise.resolve([
"group1"
]));
const client = new Client(ADMIN_USER_DN, ADMIN_PASSWORD, options, factory, Dovehash, Winston);
return client.searchGroups("user1")
.then(function () {
Assert.equal(ldapClient.searchAsyncStub.getCall(0).args[1].filter,
"member=cn=user1,ou=users,dc=example,dc=com");
});
});
});

View File

@ -0,0 +1,75 @@
import { GroupsRetriever } from "../../src/lib/ldap/GroupsRetriever";
import { LdapConfiguration } from "../../src/lib/configuration/Configuration";
import Sinon = require("sinon");
import BluebirdPromise = require("bluebird");
import Assert = require("assert");
import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub";
import { ClientStub } from "../mocks/ldap/ClientStub";
describe("test groups 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 groupsRetriever: GroupsRetriever;
let ldapConfig: LdapConfiguration;
beforeEach(function () {
clientFactoryStub = new ClientFactoryStub();
adminClientStub = new ClientStub();
ldapConfig = {
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: "member=cn={0},ou=users,dc=example,dc=com",
mail_attribute: "mail",
users_filter: "cn={0}"
};
groupsRetriever = new GroupsRetriever(ldapConfig, clientFactoryStub);
});
describe("success", function () {
it("should retrieve groups successfully", function () {
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
.returns(adminClientStub);
// admin connects successfully
adminClientStub.openStub.returns(BluebirdPromise.resolve());
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
adminClientStub.searchGroupsStub.withArgs(USERNAME)
.returns(BluebirdPromise.resolve(["user@example.com"]));
return groupsRetriever.retrieve(USERNAME);
});
});
describe("failure", function () {
it("should fail retrieving groups when search operation fails", function () {
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
.returns(adminClientStub);
// admin connects successfully
adminClientStub.openStub.returns(BluebirdPromise.resolve());
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
adminClientStub.searchGroupsStub.withArgs(USERNAME)
.returns(BluebirdPromise.reject(new Error("Error while searching groups")));
return groupsRetriever.retrieve(USERNAME)
.then(function () { return BluebirdPromise.reject(new Error("Should not be here")); })
.catch(function () { return BluebirdPromise.resolve(); });
});
});
});

View File

@ -8,7 +8,7 @@ export class ClientStub implements IClient {
closeStub: Sinon.SinonStub; closeStub: Sinon.SinonStub;
searchUserDnStub: Sinon.SinonStub; searchUserDnStub: Sinon.SinonStub;
searchEmailsStub: Sinon.SinonStub; searchEmailsStub: Sinon.SinonStub;
searchEmailsAndGroupsStub: Sinon.SinonStub; searchGroupsStub: Sinon.SinonStub;
modifyPasswordStub: Sinon.SinonStub; modifyPasswordStub: Sinon.SinonStub;
constructor() { constructor() {
@ -16,7 +16,7 @@ export class ClientStub implements IClient {
this.closeStub = Sinon.stub(); this.closeStub = Sinon.stub();
this.searchUserDnStub = Sinon.stub(); this.searchUserDnStub = Sinon.stub();
this.searchEmailsStub = Sinon.stub(); this.searchEmailsStub = Sinon.stub();
this.searchEmailsAndGroupsStub = Sinon.stub(); this.searchGroupsStub = Sinon.stub();
this.modifyPasswordStub = Sinon.stub(); this.modifyPasswordStub = Sinon.stub();
} }
@ -36,8 +36,8 @@ export class ClientStub implements IClient {
return this.searchEmailsStub(username); return this.searchEmailsStub(username);
} }
searchEmailsAndGroups(username: string): BluebirdPromise<GroupsAndEmails> { searchGroups(username: string): BluebirdPromise<string[]> {
return this.searchEmailsAndGroupsStub(username); return this.searchGroupsStub(username);
} }
modifyPassword(username: string, newPassword: string): BluebirdPromise<void> { modifyPassword(username: string, newPassword: string): BluebirdPromise<void> {

View File

@ -0,0 +1,16 @@
import Sinon = require("sinon");
import BluebirdPromise = require("bluebird");
import { ILdapClientFactory } from "../../../src/lib/ldap/ILdapClientFactory";
import { ILdapClient } from "../../../src/lib/ldap/ILdapClient";
export class LdapClientFactoryStub implements ILdapClientFactory {
createStub: Sinon.SinonStub;
constructor() {
this.createStub = Sinon.stub();
}
create(): ILdapClient {
return this.createStub();
}
}

View File

@ -0,0 +1,33 @@
import Sinon = require("sinon");
import BluebirdPromise = require("bluebird");
import { ILdapClient } from "../../../src/lib/ldap/ILdapClient";
export class LdapClientStub implements ILdapClient {
bindAsyncStub: Sinon.SinonStub;
unbindAsyncStub: Sinon.SinonStub;
searchAsyncStub: Sinon.SinonStub;
modifyAsyncStub: Sinon.SinonStub;
constructor() {
this.bindAsyncStub = Sinon.stub();
this.unbindAsyncStub = Sinon.stub();
this.searchAsyncStub = Sinon.stub();
this.modifyAsyncStub = Sinon.stub();
}
bindAsync(username: string, password: string): BluebirdPromise<void> {
return this.bindAsyncStub(username, password);
}
unbindAsync(): BluebirdPromise<void> {
return this.unbindAsyncStub();
}
searchAsync(base: string, query: any): BluebirdPromise<any[]> {
return this.searchAsyncStub(base, query);
}
modifyAsync(dn: string, changeRequest: any): BluebirdPromise<void> {
return this.modifyAsyncStub(dn, changeRequest);
}
}

View File

@ -18,14 +18,16 @@ Feature: User validate first factor
Given I visit "https://auth.test.local:8080/" Given I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password" And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0" And I register a TOTP secret called "Sec0"
When I visit "https://admin.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html" When I visit "https://admin.test.local:8080/secret.html"
And I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password" And I login with user "john" and password "password"
And I use "Sec0" as TOTP token handle And I use "Sec0" as TOTP token handle
And I click on "TOTP" And I click on "TOTP"
Then I'm redirected to "https://admin.test.local:8080/secret.html" Then I'm redirected to "https://admin.test.local:8080/secret.html"
Scenario: User fails TOTP second factor Scenario: User fails TOTP second factor
When I visit "https://admin.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html" When I visit "https://admin.test.local:8080/secret.html"
And I'm redirected to "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password" And I login with user "john" and password "password"
And I use "BADTOKEN" as TOTP token And I use "BADTOKEN" as TOTP token
And I click on "TOTP" And I click on "TOTP"