diff --git a/model/legacy-private/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/model/legacy-private/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index 255850f2c7..0f7bdc6efd 100755 --- a/model/legacy-private/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/model/legacy-private/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -75,7 +75,7 @@ public class ExportUtils { public static RealmRepresentation exportRealm(KeycloakSession session, RealmModel realm, ExportOptions options, boolean internal) { RealmRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, internal); - ModelToRepresentation.exportAuthenticationFlows(realm, rep); + ModelToRepresentation.exportAuthenticationFlows(session, realm, rep); ModelToRepresentation.exportRequiredActions(realm, rep); // Project/product version diff --git a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java index ac6d598636..721e9cbe42 100644 --- a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java @@ -21,6 +21,7 @@ import org.jboss.logging.Logger; import org.keycloak.common.enums.SslRequired; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; +import org.keycloak.deployment.DeployedConfigurationsManager; import org.keycloak.exportimport.ExportAdapter; import org.keycloak.exportimport.ExportOptions; import org.keycloak.exportimport.util.ExportUtils; @@ -307,7 +308,7 @@ public class LegacyExportImportManager implements ExportImportManager { updateParSettings(rep, newRealm); - Map mappedFlows = importAuthenticationFlows(newRealm, rep); + Map mappedFlows = importAuthenticationFlows(session, newRealm, rep); if (rep.getRequiredActions() != null) { for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) { RequiredActionProviderModel model = toModel(action); @@ -1257,7 +1258,7 @@ public class LegacyExportImportManager implements ExportImportManager { return webAuthnPolicy; } - public static Map importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) { + public static Map importAuthenticationFlows(KeycloakSession session, RealmModel newRealm, RealmRepresentation rep) { Map mappedFlows = new HashMap<>(); if (rep.getAuthenticationFlows() == null) { // assume this is an old version being imported @@ -1285,7 +1286,7 @@ public class LegacyExportImportManager implements ExportImportManager { for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) { AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias()); for (AuthenticationExecutionExportRepresentation exeRep : flowRep.getAuthenticationExecutions()) { - AuthenticationExecutionModel execution = toModel(newRealm, model, exeRep); + AuthenticationExecutionModel execution = toModel(session, newRealm, model, exeRep); newRealm.addAuthenticatorExecution(execution); } } @@ -1371,10 +1372,10 @@ public class LegacyExportImportManager implements ExportImportManager { return mappedFlows; } - private static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationFlowModel parentFlow, AuthenticationExecutionExportRepresentation rep) { + private static AuthenticationExecutionModel toModel(KeycloakSession session, RealmModel realm, AuthenticationFlowModel parentFlow, AuthenticationExecutionExportRepresentation rep) { AuthenticationExecutionModel model = new AuthenticationExecutionModel(); if (rep.getAuthenticatorConfig() != null) { - AuthenticatorConfigModel config = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig()); + AuthenticatorConfigModel config = new DeployedConfigurationsManager(session).getAuthenticatorConfigByAlias(realm, rep.getAuthenticatorConfig()); model.setAuthenticatorConfig(config.getId()); } model.setAuthenticator(rep.getAuthenticator()); diff --git a/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java b/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java index 69e81db5f0..51039d0de0 100644 --- a/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java +++ b/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java @@ -24,6 +24,7 @@ import org.keycloak.Config; import org.keycloak.common.enums.SslRequired; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; +import org.keycloak.deployment.DeployedConfigurationsManager; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientProvider; import org.keycloak.models.ClientScopeProvider; @@ -314,7 +315,7 @@ public class MapExportImportManager implements ExportImportManager { updateParSettings(rep, newRealm); - Map mappedFlows = importAuthenticationFlows(newRealm, rep); + Map mappedFlows = importAuthenticationFlows(session, newRealm, rep); if (rep.getRequiredActions() != null) { for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) { RequiredActionProviderModel model = toModel(action); @@ -1461,7 +1462,7 @@ public class MapExportImportManager implements ExportImportManager { return webAuthnPolicy; } - public static Map importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) { + public static Map importAuthenticationFlows(KeycloakSession session, RealmModel newRealm, RealmRepresentation rep) { Map mappedFlows = new HashMap<>(); if (rep.getAuthenticationFlows() == null) { // assume this is an old version being imported @@ -1489,7 +1490,7 @@ public class MapExportImportManager implements ExportImportManager { for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) { AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias()); for (AuthenticationExecutionExportRepresentation exeRep : flowRep.getAuthenticationExecutions()) { - AuthenticationExecutionModel execution = toModel(newRealm, model, exeRep); + AuthenticationExecutionModel execution = toModel(session, newRealm, model, exeRep); newRealm.addAuthenticatorExecution(execution); } } @@ -1575,10 +1576,10 @@ public class MapExportImportManager implements ExportImportManager { return mappedFlows; } - private static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationFlowModel parentFlow, AuthenticationExecutionExportRepresentation rep) { + private static AuthenticationExecutionModel toModel(KeycloakSession session, RealmModel realm, AuthenticationFlowModel parentFlow, AuthenticationExecutionExportRepresentation rep) { AuthenticationExecutionModel model = new AuthenticationExecutionModel(); if (rep.getAuthenticatorConfig() != null) { - AuthenticatorConfigModel config = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig()); + AuthenticatorConfigModel config = new DeployedConfigurationsManager(session).getAuthenticatorConfigByAlias(realm, rep.getAuthenticatorConfig()); model.setAuthenticatorConfig(config.getId()); } model.setAuthenticator(rep.getAuthenticator()); diff --git a/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsManager.java b/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsManager.java new file mode 100644 index 0000000000..211b336447 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsManager.java @@ -0,0 +1,78 @@ +/* + * 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.deployment; + +import org.jboss.logging.Logger; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; + +/** + * Allows to CRUD for configurations (like Authenticator configs). Those are typically saved in the store (realm), but can be also + * deployed and hence not saved in the DB + * + * @author Marek Posolda + */ +public class DeployedConfigurationsManager { + + private static final Logger log = Logger.getLogger(DeployedConfigurationsManager.class); + + private final KeycloakSession session; + + public DeployedConfigurationsManager(KeycloakSession session) { + this.session = session; + } + + public void registerDeployedAuthenticatorConfig(AuthenticatorConfigModel model) { + log.debugf("Register deployed authenticator config: %s", model.getId()); + session.getProvider(DeployedConfigurationsProvider.class).registerDeployedAuthenticatorConfig(model); + } + + public AuthenticatorConfigModel getDeployedAuthenticatorConfig(String configId) { + return session.getProvider(DeployedConfigurationsProvider.class).getDeployedAuthenticatorConfigs() + .filter(config -> configId.equals(config.getId())) + .findFirst().orElse(null); + } + + public AuthenticatorConfigModel getAuthenticatorConfig(RealmModel realm, String configId) { + AuthenticatorConfigModel cfgModel = getDeployedAuthenticatorConfig(configId); + if (cfgModel != null) { + log.tracef("Found deployed configuration by id: %s", configId); + return cfgModel; + } else { + return realm.getAuthenticatorConfigById(configId); + } + } + + + public AuthenticatorConfigModel getAuthenticatorConfigByAlias(RealmModel realm, String alias) { + if (alias == null) return null; + AuthenticatorConfigModel cfgModel = session.getProvider(DeployedConfigurationsProvider.class).getDeployedAuthenticatorConfigs() + .filter(config -> alias.equals(config.getAlias())) + .findFirst().orElse(null); + if (cfgModel != null) { + log.debugf("Found deployed configuration by alias: %s", alias); + return cfgModel; + } else { + return realm.getAuthenticatorConfigByAlias(alias); + } + } + +} diff --git a/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsProvider.java b/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsProvider.java new file mode 100644 index 0000000000..8de1a2b340 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsProvider.java @@ -0,0 +1,38 @@ +/* + * 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.deployment; + +import java.util.stream.Stream; + +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.provider.Provider; + +/** + * Allows to register "deployed configurations", which are retrieved in runtime from deployed providers and hence are not saved in the DB + * + * @author Marek Posolda + */ +public interface DeployedConfigurationsProvider extends Provider { + + void registerDeployedAuthenticatorConfig(AuthenticatorConfigModel model); + + Stream getDeployedAuthenticatorConfigs(); + +} diff --git a/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsProviderFactory.java new file mode 100644 index 0000000000..18f86a3d25 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsProviderFactory.java @@ -0,0 +1,28 @@ +/* + * 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.deployment; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Marek Posolda + */ +public interface DeployedConfigurationsProviderFactory extends ProviderFactory { +} diff --git a/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsSpi.java b/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsSpi.java new file mode 100644 index 0000000000..cd355841dd --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/deployment/DeployedConfigurationsSpi.java @@ -0,0 +1,50 @@ +/* + * 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.deployment; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Marek Posolda + */ +public class DeployedConfigurationsSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "deployed-configurations"; + } + + @Override + public Class getProviderClass() { + return DeployedConfigurationsProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return DeployedConfigurationsProviderFactory.class; + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 52009911e5..a15736c1da 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -35,6 +35,7 @@ import org.keycloak.common.util.Time; import org.keycloak.component.ComponentModel; import org.keycloak.credential.CredentialMetadata; import org.keycloak.credential.CredentialModel; +import org.keycloak.deployment.DeployedConfigurationsManager; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.AuthDetails; @@ -547,7 +548,7 @@ public class ModelToRepresentation { rep.setSupportedLocales(realm.getSupportedLocalesStream().collect(Collectors.toSet())); rep.setDefaultLocale(realm.getDefaultLocale()); if (internal) { - exportAuthenticationFlows(realm, rep); + exportAuthenticationFlows(session, realm, rep); exportRequiredActions(realm, rep); exportGroups(realm, rep); } @@ -586,10 +587,10 @@ public class ModelToRepresentation { rep.setGroups(toGroupHierarchy(realm, true).collect(Collectors.toList())); } - public static void exportAuthenticationFlows(RealmModel realm, RealmRepresentation rep) { + public static void exportAuthenticationFlows(KeycloakSession session, RealmModel realm, RealmRepresentation rep) { List authenticationFlows = realm.getAuthenticationFlowsStream() .sorted(AuthenticationFlowModel.AuthenticationFlowComparator.SINGLETON) - .map(flow -> toRepresentation(realm, flow)) + .map(flow -> toRepresentation(session, realm, flow)) .collect(Collectors.toList()); rep.setAuthenticationFlows(authenticationFlows); @@ -882,7 +883,7 @@ public class ModelToRepresentation { return consentRep; } - public static AuthenticationFlowRepresentation toRepresentation(RealmModel realm, AuthenticationFlowModel model) { + public static AuthenticationFlowRepresentation toRepresentation(KeycloakSession session, RealmModel realm, AuthenticationFlowModel model) { AuthenticationFlowRepresentation rep = new AuthenticationFlowRepresentation(); rep.setId(model.getId()); rep.setBuiltIn(model.isBuiltIn()); @@ -891,14 +892,14 @@ public class ModelToRepresentation { rep.setAlias(model.getAlias()); rep.setDescription(model.getDescription()); rep.setAuthenticationExecutions(realm.getAuthenticationExecutionsStream(model.getId()) - .map(e -> toRepresentation(realm, e)).collect(Collectors.toList())); + .map(e -> toRepresentation(session, realm, e)).collect(Collectors.toList())); return rep; } - public static AuthenticationExecutionExportRepresentation toRepresentation(RealmModel realm, AuthenticationExecutionModel model) { + public static AuthenticationExecutionExportRepresentation toRepresentation(KeycloakSession session, RealmModel realm, AuthenticationExecutionModel model) { AuthenticationExecutionExportRepresentation rep = new AuthenticationExecutionExportRepresentation(); if (model.getAuthenticatorConfig() != null) { - AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(model.getAuthenticatorConfig()); + AuthenticatorConfigModel config = new DeployedConfigurationsManager(session).getAuthenticatorConfig(realm, model.getAuthenticatorConfig()); rep.setAuthenticatorConfig(config.getAlias()); } rep.setAuthenticator(model.getAuthenticator()); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 9cb91b0d31..025ff00ce3 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -57,6 +57,7 @@ import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.UriUtils; import org.keycloak.component.ComponentModel; import org.keycloak.credential.CredentialModel; +import org.keycloak.deployment.DeployedConfigurationsManager; import org.keycloak.migration.migrators.MigrationUtils; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; @@ -926,7 +927,7 @@ public class RepresentationToModel { } - public static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationExecutionRepresentation rep) { + public static AuthenticationExecutionModel toModel(KeycloakSession session, RealmModel realm, AuthenticationExecutionRepresentation rep) { AuthenticationExecutionModel model = new AuthenticationExecutionModel(); model.setId(rep.getId()); model.setFlowId(rep.getFlowId()); @@ -938,7 +939,7 @@ public class RepresentationToModel { model.setRequirement(AuthenticationExecutionModel.Requirement.valueOf(rep.getRequirement())); if (rep.getAuthenticatorConfig() != null) { - AuthenticatorConfigModel cfg = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig()); + AuthenticatorConfigModel cfg = new DeployedConfigurationsManager(session).getAuthenticatorConfigByAlias(realm, rep.getAuthenticatorConfig()); model.setAuthenticatorConfig(cfg.getId()); } return model; diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 4920730d77..8e9c90c5be 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -62,6 +62,7 @@ org.keycloak.authentication.otp.OTPApplicationSpi org.keycloak.authorization.policy.provider.PolicySpi org.keycloak.authorization.store.StoreFactorySpi org.keycloak.authorization.AuthorizationSpi +org.keycloak.deployment.DeployedConfigurationsSpi org.keycloak.models.cache.authorization.CachedStoreFactorySpi org.keycloak.protocol.oidc.TokenExchangeSpi org.keycloak.protocol.oidc.TokenIntrospectionSpi diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/DeployedScriptAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/DeployedScriptAuthenticatorFactory.java index 04b9ded43c..3615758a85 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/DeployedScriptAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/DeployedScriptAuthenticatorFactory.java @@ -24,9 +24,14 @@ import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.common.Profile; +import org.keycloak.deployment.DeployedConfigurationsManager; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderEvent; import org.keycloak.representations.provider.ScriptProviderMetadata; /** @@ -88,6 +93,13 @@ public final class DeployedScriptAuthenticatorFactory extends ScriptBasedAuthent configProperties = super.getConfigProperties(); } + @Override + public void postInit(KeycloakSessionFactory factory) { + KeycloakModelUtils.runJobInTransaction(factory, session -> { + new DeployedConfigurationsManager(session).registerDeployedAuthenticatorConfig(model); + }); + } + @Override public List getConfigProperties() { return configProperties; diff --git a/services/src/main/java/org/keycloak/deployment/DefaultDeployedConfigurationsProvider.java b/services/src/main/java/org/keycloak/deployment/DefaultDeployedConfigurationsProvider.java new file mode 100644 index 0000000000..f1638da15b --- /dev/null +++ b/services/src/main/java/org/keycloak/deployment/DefaultDeployedConfigurationsProvider.java @@ -0,0 +1,51 @@ +/* + * 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.deployment; + +import java.util.Map; +import java.util.stream.Stream; + +import org.keycloak.models.AuthenticatorConfigModel; + +/** + * @author Marek Posolda + */ +public class DefaultDeployedConfigurationsProvider implements DeployedConfigurationsProvider { + + private final Map deployedAuthenticatorConfigs; + public DefaultDeployedConfigurationsProvider(Map deployedAuthenticatorConfigs) { + this.deployedAuthenticatorConfigs = deployedAuthenticatorConfigs; + } + + @Override + public void registerDeployedAuthenticatorConfig(AuthenticatorConfigModel model) { + deployedAuthenticatorConfigs.put(model.getId(), model); + } + + @Override + public Stream getDeployedAuthenticatorConfigs() { + return deployedAuthenticatorConfigs.values().stream(); + } + + @Override + public void close() { + + } +} diff --git a/services/src/main/java/org/keycloak/deployment/DefaultDeployedConfigurationsProviderFactory.java b/services/src/main/java/org/keycloak/deployment/DefaultDeployedConfigurationsProviderFactory.java new file mode 100644 index 0000000000..20dc7922ae --- /dev/null +++ b/services/src/main/java/org/keycloak/deployment/DefaultDeployedConfigurationsProviderFactory.java @@ -0,0 +1,62 @@ +/* + * 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.deployment; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import org.keycloak.Config; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * @author Marek Posolda + */ +public class DefaultDeployedConfigurationsProviderFactory implements DeployedConfigurationsProviderFactory { + + public static final String PROVIDER_ID = "default"; + private final Map deployedAuthenticatorConfigs = new ConcurrentHashMap<>(); + @Override + public DeployedConfigurationsProvider create(KeycloakSession session) { + return new DefaultDeployedConfigurationsProvider(deployedAuthenticatorConfigs); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java index 86df1b3fa5..883d18c1b5 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java @@ -33,11 +33,13 @@ import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormAuthenticator; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.deployment.DeployedConfigurationsManager; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; @@ -205,7 +207,7 @@ public class AuthenticationManagementResource { return realm.getAuthenticationFlowsStream() .filter(flow -> flow.isTopLevel() && !Objects.equals(flow.getAlias(), DefaultAuthenticationFlows.SAML_ECP_FLOW)) - .map(flow -> ModelToRepresentation.toRepresentation(realm, flow)); + .map(flow -> ModelToRepresentation.toRepresentation(session, realm, flow)); } /** @@ -264,7 +266,7 @@ public class AuthenticationManagementResource { if (flow == null) { throw new NotFoundException("Could not find flow with id"); } - return ModelToRepresentation.toRepresentation(realm, flow); + return ModelToRepresentation.toRepresentation(session, realm, flow); } /** @@ -375,7 +377,7 @@ public class AuthenticationManagementResource { logger.debug("flow not found: " + flowAlias); return Response.status(NOT_FOUND).build(); } - AuthenticationFlowModel copy = copyFlow(realm, flow, newName); + AuthenticationFlowModel copy = copyFlow(session, realm, flow, newName); data.put("id", copy.getId()); adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri()).representation(data).success(); @@ -384,7 +386,7 @@ public class AuthenticationManagementResource { } - public static AuthenticationFlowModel copyFlow(RealmModel realm, AuthenticationFlowModel flow, String newName) { + public static AuthenticationFlowModel copyFlow(KeycloakSession session, RealmModel realm, AuthenticationFlowModel flow, String newName) { AuthenticationFlowModel copy = new AuthenticationFlowModel(); copy.setAlias(newName); copy.setDescription(flow.getDescription()); @@ -392,11 +394,11 @@ public class AuthenticationManagementResource { copy.setBuiltIn(false); copy.setTopLevel(flow.isTopLevel()); copy = realm.addAuthenticationFlow(copy); - copy(realm, newName, flow, copy); + copy(session, realm, newName, flow, copy); return copy; } - public static void copy(RealmModel realm, String newName, AuthenticationFlowModel from, AuthenticationFlowModel to) { + public static void copy(KeycloakSession session, RealmModel realm, String newName, AuthenticationFlowModel from, AuthenticationFlowModel to) { realm.getAuthenticationExecutionsStream(from.getId()).forEachOrdered(execution -> { if (execution.isAuthenticatorFlow()) { AuthenticationFlowModel subFlow = realm.getAuthenticationFlowById(execution.getFlowId()); @@ -408,26 +410,32 @@ public class AuthenticationManagementResource { copy.setTopLevel(false); copy = realm.addAuthenticationFlow(copy); execution.setFlowId(copy.getId()); - copy(realm, newName, subFlow, copy); + copy(session, realm, newName, subFlow, copy); } if (execution.getAuthenticatorConfig() != null) { - AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(execution.getAuthenticatorConfig()); + DeployedConfigurationsManager configManager = new DeployedConfigurationsManager(session); + AuthenticatorConfigModel config = configManager.getAuthenticatorConfig(realm, execution.getAuthenticatorConfig()); if (config == null) { logger.debugf("Authentication execution with id [%s] not found", config.getId()); throw new IllegalStateException("Authentication execution configuration not found"); } - config.setId(null); + if (configManager.getDeployedAuthenticatorConfig(execution.getAuthenticatorConfig()) != null) { + // Shared configuration of deployed provider + execution.setAuthenticatorConfig(config.getId()); + } else { + config.setId(null); - if (config.getAlias() != null) { - config.setAlias(newName + " " + config.getAlias()); + if (config.getAlias() != null) { + config.setAlias(newName + " " + config.getAlias()); + } + + AuthenticatorConfigModel newConfig = realm.addAuthenticatorConfig(config); + + execution.setAuthenticatorConfig(newConfig.getId()); } - - AuthenticatorConfigModel newConfig = realm.addAuthenticatorConfig(config); - - execution.setAuthenticatorConfig(newConfig.getId()); } execution.setId(null); @@ -524,17 +532,7 @@ public class AuthenticationManagementResource { String provider = data.get("provider"); // make sure provider is one of the registered providers - ProviderFactory f; - if (parentFlow.getProviderId().equals(AuthenticationFlow.CLIENT_FLOW)) { - f = session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, provider); - } else if (parentFlow.getProviderId().equals(AuthenticationFlow.FORM_FLOW)) { - f = session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, provider); - } else { - f = session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, provider); - } - if (f == null) { - throw new BadRequestException("No authentication provider found for id: " + provider); - } + ProviderFactory f = getProviderFactory( parentFlow, provider); AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); execution.setParentFlow(parentFlow.getId()); @@ -551,18 +549,7 @@ public class AuthenticationManagementResource { execution = realm.addAuthenticatorExecution(execution); - if (f instanceof ConfiguredProvider) { - ConfiguredProvider internalProviderFactory = (ConfiguredProvider) f; - AuthenticatorConfigModel config = internalProviderFactory.getConfig(); - - if (config != null) { - // creates a default configuration if the factory defines one - // useful for internal providers that already provide a built-in configuration - AuthenticatorConfigRepresentation configRepresentation = ModelToRepresentation.toRepresentation( - config); - newExecutionConfig(execution.getId(), configRepresentation).close(); - } - } + checkConfigForDeployedProvider(f, execution); data.put("id", execution.getId()); adminEvent.operation(OperationType.CREATE).resource(ResourceType.AUTH_EXECUTION).resourcePath(session.getContext().getUri()).representation(data).success(); @@ -571,6 +558,38 @@ public class AuthenticationManagementResource { return Response.created(session.getContext().getUri().getBaseUriBuilder().path(session.getContext().getUri().getPath().replace(addExecutionPathSegment, "")).path("executions").path(execution.getId()).build()).build(); } + private ProviderFactory getProviderFactory(AuthenticationFlowModel parentFlow, String provider) { + ProviderFactory f = null; + if (parentFlow.getProviderId().equals(AuthenticationFlow.CLIENT_FLOW)) { + f = session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, provider); + } else if (parentFlow.getProviderId().equals(AuthenticationFlow.FORM_FLOW)) { + f = session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, provider); + } else { + f = session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, provider); + } + if (f == null) { + throw new BadRequestException("No authentication provider found for id: " + provider); + } + return f; + } + + + private void checkConfigForDeployedProvider(ProviderFactory f, AuthenticationExecutionModel execution) { + if (f instanceof ConfiguredProvider) { + ConfiguredProvider internalProviderFactory = (ConfiguredProvider) f; + AuthenticatorConfigModel config = internalProviderFactory.getConfig(); + + if (config != null) { + // use a default configuration if the factory defines one + // Assumption is that this is registered in DeployedConfigurationsProvider + // useful for internal providers that already provide a built-in configuration + logger.tracef("Updating execution of provider '%s' with shared configuration.", execution.getAuthenticator()); + execution.setAuthenticatorConfig(config.getId()); + realm.updateAuthenticatorExecution(execution); + } + } + } + /** * Get authentication executions for a flow * @@ -649,7 +668,7 @@ public class AuthenticationManagementResource { if (factory.isConfigurable()) { String authenticatorConfigId = execution.getAuthenticatorConfig(); if(authenticatorConfigId != null) { - AuthenticatorConfigModel authenticatorConfig = realm.getAuthenticatorConfigById(authenticatorConfigId); + AuthenticatorConfigModel authenticatorConfig = new DeployedConfigurationsManager(session).getAuthenticatorConfig(realm, authenticatorConfigId); if (authenticatorConfig != null) { rep.setAlias(authenticatorConfig.getAlias()); @@ -779,7 +798,7 @@ public class AuthenticationManagementResource { public Response addExecution(@Parameter(description = "JSON model describing authentication execution") AuthenticationExecutionRepresentation execution) { auth.realm().requireManageRealm(); - AuthenticationExecutionModel model = RepresentationToModel.toModel(realm, execution); + AuthenticationExecutionModel model = RepresentationToModel.toModel(session, realm, execution); AuthenticationFlowModel parentFlow = getParentFlow(model); if (parentFlow.isBuiltIn()) { throw new BadRequestException("It is illegal to add execution to a built in flow"); @@ -787,6 +806,11 @@ public class AuthenticationManagementResource { model.setPriority(getNextPriority(parentFlow)); model = realm.addAuthenticatorExecution(model); + if (!execution.isAuthenticatorFlow()) { + ProviderFactory f = getProviderFactory(parentFlow, execution.getAuthenticator()); + checkConfigForDeployedProvider(f, model); + } + adminEvent.operation(OperationType.CREATE).resource(ResourceType.AUTH_EXECUTION).resourcePath(session.getContext().getUri(), model.getId()).representation(execution).success(); return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build(); } @@ -976,7 +1000,7 @@ public class AuthenticationManagementResource { public AuthenticatorConfigRepresentation getAuthenticatorConfig(@Parameter(description = "Execution id") @PathParam("executionId") String execution, @Parameter(description = "Configuration id") @PathParam("id") String id) { auth.realm().requireViewRealm(); - AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(id); + AuthenticatorConfigModel config = new DeployedConfigurationsManager(session).getAuthenticatorConfig(realm, id); if (config == null) { throw new NotFoundException("Could not find authenticator config"); @@ -1317,7 +1341,7 @@ public class AuthenticationManagementResource { public AuthenticatorConfigRepresentation getAuthenticatorConfig(@Parameter(description = "Configuration id") @PathParam("id") String id) { auth.realm().requireViewRealm(); - AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(id); + AuthenticatorConfigModel config = new DeployedConfigurationsManager(session).getAuthenticatorConfig(realm, id); if (config == null) { throw new NotFoundException("Could not find authenticator config"); @@ -1369,6 +1393,10 @@ public class AuthenticationManagementResource { auth.realm().requireManageRealm(); ReservedCharValidator.validate(rep.getAlias()); + if (new DeployedConfigurationsManager(session).getDeployedAuthenticatorConfig(id) != null) { + throw new BadRequestException("Authenticator config is read-only"); + } + AuthenticatorConfigModel exists = realm.getAuthenticatorConfigById(id); if (exists == null) { throw new NotFoundException("Could not find authenticator config"); diff --git a/services/src/main/resources/META-INF/services/org.keycloak.deployment.DeployedConfigurationsProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.deployment.DeployedConfigurationsProviderFactory new file mode 100644 index 0000000000..ea381131d4 --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.deployment.DeployedConfigurationsProviderFactory @@ -0,0 +1,20 @@ +# +# 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. +# +# + +org.keycloak.deployment.DefaultDeployedConfigurationsProviderFactory diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index 39dc4d3373..b403f5ff0e 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -674,7 +674,7 @@ public class TestingResourceProvider implements RealmResourceProvider { RealmModel realm = getRealmByName(realmName); AuthenticationFlowModel flow = realm.getClientAuthenticationFlow(); if (flow == null) return null; - return ModelToRepresentation.toRepresentation(realm, flow); + return ModelToRepresentation.toRepresentation(session, realm, flow); } @GET @@ -684,7 +684,7 @@ public class TestingResourceProvider implements RealmResourceProvider { RealmModel realm = getRealmByName(realmName); AuthenticationFlowModel flow = realm.getResetCredentialsFlow(); if (flow == null) return null; - return ModelToRepresentation.toRepresentation(realm, flow); + return ModelToRepresentation.toRepresentation(session, realm, flow); } @GET diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptAuthenticatorTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptAuthenticatorTest.java index 427541c034..59c17d6258 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptAuthenticatorTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptAuthenticatorTest.java @@ -21,7 +21,12 @@ import static org.keycloak.common.Profile.Feature.SCRIPTS; import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT; import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.core.Response; import org.jboss.arquillian.container.test.api.Deployment; @@ -34,18 +39,23 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; +import org.keycloak.admin.client.resource.AuthenticationManagementResource; import org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.provider.ScriptProviderDescriptor; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest; import org.keycloak.testsuite.arquillian.annotation.DisableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.forms.AbstractFlowTest; @@ -180,6 +190,53 @@ public class DeployedScriptAuthenticatorTest extends AbstractFlowTest { events.expectLogin().user(okayUser()).detail(Details.USERNAME, "user").assertEvent(); } + // Issue 20005 + @Test + public void testManyScriptAuthenticatorInstances() throws Exception { + configureFlows(); + AuthenticationManagementResource authMgmtResource = adminClient.realm(TEST_REALM_NAME).flows(); + + // Endpoint used by admin console + Map scriptExecution = new HashMap<>(); + scriptExecution.put("provider", "script-authenticator-a.js"); + + // It should be possible to add another script-authenticator to the flow + authMgmtResource.addExecution("scriptBrowser", scriptExecution); + + List executions = authMgmtResource.getExecutions("scriptBrowser"); + List scriptExecutions = executions.stream() + .filter(execution -> execution.getDisplayName().equals("My Authenticator")) + .collect(Collectors.toList()); + + // Both executions refers to same config of deployed script provider + Assert.assertEquals(2, scriptExecutions.size()); + for (AuthenticationExecutionInfoRepresentation execution : scriptExecutions) { + Assert.assertEquals(execution.getAuthenticationConfig(), "script-authenticator-a.js"); + } + + // Assert updating config should fail due it's read-only + AuthenticatorConfigRepresentation configRep = authMgmtResource.getAuthenticatorConfig("script-authenticator-a.js"); + configRep.getConfig().put("scriptCode", "Something"); + try { + authMgmtResource.updateAuthenticatorConfig("script-authenticator-a.js", configRep); + Assert.fail("Update of configuration should have failed"); + } catch (BadRequestException bre) { + // Expected + } + + // Test copy flow is OK + Map newFlow = new HashMap<>(); + newFlow.put("newName", "Copy of script flow"); + Response resp = authMgmtResource.copy("scriptBrowser", newFlow); + Assert.assertEquals(201, resp.getStatus()); + resp.close(); + AuthenticationFlowRepresentation copiedFlow = AbstractAuthenticationTest.findFlowByAlias("Copy of script flow", authMgmtResource.getFlows()); + + // Cleanup + authMgmtResource.deleteFlow(copiedFlow.getId()); + authMgmtResource.removeExecution(scriptExecutions.get(1).getId()); + } + private UserRepresentation okayUser() { return adminClient.realm(TEST_REALM_NAME).users().search("user", true).get(0); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/FlowUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/FlowUtil.java index 6c47e75754..2cd8844bab 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/FlowUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/FlowUtil.java @@ -27,7 +27,8 @@ import static org.keycloak.models.utils.DefaultAuthenticationFlows.REGISTRATION_ import static org.keycloak.models.utils.DefaultAuthenticationFlows.RESET_CREDENTIALS_FLOW; public class FlowUtil { - private RealmModel realm; + private final KeycloakSession session; + private final RealmModel realm; private AuthenticationFlowModel currentFlow; private String flowAlias; private int maxPriority = 0; @@ -42,7 +43,8 @@ public class FlowUtil { } } - public FlowUtil(RealmModel realm) { + private FlowUtil(KeycloakSession session, RealmModel realm) { + this.session = session; this.realm = realm; } @@ -55,11 +57,11 @@ public class FlowUtil { } public static FlowUtil inCurrentRealm(KeycloakSession session) { - return new FlowUtil(session.getContext().getRealm()); + return new FlowUtil(session, session.getContext().getRealm()); } private FlowUtil newFlowUtil(AuthenticationFlowModel flowModel) { - FlowUtil subflow = new FlowUtil(realm); + FlowUtil subflow = new FlowUtil(session, realm); subflow.currentFlow = flowModel; return subflow; } @@ -112,7 +114,7 @@ public class FlowUtil { realm.removeAuthenticationFlow(foundFlow); } - currentFlow = AuthenticationManagementResource.copyFlow(realm, existingBrowserFlow, newFlowAlias); + currentFlow = AuthenticationManagementResource.copyFlow(session, realm, existingBrowserFlow, newFlowAlias); return this; }