diff --git a/src/main/java/sh/libre/scim/core/AbstractScimService.java b/src/main/java/sh/libre/scim/core/AbstractScimService.java index 4e42351c242cb69b307ccdadea8bbd3f75cb0e45..8f92a2268147689f42cba210a4407b67fae3c328 100644 --- a/src/main/java/sh/libre/scim/core/AbstractScimService.java +++ b/src/main/java/sh/libre/scim/core/AbstractScimService.java @@ -35,7 +35,7 @@ public abstract class AbstractScimService<RMM extends RoleMapperModel, S extends this.scimClient = ScimClient.open(scimProviderConfiguration, type); } - public void create(RMM roleMapperModel) { + public void create(RMM roleMapperModel) throws ScimPropagationException { boolean skip = isSkip(roleMapperModel); if (skip) return; @@ -71,7 +71,7 @@ public abstract class AbstractScimService<RMM extends RoleMapperModel, S extends return keycloakSession; } - public void replace(RMM roleMapperModel) { + public void replace(RMM roleMapperModel) throws ScimPropagationException { try { if (isSkip(roleMapperModel)) return; @@ -84,37 +84,42 @@ public abstract class AbstractScimService<RMM extends RoleMapperModel, S extends LOGGER.warnf("failed to replace resource %s, scim mapping not found", getId(roleMapperModel)); } catch (Exception e) { LOGGER.error(e); + throw new ScimPropagationException("[SCIM] Error while replacing SCIM resource", e); } } protected abstract S toScimForReplace(RMM roleMapperModel, EntityOnRemoteScimId externalId); - public void delete(KeycloakId id) { + public void delete(KeycloakId id) throws ScimPropagationException { try { ScimResource resource = findById(id).get(); EntityOnRemoteScimId externalId = resource.getExternalIdAsEntityOnRemoteScimId(); scimClient.delete(externalId); getScimResourceDao().delete(resource); } catch (NoSuchElementException e) { - LOGGER.warnf("Failed to delete resource %s, scim mapping not found", id); + throw new ScimPropagationException("Failed to delete resource %s, scim mapping not found : " + id, e); } } - public void refreshResources(SynchronizationResult syncRes) { + public void refreshResources(SynchronizationResult syncRes) throws ScimPropagationException { LOGGER.info("Refresh resources"); getResourceStream().forEach(resource -> { KeycloakId id = getId(resource); LOGGER.infof("Reconciling local resource %s", id); if (!isSkipRefresh(resource)) { try { - findById(id).get(); - LOGGER.info("Replacing it"); - replace(resource); - } catch (NoSuchElementException e) { - LOGGER.info("Creating it"); - create(resource); + try { + findById(id).get(); + LOGGER.info("Replacing it"); + replace(resource); + } catch (NoSuchElementException e) { + LOGGER.info("Creating it"); + create(resource); + } + syncRes.increaseUpdated(); + } catch (ScimPropagationException e) { + // TODO handle exception } - syncRes.increaseUpdated(); } }); } @@ -187,7 +192,7 @@ public abstract class AbstractScimService<RMM extends RoleMapperModel, S extends protected abstract boolean entityExists(KeycloakId keycloakId); - public void sync(SynchronizationResult syncRes) { + public void sync(SynchronizationResult syncRes) throws ScimPropagationException { if (this.scimProviderConfiguration.isSyncImport()) { this.importResources(syncRes); } diff --git a/src/main/java/sh/libre/scim/core/ScimClient.java b/src/main/java/sh/libre/scim/core/ScimClient.java index 35ab5620bc1fd44e08535d0afa21217059d8f7a6..22bfa0e7ec06a0472cc56be271ffc1cab0cc3132 100644 --- a/src/main/java/sh/libre/scim/core/ScimClient.java +++ b/src/main/java/sh/libre/scim/core/ScimClient.java @@ -19,7 +19,7 @@ import java.util.List; import java.util.Map; public class ScimClient<S extends ResourceNode> implements AutoCloseable { - private final Logger logger = Logger.getLogger(ScimClient.class); + private static final Logger LOGGER = Logger.getLogger(ScimClient.class); private final RetryRegistry retryRegistry; @@ -83,8 +83,8 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable { private void checkResponseIsSuccess(ServerResponse<S> response) { if (!response.isSuccess()) { - logger.warn(response.getResponseBody()); - logger.warn(response.getHttpStatus()); + LOGGER.warn(response.getResponseBody()); + LOGGER.warn(response.getHttpStatus()); } } @@ -98,7 +98,7 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable { public void replace(EntityOnRemoteScimId externalId, S scimForReplace) { Retry retry = retryRegistry.retry("replace-%s".formatted(externalId.asString())); - logger.warn(scimForReplace); + LOGGER.warn(scimForReplace); ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder .update(getResourceClass(), getScimEndpoint(), externalId.asString()) .setResource(scimForReplace) diff --git a/src/main/java/sh/libre/scim/core/ScimDispatcher.java b/src/main/java/sh/libre/scim/core/ScimDispatcher.java index 6804c065544429c94356767ecf1821502df448da..6be68dc3d2cd3c9bf48968474e1b18e1935cc5fa 100644 --- a/src/main/java/sh/libre/scim/core/ScimDispatcher.java +++ b/src/main/java/sh/libre/scim/core/ScimDispatcher.java @@ -6,9 +6,10 @@ import org.keycloak.models.KeycloakSession; import sh.libre.scim.storage.ScimEndpointConfigurationStorageProviderFactory; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; -import java.util.function.Consumer; +import java.util.Set; /** * In charge of sending SCIM Request to all registered Scim endpoints. @@ -69,38 +70,64 @@ public class ScimDispatcher { } } - public void dispatchUserModificationToAll(Consumer<UserScimService> operationToDispatch) { + public void dispatchUserModificationToAll(SCIMPropagationConsumer<UserScimService> operationToDispatch) { initializeClientsIfNeeded(); - userScimServices.forEach(operationToDispatch); - logger.infof("[SCIM] User operation dispatched to %d SCIM server", userScimServices.size()); + Set<UserScimService> servicesCorrectlyPropagated = new LinkedHashSet<>(); + userScimServices.forEach(userScimService -> { + try { + operationToDispatch.acceptThrows(userScimService); + servicesCorrectlyPropagated.add(userScimService); + } catch (ScimPropagationException e) { + logAndRollback(userScimService.getConfiguration(), e); + } + }); + // TODO we could iterate on servicesCorrectlyPropagated to undo modification + logger.infof("[SCIM] User operation dispatched to %d SCIM server", servicesCorrectlyPropagated.size()); } - public void dispatchGroupModificationToAll(Consumer<GroupScimService> operationToDispatch) { + public void dispatchGroupModificationToAll(SCIMPropagationConsumer<GroupScimService> operationToDispatch) { initializeClientsIfNeeded(); - groupScimServices.forEach(operationToDispatch); - logger.infof("[SCIM] Group operation dispatched to %d SCIM server", groupScimServices.size()); + Set<GroupScimService> servicesCorrectlyPropagated = new LinkedHashSet<>(); + groupScimServices.forEach(groupScimService -> { + try { + operationToDispatch.acceptThrows(groupScimService); + servicesCorrectlyPropagated.add(groupScimService); + } catch (ScimPropagationException e) { + logAndRollback(groupScimService.getConfiguration(), e); + } + }); + // TODO we could iterate on servicesCorrectlyPropagated to undo modification + logger.infof("[SCIM] Group operation dispatched to %d SCIM server", servicesCorrectlyPropagated.size()); } - public void dispatchUserModificationToOne(ComponentModel scimServerConfiguration, Consumer<UserScimService> operationToDispatch) { + public void dispatchUserModificationToOne(ComponentModel scimServerConfiguration, SCIMPropagationConsumer<UserScimService> operationToDispatch) { initializeClientsIfNeeded(); // Scim client should already have been created Optional<UserScimService> matchingClient = userScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst(); if (matchingClient.isPresent()) { - operationToDispatch.accept(matchingClient.get()); - logger.infof("[SCIM] User operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getId()); + try { + operationToDispatch.acceptThrows(matchingClient.get()); + logger.infof("[SCIM] User operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getId()); + } catch (ScimPropagationException e) { + logAndRollback(matchingClient.get().getConfiguration(), e); + } } else { logger.error("[SCIM] Could not find a Scim Client matching endpoint configuration" + scimServerConfiguration.getId()); } } - public void dispatchGroupModificationToOne(ComponentModel scimServerConfiguration, Consumer<GroupScimService> operationToDispatch) { + public void dispatchGroupModificationToOne(ComponentModel scimServerConfiguration, SCIMPropagationConsumer<GroupScimService> operationToDispatch) { initializeClientsIfNeeded(); // Scim client should already have been created Optional<GroupScimService> matchingClient = groupScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst(); if (matchingClient.isPresent()) { - operationToDispatch.accept(matchingClient.get()); - logger.infof("[SCIM] Group operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getId()); + try { + operationToDispatch.acceptThrows(matchingClient.get()); + logger.infof("[SCIM] Group operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getId()); + } catch (ScimPropagationException e) { + logAndRollback(matchingClient.get().getConfiguration(), e); + } } else { logger.error("[SCIM] Could not find a Scim Client matching endpoint configuration" + scimServerConfiguration.getId()); } @@ -117,10 +144,27 @@ public class ScimDispatcher { userScimServices.clear(); } + private void logAndRollback(ScrimProviderConfiguration scimServerConfiguration, ScimPropagationException e) { + logger.error("[SCIM] Error while propagating to SCIM endpoint " + scimServerConfiguration.getId(), e); + session.getTransactionManager().rollback(); + } + private void initializeClientsIfNeeded() { if (!clientsInitialized) { clientsInitialized = true; refreshActiveScimEndpoints(); } } + + /** + * A Consumer that throws ScimPropagationException. + * + * @param <T> An {@link AbstractScimService to call} + */ + @FunctionalInterface + public interface SCIMPropagationConsumer<T> { + + void acceptThrows(T elem) throws ScimPropagationException; + + } } diff --git a/src/main/java/sh/libre/scim/core/ScimPropagationException.java b/src/main/java/sh/libre/scim/core/ScimPropagationException.java index 145c7c4c0aac9c85413c19dfc415a09b1cd35410..135b555961234788d6e3b6ede89905bfa761dcad 100644 --- a/src/main/java/sh/libre/scim/core/ScimPropagationException.java +++ b/src/main/java/sh/libre/scim/core/ScimPropagationException.java @@ -1,4 +1,12 @@ package sh.libre.scim.core; -public class ScimPropagationException { +public class ScimPropagationException extends Exception { + + public ScimPropagationException(String message) { + super(message); + } + + public ScimPropagationException(String message, Exception e) { + super(message, e); + } }