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

org.openremote.manager.security.ManagerKeycloakIdentityProvider Maven / Gradle / Ivy

/*
 * Copyright 2017, OpenRemote Inc.
 *
 * See the CONTRIBUTORS.txt file in the distribution for a
 * full listing of individual contributors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see .
 */
package org.openremote.manager.security;

import io.undertow.util.Headers;
import jakarta.persistence.Query;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.utils.URIBuilder;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.*;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.representations.idm.*;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.persistence.PersistenceService;
import org.openremote.container.security.AuthContext;
import org.openremote.container.security.keycloak.KeycloakIdentityProvider;
import org.openremote.container.timer.TimerService;
import org.openremote.container.web.WebService;
import org.openremote.manager.apps.ConsoleAppService;
import org.openremote.manager.asset.AssetStorageService;
import org.openremote.manager.event.ClientEventService;
import org.openremote.model.Constants;
import org.openremote.model.Container;
import org.openremote.model.PersistenceEvent;
import org.openremote.model.asset.Asset;
import org.openremote.model.auth.OAuthGrant;
import org.openremote.model.auth.OAuthPasswordGrant;
import org.openremote.model.event.shared.RealmFilter;
import org.openremote.model.gateway.GatewayConnection;
import org.openremote.model.provisioning.ProvisioningConfig;
import org.openremote.model.query.AssetQuery;
import org.openremote.model.query.UserQuery;
import org.openremote.model.query.filter.RealmPredicate;
import org.openremote.model.rules.RealmRuleset;
import org.openremote.model.security.*;
import org.openremote.model.util.TextUtil;
import org.openremote.model.util.UniqueIdentifierGenerator;
import org.openremote.model.util.ValueUtil;

import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static org.openremote.container.util.MapAccess.getBoolean;
import static org.openremote.container.util.MapAccess.getString;
import static org.openremote.model.Constants.*;
import static org.openremote.model.util.ValueUtil.convert;

/**
 * All keycloak interaction is done through the admin-cli client; security is implemented downstream of here; anything
 * in the same process as this service has privileged access to keycloak.
 */
public class ManagerKeycloakIdentityProvider extends KeycloakIdentityProvider implements ManagerIdentityProvider {

    private static final Logger LOG = Logger.getLogger(ManagerKeycloakIdentityProvider.class.getName());
    public static final String REALM_KEYCLOAK_THEME_SUFFIX = "_REALM_KEYCLOAK_THEME";
    public static final String DEFAULT_REALM_KEYCLOAK_THEME = "DEFAULT_REALM_KEYCLOAK_THEME";
    public static final String DEFAULT_REALM_KEYCLOAK_THEME_DEFAULT = "openremote";
    public static final String OR_KEYCLOAK_GRANT_FILE = "OR_KEYCLOAK_GRANT_FILE";
    public static final String OR_KEYCLOAK_GRANT_FILE_DEFAULT = "manager/keycloak-credentials.json";
    public static final String OR_KEYCLOAK_PUBLIC_URI = "OR_KEYCLOAK_PUBLIC_URI";
    public static final String OR_KEYCLOAK_PUBLIC_URI_DEFAULT = "/auth";
    public static final String OR_KEYCLOAK_ENABLE_DIRECT_ACCESS_GRANT = "OR_KEYCLOAK_ENABLE_DIRECT_ACCESS_GRANT";

    protected PersistenceService persistenceService;
    protected AssetStorageService assetStorageService;
    protected TimerService timerService;
    protected MessageBrokerService messageBrokerService;
    protected ClientEventService clientEventService;
    protected ConsoleAppService consoleAppService;
    protected String keycloakAdminPassword;
    protected Container container;
    protected String frontendURI;
    protected List validRedirectUris;
    protected Map realmCache = new ConcurrentHashMap<>();

    @Override
    public void init(Container container) {
        super.init(container);
        this.container = container;

        String keycloakPublicUri = getString(container.getConfig(), OR_KEYCLOAK_PUBLIC_URI, OR_KEYCLOAK_PUBLIC_URI_DEFAULT);
        try {
            URIBuilder uriBuilder = new URIBuilder(keycloakPublicUri);
            frontendURI = uriBuilder.build().toString();
        } catch (URISyntaxException e) {
            LOG.log(Level.SEVERE, "Failed to build Keycloak public URI", e);
            throw new RuntimeException(e);
        }

        keycloakAdminPassword = container.getConfig().getOrDefault(OR_ADMIN_PASSWORD, OR_ADMIN_PASSWORD_DEFAULT);
        timerService = container.getService(TimerService.class);
        persistenceService = container.getService(PersistenceService.class);
        messageBrokerService = container.getService(MessageBrokerService.class);
        clientEventService = container.getService(ClientEventService.class);
        consoleAppService = container.getService(ConsoleAppService.class);
        assetStorageService = container.getService(AssetStorageService.class);

        // Allow all external hostnames with wildcard and same host with wildcard
        validRedirectUris = new ArrayList<>();
        validRedirectUris.add("/*");
        validRedirectUris.addAll(WebService.getExternalHostnames(container).stream().map(host -> "https://" + host + "/*").toList());
    }

    @Override
    public void start(Container container) {
        super.start(container);
        if (container.isDevMode()) {
            String keycloakPath = getString(container.getConfig(), OR_KEYCLOAK_PATH, OR_KEYCLOAK_PATH_DEFAULT);
            enableAuthProxy(container.getService(WebService.class), keycloakPath);
        }
    }

    @Override
    protected OAuthGrant getStoredCredentials(Container container) {
        // Try and load keycloak proxy credentials from the file system
        String grantFile = getString(container.getConfig(), OR_KEYCLOAK_GRANT_FILE, OR_KEYCLOAK_GRANT_FILE_DEFAULT);
        Path grantPath = TextUtil.isNullOrEmpty(grantFile) ? null : persistenceService.resolvePath(grantFile);
        OAuthGrant grant = null;

        if (grantPath != null && Files.isReadable(grantPath)) {
            LOG.info("Loading OR_KEYCLOAK_GRANT_FILE: " + grantPath);

            try (InputStream is = Files.newInputStream(grantPath)) {
                String grantJson = IOUtils.toString(is, StandardCharsets.UTF_8);
                grant = ValueUtil.parse(grantJson, OAuthGrant.class).orElseGet(() -> {
                    LOG.warning("Failed to load OR_KEYCLOAK_GRANT_FILE: " + grantPath);
                    return null;
                });
            } catch (Exception ex) {
                throw new ExceptionInInitializerError(ex);
            }
        }
        return grant;
    }

    @Override
    protected OAuthGrant generateStoredCredentials(Container container) {
        String grantFile = getString(container.getConfig(), OR_KEYCLOAK_GRANT_FILE, OR_KEYCLOAK_GRANT_FILE_DEFAULT);

        if (TextUtil.isNullOrEmpty(grantFile)) {
            return null;
        }

        Path grantPath = persistenceService.resolvePath(grantFile);

        // Create a new super user for the keycloak proxy so admin user can be modified if desired
        User keycloakProxyUser = new User()
            .setUsername(MANAGER_CLIENT_ID)
            .setEnabled(true)
            .setSystemAccount(true);
        // Make password complex enough to hopefully meet any password policy that is set in keycloak
        String password = UniqueIdentifierGenerator.generateId() + "$*#@$";

        try {
            keycloakProxyUser = createUpdateUser(MASTER_REALM, keycloakProxyUser, password, true);

            // Make this proxy user a super user by giving them admin realm role
            updateUserRealmRoles(MASTER_REALM, keycloakProxyUser.getId(), addRealmRoles(MASTER_REALM, keycloakProxyUser.getId(), REALM_ADMIN_ROLE));

            // Use same details as default keycloak grant but change the username and password to our new user
            OAuthPasswordGrant grant = getDefaultKeycloakGrant(container);
            grant.setUsername(keycloakProxyUser.getUsername()).setPassword(password);

            // Ensure the directory path exists
            grantPath.getParent().toFile().mkdirs();

            Files.write(grantPath, ValueUtil.asJSON(grant).orElse("null").getBytes(),
                StandardOpenOption.CREATE,
                StandardOpenOption.WRITE,
                StandardOpenOption.TRUNCATE_EXISTING);

            return grant;
        } catch (Exception e) {
            LOG.info("Failed to write " + OR_KEYCLOAK_GRANT_FILE + ": " + grantPath);
            return null;
        }
    }

    @Override
    protected void addClientRedirectUris(String client, List redirectUrls, boolean devMode) {
        // Callback URL used by Manager web client authentication, any relative path to "ourselves" is fine
        String realmManagerCallbackUrl = UriBuilder.fromUri("/").path(client).path("*").build().toString();
        redirectUrls.add(realmManagerCallbackUrl);
    }

    protected  T withClientResource(String realm, String client, RealmsResource realmsResource, BiFunction clientResourceConsumer, Supplier notFoundProvider) {
        ClientRepresentation clientRepresentation = null;
        ClientResource clientResource = null;

        try {
            ClientsResource clientsResource = realmsResource.realm(realm).clients();
            List clientRepresentations = clientsResource.findByClientId(client);
            if (clientRepresentations != null && !clientRepresentations.isEmpty()) {
                if (clientRepresentations.size() > 1) {
                    throw new IllegalStateException("More than one matching client found realm=" + realm + ", client=" + client);
                }
                clientRepresentation = clientRepresentations.get(0);
                clientResource = clientsResource.get(clientRepresentation.getId());
            }
        } catch (Exception e) {
            LOG.log(Level.INFO, "withClientResource failed", e);
        }
        if (clientResource != null) {
            return clientResourceConsumer.apply(clientRepresentation, clientResource);
        } else if (notFoundProvider != null) {
            return notFoundProvider.get();
        }
        return null;
    }

    @Override
    public User[] queryUsers(UserQuery userQuery) {
        return ManagerIdentityProvider.getUsersFromDb(persistenceService, userQuery);
    }

    @Override
    public User getUser(String userId) {
        return ManagerIdentityProvider.getUserByIdFromDb(persistenceService, userId);
    }

    @Override
    public User getUserByUsername(String realm, String username) {
        if (username.length() > 255) {
            // Keycloak has a 255 character limit on clientId
            username = username.substring(0, 254);
        }
        username = username.toLowerCase(); // Keycloak clients are case sensitive but pretends not to be so always force lowercase
        return ManagerIdentityProvider.getUserByUsernameFromDb(persistenceService, realm, username);
    }

    @Override
    public User createUpdateUser(String realm, final User user, String passwordSecret, boolean allowUpdate) throws WebApplicationException {
        return getRealms(realmsResource -> {

            // Force lowercase username
            if (user.getUsername() != null) {
                user.setUsername(user.getUsername().toLowerCase(Locale.ROOT));
            }
            if (user.getUsername().length() > 255) {
                // Keycloak has a 255 character limit on clientId which affects service users
                user.setUsername(user.getUsername().substring(0, 254));
            }

            if (!user.isServiceAccount() && allowUpdate) {
                if (getRealm(realm).getRegistrationEmailAsUsername() ? user.getEmail() == null : user.getUsername() == null) {
                    throw new BadRequestException("Attempt to create/update user but no username or email provided: User=" + user);
                }
            }

            boolean isUpdate = false;
            User existingUser = user.getId() != null ? getUser(user.getId()) : getUserByUsername(realm, user.getUsername());
            ClientRepresentation clientRepresentation;
            UserRepresentation userRepresentation;

            if(existingUser != null && !allowUpdate) {
                String msg = "Attempt to create user but it already exists: User=" + user;
                LOG.warning(msg);
                throw new ForbiddenException(msg);
            }

            if (existingUser == null && user.isServiceAccount()) {
                // Could be a service user
                userRepresentation = withClientResource(realm, user.getUsername(), realmsResource, (clientRep, clientResource) -> {
                    UserRepresentation userRep = clientResource.getServiceAccountUser();
                    if (userRep == null) {
                        String msg = "Attempt to update/create service user but a regular client with same client ID as this username already exists: User=" + user;
                        LOG.warning(msg);
                        throw new NotAllowedException(msg);
                    }
                    return userRep;
                }, null);

                if (userRepresentation != null) {
                    existingUser = convert(userRepresentation, User.class);
                }
            }

            if (existingUser != null && user.getId() != null && !existingUser.getId().equals(user.getId())) {
                String msg = "Attempt to update user but retrieved user ID doesn't match supplied so ignoring: User=" + user;
                LOG.warning(msg);
                throw new BadRequestException(msg);
            }

            if (existingUser != null) {
                isUpdate = true;

                if (existingUser.isServiceAccount() != user.isServiceAccount()) {
                    String msg = "Attempt to update user service account flag not allowed: User=" + user;
                    LOG.warning(msg);
                    throw new NotAllowedException(msg);
                }

                if (existingUser.isServiceAccount() && !existingUser.getUsername().equals(user.getUsername())) {
                    String msg = "Attempt to update username of service user not allowed: User=" + user;
                    LOG.warning(msg);
                    throw new NotAllowedException(msg);
                }
            }

            // For service users we don't actually create the user - keycloak does that when the client is created
            if (isUpdate) {

                // User only has a subset of user representation so overlay on actual user representation
                UserResource userResource = realmsResource.realm(realm).users().get(existingUser.getId());
                userRepresentation = userResource.toRepresentation();
                userRepresentation.setFirstName(user.getFirstName());
                userRepresentation.setLastName(user.getLastName());
                userRepresentation.setEmail(user.getEmail());
                userRepresentation.setEnabled(user.getEnabled());
                userRepresentation.setAttributes(user.getAttributeMap());
                userResource.update(userRepresentation);

            } else {

                if (user.isServiceAccount()) {

                    // Just create client with service account and user will be generated
                    clientRepresentation = new ClientRepresentation();
                    clientRepresentation.setStandardFlowEnabled(false);
                    clientRepresentation.setImplicitFlowEnabled(false);
                    clientRepresentation.setDirectAccessGrantsEnabled(false);
                    clientRepresentation.setServiceAccountsEnabled(true);
                    clientRepresentation.setClientAuthenticatorType("client-secret");
                    clientRepresentation.setClientId(user.getUsername());
                    clientRepresentation.setSecret(passwordSecret);
                    clientRepresentation = createUpdateClient(realm, clientRepresentation);
                    userRepresentation = realmsResource.realm(realm)
                        .clients()
                        .get(clientRepresentation.getId()).getServiceAccountUser();
                    userRepresentation.setEnabled(user.getEnabled());
                    realmsResource.realm(realm).users().get(userRepresentation.getId()).update(userRepresentation);

                } else {

                    userRepresentation = convert(user, UserRepresentation.class);
                    RealmResource realmResource = realmsResource.realm(realm);
                    Response response = realmResource.users().create(userRepresentation);
                    String location = response.getHeaderString(Headers.LOCATION_STRING);
                    response.close();
                    if (!response.getStatusInfo().equals(Response.Status.CREATED) || TextUtil.isNullOrEmpty(location)) {
                        throw new BadRequestException("Failed to create user: User=" + user);
                    }
                    String[] locationArr = location.split("/");
                    String userId = locationArr.length > 0 ? locationArr[locationArr.length-1] : null;
                    userRepresentation = realmResource.users().get(userId).toRepresentation();
                }
            }

            if (passwordSecret != null || (!isUpdate && user.isServiceAccount())) {
                if (user.isServiceAccount()) {
                    resetSecret(realm, userRepresentation.getId(), passwordSecret);
                } else {
                    Credential credential = new Credential(passwordSecret, false);
                    resetPassword(realm, userRepresentation.getId(), credential);
                }
            }

            User updatedUser = convert(userRepresentation, User.class);
            if (updatedUser != null) {
                updatedUser.setRealm(realm);
                if (updatedUser.isServiceAccount()) {
                    updatedUser.setSecret(passwordSecret);
                }

                if (existingUser != null) {
                    // Push realm ID into updated user
                    updatedUser.setRealmId(existingUser.getRealmId());
                }
            }

            persistenceService.publishPersistenceEvent(
                (isUpdate ? PersistenceEvent.Cause.UPDATE : PersistenceEvent.Cause.CREATE),
                updatedUser,
                existingUser,
                User.class,
                Collections.singletonList("attributes"),
                null);

            return updatedUser;
        });
    }

    @Override
    public void deleteUser(String realm, String userId) {

        User user = getUser(userId);

        if (user == null) {
            return;
        }

        if (user.getUsername().equals(MASTER_REALM_ADMIN_USER) && user.getRealm().equals(MASTER_REALM)) {
            throw new IllegalStateException("Cannot delete master realm admin user");
        }

        getRealms(realmsResource -> {

            if (user.isServiceAccount()) {
                // Delete the client
                deleteClient(realm, user.getUsername());
            } else {
                Response response = realmsResource.realm(realm).users().delete(userId);
                response.close();
                if (!response.getStatusInfo().equals(Response.Status.NO_CONTENT)) {
                    throw new IllegalStateException("Failed to delete user: " + userId);
                }
            }
            return null;
        });

        persistenceService.publishPersistenceEvent(PersistenceEvent.Cause.DELETE,
            null,
            user,
            User.class,
            Collections.singletonList("attributes"),
            null);
    }

    @Override
    public void resetPassword(String realm, String userId, Credential credential) {
        getRealms(realmsResource -> {
            realmsResource.realm(realm).users().get(userId).resetPassword(
                convert(credential, CredentialRepresentation.class)
            );
            return null;
        });
    }

    @Override
    public String resetSecret(String realm, String userId, String secret) {
        return getRealms(realmsResource -> {
            UserRepresentation userRepresentation = null;
            try {
                userRepresentation = realmsResource.realm(realm).users().get(userId).toRepresentation();
            } catch (Exception ignored) {
            }
            if (userRepresentation == null) {
                return null;
            }

            return withClientResource(
                realm,
                userRepresentation.getUsername().substring(User.SERVICE_ACCOUNT_PREFIX.length()),
                realmsResource,
                (clientRep, clientResource) -> {
                    if (TextUtil.isNullOrEmpty(secret)) {
                        CredentialRepresentation credentialRepresentation = clientResource.generateNewSecret();
                        return credentialRepresentation.getValue();
                    } else {
                        clientRep.setSecret(secret);
                        clientResource.update(clientRep);
                        return secret;
                    }
                },
                null
            );
        });
    }

    @Override
    public Role[] getRoles(String realm, String client) {
        return getRealms(realmsResource -> {
            RealmResource realmResource = realmsResource.realm(realm);
            ClientsResource clientsResource = realmResource.clients();
            ClientResource clientResource = null;

            if (client != null) {
                ClientRepresentation clientRepresentation = getClient(realm, client);

                if (clientRepresentation == null) {
                    throw new IllegalStateException("Cannot find specified client: " + client);
                }
                clientResource = clientsResource.get(clientRepresentation.getId());
            }

            List roleRepresentations = clientResource != null ? clientResource.roles().list() : realmResource.roles().list();
            List roles = new ArrayList<>();

            for (RoleRepresentation clientRole : roleRepresentations) {
                String[] composites = clientRole.isComposite() ? realmResource.rolesById().getRoleComposites(clientRole.getId()).stream().map(RoleRepresentation::getId).toArray(String[]::new) : null;
                roles.add(new Role(clientRole.getId(), clientRole.getName(), clientRole.isComposite(), null, composites).setDescription(clientRole.getDescription()));
            }

            return roles.toArray(new Role[0]);
        });
    }

    @Override
    public void updateClientRoles(String realm, String clientId, Role[] roles) {

        getRealms(realmsResource -> {
            RealmResource realmResource = realmsResource.realm(realm);
            ClientsResource clientsResource = realmResource.clients();
            ClientRepresentation clientRepresentation = getClient(realm, clientId);
            if (clientRepresentation == null) {
                throw new IllegalStateException("Cannot find specified client: " + clientId);
            }
            ClientResource clientResource = clientsResource.get(clientRepresentation.getId());
            List existingRoles = new ArrayList<>(clientResource.roles().list());

            List removedRoles = existingRoles.stream()
                .filter(existingRole -> Arrays.stream(roles).noneMatch(r -> existingRole.getId().equals(r.getId())))
                .collect(Collectors.toList());

            removedRoles.forEach(removedRole -> {
                realmResource.rolesById().deleteRole(removedRole.getId());
                existingRoles.remove(removedRole);
            });

            Arrays.stream(roles).forEach(role -> {

                RoleRepresentation existingRole;
                boolean compositesModified = false;
                Set existingComposites = new HashSet<>();
                Set requestedComposites = new HashSet<>();

                if (role.getId() == null) {
                    existingRole = saveClientRole(realmResource, clientResource, role, null);
                    existingRoles.add(existingRole);
                    compositesModified = role.getCompositeRoleIds() != null && role.getCompositeRoleIds().length > 0;
                    if (compositesModified) {
                        requestedComposites.addAll(Arrays.stream(role.getCompositeRoleIds())
                            .map(id -> existingRoles.stream().filter(er -> er.getId().equals(id)).findFirst().orElse(null))
                            .filter(Objects::nonNull)
                            .collect(Collectors.toSet()));
                    }
                } else {
                    existingRole = existingRoles.stream().filter(r -> r.getId().equals(role.getId())).findFirst().orElseThrow(() -> new IllegalStateException("One or more supplied roles have an ID that doesn't exist"));

                    boolean isComposite = role.isComposite() && role.getCompositeRoleIds() != null && role.getCompositeRoleIds().length > 0;

                    boolean rolePropertiesModified = !Objects.equals(existingRole.getName(), role.getName())
                        || !Objects.equals(existingRole.getDescription(), role.getDescription());

                    if (isComposite || existingRole.isComposite()) {
                        existingComposites.addAll(Optional.ofNullable(realmResource.rolesById().getClientRoleComposites(existingRole.getId(), clientRepresentation.getId())).orElse(new HashSet<>()));
                        requestedComposites.addAll(Arrays.stream(role.getCompositeRoleIds())
                            .map(id -> existingRoles.stream().filter(er -> er.getId().equals(id)).findFirst().orElse(null))
                            .filter(Objects::nonNull)
                            .collect(Collectors.toSet()));

                        if (requestedComposites.size() != role.getCompositeRoleIds().length) {
                            throw new IllegalStateException("One or more composite roles contain an invalid role ID");
                        }

                        compositesModified = !Objects.equals(existingComposites, requestedComposites);
                    }

                    if (rolePropertiesModified) {
                        // Merge the role property changes
                        saveClientRole(realmResource, clientResource, role, existingRole);
                    }
                }

                if (compositesModified) {
                    List removed = existingComposites.stream().filter(existing -> !requestedComposites.contains(existing)).collect(Collectors.toList());
                    List added = requestedComposites.stream().filter(existing -> !existingComposites.contains(existing)).collect(Collectors.toList());
                    if (!removed.isEmpty()) {
                        realmResource.rolesById().deleteComposites(existingRole.getId(), removed);
                    }
                    if (!added.isEmpty()) {
                        realmResource.rolesById().addComposites(existingRole.getId(), added);
                    }
                }
            });

            return null;
        });
    }

    protected RoleRepresentation saveClientRole(RealmResource realmResource, ClientResource clientResource, Role role, RoleRepresentation representation) {
        if (representation == null) {
            representation = new RoleRepresentation();
        }
        representation.setName(role.getName());
        representation.setDescription(role.getDescription());
        representation.setClientRole(true);
        if (representation.getId() == null) {
            clientResource.roles().create(representation);
        } else {
            realmResource.rolesById().updateRole(representation.getId(), representation);
        }

        return clientResource.roles().get(representation.getName()).toRepresentation();
    }

    @Override
    public Role[] getUserRoles(String realm, String userId, String client) {
        return getRealms(realmsResource -> {
            RealmResource realmResource = realmsResource.realm(realm);
            RoleMappingResource roleMappingResource = realmResource.users().get(userId).roles();

            return withClientResource(realm, client, realmsResource, (clientRepresentation, clientResource) -> {
                RolesResource rolesResource = clientResource.roles();
                List allRoles = rolesResource.list();
                List effectiveRoles = roleMappingResource.clientLevel(clientRepresentation.getId()).listEffective();

                List roles = new ArrayList<>();
                for (RoleRepresentation roleRepresentation : allRoles) {
                    boolean isAssigned = false;

                    for (RoleRepresentation effectiveRole : effectiveRoles) {
                        if (effectiveRole.getId().equals(roleRepresentation.getId()))
                            isAssigned = true;
                    }

                    roles.add(new Role(
                        roleRepresentation.getId(),
                        roleRepresentation.getName(),
                        roleRepresentation.isComposite(),
                        isAssigned,
                        null)
                        .setDescription(roleRepresentation.getDescription()));
                }

                return roles.toArray(new Role[0]);
            }, () -> new Role[0]);
        });
    }

