diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakAdminPageBuildItem.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakAdminPageBuildItem.java new file mode 100644 index 0000000000000..cd3312c53f03c --- /dev/null +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakAdminPageBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.devservices.keycloak; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; + +/** + * + */ +public final class KeycloakAdminPageBuildItem extends MultiBuildItem { + + final CardPageBuildItem cardPage; + + /** + * @param cardPage created inside extension that requires Keycloak Dev Service, this way, card page + * custom identifier deduced from a stacktrace walker will identify the extension correctly + */ + public KeycloakAdminPageBuildItem(CardPageBuildItem cardPage) { + this.cardPage = cardPage; + } +} diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java index f8c0d8f1186a5..683b8073e752a 100644 --- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java @@ -1,6 +1,9 @@ package io.quarkus.devservices.keycloak; +import static io.quarkus.devservices.keycloak.KeycloakDevServicesProcessor.KEYCLOAK_URL_KEY; + import java.util.Map; +import java.util.Optional; import io.quarkus.builder.item.SimpleBuildItem; @@ -28,4 +31,11 @@ public Map getConfig() { public boolean isContainerRestarted() { return containerRestarted; } + + public static String getKeycloakUrl(Optional configBuildItem) { + return configBuildItem + .map(KeycloakDevServicesConfigBuildItem::getConfig) + .map(config -> config.get(KEYCLOAK_URL_KEY)) + .orElse(null); + } } diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigurator.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigurator.java new file mode 100644 index 0000000000000..a06f5df1a14b9 --- /dev/null +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigurator.java @@ -0,0 +1,17 @@ +package io.quarkus.devservices.keycloak; + +import java.util.Map; + +import org.keycloak.representations.idm.RealmRepresentation; + +public interface KeycloakDevServicesConfigurator { + + record ConfigPropertiesContext(String authServerInternalUrl, String oidcClientId, String oidcClientSecret) { + } + + Map createProperties(ConfigPropertiesContext context); + + default void customizeDefaultRealm(RealmRepresentation realmRepresentation) { + } + +} diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java index 9ad6cf05fb51e..2e9a036a6dde6 100644 --- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -1,7 +1,7 @@ package io.quarkus.devservices.keycloak; -import static io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem.setOidcClientConfigProperties; -import static io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem.setOidcConfigProperties; +import static io.quarkus.devservices.keycloak.KeycloakDevServicesConfigBuildItem.getKeycloakUrl; +import static io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem.getDevServicesConfigurator; import static io.quarkus.devservices.keycloak.KeycloakDevServicesUtils.createWebClient; import static io.quarkus.devservices.keycloak.KeycloakDevServicesUtils.getPasswordAccessToken; @@ -47,6 +47,7 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; +import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -55,7 +56,6 @@ import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; -import io.quarkus.deployment.builditem.DockerStatusBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; import io.quarkus.deployment.console.StartupLogCompressor; @@ -64,8 +64,10 @@ import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerAddress; import io.quarkus.devservices.common.ContainerLocator; +import io.quarkus.devservices.keycloak.KeycloakDevServicesConfigurator.ConfigPropertiesContext; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; import io.quarkus.runtime.LaunchMode; -import io.quarkus.runtime.configuration.ConfigUtils; import io.quarkus.runtime.configuration.MemorySize; import io.smallrye.mutiny.TimeoutException; import io.smallrye.mutiny.Uni; @@ -77,26 +79,16 @@ @BuildSteps(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) public class KeycloakDevServicesProcessor { - static volatile Vertx vertxInstance; private static final Logger LOG = Logger.getLogger(KeycloakDevServicesProcessor.class); private static final String CONFIG_PREFIX = "quarkus.oidc."; - private static final String TENANT_ENABLED_CONFIG_KEY = CONFIG_PREFIX + "tenant-enabled"; - private static final String AUTH_SERVER_URL_CONFIG_KEY = CONFIG_PREFIX + "auth-server-url"; - private static final String PROVIDER_CONFIG_KEY = CONFIG_PREFIX + "provider"; + // avoid the Quarkus prefix in order to prevent warnings when the application starts in container integration tests private static final String CLIENT_AUTH_SERVER_URL_CONFIG_KEY = "client." + CONFIG_PREFIX + "auth-server-url"; - private static final String APPLICATION_TYPE_CONFIG_KEY = CONFIG_PREFIX + "application-type"; private static final String CLIENT_ID_CONFIG_KEY = CONFIG_PREFIX + "client-id"; private static final String CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret"; - private static final String KEYCLOAK_URL_KEY = "keycloak.url"; - - // OIDC Client config properties - static final String OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY = "quarkus.oidc-client.auth-server-url"; - static final String OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY = "quarkus.oidc-client.token-path"; - private static final String OIDC_CLIENT_SECRET_CONFIG_KEY = "quarkus.oidc-client.credentials.secret"; - private static final String OIDC_CLIENT_ID_CONFIG_KEY = "quarkus.oidc-client.client-id"; + static final String KEYCLOAK_URL_KEY = "keycloak.url"; private static final String KEYCLOAK_CONTAINER_NAME = "keycloak"; private static final int KEYCLOAK_PORT = 8080; @@ -130,20 +122,18 @@ public class KeycloakDevServicesProcessor { * This allows other applications to discover the running service and use it instead of starting a new instance. */ private static final String DEV_SERVICE_LABEL = "quarkus-dev-service-keycloak"; - private static final ContainerLocator keycloakDevModeContainerLocator = new ContainerLocator(DEV_SERVICE_LABEL, + private static final ContainerLocator KEYCLOAK_DEV_MODE_CONTAINER_LOCATOR = new ContainerLocator(DEV_SERVICE_LABEL, KEYCLOAK_PORT); private static volatile RunningDevService devService; - static volatile KeycloakDevServicesConfig capturedDevServicesConfiguration; + private static volatile KeycloakDevServicesConfig capturedDevServicesConfiguration; private static volatile boolean first = true; private static volatile Set capturedRealmFileLastModifiedDate; - private static volatile boolean setOidcConfigProperties = true; - private static volatile boolean setOidcClientConfigProperties = true; + private static volatile Vertx vertxInstance; @BuildStep - public DevServicesResultBuildItem startKeycloakContainer( + DevServicesResultBuildItem startKeycloakContainer( List devSvcRequiredMarkerItems, - DockerStatusBuildItem dockerStatusBuildItem, BuildProducer keycloakBuildItemBuildProducer, List devServicesSharedNetworkBuildItem, KeycloakDevServicesConfig config, @@ -154,20 +144,20 @@ public DevServicesResultBuildItem startKeycloakContainer( GlobalDevServicesConfig devServicesConfig) { if (devSvcRequiredMarkerItems.isEmpty()) { + if (devService != null) { + closeDevService(); + } return null; } + var devServicesConfigurator = getDevServicesConfigurator(devSvcRequiredMarkerItems); - setOidcConfigProperties = setOidcConfigProperties(devSvcRequiredMarkerItems); - setOidcClientConfigProperties = setOidcClientConfigProperties(devSvcRequiredMarkerItems); - - KeycloakDevServicesConfig currentDevServicesConfiguration = config; // Figure out if we need to shut down and restart any existing Keycloak container // if not and the Keycloak container has already started we just return if (devService != null) { - boolean restartRequired = !currentDevServicesConfiguration.equals(capturedDevServicesConfiguration); + boolean restartRequired = !config.equals(capturedDevServicesConfiguration); if (!restartRequired) { Set currentRealmFileLastModifiedDate = getRealmFileLastModifiedDate( - currentDevServicesConfiguration.realmPath()); + config.realmPath()); if (currentRealmFileLastModifiedDate != null && !currentRealmFileLastModifiedDate.equals(capturedRealmFileLastModifiedDate)) { restartRequired = true; @@ -182,21 +172,15 @@ public DevServicesResultBuildItem startKeycloakContainer( .map(s -> s.split("=")).collect(Collectors.toMap(s -> s[0], s -> s[1])); String realmsString = result.getConfig().get(KEYCLOAK_REALMS); List realms = (realmsString == null || realmsString.isBlank()) ? List.of() - : Arrays.stream(realmsString.split(",")).collect(Collectors.toList()); + : Arrays.stream(realmsString.split(",")).toList(); keycloakBuildItemBuildProducer .produce(new KeycloakDevServicesConfigBuildItem(result.getConfig(), Map.of(OIDC_USERS, users, KEYCLOAK_REALMS, realms), false)); return result; } - try { - devService.close(); - } catch (Throwable e) { - LOG.error("Failed to stop Keycloak container", e); - } - devService = null; - capturedDevServicesConfiguration = null; + closeDevService(); } - capturedDevServicesConfiguration = currentDevServicesConfiguration; + capturedDevServicesConfiguration = config; StartupLogCompressor compressor = new StartupLogCompressor( (launchMode.isTest() ? "(test) " : "") + "Keycloak Dev Services Starting:", consoleInstalledBuildItem, loggingSetupBuildItem); @@ -205,10 +189,8 @@ public DevServicesResultBuildItem startKeycloakContainer( boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(devServicesConfig, devServicesSharedNetworkBuildItem); - RunningDevService newDevService = startContainer(dockerStatusBuildItem, keycloakBuildItemBuildProducer, - useSharedNetwork, - devServicesConfig.timeout, - errors); + RunningDevService newDevService = startContainer(keycloakBuildItemBuildProducer, useSharedNetwork, + devServicesConfig.timeout, errors, devServicesConfigurator); if (newDevService == null) { if (errors.isEmpty()) { compressor.close(); @@ -264,13 +246,41 @@ public void run() { return devService.toBuildItem(); } - private String startURL(String scheme, String host, Integer port, boolean isKeycloakX) { + @BuildStep(onlyIf = IsDevelopment.class) + void produceDevUiCardWithKeycloakUrl(Optional configProps, + List keycloakAdminPageBuildItems, + BuildProducer cardPageProducer) { + final String keycloakAdminUrl = getKeycloakUrl(configProps); + if (keycloakAdminUrl != null) { + keycloakAdminPageBuildItems.forEach(i -> { + i.cardPage.addPage(Page + .externalPageBuilder("Keycloak Admin") + .icon("font-awesome-solid:key") + .doNotEmbed(true) + .url(keycloakAdminUrl)); + cardPageProducer.produce(i.cardPage); + }); + } + } + + private static void closeDevService() { + try { + devService.close(); + } catch (Throwable e) { + LOG.error("Failed to stop Keycloak container", e); + } + devService = null; + capturedDevServicesConfiguration = null; + } + + private static String startURL(String scheme, String host, Integer port, boolean isKeycloakX) { return scheme + host + ":" + port + (isKeycloakX ? "" : "/auth"); } - private Map prepareConfiguration( + private static Map prepareConfiguration( BuildProducer keycloakBuildItemBuildProducer, String internalURL, - String hostURL, List realmReps, List errors) { + String hostURL, List realmReps, List errors, + KeycloakDevServicesConfigurator devServicesConfigurator) { final String realmName = realmReps != null && !realmReps.isEmpty() ? realmReps.iterator().next().getRealm() : getDefaultRealmName(); final String authServerInternalUrl = realmsURL(internalURL, realmName); @@ -283,7 +293,6 @@ private Map prepareConfiguration( String oidcClientId = getOidcClientId(); String oidcClientSecret = getOidcClientSecret(); - String oidcApplicationType = getOidcApplicationType(); Map users = getUsers(capturedDevServicesConfiguration.users(), createDefaultRealm); @@ -299,7 +308,8 @@ private Map prepareConfiguration( try { String adminToken = getAdminToken(client, clientAuthServerBaseUrl); if (createDefaultRealm) { - createDefaultRealm(client, adminToken, clientAuthServerBaseUrl, users, oidcClientId, oidcClientSecret, errors); + createDefaultRealm(client, adminToken, clientAuthServerBaseUrl, users, oidcClientId, oidcClientSecret, errors, + devServicesConfigurator); realmNames.add(realmName); } else { if (realmReps != null) { @@ -314,28 +324,11 @@ private Map prepareConfiguration( } Map configProperties = new HashMap<>(); + var configPropertiesContext = new ConfigPropertiesContext(authServerInternalUrl, oidcClientId, oidcClientSecret); + configProperties.putAll(devServicesConfigurator.createProperties(configPropertiesContext)); configProperties.put(KEYCLOAK_URL_KEY, internalURL); configProperties.put(CLIENT_AUTH_SERVER_URL_CONFIG_KEY, clientAuthServerUrl); - if (setOidcClientConfigProperties) { - configProperties.put(OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY, authServerInternalUrl); - configProperties.put(OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY, "/protocol/openid-connect/tokens"); - } - if (setOidcConfigProperties) { - configProperties.put(AUTH_SERVER_URL_CONFIG_KEY, authServerInternalUrl); - configProperties.put(APPLICATION_TYPE_CONFIG_KEY, oidcApplicationType); - } - if (capturedDevServicesConfiguration.createClient()) { - if (setOidcConfigProperties) { - configProperties.put(CLIENT_ID_CONFIG_KEY, oidcClientId); - configProperties.put(CLIENT_SECRET_CONFIG_KEY, oidcClientSecret); - } - if (setOidcClientConfigProperties) { - configProperties.put(OIDC_CLIENT_ID_CONFIG_KEY, oidcClientId); - configProperties.put(OIDC_CLIENT_SECRET_CONFIG_KEY, oidcClientSecret); - } - } - configProperties.put(OIDC_USERS, users.entrySet().stream() - .map(e -> e.toString()).collect(Collectors.joining(","))); + configProperties.put(OIDC_USERS, users.entrySet().stream().map(Object::toString).collect(Collectors.joining(","))); configProperties.put(KEYCLOAK_REALMS, realmNames.stream().collect(Collectors.joining(","))); keycloakBuildItemBuildProducer @@ -345,57 +338,25 @@ private Map prepareConfiguration( return configProperties; } - private String realmsURL(String baseURL, String realmName) { + private static String realmsURL(String baseURL, String realmName) { return baseURL + "/realms/" + realmName; } - private String getDefaultRealmName() { + private static String getDefaultRealmName() { return capturedDevServicesConfiguration.realmName().orElse("quarkus"); } - private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuildItem, + private static RunningDevService startContainer( BuildProducer keycloakBuildItemBuildProducer, boolean useSharedNetwork, Optional timeout, - List errors) { + List errors, KeycloakDevServicesConfigurator devServicesConfigurator) { if (!capturedDevServicesConfiguration.enabled()) { // explicitly disabled LOG.debug("Not starting Dev Services for Keycloak as it has been disabled in the config"); return null; } - if (!isOidcTenantEnabled() && !capturedDevServicesConfiguration.startWithDisabledTenant()) { - LOG.debug("Not starting Dev Services for Keycloak as 'quarkus.oidc.tenant.enabled' is false"); - return null; - } - if (ConfigUtils.isPropertyNonEmpty(AUTH_SERVER_URL_CONFIG_KEY)) { - LOG.debug("Not starting Dev Services for Keycloak as 'quarkus.oidc.auth-server-url' has been provided"); - return null; - } - if (ConfigUtils.isPropertyNonEmpty(PROVIDER_CONFIG_KEY)) { - LOG.debug("Not starting Dev Services for Keycloak as 'quarkus.oidc.provider' has been provided"); - return null; - } - - if (!dockerStatusBuildItem.isContainerRuntimeAvailable()) { - LOG.warn("Please configure 'quarkus.oidc.auth-server-url' or get a working docker instance"); - return null; - } - - // TODO: this will need to be reworked when we integrate with other extensions like Keycloak Admin Client - if (!setOidcConfigProperties && !setOidcClientConfigProperties) { - // this can happen if OIDC is not present or disabled and user set either of following properties: - if (ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY)) { - LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided", - OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY); - return null; - } - if (ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY)) { - LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided", - OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY); - return null; - } - } - final Optional maybeContainerAddress = keycloakDevModeContainerLocator.locateContainer( + final Optional maybeContainerAddress = KEYCLOAK_DEV_MODE_CONTAINER_LOCATOR.locateContainer( capturedDevServicesConfiguration.serviceName(), capturedDevServicesConfiguration.shared(), LaunchMode.current()); @@ -432,7 +393,7 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild : null; Map configs = prepareConfiguration(keycloakBuildItemBuildProducer, internalUrl, hostUrl, - oidcContainer.realmReps, errors); + oidcContainer.realmReps, errors, devServicesConfigurator); return new RunningDevService(KEYCLOAK_CONTAINER_NAME, oidcContainer.getContainerId(), oidcContainer::close, configs); }; @@ -442,13 +403,13 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild // TODO: this probably needs to be addressed Map configs = prepareConfiguration(keycloakBuildItemBuildProducer, getSharedContainerUrl(containerAddress), - getSharedContainerUrl(containerAddress), null, errors); + getSharedContainerUrl(containerAddress), null, errors, devServicesConfigurator); return new RunningDevService(KEYCLOAK_CONTAINER_NAME, containerAddress.getId(), null, configs); }) .orElseGet(defaultKeycloakContainerSupplier); } - private Map resourcesMap(List errors) { + private static Map resourcesMap(List errors) { Map resources = new HashMap<>(); for (Map.Entry aliasEntry : capturedDevServicesConfiguration.resourceAliases().entrySet()) { if (capturedDevServicesConfiguration.resourceMappings().containsKey(aliasEntry.getKey())) { @@ -470,7 +431,7 @@ private static boolean isKeycloakX(DockerImageName dockerImageName) { : !dockerImageName.getVersionPart().endsWith(KEYCLOAK_LEGACY_IMAGE_VERSION_PART); } - private String getSharedContainerUrl(ContainerAddress containerAddress) { + private static String getSharedContainerUrl(ContainerAddress containerAddress) { return "http://" + ("0.0.0.0".equals(containerAddress.getHost()) ? "localhost" : containerAddress.getHost()) + ":" + containerAddress.getPort(); } @@ -485,7 +446,7 @@ private static class QuarkusOidcContainer extends GenericContainer realmReps = new LinkedList<>(); + private final List realmReps = new LinkedList<>(); private final Optional startCommand; private final boolean showLogs; private final MemorySize containerMemoryLimit; @@ -627,7 +588,7 @@ private void addUpConfigResource() { } } - private Integer findRandomPort() { + private static Integer findRandomPort() { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); } catch (IOException e) { @@ -635,7 +596,7 @@ private Integer findRandomPort() { } } - private Optional readRealmFile(URI uri, String realmPath, List errors) { + private static Optional readRealmFile(URI uri, String realmPath, List errors) { try { return readRealmFile(uri.toURL(), realmPath, errors); } catch (MalformedURLException ex) { @@ -644,7 +605,7 @@ private Optional readRealmFile(URI uri, String realmPath, L } } - private Optional readRealmFile(URL url, String realmPath, List errors) { + private static Optional readRealmFile(URL url, String realmPath, List errors) { try { try (InputStream is = url.openStream()) { return Optional.of(JsonSerialization.readValue(is, RealmRepresentation.class)); @@ -698,7 +659,7 @@ public boolean isHttps() { } } - private Set getRealmFileLastModifiedDate(Optional> realms) { + private static Set getRealmFileLastModifiedDate(Optional> realms) { if (realms.isPresent()) { Set times = new HashSet<>(); @@ -716,10 +677,9 @@ private Set getRealmFileLastModifiedDate(Optional> realms return null; } - private void createDefaultRealm(WebClient client, String token, String keycloakUrl, Map users, - String oidcClientId, - String oidcClientSecret, - List errors) { + private static void createDefaultRealm(WebClient client, String token, String keycloakUrl, Map users, + String oidcClientId, String oidcClientSecret, List errors, + KeycloakDevServicesConfigurator devServicesConfigurator) { RealmRepresentation realm = createDefaultRealmRep(); if (capturedDevServicesConfiguration.createClient()) { @@ -729,10 +689,12 @@ private void createDefaultRealm(WebClient client, String token, String keycloakU realm.getUsers().add(createUser(entry.getKey(), entry.getValue(), getUserRoles(entry.getKey()))); } + devServicesConfigurator.customizeDefaultRealm(realm); + createRealm(client, token, keycloakUrl, realm, errors); } - private String getAdminToken(WebClient client, String keycloakUrl) { + private static String getAdminToken(WebClient client, String keycloakUrl) { try { LOG.tracef("Acquiring admin token"); @@ -749,7 +711,7 @@ private String getAdminToken(WebClient client, String keycloakUrl) { return null; } - private void createRealm(WebClient client, String token, String keycloakUrl, RealmRepresentation realm, + private static void createRealm(WebClient client, String token, String keycloakUrl, RealmRepresentation realm, List errors) { try { LOG.tracef("Creating the realm %s", realm.getRealm()); @@ -795,7 +757,7 @@ private void createRealm(WebClient client, String token, String keycloakUrl, Rea } @SuppressWarnings("serial") - static class RealmEndpointAccessException extends RuntimeException { + private static class RealmEndpointAccessException extends RuntimeException { private final int errorStatus; public RealmEndpointAccessException(int errorStatus) { @@ -807,12 +769,12 @@ public int getErrorStatus() { } } - public static Predicate realmEndpointNotAvailable() { + private static Predicate realmEndpointNotAvailable() { return t -> (t instanceof ConnectException || (t instanceof RealmEndpointAccessException && ((RealmEndpointAccessException) t).getErrorStatus() == 404)); } - private Map getUsers(Map configuredUsers, boolean createRealm) { + private static Map getUsers(Map configuredUsers, boolean createRealm) { if (configuredUsers.isEmpty() && createRealm) { Map users = new LinkedHashMap(); users.put("alice", "alice"); @@ -823,13 +785,13 @@ private Map getUsers(Map configuredUsers, boolea } } - private List getUserRoles(String user) { + private static List getUserRoles(String user) { List roles = capturedDevServicesConfiguration.roles().get(user); return roles == null ? ("alice".equals(user) ? List.of("admin", "user") : List.of("user")) : roles; } - private RealmRepresentation createDefaultRealmRep() { + private static RealmRepresentation createDefaultRealmRep() { RealmRepresentation realm = new RealmRepresentation(); realm.setRealm(getDefaultRealmName()); @@ -863,7 +825,7 @@ private RealmRepresentation createDefaultRealmRep() { return realm; } - private ClientRepresentation createClient(String clientId, String oidcClientSecret) { + private static ClientRepresentation createClient(String clientId, String oidcClientSecret) { ClientRepresentation client = new ClientRepresentation(); client.setClientId(clientId); @@ -880,7 +842,7 @@ private ClientRepresentation createClient(String clientId, String oidcClientSecr return client; } - private UserRepresentation createUser(String username, String password, List realmRoles) { + private static UserRepresentation createUser(String username, String password, List realmRoles) { UserRepresentation user = new UserRepresentation(); user.setUsername(username); @@ -899,14 +861,6 @@ private UserRepresentation createUser(String username, String password, List items) { - return items.stream().anyMatch(i -> i.capability == Capability.OIDC); - } + private final KeycloakDevServicesConfigurator devServicesConfigurator; - static boolean setOidcClientConfigProperties(List items) { - boolean serverUrlOrTokenPathConfigured = ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY) - || ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY); - return !serverUrlOrTokenPathConfigured - && items.stream().anyMatch(i -> i.capability == Capability.OIDC_CLIENT); + public KeycloakDevServicesRequiredBuildItem(KeycloakDevServicesConfigurator devServicesConfigurator) { + this.devServicesConfigurator = requireNonNull(devServicesConfigurator); } - public static KeycloakDevServicesRequiredBuildItem requireDevServiceForOidc() { - return new KeycloakDevServicesRequiredBuildItem(Capability.OIDC); + static KeycloakDevServicesConfigurator getDevServicesConfigurator(List items) { + return new KeycloakDevServicesConfigurator() { + @Override + public Map createProperties(ConfigPropertiesContext context) { + return items + .stream() + .map(i -> i.devServicesConfigurator) + .map(producer -> producer.createProperties(context)) + .map(Map::entrySet) + .flatMap(Collection::stream) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public void customizeDefaultRealm(RealmRepresentation realmRepresentation) { + items + .stream() + .map(i -> i.devServicesConfigurator) + .forEach(i -> i.customizeDefaultRealm(realmRepresentation)); + } + }; } - public static KeycloakDevServicesRequiredBuildItem requireDevServiceForOidcClient() { - return new KeycloakDevServicesRequiredBuildItem(Capability.OIDC_CLIENT); - } } diff --git a/extensions/oidc-client-registration/deployment/pom.xml b/extensions/oidc-client-registration/deployment/pom.xml index e4720ae98627a..b4b162dd77f22 100644 --- a/extensions/oidc-client-registration/deployment/pom.xml +++ b/extensions/oidc-client-registration/deployment/pom.xml @@ -31,6 +31,10 @@ io.quarkus quarkus-oidc-common-deployment + + io.quarkus + quarkus-devservices-keycloak + io.quarkus @@ -83,10 +87,6 @@ maven-surefire-plugin false - - ${keycloak.docker.legacy.image} - false - diff --git a/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/devservices/keycloak/KeycloakDevServiceRequiredBuildStep.java b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/devservices/keycloak/KeycloakDevServiceRequiredBuildStep.java new file mode 100644 index 0000000000000..b921a0b34b2fb --- /dev/null +++ b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/devservices/keycloak/KeycloakDevServiceRequiredBuildStep.java @@ -0,0 +1,93 @@ +package io.quarkus.oidc.client.registration.deployment.devservices.keycloak; + +import java.util.List; +import java.util.Map; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.representations.idm.ComponentExportRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; + +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.devservices.keycloak.KeycloakAdminPageBuildItem; +import io.quarkus.devservices.keycloak.KeycloakDevServicesConfigurator; +import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.oidc.client.registration.deployment.OidcClientRegistrationBuildStep; +import io.quarkus.runtime.configuration.ConfigUtils; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { OidcClientRegistrationBuildStep.IsEnabled.class, + GlobalDevServicesConfig.Enabled.class }) +public class KeycloakDevServiceRequiredBuildStep { + + private static final Logger LOG = Logger.getLogger(KeycloakDevServiceRequiredBuildStep.class); + private static final String OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY = "quarkus.oidc-client-registration.auth-server-url"; + private static final String OIDC_AUTH_SERVER_URL_CONFIG_KEY = "quarkus.oidc.auth-server-url"; + + @BuildStep + void requireKeycloakDevService(BuildProducer keycloakDevSvcRequiredProducer, + DockerStatusBuildItem dockerStatusBuildItem) { + if (ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY)) { + LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided", + OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY); + return; + } + // this is for backwards compatibility - when users configured named client with OIDC auth server URL + // that is not our Keycloak Dev Service, we don't want to start our Dev Service to keep previous behavior + if (ConfigUtils.isPropertyNonEmpty(OIDC_AUTH_SERVER_URL_CONFIG_KEY)) { + LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided", + OIDC_AUTH_SERVER_URL_CONFIG_KEY); + return; + } + if (!dockerStatusBuildItem.isContainerRuntimeAvailable()) { + LOG.warnf("Please configure '%s' or get a working docker instance", OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY); + return; + } + + var devServicesConfigurator = new KeycloakDevServicesConfigurator() { + + @Override + public Map createProperties(ConfigPropertiesContext ctx) { + return Map.of(OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY, ctx.authServerInternalUrl()); + } + + @Override + public void customizeDefaultRealm(RealmRepresentation realmRepresentation) { + if (getInitialToken() == null) { + realmRepresentation.setRegistrationAllowed(true); + realmRepresentation.setRegistrationFlow("registration"); + if (realmRepresentation.getComponents() == null) { + realmRepresentation.setComponents(new MultivaluedHashMap<>()); + } + var componentExportRepresentation = new ComponentExportRepresentation(); + componentExportRepresentation.setName("Full Scope Disabled"); + componentExportRepresentation.setProviderId("scope"); + componentExportRepresentation.setSubType("anonymous"); + realmRepresentation.getComponents().put( + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy", + List.of(componentExportRepresentation)); + } + } + }; + + keycloakDevSvcRequiredProducer + .produce(new KeycloakDevServicesRequiredBuildItem(devServicesConfigurator)); + } + + @BuildStep(onlyIf = IsDevelopment.class) + KeycloakAdminPageBuildItem addCardWithLinkToKeycloakAdmin() { + return new KeycloakAdminPageBuildItem(new CardPageBuildItem()); + } + + private static String getInitialToken() { + return ConfigProvider.getConfig().getOptionalValue("quarkus.oidc-client-registration.initial-token", String.class) + .orElse(null); + } +} diff --git a/extensions/oidc-client-registration/deployment/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationKeycloakDevServiceTest.java b/extensions/oidc-client-registration/deployment/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationKeycloakDevServiceTest.java new file mode 100644 index 0000000000000..78fab599224bd --- /dev/null +++ b/extensions/oidc-client-registration/deployment/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationKeycloakDevServiceTest.java @@ -0,0 +1,65 @@ +package io.quarkus.oidc.client.registration; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.StartupEvent; +import io.quarkus.test.QuarkusUnitTest; + +public class OidcClientRegistrationKeycloakDevServiceTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource( + new StringAsset( + """ + quarkus.oidc-client-registration.metadata.client-name=Default Test Client + quarkus.oidc-client-registration.metadata.redirect-uri=http://localhost:8081/default/redirect + quarkus.oidc-client-registration.named.metadata.client-name=Named Test Client + quarkus.oidc-client-registration.named.metadata.redirect-uri=http://localhost:8081/named/redirect + quarkus.oidc-client-registration.named.auth-server-url=${quarkus.oidc-client-registration.auth-server-url} + """), + "application.properties")); + + @Inject + TestClientRegistrations testClientRegistrations; + + @Test + public void testDefaultRegisteredClient() { + assertEquals("Default Test Client", testClientRegistrations.defaultClientMetadata.getClientName()); + assertEquals("http://localhost:8081/default/redirect", + testClientRegistrations.defaultClientMetadata.getRedirectUris().get(0)); + } + + @Test + public void testNamedRegisteredClient() { + assertEquals("Named Test Client", testClientRegistrations.namedClientMetadata.getClientName()); + assertEquals("http://localhost:8081/named/redirect", + testClientRegistrations.namedClientMetadata.getRedirectUris().get(0)); + } + + @Singleton + public static final class TestClientRegistrations { + + private volatile ClientMetadata defaultClientMetadata; + private volatile ClientMetadata namedClientMetadata; + + void prepareDefaultClientMetadata(@Observes StartupEvent event, OidcClientRegistrations clientRegistrations) { + var clientRegistration = clientRegistrations.getClientRegistration(); + var registeredClient = clientRegistration.registeredClient().await().indefinitely(); + defaultClientMetadata = registeredClient.metadata(); + + clientRegistration = clientRegistrations.getClientRegistration("named"); + registeredClient = clientRegistration.registeredClient().await().indefinitely(); + namedClientMetadata = registeredClient.metadata(); + } + } +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java index 0016d5b585422..ea7cce1cfbcd1 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java @@ -105,8 +105,11 @@ public static Uni createOidcClientRegistrationUni(OidcCl if (isEmptyMetadata(oidcConfig.metadata)) { return Uni.createFrom().nullItem(); } + var clientName = DEFAULT_ID.equals(oidcConfig.id.orElse(DEFAULT_ID)) ? "" : "." + oidcConfig.id.get(); throw new ConfigurationException( - "Either 'quarkus.oidc-client-registration.auth-server-url' or absolute 'quarkus.oidc-client-registration.registration-path' URL must be set"); + "Either 'quarkus.oidc-client-registration" + clientName + + ".auth-server-url' or absolute 'quarkus.oidc-client-registration" + clientName + + ".registration-path' URL must be set"); } OidcCommonUtils.verifyEndpointUrl(getEndpointUrl(oidcConfig)); } catch (Throwable t) { @@ -194,7 +197,7 @@ public Uni apply(OidcConfigurationMetadata metadata, Thr public OidcClientRegistration apply(RegisteredClient r, Throwable t2) { RegisteredClient registeredClient; if (t2 != null) { - LOG.errorf("%s client registartion failed: %s, it can be retried later", + LOG.errorf("%s client registration failed: %s, it can be retried later", oidcConfig.id.orElse(DEFAULT_ID), t2.getMessage()); registeredClient = null; } else { diff --git a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java index 0dd562bec6fc4..fe6e9fae6f74f 100644 --- a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java +++ b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java @@ -25,8 +25,6 @@ import io.quarkus.arc.processor.DotNames; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.Feature; -import io.quarkus.deployment.IsDevelopment; -import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; @@ -37,11 +35,6 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; -import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; -import io.quarkus.devservices.keycloak.KeycloakDevServicesConfigBuildItem; -import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem; -import io.quarkus.devui.spi.page.CardPageBuildItem; -import io.quarkus.devui.spi.page.Page; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.MethodCreator; @@ -191,27 +184,6 @@ private AccessTokenInstanceBuildItem build() { return index.getIndex().getAnnotations(ACCESS_TOKEN).stream().map(ItemBuilder::new).map(ItemBuilder::build).toList(); } - @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) - KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() { - // this needs to be done as the shared Keycloak Dev Service doesn't know if the OIDC Client is enabled - return KeycloakDevServicesRequiredBuildItem.requireDevServiceForOidcClient(); - } - - @BuildStep(onlyIf = IsDevelopment.class) - void produceDevUiCardWithKeycloakUrl(Optional configProps, - BuildProducer cardPageProducer) { - final String keycloakAdminUrl = configProps.map(item -> item.getConfig().get("keycloak.url")).orElse(null); - if (keycloakAdminUrl != null) { - // Add Admin page - final CardPageBuildItem cardPage = new CardPageBuildItem(); - cardPage.addPage(Page.externalPageBuilder("Keycloak Admin") - .icon("font-awesome-solid:key") - .doNotEmbed(true) - .url(keycloakAdminUrl)); - cardPageProducer.produce(cardPage); - } - } - /** * Creates a Tokens producer class like follows: * diff --git a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/devservices/keycloak/KeycloakDevServiceRequiredBuildStep.java b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/devservices/keycloak/KeycloakDevServiceRequiredBuildStep.java new file mode 100644 index 0000000000000..7c6841def6d9e --- /dev/null +++ b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/devservices/keycloak/KeycloakDevServiceRequiredBuildStep.java @@ -0,0 +1,73 @@ +package io.quarkus.oidc.client.deployment.devservices.keycloak; + +import java.util.HashMap; + +import org.jboss.logging.Logger; + +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.devservices.keycloak.KeycloakAdminPageBuildItem; +import io.quarkus.devservices.keycloak.KeycloakDevServicesConfig; +import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.oidc.client.deployment.OidcClientBuildStep; +import io.quarkus.runtime.configuration.ConfigUtils; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { OidcClientBuildStep.IsEnabled.class, GlobalDevServicesConfig.Enabled.class }) +public class KeycloakDevServiceRequiredBuildStep { + + private static final Logger LOG = Logger.getLogger(KeycloakDevServiceRequiredBuildStep.class); + private static final String CONFIG_PREFIX = "quarkus.oidc-client."; + private static final String OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY = CONFIG_PREFIX + "auth-server-url"; + private static final String OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY = CONFIG_PREFIX + "token-path"; + private static final String OIDC_CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret"; + private static final String OIDC_CLIENT_ID_CONFIG_KEY = CONFIG_PREFIX + "client-id"; + private static final String OIDC_AUTH_SERVER_URL_CONFIG_KEY = "quarkus.oidc.auth-server-url"; + + @BuildStep + void requireKeycloakDevService(BuildProducer keycloakDevSvcRequiredProducer, + DockerStatusBuildItem dockerStatusBuildItem, KeycloakDevServicesConfig config) { + if (ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY)) { + LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided", + OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY); + return; + } + if (ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY)) { + LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided", + OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY); + return; + } + // this is for backwards compatibility - when users configured named client with OIDC auth server URL + // that is not our Keycloak Dev Service, we don't want to start our Dev Service to keep previous behavior + if (ConfigUtils.isPropertyNonEmpty(OIDC_AUTH_SERVER_URL_CONFIG_KEY)) { + LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided", + OIDC_AUTH_SERVER_URL_CONFIG_KEY); + return; + } + if (!dockerStatusBuildItem.isContainerRuntimeAvailable()) { + LOG.warnf("Please configure '%s' or get a working docker instance", OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY); + return; + } + + keycloakDevSvcRequiredProducer.produce(new KeycloakDevServicesRequiredBuildItem(ctx -> { + var configProperties = new HashMap(); + configProperties.put(OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY, ctx.authServerInternalUrl()); + configProperties.put(OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY, "/protocol/openid-connect/tokens"); + if (config.createClient()) { + configProperties.put(OIDC_CLIENT_ID_CONFIG_KEY, ctx.oidcClientId()); + configProperties.put(OIDC_CLIENT_SECRET_CONFIG_KEY, ctx.oidcClientSecret()); + } + return configProperties; + })); + } + + @BuildStep(onlyIf = IsDevelopment.class) + KeycloakAdminPageBuildItem addCardWithLinkToKeycloakAdmin() { + return new KeycloakAdminPageBuildItem(new CardPageBuildItem()); + } +} diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index d173468d4383f..c5b4ccea69d6b 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -46,7 +46,6 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; -import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; @@ -58,8 +57,6 @@ import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; -import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem; import io.quarkus.oidc.AuthorizationCodeFlow; import io.quarkus.oidc.BearerTokenAuthentication; import io.quarkus.oidc.IdToken; @@ -398,12 +395,6 @@ List registerHttpAuthMechanismAnnotation() new HttpAuthMechanismAnnotationBuildItem(DotName.createSimple(BearerTokenAuthentication.class), BEARER_SCHEME)); } - @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) - KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() { - // this needs to be done as the shared Keycloak Dev Service doesn't know if the OIDC is enabled - return KeycloakDevServicesRequiredBuildItem.requireDevServiceForOidc(); - } - private static boolean isInjected(BeanRegistrationPhaseBuildItem beanRegistrationPhaseBuildItem, DotName requiredType, DotName withoutQualifier) { for (InjectionPointInfo injectionPoint : beanRegistrationPhaseBuildItem.getInjectionPoints()) { diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServiceRequiredBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServiceRequiredBuildStep.java new file mode 100644 index 0000000000000..d4757322480ab --- /dev/null +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServiceRequiredBuildStep.java @@ -0,0 +1,70 @@ +package io.quarkus.oidc.deployment.devservices.keycloak; + +import java.util.HashMap; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; + +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.devservices.keycloak.KeycloakDevServicesConfig; +import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem; +import io.quarkus.oidc.deployment.OidcBuildStep; +import io.quarkus.runtime.configuration.ConfigUtils; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { OidcBuildStep.IsEnabled.class, GlobalDevServicesConfig.Enabled.class }) +public class KeycloakDevServiceRequiredBuildStep { + + private static final Logger LOG = Logger.getLogger(KeycloakDevServiceRequiredBuildStep.class); + private static final String CONFIG_PREFIX = "quarkus.oidc."; + private static final String TENANT_ENABLED_CONFIG_KEY = CONFIG_PREFIX + "tenant-enabled"; + private static final String AUTH_SERVER_URL_CONFIG_KEY = CONFIG_PREFIX + "auth-server-url"; + private static final String PROVIDER_CONFIG_KEY = CONFIG_PREFIX + "provider"; + private static final String APPLICATION_TYPE_CONFIG_KEY = CONFIG_PREFIX + "application-type"; + private static final String CLIENT_ID_CONFIG_KEY = CONFIG_PREFIX + "client-id"; + private static final String CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret"; + + @BuildStep + void requireKeycloakDevService(BuildProducer keycloakDevSvcRequiredProducer, + DockerStatusBuildItem dockerStatusBuildItem, KeycloakDevServicesConfig config) { + if (!isOidcTenantEnabled() && !config.startWithDisabledTenant()) { + LOG.debug("Not starting Dev Services for Keycloak as 'quarkus.oidc.tenant.enabled' is false"); + return; + } + if (ConfigUtils.isPropertyNonEmpty(AUTH_SERVER_URL_CONFIG_KEY)) { + LOG.debug("Not starting Dev Services for Keycloak as 'quarkus.oidc.auth-server-url' has been provided"); + return; + } + if (ConfigUtils.isPropertyNonEmpty(PROVIDER_CONFIG_KEY)) { + LOG.debug("Not starting Dev Services for Keycloak as 'quarkus.oidc.provider' has been provided"); + return; + } + if (!dockerStatusBuildItem.isContainerRuntimeAvailable()) { + LOG.warn("Please configure 'quarkus.oidc.auth-server-url' or get a working docker instance"); + return; + } + + keycloakDevSvcRequiredProducer.produce(new KeycloakDevServicesRequiredBuildItem(ctx -> { + var configProperties = new HashMap(); + configProperties.put(AUTH_SERVER_URL_CONFIG_KEY, ctx.authServerInternalUrl()); + configProperties.put(APPLICATION_TYPE_CONFIG_KEY, getOidcApplicationType()); + if (config.createClient()) { + configProperties.put(CLIENT_ID_CONFIG_KEY, ctx.oidcClientId()); + configProperties.put(CLIENT_SECRET_CONFIG_KEY, ctx.oidcClientSecret()); + } + return configProperties; + })); + } + + private static boolean isOidcTenantEnabled() { + return ConfigProvider.getConfig().getOptionalValue(TENANT_ENABLED_CONFIG_KEY, Boolean.class).orElse(true); + } + + private static String getOidcApplicationType() { + return ConfigProvider.getConfig().getOptionalValue(APPLICATION_TYPE_CONFIG_KEY, String.class).orElse("service"); + } +} diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java index 2cd325f68eadb..52876537d5b2e 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java @@ -14,10 +14,10 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ConfigurationBuildItem; import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; +import io.quarkus.devservices.keycloak.KeycloakAdminPageBuildItem; import io.quarkus.devservices.keycloak.KeycloakDevServicesConfigBuildItem; import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; import io.quarkus.devui.spi.page.CardPageBuildItem; -import io.quarkus.devui.spi.page.Page; import io.quarkus.oidc.deployment.DevUiConfig; import io.quarkus.oidc.deployment.OidcBuildTimeConfig; import io.quarkus.oidc.deployment.devservices.AbstractDevUIProcessor; @@ -33,19 +33,18 @@ public class KeycloakDevUIProcessor extends AbstractDevUIProcessor { @BuildStep(onlyIf = IsDevelopment.class) @Consume(RuntimeConfigSetupCompleteBuildItem.class) void produceProviderComponent(Optional configProps, - BuildProducer cardPageProducer, + BuildProducer keycloakAdminPageProducer, OidcDevUiRecorder recorder, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, BuildProducer syntheticBeanBuildItemBuildProducer, ConfigurationBuildItem configurationBuildItem, Capabilities capabilities) { - if (configProps.isPresent() && configProps.get().getConfig().containsKey("keycloak.url")) { + final String keycloakAdminUrl = KeycloakDevServicesConfigBuildItem.getKeycloakUrl(configProps); + if (keycloakAdminUrl != null) { String realmUrl = configProps.get().getConfig().get("quarkus.oidc.auth-server-url"); @SuppressWarnings("unchecked") Map users = (Map) configProps.get().getProperties().get("oidc.users"); - String keycloakAdminUrl = configProps.get().getConfig().get("keycloak.url"); - @SuppressWarnings("unchecked") final List keycloakRealms = (List) configProps.get().getProperties().get("keycloak.realms"); @@ -68,13 +67,9 @@ void produceProviderComponent(Optional confi users, keycloakRealms, configProps.get().isContainerRestarted()); - - // Also add Admin page - cardPageBuildItem.addPage(Page.externalPageBuilder("Keycloak Admin") - .icon("font-awesome-solid:key") - .doNotEmbed(true) - .url(keycloakAdminUrl)); - cardPageProducer.produce(cardPageBuildItem); + // use same card page so that both pages appear on the same card + var keycloakAdminPageItem = new KeycloakAdminPageBuildItem(cardPageBuildItem); + keycloakAdminPageProducer.produce(keycloakAdminPageItem); } }