Simplify ScimStorageProvider

This commit is contained in:
Alex Morel 2024-06-18 16:49:24 +02:00
parent 764767185e
commit f00130d37a
6 changed files with 161 additions and 162 deletions

View file

@ -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)
*/ */

View file

@ -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 {

View file

@ -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
} }

View file

@ -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

View file

@ -1,9 +0,0 @@
package sh.libre.scim.storage;
import org.keycloak.storage.UserStorageProvider;
public class ScimStorageProvider implements UserStorageProvider {
@Override
public void close() {
}
}

View file

@ -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
}
}
}