    @Override
    public Role[] getUserRealmRoles(String realm, String userId) {
        return getRealms(realmsResource -> {
            RealmResource realmResource = realmsResource.realm(realm);
            RoleMappingResource roleMappingResource = realmResource.users().get(userId).roles();

                RolesResource rolesResource = realmResource.roles();
                List allRoles = rolesResource.list();
                List effectiveRoles = roleMappingResource.realmLevel().listEffective();

                List roles = new ArrayList<>();
                for (RoleRepresentation roleRepresentation : allRoles) {
                    boolean isAssigned = false;

                    for (RoleRepresentation effectiveRole : effectiveRoles) {
                        if (effectiveRole.getId().equals(roleRepresentation.getId()))
                            isAssigned = true;
                    }

                    roles.add(new Role(
                            roleRepresentation.getId(),
                            roleRepresentation.getName(),
                            roleRepresentation.isComposite(),
                            isAssigned,
                            null)
                            .setDescription(roleRepresentation.getDescription()));
                }

                return roles.toArray(new Role[0]);
        });
    }

    @Override
    public void updateUserRoles(@NotNull String realm, @NotNull String userId, @NotNull String client, String...roles) {
        getRealms(realmsResource -> {
            RealmResource realmResource = realmsResource.realm(realm);
            UserRepresentation user = realmResource.users().get(userId).toRepresentation();

            if (user == null) {
                throw new IllegalStateException("Multiple users with the same username found");
            }

            RoleMappingResource roleMappingResource = realmResource.users().get(user.getId()).roles();
            ClientRepresentation clientRepresentation = getClient(realm, client);

            if (clientRepresentation == null) {
                throw new IllegalStateException("Invalid client: " + client);
            }

            ClientResource clientResource = realmResource.clients().get(clientRepresentation.getId());

            // Get all roles
            List existingRoles = roleMappingResource.clientLevel(clientRepresentation.getId()).listAll();
            List availableRoles = clientResource.roles().list();
            List requestedRoles = availableRoles.stream().filter(role -> Arrays.stream(roles).anyMatch(name -> role.getName().equals(name))).collect(Collectors.toList());

            // Strip out requested roles that are already in a requested composite role
            List removeRequestedRoles = requestedRoles.stream()
                .filter(RoleRepresentation::isComposite)
                .flatMap(role ->
                    realmResource.rolesById().getRoleComposites(role.getId()).stream().map(RoleRepresentation::getId)
                ).collect(Collectors.toList());

            requestedRoles = requestedRoles.stream()
                .filter(role -> removeRequestedRoles.stream().noneMatch(id -> id.equals(role.getId())))
                .collect(Collectors.toList());


            // Get newly defined roles
            List addRoles = requestedRoles.isEmpty() ? Collections.emptyList() : requestedRoles.stream()
                .filter(requestedRole -> existingRoles.stream().noneMatch(r -> r.getId().equals(requestedRole.getId())))
                .collect(Collectors.toList());

            // Remove obsolete roles
            List finalRequestedRoles = requestedRoles;
            List removeRoles = requestedRoles.isEmpty() ? existingRoles : existingRoles.stream()
                .filter(r -> finalRequestedRoles.stream().noneMatch(requestedRole -> requestedRole.getId().equals(r.getId())))
                .collect(Collectors.toList());

            if (!removeRoles.isEmpty()) {
                roleMappingResource.clientLevel(clientRepresentation.getId()).remove(removeRoles);
            }
            if (!addRoles.isEmpty()) {
                roleMappingResource.clientLevel(clientRepresentation.getId()).add(addRoles);
            }

            return null;
        });
    }

    @Override
    public void updateUserRealmRoles(String realm, String userId, String... roles) {
        getRealms(realmsResource -> {
            RealmResource realmResource = realmsResource.realm(realm);
            UserRepresentation user = realmResource.users().get(userId).toRepresentation();

            if (user == null) {
                throw new IllegalStateException("Multiple users with the same username found");
            }

            RoleMappingResource roleMappingResource = realmResource.users().get(user.getId()).roles();

            // Get all roles
            List existingRoles = roleMappingResource.realmLevel().listAll();
            List availableRoles = realmResource.roles().list();
            List requestedRoles = availableRoles.stream().filter(role -> Arrays.stream(roles).anyMatch(name -> role.getName().equals(name))).collect(Collectors.toList());

            // Strip out requested roles that are already in a requested composite role
            List removeRequestedRoles = requestedRoles.stream()
                    .filter(RoleRepresentation::isComposite)
                    .flatMap(role ->
                            realmResource.rolesById().getRoleComposites(role.getId()).stream().map(RoleRepresentation::getId)
                    ).collect(Collectors.toList());

            requestedRoles = requestedRoles.stream()
                    .filter(role -> removeRequestedRoles.stream().noneMatch(id -> id.equals(role.getId())))
                    .collect(Collectors.toList());


            // Get newly defined roles
            List addRoles = requestedRoles.isEmpty() ? Collections.emptyList() : requestedRoles.stream()
                    .filter(requestedRole -> existingRoles.stream().noneMatch(r -> r.getId().equals(requestedRole.getId())))
                    .collect(Collectors.toList());

            // Remove obsolete roles
            List finalRequestedRoles = requestedRoles;
            List removeRoles = requestedRoles.isEmpty() ? existingRoles : existingRoles.stream()
                    .filter(r -> finalRequestedRoles.stream().noneMatch(requestedRole -> requestedRole.getId().equals(r.getId())))
                    .collect(Collectors.toList());

            if (!removeRoles.isEmpty()) {
                roleMappingResource.realmLevel().remove(removeRoles);
            }
            if (!addRoles.isEmpty()) {
                roleMappingResource.realmLevel().add(addRoles);
            }

            return null;
        });
    }

    @Override
    public boolean isMasterRealmAdmin(String userId) {
        Optional adminUser = getRealms(realmsResource ->
            realmsResource.realm(MASTER_REALM)
                .users()
                .search(MASTER_REALM_ADMIN_USER, null, null))
            .stream()
            .filter(user -> user.getUsername().equals(MASTER_REALM_ADMIN_USER))
            .findFirst();

        if (!adminUser.isPresent()) {
            throw new IllegalStateException("Can't load master realm admin user");
        }
        return adminUser.map(UserRepresentation::getId).map(id -> id.equals(userId)).orElse(false);
    }

    @Override
    public Realm[] getRealms() {
        return ManagerIdentityProvider.getRealmsFromDb(persistenceService);
    }

    @Override
    public Realm getRealm(String realm) {
        // This gets hit a lot for event authorisation so caching added
        return realmCache.computeIfAbsent(realm, (name) -> {
            try {
                return ManagerIdentityProvider.getRealmFromDb(persistenceService, realm);
            } catch (Exception ex) {
                LOG.log(Level.INFO, "Failed to get realm by name: " + realm, ex);
            }
            return null;
        });
    }

    @Override
    public void updateRealm(Realm realm) {
        LOG.fine("Update realm: " + realm);
        realmCache.remove(realm.getName());
        getRealms(realmsResource -> {

            if (TextUtil.isNullOrEmpty(realm.getId())) {
                throw new IllegalStateException("Realm must already exist, ID does not match an existing realm");
            }

            // Force realm to lowercase
            realm.setName(realm.getName().toLowerCase(Locale.ROOT));

            // Find existing realm by ID as realm name could have been changed
            RealmRepresentation realmRepresentation = realmsResource.findAll().stream().filter(r -> r.getId().equals(realm.getId())).findFirst().orElse(null);

            if (realmRepresentation == null) {
                throw new IllegalStateException("Realm must already exist, ID does not match an existing realm");
            }

            String realmName = realmRepresentation.getRealm();
            RealmResource realmResource = realmsResource.realm(realmName);
            Realm existingRealm = getRealm(realmName);

            // Realm only has a subset of realm representation so overlay on actual realm representation
            realmRepresentation.setDisplayName(realm.getDisplayName());
            realmRepresentation.setAccountTheme(realm.getAccountTheme());
            realmRepresentation.setAdminTheme(realm.getAdminTheme());
            realmRepresentation.setEmailTheme(realm.getEmailTheme());
            realmRepresentation.setLoginTheme(realm.getLoginTheme());
            realmRepresentation.setRememberMe(realm.getRememberMe());
            realmRepresentation.setVerifyEmail(realm.getVerifyEmail());
            realmRepresentation.setLoginWithEmailAllowed(realm.getLoginWithEmail());
            realmRepresentation.setRegistrationAllowed(realm.getRegistrationAllowed());
            realmRepresentation.setRegistrationEmailAsUsername(realm.getRegistrationEmailAsUsername());
            realmRepresentation.setEnabled(realm.getEnabled());
            realmRepresentation.setDuplicateEmailsAllowed(realm.getDuplicateEmailsAllowed());
            realmRepresentation.setResetPasswordAllowed(realm.getResetPasswordAllowed());
            realmRepresentation.setPasswordPolicy(realm.getPasswordPolicyString());
            realmRepresentation.setNotBefore(realm.getNotBefore() != null ? realm.getNotBefore().intValue() : null);
            configureRealm(realmRepresentation);
            realmResource.update(realmRepresentation);

            Set existingRealmRoles = existingRealm.getRealmRoles();
            existingRealm.setRealmRoles(existingRealmRoles);

            // Update realm roles if required
            if (realm.getRealmRoles() != null) {

                Set realmRoles = realm.getNormalisedRealmRoles();
                RolesResource rolesResource = realmResource.roles();

                // Handle removed roles
                existingRealmRoles.stream().filter(realmRole -> !realmRoles.contains(realmRole)).forEach(realmRole -> {
                    LOG.finest("Removing realm role + " + realmRole);
                    rolesResource.deleteRole(realmRole.getName());
                });
                // Handle added roles
                realmRoles.stream().filter(realmRole -> !existingRealmRoles.contains(realmRole)).forEach(realmRole -> {
                    LOG.finest("Adding realm role + " + realmRole);
                    rolesResource.create(new RoleRepresentation(realmRole.getName(), realmRole.getDescription(), false));
                });
            }

            Realm updatedRealm = convert(realmRepresentation, Realm.class);
            updatedRealm.setName(realmRepresentation.getRealm());
            updatedRealm.setRealmRoles((realm.getRealmRoles() == null) ? existingRealmRoles : realm.getNormalisedRealmRoles());
            persistenceService.publishPersistenceEvent(PersistenceEvent.Cause.UPDATE, updatedRealm, existingRealm, Realm.class, null, null);
            return null;
        });
    }

    @Override
    public Realm createRealm(Realm realm) {
        LOG.fine("Create realm: " + realm);
        return getRealms(realmsResource -> {

            // Force realm to lowercase
            realm.setName(realm.getName().toLowerCase(Locale.ROOT));

            RealmRepresentation realmRepresentation = convert(realm, RealmRepresentation.class);
            // Inject name as it is called realm in the realmRepresentation
            realmRepresentation.setRealm(realm.getName());

            try {
                realmsResource.create(realmRepresentation);
                RealmResource realmResource = realmsResource.realm(realm.getName());
                realmRepresentation = realmResource.toRepresentation();

                // Need a committed realmRepresentation to update the security
                configureRealm(realmRepresentation);
                realmResource.update(realmRepresentation);

                // Set realm roles
                RolesResource rolesResource = realmResource.roles();
                List existingRealmRoles = rolesResource.list();
                realm.getNormalisedRealmRoles().stream().filter(realmRole -> existingRealmRoles.stream().noneMatch(roleRepresentation -> roleRepresentation.getName().equals(realmRole.getName())))
                .forEach(realmRole -> {
                    LOG.finest("Adding realm role + " + realmRole);
                    rolesResource.create(new RoleRepresentation(realmRole.getName(), realmRole.getDescription(), false));
                });

                // Auto create the standard openremote client
                ClientRepresentation clientRepresentation = generateOpenRemoteClientRepresentation();
                createUpdateClient(realm.getName(), clientRepresentation);

                Realm createdRealm = convert(realmRepresentation, Realm.class);
                createdRealm.setName(realmRepresentation.getRealm());
                createdRealm.setRealmRoles(realm.getRealmRoles());
                persistenceService.publishPersistenceEvent(PersistenceEvent.Cause.CREATE, realm, null, Realm.class, null, null);
                return createdRealm;
            } catch (Exception e) {
                LOG.log(Level.INFO, "Failed to create realm: " + realm, e);
                throw e;
            }
        });
    }

    @Override
    public void deleteRealm(String realmName) {
        Realm realm = getRealm(realmName);

        if (realm == null) {
            throw new NotFoundException("Realm does not exist: " + realmName);
        }

        realmCache.remove(realmName);
        persistenceService.doTransaction(entityManager -> {

            // Delete gateway connections
            Query query = entityManager.createQuery("delete from " + GatewayConnection.class.getSimpleName() + " gc " +
                "where gc.localRealm = ?1");

            query.setParameter(1, realmName);
            query.executeUpdate();

            // Delete provisioning configs
            query = entityManager.createQuery("delete from " + ProvisioningConfig.class.getSimpleName() + " pc " +
                "where pc.realm = ?1");

            query.setParameter(1, realmName);
            query.executeUpdate();

            // Delete Rules
            query = entityManager.createQuery("delete from " + RealmRuleset.class.getSimpleName() + " rs " +
                "where rs.realm = ?1");
            query.setParameter(1, realmName);
            query.executeUpdate();

            // Delete Assets
            List assetIds = assetStorageService.findAll(new AssetQuery().select(new AssetQuery.Select().excludeAttributes()).realm(new RealmPredicate(realmName))).stream().map(Asset::getId).toList();
            assetStorageService.delete(assetIds);
        });

        LOG.fine("Deleting realm: " + realmName);
        getRealms(realmsResource -> {
            realmsResource.realm(realmName).remove();
            return null;
        });
        persistenceService.publishPersistenceEvent(PersistenceEvent.Cause.DELETE, null, realm, Realm.class, null, null);
    }

