Allow LDAP provider to search using any attribute configured via mappers (#26235)

Closes #22436

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
Ricardo Martin 2024-02-21 09:48:39 +01:00 committed by GitHub
parent 7ea595d27b
commit 3bc074913e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 171 additions and 39 deletions

View file

@ -32,6 +32,7 @@ import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.naming.AuthenticationException;
@ -366,15 +367,9 @@ public class LDAPStorageProvider implements UserStorageProvider,
}
/**
* It supports
* <ul>
* <li>{@link UserModel#FIRST_NAME}</li>
* <li>{@link UserModel#LAST_NAME}</li>
* <li>{@link UserModel#EMAIL}</li>
* <li>{@link UserModel#USERNAME}</li>
* </ul>
*
* Other fields are not supported. The search for LDAP REST endpoints is done in the context of fields which are stored in LDAP (above).
* LDAP search supports {@link UserModel#SEARCH}, {@link UserModel#EXACT} and
* all the other user attributes that are managed by a mapper (method
* <em>getUserAttributes</em>).
*/
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults) {
@ -510,37 +505,62 @@ public class LDAPStorageProvider implements UserStorageProvider,
}
/**
* Searches LDAP using logical conjunction of params. It supports
* <ul>
* <li>{@link UserModel#FIRST_NAME}</li>
* <li>{@link UserModel#LAST_NAME}</li>
* <li>{@link UserModel#EMAIL}</li>
* <li>{@link UserModel#USERNAME}</li>
* </ul>
*
* For zero or any other param it returns all users.
* Searches LDAP using logical conjunction of params. It uses the LDAP mappers
* (method <em>getUserAttributes</em>) to control what attributes are
* managed by the ldap server. If one attribute is not defined by the
* mappers then empty stream is returned (the attribute is not mapped
* into ldap, therefore no ldap user can have the specified value).
*/
private Stream<LDAPObject> searchLDAPByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
// get the attributes that are managed by the configured ldap mappers
Set<String> managedAttrs = realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName())
.map(mapperManager::getMapper)
.map(LDAPStorageMapper::getUserAttributes)
.flatMap(Set::stream)
.collect(Collectors.toSet());
final boolean exact = Boolean.parseBoolean(attributes.get(UserModel.EXACT));
try (LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm)) {
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
// Mapper should replace parameter with correct LDAP mapped attributes
if (attributes.containsKey(UserModel.USERNAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.USERNAME, attributes.get(UserModel.USERNAME)));
for (Map.Entry<String, String> entry : attributes.entrySet()) {
String attrName = entry.getKey();
if (LDAPConstants.LDAP_ID.equals(attrName)) {
String uuidLDAPAttributeName = this.ldapIdentityStore.getConfig().getUuidLDAPAttributeName();
Condition usernameCondition = conditionsBuilder.equal(uuidLDAPAttributeName, entry.getValue());
ldapQuery.addWhereCondition(usernameCondition);
} else if (LDAPConstants.LDAP_ENTRY_DN.equals(attrName)) {
ldapQuery.setSearchDn(entry.getValue());
ldapQuery.setSearchScope(SearchControls.OBJECT_SCOPE);
} else if (managedAttrs.contains(attrName)) {
// we can search any attribute that is mapped to a user attribute
switch (attrName) {
case UserModel.USERNAME:
case UserModel.EMAIL:
case UserModel.FIRST_NAME:
case UserModel.LAST_NAME:
if (exact) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(attrName, entry.getValue()));
} else {
// doing a *value* search
ldapQuery.addWhereCondition(conditionsBuilder.substring(attrName, null, new String[]{entry.getValue()}, null));
}
break;
default:
// custom attributes are only equals
ldapQuery.addWhereCondition(conditionsBuilder.equal(attrName, entry.getValue()));
break;
}
} else if (!attrName.equals(UserModel.EXACT)
&& !attrName.equals(UserModel.INCLUDE_SERVICE_ACCOUNT)
&& !(UserModel.ENABLED.equals(attrName) && Boolean.parseBoolean(entry.getValue()))) {
// if the attr is not mapped just return empty stream
// skip special names and enabled if looking for true (enabled is not mapped so it's always true)
logger.debugf("Searching in LDAP using unmapped attribute [%s], returning empty stream", attrName);
return Stream.empty();
}
}
if (attributes.containsKey(UserModel.EMAIL)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.EMAIL, attributes.get(UserModel.EMAIL)));
}
if (attributes.containsKey(UserModel.FIRST_NAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.FIRST_NAME, attributes.get(UserModel.FIRST_NAME)));
}
if (attributes.containsKey(UserModel.LAST_NAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.LAST_NAME, attributes.get(UserModel.LAST_NAME)));
}
// for all other searchable fields: Ignoring is the fallback option, since it may overestimate the results but does not ignore matches.
// for empty params: all users are returned (pagination applies)
return paginatedSearchLDAP(ldapQuery, firstResult, maxResults);
}
}

View file

@ -139,7 +139,7 @@ public class ApiUtil {
public static UserRepresentation findUserByUsername(RealmResource realm, String username) {
UserRepresentation user = null;
List<UserRepresentation> ur = realm.users().search(username, null, null, null, 0, -1);
List<UserRepresentation> ur = realm.users().search(username, true);
if (ur.size() == 1) {
user = ur.get(0);
}

View file

@ -21,6 +21,7 @@ package org.keycloak.testsuite.federation.ldap;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
@ -29,10 +30,12 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.util.LDAPRule;
@ -55,6 +58,7 @@ public class LDAPSearchForUsersPaginationTest extends AbstractLDAPTest {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
LDAPTestUtils.addUserAttributeMapper(appRealm, ctx.getLdapModel(), "streetMapper", LDAPConstants.STREET, LDAPConstants.STREET);
// Delete all local users and add some new for testing
session.users().searchForUserStream(appRealm, new HashMap<>()).collect(Collectors.toList()).forEach(u -> session.users().removeUser(appRealm, u));
@ -62,9 +66,9 @@ public class LDAPSearchForUsersPaginationTest extends AbstractLDAPTest {
// Delete all LDAP users and add some new for testing
LDAPTestUtils.removeAllLDAPUsers(ctx.getLdapProvider(), appRealm);
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john", "Some", "Some", "john14@email.org", null, "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john00", "john", "Doe", "john0@email.org", null, "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john01", "john", "Doe", "john1@email.org", null, "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john", "Some", "Some", "john14@email.org", "Acacia Avenue", "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john00", "john", "Doe", "john0@email.org", "Acacia Avenue", "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john01", "john", "Doe", "john1@email.org", "Acacia Avenue", "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john02", "john", "Doe", "john2@email.org", null, "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john03", "john", "Doe", "john3@email.org", null, "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john04", "john", "Doe", "john4@email.org", null, "1234");
@ -124,6 +128,58 @@ public class LDAPSearchForUsersPaginationTest extends AbstractLDAPTest {
assertLDAPSearchMatchesLocalDB("john");
}
@Test
public void testSearchLDAPStreet() {
Set<String> usernames = testRealm().users().searchByAttributes("street:\"Acacia Avenue\"")
.stream().map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john", "john00", "john01"), usernames);
usernames = testRealm().users().searchByAttributes(0, 5, true, true, "street:\"Acacia Avenue\"")
.stream().map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john", "john00", "john01"), usernames);
}
@Test
public void testSearchNonExact() {
Set<String> usernames = testRealm().users().searchByEmail("1@email.org", false)
.stream()
.map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john01", "john11"), usernames);
usernames = testRealm().users().searchByEmail("1@email.org", false)
.stream()
.map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john01", "john11"), usernames);
}
@Test
public void testSearchLDAPLdapId() {
UserRepresentation john = testRealm().users().search("john", true).stream().findAny().orElse(null);
Assert.assertNotNull(john);
Assert.assertNotNull(john.firstAttribute(LDAPConstants.LDAP_ID));
Set<String> usernames = testRealm().users()
.searchByAttributes(LDAPConstants.LDAP_ID + ":" + john.firstAttribute(LDAPConstants.LDAP_ID))
.stream().map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john"), usernames);
}
@Test
public void testSearchLDAPLdapEntryDn() {
UserRepresentation john = testRealm().users().search("john", true).stream().findAny().orElse(null);
Assert.assertNotNull(john);
Assert.assertNotNull(john.firstAttribute(LDAPConstants.LDAP_ENTRY_DN));
Set<String> usernames = testRealm().users()
.searchByAttributes(LDAPConstants.LDAP_ENTRY_DN + ":" + john.firstAttribute(LDAPConstants.LDAP_ENTRY_DN))
.stream().map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john"), usernames);
}
private void assertLDAPSearchMatchesLocalDB(String searchString) {
//this call should import some users into local database
List<String> importedUsers = adminClient.realm(TEST_REALM_NAME).users().search(searchString, null, null).stream().map(UserRepresentation::getUsername).collect(Collectors.toList());

View file

@ -20,11 +20,14 @@ package org.keycloak.testsuite.federation.ldap.noimport;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.federation.ldap.AbstractLDAPTest;
@ -59,6 +62,7 @@ public class LDAPSearchForUsersPaginationNoImportTest extends AbstractLDAPTest {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
LDAPTestUtils.addUserAttributeMapper(appRealm, ctx.getLdapModel(), "streetMapper", LDAPConstants.STREET, LDAPConstants.STREET);
// Delete all local users to not interfere with federated ones
session.users().searchForUserStream(appRealm, new HashMap<>()).collect(Collectors.toList()).forEach(u -> session.users().removeUser(appRealm, u));
@ -66,9 +70,9 @@ public class LDAPSearchForUsersPaginationNoImportTest extends AbstractLDAPTest {
// Delete all LDAP users and add some new for testing
LDAPTestUtils.removeAllLDAPUsers(ctx.getLdapProvider(), appRealm);
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john", "Some", "Some", "john14@email.org", null, "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john00", "john", "Doe", "john0@email.org", null, "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john01", "john", "Doe", "john1@email.org", null, "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john", "Some", "Some", "john14@email.org", "Acacia Avenue", "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john00", "john", "Doe", "john0@email.org", "Acacia Avenue", "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john01", "john", "Doe", "john1@email.org", "Acacia Avenue", "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john02", "john", "Doe", "john2@email.org", null, "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john03", "john", "Doe", "john3@email.org", null, "1234");
LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "john04", "john", "Doe", "john4@email.org", null, "1234");
@ -136,4 +140,56 @@ public class LDAPSearchForUsersPaginationNoImportTest extends AbstractLDAPTest {
thirdFive.forEach(username -> assertThat(firstFive, not(hasItem(username))));
thirdFive.forEach(username -> assertThat(secondFive, not(hasItem(username))));
}
@Test
public void testSearchLDAPStreet() {
Set<String> usernames = testRealm().users().searchByAttributes("street:\"Acacia Avenue\"")
.stream().map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john", "john00", "john01"), usernames);
usernames = testRealm().users().searchByAttributes(0, 5, true, true, "street:\"Acacia Avenue\"")
.stream().map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john", "john00", "john01"), usernames);
}
@Test
public void testSearchNonExact() {
Set<String> usernames = testRealm().users().searchByEmail("1@email.org", false)
.stream()
.map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john01", "john11"), usernames);
usernames = testRealm().users().searchByEmail("1@email.org", false)
.stream()
.map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john01", "john11"), usernames);
}
@Test
public void testSearchLDAPLdapId() {
UserRepresentation john = testRealm().users().search("john", true).stream().findAny().orElse(null);
Assert.assertNotNull(john);
Assert.assertNotNull(john.firstAttribute(LDAPConstants.LDAP_ID));
Set<String> usernames = testRealm().users()
.searchByAttributes(LDAPConstants.LDAP_ID + ":" + john.firstAttribute(LDAPConstants.LDAP_ID))
.stream().map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john"), usernames);
}
@Test
public void testSearchLDAPLdapEntryDn() {
UserRepresentation john = testRealm().users().search("john", true).stream().findAny().orElse(null);
Assert.assertNotNull(john);
Assert.assertNotNull(john.firstAttribute(LDAPConstants.LDAP_ENTRY_DN));
Set<String> usernames = testRealm().users()
.searchByAttributes(LDAPConstants.LDAP_ENTRY_DN + ":" + john.firstAttribute(LDAPConstants.LDAP_ENTRY_DN))
.stream().map(UserRepresentation::getUsername)
.collect(Collectors.toSet());
Assert.assertEquals(Set.of("john"), usernames);
}
}