MigrateTo24_0_0.java

/*
 * Copyright 2023 Red Hat, Inc. and/or its affiliates
 *  and other contributors as indicated by the @author tags.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package org.keycloak.migration.migrators;

import org.jboss.logging.Logger;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.DefaultKeyProviders;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.userprofile.UserProfileProvider;

public class MigrateTo24_0_0 implements Migration {

    private static final Logger LOG = Logger.getLogger(MigrateTo24_0_0.class);
    public static final ModelVersion VERSION = new ModelVersion("24.0.0");
    public static final String REALM_USER_PROFILE_ENABLED = "userProfileEnabled";

    @Override
    public void migrate(KeycloakSession session) {
        session.realms().getRealmsStream().forEach(realm -> migrateRealm(session, realm));
    }

    @Override
    public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
        migrateRealm(session, realm);
    }

    @Override
    public ModelVersion getVersion() {
        return VERSION;
    }

    private void migrateRealm(KeycloakSession session, RealmModel realm) {
        KeycloakContext context = session.getContext();
        try {
            context.setRealm(realm);
            updateUserProfileSettings(session);
            updateLdapProviderConfig(session);
            createHS512ComponentModelKey(session);
            bindFirstBrokerLoginFlow(session);
        } finally {
            context.setRealm(null);
        }
    }

    private void updateUserProfileSettings(KeycloakSession session) {
        RealmModel realm = session.getContext().getRealm();
        boolean isUserProfileEnabled = Boolean.parseBoolean(realm.getAttribute(REALM_USER_PROFILE_ENABLED));

        // Remove attribute as user profile is always enabled from this version
        realm.removeAttribute(REALM_USER_PROFILE_ENABLED);

        if (isUserProfileEnabled) {
            // existing realms with user profile enabled does not need any addition migration step
            LOG.debugf("Skipping migration for realm %s. The declarative user profile is already enabled.", realm.getName());
            return;
        }

        // for backward compatibility in terms of behavior, we enable unmanaged attributes for existing realms
        // that don't have the declarative user profile enabled
        UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
        UPConfig upConfig = provider.getConfiguration();
        upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
        provider.setConfiguration(upConfig);

        LOG.debugf("Enabled the declarative user profile to realm %s with support for unmanaged attributes", realm.getName());
    }

    private void updateLdapProviderConfig(final KeycloakSession session) {
        RealmModel realm = session.getContext().getRealm();
        // ensure `ldapsOnly` value for `useTruststoreSpi` in LDAP providers is migrated to `always`.
        realm.getComponentsStream(realm.getId(), UserStorageProvider.class.getName())
                .filter(c -> LDAPConstants.USE_TRUSTSTORE_LDAPS_ONLY.equals(c.getConfig().getFirst(LDAPConstants.USE_TRUSTSTORE_SPI)))
                .forEach(c -> {
                    c.getConfig().putSingle(LDAPConstants.USE_TRUSTSTORE_SPI, LDAPConstants.USE_TRUSTSTORE_ALWAYS);
                    realm.updateComponent(c);
                });
    }

    private void createHS512ComponentModelKey(KeycloakSession session) {
        RealmModel realm = session.getContext().getRealm();
        DefaultKeyProviders.createSecretProvider(realm);
    }

    private void bindFirstBrokerLoginFlow(KeycloakSession session) {
        RealmModel realm = session.getContext().getRealm();
        String flowAlias = DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW;
        AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias);
        if (flow == null) {
           LOG.debugf("No flow found for alias '%s'. Skipping.", flowAlias);
           return;
        }
        realm.setFirstBrokerLoginFlow(flow);
        LOG.debugf("Flow '%s' has been bound to realm %s as 'First broker login' flow", flow.getId(), realm.getName());
    }
}