Modify RealmsAdminResource.importRealm to work with InputStream

Closes #13609
This commit is contained in:
Alexander Schwartz 2022-09-12 10:29:47 +02:00 committed by Hynek Mlnařík
parent cff5cfb6df
commit be2deb0517
11 changed files with 262 additions and 11 deletions

View file

@ -41,6 +41,7 @@ import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.models.OAuth2DeviceConfig; import org.keycloak.models.OAuth2DeviceConfig;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.ParConfig; 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.UserFederationProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.ExportImportManager; import org.keycloak.storage.ExportImportManager;
import org.keycloak.storage.ImportRealmFromRepresentation;
import org.keycloak.storage.UserStoragePrivateUtil; import org.keycloak.storage.UserStoragePrivateUtil;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.UserStorageProviderModel;
@ -96,6 +98,8 @@ import org.keycloak.util.JsonSerialization;
import org.keycloak.validation.ValidationUtil; import org.keycloak.validation.ValidationUtil;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; 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 @Override
public void importRealm(RealmRepresentation rep, RealmModel newRealm, boolean skipUserDependent) { public void importRealm(RealmRepresentation rep, RealmModel newRealm, boolean skipUserDependent) {
convertDeprecatedSocialProviders(rep); convertDeprecatedSocialProviders(rep);

View file

@ -79,9 +79,13 @@ import org.keycloak.representations.idm.UserConsentRepresentation;
import org.keycloak.representations.idm.UserFederationMapperRepresentation; import org.keycloak.representations.idm.UserFederationMapperRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.ExportImportManager; import org.keycloak.storage.ExportImportManager;
import org.keycloak.storage.ImportRealmFromRepresentation;
import org.keycloak.userprofile.UserProfileProvider; import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.util.JsonSerialization;
import org.keycloak.validation.ValidationUtil; import org.keycloak.validation.ValidationUtil;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; 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. * This wraps the functionality about export/import for legacy storage.
* *
* <p> * <p>
* Currently this only removes the user-storage and federation code from LegacyExportImportManager. * Currently, this only removes the user-storage and federation code from LegacyExportImportManager.
* <p> * <p>
* In the future, this needs to be rewritten completely. * 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"); 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) { private static void convertDeprecatedDefaultRoles(RealmRepresentation rep, RealmModel newRealm) {
if (rep.getDefaultRole() == null) { if (rep.getDefaultRole() == null) {

View file

@ -24,6 +24,8 @@ import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import java.io.InputStream;
/** /**
* Manage importing and updating of realms for the legacy store. * Manage importing and updating of realms for the legacy store.
* *
@ -37,4 +39,6 @@ public interface ExportImportManager {
UserModel createUser(RealmModel realm, UserRepresentation userRep); UserModel createUser(RealmModel realm, UserRepresentation userRep);
void exportRealm(RealmModel realm, ExportOptions options, ExportAdapter callback); void exportRealm(RealmModel realm, ExportOptions options, ExportAdapter callback);
RealmModel importRealm(InputStream requestBody);
} }

View file

@ -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.
* <p />
* 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.
* <p />
* 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;
}
}

View file

@ -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<RealmManagerProviderFactory>, 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";
}
}

View file

@ -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<? extends Provider> getProviderClass() {
return RealmManagerProviderFactory.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return RealmManagerProviderFactory.class;
}
}

View file

@ -34,6 +34,8 @@ import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.admin.permissions.AdminPermissions; 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.Consumes;
import javax.ws.rs.DefaultValue; 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.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
@ -114,23 +117,21 @@ public class RealmsAdminResource {
} }
/** /**
* Import a realm * Import a realm.
* * <p>
* Imports a realm from a full representation of that realm. Realm name must be unique. * Imports a realm from a full representation of that realm. Realm name must be unique.
* *
* @param rep JSON representation of the realm
* @return
*/ */
@POST @POST
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public Response importRealm(final RealmRepresentation rep) { public Response importRealm(InputStream requestBody) {
RealmManager realmManager = new RealmManager(session);
AdminPermissions.realms(session, auth).requireCreateRealm(); AdminPermissions.realms(session, auth).requireCreateRealm();
logger.debugv("importRealm: {0}", rep.getRealm()); ExportImportManager exportImportManager = session.getProvider(DatastoreProvider.class).getExportImportManager();
try { try {
RealmModel realm = realmManager.importRealm(rep); RealmModel realm = exportImportManager.importRealm(requestBody);
grantPermissionsToRealmCreator(realm); grantPermissionsToRealmCreator(realm);
URI location = AdminRoot.realmsUrl(session.getContext().getUri()).path(realm.getName()).build(); URI location = AdminRoot.realmsUrl(session.getContext().getUri()).path(realm.getName()).build();
@ -139,6 +140,7 @@ public class RealmsAdminResource {
return Response.created(location).build(); return Response.created(location).build();
} catch (ModelDuplicateException e) { } catch (ModelDuplicateException e) {
logger.error("Conflict detected", e); logger.error("Conflict detected", e);
if (session.getTransactionManager().isActive()) session.getTransactionManager().setRollbackOnly();
return ErrorResponse.exists("Conflict detected. See logs for details"); return ErrorResponse.exists("Conflict detected. See logs for details");
} catch (PasswordPolicyNotMetException e) { } catch (PasswordPolicyNotMetException e) {
logger.error("Password policy not met for user " + e.getUsername(), e); logger.error("Password policy not met for user " + e.getUsername(), e);

View file

@ -19,6 +19,7 @@ org.keycloak.exportimport.ClientDescriptionConverterSpi
org.keycloak.wellknown.WellKnownSpi org.keycloak.wellknown.WellKnownSpi
org.keycloak.services.clientregistration.ClientRegistrationSpi org.keycloak.services.clientregistration.ClientRegistrationSpi
org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi
org.keycloak.services.managers.RealmManagerSpi
org.keycloak.authentication.actiontoken.ActionTokenHandlerSpi org.keycloak.authentication.actiontoken.ActionTokenHandlerSpi
org.keycloak.services.x509.X509ClientCertificateLookupSpi org.keycloak.services.x509.X509ClientCertificateLookupSpi
org.keycloak.protocol.oidc.ext.OIDCExtSPI org.keycloak.protocol.oidc.ext.OIDCExtSPI

View file

@ -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

View file

@ -102,7 +102,7 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) { try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
String accessToken = adminClient.tokenManager().getAccessTokenString(); 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.setEntity(new StringEntity("{ invalid : invalid }"));
post.setHeader("Authorization", "bearer " + accessToken); post.setHeader("Authorization", "bearer " + accessToken);
post.setHeader("Content-Type", "application/json"); post.setHeader("Content-Type", "application/json");
@ -122,7 +122,7 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) { try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
String accessToken = adminClient.tokenManager().getAccessTokenString(); 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("{\"<img src=alert(1)>\":1}")); post.setEntity(new StringEntity("{\"<img src=alert(1)>\":1}"));
post.setHeader("Authorization", "bearer " + accessToken); post.setHeader("Authorization", "bearer " + accessToken);
post.setHeader("Content-Type", "application/json"); post.setHeader("Content-Type", "application/json");