Simplfy code by splitting UserScimClient and GroupSclient - basic UserScimClient
This commit is contained in:
parent
3ef6c81e98
commit
9a45e9c30f
2 changed files with 356 additions and 0 deletions
|
@ -0,0 +1,77 @@
|
||||||
|
package sh.libre.scim.core;
|
||||||
|
|
||||||
|
import de.captaingoldfish.scim.sdk.client.http.BasicAuth;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
|
||||||
|
public class ScrimProviderConfiguration {
|
||||||
|
|
||||||
|
private final String endPoint;
|
||||||
|
private final String id;
|
||||||
|
private final String contentType;
|
||||||
|
private final String authorizationHeaderValue;
|
||||||
|
private final ImportAction importAction;
|
||||||
|
private final boolean syncImport;
|
||||||
|
private final boolean syncRefresh;
|
||||||
|
|
||||||
|
public ScrimProviderConfiguration(ComponentModel scimProviderConfiguration) {
|
||||||
|
AuthMode authMode = AuthMode.valueOf(scimProviderConfiguration.get("auth-mode"));
|
||||||
|
authorizationHeaderValue = switch (authMode) {
|
||||||
|
case BEARER -> "Bearer " + scimProviderConfiguration.get("auth-pass");
|
||||||
|
case BASIC_AUTH -> {
|
||||||
|
BasicAuth basicAuth = BasicAuth.builder()
|
||||||
|
.username(scimProviderConfiguration.get("auth-user"))
|
||||||
|
.password(scimProviderConfiguration.get("auth-pass"))
|
||||||
|
.build();
|
||||||
|
yield basicAuth.getAuthorizationHeaderValue();
|
||||||
|
}
|
||||||
|
default ->
|
||||||
|
throw new IllegalArgumentException("authMode " + scimProviderConfiguration + " is not supported");
|
||||||
|
};
|
||||||
|
contentType = scimProviderConfiguration.get("content-type");
|
||||||
|
endPoint = scimProviderConfiguration.get("endpoint");
|
||||||
|
id = scimProviderConfiguration.getId();
|
||||||
|
importAction = ImportAction.valueOf(scimProviderConfiguration.get("sync-import-action"));
|
||||||
|
syncImport = scimProviderConfiguration.get("sync-import", false);
|
||||||
|
syncRefresh = scimProviderConfiguration.get("sync-refresh", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSyncRefresh() {
|
||||||
|
return syncRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSyncImport() {
|
||||||
|
return syncImport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthorizationHeaderValue() {
|
||||||
|
return authorizationHeaderValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportAction getImportAction() {
|
||||||
|
return importAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEndPoint() {
|
||||||
|
return endPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AuthMode {
|
||||||
|
BEARER, BASIC_AUTH, NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum EndpointContentType {
|
||||||
|
JSON, SCIM_JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ImportAction {
|
||||||
|
CREATE_LOCAL, DELETE_REMOTE, NOTHING
|
||||||
|
}
|
||||||
|
}
|
279
src/main/java/sh/libre/scim/core/UserScimClient.java
Normal file
279
src/main/java/sh/libre/scim/core/UserScimClient.java
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
package sh.libre.scim.core;
|
||||||
|
|
||||||
|
import com.google.common.net.HttpHeaders;
|
||||||
|
import de.captaingoldfish.scim.sdk.client.ScimClientConfig;
|
||||||
|
import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder;
|
||||||
|
import de.captaingoldfish.scim.sdk.client.response.ServerResponse;
|
||||||
|
import de.captaingoldfish.scim.sdk.common.exceptions.ResponseException;
|
||||||
|
import de.captaingoldfish.scim.sdk.common.resources.User;
|
||||||
|
import de.captaingoldfish.scim.sdk.common.response.ListResponse;
|
||||||
|
import io.github.resilience4j.core.IntervalFunction;
|
||||||
|
import io.github.resilience4j.retry.Retry;
|
||||||
|
import io.github.resilience4j.retry.RetryConfig;
|
||||||
|
import io.github.resilience4j.retry.RetryRegistry;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.NoResultException;
|
||||||
|
import jakarta.ws.rs.ProcessingException;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.storage.user.SynchronizationResult;
|
||||||
|
import sh.libre.scim.jpa.ScimResource;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class UserScimClient implements AutoCloseable {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(UserScimClient.class);
|
||||||
|
|
||||||
|
private final ScimRequestBuilder scimRequestBuilder;
|
||||||
|
|
||||||
|
private final RetryRegistry retryRegistry;
|
||||||
|
|
||||||
|
private final KeycloakSession keycloakSession;
|
||||||
|
|
||||||
|
private final ScrimProviderConfiguration scimProviderConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new {@link UserScimClient}
|
||||||
|
*
|
||||||
|
* @param scimRequestBuilder
|
||||||
|
* @param retryRegistry Retry policy to use
|
||||||
|
* @param keycloakSession
|
||||||
|
* @param scimProviderConfiguration
|
||||||
|
*/
|
||||||
|
private UserScimClient(ScimRequestBuilder scimRequestBuilder, RetryRegistry retryRegistry, KeycloakSession keycloakSession, ScrimProviderConfiguration scimProviderConfiguration) {
|
||||||
|
this.scimRequestBuilder = scimRequestBuilder;
|
||||||
|
this.retryRegistry = retryRegistry;
|
||||||
|
this.keycloakSession = keycloakSession;
|
||||||
|
this.scimProviderConfiguration = scimProviderConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static UserScimClient newUserScimClient(ComponentModel componentModel, KeycloakSession session) {
|
||||||
|
ScrimProviderConfiguration scimProviderConfiguration = new ScrimProviderConfiguration(componentModel);
|
||||||
|
Map<String, String> httpHeaders = new HashMap<>();
|
||||||
|
httpHeaders.put(HttpHeaders.AUTHORIZATION, scimProviderConfiguration.getAuthorizationHeaderValue());
|
||||||
|
httpHeaders.put(HttpHeaders.CONTENT_TYPE, scimProviderConfiguration.getContentType());
|
||||||
|
|
||||||
|
ScimClientConfig scimClientConfig = ScimClientConfig.builder()
|
||||||
|
.httpHeaders(httpHeaders)
|
||||||
|
.connectTimeout(5)
|
||||||
|
.requestTimeout(5)
|
||||||
|
.socketTimeout(5)
|
||||||
|
.expectedHttpResponseHeaders(Collections.emptyMap()) // strange, useful?
|
||||||
|
// TODO Question Indiehoster : should we really allow connection with TLS ? .hostnameVerifier((s, sslSession) -> true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String scimApplicationBaseUrl = scimProviderConfiguration.getEndPoint();
|
||||||
|
ScimRequestBuilder scimRequestBuilder =
|
||||||
|
new ScimRequestBuilder(
|
||||||
|
scimApplicationBaseUrl,
|
||||||
|
scimClientConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
RetryConfig retryConfig = RetryConfig.custom()
|
||||||
|
.maxAttempts(10)
|
||||||
|
.intervalFunction(IntervalFunction.ofExponentialBackoff())
|
||||||
|
.retryExceptions(ProcessingException.class)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RetryRegistry retryRegistry = RetryRegistry.of(retryConfig);
|
||||||
|
return new UserScimClient(scimRequestBuilder, retryRegistry, session, scimProviderConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void create(UserModel userModel) {
|
||||||
|
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
||||||
|
adapter.apply(userModel);
|
||||||
|
if (adapter.skip)
|
||||||
|
return;
|
||||||
|
// If mapping exist then it was created by import so skip.
|
||||||
|
if (adapter.query("findById", adapter.getId()).getResultList().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Retry retry = retryRegistry.retry("create-" + adapter.getId());
|
||||||
|
ServerResponse<User> response = retry.executeSupplier(() -> {
|
||||||
|
try {
|
||||||
|
return scimRequestBuilder
|
||||||
|
.create(adapter.getResourceClass(), adapter.getScimEndpoint())
|
||||||
|
.setResource(adapter.toScim())
|
||||||
|
.sendRequest();
|
||||||
|
} catch (ResponseException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
LOGGER.warn(response.getResponseBody());
|
||||||
|
LOGGER.warn(response.getHttpStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.apply(response.getResource());
|
||||||
|
adapter.saveMapping();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replace(UserModel userModel) {
|
||||||
|
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
||||||
|
try {
|
||||||
|
adapter.apply(userModel);
|
||||||
|
if (adapter.skip)
|
||||||
|
return;
|
||||||
|
ScimResource resource = adapter.query("findById", adapter.getId()).getSingleResult();
|
||||||
|
adapter.apply(resource);
|
||||||
|
Retry retry = retryRegistry.retry("replace-" + adapter.getId());
|
||||||
|
ServerResponse<User> response = retry.executeSupplier(() -> {
|
||||||
|
try {
|
||||||
|
return scimRequestBuilder
|
||||||
|
.update(adapter.getResourceClass(), adapter.getScimEndpoint(), adapter.getExternalId())
|
||||||
|
.setResource(adapter.toScim())
|
||||||
|
.sendRequest();
|
||||||
|
} catch (ResponseException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
LOGGER.warn(response.getResponseBody());
|
||||||
|
LOGGER.warn(response.getHttpStatus());
|
||||||
|
}
|
||||||
|
} catch (NoResultException e) {
|
||||||
|
LOGGER.warnf("failed to replace resource %s, scim mapping not found", adapter.getId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String id) {
|
||||||
|
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
||||||
|
adapter.setId(id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ScimResource resource = adapter.query("findById", adapter.getId()).getSingleResult();
|
||||||
|
adapter.apply(resource);
|
||||||
|
|
||||||
|
Retry retry = retryRegistry.retry("delete-" + id);
|
||||||
|
ServerResponse<User> response = retry.executeSupplier(() -> {
|
||||||
|
try {
|
||||||
|
return scimRequestBuilder.delete(adapter.getResourceClass(), adapter.getScimEndpoint(), adapter.getExternalId())
|
||||||
|
.sendRequest();
|
||||||
|
} catch (ResponseException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
LOGGER.warn(response.getResponseBody());
|
||||||
|
LOGGER.warn(response.getHttpStatus());
|
||||||
|
}
|
||||||
|
EntityManager entityManager = this.keycloakSession.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||||
|
entityManager.remove(resource);
|
||||||
|
} catch (NoResultException e) {
|
||||||
|
LOGGER.warnf("Failed to delete resource %s, scim mapping not found", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshResources(
|
||||||
|
SynchronizationResult syncRes) {
|
||||||
|
LOGGER.info("Refresh resources");
|
||||||
|
UserAdapter a = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
||||||
|
a.getResourceStream().forEach(resource -> {
|
||||||
|
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
||||||
|
adapter.apply(resource);
|
||||||
|
LOGGER.infof("Reconciling local resource %s", adapter.getId());
|
||||||
|
if (!adapter.skipRefresh()) {
|
||||||
|
ScimResource mapping = adapter.getMapping();
|
||||||
|
if (mapping == null) {
|
||||||
|
LOGGER.info("Creating it");
|
||||||
|
this.create(resource);
|
||||||
|
} else {
|
||||||
|
LOGGER.info("Replacing it");
|
||||||
|
this.replace(resource);
|
||||||
|
}
|
||||||
|
syncRes.increaseUpdated();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void importResources(SynchronizationResult syncRes) {
|
||||||
|
LOGGER.info("Import");
|
||||||
|
try {
|
||||||
|
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
||||||
|
ServerResponse<ListResponse<User>> response = scimRequestBuilder.list(adapter.getResourceClass(), adapter.getScimEndpoint()).get().sendRequest();
|
||||||
|
ListResponse<User> resourceTypeListResponse = response.getResource();
|
||||||
|
|
||||||
|
for (User resource : resourceTypeListResponse.getListedResources()) {
|
||||||
|
try {
|
||||||
|
LOGGER.infof("Reconciling remote resource %s", resource);
|
||||||
|
adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
||||||
|
adapter.apply(resource);
|
||||||
|
|
||||||
|
ScimResource mapping = adapter.getMapping();
|
||||||
|
if (mapping != null) {
|
||||||
|
adapter.apply(mapping);
|
||||||
|
if (adapter.entityExists()) {
|
||||||
|
LOGGER.info("Valid mapping found, skipping");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
LOGGER.info("Delete a dangling mapping");
|
||||||
|
adapter.deleteMapping();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean mapped = adapter.tryToMap();
|
||||||
|
if (mapped) {
|
||||||
|
LOGGER.info("Matched");
|
||||||
|
adapter.saveMapping();
|
||||||
|
} else {
|
||||||
|
switch (this.scimProviderConfiguration.getImportAction()) {
|
||||||
|
case CREATE_LOCAL:
|
||||||
|
LOGGER.info("Create local resource");
|
||||||
|
try {
|
||||||
|
adapter.createEntity();
|
||||||
|
adapter.saveMapping();
|
||||||
|
syncRes.increaseAdded();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DELETE_REMOTE:
|
||||||
|
LOGGER.info("Delete remote resource");
|
||||||
|
scimRequestBuilder
|
||||||
|
.delete(adapter.getResourceClass(), adapter.getScimEndpoint(), resource.getId().get())
|
||||||
|
.sendRequest();
|
||||||
|
syncRes.increaseRemoved();
|
||||||
|
break;
|
||||||
|
case NOTHING:
|
||||||
|
LOGGER.info("Import action set to NOTHING");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e);
|
||||||
|
e.printStackTrace();
|
||||||
|
syncRes.increaseFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ResponseException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sync(SynchronizationResult syncRes) {
|
||||||
|
if (this.scimProviderConfiguration.isSyncImport()) {
|
||||||
|
this.importResources(syncRes);
|
||||||
|
}
|
||||||
|
if (this.scimProviderConfiguration.isSyncRefresh()) {
|
||||||
|
this.refreshResources(syncRes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
scimRequestBuilder.close();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue