From be2deb0517837f8660f0f9bfe6650800a102777a Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Mon, 12 Sep 2022 10:29:47 +0200 Subject: [PATCH] Modify RealmsAdminResource.importRealm to work with InputStream Closes #13609 --- .../migration/MigrationModelManager.java | 0 .../datastore/LegacyExportImportManager.java | 16 +++++ .../map/datastore/MapExportImportManager.java | 26 ++++++- .../keycloak/storage/ExportImportManager.java | 4 ++ .../ImportRealmFromRepresentation.java | 70 +++++++++++++++++++ .../managers/RealmManagerProviderFactory.java | 66 +++++++++++++++++ .../services/managers/RealmManagerSpi.java | 51 ++++++++++++++ .../resources/admin/RealmsAdminResource.java | 18 ++--- .../services/org.keycloak.provider.Spi | 1 + ...vices.managers.RealmManagerProviderFactory | 17 +++++ .../error/UncaughtErrorPageTest.java | 4 +- 11 files changed, 262 insertions(+), 11 deletions(-) mode change 100755 => 100644 model/legacy-private/src/main/java/org/keycloak/migration/MigrationModelManager.java create mode 100644 server-spi-private/src/main/java/org/keycloak/storage/ImportRealmFromRepresentation.java create mode 100644 services/src/main/java/org/keycloak/services/managers/RealmManagerProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/services/managers/RealmManagerSpi.java create mode 100644 services/src/main/resources/META-INF/services/org.keycloak.services.managers.RealmManagerProviderFactory diff --git a/model/legacy-private/src/main/java/org/keycloak/migration/MigrationModelManager.java b/model/legacy-private/src/main/java/org/keycloak/migration/MigrationModelManager.java old mode 100755 new mode 100644 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 330ffa0a41..eefef8032b 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 @@ -41,6 +41,7 @@ import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ModelException; import org.keycloak.models.OAuth2DeviceConfig; import org.keycloak.models.OTPPolicy; import org.keycloak.models.ParConfig; @@ -86,6 +87,7 @@ import org.keycloak.representations.idm.UserFederationMapperRepresentation; import org.keycloak.representations.idm.UserFederationProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.ExportImportManager; +import org.keycloak.storage.ImportRealmFromRepresentation; import org.keycloak.storage.UserStoragePrivateUtil; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; @@ -96,6 +98,8 @@ import org.keycloak.util.JsonSerialization; import org.keycloak.validation.ValidationUtil; import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -139,6 +143,18 @@ public class LegacyExportImportManager implements ExportImportManager { }); } + @Override + public RealmModel importRealm(InputStream requestBody) { + RealmRepresentation rep; + try { + rep = JsonSerialization.readValue(requestBody, RealmRepresentation.class); + } catch (IOException e) { + throw new ModelException("unable to read contents from stream", e); + } + logger.debugv("importRealm: {0}", rep.getRealm()); + return ImportRealmFromRepresentation.fire(session, rep); + } + @Override public void importRealm(RealmRepresentation rep, RealmModel newRealm, boolean skipUserDependent) { convertDeprecatedSocialProviders(rep); 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 c2a1bf3aac..b159526258 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 @@ -79,9 +79,13 @@ import org.keycloak.representations.idm.UserConsentRepresentation; import org.keycloak.representations.idm.UserFederationMapperRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.ExportImportManager; +import org.keycloak.storage.ImportRealmFromRepresentation; import org.keycloak.userprofile.UserProfileProvider; +import org.keycloak.util.JsonSerialization; import org.keycloak.validation.ValidationUtil; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -104,7 +108,7 @@ import static org.keycloak.models.utils.RepresentationToModel.importRoles; * This wraps the functionality about export/import for legacy storage. * *

- * Currently this only removes the user-storage and federation code from LegacyExportImportManager. + * Currently, this only removes the user-storage and federation code from LegacyExportImportManager. *

* In the future, this needs to be rewritten completely. * @@ -420,6 +424,26 @@ public class MapExportImportManager implements ExportImportManager { throw new ModelException("exporting for map storage is currently not supported"); } + @Override + public RealmModel importRealm(InputStream requestBody) { + /* A future implementation that would differentiate between the old JSON representations and the new file store + might want to add the file name or the media type as a method parameter to switch between different implementations. */ + + RealmRepresentation rep; + try { + rep = JsonSerialization.readValue(requestBody, RealmRepresentation.class); + } catch (IOException e) { + throw new ModelException("unable to read contents from stream", e); + } + logger.debugv("importRealm: {0}", rep.getRealm()); + + /* The import for the JSON representation might be called from the Admin UI, where it will be empty except for + the realm name and if the realm is enabled. For that scenario, it would need to create all missing elements, + which is done by firing an event to call the existing implementation in the RealmManager. */ + + return ImportRealmFromRepresentation.fire(session, rep); + } + private static void convertDeprecatedDefaultRoles(RealmRepresentation rep, RealmModel newRealm) { if (rep.getDefaultRole() == null) { diff --git a/server-spi-private/src/main/java/org/keycloak/storage/ExportImportManager.java b/server-spi-private/src/main/java/org/keycloak/storage/ExportImportManager.java index cdbc9b3760..5a73acdcc3 100644 --- a/server-spi-private/src/main/java/org/keycloak/storage/ExportImportManager.java +++ b/server-spi-private/src/main/java/org/keycloak/storage/ExportImportManager.java @@ -24,6 +24,8 @@ import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import java.io.InputStream; + /** * Manage importing and updating of realms for the legacy store. * @@ -37,4 +39,6 @@ public interface ExportImportManager { UserModel createUser(RealmModel realm, UserRepresentation userRep); void exportRealm(RealmModel realm, ExportOptions options, ExportAdapter callback); + + RealmModel importRealm(InputStream requestBody); } diff --git a/server-spi-private/src/main/java/org/keycloak/storage/ImportRealmFromRepresentation.java b/server-spi-private/src/main/java/org/keycloak/storage/ImportRealmFromRepresentation.java new file mode 100644 index 0000000000..9271793fc8 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/ImportRealmFromRepresentation.java @@ -0,0 +1,70 @@ +/* + * Copyright 2022 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.storage; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ProviderEvent; +import org.keycloak.representations.idm.RealmRepresentation; + +/** + * Event to trigger that will complete the import for a given realm representation. + *

+ * This event was created as the import of a JSON via the UI/REST API can be called using a JSON representation that contains + * only the name of the realm and if it is enabled. + *

+ * In the future, this might not be needed if this is done when the legacy store migration is complete and the functionality + * is bundled within the map storage. + * + * @author Alexander Schwartz + */ +@Deprecated +public class ImportRealmFromRepresentation implements ProviderEvent { + private final KeycloakSession session; + private final RealmRepresentation realmRepresentation; + + private RealmModel realmModel; + + public ImportRealmFromRepresentation(KeycloakSession session, RealmRepresentation realmRepresentation) { + this.session = session; + this.realmRepresentation = realmRepresentation; + } + + public static RealmModel fire(KeycloakSession session, RealmRepresentation rep) { + ImportRealmFromRepresentation event = new ImportRealmFromRepresentation(session, rep); + session.getKeycloakSessionFactory().publish(event); + return event.getRealmModel(); + } + + public KeycloakSession getSession() { + return session; + } + + public RealmRepresentation getRealmRepresentation() { + return realmRepresentation; + } + + public void setRealmModel(RealmModel realmModel) { + this.realmModel = realmModel; + } + + public RealmModel getRealmModel() { + return realmModel; + } +} + diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManagerProviderFactory.java b/services/src/main/java/org/keycloak/services/managers/RealmManagerProviderFactory.java new file mode 100644 index 0000000000..e71b436d83 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/RealmManagerProviderFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 2022 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.services.managers; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.storage.ImportRealmFromRepresentation; + +/** + * Provider to listen for {@link org.keycloak.storage.ImportRealmFromRepresentation} events. + * If that is no longer needed after further steps around the legacy storage migration, it can be removed. + * + * @author Alexander Schwartz + */ +@Deprecated +public class RealmManagerProviderFactory implements ProviderFactory, Provider { + @Override + public RealmManagerProviderFactory create(KeycloakSession session) { + throw new ModelException("This shouldn't be instantiated, this should only listen to events"); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + factory.register(event -> { + if (event instanceof ImportRealmFromRepresentation) { + ImportRealmFromRepresentation importRealmFromRepresentation = (ImportRealmFromRepresentation) event; + RealmModel realmModel = new RealmManager(importRealmFromRepresentation.getSession()).importRealm(importRealmFromRepresentation.getRealmRepresentation()); + importRealmFromRepresentation.setRealmModel(realmModel); + } + }); + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "default"; + } +} diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManagerSpi.java b/services/src/main/java/org/keycloak/services/managers/RealmManagerSpi.java new file mode 100644 index 0000000000..a9f4131d32 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/RealmManagerSpi.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 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.services.managers; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * Provider to listen for {@link org.keycloak.storage.ImportRealmFromRepresentation} events. + * If that is no longer needed after further steps around the legacy storage migration, it can be removed. + * + * @author Alexander Schwartz + */ +@Deprecated +public class RealmManagerSpi implements Spi { + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "realm-manager"; + } + + @Override + public Class getProviderClass() { + return RealmManagerProviderFactory.class; + } + + @Override + public Class getProviderFactoryClass() { + return RealmManagerProviderFactory.class; + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java index fd57c7e278..78628ef113 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java @@ -34,6 +34,8 @@ import org.keycloak.services.ForbiddenException; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.storage.DatastoreProvider; +import org.keycloak.storage.ExportImportManager; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; @@ -49,6 +51,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.io.InputStream; import java.net.URI; import java.util.Arrays; import java.util.Objects; @@ -114,23 +117,21 @@ public class RealmsAdminResource { } /** - * Import a realm - * + * Import a realm. + *

* Imports a realm from a full representation of that realm. Realm name must be unique. * - * @param rep JSON representation of the realm - * @return */ @POST @Consumes(MediaType.APPLICATION_JSON) - public Response importRealm(final RealmRepresentation rep) { - RealmManager realmManager = new RealmManager(session); + public Response importRealm(InputStream requestBody) { AdminPermissions.realms(session, auth).requireCreateRealm(); - logger.debugv("importRealm: {0}", rep.getRealm()); + ExportImportManager exportImportManager = session.getProvider(DatastoreProvider.class).getExportImportManager(); try { - RealmModel realm = realmManager.importRealm(rep); + RealmModel realm = exportImportManager.importRealm(requestBody); + grantPermissionsToRealmCreator(realm); URI location = AdminRoot.realmsUrl(session.getContext().getUri()).path(realm.getName()).build(); @@ -139,6 +140,7 @@ public class RealmsAdminResource { return Response.created(location).build(); } catch (ModelDuplicateException e) { logger.error("Conflict detected", e); + if (session.getTransactionManager().isActive()) session.getTransactionManager().setRollbackOnly(); return ErrorResponse.exists("Conflict detected. See logs for details"); } catch (PasswordPolicyNotMetException e) { logger.error("Password policy not met for user " + e.getUsername(), e); diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 6fd962bfad..1b5b991a72 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -19,6 +19,7 @@ org.keycloak.exportimport.ClientDescriptionConverterSpi org.keycloak.wellknown.WellKnownSpi org.keycloak.services.clientregistration.ClientRegistrationSpi org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi +org.keycloak.services.managers.RealmManagerSpi org.keycloak.authentication.actiontoken.ActionTokenHandlerSpi org.keycloak.services.x509.X509ClientCertificateLookupSpi org.keycloak.protocol.oidc.ext.OIDCExtSPI diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.managers.RealmManagerProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.managers.RealmManagerProviderFactory new file mode 100644 index 0000000000..c5e551d2db --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.services.managers.RealmManagerProviderFactory @@ -0,0 +1,17 @@ +# +# Copyright 2022 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.services.managers.RealmManagerProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java index 2a6fc78b93..7fc43ca46d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/UncaughtErrorPageTest.java @@ -102,7 +102,7 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest { try (CloseableHttpClient client = HttpClientBuilder.create().build()) { String accessToken = adminClient.tokenManager().getAccessTokenString(); - HttpPost post = new HttpPost(suiteContext.getAuthServerInfo().getUriBuilder().path("/auth/admin/realms").build()); + HttpPost post = new HttpPost(suiteContext.getAuthServerInfo().getUriBuilder().path("/auth/admin/realms/master/components").build()); post.setEntity(new StringEntity("{ invalid : invalid }")); post.setHeader("Authorization", "bearer " + accessToken); post.setHeader("Content-Type", "application/json"); @@ -122,7 +122,7 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest { try (CloseableHttpClient client = HttpClientBuilder.create().build()) { String accessToken = adminClient.tokenManager().getAccessTokenString(); - HttpPost post = new HttpPost(suiteContext.getAuthServerInfo().getUriBuilder().path("/auth/admin/realms").build()); + HttpPost post = new HttpPost(suiteContext.getAuthServerInfo().getUriBuilder().path("/auth/admin/realms/master/components").build()); post.setEntity(new StringEntity("{\"\":1}")); post.setHeader("Authorization", "bearer " + accessToken); post.setHeader("Content-Type", "application/json");