Add {dn} as an available matcher in LDAP groups filter

Sometimes, LDAP organization is such that groups membership cannot be computed
with username only. User DN is required to retrieve groups.

e.g. user Joe has a username joe and a cn of Joe Blogs, resulting in a dn of
cn=Joe Blogs,ou=users,dc=example,dc=com which is needed to retrieve groups
but cannot be computed from joe only.

Issue was reported in issue #146
This commit is contained in:
Clement Michaud 2017-10-15 14:27:20 +02:00
parent 15fa6286ad
commit ce264ff4d3
7 changed files with 82 additions and 21 deletions

View File

@ -33,8 +33,9 @@ ldap:
# The groups filter used for retrieving groups of a given user. # The groups filter used for retrieving groups of a given user.
# {0} is a matcher replaced by username. # {0} is a matcher replaced by username.
# 'member=cn={0},<additional_users_dn>,<base_dn>' by default. # {dn} is a matcher replaced by user DN.
groups_filter: (&(member=cn={0},ou=users,dc=example,dc=com)(objectclass=groupOfNames)) # 'member={dn}' by default.
groups_filter: (&(member={dn})(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

@ -33,8 +33,9 @@ ldap:
# The groups filter used for retrieving groups of a given user. # The groups filter used for retrieving groups of a given user.
# {0} is a matcher replaced by username. # {0} is a matcher replaced by username.
# 'member=cn={0},<additional_users_dn>,<base_dn>' by default. # {dn} is a matcher replaced by user DN.
groups_filter: (&(member=cn={0},ou=users,dc=example,dc=com)(objectclass=groupOfNames)) # 'member={dn}' by default.
groups_filter: (&(member={dn})(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

@ -29,10 +29,7 @@ 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 = const DEFAULT_GROUPS_FILTER = "member={dn}";
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

@ -47,18 +47,34 @@ export class Client implements IClient {
}); });
} }
private createGroupsFilter(userGroupsFilter: string, username: string): BluebirdPromise<string> {
if (userGroupsFilter.indexOf("{0}") > 0) {
return BluebirdPromise.resolve(userGroupsFilter.replace("{0}", username));
}
else if (userGroupsFilter.indexOf("{dn}") > 0) {
return this.searchUserDn(username)
.then(function (userDN: string) {
return BluebirdPromise.resolve(userGroupsFilter.replace("{dn}", userDN));
});
}
return BluebirdPromise.resolve(userGroupsFilter);
}
searchGroups(username: string): BluebirdPromise<string[]> { searchGroups(username: string): BluebirdPromise<string[]> {
const that = this; const that = this;
const filter = that.options.groups_filter.replace("{0}", username); return this.createGroupsFilter(this.options.groups_filter, username)
const query = { .then(function (groupsFilter: string) {
scope: "sub", that.logger.debug("Computed groups filter is %s", groupsFilter);
attributes: [that.options.group_name_attribute], const query = {
filter: filter scope: "sub",
}; attributes: [that.options.group_name_attribute],
return this.ldapClient.searchAsync(that.options.groups_dn, query) filter: groupsFilter
};
return that.ldapClient.searchAsync(that.options.groups_dn, query);
})
.then(function (docs: { cn: string }[]) { .then(function (docs: { cn: string }[]) {
const groups = docs.map((doc: any) => { return doc.cn; }); const groups = docs.map((doc: any) => { return doc.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.join(","));
return BluebirdPromise.resolve(groups); return BluebirdPromise.resolve(groups);
}); });
} }
@ -66,6 +82,7 @@ export class Client implements IClient {
searchUserDn(username: string): BluebirdPromise<string> { searchUserDn(username: string): BluebirdPromise<string> {
const that = this; const that = this;
const filter = this.options.users_filter.replace("{0}", username); const filter = this.options.users_filter.replace("{0}", username);
this.logger.debug("Computed users filter is %s", filter);
const query = { const query = {
scope: "sub", scope: "sub",
sizeLimit: 1, sizeLimit: 1,

View File

@ -45,7 +45,7 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro
const isAllowed = accessController.isAccessAllowed(domain, path, username, groups); const isAllowed = accessController.isAccessAllowed(domain, path, username, groups);
if (!isAllowed) return BluebirdPromise.reject( if (!isAllowed) return BluebirdPromise.reject(
new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%'", new exceptions.DomainAccessDenied(Util.format("User '%s' does not have access to '%s'",
username, domain))); username, domain)));
if (authenticationMethod == "two_factor" && !authSession.second_factor) if (authenticationMethod == "two_factor" && !authSession.second_factor)

View File

@ -56,7 +56,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=cn={0},dc=example,dc=com", groups_filter: "member={dn}",
group_name_attribute: "cn", group_name_attribute: "cn",
mail_attribute: "mail", mail_attribute: "mail",
user: "admin", user: "admin",

View File

@ -31,9 +31,9 @@ describe("test authelia ldap client", function () {
const ldapClient = new LdapClientStub(); const ldapClient = new LdapClientStub();
factory.createStub.returns(ldapClient); factory.createStub.returns(ldapClient);
ldapClient.searchAsyncStub.returns(BluebirdPromise.resolve([ ldapClient.searchAsyncStub.returns(BluebirdPromise.resolve([{
"group1" cn: "group1"
])); }]));
const client = new Client(ADMIN_USER_DN, ADMIN_PASSWORD, options, factory, Dovehash, Winston); const client = new Client(ADMIN_USER_DN, ADMIN_PASSWORD, options, factory, Dovehash, Winston);
return client.searchGroups("user1") return client.searchGroups("user1")
@ -42,4 +42,49 @@ describe("test authelia ldap client", function () {
"member=cn=user1,ou=users,dc=example,dc=com"); "member=cn=user1,ou=users,dc=example,dc=com");
}); });
}); });
it("should replace {dn} by user DN when searching for groups in LDAP", function () {
const USER_DN = "cn=user1,ou=users,dc=example,dc=com";
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={dn}",
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);
// Retrieve user DN
ldapClient.searchAsyncStub.withArgs("ou=users,dc=example,dc=com", {
scope: "sub",
sizeLimit: 1,
attributes: ["dn"],
filter: "cn=user1"
}).returns(BluebirdPromise.resolve([{
dn: USER_DN
}]));
// Retrieve groups
ldapClient.searchAsyncStub.withArgs("ou=groups,dc=example,dc=com", {
scope: "sub",
attributes: ["cn"],
filter: "member=" + USER_DN
}).returns(BluebirdPromise.resolve([{
cn: "group1"
}]));
const client = new Client(ADMIN_USER_DN, ADMIN_PASSWORD, options, factory, Dovehash, Winston);
return client.searchGroups("user1")
.then(function (groups: string[]) {
Assert.deepEqual(groups, ["group1"]);
});
});
}); });