All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkus.oidc.deployment.devservices.keycloak.KeycloakDevServicesProcessor Maven / Gradle / Ivy

There is a newer version: 3.15.0
Show newest version
package io.quarkus.oidc.deployment.devservices.keycloak;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.RolesRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.util.JsonSerialization;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;

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.CuratedApplicationShutdownBuildItem;
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;
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
import io.quarkus.deployment.logging.LoggingSetupBuildItem;
import io.quarkus.devservices.common.ConfigureUtil;
import io.quarkus.devservices.common.ContainerAddress;
import io.quarkus.devservices.common.ContainerLocator;
import io.quarkus.oidc.deployment.OidcBuildStep.IsEnabled;
import io.quarkus.oidc.deployment.OidcBuildTimeConfig;
import io.quarkus.oidc.deployment.devservices.OidcDevServicesBuildItem;
import io.quarkus.oidc.runtime.devui.OidcDevServicesUtils;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.configuration.ConfigUtils;
import io.smallrye.mutiny.TimeoutException;
import io.smallrye.mutiny.Uni;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpHeaders;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpResponse;
import io.vertx.mutiny.ext.web.client.WebClient;

@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { IsEnabled.class, 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";

    private static final String KEYCLOAK_CONTAINER_NAME = "keycloak";
    private static final int KEYCLOAK_PORT = 8080;

    private static final String KEYCLOAK_LEGACY_IMAGE_VERSION_PART = "-legacy";

    private static final String KEYCLOAK_ADMIN_USER = "admin";
    private static final String KEYCLOAK_ADMIN_PASSWORD = "admin";

    // Properties recognized by Wildfly-powered Keycloak
    private static final String KEYCLOAK_WILDFLY_FRONTEND_URL = "KEYCLOAK_FRONTEND_URL";
    private static final String KEYCLOAK_WILDFLY_USER_PROP = "KEYCLOAK_USER";
    private static final String KEYCLOAK_WILDFLY_PASSWORD_PROP = "KEYCLOAK_PASSWORD";
    private static final String KEYCLOAK_WILDFLY_DB_VENDOR = "H2";
    private static final String KEYCLOAK_WILDFLY_VENDOR_PROP = "DB_VENDOR";

    // Properties recognized by Quarkus-powered Keycloak
    private static final String KEYCLOAK_QUARKUS_HOSTNAME = "KC_HOSTNAME";
    private static final String KEYCLOAK_QUARKUS_ADMIN_PROP = "KEYCLOAK_ADMIN";
    private static final String KEYCLOAK_QUARKUS_ADMIN_PASSWORD_PROP = "KEYCLOAK_ADMIN_PASSWORD";
    private static final String KEYCLOAK_QUARKUS_START_CMD = "start --http-enabled=true --hostname-strict=false "
            + "--spi-user-profile-declarative-user-profile-config-file=/opt/keycloak/upconfig.json";

    private static final String JAVA_OPTS = "JAVA_OPTS";
    private static final String OIDC_USERS = "oidc.users";
    private static final String KEYCLOAK_REALMS = "keycloak.realms";

    /**
     * Label to add to shared Dev Service for Keycloak running in containers.
     * 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,
            KEYCLOAK_PORT);

    private static volatile RunningDevService devService;
    static volatile DevServicesConfig capturedDevServicesConfiguration;
    private static volatile boolean first = true;
    private static volatile Set capturedRealmFileLastModifiedDate;

    OidcBuildTimeConfig oidcConfig;

    @BuildStep
    public DevServicesResultBuildItem startKeycloakContainer(
            DockerStatusBuildItem dockerStatusBuildItem,
            BuildProducer keycloakBuildItemBuildProducer,
            List devServicesSharedNetworkBuildItem,
            Optional oidcProviderBuildItem,
            KeycloakBuildTimeConfig config,
            CuratedApplicationShutdownBuildItem closeBuildItem,
            LaunchModeBuildItem launchMode,
            Optional consoleInstalledBuildItem,
            LoggingSetupBuildItem loggingSetupBuildItem,
            GlobalDevServicesConfig devServicesConfig) {

        if (oidcProviderBuildItem.isPresent()) {
            // Dev Services for the alternative OIDC provider are enabled
            return null;
        }

        DevServicesConfig currentDevServicesConfiguration = config.devservices;
        // 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);
            if (!restartRequired) {
                Set currentRealmFileLastModifiedDate = getRealmFileLastModifiedDate(
                        currentDevServicesConfiguration.realmPath);
                if (currentRealmFileLastModifiedDate != null
                        && !currentRealmFileLastModifiedDate.equals(capturedRealmFileLastModifiedDate)) {
                    restartRequired = true;
                    capturedRealmFileLastModifiedDate = currentRealmFileLastModifiedDate;
                }
            }
            if (!restartRequired) {
                DevServicesResultBuildItem result = devService.toBuildItem();
                String usersString = result.getConfig().get(OIDC_USERS);
                Map users = (usersString == null || usersString.isBlank()) ? Map.of()
                        : Arrays.stream(usersString.split(","))
                                .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());
                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;
        }
        capturedDevServicesConfiguration = currentDevServicesConfiguration;
        StartupLogCompressor compressor = new StartupLogCompressor(
                (launchMode.isTest() ? "(test) " : "") + "Keycloak Dev Services Starting:",
                consoleInstalledBuildItem, loggingSetupBuildItem);
        try {
            List errors = new ArrayList<>();

            boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(devServicesConfig,
                    devServicesSharedNetworkBuildItem);
            RunningDevService newDevService = startContainer(dockerStatusBuildItem, keycloakBuildItemBuildProducer,
                    useSharedNetwork,
                    devServicesConfig.timeout,
                    errors);
            if (newDevService == null) {
                if (errors.isEmpty()) {
                    compressor.close();
                } else {
                    compressor.closeAndDumpCaptured();
                }
                return null;
            }

            devService = newDevService;

            if (first) {
                first = false;
                Runnable closeTask = new Runnable() {
                    @Override
                    public void run() {
                        if (devService != null) {
                            try {
                                devService.close();
                            } catch (Throwable t) {
                                LOG.error("Failed to stop Keycloak container", t);
                            }
                        }
                        if (vertxInstance != null) {
                            try {
                                vertxInstance.close();
                            } catch (Throwable t) {
                                LOG.error("Failed to close Vertx instance", t);
                            }
                        }
                        first = true;
                        devService = null;
                        capturedDevServicesConfiguration = null;
                        vertxInstance = null;
                        capturedRealmFileLastModifiedDate = null;
                    }
                };
                closeBuildItem.addCloseTask(closeTask, true);
            }

            capturedRealmFileLastModifiedDate = getRealmFileLastModifiedDate(capturedDevServicesConfiguration.realmPath);
            if (devService != null && errors.isEmpty()) {
                compressor.close();
            } else {
                compressor.closeAndDumpCaptured();
            }
        } catch (Throwable t) {
            compressor.closeAndDumpCaptured();
            throw new RuntimeException(t);
        }
        LOG.info("Dev Services for Keycloak started.");

        return devService.toBuildItem();
    }

    private String startURL(String host, Integer port, boolean isKeycloakX) {
        return "http://" + host + ":" + port + (isKeycloakX ? "" : "/auth");
    }

    private Map prepareConfiguration(
            BuildProducer keycloakBuildItemBuildProducer, String internalURL,
            String hostURL, List realmReps,
            boolean keycloakX, List errors) {
        final String realmName = realmReps != null && !realmReps.isEmpty() ? realmReps.iterator().next().getRealm()
                : getDefaultRealmName();
        final String authServerInternalUrl = realmsURL(internalURL, realmName);

        String clientAuthServerBaseUrl = hostURL != null ? hostURL : internalURL;
        String clientAuthServerUrl = realmsURL(clientAuthServerBaseUrl, realmName);

        boolean createDefaultRealm = (realmReps == null || realmReps.isEmpty()) && capturedDevServicesConfiguration.createRealm;

        String oidcClientId = getOidcClientId(createDefaultRealm);
        String oidcClientSecret = getOidcClientSecret(createDefaultRealm);
        String oidcApplicationType = getOidcApplicationType();

        Map users = getUsers(capturedDevServicesConfiguration.users, createDefaultRealm);

        List realmNames = new LinkedList<>();

        // this needs to be only if we actually start the dev-service as it adds a shutdown hook
        // whose TCCL is the Augmentation CL, which if not removed, causes a massive memory leaks
        if (vertxInstance == null) {
            vertxInstance = Vertx.vertx();
        }

        WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance);
        try {
            String adminToken = getAdminToken(client, clientAuthServerBaseUrl);
            if (createDefaultRealm) {
                createDefaultRealm(client, adminToken, clientAuthServerBaseUrl, users, oidcClientId, oidcClientSecret, errors);
                realmNames.add(realmName);
            } else {
                if (realmReps != null) {
                    for (RealmRepresentation realmRep : realmReps) {
                        createRealm(client, adminToken, clientAuthServerBaseUrl, realmRep, errors);
                        realmNames.add(realmRep.getRealm());
                    }
                }
            }
        } finally {
            client.close();
        }

        Map configProperties = new HashMap<>();
        configProperties.put(KEYCLOAK_URL_KEY, internalURL);
        configProperties.put(AUTH_SERVER_URL_CONFIG_KEY, authServerInternalUrl);
        configProperties.put(CLIENT_AUTH_SERVER_URL_CONFIG_KEY, clientAuthServerUrl);
        configProperties.put(APPLICATION_TYPE_CONFIG_KEY, oidcApplicationType);
        configProperties.put(CLIENT_ID_CONFIG_KEY, oidcClientId);
        configProperties.put(CLIENT_SECRET_CONFIG_KEY, oidcClientSecret);
        configProperties.put(OIDC_USERS, users.entrySet().stream()
                .map(e -> e.toString()).collect(Collectors.joining(",")));
        configProperties.put(KEYCLOAK_REALMS, realmNames.stream().collect(Collectors.joining(",")));

        keycloakBuildItemBuildProducer
                .produce(new KeycloakDevServicesConfigBuildItem(configProperties,
                        Map.of(OIDC_USERS, users, KEYCLOAK_REALMS, realmNames), true));

        return configProperties;
    }

    private String realmsURL(String baseURL, String realmName) {
        return baseURL + "/realms/" + realmName;
    }

    private String getDefaultRealmName() {
        return capturedDevServicesConfiguration.realmName.orElse("quarkus");
    }

    private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuildItem,
            BuildProducer keycloakBuildItemBuildProducer,
            boolean useSharedNetwork, Optional timeout,
            List errors) {
        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()) {
            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;
        }

        final Optional maybeContainerAddress = keycloakDevModeContainerLocator.locateContainer(
                capturedDevServicesConfiguration.serviceName,
                capturedDevServicesConfiguration.shared,
                LaunchMode.current());

        String imageName = capturedDevServicesConfiguration.imageName;
        DockerImageName dockerImageName = DockerImageName.parse(imageName).asCompatibleSubstituteFor(imageName);

        final Supplier defaultKeycloakContainerSupplier = () -> {

            QuarkusOidcContainer oidcContainer = new QuarkusOidcContainer(dockerImageName,
                    capturedDevServicesConfiguration.port,
                    useSharedNetwork,
                    capturedDevServicesConfiguration.realmPath.orElse(List.of()),
                    resourcesMap(errors),
                    capturedDevServicesConfiguration.serviceName,
                    capturedDevServicesConfiguration.shared,
                    capturedDevServicesConfiguration.javaOpts,
                    capturedDevServicesConfiguration.startCommand,
                    capturedDevServicesConfiguration.showLogs,
                    errors);

            timeout.ifPresent(oidcContainer::withStartupTimeout);
            oidcContainer.withEnv(capturedDevServicesConfiguration.containerEnv);
            oidcContainer.start();

            String internalUrl = startURL(oidcContainer.getHost(), oidcContainer.getPort(), oidcContainer.keycloakX);
            String hostUrl = oidcContainer.useSharedNetwork
                    // we need to use auto-detected host and port, so it works when docker host != localhost
                    ? startURL(oidcContainer.getSharedNetworkExternalHost(), oidcContainer.getSharedNetworkExternalPort(),
                            oidcContainer.keycloakX)
                    : null;

            Map configs = prepareConfiguration(keycloakBuildItemBuildProducer, internalUrl, hostUrl,
                    oidcContainer.realmReps,
                    oidcContainer.keycloakX,
                    errors);
            return new RunningDevService(KEYCLOAK_CONTAINER_NAME, oidcContainer.getContainerId(),
                    oidcContainer::close, configs);
        };

        return maybeContainerAddress
                .map(containerAddress -> {
                    // TODO: this probably needs to be addressed
                    Map configs = prepareConfiguration(keycloakBuildItemBuildProducer,
                            getSharedContainerUrl(containerAddress),
                            getSharedContainerUrl(containerAddress), null, false, errors);
                    return new RunningDevService(KEYCLOAK_CONTAINER_NAME, containerAddress.getId(), null, configs);
                })
                .orElseGet(defaultKeycloakContainerSupplier);
    }

    private Map resourcesMap(List errors) {
        Map resources = new HashMap<>();
        for (Map.Entry aliasEntry : capturedDevServicesConfiguration.resourceAliases.entrySet()) {
            if (capturedDevServicesConfiguration.resourceMappings.containsKey(aliasEntry.getKey())) {
                resources.put(aliasEntry.getValue(),
                        capturedDevServicesConfiguration.resourceMappings.get(aliasEntry.getKey()));
            } else {
                errors.add(String.format("%s alias for the %s resource does not have a mapping", aliasEntry.getKey(),
                        aliasEntry.getValue()));
                LOG.errorf("%s alias for the %s resource does not have a mapping", aliasEntry.getKey(),
                        aliasEntry.getValue());
            }
        }
        return resources;
    }

    private static boolean isKeycloakX(DockerImageName dockerImageName) {
        return capturedDevServicesConfiguration.keycloakXImage.isPresent()
                ? capturedDevServicesConfiguration.keycloakXImage.get()
                : !dockerImageName.getVersionPart().endsWith(KEYCLOAK_LEGACY_IMAGE_VERSION_PART);
    }

    private String getSharedContainerUrl(ContainerAddress containerAddress) {
        return "http://" + ("0.0.0.0".equals(containerAddress.getHost()) ? "localhost" : containerAddress.getHost())
                + ":" + containerAddress.getPort();
    }

    private static class QuarkusOidcContainer extends GenericContainer {
        private final OptionalInt fixedExposedPort;
        private final boolean useSharedNetwork;
        private final List realmPaths;
        private final Map resources;
        private final String containerLabelValue;
        private final Optional javaOpts;
        private final boolean sharedContainer;
        private String hostName;
        private final boolean keycloakX;
        private List realmReps = new LinkedList<>();
        private final Optional startCommand;
        private final boolean showLogs;
        private final List errors;

        public QuarkusOidcContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, boolean useSharedNetwork,
                List realmPaths, Map resources, String containerLabelValue,
                boolean sharedContainer, Optional javaOpts, Optional startCommand, boolean showLogs,
                List errors) {
            super(dockerImageName);

            this.useSharedNetwork = useSharedNetwork;
            this.realmPaths = realmPaths;
            this.resources = resources;
            this.containerLabelValue = containerLabelValue;
            this.sharedContainer = sharedContainer;
            this.javaOpts = javaOpts;
            this.keycloakX = isKeycloakX(dockerImageName);

            if (useSharedNetwork && fixedExposedPort.isEmpty()) {
                // We need to know the port we are exposing when using the shared network, in order to be able to tell
                // Keycloak what the client URL is. This is necessary in order for Keycloak to create the proper 'issuer'
                // when creating tokens
                fixedExposedPort = OptionalInt.of(findRandomPort());
            }

            this.fixedExposedPort = fixedExposedPort;
            this.startCommand = startCommand;
            this.showLogs = showLogs;
            this.errors = errors;

            super.setWaitStrategy(Wait.forLogMessage(".*Keycloak.*started.*", 1));
        }

        @Override
        protected void configure() {
            super.configure();

            if (useSharedNetwork) {
                hostName = ConfigureUtil.configureSharedNetwork(this, "keycloak");
                if (keycloakX) {
                    addEnv(KEYCLOAK_QUARKUS_HOSTNAME, "localhost");
                } else {
                    addEnv(KEYCLOAK_WILDFLY_FRONTEND_URL, "http://localhost:" + fixedExposedPort.getAsInt());
                }
            }

            if (fixedExposedPort.isPresent()) {
                addFixedExposedPort(fixedExposedPort.getAsInt(), KEYCLOAK_PORT);
                if (useSharedNetwork) {
                    // expose random port for which we are able to ask Testcontainers for the actual mapped port at runtime
                    // as from the host's perspective Testcontainers actually expose container ports on random host port
                    addExposedPort(KEYCLOAK_PORT);
                }
            } else {
                addExposedPort(KEYCLOAK_PORT);
            }

            if (sharedContainer && LaunchMode.current() == LaunchMode.DEVELOPMENT) {
                withLabel(DEV_SERVICE_LABEL, containerLabelValue);
            }

            if (javaOpts.isPresent()) {
                addEnv(JAVA_OPTS, javaOpts.get());
            }

            if (keycloakX) {
                addEnv(KEYCLOAK_QUARKUS_ADMIN_PROP, KEYCLOAK_ADMIN_USER);
                addEnv(KEYCLOAK_QUARKUS_ADMIN_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD);
                withCommand(startCommand.orElse(KEYCLOAK_QUARKUS_START_CMD)
                        + (useSharedNetwork ? " --hostname-port=" + fixedExposedPort.getAsInt() : ""));
                addUpConfigResource();
            } else {
                addEnv(KEYCLOAK_WILDFLY_USER_PROP, KEYCLOAK_ADMIN_USER);
                addEnv(KEYCLOAK_WILDFLY_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD);
                addEnv(KEYCLOAK_WILDFLY_VENDOR_PROP, KEYCLOAK_WILDFLY_DB_VENDOR);
            }

            for (String realmPath : realmPaths) {
                URL realmPathUrl = null;
                if ((realmPathUrl = Thread.currentThread().getContextClassLoader().getResource(realmPath)) != null) {
                    readRealmFile(realmPathUrl, realmPath, errors).ifPresent(realmRep -> realmReps.add(realmRep));
                } else {
                    Path filePath = Paths.get(realmPath);
                    if (Files.exists(filePath)) {
                        readRealmFile(filePath.toUri(), realmPath, errors).ifPresent(realmRep -> realmReps.add(realmRep));
                    } else {
                        errors.add(String.format("Realm %s resource is not available", realmPath));
                        LOG.debugf("Realm %s resource is not available", realmPath);
                    }
                }

            }
            for (Map.Entry resource : resources.entrySet()) {
                mapResource(resource.getKey(), resource.getValue());
            }

            if (showLogs) {
                super.withLogConsumer(t -> {
                    LOG.info("Keycloak: " + t.getUtf8StringWithoutLineEnding());
                });
            }

            LOG.infof("Using %s powered Keycloak distribution", keycloakX ? "Quarkus" : "WildFly");
        }

        private void mapResource(String resourcePath, String mappedResource) {
            if (Thread.currentThread().getContextClassLoader().getResource(resourcePath) != null) {
                LOG.debugf("Mapping the classpath %s resource to %s", resourcePath, mappedResource);
                withClasspathResourceMapping(resourcePath, mappedResource, BindMode.READ_ONLY);
            } else if (Files.exists(Paths.get(resourcePath))) {
                LOG.debugf("Mapping the file system %s resource to %s", resourcePath, mappedResource);
                withFileSystemBind(resourcePath, mappedResource, BindMode.READ_ONLY);
            } else {
                errors.add(
                        String.format(
                                "%s resource can not be mapped to %s because it is not available on the classpath and file system",
                                resourcePath, mappedResource));
                LOG.errorf("%s resource can not be mapped to %s because it is not available on the classpath and file system",
                        resourcePath, mappedResource);
            }
        }

        private void addUpConfigResource() {
            if (Thread.currentThread().getContextClassLoader().getResource("/dev-service/upconfig.json") != null) {
                LOG.debug("Mapping the classpath /dev-service/upconfig.json resource to /opt/keycloak/upconfig.json");
                withClasspathResourceMapping("/dev-service/upconfig.json", "/opt/keycloak/upconfig.json", BindMode.READ_ONLY);
            }
        }

        private Integer findRandomPort() {
            try (ServerSocket socket = new ServerSocket(0)) {
                return socket.getLocalPort();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }

        private Optional readRealmFile(URI uri, String realmPath, List errors) {
            try {
                return readRealmFile(uri.toURL(), realmPath, errors);
            } catch (MalformedURLException ex) {
                // Will not happen as this method is called only when it is confirmed the file exists
                throw new RuntimeException(ex);
            }
        }

        private Optional readRealmFile(URL url, String realmPath, List errors) {
            try {
                try (InputStream is = url.openStream()) {
                    return Optional.of(JsonSerialization.readValue(is, RealmRepresentation.class));
                }
            } catch (IOException ex) {
                errors.add(String.format("Realm %s resource can not be opened: %s", realmPath, ex.getMessage()));

                LOG.errorf("Realm %s resource can not be opened: %s", realmPath, ex.getMessage());
            }
            return Optional.empty();
        }

        @Override
        public String getHost() {
            if (useSharedNetwork) {
                return hostName;
            }
            return super.getHost();
        }

        /**
         * Host name used for calls from outside of docker when {@code useSharedNetwork} is true.
         *
         * @return host name
         */
        private String getSharedNetworkExternalHost() {
            return super.getHost();
        }

        /**
         * Host port used for calls from outside of docker when {@code useSharedNetwork} is true.
         *
         * @return port
         */
        private int getSharedNetworkExternalPort() {
            return getFirstMappedPort();
        }

        public int getPort() {
            if (useSharedNetwork) {
                return KEYCLOAK_PORT;
            }
            if (fixedExposedPort.isPresent()) {
                return fixedExposedPort.getAsInt();
            }
            return getFirstMappedPort();
        }
    }

    private Set getRealmFileLastModifiedDate(Optional> realms) {
        if (realms.isPresent()) {
            Set times = new HashSet<>();

            for (String realm : realms.get()) {
                Path realmPath = Paths.get(realm);
                try {
                    times.add(Files.getLastModifiedTime(realmPath));
                } catch (IOException ex) {
                    LOG.tracef("Unable to get the last modified date of the realm file %s", realmPath);
                }
            }

            return times;
        }
        return null;
    }

    private void createDefaultRealm(WebClient client, String token, String keycloakUrl, Map users,
            String oidcClientId,
            String oidcClientSecret,
            List errors) {
        RealmRepresentation realm = createDefaultRealmRep();

        realm.getClients().add(createClient(oidcClientId, oidcClientSecret));
        for (Map.Entry entry : users.entrySet()) {
            realm.getUsers().add(createUser(entry.getKey(), entry.getValue(), getUserRoles(entry.getKey())));
        }

        createRealm(client, token, keycloakUrl, realm, errors);
    }

    private String getAdminToken(WebClient client, String keycloakUrl) {
        try {
            LOG.tracef("Acquiring admin token");

            return OidcDevServicesUtils.getPasswordAccessToken(client,
                    keycloakUrl + "/realms/master/protocol/openid-connect/token",
                    "admin-cli", null, "admin", "admin", null)
                    .await().atMost(oidcConfig.devui.webClientTimeout);
        } catch (TimeoutException e) {
            LOG.error("Admin token can not be acquired due to a client connection timeout. " +
                    "You may try increasing the `quarkus.oidc.devui.web-client-timeout` property.");
        } catch (Throwable t) {
            LOG.error("Admin token can not be acquired", t);
        }
        return null;
    }

    private void createRealm(WebClient client, String token, String keycloakUrl, RealmRepresentation realm,
            List errors) {
        try {
            LOG.tracef("Creating the realm %s", realm.getRealm());
            HttpResponse createRealmResponse = client.postAbs(keycloakUrl + "/admin/realms")
                    .putHeader(HttpHeaders.CONTENT_TYPE.toString(), "application/json")
                    .putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer " + token)
                    .sendBuffer(Buffer.buffer().appendString(JsonSerialization.writeValueAsString(realm)))
                    .await().atMost(oidcConfig.devui.webClientTimeout);

            if (createRealmResponse.statusCode() > 299) {
                errors.add(String.format("Realm %s can not be created %d - %s ", realm.getRealm(),
                        createRealmResponse.statusCode(),
                        createRealmResponse.statusMessage()));

                LOG.errorf("Realm %s can not be created %d - %s ", realm.getRealm(), createRealmResponse.statusCode(),
                        createRealmResponse.statusMessage());
            }

            Uni realmStatusCodeUni = client.getAbs(keycloakUrl + "/realms/" + realm.getRealm())
                    .send().onItem()
                    .transform(resp -> {
                        LOG.debugf("Realm status: %d", resp.statusCode());
                        if (resp.statusCode() == 200) {
                            LOG.debugf("Realm %s has been created", realm.getRealm());
                            return 200;
                        } else {
                            throw new RealmEndpointAccessException(resp.statusCode());
                        }
                    }).onFailure(realmEndpointNotAvailable())
                    .retry()
                    .withBackOff(Duration.ofSeconds(2), Duration.ofSeconds(2))
                    .expireIn(10 * 1000)
                    .onFailure().transform(t -> {
                        return new RuntimeException("Keycloak server is not available"
                                + (t.getMessage() != null ? (": " + t.getMessage()) : ""));
                    });
            realmStatusCodeUni.await().atMost(Duration.ofSeconds(10));
        } catch (Throwable t) {
            errors.add(String.format("Realm %s can not be created: %s", realm.getRealm(), t.getMessage()));

            LOG.errorf(t, "Realm %s can not be created", realm.getRealm());
        }
    }

    @SuppressWarnings("serial")
    static class RealmEndpointAccessException extends RuntimeException {
        private final int errorStatus;

        public RealmEndpointAccessException(int errorStatus) {
            this.errorStatus = errorStatus;
        }

        public int getErrorStatus() {
            return errorStatus;
        }
    }

    public static Predicate realmEndpointNotAvailable() {
        return t -> (t instanceof ConnectException
                || (t instanceof RealmEndpointAccessException && ((RealmEndpointAccessException) t).getErrorStatus() == 404));
    }

    private Map getUsers(Map configuredUsers, boolean createRealm) {
        if (configuredUsers.isEmpty() && createRealm) {
            Map users = new LinkedHashMap();
            users.put("alice", "alice");
            users.put("bob", "bob");
            return users;
        } else {
            return configuredUsers;
        }
    }

    private 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() {
        RealmRepresentation realm = new RealmRepresentation();

        realm.setRealm(getDefaultRealmName());
        realm.setEnabled(true);
        realm.setUsers(new ArrayList<>());
        realm.setClients(new ArrayList<>());
        realm.setAccessTokenLifespan(600);
        realm.setSsoSessionMaxLifespan(600);
        realm.setRefreshTokenMaxReuse(10);

        RolesRepresentation roles = new RolesRepresentation();
        List realmRoles = new ArrayList<>();

        roles.setRealm(realmRoles);
        realm.setRoles(roles);

        if (capturedDevServicesConfiguration.roles.isEmpty()) {
            realm.getRoles().getRealm().add(new RoleRepresentation("user", null, false));
            realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false));
        } else {
            Set allRoles = new HashSet<>();
            for (List distinctRoles : capturedDevServicesConfiguration.roles.values()) {
                for (String role : distinctRoles) {
                    if (!allRoles.contains(role)) {
                        allRoles.add(role);
                        realm.getRoles().getRealm().add(new RoleRepresentation(role, null, false));
                    }
                }
            }
        }
        return realm;
    }

    private ClientRepresentation createClient(String clientId, String oidcClientSecret) {
        ClientRepresentation client = new ClientRepresentation();

        client.setClientId(clientId);
        client.setRedirectUris(List.of("*"));
        client.setPublicClient(false);
        client.setSecret(oidcClientSecret);
        client.setDirectAccessGrantsEnabled(true);
        client.setServiceAccountsEnabled(true);
        client.setImplicitFlowEnabled(true);
        client.setEnabled(true);
        client.setRedirectUris(List.of("*"));
        client.setDefaultClientScopes(List.of("microprofile-jwt"));

        return client;
    }

    private UserRepresentation createUser(String username, String password, List realmRoles) {
        UserRepresentation user = new UserRepresentation();

        user.setUsername(username);
        user.setEnabled(true);
        user.setCredentials(new ArrayList<>());
        user.setRealmRoles(realmRoles);

        CredentialRepresentation credential = new CredentialRepresentation();

        credential.setType(CredentialRepresentation.PASSWORD);
        credential.setValue(password);
        credential.setTemporary(false);

        user.getCredentials().add(credential);

        return user;
    }

    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");
    }

    private static String getOidcClientId(boolean createRealm) {
        return ConfigProvider.getConfig().getOptionalValue(CLIENT_ID_CONFIG_KEY, String.class)
                .orElse(createRealm ? "quarkus-app" : "");
    }

    private static String getOidcClientSecret(boolean createRealm) {
        return ConfigProvider.getConfig().getOptionalValue(CLIENT_SECRET_CONFIG_KEY, String.class)
                .orElse(createRealm ? "secret" : "");
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy