Simplify ScimStorageProvider
This commit is contained in:
parent
764767185e
commit
f00130d37a
6 changed files with 161 additions and 162 deletions
|
@ -5,14 +5,14 @@ import org.keycloak.storage.user.SynchronizationResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface for defining ScimClient.
|
* An interface for defining ScimClient.
|
||||||
* A ScimClient provides methods for propagating CRUD and sync of a dedicated SCIM Resource (e.g. {@link de.captaingoldfish.scim.sdk.common.resources.User}). to a SCIM server defined in a {@link sh.libre.scim.storage.ScimStorageProvider}.
|
* A ScimClient provides methods for propagating CRUD and sync of a dedicated SCIM Resource (e.g. {@link de.captaingoldfish.scim.sdk.common.resources.User}). to a Scim endpoint defined in a {@link sh.libre.scim.storage.ScimStorageProvider}.
|
||||||
*
|
*
|
||||||
* @param <M> the keycloack model to synchronize (e.g. UserModel or GroupModel)
|
* @param <M> the keycloack model to synchronize (e.g. UserModel or GroupModel)
|
||||||
*/
|
*/
|
||||||
public interface ScimClientInterface<M extends RoleMapperModel> extends AutoCloseable {
|
public interface ScimClientInterface<M extends RoleMapperModel> extends AutoCloseable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Propagates the creation of the given keycloack model to a SCIM server.
|
* Propagates the creation of the given keycloack model to a Scim endpoint.
|
||||||
*
|
*
|
||||||
* @param resource the created resource to propagate (e.g. a new UserModel)
|
* @param resource the created resource to propagate (e.g. a new UserModel)
|
||||||
*/
|
*/
|
||||||
|
@ -20,21 +20,21 @@ public interface ScimClientInterface<M extends RoleMapperModel> extends AutoClos
|
||||||
void create(M resource);
|
void create(M resource);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Propagates the update of the given keycloack model to a SCIM server.
|
* Propagates the update of the given keycloack model to a Scim endpoint.
|
||||||
*
|
*
|
||||||
* @param resource the resource creation to propagate (e.g. a UserModel)
|
* @param resource the resource creation to propagate (e.g. a UserModel)
|
||||||
*/
|
*/
|
||||||
void replace(M resource);
|
void replace(M resource);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Propagates the deletion of an element to a SCIM server.
|
* Propagates the deletion of an element to a Scim endpoint.
|
||||||
*
|
*
|
||||||
* @param id the deleted resource's id to propagate (e.g. id of a UserModel)
|
* @param id the deleted resource's id to propagate (e.g. id of a UserModel)
|
||||||
*/
|
*/
|
||||||
void delete(String id);
|
void delete(String id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronizes resources between SCIM server and keycloack, according to configuration.
|
* Synchronizes resources between Scim endpoint and keycloack, according to configuration.
|
||||||
*
|
*
|
||||||
* @param result the synchronization result to update for indicating triggered operations (e.g. user deletions)
|
* @param result the synchronization result to update for indicating triggered operations (e.g. user deletions)
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -9,7 +9,7 @@ import java.util.function.Consumer;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In charge of sending SCIM Request to all registered SCIM servers.
|
* In charge of sending SCIM Request to all registered Scim endpoints.
|
||||||
*/
|
*/
|
||||||
public class ScimDispatcher {
|
public class ScimDispatcher {
|
||||||
|
|
||||||
|
@ -22,12 +22,12 @@ public class ScimDispatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispatchUserModificationToAll(Consumer<UserScimClient> operationToDispatch) {
|
public void dispatchUserModificationToAll(Consumer<UserScimClient> operationToDispatch) {
|
||||||
getAllSCIMServer(Scope.USER).forEach(scimServer -> dispatchUserModificationToOne(scimServer, operationToDispatch));
|
getAllSCIMServer(Scope.USER).forEach(scimServerConfiguration -> dispatchUserModificationToOne(scimServerConfiguration, operationToDispatch));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispatchUserModificationToOne(ComponentModel scimServer, Consumer<UserScimClient> operationToDispatch) {
|
public void dispatchUserModificationToOne(ComponentModel scimServerConfiguration, Consumer<UserScimClient> operationToDispatch) {
|
||||||
LOGGER.infof("%s %s %s %s", scimServer.getId(), scimServer.getName(), scimServer.getProviderId(), scimServer.getProviderType());
|
LOGGER.infof("%s %s %s %s", scimServerConfiguration.getId(), scimServerConfiguration.getName(), scimServerConfiguration.getProviderId(), scimServerConfiguration.getProviderType());
|
||||||
try (UserScimClient client = UserScimClient.newUserScimClient(scimServer, session)) {
|
try (UserScimClient client = UserScimClient.newUserScimClient(scimServerConfiguration, session)) {
|
||||||
operationToDispatch.accept(client);
|
operationToDispatch.accept(client);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error(e);
|
LOGGER.error(e);
|
||||||
|
@ -35,12 +35,12 @@ public class ScimDispatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispatchGroupModificationToAll(Consumer<GroupScimClient> operationToDispatch) {
|
public void dispatchGroupModificationToAll(Consumer<GroupScimClient> operationToDispatch) {
|
||||||
getAllSCIMServer(Scope.GROUP).forEach(scimServer -> dispatchGroupModificationToOne(scimServer, operationToDispatch));
|
getAllSCIMServer(Scope.GROUP).forEach(scimServerConfiguration -> dispatchGroupModificationToOne(scimServerConfiguration, operationToDispatch));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispatchGroupModificationToOne(ComponentModel scimServer, Consumer<GroupScimClient> operationToDispatch) {
|
public void dispatchGroupModificationToOne(ComponentModel scimServerConfiguration, Consumer<GroupScimClient> operationToDispatch) {
|
||||||
LOGGER.infof("%s %s %s %s", scimServer.getId(), scimServer.getName(), scimServer.getProviderId(), scimServer.getProviderType());
|
LOGGER.infof("%s %s %s %s", scimServerConfiguration.getId(), scimServerConfiguration.getName(), scimServerConfiguration.getProviderId(), scimServerConfiguration.getProviderType());
|
||||||
try (GroupScimClient client = GroupScimClient.newGroupScimClient(scimServer, session)) {
|
try (GroupScimClient client = GroupScimClient.newGroupScimClient(scimServerConfiguration, session)) {
|
||||||
operationToDispatch.accept(client);
|
operationToDispatch.accept(client);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error(e);
|
LOGGER.error(e);
|
||||||
|
@ -49,14 +49,19 @@ public class ScimDispatcher {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param scope The {@link Scope} to consider (User or Group)
|
* @param scope The {@link Scope} to consider (User or Group)
|
||||||
* @return all enabled registered SCIM Servers with propagation enabled for the given scope
|
* @return all enabled registered Scim endpoints with propagation enabled for the given scope
|
||||||
*/
|
*/
|
||||||
private Stream<ComponentModel> getAllSCIMServer(Scope scope) {
|
private Stream<ComponentModel> getAllSCIMServer(Scope scope) {
|
||||||
// TODO : we could initiative this list once and invalidate it when configuration changes
|
// TODO : we could initiative this list once and invalidate it when configuration changes
|
||||||
|
|
||||||
|
String propagationConfKey = switch (scope) {
|
||||||
|
case GROUP -> ScrimProviderConfiguration.CONF_KEY_PROPAGATION_GROUP;
|
||||||
|
case USER -> ScrimProviderConfiguration.CONF_KEY_PROPAGATION_USER;
|
||||||
|
};
|
||||||
return session.getContext().getRealm().getComponentsStream()
|
return session.getContext().getRealm().getComponentsStream()
|
||||||
.filter(m -> ScimStorageProviderFactory.ID.equals(m.getProviderId())
|
.filter(m -> ScimStorageProviderFactory.ID.equals(m.getProviderId())
|
||||||
&& m.get("enabled", true)
|
&& m.get("enabled", true)
|
||||||
&& m.get("propagation-" + scope.name(), false));
|
&& m.get(propagationConfKey, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Scope {
|
public enum Scope {
|
||||||
|
|
|
@ -4,6 +4,17 @@ import de.captaingoldfish.scim.sdk.client.http.BasicAuth;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
|
|
||||||
public class ScrimProviderConfiguration {
|
public class ScrimProviderConfiguration {
|
||||||
|
// 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";
|
||||||
|
public static final String CONF_KEY_AUTH_USER = "auth-user";
|
||||||
|
public static final String CONF_KEY_CONTENT_TYPE = "content-type";
|
||||||
|
public static final String CONF_KEY_ENDPOINT = "endpoint";
|
||||||
|
public static final String CONF_KEY_SYNC_IMPORT_ACTION = "sync-import-action";
|
||||||
|
public static final String CONF_KEY_SYNC_IMPORT = "sync-import";
|
||||||
|
public static final String CONF_KEY_SYNC_REFRESH = "sync-refresh";
|
||||||
|
public static final String CONF_KEY_PROPAGATION_USER = "propagation-user";
|
||||||
|
public static final String CONF_KEY_PROPAGATION_GROUP = "propagation-group";
|
||||||
|
|
||||||
private final String endPoint;
|
private final String endPoint;
|
||||||
private final String id;
|
private final String id;
|
||||||
|
@ -14,25 +25,25 @@ public class ScrimProviderConfiguration {
|
||||||
private final boolean syncRefresh;
|
private final boolean syncRefresh;
|
||||||
|
|
||||||
public ScrimProviderConfiguration(ComponentModel scimProviderConfiguration) {
|
public ScrimProviderConfiguration(ComponentModel scimProviderConfiguration) {
|
||||||
AuthMode authMode = AuthMode.valueOf(scimProviderConfiguration.get("auth-mode"));
|
AuthMode authMode = AuthMode.valueOf(scimProviderConfiguration.get(CONF_KEY_AUTH_MODE));
|
||||||
authorizationHeaderValue = switch (authMode) {
|
authorizationHeaderValue = switch (authMode) {
|
||||||
case BEARER -> "Bearer " + scimProviderConfiguration.get("auth-pass");
|
case BEARER -> "Bearer " + scimProviderConfiguration.get(CONF_KEY_AUTH_PASSWORD);
|
||||||
case BASIC_AUTH -> {
|
case BASIC_AUTH -> {
|
||||||
BasicAuth basicAuth = BasicAuth.builder()
|
BasicAuth basicAuth = BasicAuth.builder()
|
||||||
.username(scimProviderConfiguration.get("auth-user"))
|
.username(scimProviderConfiguration.get(CONF_KEY_AUTH_USER))
|
||||||
.password(scimProviderConfiguration.get("auth-pass"))
|
.password(scimProviderConfiguration.get(CONF_KEY_AUTH_PASSWORD))
|
||||||
.build();
|
.build();
|
||||||
yield basicAuth.getAuthorizationHeaderValue();
|
yield basicAuth.getAuthorizationHeaderValue();
|
||||||
}
|
}
|
||||||
default ->
|
default ->
|
||||||
throw new IllegalArgumentException("authMode " + scimProviderConfiguration + " is not supported");
|
throw new IllegalArgumentException("authMode " + scimProviderConfiguration + " is not supported");
|
||||||
};
|
};
|
||||||
contentType = scimProviderConfiguration.get("content-type");
|
contentType = scimProviderConfiguration.get(CONF_KEY_CONTENT_TYPE);
|
||||||
endPoint = scimProviderConfiguration.get("endpoint");
|
endPoint = scimProviderConfiguration.get(CONF_KEY_ENDPOINT);
|
||||||
id = scimProviderConfiguration.getId();
|
id = scimProviderConfiguration.getId();
|
||||||
importAction = ImportAction.valueOf(scimProviderConfiguration.get("sync-import-action"));
|
importAction = ImportAction.valueOf(scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT_ACTION));
|
||||||
syncImport = scimProviderConfiguration.get("sync-import", false);
|
syncImport = scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT, false);
|
||||||
syncRefresh = scimProviderConfiguration.get("sync-refresh", false);
|
syncRefresh = scimProviderConfiguration.get(CONF_KEY_SYNC_REFRESH, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSyncRefresh() {
|
public boolean isSyncRefresh() {
|
||||||
|
@ -67,10 +78,6 @@ public class ScrimProviderConfiguration {
|
||||||
BEARER, BASIC_AUTH, NONE
|
BEARER, BASIC_AUTH, NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum EndpointContentType {
|
|
||||||
JSON, SCIM_JSON
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ImportAction {
|
public enum ImportAction {
|
||||||
CREATE_LOCAL, DELETE_REMOTE, NOTHING
|
CREATE_LOCAL, DELETE_REMOTE, NOTHING
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import java.util.regex.Pattern;
|
||||||
/**
|
/**
|
||||||
* An {@link java.util.EventListener} in charge of reaction to Keycloak models
|
* An {@link java.util.EventListener} in charge of reaction to Keycloak models
|
||||||
* modification (e.g. User creation, Group deletion, membership modifications...)
|
* modification (e.g. User creation, Group deletion, membership modifications...)
|
||||||
* by propagating it to all registered SCIM servers.
|
* by propagating it to all registered Scim endpoints.
|
||||||
*/
|
*/
|
||||||
public class ScimEventListenerProvider implements EventListenerProvider {
|
public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
}
|
}
|
||||||
case DELETE_ACCOUNT -> dispatcher.dispatchUserModificationToAll(client -> client.delete(eventUserId));
|
case DELETE_ACCOUNT -> dispatcher.dispatchUserModificationToAll(client -> client.delete(eventUserId));
|
||||||
default -> {
|
default -> {
|
||||||
// No other event has to be propagated to SCIM Servers
|
// No other event has to be propagated to Scim endpoints
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
handleRoleMappingEvent(event, type, id);
|
handleRoleMappingEvent(event, type, id);
|
||||||
}
|
}
|
||||||
default -> {
|
default -> {
|
||||||
// No other resource modification has to be propagated to SCIM Servers
|
// No other resource modification has to be propagated to Scim endpoints
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Propagating the given group-related event to SCIM servers.
|
* Propagating the given group-related event to Scim endpoints.
|
||||||
*
|
*
|
||||||
* @param event the event to propagate
|
* @param event the event to propagate
|
||||||
* @param groupId event target's id
|
* @param groupId event target's id
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
package sh.libre.scim.storage;
|
|
||||||
|
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
|
||||||
|
|
||||||
public class ScimStorageProvider implements UserStorageProvider {
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,131 +8,41 @@ import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.KeycloakSessionTask;
|
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.UserStorageProviderFactory;
|
import org.keycloak.storage.UserStorageProviderFactory;
|
||||||
import org.keycloak.storage.UserStorageProviderModel;
|
import org.keycloak.storage.UserStorageProviderModel;
|
||||||
import org.keycloak.storage.user.ImportSynchronization;
|
import org.keycloak.storage.user.ImportSynchronization;
|
||||||
import org.keycloak.storage.user.SynchronizationResult;
|
import org.keycloak.storage.user.SynchronizationResult;
|
||||||
import org.keycloak.timer.TimerProvider;
|
import org.keycloak.timer.TimerProvider;
|
||||||
import sh.libre.scim.core.GroupAdapter;
|
|
||||||
import sh.libre.scim.core.ScimDispatcher;
|
import sh.libre.scim.core.ScimDispatcher;
|
||||||
import sh.libre.scim.core.UserAdapter;
|
import sh.libre.scim.core.ScrimProviderConfiguration;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ScimStorageProviderFactory
|
public class ScimStorageProviderFactory
|
||||||
implements UserStorageProviderFactory<ScimStorageProvider>, ImportSynchronization {
|
implements UserStorageProviderFactory<ScimStorageProviderFactory.ScimStorageProvider>, ImportSynchronization {
|
||||||
|
|
||||||
private final Logger LOGGER = Logger.getLogger(ScimStorageProviderFactory.class);
|
|
||||||
|
|
||||||
public static final String ID = "scim";
|
public static final String ID = "scim";
|
||||||
|
private final Logger logger = Logger.getLogger(ScimStorageProviderFactory.class);
|
||||||
private static final List<ProviderConfigProperty> CONFIG_METADATA;
|
|
||||||
|
|
||||||
static {
|
|
||||||
CONFIG_METADATA = ProviderConfigurationBuilder.create()
|
|
||||||
.property()
|
|
||||||
.name("endpoint")
|
|
||||||
.type(ProviderConfigProperty.STRING_TYPE)
|
|
||||||
.required(true)
|
|
||||||
.label("SCIM 2.0 endpoint")
|
|
||||||
.helpText("External SCIM 2.0 base " +
|
|
||||||
"URL (/ServiceProviderConfig /Schemas and /ResourcesTypes should be accessible)")
|
|
||||||
.add()
|
|
||||||
.property()
|
|
||||||
.name("content-type")
|
|
||||||
.type(ProviderConfigProperty.LIST_TYPE)
|
|
||||||
.label("Endpoint content type")
|
|
||||||
.helpText("Only used when endpoint doesn't support application/scim+json")
|
|
||||||
.options(MediaType.APPLICATION_JSON, HttpHeader.SCIM_CONTENT_TYPE)
|
|
||||||
.defaultValue(HttpHeader.SCIM_CONTENT_TYPE)
|
|
||||||
.add()
|
|
||||||
.property()
|
|
||||||
.name("auth-mode")
|
|
||||||
.type(ProviderConfigProperty.LIST_TYPE)
|
|
||||||
.label("Auth mode")
|
|
||||||
.helpText("Select the authorization mode")
|
|
||||||
.options("NONE", "BASIC_AUTH", "BEARER")
|
|
||||||
.defaultValue("NONE")
|
|
||||||
.add()
|
|
||||||
.property()
|
|
||||||
.name("auth-user")
|
|
||||||
.type(ProviderConfigProperty.STRING_TYPE)
|
|
||||||
.label("Auth username")
|
|
||||||
.helpText("Required for basic authentication.")
|
|
||||||
.add()
|
|
||||||
.property()
|
|
||||||
.name("auth-pass")
|
|
||||||
.type(ProviderConfigProperty.PASSWORD)
|
|
||||||
.label("Auth password/token")
|
|
||||||
.helpText("Password or token required for basic or bearer authentication.")
|
|
||||||
.add()
|
|
||||||
.property()
|
|
||||||
.name("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("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("sync-import")
|
|
||||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
|
||||||
.label("Enable import during sync")
|
|
||||||
.add()
|
|
||||||
.property()
|
|
||||||
.name("sync-import-action")
|
|
||||||
.type(ProviderConfigProperty.LIST_TYPE)
|
|
||||||
.label("Import action")
|
|
||||||
.helpText("What to do when the user doesn't exists in Keycloak.")
|
|
||||||
.options("NOTHING", "CREATE_LOCAL", "DELETE_REMOTE")
|
|
||||||
.defaultValue("CREATE_LOCAL")
|
|
||||||
.add()
|
|
||||||
.property()
|
|
||||||
.name("sync-refresh")
|
|
||||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
|
||||||
.label("Enable refresh during sync")
|
|
||||||
.add()
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ScimStorageProvider create(KeycloakSession session, ComponentModel model) {
|
|
||||||
LOGGER.info("create");
|
|
||||||
return new ScimStorageProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return ID;
|
return ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<ProviderConfigProperty> getConfigProperties() {
|
|
||||||
return CONFIG_METADATA;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId,
|
public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId,
|
||||||
UserStorageProviderModel model) {
|
UserStorageProviderModel model) {
|
||||||
LOGGER.info("sync");
|
// TODO if this should be kept here, better document prupose & usage
|
||||||
|
logger.infof("[SCIM] Sync from ScimStorageProvider - Realm %s", realmId);
|
||||||
SynchronizationResult result = new SynchronizationResult();
|
SynchronizationResult result = new SynchronizationResult();
|
||||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> {
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run(KeycloakSession session) {
|
|
||||||
RealmModel realm = session.realms().getRealm(realmId);
|
RealmModel realm = session.realms().getRealm(realmId);
|
||||||
session.getContext().setRealm(realm);
|
session.getContext().setRealm(realm);
|
||||||
ScimDispatcher dispatcher = new ScimDispatcher(session);
|
ScimDispatcher dispatcher = new ScimDispatcher(session);
|
||||||
|
@ -142,12 +52,8 @@ public class ScimStorageProviderFactory
|
||||||
if (BooleanUtils.TRUE.equals(model.get("propagation-group"))) {
|
if (BooleanUtils.TRUE.equals(model.get("propagation-group"))) {
|
||||||
dispatcher.dispatchGroupModificationToOne(model, client -> client.sync(result));
|
dispatcher.dispatchGroupModificationToOne(model, client -> client.sync(result));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -158,24 +64,114 @@ public class ScimStorageProviderFactory
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
TimerProvider timer = factory.create().getProvider(TimerProvider.class);
|
// TODO : find a better way to handle scim dirty (use a QUEUE for SCIM queries ?)
|
||||||
|
try (KeycloakSession keycloakSession = factory.create()) {
|
||||||
|
TimerProvider timer = keycloakSession.getProvider(TimerProvider.class);
|
||||||
timer.scheduleTask(taskSession -> {
|
timer.scheduleTask(taskSession -> {
|
||||||
for (RealmModel realm : taskSession.realms().getRealmsStream().toList()) {
|
for (RealmModel realm : taskSession.realms().getRealmsStream().toList()) {
|
||||||
KeycloakModelUtils.runJobInTransaction(factory, new KeycloakSessionTask() {
|
KeycloakModelUtils.runJobInTransaction(factory, session -> {
|
||||||
@Override
|
|
||||||
public void run(KeycloakSession session) {
|
|
||||||
session.getContext().setRealm(realm);
|
session.getContext().setRealm(realm);
|
||||||
ScimDispatcher dispatcher = new ScimDispatcher(session);
|
ScimDispatcher dispatcher = new ScimDispatcher(session);
|
||||||
for (GroupModel group : session.groups().getGroupsStream(realm)
|
for (GroupModel group : session.groups().getGroupsStream(realm)
|
||||||
.filter(x -> BooleanUtils.TRUE.equals(x.getFirstAttribute("scim-dirty"))).toList()) {
|
.filter(x -> BooleanUtils.TRUE.equals(x.getFirstAttribute("scim-dirty"))).toList()) {
|
||||||
LOGGER.debug(group.getName() + " is dirty");
|
logger.infof("[SCIM] Dirty group : %s", group.getName());
|
||||||
dispatcher.dispatchGroupModificationToAll(client -> client.replace(group));
|
dispatcher.dispatchGroupModificationToAll(client -> client.replace(group));
|
||||||
group.removeAttribute("scim-dirty");
|
group.removeAttribute("scim-dirty");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, Duration.ofSeconds(30).toMillis(), "scim-background");
|
}, Duration.ofSeconds(30).toMillis(), "scim-background");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
// These Config Properties will be use to generate configuration page in Admin Console
|
||||||
|
return ProviderConfigurationBuilder.create()
|
||||||
|
.property()
|
||||||
|
.name(ScrimProviderConfiguration.CONF_KEY_ENDPOINT)
|
||||||
|
.type(ProviderConfigProperty.STRING_TYPE)
|
||||||
|
.required(true)
|
||||||
|
.label("SCIM 2.0 endpoint")
|
||||||
|
.helpText("External SCIM 2.0 base " +
|
||||||
|
"URL (/ServiceProviderConfig /Schemas and /ResourcesTypes should be accessible)")
|
||||||
|
.add()
|
||||||
|
.property()
|
||||||
|
.name(ScrimProviderConfiguration.CONF_KEY_CONTENT_TYPE)
|
||||||
|
.type(ProviderConfigProperty.LIST_TYPE)
|
||||||
|
.label("Endpoint content type")
|
||||||
|
.helpText("Only used when endpoint doesn't support application/scim+json")
|
||||||
|
.options(MediaType.APPLICATION_JSON, HttpHeader.SCIM_CONTENT_TYPE)
|
||||||
|
.defaultValue(HttpHeader.SCIM_CONTENT_TYPE)
|
||||||
|
.add()
|
||||||
|
.property()
|
||||||
|
.name(ScrimProviderConfiguration.CONF_KEY_AUTH_MODE)
|
||||||
|
.type(ProviderConfigProperty.LIST_TYPE)
|
||||||
|
.label("Auth mode")
|
||||||
|
.helpText("Select the authorization mode")
|
||||||
|
.options("NONE", "BASIC_AUTH", "BEARER")
|
||||||
|
.defaultValue("NONE")
|
||||||
|
.add()
|
||||||
|
.property()
|
||||||
|
.name(ScrimProviderConfiguration.CONF_KEY_AUTH_USER)
|
||||||
|
.type(ProviderConfigProperty.STRING_TYPE)
|
||||||
|
.label("Auth username")
|
||||||
|
.helpText("Required for basic authentication.")
|
||||||
|
.add()
|
||||||
|
.property()
|
||||||
|
.name(ScrimProviderConfiguration.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)
|
||||||
|
.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)
|
||||||
|
.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)
|
||||||
|
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||||
|
.label("Enable import during sync")
|
||||||
|
.add()
|
||||||
|
.property()
|
||||||
|
.name(ScrimProviderConfiguration.CONF_KEY_SYNC_IMPORT_ACTION)
|
||||||
|
.type(ProviderConfigProperty.LIST_TYPE)
|
||||||
|
.label("Import action")
|
||||||
|
.helpText("What to do when the user doesn't exists in Keycloak.")
|
||||||
|
.options("NOTHING", "CREATE_LOCAL", "DELETE_REMOTE")
|
||||||
|
.defaultValue("CREATE_LOCAL")
|
||||||
|
.add()
|
||||||
|
.property()
|
||||||
|
.name(ScrimProviderConfiguration.CONF_KEY_SYNC_REFRESH)
|
||||||
|
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||||
|
.label("Enable refresh during sync")
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScimStorageProvider create(KeycloakSession session, ComponentModel model) {
|
||||||
|
return new ScimStorageProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty implementation : we used this {@link ScimStorageProviderFactory} to generate Admin Console page.
|
||||||
|
*/
|
||||||
|
public static final class ScimStorageProvider implements UserStorageProvider {
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// Nothing to close here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue