Skip to content

Commit

Permalink
Use leniant non-managed ObjectMapper in Keycloak Admin clients
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik authored and sberyozkin committed Dec 23, 2024
1 parent 0304e70 commit 27d6bc2
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@

import javax.net.ssl.SSLContext;

import jakarta.enterprise.inject.Instance;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.MediaType;

import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.resteasy.reactive.client.TlsConfig;
import org.jboss.resteasy.reactive.client.api.ClientLogger;
import org.jboss.resteasy.reactive.client.impl.ClientBuilderImpl;
Expand All @@ -27,7 +25,6 @@
import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.jackson.ObjectMapperCustomizer;
import io.quarkus.rest.client.reactive.jackson.runtime.serialisers.ClientJacksonMessageBodyWriter;
import io.quarkus.tls.TlsConfiguration;
import io.vertx.core.net.KeyCertOptions;
Expand Down Expand Up @@ -71,38 +68,13 @@ private ClientBuilderImpl registerJacksonProviders(ClientBuilderImpl clientBuild
if (arcContainer == null) {
throw new IllegalStateException(this.getClass().getName() + " should only be used in a Quarkus application");
} else {
InstanceHandle<ObjectMapper> objectMapperInstance = arcContainer.instance(ObjectMapper.class);
boolean canReuseObjectMapper = canReuseObjectMapper(objectMapperInstance, arcContainer);
if (canReuseObjectMapper) {

ObjectMapper objectMapper = null;

InstanceHandle<JacksonBasicMessageBodyReader> readerInstance = arcContainer
.instance(JacksonBasicMessageBodyReader.class);
if (readerInstance.isAvailable()) {
clientBuilder = clientBuilder.register(readerInstance.get());
} else {
objectMapper = getObjectMapper(objectMapper, objectMapperInstance);
clientBuilder = clientBuilder.register(new JacksonBasicMessageBodyReader(objectMapper));
}

InstanceHandle<ClientJacksonMessageBodyWriter> writerInstance = arcContainer
.instance(ClientJacksonMessageBodyWriter.class);
if (writerInstance.isAvailable()) {
clientBuilder = clientBuilder.register(writerInstance.get());
} else {
objectMapper = getObjectMapper(objectMapper, objectMapperInstance);
clientBuilder = clientBuilder.register(new ClientJacksonMessageBodyWriter(objectMapper));
}
} else {
ObjectMapper newObjectMapper = newKeycloakAdminClientObjectMapper();
clientBuilder = clientBuilder
.registerMessageBodyReader(new JacksonBasicMessageBodyReader(newObjectMapper), Object.class,
HANDLED_MEDIA_TYPES, true,
READER_PROVIDER_PRIORITY)
.registerMessageBodyWriter(new ClientJacksonMessageBodyWriter(newObjectMapper), Object.class,
HANDLED_MEDIA_TYPES, true, WRITER_PROVIDER_PRIORITY);
}
ObjectMapper newObjectMapper = newKeycloakAdminClientObjectMapper();
clientBuilder = clientBuilder
.registerMessageBodyReader(new JacksonBasicMessageBodyReader(newObjectMapper), Object.class,
HANDLED_MEDIA_TYPES, true,
READER_PROVIDER_PRIORITY)
.registerMessageBodyWriter(new ClientJacksonMessageBodyWriter(newObjectMapper), Object.class,
HANDLED_MEDIA_TYPES, true, WRITER_PROVIDER_PRIORITY);
InstanceHandle<ClientLogger> clientLogger = arcContainer.instance(ClientLogger.class);
if (clientLogger.isAvailable()) {
clientBuilder.clientLogger(clientLogger.get());
Expand All @@ -111,38 +83,6 @@ private ClientBuilderImpl registerJacksonProviders(ClientBuilderImpl clientBuild
return clientBuilder;
}

// the idea is to only reuse the ObjectMapper if no known customizations would break Keycloak
// TODO: in the future we could also look into checking the ObjectMapper bean itself to see how it has been configured
private boolean canReuseObjectMapper(InstanceHandle<ObjectMapper> objectMapperInstance, ArcContainer arcContainer) {
if (objectMapperInstance.isAvailable() && !objectMapperInstance.getBean().isDefaultBean()) {
// in this case a user provided a completely custom ObjectMapper, so we can't use it
return false;
}

Instance<ObjectMapperCustomizer> customizers = arcContainer.beanManager().createInstance()
.select(ObjectMapperCustomizer.class);
if (!customizers.isUnsatisfied()) {
// ObjectMapperCustomizer can make arbitrary changes, so in order to be safe we won't allow reuse
return false;
}
// if any Jackson properties were configured, disallow reuse - this is done in order to provide forward compatibility with new Jackson configuration options
for (String propertyName : ConfigProvider.getConfig().getPropertyNames()) {
if (propertyName.startsWith("quarkus.jackson")) {
return false;
}
}
return true;
}

// the whole idea here is to reuse the ObjectMapper instance
private ObjectMapper getObjectMapper(ObjectMapper value,
InstanceHandle<ObjectMapper> objectMapperInstance) {
if (value == null) {
return objectMapperInstance.isAvailable() ? objectMapperInstance.get() : newKeycloakAdminClientObjectMapper();
}
return value;
}

// creates new ObjectMapper compatible with Keycloak Admin Client
private ObjectMapper newKeycloakAdminClientObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@

import io.quarkus.arc.BeanDestroyer;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
Expand Down Expand Up @@ -58,11 +56,8 @@ void avoidRuntimeInitIssueInClientBuilderWrapper(ResteasyKeycloakAdminClientReco
@Record(ExecutionTime.RUNTIME_INIT)
@Produce(ServiceStartBuildItem.class)
@BuildStep
public void integrate(ResteasyKeycloakAdminClientRecorder recorder, Capabilities capabilities,
TlsRegistryBuildItem tlsRegistryBuildItem) {
boolean areJSONBProvidersPresent = capabilities.isPresent(Capability.RESTEASY_JSON_JSONB)
|| capabilities.isPresent(Capability.RESTEASY_JSON_JSONB_CLIENT);
recorder.setClientProvider(areJSONBProvidersPresent, tlsRegistryBuildItem.registry());
public void integrate(ResteasyKeycloakAdminClientRecorder recorder, TlsRegistryBuildItem tlsRegistryBuildItem) {
recorder.setClientProvider(tlsRegistryBuildItem.registry());
}

@Record(ExecutionTime.RUNTIME_INIT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.admin.client.spi.ResteasyClientProvider;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientConfig;
import io.quarkus.resteasy.common.runtime.jackson.QuarkusJacksonSerializer;
import io.quarkus.runtime.RuntimeValue;
Expand Down Expand Up @@ -66,7 +70,7 @@ public Keycloak get() {
};
}

public void setClientProvider(boolean areJSONBProvidersPresent, Supplier<TlsConfigurationRegistry> registrySupplier) {
public void setClientProvider(Supplier<TlsConfigurationRegistry> registrySupplier) {
var registry = registrySupplier.get();
var namedTlsConfig = TlsConfiguration.from(registry,
keycloakAdminClientConfigRuntimeValue.getValue().tlsConfigurationName()).orElse(null);
Expand Down Expand Up @@ -100,12 +104,10 @@ public Client newRestEasyClient(Object customJacksonProvider, SSLContext sslCont
}
}

// point here is to use default Quarkus providers rather than org.keycloak.admin.client.JacksonProvider
// as it doesn't work properly in native mode
if (areJSONBProvidersPresent) {
// when both Jackson and JSONB providers are present, we need to ensure Jackson is used
builder.register(new AppJsonQuarkusJacksonSerializer(), 100);
}
// this ensures we don't customize managed (shared) ObjectMapper available in the CDI container
// and that we use QuarkusJacksonSerializer that works in native mode
builder.register(new AppJsonQuarkusJacksonSerializer(), 100);

return builder.build();
}

Expand All @@ -126,6 +128,24 @@ public void avoidRuntimeInitIssueInClientBuilderWrapper() {
// makes media type more specific which ensures that it will be used first
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
static class AppJsonQuarkusJacksonSerializer extends QuarkusJacksonSerializer {
static final class AppJsonQuarkusJacksonSerializer extends QuarkusJacksonSerializer {

private final ObjectMapper objectMapper;

private AppJsonQuarkusJacksonSerializer() {
this.objectMapper = new ObjectMapper();
// Same like JSONSerialization class. Makes it possible to use admin-client against older
// versions of Keycloak server where the properties on representations might be different
this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// The client must work with the newer versions of Keycloak server, which might contain the JSON fields
// not yet known by the client. So unknown fields will be ignored.
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}

@Override
public ObjectMapper locateMapper(Class<?> type, MediaType mediaType) {
return objectMapper;
}

}
}

0 comments on commit 27d6bc2

Please sign in to comment.