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