
org.keycloak.quarkus.deployment.KeycloakProcessor Maven / Gradle / Ivy
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.deployment;
import io.quarkus.agroal.runtime.DataSourcesJdbcBuildTimeConfig;
import io.quarkus.agroal.runtime.TransactionIntegration;
import io.quarkus.agroal.runtime.health.DataSourceHealthCheck;
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
import io.quarkus.agroal.spi.JdbcDriverBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.BuildTimeConditionBuildItem;
import io.quarkus.bootstrap.logging.InitialConfigurator;
import io.quarkus.datasource.deployment.spi.DevServicesDatasourceResultBuildItem;
import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Produce;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
import io.quarkus.deployment.builditem.StaticInitConfigBuilderBuildItem;
import io.quarkus.hibernate.orm.deployment.HibernateOrmConfig;
import io.quarkus.hibernate.orm.deployment.PersistenceXmlDescriptorBuildItem;
import io.quarkus.hibernate.orm.deployment.integration.HibernateOrmIntegrationRuntimeConfiguredBuildItem;
import io.quarkus.hibernate.orm.deployment.spi.AdditionalJpaModelBuildItem;
import io.quarkus.narayana.jta.runtime.TransactionManagerBuildTimeConfig;
import io.quarkus.narayana.jta.runtime.TransactionManagerBuildTimeConfig.UnsafeMultipleLastResourcesMode;
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import org.eclipse.microprofile.health.Readiness;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTransformation;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer;
import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorSpi;
import org.keycloak.authentication.authenticators.browser.DeployedScriptAuthenticatorFactory;
import org.keycloak.authorization.policy.provider.PolicySpi;
import org.keycloak.authorization.policy.provider.js.DeployedScriptPolicyFactory;
import org.keycloak.common.Profile;
import org.keycloak.common.crypto.FipsMode;
import org.keycloak.common.util.MultiSiteUtils;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.config.DatabaseOptions;
import org.keycloak.config.HealthOptions;
import org.keycloak.config.HttpOptions;
import org.keycloak.config.ManagementOptions;
import org.keycloak.config.MetricsOptions;
import org.keycloak.config.SecurityOptions;
import org.keycloak.config.TracingOptions;
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.connections.jpa.JpaConnectionSpi;
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.conn.DefaultLiquibaseConnectionProvider;
import org.keycloak.policy.BlacklistPasswordPolicyProviderFactory;
import org.keycloak.protocol.ProtocolMapperSpi;
import org.keycloak.protocol.oidc.mappers.DeployedScriptOIDCProtocolMapper;
import org.keycloak.protocol.saml.mappers.DeployedScriptSAMLProtocolMapper;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ProviderManager;
import org.keycloak.provider.Spi;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.KeycloakRecorder;
import org.keycloak.quarkus.runtime.cli.Picocli;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.quarkus.runtime.integration.resteasy.KeycloakHandlerChainCustomizer;
import org.keycloak.quarkus.runtime.integration.resteasy.KeycloakTracingCustomizer;
import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck;
import org.keycloak.quarkus.runtime.storage.database.jpa.NamedJpaConnectionProviderFactory;
import org.keycloak.quarkus.runtime.themes.FlatClasspathThemeResourceProviderFactory;
import org.keycloak.representations.provider.ScriptProviderDescriptor;
import org.keycloak.representations.provider.ScriptProviderMetadata;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.services.DefaultKeycloakSessionFactory;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.resources.LoadBalancerResource;
import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.theme.ClasspathThemeProviderFactory;
import org.keycloak.theme.ClasspathThemeResourceProviderFactory;
import org.keycloak.theme.FolderThemeProviderFactory;
import org.keycloak.theme.JarThemeProviderFactory;
import org.keycloak.theme.ThemeResourceSpi;
import org.keycloak.transaction.JBossJtaTransactionManagerLookup;
import org.keycloak.userprofile.config.UPConfigUtils;
import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.StringUtil;
import org.keycloak.vault.FilesKeystoreVaultProviderFactory;
import org.keycloak.vault.FilesPlainTextVaultProviderFactory;
import jakarta.persistence.Entity;
import jakarta.persistence.spi.PersistenceUnitTransactionType;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Handler;
import static org.keycloak.connections.jpa.util.JpaUtils.loadSpecificNamedQueries;
import static org.keycloak.quarkus.runtime.Environment.getCurrentOrCreateFeatureProfile;
import static org.keycloak.quarkus.runtime.Providers.getProviderManager;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
import static org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaConnectionProviderFactory.QUERY_PROPERTY_PREFIX;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.SAML_MAPPERS;
import static org.keycloak.theme.ClasspathThemeProviderFactory.KEYCLOAK_THEMES_JSON;
class KeycloakProcessor {
private static final Logger logger = Logger.getLogger(KeycloakProcessor.class);
private static final String JAR_FILE_SEPARATOR = "!/";
private static final Map> DEPLOYEABLE_SCRIPT_PROVIDERS = new HashMap<>();
private static final String KEYCLOAK_SCRIPTS_JSON_PATH = "META-INF/keycloak-scripts.json";
private static final List> IGNORED_PROVIDER_FACTORY = List.of(
JBossJtaTransactionManagerLookup.class,
DefaultJpaConnectionProviderFactory.class,
DefaultLiquibaseConnectionProvider.class,
FolderThemeProviderFactory.class,
LiquibaseJpaUpdaterProviderFactory.class,
FilesKeystoreVaultProviderFactory.class,
FilesPlainTextVaultProviderFactory.class,
BlacklistPasswordPolicyProviderFactory.class,
ClasspathThemeResourceProviderFactory.class,
JarThemeProviderFactory.class);
static {
DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator);
DEPLOYEABLE_SCRIPT_PROVIDERS.put(POLICIES, KeycloakProcessor::registerScriptPolicy);
DEPLOYEABLE_SCRIPT_PROVIDERS.put(MAPPERS, KeycloakProcessor::registerScriptMapper);
DEPLOYEABLE_SCRIPT_PROVIDERS.put(SAML_MAPPERS, KeycloakProcessor::registerSAMLScriptMapper);
}
private static ProviderFactory registerScriptAuthenticator(ScriptProviderMetadata metadata) {
return new DeployedScriptAuthenticatorFactory(metadata);
}
private static ProviderFactory registerScriptPolicy(ScriptProviderMetadata metadata) {
return new DeployedScriptPolicyFactory(metadata);
}
private static ProviderFactory registerScriptMapper(ScriptProviderMetadata metadata) {
return new DeployedScriptOIDCProtocolMapper(metadata);
}
private static ProviderFactory registerSAMLScriptMapper(ScriptProviderMetadata metadata) {
return new DeployedScriptSAMLProtocolMapper(metadata);
}
@BuildStep
FeatureBuildItem getFeature() {
return new FeatureBuildItem("keycloak");
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
@Produce(ConfigBuildItem.class)
void initConfig(KeycloakRecorder recorder) {
Config.init(new MicroProfileConfigProvider());
recorder.initConfig();
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
@Consume(ConfigBuildItem.class)
@Produce(ProfileBuildItem.class)
void configureProfile(KeycloakRecorder recorder) {
Profile profile = getCurrentOrCreateFeatureProfile();
// record the features so that they are not calculated again at runtime
recorder.configureProfile(profile.getName(), profile.getFeatures());
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
@Consume(ConfigBuildItem.class)
void configureRedirectForRootPath(BuildProducer routes,
HttpRootPathBuildItem httpRootPathBuildItem,
KeycloakRecorder recorder) {
Configuration.getOptionalKcValue(HttpOptions.HTTP_RELATIVE_PATH)
.filter(StringUtil::isNotBlank)
.filter(f -> !f.equals("/"))
.ifPresent(relativePath ->
routes.produce(httpRootPathBuildItem.routeBuilder()
.route("/")
.handler(recorder.getRedirectHandler(relativePath))
.build())
);
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep(onlyIf = IsManagementEnabled.class)
@Consume(ConfigBuildItem.class)
void configureManagementInterface(BuildProducer routes,
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem,
KeycloakRecorder recorder) {
final var relativePath = Configuration.getOptionalKcValue(ManagementOptions.HTTP_MANAGEMENT_RELATIVE_PATH).orElse("/");
if (StringUtil.isNotBlank(relativePath) && !relativePath.equals("/")) {
// redirect from / to the relativePath
routes.produce(nonApplicationRootPathBuildItem.routeBuilder()
.management()
.route("/")
.handler(recorder.getRedirectHandler(relativePath))
.build());
}
routes.produce(nonApplicationRootPathBuildItem.routeBuilder()
.management()
.route(relativePath)
.handler(recorder.getManagementHandler())
.build());
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
@Consume(ConfigBuildItem.class)
void configureTruststore(KeycloakRecorder recorder) {
recorder.configureTruststore();
}
/**
* Check whether JDBC driver is present for the specified DB
*
* @param ignore used for changing build items execution order with regards to AgroalProcessor
*/
@BuildStep
@Produce(CheckJdbcBuildStep.class)
void checkJdbcDriver(BuildProducer ignore) {
final Optional dbDriver = Configuration.getOptionalValue("quarkus.datasource.jdbc.driver");
if (dbDriver.isPresent()) {
try {
// We do not want to initialize the JDBC driver class
Class.forName(dbDriver.get(), false, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
throwConfigError(String.format("Unable to find the JDBC driver (%s). You need to install it.", dbDriver.get()));
}
}
}
// Inspired by AgroalProcessor
@BuildStep
@Produce(CheckMultipleDatasourcesBuildStep.class)
void checkMultipleDatasourcesUseXA(TransactionManagerBuildTimeConfig transactionManagerConfig, DataSourcesBuildTimeConfig dataSourcesConfig, DataSourcesJdbcBuildTimeConfig jdbcConfig) {
if (transactionManagerConfig.unsafeMultipleLastResources
.orElse(UnsafeMultipleLastResourcesMode.DEFAULT) != UnsafeMultipleLastResourcesMode.FAIL) {
return;
}
long nonXADatasourcesCount = dataSourcesConfig.dataSources().keySet().stream()
.map(ds -> jdbcConfig.dataSources().get(ds).jdbc())
.filter(jdbc -> jdbc.enabled() && jdbc.transactions() != TransactionIntegration.XA)
.count();
if (nonXADatasourcesCount > 1) {
throwConfigError("Multiple datasources are configured but more than 1 is using non-XA transactions. " +
"All the datasources except one must must be XA to be able to use Last Resource Commit Optimization (LRCO). " +
"Please update your configuration by setting --transaction-xa-enabled=true " +
"and/or quarkus.datasource..jdbc.transactions=xa.");
}
}
private void throwConfigError(String msg) {
// Ignore queued TRACE and DEBUG messages for not initialized log handlers
InitialConfigurator.DELAYED_HANDLER.setBuildTimeHandlers(new Handler[]{});
throw new ConfigurationException(msg);
}
/**
* Parse the default configuration for the User Profile provider
*/
@BuildStep
@Produce(UserProfileBuildItem.class)
UserProfileBuildItem parseDefaultUserProfileConfig() {
UPConfig defaultConfig = UPConfigUtils.parseSystemDefaultConfig();
logger.debug("Parsing default configuration for the User Profile provider");
return new UserProfileBuildItem(defaultConfig);
}
/**
* Set the default configuration to the User Profile provider
*/
@BuildStep
@Consume(ProfileBuildItem.class)
@Record(ExecutionTime.STATIC_INIT)
void setDefaultUserProfileConfig(KeycloakRecorder recorder, UserProfileBuildItem configuration) {
recorder.setDefaultUserProfileConfiguration(configuration.getDefaultConfig());
}
/**
* Configures the persistence unit for Quarkus.
*
*
The {@code hibernate-orm} extension expects that the dialect is statically
* set to the persistence unit if there is any from the classpath and we use this method to obtain the dialect from the configuration
* file so that we can build the application with whatever dialect we want. In addition to the dialect, we should also be
* allowed to set any additional defaults that we think that makes sense.
*
* @param config
* @param descriptors
*/
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void configurePersistenceUnits(HibernateOrmConfig config,
List descriptors,
List jdbcDataSources,
BuildProducer additionalJpaModel,
CombinedIndexBuildItem indexBuildItem,
BuildProducer runtimeConfigured,
KeycloakRecorder recorder) {
ParsedPersistenceXmlDescriptor defaultUnitDescriptor = null;
List userManagedEntities = new ArrayList<>();
for (PersistenceXmlDescriptorBuildItem item : descriptors) {
ParsedPersistenceXmlDescriptor descriptor = (ParsedPersistenceXmlDescriptor) item.getDescriptor();
if ("keycloak-default".equals(descriptor.getName())) {
defaultUnitDescriptor = descriptor;
configureDefaultPersistenceUnitProperties(defaultUnitDescriptor, config, getDefaultDataSource(jdbcDataSources));
runtimeConfigured.produce(new HibernateOrmIntegrationRuntimeConfiguredBuildItem("keycloak", defaultUnitDescriptor.getName())
.setInitListener(recorder.createDefaultUnitListener()));
} else {
Properties properties = descriptor.getProperties();
// register a listener for customizing the unit configuration at runtime
runtimeConfigured.produce(new HibernateOrmIntegrationRuntimeConfiguredBuildItem("keycloak", descriptor.getName())
.setInitListener(recorder.createUserDefinedUnitListener(properties.getProperty(AvailableSettings.DATASOURCE))));
userManagedEntities.addAll(descriptor.getManagedClassNames());
}
}
if (defaultUnitDescriptor == null) {
throw new RuntimeException("No default persistence unit found.");
}
configureDefaultPersistenceUnitEntities(defaultUnitDescriptor, indexBuildItem, userManagedEntities);
}
@BuildStep
@Consume(CheckJdbcBuildStep.class)
@Consume(CheckMultipleDatasourcesBuildStep.class)
void produceDefaultPersistenceUnit(BuildProducer producer) {
ParsedPersistenceXmlDescriptor descriptor = PersistenceXmlParser.locateIndividualPersistenceUnit(
Thread.currentThread().getContextClassLoader().getResource("default-persistence.xml"));
producer.produce(new PersistenceXmlDescriptorBuildItem(descriptor));
}
private void configureDefaultPersistenceUnitProperties(ParsedPersistenceXmlDescriptor descriptor, HibernateOrmConfig config,
JdbcDataSourceBuildItem defaultDataSource) {
if (defaultDataSource == null || !defaultDataSource.isDefault()) {
throw new RuntimeException("The server datasource must be the default datasource.");
}
Properties unitProperties = descriptor.getProperties();
final Optional dialect = getOptionalKcValue(DatabaseOptions.DB_DIALECT.getKey());
dialect.ifPresent(d -> unitProperties.setProperty(AvailableSettings.DIALECT, d));
final Optional defaultSchema = getOptionalKcValue(DatabaseOptions.DB_SCHEMA.getKey());
defaultSchema.ifPresent(ds -> unitProperties.setProperty(AvailableSettings.DEFAULT_SCHEMA, ds));
unitProperties.setProperty(AvailableSettings.JAKARTA_TRANSACTION_TYPE, PersistenceUnitTransactionType.JTA.name());
descriptor.setTransactionType(PersistenceUnitTransactionType.JTA);
unitProperties.setProperty(AvailableSettings.QUERY_STARTUP_CHECKING, Boolean.FALSE.toString());
String dbKind = defaultDataSource.getDbKind();
for (Entry