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:
parent
7ea595d27b
commit
3bc074913e
4 changed files with 171 additions and 39 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue