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);
}
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);
}

View file

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

View file

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

View file

@ -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);
}
}