Avoid including user managed entities into the default PU

Closes #12442
This commit is contained in:
Pedro Igor 2022-09-23 09:29:05 -03:00 committed by Václav Muzikář
parent c56b69bbc9
commit cff5cfb6df
13 changed files with 380 additions and 10 deletions

View file

@ -222,21 +222,31 @@ class KeycloakProcessor {
CombinedIndexBuildItem indexBuildItem,
BuildProducer<HibernateOrmIntegrationRuntimeConfiguredBuildItem> runtimeConfigured,
KeycloakRecorder recorder) {
ParsedPersistenceXmlDescriptor defaultUnitDescriptor = null;
List<String> userManagedEntities = new ArrayList<>();
for (PersistenceXmlDescriptorBuildItem item : descriptors) {
ParsedPersistenceXmlDescriptor descriptor = item.getDescriptor();
if ("keycloak-default".equals(descriptor.getName())) {
configureJpaProperties(descriptor, config, jdbcDataSources);
configureJpaModel(descriptor, indexBuildItem);
runtimeConfigured.produce(new HibernateOrmIntegrationRuntimeConfiguredBuildItem("keycloak", descriptor.getName())
defaultUnitDescriptor = descriptor;
configureDefaultPersistenceUnitProperties(defaultUnitDescriptor, config, 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(onlyIf = IsJpaStoreEnabled.class)
@ -256,7 +266,7 @@ class KeycloakProcessor {
producer.produce(new PersistenceXmlDescriptorBuildItem(descriptor));
}
private void configureJpaProperties(ParsedPersistenceXmlDescriptor descriptor, HibernateOrmConfig config,
private void configureDefaultPersistenceUnitProperties(ParsedPersistenceXmlDescriptor descriptor, HibernateOrmConfig config,
List<JdbcDataSourceBuildItem> jdbcDataSources) {
Properties unitProperties = descriptor.getProperties();
@ -271,7 +281,8 @@ class KeycloakProcessor {
}
}
private void configureJpaModel(ParsedPersistenceXmlDescriptor descriptor, CombinedIndexBuildItem indexBuildItem) {
private void configureDefaultPersistenceUnitEntities(ParsedPersistenceXmlDescriptor descriptor, CombinedIndexBuildItem indexBuildItem,
List<String> userManagedEntities) {
IndexView index = indexBuildItem.getIndex();
Collection<AnnotationInstance> annotations = index.getAnnotations(DotName.createSimple(Entity.class.getName()));
@ -279,16 +290,13 @@ class KeycloakProcessor {
AnnotationTarget target = annotation.target();
String targetName = target.asClass().name().toString();
if (isCustomJpaModel(targetName)) {
if (!userManagedEntities.contains(targetName)
&& (!targetName.startsWith("org.keycloak") || targetName.startsWith("org.keycloak.testsuite"))) {
descriptor.addClasses(targetName);
}
}
}
private boolean isCustomJpaModel(String targetName) {
return !targetName.startsWith("org.keycloak") || targetName.startsWith("org.keycloak.testsuite");
}
/**
* <p>Load the built-in provider factories during build time so we don't spend time looking up them at runtime. By loading
* providers at this stage we are also able to perform a more dynamic configuration based on the default providers.

View file

@ -35,6 +35,7 @@
<properties>
<kc.quarkus.tests.dist>raw</kc.quarkus.tests.dist>
<approvaltests.version>14.0.0</approvaltests.version>
<build-helper-maven-plugin.version>3.3.0</build-helper-maven-plugin.version>
</properties>
<dependencies>
@ -57,6 +58,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-quarkus-dist</artifactId>
@ -104,6 +109,39 @@
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>${build-helper-maven-plugin.version}</version>
<executions>
<execution>
<id>add-test-provider-sources</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/test-providers/java</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-test-provider-resources</id>
<phase>generate-test-resources</phase>
<goals>
<goal>add-test-resource</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>src/test-providers/resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

View file

@ -0,0 +1,49 @@
/*
* Copyright 2022 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.it;
import java.util.Map;
/**
* A base interface for defining a provider so that its corresponding JAR file can be installed before executing tests.
*/
public interface TestProvider {
/**
* The provider name.
*
* @return the name
*/
default String getName() {
return "provider";
}
/**
* The classes that should be added to the provider JAR file.
*
* @return the classes
*/
Class[] getClasses();
/**
* A {@link Map} where the key is the name of a file at the package where this provider is located and the value is the
* name of the manifest resource that should be created in the provider JAR file.
* @return
*/
Map<String, String> getManifestResources();
}

View file

@ -17,6 +17,7 @@
package org.keycloak.it.junit5.extension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@ -24,6 +25,7 @@ import static org.testcontainers.shaded.org.hamcrest.MatcherAssert.assertThat;
import static org.testcontainers.shaded.org.hamcrest.Matchers.*;
import java.util.List;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
@ -150,4 +152,8 @@ public interface CLIResult extends LaunchResult {
}
}
default void assertStringCount(String msg, int count) {
Pattern pattern = Pattern.compile(msg);
assertEquals(count, pattern.matcher(getOutput()).results().count());
}
}

View file

@ -94,6 +94,8 @@ public class CLITestExtension extends QuarkusMainTestExtension {
dist = createDistribution(distConfig);
}
copyTestProvider(context.getRequiredTestClass().getAnnotation(TestProvider.class));
copyTestProvider(context.getRequiredTestMethod().getAnnotation(TestProvider.class));
onBeforeStartDistribution(context.getRequiredTestClass().getAnnotation(BeforeStartDistribution.class));
onBeforeStartDistribution(context.getRequiredTestMethod().getAnnotation(BeforeStartDistribution.class));
@ -106,6 +108,20 @@ public class CLITestExtension extends QuarkusMainTestExtension {
}
}
private void copyTestProvider(TestProvider provider) {
if (provider == null) {
return;
}
if (dist instanceof RawKeycloakDistribution) {
try {
((RawKeycloakDistribution) dist).copyProvider(provider.value().getDeclaredConstructor().newInstance());
} catch (Exception cause) {
throw new RuntimeException("Failed to instantiate test provider: " + provider.getClass(), cause);
}
}
}
@Override
public void interceptTestMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {

View file

@ -0,0 +1,39 @@
/*
* Copyright 2021 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.it.junit5.extension;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@link TestProvider} is used to install a custom provider into the distribution.
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface TestProvider {
/**
* The {@link TestProvider} to install into the distribution.
*
* @return
*/
Class<? extends org.keycloak.it.TestProvider> value();
}

View file

@ -58,4 +58,8 @@ public interface KeycloakDistribution {
default void setEnvVar(String kc_db_username, String bad) {
throw new RuntimeException("Not implemented");
}
default void copyOrReplaceFile(Path file, Path targetFile) {
throw new RuntimeException("Not implemented");
}
}

View file

@ -53,7 +53,12 @@ import javax.net.ssl.X509TrustManager;
import io.quarkus.deployment.util.FileUtil;
import io.quarkus.fs.util.ZipUtils;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.keycloak.common.Version;
import org.keycloak.it.TestProvider;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
@ -467,6 +472,23 @@ public final class RawKeycloakDistribution implements KeycloakDistribution {
}
}
@Override
public void copyOrReplaceFile(Path file, Path targetFile) {
if (!file.toFile().exists()) {
return;
}
File targetDir = distPath.resolve(targetFile).toFile();
targetDir.mkdirs();
try {
Files.copy(file, targetDir.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException cause) {
throw new RuntimeException("Failed to copy file", cause);
}
}
public void copyProvider(String groupId, String artifactId) {
try {
Files.copy(Maven.resolveArtifact(groupId, artifactId), getDistPath().resolve("providers").resolve(artifactId + ".jar"));
@ -506,4 +528,24 @@ public final class RawKeycloakDistribution implements KeycloakDistribution {
public Path getDistPath() {
return distPath;
}
public void copyProvider(TestProvider provider) {
Path providerPackagePath = Paths.get(provider.getClass().getResource(".").getPath());
JavaArchive providerJar = ShrinkWrap.create(JavaArchive.class, provider.getName() + ".jar")
.addClasses(provider.getClasses())
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
Map<String, String> manifestResources = provider.getManifestResources();
for (Map.Entry<String, String> resource : manifestResources.entrySet()) {
try {
providerJar.addAsManifestResource(providerPackagePath.resolve(resource.getKey()).toFile(), resource.getValue());
} catch (Exception cause) {
throw new RuntimeException("Failed to add manifest resource: " + resource.getKey(), cause);
}
}
copyOrReplaceFile(providerPackagePath.resolve("quarkus.properties"), Path.of("conf", "quarkus.properties"));
providerJar.as(ZipExporter.class).exportTo(getDistPath().resolve("providers").resolve(providerJar.getName()).toFile());
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2022 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 com.acme.provider.user;
import java.util.Collections;
import java.util.Map;
import org.keycloak.it.TestProvider;
public class CustomUserProvider implements TestProvider {
@Override
public Class[] getClasses() {
return new Class[] { Realm.class };
}
@Override
public Map<String, String> getManifestResources() {
return Collections.singletonMap("persistence.xml", "persistence.xml");
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2022 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 com.acme.provider.user;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Realm {
@Id
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2022 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.
-->
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="user-store" transaction-type="JTA">
<class>com.acme.provider.user.Realm</class>
<properties>
<property name="hibernate.dialect" value="io.quarkus.hibernate.orm.runtime.dialect.QuarkusH2Dialect" />
<!-- Sets the name of the datasource to be the same as the datasource name in quarkus.properties-->
<property name="hibernate.connection.datasource" value="user-store" />
<property name="javax.persistence.transactionType" value="JTA" />
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.show_sql" value="false" />
</properties>
</persistence-unit>
</persistence>

View file

@ -0,0 +1,20 @@
#
# Copyright 2022 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.
#
quarkus.datasource.user-store.db-kind=h2
quarkus.datasource.user-store.username=sa
quarkus.datasource.user-store.jdbc.url=jdbc:h2:mem:user-store;DB_CLOSE_DELAY=-1

View file

@ -0,0 +1,43 @@
/*
* Copyright 2021 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.it.cli.dist;
import org.junit.jupiter.api.Test;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.junit5.extension.TestProvider;
import com.acme.provider.user.CustomUserProvider;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
@DistributionTest(reInstall = DistributionTest.ReInstall.BEFORE_TEST)
@RawDistOnly(reason = "Containers are immutable")
public class CustomJpaUserProviderDistTest {
@Test
@TestProvider(CustomUserProvider.class)
@Launch({ "start-dev", "--log-level=org.hibernate.jpa.internal.util.LogHelper:debug" })
void testUserManagedEntityNotAddedToDefaultPU(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStringCount("name: user-store", 1);
cliResult.assertStringCount("com.acme.provider.user.Realm", 1);
cliResult.assertStartedDevMode();
}
}