Support injecting admin client configured from managed realm (#35693)

Closes #35501

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-12-06 14:00:16 +01:00 committed by GitHub
parent e05bd40d92
commit d3b759b56f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 122 additions and 20 deletions

View File

@ -0,0 +1,9 @@
package org.keycloak.test.framework;
public class TestFrameworkException extends RuntimeException {
public TestFrameworkException(String message) {
super(message);
}
}

View File

@ -3,12 +3,18 @@ package org.keycloak.test.framework.admin;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.test.framework.TestFrameworkException;
import org.keycloak.test.framework.annotations.InjectAdminClient;
import org.keycloak.test.framework.config.Config;
import org.keycloak.test.framework.injection.InstanceContext;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.realm.ManagedRealm;
import org.keycloak.test.framework.realm.ManagedUser;
import org.keycloak.test.framework.server.KeycloakServer;
public class KeycloakAdminClientSupplier implements Supplier<Keycloak, InjectAdminClient> {
@ -25,14 +31,46 @@ public class KeycloakAdminClientSupplier implements Supplier<Keycloak, InjectAdm
@Override
public Keycloak getValue(InstanceContext<Keycloak, InjectAdminClient> instanceContext) {
InjectAdminClient annotation = instanceContext.getAnnotation();
InjectAdminClient.Mode mode = annotation.mode();
KeycloakServer server = instanceContext.getDependency(KeycloakServer.class);
return KeycloakBuilder.builder()
KeycloakBuilder clientBuilder = KeycloakBuilder.builder()
.serverUrl(server.getBaseUrl())
.realm("master")
.grantType(OAuth2Constants.CLIENT_CREDENTIALS)
.clientId(Config.getAdminClientId())
.clientSecret(Config.getAdminClientSecret())
.build();
.grantType(OAuth2Constants.CLIENT_CREDENTIALS);
if (mode.equals(InjectAdminClient.Mode.BOOTSTRAP)) {
clientBuilder.realm("master").clientId(Config.getAdminClientId()).clientSecret(Config.getAdminClientSecret());
} else if (mode.equals(InjectAdminClient.Mode.MANAGED_REALM)) {
ManagedRealm managedRealm = instanceContext.getDependency(ManagedRealm.class);
clientBuilder.realm(managedRealm.getName());
String clientId = !annotation.client().isEmpty() ? annotation.client() : null;
String userId = !annotation.user().isEmpty() ? annotation.user() : null;
if (clientId == null) {
throw new TestFrameworkException("Client is required when using managed realm mode");
}
RealmRepresentation realmRep = managedRealm.getCreatedRepresentation();
ClientRepresentation clientRep = realmRep.getClients().stream()
.filter(c -> c.getClientId().equals(annotation.client()))
.findFirst().orElseThrow(() -> new TestFrameworkException("Client " + annotation.client() + " not found in managed realm"));
clientBuilder.clientId(clientId).clientSecret(clientRep.getSecret());
if (userId != null) {
UserRepresentation userRep = realmRep.getUsers().stream()
.filter(u -> u.getUsername().equals(annotation.user()))
.findFirst().orElseThrow(() -> new TestFrameworkException("User " + annotation.user() + " not found in managed realm"));
String password = ManagedUser.getPassword(userRep);
clientBuilder.username(userRep.getUsername()).password(password);
clientBuilder.grantType(OAuth2Constants.PASSWORD);
}
}
return clientBuilder.build();
}
@Override

View File

@ -10,4 +10,19 @@ import java.lang.annotation.Target;
@Target(ElementType.FIELD)
public @interface InjectAdminClient {
String ref() default "";
Mode mode() default Mode.BOOTSTRAP;
String client() default "";
String user() default "";
enum Mode {
BOOTSTRAP,
MANAGED_REALM
}
}

View File

@ -7,19 +7,23 @@ import java.lang.reflect.Proxy;
public class DefaultAnnotationProxy implements InvocationHandler {
private final Class<?> annotationClass;
private final String ref;
public <S> DefaultAnnotationProxy(Class<?> annotationClass) {
public <S> DefaultAnnotationProxy(Class<?> annotationClass, String ref) {
this.annotationClass = annotationClass;
this.ref = ref;
}
public static <S> S proxy(Class<S> annotationClass) {
return (S) Proxy.newProxyInstance(DefaultAnnotationProxy.class.getClassLoader(), new Class<?>[]{annotationClass}, new DefaultAnnotationProxy(annotationClass));
public static <S> S proxy(Class<S> annotationClass, String ref) {
return (S) Proxy.newProxyInstance(DefaultAnnotationProxy.class.getClassLoader(), new Class<?>[]{annotationClass}, new DefaultAnnotationProxy(annotationClass, ref));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("annotationType")) {
return annotationClass;
} else if (method.getName().equals("ref")) {
return ref;
} else {
return annotationClass.getMethod(method.getName()).getDefaultValue();
}

View File

@ -100,7 +100,7 @@ public class Registry implements ExtensionContext.Store.CloseableResource {
private <T> T getUnConfiguredDependency(Class<T> typeClass, String ref, InstanceContext dependent) {
InstanceContext dependency;
Supplier<?, ?> supplier = extensions.findSupplierByType(typeClass);
Annotation defaultAnnotation = DefaultAnnotationProxy.proxy(supplier.getAnnotationClass());
Annotation defaultAnnotation = DefaultAnnotationProxy.proxy(supplier.getAnnotationClass(), ref);
dependency = new InstanceContext(-1, this, supplier, defaultAnnotation, typeClass);
dependency.registerDependency(dependent);
@ -238,7 +238,7 @@ public class Registry implements ExtensionContext.Store.CloseableResource {
} else {
Supplier<?, ?> supplier = extensions.findSupplierByType(valueType);
if (supplier != null) {
Annotation defaultAnnotation = DefaultAnnotationProxy.proxy(supplier.getAnnotationClass());
Annotation defaultAnnotation = DefaultAnnotationProxy.proxy(supplier.getAnnotationClass(), "");
return new RequestedInstance(supplier, defaultAnnotation, valueType);
}
}

View File

@ -40,6 +40,10 @@ public class ManagedRealm extends ManagedTestResource {
return realmResource;
}
public RealmRepresentation getCreatedRepresentation() {
return createdRepresentation;
}
public void updateWithCleanup(RealmUpdate... updates) {
RealmRepresentation rep = admin().toRepresentation();
cleanup().resetToOriginalRepresentation(rep);

View File

@ -26,12 +26,16 @@ public class ManagedUser {
}
public String getPassword() {
Optional<CredentialRepresentation> password = createdRepresentation.getCredentials().stream().filter(c -> c.getType().equals(CredentialRepresentation.PASSWORD)).findFirst();
return password.map(CredentialRepresentation::getValue).orElse(null);
return getPassword(createdRepresentation);
}
public UserResource admin() {
return userResource;
}
public static String getPassword(UserRepresentation userRepresentation) {
Optional<CredentialRepresentation> password = userRepresentation.getCredentials().stream().filter(c -> c.getType().equals(CredentialRepresentation.PASSWORD)).findFirst();
return password.map(CredentialRepresentation::getValue).orElse(null);
}
}

View File

@ -30,7 +30,7 @@ public class RealmSupplier implements Supplier<ManagedRealm, InjectRealm> {
@Override
public ManagedRealm getValue(InstanceContext<ManagedRealm, InjectRealm> instanceContext) {
KeycloakServer server = instanceContext.getDependency(KeycloakServer.class);
Keycloak adminClient = instanceContext.getDependency(Keycloak.class);
Keycloak adminClient = instanceContext.getDependency(Keycloak.class, "bootstrap-client");
RealmConfig config = SupplierHelpers.getInstance(instanceContext.getAnnotation().config());

View File

@ -16,16 +16,22 @@ public class DefaultAnnotationProxyTest {
@Test
public void testGetField() {
MockAnnotation proxy = DefaultAnnotationProxy.proxy(MockAnnotation.class);
MockAnnotation proxy = DefaultAnnotationProxy.proxy(MockAnnotation.class, "");
Assertions.assertEquals(LifeCycle.CLASS, proxy.lifecycle());
Assertions.assertEquals(LinkedList.class, proxy.config());
Assertions.assertEquals("", proxy.ref());
Assertions.assertEquals("else", proxy.something());
}
@Test
public void testCustomRef() {
MockAnnotation proxy = DefaultAnnotationProxy.proxy(MockAnnotation.class, "myref");
Assertions.assertEquals("myref", proxy.ref());
}
@Test
public void testAnnotationReflection() {
MockAnnotation proxy = DefaultAnnotationProxy.proxy(MockAnnotation.class);
MockAnnotation proxy = DefaultAnnotationProxy.proxy(MockAnnotation.class, "");
Assertions.assertEquals(LifeCycle.CLASS, SupplierHelpers.getAnnotationField(proxy, "lifecycle"));
Assertions.assertEquals(LinkedList.class, SupplierHelpers.getAnnotationField(proxy, "config"));
Assertions.assertEquals("", SupplierHelpers.getAnnotationField(proxy, "ref"));

View File

@ -2,12 +2,17 @@ package org.keycloak.test.examples;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.test.framework.annotations.InjectAdminClient;
import org.keycloak.test.framework.annotations.InjectRealm;
import org.keycloak.test.framework.annotations.KeycloakIntegrationTest;
import org.keycloak.test.framework.realm.ManagedRealm;
@ -17,14 +22,31 @@ import org.keycloak.test.framework.realm.RealmConfigBuilder;
import java.util.List;
@KeycloakIntegrationTest
public class RealmWithClientAndUserTest {
public class RealmSpecificAdminClientTest {
@InjectRealm(config = RealmWithClientAndUser.class)
ManagedRealm realm;
@InjectAdminClient(ref = "bootstrap-client")
Keycloak bootstrapAdminClient;
@InjectAdminClient(mode = InjectAdminClient.Mode.MANAGED_REALM, client = "myclient", user = "myadmin")
Keycloak realmAdminClient;
@Test
public void testAdminClientIssuers() throws JWSInputException {
AccessToken bootstrapAccessToken = new JWSInput(bootstrapAdminClient.tokenManager().getAccessToken().getToken()).readJsonContent(AccessToken.class);
Assertions.assertTrue(bootstrapAccessToken.getIssuer().endsWith("/realms/master"));
AccessToken realmAccessToken = new JWSInput(realmAdminClient.tokenManager().getAccessToken().getToken()).readJsonContent(AccessToken.class);
Assertions.assertTrue(realmAccessToken.getIssuer().endsWith("/realms/" + realm.getName()));
}
@Test
public void testRealmWithClientAndUser() {
List<ClientRepresentation> clients = realm.admin().clients().findByClientId("myclient");
RealmResource realmResource = realmAdminClient.realms().realm(realm.getName());
List<ClientRepresentation> clients = realmResource.clients().findByClientId("myclient");
Assertions.assertEquals(1, clients.size());
ClientRepresentation client = clients.get(0);
@ -42,7 +64,7 @@ public class RealmWithClientAndUserTest {
Assertions.assertEquals("myadmin@localhost", user.getEmail());
Assertions.assertTrue(user.isEmailVerified());
MappingsRepresentation roles = realm.admin().users().get(user.getId()).roles().getAll();
MappingsRepresentation roles = realmResource.users().get(user.getId()).roles().getAll();
Assertions.assertEquals(1, roles.getClientMappings().get(Constants.REALM_MANAGEMENT_CLIENT_ID).getMappings().size());
}