Handle retry exceptions as a 500 server response
This commit is contained in:
parent
9c7c557b76
commit
d8cba394b2
14 changed files with 95 additions and 59 deletions
|
@ -113,7 +113,7 @@ public class ScimDispatcher {
|
|||
if (matchingClient.isPresent()) {
|
||||
try {
|
||||
operationToDispatch.acceptThrows(matchingClient.get());
|
||||
LOGGER.infof("[SCIM] User operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getId());
|
||||
LOGGER.infof("[SCIM] User operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getName());
|
||||
} catch (ScimPropagationException e) {
|
||||
exceptionHandler.handleException(matchingClient.get().getConfiguration(), e);
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ public class ScimDispatcher {
|
|||
if (matchingClient.isPresent()) {
|
||||
try {
|
||||
operationToDispatch.acceptThrows(matchingClient.get());
|
||||
LOGGER.infof("[SCIM] Group operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getId());
|
||||
LOGGER.infof("[SCIM] Group operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getName());
|
||||
} catch (ScimPropagationException e) {
|
||||
exceptionHandler.handleException(matchingClient.get().getConfiguration(), e);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ public class ScrimEndPointConfiguration {
|
|||
|
||||
private final String endPoint;
|
||||
private final String id;
|
||||
private final String name;
|
||||
private final String contentType;
|
||||
private final String authorizationHeaderValue;
|
||||
private final ImportAction importAction;
|
||||
|
@ -42,6 +43,7 @@ public class ScrimEndPointConfiguration {
|
|||
contentType = scimProviderConfiguration.get(CONF_KEY_CONTENT_TYPE, "");
|
||||
endPoint = scimProviderConfiguration.get(CONF_KEY_ENDPOINT, "");
|
||||
id = scimProviderConfiguration.getId();
|
||||
name = scimProviderConfiguration.getName();
|
||||
importAction = ImportAction.valueOf(scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT_ACTION));
|
||||
pullFromScimSynchronisationActivated = scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT, false);
|
||||
pushToScimSynchronisationActivated = scimProviderConfiguration.get(CONF_KEY_SYNC_REFRESH, false);
|
||||
|
@ -70,6 +72,10 @@ public class ScrimEndPointConfiguration {
|
|||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public ImportAction getImportAction() {
|
||||
return importAction;
|
||||
}
|
||||
|
|
|
@ -2,16 +2,27 @@ package sh.libre.scim.core.exceptions;
|
|||
|
||||
import de.captaingoldfish.scim.sdk.client.response.ServerResponse;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class InvalidResponseFromScimEndpointException extends ScimPropagationException {
|
||||
|
||||
private final ServerResponse response;
|
||||
|
||||
private final transient Optional<ServerResponse> response;
|
||||
|
||||
public InvalidResponseFromScimEndpointException(ServerResponse response, String message) {
|
||||
super(message);
|
||||
this.response = response;
|
||||
this.response = Optional.of(response);
|
||||
}
|
||||
|
||||
public ServerResponse getResponse() {
|
||||
public InvalidResponseFromScimEndpointException(String message, Exception e) {
|
||||
super(message, e);
|
||||
this.response = Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Empty response can occur if a major exception was thrown while retrying the request.
|
||||
*/
|
||||
public Optional<ServerResponse> getResponse() {
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
|
@ -40,10 +40,16 @@ public enum RollbackApproach implements RollbackStrategy {
|
|||
}
|
||||
|
||||
private boolean shouldRollbackBecauseOfResponse(InvalidResponseFromScimEndpointException e) {
|
||||
// We consider that 404 are acceptable, otherwise rollback
|
||||
int httpStatus = e.getResponse().getHttpStatus();
|
||||
ArrayList<Integer> acceptableStatus = Lists.newArrayList(200, 204, 404);
|
||||
return !acceptableStatus.contains(httpStatus);
|
||||
// If we have a response
|
||||
return e.getResponse().map(r -> {
|
||||
// We consider that 404 are acceptable, otherwise rollback
|
||||
ArrayList<Integer> acceptableStatus = Lists.newArrayList(200, 204, 404);
|
||||
return !acceptableStatus.contains(r.getHttpStatus());
|
||||
}).orElse(
|
||||
// Never got an answer, server was either misconfigured or unreachable
|
||||
// No rollback in that case.
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,10 +32,10 @@ public class ScimExceptionHandler {
|
|||
* @param e the occuring exception
|
||||
*/
|
||||
public void handleException(ScrimEndPointConfiguration scimProviderConfiguration, ScimPropagationException e) {
|
||||
String errorMessage = "[SCIM] Error while propagating to SCIM endpoint " + scimProviderConfiguration.getId();
|
||||
String errorMessage = "[SCIM] Error while propagating to SCIM endpoint " + scimProviderConfiguration.getName();
|
||||
if (rollbackStrategy.shouldRollback(scimProviderConfiguration, e)) {
|
||||
session.getTransactionManager().rollback();
|
||||
LOGGER.error(errorMessage, e);
|
||||
LOGGER.error("TRANSACTION ROLLBACK - " + errorMessage, e);
|
||||
} else {
|
||||
LOGGER.warn(errorMessage);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@ package sh.libre.scim.core.exceptions;
|
|||
|
||||
public abstract class ScimPropagationException extends Exception {
|
||||
|
||||
public ScimPropagationException(String message) {
|
||||
protected ScimPropagationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ScimPropagationException(String message, Exception e) {
|
||||
protected ScimPropagationException(String message, Exception e) {
|
||||
super(message, e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import java.util.TreeSet;
|
|||
import java.util.stream.Stream;
|
||||
|
||||
public class GroupScimService extends AbstractScimService<GroupModel, Group> {
|
||||
private final Logger LOGGER = Logger.getLogger(GroupScimService.class);
|
||||
private static final Logger LOGGER = Logger.getLogger(GroupScimService.class);
|
||||
|
||||
public GroupScimService(KeycloakSession keycloakSession, ScrimEndPointConfiguration scimProviderConfiguration, SkipOrStopStrategy skipOrStopStrategy) {
|
||||
super(keycloakSession, scimProviderConfiguration, ScimResourceType.GROUP, skipOrStopStrategy);
|
||||
|
|
|
@ -29,7 +29,9 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
|
|||
|
||||
private final ScimResourceType scimResourceType;
|
||||
|
||||
{
|
||||
private ScimClient(ScimRequestBuilder scimRequestBuilder, ScimResourceType scimResourceType) {
|
||||
this.scimRequestBuilder = scimRequestBuilder;
|
||||
this.scimResourceType = scimResourceType;
|
||||
RetryConfig retryConfig = RetryConfig.custom()
|
||||
.maxAttempts(10)
|
||||
.intervalFunction(IntervalFunction.ofExponentialBackoff())
|
||||
|
@ -38,11 +40,6 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
|
|||
retryRegistry = RetryRegistry.of(retryConfig);
|
||||
}
|
||||
|
||||
private ScimClient(ScimRequestBuilder scimRequestBuilder, ScimResourceType scimResourceType) {
|
||||
this.scimRequestBuilder = scimRequestBuilder;
|
||||
this.scimResourceType = scimResourceType;
|
||||
}
|
||||
|
||||
public static ScimClient open(ScrimEndPointConfiguration scimProviderConfiguration, ScimResourceType scimResourceType) {
|
||||
String scimApplicationBaseUrl = scimProviderConfiguration.getEndPoint();
|
||||
Map<String, String> httpHeaders = new HashMap<>();
|
||||
|
@ -69,18 +66,23 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
|
|||
"User to create should never have an existing id: %s %s".formatted(id, scimForCreationId.get())
|
||||
);
|
||||
}
|
||||
// TODO Exception handling : check that all exceptions are wrapped in server response
|
||||
Retry retry = retryRegistry.retry("create-%s".formatted(id.asString()));
|
||||
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
|
||||
.create(getResourceClass(), getScimEndpoint())
|
||||
.setResource(scimForCreation)
|
||||
.sendRequest()
|
||||
);
|
||||
checkResponseIsSuccess(response);
|
||||
S resource = response.getResource();
|
||||
return resource.getId()
|
||||
.map(EntityOnRemoteScimId::new)
|
||||
.orElseThrow(() -> new InvalidResponseFromScimEndpointException(response, "Created SCIM resource does not have id"));
|
||||
try {
|
||||
Retry retry = retryRegistry.retry("create-%s".formatted(id.asString()));
|
||||
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
|
||||
.create(getResourceClass(), getScimEndpoint())
|
||||
.setResource(scimForCreation)
|
||||
.sendRequest()
|
||||
);
|
||||
checkResponseIsSuccess(response);
|
||||
S resource = response.getResource();
|
||||
return resource.getId()
|
||||
.map(EntityOnRemoteScimId::new)
|
||||
.orElseThrow(() -> new InvalidResponseFromScimEndpointException(response, "Created SCIM resource does not have id"));
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn(e);
|
||||
throw new InvalidResponseFromScimEndpointException("Exception while retrying create " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkResponseIsSuccess(ServerResponse<S> response) throws InvalidResponseFromScimEndpointException {
|
||||
|
@ -99,23 +101,31 @@ public class ScimClient<S extends ResourceNode> implements AutoCloseable {
|
|||
|
||||
public void update(EntityOnRemoteScimId externalId, S scimForReplace) throws InvalidResponseFromScimEndpointException {
|
||||
Retry retry = retryRegistry.retry("replace-%s".formatted(externalId.asString()));
|
||||
// TODO Exception handling : check that all exceptions are wrapped in server response
|
||||
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
|
||||
.update(getResourceClass(), getScimEndpoint(), externalId.asString())
|
||||
.setResource(scimForReplace)
|
||||
.sendRequest()
|
||||
);
|
||||
checkResponseIsSuccess(response);
|
||||
try {
|
||||
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
|
||||
.update(getResourceClass(), getScimEndpoint(), externalId.asString())
|
||||
.setResource(scimForReplace)
|
||||
.sendRequest()
|
||||
);
|
||||
checkResponseIsSuccess(response);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn(e);
|
||||
throw new InvalidResponseFromScimEndpointException("Exception while retrying update " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(EntityOnRemoteScimId externalId) throws InvalidResponseFromScimEndpointException {
|
||||
Retry retry = retryRegistry.retry("delete-%s".formatted(externalId.asString()));
|
||||
// TODO Exception handling : check that all exceptions are wrapped in server response
|
||||
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
|
||||
.delete(getResourceClass(), getScimEndpoint(), externalId.asString())
|
||||
.sendRequest()
|
||||
);
|
||||
checkResponseIsSuccess(response);
|
||||
try {
|
||||
ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder
|
||||
.delete(getResourceClass(), getScimEndpoint(), externalId.asString())
|
||||
.sendRequest()
|
||||
);
|
||||
checkResponseIsSuccess(response);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn(e);
|
||||
throw new InvalidResponseFromScimEndpointException("Exception while retrying delete " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -20,11 +20,12 @@ import sh.libre.scim.core.exceptions.UnexpectedScimDataException;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class UserScimService extends AbstractScimService<UserModel, User> {
|
||||
private final Logger LOGGER = Logger.getLogger(UserScimService.class);
|
||||
private static final Logger LOGGER = Logger.getLogger(UserScimService.class);
|
||||
|
||||
public UserScimService(
|
||||
KeycloakSession keycloakSession,
|
||||
|
@ -56,7 +57,9 @@ public class UserScimService extends AbstractScimService<UserModel, User> {
|
|||
if (matchedByUsername.isPresent()
|
||||
&& matchedByEmail.isPresent()
|
||||
&& !matchedByUsername.equals(matchedByEmail)) {
|
||||
throw new InconsistentScimMappingException("Found 2 possible users for remote user " + matchedByUsername.get() + " - " + matchedByEmail.get());
|
||||
String inconstencyErrorMessage = "Found 2 possible users for remote user " + matchedByUsername.get() + " - " + matchedByEmail.get();
|
||||
LOGGER.warn(inconstencyErrorMessage);
|
||||
throw new InconsistentScimMappingException(inconstencyErrorMessage);
|
||||
}
|
||||
if (matchedByUsername.isPresent()) {
|
||||
return matchedByUsername;
|
||||
|
@ -94,12 +97,12 @@ public class UserScimService extends AbstractScimService<UserModel, User> {
|
|||
String firstAndLastName = String.format("%s %s",
|
||||
StringUtils.defaultString(roleMapperModel.getFirstName()),
|
||||
StringUtils.defaultString(roleMapperModel.getLastName())).trim();
|
||||
String displayName = StringUtils.defaultString(firstAndLastName, roleMapperModel.getUsername());
|
||||
String displayName = Objects.toString(firstAndLastName, roleMapperModel.getUsername());
|
||||
Stream<RoleModel> groupRoleModels = roleMapperModel.getGroupsStream().flatMap(RoleMapperModel::getRoleMappingsStream);
|
||||
Stream<RoleModel> roleModels = roleMapperModel.getRoleMappingsStream();
|
||||
Stream<RoleModel> allRoleModels = Stream.concat(groupRoleModels, roleModels);
|
||||
List<PersonRole> roles = allRoleModels
|
||||
.filter((r) -> BooleanUtils.TRUE.equals(r.getFirstAttribute("scim")))
|
||||
.filter(r -> BooleanUtils.TRUE.equals(r.getFirstAttribute("scim")))
|
||||
.map(RoleModel::getName)
|
||||
.map(roleName -> {
|
||||
PersonRole personRole = new PersonRole();
|
||||
|
|
|
@ -63,7 +63,7 @@ public class ScimBackgroundGroupMembershipUpdater {
|
|||
private boolean isDirtyGroup(GroupModel g) {
|
||||
String groupDirtySinceAttribute = g.getFirstAttribute(GROUP_DIRTY_SINCE_ATTRIBUTE_NAME);
|
||||
try {
|
||||
int groupDirtySince = Integer.parseInt(groupDirtySinceAttribute);
|
||||
long groupDirtySince = Long.parseLong(groupDirtySinceAttribute);
|
||||
// Must be dirty for more than DEBOUNCE_DELAY_MS
|
||||
// (otherwise update will be dispatched in next scheduled loop)
|
||||
return System.currentTimeMillis() - groupDirtySince > DEBOUNCE_DELAY_MS;
|
||||
|
|
|
@ -91,7 +91,6 @@ public class ScimResourceDao {
|
|||
}
|
||||
|
||||
public void delete(ScimResourceMapping resource) {
|
||||
EntityManager entityManager = getEntityManager();
|
||||
entityManager.remove(resource);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ import sh.libre.scim.core.service.KeycloakId;
|
|||
@IdClass(ScimResourceId.class)
|
||||
@Table(name = "SCIM_RESOURCE_MAPPING")
|
||||
@NamedQueries({
|
||||
@NamedQuery(name = "findById", query = "from ScimResource where realmId = :realmId and componentId = :componentId and type = :type and id = :id"),
|
||||
@NamedQuery(name = "findByExternalId", query = "from ScimResource where realmId = :realmId and componentId = :componentId and type = :type and externalId = :id")
|
||||
@NamedQuery(name = "findById", query = "from ScimResourceMapping where realmId = :realmId and componentId = :componentId and type = :type and id = :id"),
|
||||
@NamedQuery(name = "findByExternalId", query = "from ScimResourceMapping where realmId = :realmId and componentId = :componentId and type = :type and externalId = :id")
|
||||
})
|
||||
public class ScimResourceMapping {
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ public class ScimResourceProvider implements JpaEntityProvider {
|
|||
|
||||
@Override
|
||||
public void close() {
|
||||
// Nothing to close
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -22,13 +22,13 @@
|
|||
</column>
|
||||
</createTable>
|
||||
|
||||
<addPrimaryKey constraintName="PK_SCIM_RESOURCE" tableName="SCIM_RESOURCE"
|
||||
<addPrimaryKey constraintName="PK_SCIM_RESOURCE_MAPPING" tableName="SCIM_RESOURCE_MAPPING"
|
||||
columnNames="ID,REALM_ID,TYPE,COMPONENT_ID,EXTERNAL_ID"/>
|
||||
<addForeignKeyConstraint baseTableName="SCIM_RESOURCE" baseColumnNames="REALM_ID"
|
||||
constraintName="FK_SCIM_RESOURCE_REALM" referencedTableName="REALM"
|
||||
<addForeignKeyConstraint baseTableName="SCIM_RESOURCE_MAPPING" baseColumnNames="REALM_ID"
|
||||
constraintName="FK_SCIM_RESOURCE_MAPPING_REALM" referencedTableName="REALM"
|
||||
referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||
<addForeignKeyConstraint baseTableName="SCIM_RESOURCE" baseColumnNames="COMPONENT_ID"
|
||||
constraintName="FK_SCIM_RESOURCE_COMPONENT" referencedTableName="COMPONENT"
|
||||
<addForeignKeyConstraint baseTableName="SCIM_RESOURCE_MAPPING" baseColumnNames="COMPONENT_ID"
|
||||
constraintName="FK_SCIM_RESOURCE_MAPPING_COMPONENT" referencedTableName="COMPONENT"
|
||||
referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||
</changeSet>
|
||||
|
||||
|
|
Loading…
Reference in a new issue