mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Get client by client attribute
Closes #42543 Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
parent
d98c474cdc
commit
51465f52a3
@ -18,6 +18,7 @@
|
||||
package org.keycloak.models.cache.infinispan;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.CacheStream;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
|
||||
@ -33,6 +34,7 @@ import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
@ -127,6 +129,13 @@ public class RealmCacheManager extends CacheManager {
|
||||
((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
|
||||
}
|
||||
|
||||
public <T> CacheStream<T> searchWithPredicate(Predicate<T> predicate, Class<T> tClass) {
|
||||
return cache.values().stream()
|
||||
.filter(tClass::isInstance)
|
||||
.map(tClass::cast)
|
||||
.filter(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a cached realm and ensure that this happens only once with the current Keycloak instance.
|
||||
* Use this to avoid concurrent preparations of a realm in parallel threads. This helps to break the load on
|
||||
|
||||
@ -41,6 +41,7 @@ import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.models.GroupProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RealmProvider;
|
||||
import org.keycloak.models.RoleModel;
|
||||
@ -60,6 +61,7 @@ import org.keycloak.models.cache.infinispan.entities.ClientScopeListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.GroupListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.GroupNameQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.RealmListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.Revisioned;
|
||||
import org.keycloak.models.cache.infinispan.entities.RoleListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.RoleByNameQuery;
|
||||
import org.keycloak.models.cache.infinispan.events.ClientAddedEvent;
|
||||
@ -1310,6 +1312,16 @@ public class RealmCacheSession implements CacheRealmProvider {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClientByAttribute(RealmModel realm, String name, String value) {
|
||||
List<CachedClient> clients = cache.searchWithPredicate(c -> value.equals(c.getAttributes().get(name)), CachedClient.class).limit(2).toList();
|
||||
return switch (clients.size()) {
|
||||
case 0 -> getClientDelegate().getClientByAttribute(realm, name, value);
|
||||
case 1 -> getClientById(realm, clients.get(0).getId());
|
||||
default -> throw new ModelException("Multiple clients found with the same attribute name and value");
|
||||
};
|
||||
}
|
||||
|
||||
private ClientModel prepareCachedClientByClientId(RealmModel realm, String clientId, String cacheKey) {
|
||||
ClientListQuery query = cache.get(cacheKey, ClientListQuery.class);
|
||||
String id;
|
||||
|
||||
@ -45,7 +45,8 @@ public class JpaClientProviderFactory implements ClientProviderFactory {
|
||||
|
||||
private static final List<String> REQUIRED_SEARCHABLE_ATTRIBUTES = Arrays.asList(
|
||||
"saml_idp_initiated_sso_url_name",
|
||||
SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER
|
||||
SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER,
|
||||
"jwt.credential.sub"
|
||||
);
|
||||
|
||||
@Override
|
||||
|
||||
@ -979,6 +979,16 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
||||
return session.clients().getClientById(realm, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClientByAttribute(RealmModel realm, String name, String value) {
|
||||
List<ClientModel> clients = searchClientsByAttributes(realm, Map.of(name, value), 0, 2).toList();
|
||||
return switch (clients.size()) {
|
||||
case 0 -> null;
|
||||
case 1 -> clients.get(0);
|
||||
default -> throw new ModelException("Multiple clients found with the same attribute name and value");
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, String clientId, Integer firstResult, Integer maxResults) {
|
||||
CriteriaBuilder builder = em.getCriteriaBuilder();
|
||||
|
||||
@ -157,6 +157,19 @@ public class ClientStorageManager implements ClientProvider {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClientByAttribute(RealmModel realm, String name, String value) {
|
||||
ClientModel client = localStorage().getClientByAttribute(realm, name, value);
|
||||
if (client != null) {
|
||||
return client;
|
||||
}
|
||||
return getEnabledStorageProviders(session, realm, ClientLookupProvider.class)
|
||||
.map(provider -> provider.getClientByAttribute(realm, name, value))
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, String clientId, Integer firstResult, Integer maxResults) {
|
||||
return query((p, f, m) -> p.searchClientsByClientIdStream(realm, clientId, f, m), realm, firstResult, maxResults);
|
||||
|
||||
@ -48,6 +48,17 @@ public interface ClientLookupProvider {
|
||||
*/
|
||||
ClientModel getClientByClientId(RealmModel realm, String clientId);
|
||||
|
||||
/**
|
||||
* Exact search for a client by an attribute. Throws an exception if
|
||||
* multi clients are found.
|
||||
*
|
||||
* @param realm Realm to limit the search for clients.
|
||||
* @param name The name of the client attribute
|
||||
* @param value The value of the client attribute
|
||||
* @return
|
||||
*/
|
||||
ClientModel getClientByAttribute(RealmModel realm, String name, String value);
|
||||
|
||||
/**
|
||||
* Case-insensitive search for clients that contain the given string in their public client identifier.
|
||||
* @param realm Realm to limit the search for clients.
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
package org.keycloak.tests.admin.client;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.cache.infinispan.ClientAdapter;
|
||||
import org.keycloak.testframework.annotations.InjectAdminClient;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.FetchOnServer;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.FetchOnServerWrapper;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@KeycloakIntegrationTest
|
||||
public class ClientByAttributesTest {
|
||||
|
||||
@InjectRealm(config = ClientByAttributesRealm.class)
|
||||
ManagedRealm realm;
|
||||
|
||||
@InjectRunOnServer
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectAdminClient
|
||||
Keycloak adminClient;
|
||||
|
||||
@Test
|
||||
public void lookupByAttribute() {
|
||||
runOnServer.run(s -> {
|
||||
ClientModel c = s.clients().getClientByAttribute(s.getContext().getRealm(), "jwt.credential.sub", "value1");
|
||||
Assertions.assertEquals("client1", c.getClientId());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookupByAttributeMultipleMatches() {
|
||||
runOnServer.run(s -> {
|
||||
try {
|
||||
s.clients().getClientByAttribute(s.getContext().getRealm(), "jwt.credential.sub", "value2");
|
||||
Assertions.fail("Expected exception");
|
||||
} catch (Exception e) {
|
||||
Assertions.assertEquals("Multiple clients found with the same attribute name and value", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookupByAttributeTestCached() {
|
||||
CachedTimeStamp cachedTimeStamp = new CachedTimeStamp("value1");
|
||||
Long cachedTimeStamp1 = runOnServer.fetch(cachedTimeStamp);
|
||||
Assertions.assertEquals(cachedTimeStamp1, runOnServer.fetch(cachedTimeStamp));
|
||||
|
||||
realm.admin().clearRealmCache();
|
||||
|
||||
Assertions.assertNotEquals(cachedTimeStamp1, runOnServer.fetch(cachedTimeStamp));
|
||||
}
|
||||
|
||||
public static class ClientByAttributesRealm implements RealmConfig {
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
realm.addClient("client1").attribute("jwt.credential.sub", "value1");
|
||||
realm.addClient("client2").attribute("jwt.credential.sub", "value2");
|
||||
realm.addClient("client3").attribute("jwt.credential.sub", "value2");
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedTimeStamp(String jwtCredentialSub) implements FetchOnServerWrapper<Long>, Serializable {
|
||||
|
||||
@Override
|
||||
public FetchOnServer getRunOnServer() {
|
||||
return s -> {
|
||||
ClientModel client = s.clients().getClientByAttribute(s.getContext().getRealm(), "jwt.credential.sub", jwtCredentialSub);
|
||||
return ((ClientAdapter) client).getCacheTimestamp();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Long> getResultClass() {
|
||||
return Long.class;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -74,6 +74,11 @@ public class HardcodedClientStorageProvider implements ClientStorageProvider, Cl
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClientByAttribute(RealmModel realm, String name, String value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user