Exception Handler - Step 4 : define SkipOrStop strategy
This commit is contained in:
parent
9d52c0eb4b
commit
09d0b6544b
12 changed files with 389 additions and 130 deletions
|
@ -8,7 +8,7 @@ import org.keycloak.models.RoleMapperModel;
|
|||
import org.keycloak.storage.user.SynchronizationResult;
|
||||
import sh.libre.scim.core.exceptions.ErrorForScimEndpointException;
|
||||
import sh.libre.scim.core.exceptions.InconsistentScimDataException;
|
||||
import sh.libre.scim.core.exceptions.ScimPropagationException;
|
||||
import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
|
||||
import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
|
||||
import sh.libre.scim.jpa.ScimResourceDao;
|
||||
import sh.libre.scim.jpa.ScimResourceMapping;
|
||||
|
@ -20,24 +20,29 @@ import java.util.Set;
|
|||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
// TODO Document K and S
|
||||
/**
|
||||
* A service in charge of synchronisation (CRUD) between
|
||||
* a Keykloak Role (UserModel, GroupModel) and a SCIM Resource (User,Group).
|
||||
*
|
||||
* @param <K> The Keycloack Model (e.g. UserModel, GroupModel)
|
||||
* @param <S> The SCIM Resource (e.g. User, Group)
|
||||
*/
|
||||
public abstract class AbstractScimService<K extends RoleMapperModel, S extends ResourceNode> implements AutoCloseable {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(AbstractScimService.class);
|
||||
|
||||
private final KeycloakSession keycloakSession;
|
||||
|
||||
private final ScrimProviderConfiguration scimProviderConfiguration;
|
||||
|
||||
protected final SkipOrStopStrategy skipOrStopStrategy;
|
||||
private final ScrimEndPointConfiguration scimProviderConfiguration;
|
||||
private final ScimResourceType type;
|
||||
|
||||
private final ScimClient<S> scimClient;
|
||||
|
||||
protected AbstractScimService(KeycloakSession keycloakSession, ScrimProviderConfiguration scimProviderConfiguration, ScimResourceType type) {
|
||||
protected AbstractScimService(KeycloakSession keycloakSession, ScrimEndPointConfiguration scimProviderConfiguration, ScimResourceType type, SkipOrStopStrategy skipOrStopStrategy) {
|
||||
this.keycloakSession = keycloakSession;
|
||||
this.scimProviderConfiguration = scimProviderConfiguration;
|
||||
this.type = type;
|
||||
this.scimClient = ScimClient.open(scimProviderConfiguration, type);
|
||||
this.skipOrStopStrategy = skipOrStopStrategy;
|
||||
}
|
||||
|
||||
public void create(K roleMapperModel) throws InconsistentScimDataException, ErrorForScimEndpointException {
|
||||
|
@ -68,7 +73,7 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
|||
scimClient.update(entityOnRemoteScimId, scimForReplace);
|
||||
}
|
||||
|
||||
protected abstract S scimRequestBodyForUpdate(K roleMapperModel, EntityOnRemoteScimId externalId);
|
||||
protected abstract S scimRequestBodyForUpdate(K roleMapperModel, EntityOnRemoteScimId externalId) throws InconsistentScimDataException;
|
||||
|
||||
public void delete(KeycloakId id) throws InconsistentScimDataException, ErrorForScimEndpointException {
|
||||
ScimResourceMapping resource = findMappingById(id)
|
||||
|
@ -78,17 +83,30 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
|||
getScimResourceDao().delete(resource);
|
||||
}
|
||||
|
||||
public void pushResourcesToScim(SynchronizationResult syncRes) throws ErrorForScimEndpointException, InconsistentScimDataException {
|
||||
public void pushAllResourcesToScim(SynchronizationResult syncRes) throws ErrorForScimEndpointException, InconsistentScimDataException {
|
||||
LOGGER.info("[SCIM] Push resources to endpoint " + this.getConfiguration().getEndPoint());
|
||||
try (Stream<K> resourcesStream = getResourceStream()) {
|
||||
Set<K> resources = resourcesStream.collect(Collectors.toUnmodifiableSet());
|
||||
for (K resource : resources) {
|
||||
try {
|
||||
KeycloakId id = getId(resource);
|
||||
pushSingleResourceToScim(syncRes, resource, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void pullAllResourcesFromScim(SynchronizationResult syncRes) throws UnexpectedScimDataException, InconsistentScimDataException, ErrorForScimEndpointException {
|
||||
LOGGER.info("[SCIM] Pull resources from endpoint " + this.getConfiguration().getEndPoint());
|
||||
for (S resource : scimClient.listResources()) {
|
||||
pullSingleResourceFromScim(syncRes, resource);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushSingleResourceToScim(SynchronizationResult syncRes, K resource, KeycloakId id) throws ErrorForScimEndpointException, InconsistentScimDataException {
|
||||
try {
|
||||
LOGGER.infof("[SCIM] Reconciling local resource %s", id);
|
||||
if (shouldIgnoreForScimSynchronization(resource)) {
|
||||
LOGGER.infof("[SCIM] Skip local resource %s", id);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
if (findMappingById(id).isPresent()) {
|
||||
LOGGER.info("[SCIM] Replacing it");
|
||||
|
@ -99,17 +117,22 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
|||
}
|
||||
syncRes.increaseUpdated();
|
||||
} catch (ErrorForScimEndpointException e) {
|
||||
// TODO Error handling use strategy SOUPLE or STICT to determine if we re-trhoguh or log & continue
|
||||
if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) {
|
||||
LOGGER.warn("Error while syncing " + id + " to endpoint " + getConfiguration().getEndPoint());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} catch (InconsistentScimDataException e) {
|
||||
if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) {
|
||||
LOGGER.warn("Inconsistent data for element " + id + " and endpoint " + getConfiguration().getEndPoint());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void pullResourcesFromScim(SynchronizationResult syncRes) {
|
||||
LOGGER.info("[SCIM] Import resources from endpoint " + this.getConfiguration().getEndPoint());
|
||||
for (S resource : scimClient.listResources()) {
|
||||
private void pullSingleResourceFromScim(SynchronizationResult syncRes, S resource) throws UnexpectedScimDataException, InconsistentScimDataException, ErrorForScimEndpointException {
|
||||
try {
|
||||
LOGGER.infof("[SCIM] Reconciling remote resource %s", resource);
|
||||
EntityOnRemoteScimId externalId = resource.getId()
|
||||
|
@ -122,7 +145,7 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
|||
ScimResourceMapping mapping = optionalMapping.get();
|
||||
if (entityExists(mapping.getIdAsKeycloakId())) {
|
||||
LOGGER.debug("[SCIM] Valid mapping found, skipping");
|
||||
continue;
|
||||
return;
|
||||
} else {
|
||||
LOGGER.info("[SCIM] Delete a dangling mapping");
|
||||
getScimResourceDao().delete(mapping);
|
||||
|
@ -151,15 +174,31 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
|||
case NOTHING -> LOGGER.info("[SCIM] Import action set to NOTHING");
|
||||
}
|
||||
}
|
||||
} catch (ScimPropagationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (UnexpectedScimDataException e) {
|
||||
if (skipOrStopStrategy.skipInvalidDataFromScimEndpoint(getConfiguration())) {
|
||||
LOGGER.warn("[SCIM] Skipping element synchronisation because of invalid Scim Data for element " + resource.getId() + " : " + e.getMessage());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
||||
} catch (InconsistentScimDataException e) {
|
||||
if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) {
|
||||
LOGGER.warn("[SCIM] Skipping element synchronisation because of inconsistent mapping for element " + resource.getId() + " : " + e.getMessage());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} catch (ErrorForScimEndpointException e) {
|
||||
// Can only occur in case of a DELETE_REMOTE conflict action
|
||||
if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) {
|
||||
LOGGER.warn("[SCIM] Could not delete SCIM resource " + resource.getId() + " during synchronisation");
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected abstract S scimRequestBodyForCreate(K roleMapperModel);
|
||||
|
||||
protected abstract S scimRequestBodyForCreate(K roleMapperModel) throws InconsistentScimDataException;
|
||||
|
||||
protected abstract KeycloakId getId(K roleMapperModel);
|
||||
|
||||
|
@ -192,12 +231,12 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
|||
|
||||
protected abstract boolean entityExists(KeycloakId keycloakId);
|
||||
|
||||
public void sync(SynchronizationResult syncRes) throws InconsistentScimDataException, ErrorForScimEndpointException {
|
||||
if (this.scimProviderConfiguration.isSyncImport()) {
|
||||
this.pullResourcesFromScim(syncRes);
|
||||
public void sync(SynchronizationResult syncRes) throws InconsistentScimDataException, ErrorForScimEndpointException, UnexpectedScimDataException {
|
||||
if (this.scimProviderConfiguration.isPullFromScimSynchronisationActivated()) {
|
||||
this.pullAllResourcesFromScim(syncRes);
|
||||
}
|
||||
if (this.scimProviderConfiguration.isSyncRefresh()) {
|
||||
this.pushResourcesToScim(syncRes);
|
||||
if (this.scimProviderConfiguration.isPushToScimSynchronisationActivated()) {
|
||||
this.pushAllResourcesToScim(syncRes);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,7 +264,7 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
|||
scimClient.close();
|
||||
}
|
||||
|
||||
public ScrimProviderConfiguration getConfiguration() {
|
||||
public ScrimEndPointConfiguration getConfiguration() {
|
||||
return scimProviderConfiguration;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import sh.libre.scim.core.exceptions.InconsistentScimDataException;
|
||||
import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
|
||||
import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
|
||||
import sh.libre.scim.jpa.ScimResourceMapping;
|
||||
|
||||
|
@ -23,8 +25,8 @@ import java.util.stream.Stream;
|
|||
public class GroupScimService extends AbstractScimService<GroupModel, Group> {
|
||||
private final Logger LOGGER = Logger.getLogger(GroupScimService.class);
|
||||
|
||||
public GroupScimService(KeycloakSession keycloakSession, ScrimProviderConfiguration scimProviderConfiguration) {
|
||||
super(keycloakSession, scimProviderConfiguration, ScimResourceType.GROUP);
|
||||
public GroupScimService(KeycloakSession keycloakSession, ScrimEndPointConfiguration scimProviderConfiguration, SkipOrStopStrategy skipOrStopStrategy) {
|
||||
super(keycloakSession, scimProviderConfiguration, ScimResourceType.GROUP, skipOrStopStrategy);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -66,6 +68,7 @@ public class GroupScimService extends AbstractScimService<GroupModel, Group> {
|
|||
.orElseThrow(() -> new UnexpectedScimDataException("can't create group member for group '%s' without id: ".formatted(displayName) + resource));
|
||||
KeycloakId userId = getScimResourceDao().findUserByExternalId(externalId)
|
||||
.map(ScimResourceMapping::getIdAsKeycloakId)
|
||||
// TODO Exception handling : here if think this is a InconsistentScimData : Scim member is valid, but not mapped yet in our keycloak
|
||||
.orElseThrow(() -> new UnexpectedScimDataException("can't find mapping for group member %s".formatted(externalId)));
|
||||
UserModel userModel = getKeycloakDao().getUserById(userId);
|
||||
userModel.joinGroup(group);
|
||||
|
@ -85,7 +88,7 @@ public class GroupScimService extends AbstractScimService<GroupModel, Group> {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Group scimRequestBodyForCreate(GroupModel groupModel) {
|
||||
protected Group scimRequestBodyForCreate(GroupModel groupModel) throws InconsistentScimDataException {
|
||||
Set<KeycloakId> members = getKeycloakDao().getGroupMembers(groupModel);
|
||||
Group group = new Group();
|
||||
group.setExternalId(groupModel.getId());
|
||||
|
@ -101,15 +104,19 @@ public class GroupScimService extends AbstractScimService<GroupModel, Group> {
|
|||
groupMember.setRef(ref.toString());
|
||||
group.addMember(groupMember);
|
||||
} else {
|
||||
// TODO Exception Handling : should we throw an exception when some group members can't be found ?
|
||||
LOGGER.warnf("member %s not found for group %s", member, groupModel.getId());
|
||||
String message = "Unmapped member " + member + " for group " + groupModel.getId();
|
||||
if (skipOrStopStrategy.allowMissingMembersWhenPushingGroupToScim(this.getConfiguration())) {
|
||||
LOGGER.warn(message);
|
||||
} else {
|
||||
throw new InconsistentScimDataException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Group scimRequestBodyForUpdate(GroupModel groupModel, EntityOnRemoteScimId externalId) {
|
||||
protected Group scimRequestBodyForUpdate(GroupModel groupModel, EntityOnRemoteScimId externalId) throws InconsistentScimDataException {
|
||||
Group group = scimRequestBodyForCreate(groupModel);
|
||||
group.setId(externalId.asString());
|
||||
Meta meta = newMetaLocation(externalId);
|
||||
|
|
|
@ -43,7 +43,7 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
|
|||
this.scimResourceType = scimResourceType;
|
||||
}
|
||||
|
||||
public static ScimClient open(ScrimProviderConfiguration scimProviderConfiguration, ScimResourceType scimResourceType) {
|
||||
public static ScimClient open(ScrimEndPointConfiguration scimProviderConfiguration, ScimResourceType scimResourceType) {
|
||||
String scimApplicationBaseUrl = scimProviderConfiguration.getEndPoint();
|
||||
Map<String, String> httpHeaders = new HashMap<>();
|
||||
httpHeaders.put(HttpHeaders.AUTHORIZATION, scimProviderConfiguration.getAuthorizationHeaderValue());
|
||||
|
|
|
@ -5,6 +5,8 @@ import org.keycloak.component.ComponentModel;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import sh.libre.scim.core.exceptions.ScimExceptionHandler;
|
||||
import sh.libre.scim.core.exceptions.ScimPropagationException;
|
||||
import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
|
||||
import sh.libre.scim.core.exceptions.SkipOrStopStrategyFactory;
|
||||
import sh.libre.scim.storage.ScimEndpointConfigurationStorageProviderFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -22,6 +24,7 @@ public class ScimDispatcher {
|
|||
|
||||
private final KeycloakSession session;
|
||||
private final ScimExceptionHandler exceptionHandler;
|
||||
private final SkipOrStopStrategy skipOrStopStrategy;
|
||||
private boolean clientsInitialized = false;
|
||||
private final List<UserScimService> userScimServices = new ArrayList<>();
|
||||
private final List<GroupScimService> groupScimServices = new ArrayList<>();
|
||||
|
@ -30,6 +33,10 @@ public class ScimDispatcher {
|
|||
public ScimDispatcher(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.exceptionHandler = new ScimExceptionHandler(session);
|
||||
// By default, use a permissive Skip or Stop strategy
|
||||
this.skipOrStopStrategy = SkipOrStopStrategyFactory.create(
|
||||
SkipOrStopStrategyFactory.SkipOrStopApproach.ALWAYS_SKIP_AND_CONTINUE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,19 +54,24 @@ public class ScimDispatcher {
|
|||
.filter(m -> ScimEndpointConfigurationStorageProviderFactory.ID.equals(m.getProviderId())
|
||||
&& m.get("enabled", true))
|
||||
.forEach(scimEndpointConfigurationRaw -> {
|
||||
ScrimProviderConfiguration scrimProviderConfiguration = new ScrimProviderConfiguration(scimEndpointConfigurationRaw);
|
||||
try {
|
||||
ScrimEndPointConfiguration scrimEndPointConfiguration = new ScrimEndPointConfiguration(scimEndpointConfigurationRaw);
|
||||
|
||||
// Step 3 : create scim clients for each endpoint
|
||||
if (scimEndpointConfigurationRaw.get(ScrimProviderConfiguration.CONF_KEY_PROPAGATION_GROUP, false)) {
|
||||
GroupScimService groupScimService = new GroupScimService(session, scrimProviderConfiguration);
|
||||
if (scimEndpointConfigurationRaw.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP, false)) {
|
||||
GroupScimService groupScimService = new GroupScimService(session, scrimEndPointConfiguration, skipOrStopStrategy);
|
||||
groupScimServices.add(groupScimService);
|
||||
}
|
||||
if (scimEndpointConfigurationRaw.get(ScrimProviderConfiguration.CONF_KEY_PROPAGATION_USER, false)) {
|
||||
UserScimService userScimService = new UserScimService(session, scrimProviderConfiguration);
|
||||
if (scimEndpointConfigurationRaw.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER, false)) {
|
||||
UserScimService userScimService = new UserScimService(session, scrimEndPointConfiguration, skipOrStopStrategy);
|
||||
userScimServices.add(userScimService);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
exceptionHandler.handleInvalidEndpointConfiguration(scimEndpointConfigurationRaw, e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (skipOrStopStrategy.allowInvalidEndpointConfiguration()) {
|
||||
LOGGER.warn("[SCIM] Invalid Endpoint configuration " + scimEndpointConfigurationRaw.getId(), e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package sh.libre.scim.core;
|
|||
import de.captaingoldfish.scim.sdk.client.http.BasicAuth;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
|
||||
public class ScrimProviderConfiguration {
|
||||
public class ScrimEndPointConfiguration {
|
||||
// Configuration keys : also used in Admin Console page
|
||||
public static final String CONF_KEY_AUTH_MODE = "auth-mode";
|
||||
public static final String CONF_KEY_AUTH_PASSWORD = "auth-pass";
|
||||
|
@ -21,10 +21,10 @@ public class ScrimProviderConfiguration {
|
|||
private final String contentType;
|
||||
private final String authorizationHeaderValue;
|
||||
private final ImportAction importAction;
|
||||
private final boolean syncImport;
|
||||
private final boolean syncRefresh;
|
||||
private final boolean pullFromScimSynchronisationActivated;
|
||||
private final boolean pushToScimSynchronisationActivated;
|
||||
|
||||
public ScrimProviderConfiguration(ComponentModel scimProviderConfiguration) {
|
||||
public ScrimEndPointConfiguration(ComponentModel scimProviderConfiguration) {
|
||||
try {
|
||||
AuthMode authMode = AuthMode.valueOf(scimProviderConfiguration.get(CONF_KEY_AUTH_MODE));
|
||||
|
||||
|
@ -43,19 +43,19 @@ public class ScrimProviderConfiguration {
|
|||
endPoint = scimProviderConfiguration.get(CONF_KEY_ENDPOINT, "");
|
||||
id = scimProviderConfiguration.getId();
|
||||
importAction = ImportAction.valueOf(scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT_ACTION));
|
||||
syncImport = scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT, false);
|
||||
syncRefresh = scimProviderConfiguration.get(CONF_KEY_SYNC_REFRESH, false);
|
||||
pullFromScimSynchronisationActivated = scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT, false);
|
||||
pushToScimSynchronisationActivated = scimProviderConfiguration.get(CONF_KEY_SYNC_REFRESH, false);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("authMode '" + scimProviderConfiguration.get(CONF_KEY_AUTH_MODE) + "' is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSyncRefresh() {
|
||||
return syncRefresh;
|
||||
public boolean isPushToScimSynchronisationActivated() {
|
||||
return pushToScimSynchronisationActivated;
|
||||
}
|
||||
|
||||
public boolean isSyncImport() {
|
||||
return syncImport;
|
||||
public boolean isPullFromScimSynchronisationActivated() {
|
||||
return pullFromScimSynchronisationActivated;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
|
@ -14,6 +14,7 @@ import org.keycloak.models.RoleMapperModel;
|
|||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import sh.libre.scim.core.exceptions.InconsistentScimDataException;
|
||||
import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
|
||||
import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -26,8 +27,9 @@ public class UserScimService extends AbstractScimService<UserModel, User> {
|
|||
|
||||
public UserScimService(
|
||||
KeycloakSession keycloakSession,
|
||||
ScrimProviderConfiguration scimProviderConfiguration) {
|
||||
super(keycloakSession, scimProviderConfiguration, ScimResourceType.USER);
|
||||
ScrimEndPointConfiguration scimProviderConfiguration,
|
||||
SkipOrStopStrategy skipOrStopStrategy) {
|
||||
super(keycloakSession, scimProviderConfiguration, ScimResourceType.USER, skipOrStopStrategy);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package sh.libre.scim.core.exceptions;
|
||||
|
||||
import sh.libre.scim.core.ScrimEndPointConfiguration;
|
||||
|
||||
/**
|
||||
* In charge of deciding, when facing a SCIM-related issue during an operation (e.g User creation),
|
||||
* whether we should :
|
||||
* - Log the issue and let the operation succeed in Keycloack database (potentially unsynchronising
|
||||
* Keycloack with the SCIM servers)
|
||||
* - Rollback the whole operation
|
||||
*/
|
||||
public interface RollbackStrategy {
|
||||
|
||||
/**
|
||||
* Indicates whether we should rollback the whole transaction because of the given exception.
|
||||
*
|
||||
* @param configuration The SCIM Endpoint configuration for which the exception occured
|
||||
* @param e the exception that we have to handle
|
||||
* @return true if transaction should be rolled back, false if we should log and continue operation
|
||||
*/
|
||||
boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package sh.libre.scim.core.exceptions;
|
||||
|
||||
import sh.libre.scim.core.ScrimEndPointConfiguration;
|
||||
|
||||
public class RollbackStrategyFactory {
|
||||
|
||||
public static RollbackStrategy create(RollbackApproach approach) {
|
||||
// We could imagine more fine-grained rollback strategies (e.g. based on each Scim endpoint configuration)
|
||||
return switch (approach) {
|
||||
case ALWAYS_ROLLBACK -> new AlwaysRollbackStrategy();
|
||||
case NEVER_ROLLBACK -> new NeverRollbackStrategy();
|
||||
};
|
||||
}
|
||||
|
||||
public enum RollbackApproach {
|
||||
ALWAYS_ROLLBACK, NEVER_ROLLBACK
|
||||
}
|
||||
|
||||
private static final class AlwaysRollbackStrategy implements RollbackStrategy {
|
||||
|
||||
@Override
|
||||
public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NeverRollbackStrategy implements RollbackStrategy {
|
||||
|
||||
@Override
|
||||
public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
package sh.libre.scim.core.exceptions;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import sh.libre.scim.core.ScrimProviderConfiguration;
|
||||
import sh.libre.scim.core.ScrimEndPointConfiguration;
|
||||
|
||||
/**
|
||||
* In charge of dealing with SCIM exceptions by ignoring, logging or rollback transaction according to :
|
||||
|
@ -15,9 +14,15 @@ public class ScimExceptionHandler {
|
|||
private static final Logger LOGGER = Logger.getLogger(ScimExceptionHandler.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final RollbackStrategy rollbackStrategy;
|
||||
|
||||
public ScimExceptionHandler(KeycloakSession session) {
|
||||
this(session, RollbackStrategyFactory.create(RollbackStrategyFactory.RollbackApproach.NEVER_ROLLBACK));
|
||||
}
|
||||
|
||||
public ScimExceptionHandler(KeycloakSession session, RollbackStrategy rollbackStrategy) {
|
||||
this.session = session;
|
||||
this.rollbackStrategy = rollbackStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,15 +31,13 @@ public class ScimExceptionHandler {
|
|||
* @param scimProviderConfiguration the configuration of the endpoint for which the propagation exception occured
|
||||
* @param e the occuring exception
|
||||
*/
|
||||
public void handleException(ScrimProviderConfiguration scimProviderConfiguration, ScimPropagationException e) {
|
||||
LOGGER.error("[SCIM] Error while propagating to SCIM endpoint %s", scimProviderConfiguration.getId(), e);
|
||||
// TODO Exception Handling : rollback only for critical operations, if configuration says so
|
||||
// session.getTransactionManager().rollback();
|
||||
public void handleException(ScrimEndPointConfiguration scimProviderConfiguration, ScimPropagationException e) {
|
||||
String errorMessage = "[SCIM] Error while propagating to SCIM endpoint " + scimProviderConfiguration.getId();
|
||||
if (rollbackStrategy.shouldRollback(scimProviderConfiguration, e)) {
|
||||
session.getTransactionManager().rollback();
|
||||
LOGGER.error(errorMessage, e);
|
||||
} else {
|
||||
LOGGER.warn(errorMessage);
|
||||
}
|
||||
|
||||
public void handleInvalidEndpointConfiguration(ComponentModel scimEndpointConfigurationRaw, Exception e) {
|
||||
LOGGER.error("[SCIM] Invalid Endpoint configuration " + scimEndpointConfigurationRaw.getId(), e);
|
||||
// TODO Exception Handling is it ok to ignore an invalid Scim endpoint Configuration ?
|
||||
// IF not, we should propagate the exception here
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package sh.libre.scim.core.exceptions;
|
||||
|
||||
import sh.libre.scim.core.ScrimEndPointConfiguration;
|
||||
|
||||
/**
|
||||
* In charge of deciding, when facing a SCIM-related issue, whether we should :
|
||||
* - log a warning, skip the problematic element and continue the rest of the operation
|
||||
* - stop immediately the whole operation (typically, a synchronisation between SCIM and Keycloack)
|
||||
*/
|
||||
public interface SkipOrStopStrategy {
|
||||
/**
|
||||
* Indicates if, during a synchronisation from Keycloack to a SCIM endpoint, we should :
|
||||
* - cancel the whole synchronisation if an element CRUD fail, or
|
||||
* - keep on with synchronisation, allowing a partial synchronisation
|
||||
*
|
||||
* @param configuration the configuration of the endpoint in which the error occurred
|
||||
* @return true if a partial synchronisation is allowed,
|
||||
* false if we should stop the whole synchronisation at first issue
|
||||
*/
|
||||
boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration);
|
||||
|
||||
/**
|
||||
* Indicates if, during a synchronisation from a SCIM endpoint to Keycloack, we should :
|
||||
* - cancel the whole synchronisation if an element CRUD fail, or
|
||||
* - keep on with synchronisation, allowing a partial synchronisation
|
||||
*
|
||||
* @param configuration the configuration of the endpoint in which the error occurred
|
||||
* @return true if a partial synchronisation is allowed,
|
||||
* false if we should interrupt the whole synchronisation at first issue
|
||||
*/
|
||||
boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration);
|
||||
|
||||
|
||||
/**
|
||||
* Indicates if, when we propagate a group creation or update to a SCIM endpoint and some
|
||||
* of its members are not mapped to SCIM, we should allow partial group update or interrupt completely.
|
||||
*
|
||||
* @param configuration the configuration of the endpoint in which the error occurred
|
||||
* @return true if a partial group update is allowed,
|
||||
* false if we should interrupt the group update in case of any unmapped member
|
||||
*/
|
||||
boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration);
|
||||
|
||||
/**
|
||||
* Indicates if, when facing an invalid SCIM endpoint configuration (resulting in a unreachable SCIM server),
|
||||
* we should stop or ignore this configuration.
|
||||
*
|
||||
* @return true the invalid endpoint should be ignored,
|
||||
* * false if we should interrupt the rest of the synchronisation
|
||||
*/
|
||||
boolean allowInvalidEndpointConfiguration();
|
||||
|
||||
/**
|
||||
* Indicates if, when trying to pull User or Groups from a SCIM endpoint,
|
||||
* we encounter a invalid data (e.g. group with empty name), we should :
|
||||
* - Skip the invalid element pull and continue
|
||||
* - Cancel the whole synchronisation
|
||||
*
|
||||
* @param configuration the configuration of the endpoint in which the error occurred
|
||||
* @return true if we should skip the invalid data synchronisation and pursue,
|
||||
* false if we should interrupt immediately the whole synchronisation
|
||||
*/
|
||||
boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration);
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package sh.libre.scim.core.exceptions;
|
||||
|
||||
import sh.libre.scim.core.ScrimEndPointConfiguration;
|
||||
|
||||
public class SkipOrStopStrategyFactory {
|
||||
|
||||
public static SkipOrStopStrategy create(SkipOrStopApproach approach) {
|
||||
// We could imagine more fine-grained strategies (e.g. based on each Scim endpoint configuration)
|
||||
return switch (approach) {
|
||||
case ALWAYS_STOP -> new AlwaysStopStrategy();
|
||||
case ALWAYS_SKIP_AND_CONTINUE -> new AlwaysSkipAndContinueStrategy();
|
||||
};
|
||||
}
|
||||
|
||||
public enum SkipOrStopApproach {
|
||||
ALWAYS_SKIP_AND_CONTINUE, ALWAYS_STOP
|
||||
}
|
||||
|
||||
private static final class AlwaysStopStrategy implements SkipOrStopStrategy {
|
||||
|
||||
@Override
|
||||
public boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowInvalidEndpointConfiguration() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AlwaysSkipAndContinueStrategy implements SkipOrStopStrategy {
|
||||
|
||||
@Override
|
||||
public boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowInvalidEndpointConfiguration() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ import org.keycloak.storage.user.ImportSynchronization;
|
|||
import org.keycloak.storage.user.SynchronizationResult;
|
||||
import org.keycloak.timer.TimerProvider;
|
||||
import sh.libre.scim.core.ScimDispatcher;
|
||||
import sh.libre.scim.core.ScrimProviderConfiguration;
|
||||
import sh.libre.scim.core.ScrimEndPointConfiguration;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Date;
|
||||
|
@ -94,7 +94,7 @@ public class ScimEndpointConfigurationStorageProviderFactory
|
|||
// These Config Properties will be use to generate configuration page in Admin Console
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property()
|
||||
.name(ScrimProviderConfiguration.CONF_KEY_ENDPOINT)
|
||||
.name(ScrimEndPointConfiguration.CONF_KEY_ENDPOINT)
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.required(true)
|
||||
.label("SCIM 2.0 endpoint")
|
||||
|
@ -102,7 +102,7 @@ public class ScimEndpointConfigurationStorageProviderFactory
|
|||
"URL (/ServiceProviderConfig /Schemas and /ResourcesTypes should be accessible)")
|
||||
.add()
|
||||
.property()
|
||||
.name(ScrimProviderConfiguration.CONF_KEY_CONTENT_TYPE)
|
||||
.name(ScrimEndPointConfiguration.CONF_KEY_CONTENT_TYPE)
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.label("Endpoint content type")
|
||||
.helpText("Only used when endpoint doesn't support application/scim+json")
|
||||
|
@ -110,7 +110,7 @@ public class ScimEndpointConfigurationStorageProviderFactory
|
|||
.defaultValue(HttpHeader.SCIM_CONTENT_TYPE)
|
||||
.add()
|
||||
.property()
|
||||
.name(ScrimProviderConfiguration.CONF_KEY_AUTH_MODE)
|
||||
.name(ScrimEndPointConfiguration.CONF_KEY_AUTH_MODE)
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.label("Auth mode")
|
||||
.helpText("Select the authorization mode")
|
||||
|
@ -118,38 +118,38 @@ public class ScimEndpointConfigurationStorageProviderFactory
|
|||
.defaultValue("NONE")
|
||||
.add()
|
||||
.property()
|
||||
.name(ScrimProviderConfiguration.CONF_KEY_AUTH_USER)
|
||||
.name(ScrimEndPointConfiguration.CONF_KEY_AUTH_USER)
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.label("Auth username")
|
||||
.helpText("Required for basic authentication.")
|
||||
.add()
|
||||
.property()
|
||||
.name(ScrimProviderConfiguration.CONF_KEY_AUTH_PASSWORD)
|
||||
.name(ScrimEndPointConfiguration.CONF_KEY_AUTH_PASSWORD)
|
||||
.type(ProviderConfigProperty.PASSWORD)
|
||||
.label("Auth password/token")
|
||||
.helpText("Password or token required for basic or bearer authentication.")
|
||||
.add()
|
||||
.property()
|
||||
.name(ScrimProviderConfiguration.CONF_KEY_PROPAGATION_USER)
|
||||
.name(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.label("Enable user propagation")
|
||||
.helpText("Should operation on users be propagated to this provider?")
|
||||
.defaultValue(BooleanUtils.TRUE)
|
||||
.add()
|
||||
.property()
|
||||
.name(ScrimProviderConfiguration.CONF_KEY_PROPAGATION_GROUP)
|
||||
.name(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.label("Enable group propagation")
|
||||
.helpText("Should operation on groups be propagated to this provider?")
|
||||
.defaultValue(BooleanUtils.TRUE)
|
||||
.add()
|
||||
.property()
|
||||
.name(ScrimProviderConfiguration.CONF_KEY_SYNC_IMPORT)
|
||||
.name(ScrimEndPointConfiguration.CONF_KEY_SYNC_IMPORT)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.label("Enable import during sync")
|
||||
.add()
|
||||
.property()
|
||||
.name(ScrimProviderConfiguration.CONF_KEY_SYNC_IMPORT_ACTION)
|
||||
.name(ScrimEndPointConfiguration.CONF_KEY_SYNC_IMPORT_ACTION)
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.label("Import action")
|
||||
.helpText("What to do when the user doesn't exists in Keycloak.")
|
||||
|
@ -157,7 +157,7 @@ public class ScimEndpointConfigurationStorageProviderFactory
|
|||
.defaultValue("CREATE_LOCAL")
|
||||
.add()
|
||||
.property()
|
||||
.name(ScrimProviderConfiguration.CONF_KEY_SYNC_REFRESH)
|
||||
.name(ScrimEndPointConfiguration.CONF_KEY_SYNC_REFRESH)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.label("Enable refresh during sync")
|
||||
.add()
|
||||
|
|
Loading…
Reference in a new issue