Exception Handler - Step 5 : basic Rolback Strategy implementation
This commit is contained in:
parent
09d0b6544b
commit
620c9e84bb
12 changed files with 115 additions and 65 deletions
|
@ -6,8 +6,8 @@ import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RoleMapperModel;
|
import org.keycloak.models.RoleMapperModel;
|
||||||
import org.keycloak.storage.user.SynchronizationResult;
|
import org.keycloak.storage.user.SynchronizationResult;
|
||||||
import sh.libre.scim.core.exceptions.ErrorForScimEndpointException;
|
import sh.libre.scim.core.exceptions.InconsistentScimMappingException;
|
||||||
import sh.libre.scim.core.exceptions.InconsistentScimDataException;
|
import sh.libre.scim.core.exceptions.InvalidResponseFromScimEndpointException;
|
||||||
import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
|
import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
|
||||||
import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
|
import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
|
||||||
import sh.libre.scim.jpa.ScimResourceDao;
|
import sh.libre.scim.jpa.ScimResourceDao;
|
||||||
|
@ -45,7 +45,7 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
||||||
this.skipOrStopStrategy = skipOrStopStrategy;
|
this.skipOrStopStrategy = skipOrStopStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void create(K roleMapperModel) throws InconsistentScimDataException, ErrorForScimEndpointException {
|
public void create(K roleMapperModel) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
|
||||||
if (isMarkedToIgnore(roleMapperModel)) {
|
if (isMarkedToIgnore(roleMapperModel)) {
|
||||||
// Silently return: resource is explicitly marked as to ignore
|
// Silently return: resource is explicitly marked as to ignore
|
||||||
return;
|
return;
|
||||||
|
@ -53,14 +53,14 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
||||||
// If mapping, then we are trying to recreate a user that was already created by import
|
// If mapping, then we are trying to recreate a user that was already created by import
|
||||||
KeycloakId id = getId(roleMapperModel);
|
KeycloakId id = getId(roleMapperModel);
|
||||||
if (findMappingById(id).isPresent()) {
|
if (findMappingById(id).isPresent()) {
|
||||||
throw new InconsistentScimDataException("Trying to create user with id " + id + ": id already exists in Keycloak database");
|
throw new InconsistentScimMappingException("Trying to create user with id " + id + ": id already exists in Keycloak database");
|
||||||
}
|
}
|
||||||
S scimForCreation = scimRequestBodyForCreate(roleMapperModel);
|
S scimForCreation = scimRequestBodyForCreate(roleMapperModel);
|
||||||
EntityOnRemoteScimId externalId = scimClient.create(id, scimForCreation);
|
EntityOnRemoteScimId externalId = scimClient.create(id, scimForCreation);
|
||||||
createMapping(id, externalId);
|
createMapping(id, externalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(K roleMapperModel) throws InconsistentScimDataException, ErrorForScimEndpointException {
|
public void update(K roleMapperModel) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
|
||||||
if (isMarkedToIgnore(roleMapperModel)) {
|
if (isMarkedToIgnore(roleMapperModel)) {
|
||||||
// Silently return: resource is explicitly marked as to ignore
|
// Silently return: resource is explicitly marked as to ignore
|
||||||
return;
|
return;
|
||||||
|
@ -68,22 +68,22 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
||||||
KeycloakId keycloakId = getId(roleMapperModel);
|
KeycloakId keycloakId = getId(roleMapperModel);
|
||||||
EntityOnRemoteScimId entityOnRemoteScimId = findMappingById(keycloakId)
|
EntityOnRemoteScimId entityOnRemoteScimId = findMappingById(keycloakId)
|
||||||
.map(ScimResourceMapping::getExternalIdAsEntityOnRemoteScimId)
|
.map(ScimResourceMapping::getExternalIdAsEntityOnRemoteScimId)
|
||||||
.orElseThrow(() -> new InconsistentScimDataException("Failed to find SCIM mapping for " + keycloakId));
|
.orElseThrow(() -> new InconsistentScimMappingException("Failed to find SCIM mapping for " + keycloakId));
|
||||||
S scimForReplace = scimRequestBodyForUpdate(roleMapperModel, entityOnRemoteScimId);
|
S scimForReplace = scimRequestBodyForUpdate(roleMapperModel, entityOnRemoteScimId);
|
||||||
scimClient.update(entityOnRemoteScimId, scimForReplace);
|
scimClient.update(entityOnRemoteScimId, scimForReplace);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract S scimRequestBodyForUpdate(K roleMapperModel, EntityOnRemoteScimId externalId) throws InconsistentScimDataException;
|
protected abstract S scimRequestBodyForUpdate(K roleMapperModel, EntityOnRemoteScimId externalId) throws InconsistentScimMappingException;
|
||||||
|
|
||||||
public void delete(KeycloakId id) throws InconsistentScimDataException, ErrorForScimEndpointException {
|
public void delete(KeycloakId id) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
|
||||||
ScimResourceMapping resource = findMappingById(id)
|
ScimResourceMapping resource = findMappingById(id)
|
||||||
.orElseThrow(() -> new InconsistentScimDataException("Failed to delete resource %s, scim mapping not found: ".formatted(id)));
|
.orElseThrow(() -> new InconsistentScimMappingException("Failed to delete resource %s, scim mapping not found: ".formatted(id)));
|
||||||
EntityOnRemoteScimId externalId = resource.getExternalIdAsEntityOnRemoteScimId();
|
EntityOnRemoteScimId externalId = resource.getExternalIdAsEntityOnRemoteScimId();
|
||||||
scimClient.delete(externalId);
|
scimClient.delete(externalId);
|
||||||
getScimResourceDao().delete(resource);
|
getScimResourceDao().delete(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pushAllResourcesToScim(SynchronizationResult syncRes) throws ErrorForScimEndpointException, InconsistentScimDataException {
|
public void pushAllResourcesToScim(SynchronizationResult syncRes) throws InvalidResponseFromScimEndpointException, InconsistentScimMappingException {
|
||||||
LOGGER.info("[SCIM] Push resources to endpoint " + this.getConfiguration().getEndPoint());
|
LOGGER.info("[SCIM] Push resources to endpoint " + this.getConfiguration().getEndPoint());
|
||||||
try (Stream<K> resourcesStream = getResourceStream()) {
|
try (Stream<K> resourcesStream = getResourceStream()) {
|
||||||
Set<K> resources = resourcesStream.collect(Collectors.toUnmodifiableSet());
|
Set<K> resources = resourcesStream.collect(Collectors.toUnmodifiableSet());
|
||||||
|
@ -94,14 +94,14 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pullAllResourcesFromScim(SynchronizationResult syncRes) throws UnexpectedScimDataException, InconsistentScimDataException, ErrorForScimEndpointException {
|
public void pullAllResourcesFromScim(SynchronizationResult syncRes) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
|
||||||
LOGGER.info("[SCIM] Pull resources from endpoint " + this.getConfiguration().getEndPoint());
|
LOGGER.info("[SCIM] Pull resources from endpoint " + this.getConfiguration().getEndPoint());
|
||||||
for (S resource : scimClient.listResources()) {
|
for (S resource : scimClient.listResources()) {
|
||||||
pullSingleResourceFromScim(syncRes, resource);
|
pullSingleResourceFromScim(syncRes, resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pushSingleResourceToScim(SynchronizationResult syncRes, K resource, KeycloakId id) throws ErrorForScimEndpointException, InconsistentScimDataException {
|
private void pushSingleResourceToScim(SynchronizationResult syncRes, K resource, KeycloakId id) throws InvalidResponseFromScimEndpointException, InconsistentScimMappingException {
|
||||||
try {
|
try {
|
||||||
LOGGER.infof("[SCIM] Reconciling local resource %s", id);
|
LOGGER.infof("[SCIM] Reconciling local resource %s", id);
|
||||||
if (shouldIgnoreForScimSynchronization(resource)) {
|
if (shouldIgnoreForScimSynchronization(resource)) {
|
||||||
|
@ -116,13 +116,13 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
||||||
create(resource);
|
create(resource);
|
||||||
}
|
}
|
||||||
syncRes.increaseUpdated();
|
syncRes.increaseUpdated();
|
||||||
} catch (ErrorForScimEndpointException e) {
|
} catch (InvalidResponseFromScimEndpointException e) {
|
||||||
if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) {
|
if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) {
|
||||||
LOGGER.warn("Error while syncing " + id + " to endpoint " + getConfiguration().getEndPoint());
|
LOGGER.warn("Error while syncing " + id + " to endpoint " + getConfiguration().getEndPoint());
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
} catch (InconsistentScimDataException e) {
|
} catch (InconsistentScimMappingException e) {
|
||||||
if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) {
|
if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) {
|
||||||
LOGGER.warn("Inconsistent data for element " + id + " and endpoint " + getConfiguration().getEndPoint());
|
LOGGER.warn("Inconsistent data for element " + id + " and endpoint " + getConfiguration().getEndPoint());
|
||||||
} else {
|
} else {
|
||||||
|
@ -132,7 +132,7 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void pullSingleResourceFromScim(SynchronizationResult syncRes, S resource) throws UnexpectedScimDataException, InconsistentScimDataException, ErrorForScimEndpointException {
|
private void pullSingleResourceFromScim(SynchronizationResult syncRes, S resource) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException {
|
||||||
try {
|
try {
|
||||||
LOGGER.infof("[SCIM] Reconciling remote resource %s", resource);
|
LOGGER.infof("[SCIM] Reconciling remote resource %s", resource);
|
||||||
EntityOnRemoteScimId externalId = resource.getId()
|
EntityOnRemoteScimId externalId = resource.getId()
|
||||||
|
@ -180,13 +180,13 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
} catch (InconsistentScimDataException e) {
|
} catch (InconsistentScimMappingException e) {
|
||||||
if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) {
|
if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) {
|
||||||
LOGGER.warn("[SCIM] Skipping element synchronisation because of inconsistent mapping for element " + resource.getId() + " : " + e.getMessage());
|
LOGGER.warn("[SCIM] Skipping element synchronisation because of inconsistent mapping for element " + resource.getId() + " : " + e.getMessage());
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
} catch (ErrorForScimEndpointException e) {
|
} catch (InvalidResponseFromScimEndpointException e) {
|
||||||
// Can only occur in case of a DELETE_REMOTE conflict action
|
// Can only occur in case of a DELETE_REMOTE conflict action
|
||||||
if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) {
|
if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) {
|
||||||
LOGGER.warn("[SCIM] Could not delete SCIM resource " + resource.getId() + " during synchronisation");
|
LOGGER.warn("[SCIM] Could not delete SCIM resource " + resource.getId() + " during synchronisation");
|
||||||
|
@ -198,7 +198,7 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected abstract S scimRequestBodyForCreate(K roleMapperModel) throws InconsistentScimDataException;
|
protected abstract S scimRequestBodyForCreate(K roleMapperModel) throws InconsistentScimMappingException;
|
||||||
|
|
||||||
protected abstract KeycloakId getId(K roleMapperModel);
|
protected abstract KeycloakId getId(K roleMapperModel);
|
||||||
|
|
||||||
|
@ -225,13 +225,13 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
|
||||||
|
|
||||||
protected abstract Stream<K> getResourceStream();
|
protected abstract Stream<K> getResourceStream();
|
||||||
|
|
||||||
protected abstract KeycloakId createEntity(S resource) throws UnexpectedScimDataException;
|
protected abstract KeycloakId createEntity(S resource) throws UnexpectedScimDataException, InconsistentScimMappingException;
|
||||||
|
|
||||||
protected abstract Optional<KeycloakId> matchKeycloakMappingByScimProperties(S resource) throws InconsistentScimDataException;
|
protected abstract Optional<KeycloakId> matchKeycloakMappingByScimProperties(S resource) throws InconsistentScimMappingException;
|
||||||
|
|
||||||
protected abstract boolean entityExists(KeycloakId keycloakId);
|
protected abstract boolean entityExists(KeycloakId keycloakId);
|
||||||
|
|
||||||
public void sync(SynchronizationResult syncRes) throws InconsistentScimDataException, ErrorForScimEndpointException, UnexpectedScimDataException {
|
public void sync(SynchronizationResult syncRes) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException, UnexpectedScimDataException {
|
||||||
if (this.scimProviderConfiguration.isPullFromScimSynchronisationActivated()) {
|
if (this.scimProviderConfiguration.isPullFromScimSynchronisationActivated()) {
|
||||||
this.pullAllResourcesFromScim(syncRes);
|
this.pullAllResourcesFromScim(syncRes);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import sh.libre.scim.core.exceptions.InconsistentScimDataException;
|
import sh.libre.scim.core.exceptions.InconsistentScimMappingException;
|
||||||
import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
|
import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
|
||||||
import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
|
import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
|
||||||
import sh.libre.scim.jpa.ScimResourceMapping;
|
import sh.libre.scim.jpa.ScimResourceMapping;
|
||||||
|
@ -55,7 +55,7 @@ public class GroupScimService extends AbstractScimService<GroupModel, Group> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected KeycloakId createEntity(Group resource) throws UnexpectedScimDataException {
|
protected KeycloakId createEntity(Group resource) throws UnexpectedScimDataException, InconsistentScimMappingException {
|
||||||
String displayName = resource.getDisplayName()
|
String displayName = resource.getDisplayName()
|
||||||
.filter(StringUtils::isNotBlank)
|
.filter(StringUtils::isNotBlank)
|
||||||
.orElseThrow(() -> new UnexpectedScimDataException("Remote Scim group has empty name, can't create. Resource id = %s".formatted(resource.getId())));
|
.orElseThrow(() -> new UnexpectedScimDataException("Remote Scim group has empty name, can't create. Resource id = %s".formatted(resource.getId())));
|
||||||
|
@ -68,8 +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));
|
.orElseThrow(() -> new UnexpectedScimDataException("can't create group member for group '%s' without id: ".formatted(displayName) + resource));
|
||||||
KeycloakId userId = getScimResourceDao().findUserByExternalId(externalId)
|
KeycloakId userId = getScimResourceDao().findUserByExternalId(externalId)
|
||||||
.map(ScimResourceMapping::getIdAsKeycloakId)
|
.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 InconsistentScimMappingException("can't find mapping for group member %s".formatted(externalId)));
|
||||||
.orElseThrow(() -> new UnexpectedScimDataException("can't find mapping for group member %s".formatted(externalId)));
|
|
||||||
UserModel userModel = getKeycloakDao().getUserById(userId);
|
UserModel userModel = getKeycloakDao().getUserById(userId);
|
||||||
userModel.joinGroup(group);
|
userModel.joinGroup(group);
|
||||||
}
|
}
|
||||||
|
@ -88,7 +87,7 @@ public class GroupScimService extends AbstractScimService<GroupModel, Group> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Group scimRequestBodyForCreate(GroupModel groupModel) throws InconsistentScimDataException {
|
protected Group scimRequestBodyForCreate(GroupModel groupModel) throws InconsistentScimMappingException {
|
||||||
Set<KeycloakId> members = getKeycloakDao().getGroupMembers(groupModel);
|
Set<KeycloakId> members = getKeycloakDao().getGroupMembers(groupModel);
|
||||||
Group group = new Group();
|
Group group = new Group();
|
||||||
group.setExternalId(groupModel.getId());
|
group.setExternalId(groupModel.getId());
|
||||||
|
@ -108,7 +107,7 @@ public class GroupScimService extends AbstractScimService<GroupModel, Group> {
|
||||||
if (skipOrStopStrategy.allowMissingMembersWhenPushingGroupToScim(this.getConfiguration())) {
|
if (skipOrStopStrategy.allowMissingMembersWhenPushingGroupToScim(this.getConfiguration())) {
|
||||||
LOGGER.warn(message);
|
LOGGER.warn(message);
|
||||||
} else {
|
} else {
|
||||||
throw new InconsistentScimDataException(message);
|
throw new InconsistentScimMappingException(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +115,7 @@ public class GroupScimService extends AbstractScimService<GroupModel, Group> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Group scimRequestBodyForUpdate(GroupModel groupModel, EntityOnRemoteScimId externalId) throws InconsistentScimDataException {
|
protected Group scimRequestBodyForUpdate(GroupModel groupModel, EntityOnRemoteScimId externalId) throws InconsistentScimMappingException {
|
||||||
Group group = scimRequestBodyForCreate(groupModel);
|
Group group = scimRequestBodyForCreate(groupModel);
|
||||||
group.setId(externalId.asString());
|
group.setId(externalId.asString());
|
||||||
Meta meta = newMetaLocation(externalId);
|
Meta meta = newMetaLocation(externalId);
|
||||||
|
|
|
@ -12,7 +12,7 @@ import io.github.resilience4j.retry.RetryConfig;
|
||||||
import io.github.resilience4j.retry.RetryRegistry;
|
import io.github.resilience4j.retry.RetryRegistry;
|
||||||
import jakarta.ws.rs.ProcessingException;
|
import jakarta.ws.rs.ProcessingException;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import sh.libre.scim.core.exceptions.ErrorForScimEndpointException;
|
import sh.libre.scim.core.exceptions.InvalidResponseFromScimEndpointException;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -64,7 +64,7 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
|
||||||
return new ScimClient(scimRequestBuilder, scimResourceType);
|
return new ScimClient(scimRequestBuilder, scimResourceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EntityOnRemoteScimId create(KeycloakId id, S scimForCreation) throws ErrorForScimEndpointException {
|
public EntityOnRemoteScimId create(KeycloakId id, S scimForCreation) throws InvalidResponseFromScimEndpointException {
|
||||||
Optional<String> scimForCreationId = scimForCreation.getId();
|
Optional<String> scimForCreationId = scimForCreation.getId();
|
||||||
if (scimForCreationId.isPresent()) {
|
if (scimForCreationId.isPresent()) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
|
@ -82,12 +82,12 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
|
||||||
S resource = response.getResource();
|
S resource = response.getResource();
|
||||||
return resource.getId()
|
return resource.getId()
|
||||||
.map(EntityOnRemoteScimId::new)
|
.map(EntityOnRemoteScimId::new)
|
||||||
.orElseThrow(() -> new ErrorForScimEndpointException("Created SCIM resource does not have id"));
|
.orElseThrow(() -> new InvalidResponseFromScimEndpointException(response, "Created SCIM resource does not have id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkResponseIsSuccess(ServerResponse<S> response) throws ErrorForScimEndpointException {
|
private void checkResponseIsSuccess(ServerResponse<S> response) throws InvalidResponseFromScimEndpointException {
|
||||||
if (!response.isSuccess()) {
|
if (!response.isSuccess()) {
|
||||||
throw new ErrorForScimEndpointException("Server answered with status " + response.getResponseBody() + ": " + response.getResponseBody());
|
throw new InvalidResponseFromScimEndpointException(response, "Server answered with status " + response.getResponseBody() + ": " + response.getResponseBody());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
|
||||||
return scimResourceType.getResourceClass();
|
return scimResourceType.getResourceClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(EntityOnRemoteScimId externalId, S scimForReplace) throws ErrorForScimEndpointException {
|
public void update(EntityOnRemoteScimId externalId, S scimForReplace) throws InvalidResponseFromScimEndpointException {
|
||||||
Retry retry = retryRegistry.retry("replace-%s".formatted(externalId.asString()));
|
Retry retry = retryRegistry.retry("replace-%s".formatted(externalId.asString()));
|
||||||
// TODO Exception handling : check that all exceptions are wrapped in server response
|
// TODO Exception handling : check that all exceptions are wrapped in server response
|
||||||
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
|
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
|
||||||
|
@ -110,7 +110,7 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
|
||||||
checkResponseIsSuccess(response);
|
checkResponseIsSuccess(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(EntityOnRemoteScimId externalId) throws ErrorForScimEndpointException {
|
public void delete(EntityOnRemoteScimId externalId) throws InvalidResponseFromScimEndpointException {
|
||||||
Retry retry = retryRegistry.retry("delete-%s".formatted(externalId.asString()));
|
Retry retry = retryRegistry.retry("delete-%s".formatted(externalId.asString()));
|
||||||
// TODO Exception handling : check that all exceptions are wrapped in server response
|
// TODO Exception handling : check that all exceptions are wrapped in server response
|
||||||
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
|
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
|
||||||
|
|
|
@ -13,7 +13,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RoleMapperModel;
|
import org.keycloak.models.RoleMapperModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import sh.libre.scim.core.exceptions.InconsistentScimDataException;
|
import sh.libre.scim.core.exceptions.InconsistentScimMappingException;
|
||||||
import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
|
import sh.libre.scim.core.exceptions.SkipOrStopStrategy;
|
||||||
import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
|
import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ public class UserScimService extends AbstractScimService<UserModel, User> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Optional<KeycloakId> matchKeycloakMappingByScimProperties(User resource) throws InconsistentScimDataException {
|
protected Optional<KeycloakId> matchKeycloakMappingByScimProperties(User resource) throws InconsistentScimMappingException {
|
||||||
Optional<KeycloakId> matchedByUsername = resource.getUserName()
|
Optional<KeycloakId> matchedByUsername = resource.getUserName()
|
||||||
.map(getKeycloakDao()::getUserByUsername)
|
.map(getKeycloakDao()::getUserByUsername)
|
||||||
.map(this::getId);
|
.map(this::getId);
|
||||||
|
@ -55,7 +55,7 @@ public class UserScimService extends AbstractScimService<UserModel, User> {
|
||||||
if (matchedByUsername.isPresent()
|
if (matchedByUsername.isPresent()
|
||||||
&& matchedByEmail.isPresent()
|
&& matchedByEmail.isPresent()
|
||||||
&& !matchedByUsername.equals(matchedByEmail)) {
|
&& !matchedByUsername.equals(matchedByEmail)) {
|
||||||
throw new InconsistentScimDataException("Found 2 possible users for remote user " + matchedByUsername.get() + " - " + matchedByEmail.get());
|
throw new InconsistentScimMappingException("Found 2 possible users for remote user " + matchedByUsername.get() + " - " + matchedByEmail.get());
|
||||||
}
|
}
|
||||||
if (matchedByUsername.isPresent()) {
|
if (matchedByUsername.isPresent()) {
|
||||||
return matchedByUsername;
|
return matchedByUsername;
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
package sh.libre.scim.core.exceptions;
|
|
||||||
|
|
||||||
public class ErrorForScimEndpointException extends ScimPropagationException {
|
|
||||||
|
|
||||||
public ErrorForScimEndpointException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package sh.libre.scim.core.exceptions;
|
|
||||||
|
|
||||||
public class InconsistentScimDataException extends ScimPropagationException {
|
|
||||||
public InconsistentScimDataException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package sh.libre.scim.core.exceptions;
|
||||||
|
|
||||||
|
public class InconsistentScimMappingException extends ScimPropagationException {
|
||||||
|
public InconsistentScimMappingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package sh.libre.scim.core.exceptions;
|
||||||
|
|
||||||
|
import de.captaingoldfish.scim.sdk.client.response.ServerResponse;
|
||||||
|
|
||||||
|
public class InvalidResponseFromScimEndpointException extends ScimPropagationException {
|
||||||
|
|
||||||
|
private final ServerResponse response;
|
||||||
|
|
||||||
|
public InvalidResponseFromScimEndpointException(ServerResponse response, String message) {
|
||||||
|
super(message);
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerResponse getResponse() {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package sh.libre.scim.core.exceptions;
|
||||||
|
|
||||||
|
import sh.libre.scim.core.ScrimEndPointConfiguration;
|
||||||
|
|
||||||
|
public class RollbackOnlyForCriticalErrorsStrategy implements RollbackStrategy {
|
||||||
|
|
||||||
|
private boolean shouldRollback(InvalidResponseFromScimEndpointException e) {
|
||||||
|
int httpStatus = e.getResponse().getHttpStatus();
|
||||||
|
return httpStatus == 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) {
|
||||||
|
if (e instanceof InvalidResponseFromScimEndpointException invalidResponseFromScimEndpointException) {
|
||||||
|
return shouldRollback(invalidResponseFromScimEndpointException);
|
||||||
|
}
|
||||||
|
if (e instanceof InconsistentScimMappingException) {
|
||||||
|
// Occurs when mapping between a SCIM resource and a keycloak user failed (missing, ambiguous..)
|
||||||
|
// Log can be sufficient here, no rollback required
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (e instanceof UnexpectedScimDataException) {
|
||||||
|
// Occurs when a SCIM endpoint sends invalid date (e.g. group with empty name, user without ids...)
|
||||||
|
// No rollback required : we cannot recover. This needs to be fixed in the SCIM endpoint data
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Should not occur
|
||||||
|
throw new IllegalStateException("Unkown ScimPropagationException", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -9,13 +9,15 @@ public class RollbackStrategyFactory {
|
||||||
return switch (approach) {
|
return switch (approach) {
|
||||||
case ALWAYS_ROLLBACK -> new AlwaysRollbackStrategy();
|
case ALWAYS_ROLLBACK -> new AlwaysRollbackStrategy();
|
||||||
case NEVER_ROLLBACK -> new NeverRollbackStrategy();
|
case NEVER_ROLLBACK -> new NeverRollbackStrategy();
|
||||||
|
case CRITICAL_ONLY_ROLLBACK -> new RollbackOnlyForCriticalErrorsStrategy();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RollbackApproach {
|
public enum RollbackApproach {
|
||||||
ALWAYS_ROLLBACK, NEVER_ROLLBACK
|
ALWAYS_ROLLBACK, NEVER_ROLLBACK, CRITICAL_ONLY_ROLLBACK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static final class AlwaysRollbackStrategy implements RollbackStrategy {
|
private static final class AlwaysRollbackStrategy implements RollbackStrategy {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,7 +17,7 @@ public class ScimExceptionHandler {
|
||||||
private final RollbackStrategy rollbackStrategy;
|
private final RollbackStrategy rollbackStrategy;
|
||||||
|
|
||||||
public ScimExceptionHandler(KeycloakSession session) {
|
public ScimExceptionHandler(KeycloakSession session) {
|
||||||
this(session, RollbackStrategyFactory.create(RollbackStrategyFactory.RollbackApproach.NEVER_ROLLBACK));
|
this(session, RollbackStrategyFactory.create(RollbackStrategyFactory.RollbackApproach.CRITICAL_ONLY_ROLLBACK));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScimExceptionHandler(KeycloakSession session, RollbackStrategy rollbackStrategy) {
|
public ScimExceptionHandler(KeycloakSession session, RollbackStrategy rollbackStrategy) {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||||
<changeSet author="contact@indiehosters.net" id="scim-resource-1.0">
|
<changeSet author="contact@indiehosters.net" id="scim-resource-1.0">
|
||||||
|
|
||||||
<createTable tableName="SCIM_RESOURCE">
|
<createTable tableName="SCIM_RESOURCE_MAPPING">
|
||||||
<column name="ID" type="VARCHAR(36)">
|
<column name="ID" type="VARCHAR(36)">
|
||||||
<constraints nullable="false"/>
|
<constraints nullable="false"/>
|
||||||
</column>
|
</column>
|
||||||
|
@ -20,9 +22,14 @@
|
||||||
</column>
|
</column>
|
||||||
</createTable>
|
</createTable>
|
||||||
|
|
||||||
<addPrimaryKey constraintName="PK_SCIM_RESOURCE" tableName="SCIM_RESOURCE" columnNames="ID,REALM_ID,TYPE,COMPONENT_ID,EXTERNAL_ID" />
|
<addPrimaryKey constraintName="PK_SCIM_RESOURCE" tableName="SCIM_RESOURCE"
|
||||||
<addForeignKeyConstraint baseTableName="SCIM_RESOURCE" baseColumnNames="REALM_ID" constraintName="FK_SCIM_RESOURCE_REALM" referencedTableName="REALM" referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE" />
|
columnNames="ID,REALM_ID,TYPE,COMPONENT_ID,EXTERNAL_ID"/>
|
||||||
<addForeignKeyConstraint baseTableName="SCIM_RESOURCE" baseColumnNames="COMPONENT_ID" constraintName="FK_SCIM_RESOURCE_COMPONENT" referencedTableName="COMPONENT" referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE" />
|
<addForeignKeyConstraint baseTableName="SCIM_RESOURCE" baseColumnNames="REALM_ID"
|
||||||
|
constraintName="FK_SCIM_RESOURCE_REALM" referencedTableName="REALM"
|
||||||
|
referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="SCIM_RESOURCE" baseColumnNames="COMPONENT_ID"
|
||||||
|
constraintName="FK_SCIM_RESOURCE_COMPONENT" referencedTableName="COMPONENT"
|
||||||
|
referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
Loading…
Reference in a new issue