Merge pull request #106 from clems4ever/username-in-group-filter
Use username matcher instead of user dn in group filterpull/119/head
commit
9933900395
|
@ -23,18 +23,18 @@ ldap:
|
|||
# An additional dn to define the scope to all users
|
||||
additional_users_dn: ou=users
|
||||
|
||||
# The users filter.
|
||||
# {0} is the matcher replaced by username.
|
||||
# The users filter used to find the user DN
|
||||
# {0} is a matcher replaced by username.
|
||||
# 'cn={0}' by default.
|
||||
users_filter: cn={0}
|
||||
|
||||
# An additional dn to define the scope of groups
|
||||
additional_groups_dn: ou=groups
|
||||
|
||||
# The groups filter.
|
||||
# {0} is the matcher replaced by user dn.
|
||||
# 'member={0}' by default.
|
||||
groups_filter: (&(member={0})(objectclass=groupOfNames))
|
||||
# The groups filter used for retrieving groups of a given user.
|
||||
# {0} is a matcher replaced by username.
|
||||
# 'member=cn={0},<additional_users_dn>,<base_dn>' by default.
|
||||
groups_filter: (&(member=cn={0},ou=users,dc=example,dc=com)(objectclass=groupOfNames))
|
||||
|
||||
# The attribute holding the name of the group
|
||||
group_name_attribute: cn
|
||||
|
|
|
@ -31,10 +31,10 @@ ldap:
|
|||
# An additional dn to define the scope of groups
|
||||
additional_groups_dn: ou=groups
|
||||
|
||||
# The groups filter.
|
||||
# {0} is the matcher replaced by user dn.
|
||||
# 'member={0}' by default.
|
||||
groups_filter: (&(member={0})(objectclass=groupOfNames))
|
||||
# The groups filter used for retrieving groups of a given user.
|
||||
# {0} is a matcher replaced by username.
|
||||
# 'member=cn={0},<additional_users_dn>,<base_dn>' by default.
|
||||
groups_filter: (&(member=cn={0},ou=users,dc=example,dc=com)(objectclass=groupOfNames))
|
||||
|
||||
# The attribute holding the name of the group
|
||||
group_name_attribute: cn
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
|
||||
import winston = require("winston");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import U2F = require("u2f");
|
||||
|
||||
import { IAuthenticator } from "./ldap/IAuthenticator";
|
||||
import { IPasswordUpdater } from "./ldap/IPasswordUpdater";
|
||||
import { IEmailsRetriever } from "./ldap/IEmailsRetriever";
|
||||
|
@ -8,10 +10,10 @@ import { Authenticator } from "./ldap/Authenticator";
|
|||
import { PasswordUpdater } from "./ldap/PasswordUpdater";
|
||||
import { EmailsRetriever } from "./ldap/EmailsRetriever";
|
||||
import { ClientFactory } from "./ldap/ClientFactory";
|
||||
import { LdapClientFactory } from "./ldap/LdapClientFactory";
|
||||
|
||||
import { TOTPValidator } from "./TOTPValidator";
|
||||
import { TOTPGenerator } from "./TOTPGenerator";
|
||||
import U2F = require("u2f");
|
||||
import { IUserDataStore } from "./storage/IUserDataStore";
|
||||
import { UserDataStore } from "./storage/UserDataStore";
|
||||
import { INotifier } from "./notifiers/INotifier";
|
||||
|
@ -73,11 +75,12 @@ class UserDataStoreFactory {
|
|||
export class ServerVariablesHandler {
|
||||
static initialize(app: express.Application, config: Configuration.AppConfiguration, deps: GlobalDependencies): BluebirdPromise<void> {
|
||||
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 ldapPasswordUpdater = new PasswordUpdater(config.ldap, ldapClientFactory);
|
||||
const ldapEmailsRetriever = new EmailsRetriever(config.ldap, ldapClientFactory);
|
||||
const ldapAuthenticator = new Authenticator(config.ldap, clientFactory);
|
||||
const ldapPasswordUpdater = new PasswordUpdater(config.ldap, clientFactory);
|
||||
const ldapEmailsRetriever = new EmailsRetriever(config.ldap, clientFactory);
|
||||
const accessController = new AccessController(config.access_control, deps.winston);
|
||||
const totpValidator = new TOTPValidator(deps.speakeasy);
|
||||
const totpGenerator = new TOTPGenerator(deps.speakeasy);
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
MongoStorageConfiguration, LocalStorageConfiguration,
|
||||
UserLdapConfiguration
|
||||
} from "./Configuration";
|
||||
import Util = require("util");
|
||||
|
||||
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 {
|
||||
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_MAIL_ATTRIBUTE = "mail";
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { GroupsAndEmails } from "./IClient";
|
|||
|
||||
import { IAuthenticator } from "./IAuthenticator";
|
||||
import { LdapConfiguration } from "../configuration/Configuration";
|
||||
import { Winston, Ldapjs, Dovehash } from "../../../types/Dependencies";
|
||||
import { EmailsAndGroupsRetriever } from "./EmailsAndGroupsRetriever";
|
||||
|
||||
|
||||
export class Authenticator implements IAuthenticator {
|
||||
|
@ -23,7 +23,7 @@ export class Authenticator implements IAuthenticator {
|
|||
const that = this;
|
||||
let userClient: IClient;
|
||||
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()
|
||||
.then(function () {
|
||||
|
@ -37,16 +37,9 @@ export class Authenticator implements IAuthenticator {
|
|||
return userClient.close();
|
||||
})
|
||||
.then(function () {
|
||||
return adminClient.open();
|
||||
return emailsAndGroupsRetriever.retrieve(username);
|
||||
})
|
||||
.then(function () {
|
||||
return adminClient.searchEmailsAndGroups(username);
|
||||
})
|
||||
.then(function (gae: GroupsAndEmails) {
|
||||
groupsAndEmails = gae;
|
||||
return adminClient.close();
|
||||
})
|
||||
.then(function () {
|
||||
.then(function (groupsAndEmails: GroupsAndEmails) {
|
||||
return BluebirdPromise.resolve(groupsAndEmails);
|
||||
})
|
||||
.error(function (err: Error) {
|
||||
|
|
|
@ -2,63 +2,37 @@
|
|||
import util = require("util");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import exceptions = require("../Exceptions");
|
||||
import Ldapjs = require("ldapjs");
|
||||
import Dovehash = require("dovehash");
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { IClient, GroupsAndEmails } from "./IClient";
|
||||
import { ILdapClient } from "./ILdapClient";
|
||||
import { ILdapClientFactory } from "./ILdapClientFactory";
|
||||
import { LdapConfiguration } from "../configuration/Configuration";
|
||||
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 {
|
||||
private userDN: string;
|
||||
private password: string;
|
||||
private client: Ldapjs.ClientAsync;
|
||||
|
||||
private ldapjs: typeof Ldapjs;
|
||||
private ldapClient: ILdapClient;
|
||||
private logger: Winston;
|
||||
private dovehash: typeof Dovehash;
|
||||
private 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.ldapjs = ldapjs;
|
||||
this.dovehash = dovehash;
|
||||
this.logger = logger;
|
||||
this.userDN = userDN;
|
||||
this.password = password;
|
||||
|
||||
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;
|
||||
this.ldapClient = ldapClientFactory.create();
|
||||
}
|
||||
|
||||
open(): BluebirdPromise<void> {
|
||||
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) {
|
||||
return BluebirdPromise.reject(new exceptions.LdapBindError(err.message));
|
||||
});
|
||||
|
@ -66,61 +40,24 @@ export class Client implements IClient {
|
|||
|
||||
close(): BluebirdPromise<void> {
|
||||
this.logger.debug("LDAP: Unbind user '%s'", this.userDN);
|
||||
return this.client.unbindAsync()
|
||||
return this.ldapClient.unbindAsync()
|
||||
.error(function (err: Error) {
|
||||
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;
|
||||
|
||||
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 = {
|
||||
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);
|
||||
}
|
||||
const filter = that.options.groups_filter.replace("{0}", username);
|
||||
const query = {
|
||||
scope: "sub",
|
||||
attributes: [that.options.group_name_attribute],
|
||||
filter: filter
|
||||
};
|
||||
return this.ldapClient.searchAsync(that.options.groups_dn, query)
|
||||
.then(function (docs: { cn: string }[]) {
|
||||
const groups = docs.map((doc: any) => { return doc.cn; });
|
||||
that.logger.debug("LDAP: groups of user %s are %s", username, groups);
|
||||
})
|
||||
.then(function () {
|
||||
return BluebirdPromise.resolve(groups);
|
||||
});
|
||||
}
|
||||
|
@ -136,7 +73,7 @@ export class Client implements IClient {
|
|||
};
|
||||
|
||||
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 }[]) {
|
||||
that.logger.debug("LDAP: retrieved user dn is %s", users[0].dn);
|
||||
return BluebirdPromise.resolve(users[0].dn);
|
||||
|
@ -153,35 +90,17 @@ export class Client implements IClient {
|
|||
|
||||
return this.searchUserDn(username)
|
||||
.then(function (userDN) {
|
||||
return that.search(userDN, query);
|
||||
return that.ldapClient.searchAsync(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);
|
||||
}
|
||||
const emails: string[] = docs
|
||||
.filter((d) => { return typeof d.mail === "string"; })
|
||||
.map((d) => { return d.mail; });
|
||||
that.logger.debug("LDAP: emails of user '%s' are %s", username, 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[]) {
|
||||
retrievedGroups = groups;
|
||||
return BluebirdPromise.resolve({
|
||||
emails: retrievedEmails,
|
||||
groups: retrievedGroups
|
||||
});
|
||||
.catch(function (err: Error) {
|
||||
return BluebirdPromise.reject(new exceptions.LdapError("Error while searching emails. " + err.stack));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -198,10 +117,10 @@ export class Client implements IClient {
|
|||
this.logger.debug("LDAP: update password of user '%s'", username);
|
||||
return this.searchUserDn(username)
|
||||
.then(function (userDN: string) {
|
||||
that.client.modifyAsync(userDN, change);
|
||||
that.ldapClient.modifyAsync(userDN, change);
|
||||
})
|
||||
.then(function () {
|
||||
return that.client.unbindAsync();
|
||||
return that.ldapClient.unbindAsync();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { IClientFactory } from "./IClientFactory";
|
||||
import { IClient } from "./IClient";
|
||||
import { Client } from "./Client";
|
||||
import { ILdapClientFactory } from "./ILdapClientFactory";
|
||||
import { LdapConfiguration } from "../configuration/Configuration";
|
||||
|
||||
import Ldapjs = require("ldapjs");
|
||||
|
@ -9,19 +10,20 @@ import Winston = require("winston");
|
|||
|
||||
export class ClientFactory implements IClientFactory {
|
||||
private config: LdapConfiguration;
|
||||
private ldapjs: typeof Ldapjs;
|
||||
private ldapClientFactory: ILdapClientFactory;
|
||||
private dovehash: typeof Dovehash;
|
||||
private logger: typeof Winston;
|
||||
|
||||
constructor(ldapConfiguration: LdapConfiguration, ldapjs: typeof Ldapjs,
|
||||
constructor(ldapConfiguration: LdapConfiguration, ldapClientFactory: ILdapClientFactory,
|
||||
dovehash: typeof Dovehash, logger: typeof Winston) {
|
||||
this.config = ldapConfiguration;
|
||||
this.ldapjs = ldapjs;
|
||||
this.ldapClientFactory = ldapClientFactory;
|
||||
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);
|
||||
return new Client(userDN, password, this.config, this.ldapClientFactory,
|
||||
this.dovehash, this.logger);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -32,8 +32,8 @@ export class EmailsRetriever implements IEmailsRetriever {
|
|||
.then(function () {
|
||||
return BluebirdPromise.resolve(emails);
|
||||
})
|
||||
.error(function (err: Error) {
|
||||
return BluebirdPromise.reject(new exceptions.LdapError("Failed during password update: " + err.message));
|
||||
.catch(function (err: Error) {
|
||||
return BluebirdPromise.reject(new exceptions.LdapError("Failed during email retrieval: " + err.message));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -11,6 +11,6 @@ export interface IClient {
|
|||
close(): BluebirdPromise<void>;
|
||||
searchUserDn(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>;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import BluebirdPromise = require("bluebird");
|
||||
import { IClient } from "./IClient";
|
||||
|
||||
export interface IEmailsRetriever {
|
||||
retrieve(username: string): BluebirdPromise<string[]>;
|
||||
retrieve(username: string, client?: IClient): BluebirdPromise<string[]>;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import BluebirdPromise = require("bluebird");
|
||||
import { IClient } from "./IClient";
|
||||
|
||||
export interface IGroupsRetriever {
|
||||
retrieve(username: string): BluebirdPromise<string[]>;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
import { ILdapClient } from "./ILdapClient";
|
||||
|
||||
export interface ILdapClientFactory {
|
||||
create(): ILdapClient;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -55,7 +55,7 @@ describe("test ldap configuration adaptation", function () {
|
|||
users_dn: "dc=example,dc=com",
|
||||
users_filter: "cn={0}",
|
||||
groups_dn: "dc=example,dc=com",
|
||||
groups_filter: "member={0}",
|
||||
groups_filter: "member=cn={0},dc=example,dc=com",
|
||||
group_name_attribute: "cn",
|
||||
mail_attribute: "mail",
|
||||
user: "admin",
|
||||
|
|
|
@ -64,10 +64,8 @@ describe("test ldap authentication", function () {
|
|||
userClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||
|
||||
// admin retrieves emails and groups of user
|
||||
adminClientStub.searchEmailsAndGroupsStub.returns(BluebirdPromise.resolve({
|
||||
groups: ["group1"],
|
||||
emails: ["user@example.com"]
|
||||
}));
|
||||
adminClientStub.searchEmailsStub.returns(BluebirdPromise.resolve(["group1"]));
|
||||
adminClientStub.searchGroupsStub.returns(BluebirdPromise.resolve(["user@example.com"]));
|
||||
|
||||
return authenticator.authenticate(USERNAME, PASSWORD);
|
||||
});
|
||||
|
@ -117,8 +115,9 @@ describe("test ldap authentication", function () {
|
|||
userClientStub.openStub.returns(BluebirdPromise.resolve());
|
||||
userClientStub.closeStub.returns(BluebirdPromise.resolve());
|
||||
|
||||
adminClientStub.searchEmailsStub.returns(BluebirdPromise.resolve(["group1"]));
|
||||
// admin retrieves emails and groups of user
|
||||
adminClientStub.searchEmailsAndGroupsStub
|
||||
adminClientStub.searchGroupsStub
|
||||
.returns(BluebirdPromise.reject(new Error("Error while retrieving emails and groups")));
|
||||
|
||||
return authenticator.authenticate(USERNAME, PASSWORD)
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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(); });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,7 +8,7 @@ export class ClientStub implements IClient {
|
|||
closeStub: Sinon.SinonStub;
|
||||
searchUserDnStub: Sinon.SinonStub;
|
||||
searchEmailsStub: Sinon.SinonStub;
|
||||
searchEmailsAndGroupsStub: Sinon.SinonStub;
|
||||
searchGroupsStub: Sinon.SinonStub;
|
||||
modifyPasswordStub: Sinon.SinonStub;
|
||||
|
||||
constructor() {
|
||||
|
@ -16,7 +16,7 @@ export class ClientStub implements IClient {
|
|||
this.closeStub = Sinon.stub();
|
||||
this.searchUserDnStub = Sinon.stub();
|
||||
this.searchEmailsStub = Sinon.stub();
|
||||
this.searchEmailsAndGroupsStub = Sinon.stub();
|
||||
this.searchGroupsStub = Sinon.stub();
|
||||
this.modifyPasswordStub = Sinon.stub();
|
||||
}
|
||||
|
||||
|
@ -36,8 +36,8 @@ export class ClientStub implements IClient {
|
|||
return this.searchEmailsStub(username);
|
||||
}
|
||||
|
||||
searchEmailsAndGroups(username: string): BluebirdPromise<GroupsAndEmails> {
|
||||
return this.searchEmailsAndGroupsStub(username);
|
||||
searchGroups(username: string): BluebirdPromise<string[]> {
|
||||
return this.searchGroupsStub(username);
|
||||
}
|
||||
|
||||
modifyPassword(username: string, newPassword: string): BluebirdPromise<void> {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -18,14 +18,16 @@ Feature: User validate first factor
|
|||
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 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 use "Sec0" as TOTP token handle
|
||||
And I click on "TOTP"
|
||||
Then I'm redirected to "https://admin.test.local:8080/secret.html"
|
||||
|
||||
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 use "BADTOKEN" as TOTP token
|
||||
And I click on "TOTP"
|
||||
|
|
Loading…
Reference in New Issue