    public ClientRepresentation generateOpenRemoteClientRepresentation() {
        ClientRepresentation client = new ClientRepresentation();
        client.setClientId(KEYCLOAK_CLIENT_ID);
        client.setName("OpenRemote");
        client.setPublicClient(true);

        boolean enableDirectAccessGrant = getBoolean(container.getConfig(), OR_KEYCLOAK_ENABLE_DIRECT_ACCESS_GRANT, container.isDevMode());

        if (enableDirectAccessGrant) {
            // We need direct access for integration/load tests
            LOG.info("### Allowing direct access grants for client id '" + client.getClientId() + "', this must NOT be used in production! ###");
            client.setDirectAccessGrantsEnabled(true);
        }

        if (container.isDevMode()) {
            // Allow any web origin (this will add CORS headers to token requests etc.)
            client.setWebOrigins(Collections.singletonList("*"));
            client.setRedirectUris(Collections.singletonList("*"));
        } else {
            client.setWebOrigins(Collections.singletonList("+"));
            client.setRedirectUris(validRedirectUris);
        }

        return client;
    }

    // TODO: Provide an implementation agnostic client
    public ClientRepresentation getClient(String realm, String client) {
        return getRealms(realmsResource ->
            withClientResource(realm, client, realmsResource, (clientRepresentation, clientResource) ->
                clientRepresentation, null));
    }

    // TODO: Provide an implementation agnostic client
    public ClientRepresentation[] getClients(String realm) {
        return getRealms(realmsResource -> realmsResource.realm(realm).clients().findAll().toArray(new ClientRepresentation[0]));
    }

    // TODO: Provide an implementation agnostic client
    public ClientRepresentation createUpdateClient(String realm, ClientRepresentation client) {

        if (client == null || client.getClientId() == null) {
            throw new IllegalArgumentException("Client is null or clientId is missing");
        }

        return getRealms(realmsResource ->
            withClientResource(realm, client.getClientId(), realmsResource, (clientRepresentation, clientResource) -> {
                clientResource.update(client);
                return client;
            },
        () -> {
            ClientsResource clientsResource = realmsResource.realm(realm).clients();
            Response response = clientsResource.create(client);
            response.close();
            if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
                LOG.fine("Failed to create client response=" + response.getStatusInfo().getStatusCode() + ": " + client);
                return null;
            }

            ClientRepresentation newClient = clientsResource.findByClientId(client.getClientId()).get(0);
            ClientResource clientResource = clientsResource.get(newClient.getId());
            addDefaultRoles(clientResource.roles());
            return newClient;
        }));
    }

    public void deleteClient(String realm, String clientId) {

        if (TextUtil.isNullOrEmpty(realm)
            || TextUtil.isNullOrEmpty(clientId)) {
            throw new IllegalArgumentException("Invalid client credentials realm and client ID must be specified");
        }

        getRealms(realmsResource -> {
            RealmResource realmResource = realmsResource.realm(realm);

            if (realmResource == null) {
                LOG.fine("Invalid realm provided for deleteClient call: " + realm);
                return null;
            }

            LOG.fine("Deleting client: realm=" + realm + ", client ID=" + clientId);
            return withClientResource(realm, clientId, realmsResource, (clientRepresentation, clientResource) -> {
                clientResource.remove();
                return null;
            }, () -> {
                // Do nothing as could have been deleted by cleanup of previous test
                return null;
            });
        });
    }

    /**
     * @return true if the user is the superuser (admin) or if the user is authenticated
     * in the same realm as the realm and the realm is active.
     */
    @Override
    public boolean isRealmActiveAndAccessible(AuthContext authContext, Realm realm) {
        if (realm == null) {
            return false;
        }

        boolean isSuperUser = authContext != null && authContext.isSuperUser();
        boolean isUsersRealm = isSuperUser || authContext == null || authContext.isRealmAccessibleByUser(realm.getName());

        return isSuperUser || (isUsersRealm && realm.isActive(timerService.getCurrentTimeMillis()));
    }

    /**
     * @return true if the user is the superuser (admin) or if the user is authenticated
     * in the same realm and the realm is active.
     */
    @Override
    public boolean isRealmActiveAndAccessible(AuthContext authContext, String realm) {
        return isRealmActiveAndAccessible(authContext, getRealm(realm));
    }

    @Override
    public boolean realmExists(String realm) {
        return ManagerIdentityProvider.realmExistsFromDb(persistenceService, realm);
    }

    @Override
    public boolean isRestrictedUser(AuthContext authContext) {
        return authContext != null && authContext.hasRealmRole(RESTRICTED_USER_REALM_ROLE);
    }

    @Override
    public boolean isUserInRealm(String userId, String realm) {
        return ManagerIdentityProvider.userInRealmFromDb(persistenceService, userId, realm);
    }

    @Override
    public boolean canSubscribeWith(AuthContext auth, RealmFilter filter, ClientRole... requiredRoles) {
        // Superuser can always subscribe
        if (auth.isSuperUser())
            return true;

        // Restricted users get nothing
        if (isRestrictedUser(auth))
            return false;

        // User must have role
        if (requiredRoles != null) {
            for (ClientRole requiredRole : requiredRoles) {
                if (!auth.hasResourceRole(requiredRole.getValue(), Constants.KEYCLOAK_CLIENT_ID)) {
                    return false;
                }
            }
        }

        // Ensure filter matches authenticated realm
        if (filter != null) {
            String authenticatedRealm = auth.getAuthenticatedRealmName();

            if (TextUtil.isNullOrEmpty(authenticatedRealm))
                return false;
            if (authenticatedRealm.equals(filter.getName()))
                return true;
        }

        return false;
    }

    @Override
    public String getFrontendURI() {
        return frontendURI;
    }

    protected void configureRealm(RealmRepresentation realmRepresentation) {

        realmRepresentation.setAccessTokenLifespan(Constants.ACCESS_TOKEN_LIFESPAN_SECONDS);

        String themeName = getString(container.getConfig(), realmRepresentation.getRealm().toUpperCase(Locale.ROOT) + REALM_KEYCLOAK_THEME_SUFFIX, getString(container.getConfig(), DEFAULT_REALM_KEYCLOAK_THEME, DEFAULT_REALM_KEYCLOAK_THEME_DEFAULT));

        if(TextUtil.isNullOrEmpty(realmRepresentation.getLoginTheme())) {
            realmRepresentation.setLoginTheme(themeName);
        }
        if(TextUtil.isNullOrEmpty(realmRepresentation.getAccountTheme())) {
            realmRepresentation.setAccountTheme(themeName);
        }
        if(TextUtil.isNullOrEmpty(realmRepresentation.getEmailTheme())) {
            realmRepresentation.setEmailTheme(themeName);
        }

        realmRepresentation.setDisplayNameHtml(
            realmRepresentation.getDisplayName().replaceAll("[^A-Za-z0-9]", "")
        );
        realmRepresentation.setSsoSessionIdleTimeout(sessionTimeoutSeconds);
        realmRepresentation.setSsoSessionMaxLifespan(sessionMaxSeconds);
        realmRepresentation.setOfflineSessionIdleTimeout(sessionOfflineTimeoutSeconds);

        // Service-internal network (between manager and keycloak service containers) does not use SSL
        realmRepresentation.setSslRequired(SslRequired.NONE.toString());

        // Configure SMTP
        String host = container.getConfig().getOrDefault(OR_EMAIL_HOST, null);
        if (!TextUtil.isNullOrEmpty(host) && (realmRepresentation.getSmtpServer() == null || realmRepresentation.getSmtpServer().isEmpty())) {
            LOG.info("Configuring Keycloak SMTP settings for realm: " + realmRepresentation.getRealm());
            Map emailConfig = new HashMap<>();
            emailConfig.put("host", host);
            emailConfig.put("port", container.getConfig().getOrDefault(OR_EMAIL_PORT, Integer.toString(OR_EMAIL_PORT_DEFAULT)));
            emailConfig.put("user", container.getConfig().getOrDefault(OR_EMAIL_USER, null));
            emailConfig.put("password", container.getConfig().getOrDefault(OR_EMAIL_PASSWORD, null));
            emailConfig.put("auth", container.getConfig().containsKey(OR_EMAIL_USER) ? "true" : "false");
            emailConfig.put("starttls", Boolean.toString(getBoolean(container.getConfig(), OR_EMAIL_TLS, OR_EMAIL_TLS_DEFAULT)));
            emailConfig.put("ssl", Boolean.toString(!getBoolean(container.getConfig(), OR_EMAIL_TLS, OR_EMAIL_TLS_DEFAULT) && getString(container.getConfig(), OR_EMAIL_PROTOCOL, OR_EMAIL_PROTOCOL_DEFAULT).equals("smtps")));
            emailConfig.put("from", getString(container.getConfig(), OR_EMAIL_FROM, OR_EMAIL_FROM_DEFAULT));
            realmRepresentation.setSmtpServer(emailConfig);
        }

        // Configure CSP header
        Map headers = realmRepresentation.getBrowserSecurityHeaders();
        if (headers == null) {
            headers = new HashMap<>();
            realmRepresentation.setBrowserSecurityHeaders(headers);
        }

        if (container.isDevMode()) {
                headers.computeIfPresent("contentSecurityPolicy", (hdrName, hdrValue) -> "frame-src *; frame-ancestors *; object-src 'none'");
        } else {
            String allowedOriginsStr = String.join(" ", WebService.getAllowedOrigins(container));
            if (!TextUtil.isNullOrEmpty(allowedOriginsStr)) {
                headers.compute("contentSecurityPolicy", (hdrName, hdrValue) ->
                        "frame-src 'self' " +
                        allowedOriginsStr.replace(';', ' ') +
                        "; frame-ancestors 'self' " +
                        allowedOriginsStr.replace(';', ' ') +
                        "; object-src 'none'");
            }
        }
    }

    protected void addDefaultRoles(RolesResource rolesResource) {

        for (ClientRole clientRole : ClientRole.values()) {
            rolesResource.create(clientRole.getRepresentation());
        }

        for (ClientRole clientRole : ClientRole.values()) {
            if (clientRole.getComposites() == null)
                continue;
            List composites = new ArrayList<>();
            for (ClientRole composite : clientRole.getComposites()) {
                composites.add(rolesResource.get(composite.getValue()).toRepresentation());
            }
            rolesResource.get(clientRole.getValue()).addComposites(composites);
        }
    }

    public String addLDAPConfiguration(String realm, ComponentRepresentation componentRepresentation) {

        return getRealms(realmsResource -> {
            RealmResource realmResource = realmsResource.realm(realm);
            Response response = realmResource.components().add(componentRepresentation);
            response.close();

            if (!response.getStatusInfo().equals(Response.Status.CREATED)) {
                throw new IllegalStateException("Failed to add LDAP configuration");
            } else {
                ComponentRepresentation newComponentRepresentation = realmResource.components()
                    .query(componentRepresentation.getParentId(),
                        componentRepresentation.getProviderType(),
                        componentRepresentation.getName()).get(0);
                syncUsers(newComponentRepresentation.getId(), realm, "triggerFullSync");
                return newComponentRepresentation.getId();
            }
        });
    }

    public String addLDAPMapper(String realm, ComponentRepresentation componentRepresentation) {
        return getRealms(realmsResource -> {
            RealmResource realmResource = realmsResource.realm(realm);
            Response response = realmResource.components().add(componentRepresentation);
            response.close();

            if (!response.getStatusInfo().equals(Response.Status.CREATED)) {
                throw new IllegalStateException("Failed to add LDAP mapper");
            } else {
                ComponentRepresentation newComponentRepresentation = realmResource.components()
                    .query(componentRepresentation.getParentId(),
                        componentRepresentation.getProviderType(),
                        componentRepresentation.getName()).get(0);
                realmResource.userStorage().syncMapperData(newComponentRepresentation.getParentId(), newComponentRepresentation.getId(), "fedToKeycloak");
                return newComponentRepresentation.getId();
            }
        });
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "{}";
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy