Rollback on ScimPropagationException

This commit is contained in:
Alex Morel 2024-06-21 15:30:24 +02:00
parent 81d9f5424c
commit 0cac71c4a1
4 changed files with 88 additions and 31 deletions

View file

@ -35,7 +35,7 @@ public abstract class AbstractScimService<RMM extends RoleMapperModel, S extends
this.scimClient = ScimClient.open(scimProviderConfiguration, type); this.scimClient = ScimClient.open(scimProviderConfiguration, type);
} }
public void create(RMM roleMapperModel) { public void create(RMM roleMapperModel) throws ScimPropagationException {
boolean skip = isSkip(roleMapperModel); boolean skip = isSkip(roleMapperModel);
if (skip) if (skip)
return; return;
@ -71,7 +71,7 @@ public abstract class AbstractScimService<RMM extends RoleMapperModel, S extends
return keycloakSession; return keycloakSession;
} }
public void replace(RMM roleMapperModel) { public void replace(RMM roleMapperModel) throws ScimPropagationException {
try { try {
if (isSkip(roleMapperModel)) if (isSkip(roleMapperModel))
return; 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)); LOGGER.warnf("failed to replace resource %s, scim mapping not found", getId(roleMapperModel));
} catch (Exception e) { } catch (Exception e) {
LOGGER.error(e); LOGGER.error(e);
throw new ScimPropagationException("[SCIM] Error while replacing SCIM resource", e);
} }
} }
protected abstract S toScimForReplace(RMM roleMapperModel, EntityOnRemoteScimId externalId); protected abstract S toScimForReplace(RMM roleMapperModel, EntityOnRemoteScimId externalId);
public void delete(KeycloakId id) { public void delete(KeycloakId id) throws ScimPropagationException {
try { try {
ScimResource resource = findById(id).get(); ScimResource resource = findById(id).get();
EntityOnRemoteScimId externalId = resource.getExternalIdAsEntityOnRemoteScimId(); EntityOnRemoteScimId externalId = resource.getExternalIdAsEntityOnRemoteScimId();
scimClient.delete(externalId); scimClient.delete(externalId);
getScimResourceDao().delete(resource); getScimResourceDao().delete(resource);
} catch (NoSuchElementException e) { } 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"); LOGGER.info("Refresh resources");
getResourceStream().forEach(resource -> { getResourceStream().forEach(resource -> {
KeycloakId id = getId(resource); KeycloakId id = getId(resource);
LOGGER.infof("Reconciling local resource %s", id); LOGGER.infof("Reconciling local resource %s", id);
if (!isSkipRefresh(resource)) { if (!isSkipRefresh(resource)) {
try { try {
findById(id).get(); try {
LOGGER.info("Replacing it"); findById(id).get();
replace(resource); LOGGER.info("Replacing it");
} catch (NoSuchElementException e) { replace(resource);
LOGGER.info("Creating it"); } catch (NoSuchElementException e) {
create(resource); 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); protected abstract boolean entityExists(KeycloakId keycloakId);
public void sync(SynchronizationResult syncRes) { public void sync(SynchronizationResult syncRes) throws ScimPropagationException {
if (this.scimProviderConfiguration.isSyncImport()) { if (this.scimProviderConfiguration.isSyncImport()) {
this.importResources(syncRes); this.importResources(syncRes);
} }

View file

@ -19,7 +19,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public class ScimClient<S extends ResourceNode> implements AutoCloseable { 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; private final RetryRegistry retryRegistry;
@ -83,8 +83,8 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
private void checkResponseIsSuccess(ServerResponse<S> response) { private void checkResponseIsSuccess(ServerResponse<S> response) {
if (!response.isSuccess()) { if (!response.isSuccess()) {
logger.warn(response.getResponseBody()); LOGGER.warn(response.getResponseBody());
logger.warn(response.getHttpStatus()); LOGGER.warn(response.getHttpStatus());
} }
} }
@ -98,7 +98,7 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
public void replace(EntityOnRemoteScimId externalId, S scimForReplace) { public void replace(EntityOnRemoteScimId externalId, S scimForReplace) {
Retry retry = retryRegistry.retry("replace-%s".formatted(externalId.asString())); Retry retry = retryRegistry.retry("replace-%s".formatted(externalId.asString()));
logger.warn(scimForReplace); LOGGER.warn(scimForReplace);
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
.update(getResourceClass(), getScimEndpoint(), externalId.asString()) .update(getResourceClass(), getScimEndpoint(), externalId.asString())
.setResource(scimForReplace) .setResource(scimForReplace)

View file

@ -6,9 +6,10 @@ import org.keycloak.models.KeycloakSession;
import sh.libre.scim.storage.ScimEndpointConfigurationStorageProviderFactory; import sh.libre.scim.storage.ScimEndpointConfigurationStorageProviderFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.Set;
/** /**
* In charge of sending SCIM Request to all registered Scim endpoints. * 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(); initializeClientsIfNeeded();
userScimServices.forEach(operationToDispatch); Set<UserScimService> servicesCorrectlyPropagated = new LinkedHashSet<>();
logger.infof("[SCIM] User operation dispatched to %d SCIM server", userScimServices.size()); 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(); initializeClientsIfNeeded();
groupScimServices.forEach(operationToDispatch); Set<GroupScimService> servicesCorrectlyPropagated = new LinkedHashSet<>();
logger.infof("[SCIM] Group operation dispatched to %d SCIM server", groupScimServices.size()); 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(); initializeClientsIfNeeded();
// Scim client should already have been created // Scim client should already have been created
Optional<UserScimService> matchingClient = userScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst(); Optional<UserScimService> matchingClient = userScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst();
if (matchingClient.isPresent()) { if (matchingClient.isPresent()) {
operationToDispatch.accept(matchingClient.get()); try {
logger.infof("[SCIM] User operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getId()); 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 { } else {
logger.error("[SCIM] Could not find a Scim Client matching endpoint configuration" + scimServerConfiguration.getId()); 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(); initializeClientsIfNeeded();
// Scim client should already have been created // Scim client should already have been created
Optional<GroupScimService> matchingClient = groupScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst(); Optional<GroupScimService> matchingClient = groupScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst();
if (matchingClient.isPresent()) { if (matchingClient.isPresent()) {
operationToDispatch.accept(matchingClient.get()); try {
logger.infof("[SCIM] Group operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getId()); 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 { } else {
logger.error("[SCIM] Could not find a Scim Client matching endpoint configuration" + scimServerConfiguration.getId()); logger.error("[SCIM] Could not find a Scim Client matching endpoint configuration" + scimServerConfiguration.getId());
} }
@ -117,10 +144,27 @@ public class ScimDispatcher {
userScimServices.clear(); 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() { private void initializeClientsIfNeeded() {
if (!clientsInitialized) { if (!clientsInitialized) {
clientsInitialized = true; clientsInitialized = true;
refreshActiveScimEndpoints(); refreshActiveScimEndpoints();
} }
} }
/**
* A Consumer that throws ScimPropagationException.
*
* @param <T> An {@link AbstractScimService to call}
*/
@FunctionalInterface
public interface SCIMPropagationConsumer<T> {
void acceptThrows(T elem) throws ScimPropagationException;
}
} }

View file

@ -1,4 +1,12 @@
package sh.libre.scim.core; 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);
}
} }