ScimDispatcher simplification : split User and Group client class to simplify usage
This commit is contained in:
parent
9a45e9c30f
commit
5976031ac6
6 changed files with 139 additions and 40 deletions
37
src/main/java/sh/libre/scim/core/GroupScimClient.java
Normal file
37
src/main/java/sh/libre/scim/core/GroupScimClient.java
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package sh.libre.scim.core;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.storage.user.SynchronizationResult;
|
||||||
|
|
||||||
|
public class GroupScimClient implements ScimClientInterface<GroupModel> {
|
||||||
|
public static GroupScimClient newGroupScimClient(ComponentModel scimServer, KeycloakSession session) {
|
||||||
|
return new GroupScimClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create(GroupModel resource) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void replace(GroupModel resource) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String id) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sync(SynchronizationResult result) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
42
src/main/java/sh/libre/scim/core/ScimClientInterface.java
Normal file
42
src/main/java/sh/libre/scim/core/ScimClientInterface.java
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package sh.libre.scim.core;
|
||||||
|
|
||||||
|
import org.keycloak.models.RoleMapperModel;
|
||||||
|
import org.keycloak.storage.user.SynchronizationResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}.
|
||||||
|
*
|
||||||
|
* @param <M> the keycloack model to synchronize (e.g. UserModel or GroupModel)
|
||||||
|
*/
|
||||||
|
public interface ScimClientInterface<M extends RoleMapperModel> extends AutoCloseable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Propagates the creation of the given keycloack model to a SCIM server.
|
||||||
|
*
|
||||||
|
* @param resource the created resource to propagate (e.g. a new UserModel)
|
||||||
|
*/
|
||||||
|
// TODO rename method (e.g. propagateCreation)
|
||||||
|
void create(M resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Propagates the update of the given keycloack model to a SCIM server.
|
||||||
|
*
|
||||||
|
* @param resource the resource creation to propagate (e.g. a UserModel)
|
||||||
|
*/
|
||||||
|
void replace(M resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Propagates the deletion of an element to a SCIM server.
|
||||||
|
*
|
||||||
|
* @param id the deleted resource's id to propagate (e.g. id of a UserModel)
|
||||||
|
*/
|
||||||
|
void delete(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes resources between SCIM server and keycloack, according to configuration.
|
||||||
|
*
|
||||||
|
* @param result the synchronization result to update for indicating triggered operations (e.g. user deletions)
|
||||||
|
*/
|
||||||
|
void sync(SynchronizationResult result);
|
||||||
|
}
|
|
@ -6,13 +6,13 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import sh.libre.scim.storage.ScimStorageProviderFactory;
|
import sh.libre.scim.storage.ScimStorageProviderFactory;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In charge of sending SCIM Request to all registered SCIM servers.
|
||||||
|
*/
|
||||||
public class ScimDispatcher {
|
public class ScimDispatcher {
|
||||||
|
|
||||||
public static final String SCOPE_USER = "user";
|
|
||||||
|
|
||||||
public static final String SCOPE_GROUP = "group";
|
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(ScimDispatcher.class);
|
private static final Logger LOGGER = Logger.getLogger(ScimDispatcher.class);
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
|
@ -21,22 +21,45 @@ public class ScimDispatcher {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void run(String scope, Consumer<ScimClient> f) {
|
public void dispatchUserModificationToAll(Consumer<UserScimClient> operationToDispatch) {
|
||||||
session.getContext().getRealm().getComponentsStream()
|
getAllSCIMServer(Scope.USER).forEach(scimServer -> dispatchUserModificationToOne(scimServer, operationToDispatch));
|
||||||
.filter((m) -> {
|
|
||||||
return ScimStorageProviderFactory.ID.equals(m.getProviderId())
|
|
||||||
&& m.get("enabled", true)
|
|
||||||
&& m.get("propagation-" + scope, false);
|
|
||||||
})
|
|
||||||
.forEach(m -> runOne(m, f));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runOne(ComponentModel m, Consumer<ScimClient> f) {
|
public void dispatchUserModificationToOne(ComponentModel scimServer, Consumer<UserScimClient> operationToDispatch) {
|
||||||
LOGGER.infof("%s %s %s %s", m.getId(), m.getName(), m.getProviderId(), m.getProviderType());
|
LOGGER.infof("%s %s %s %s", scimServer.getId(), scimServer.getName(), scimServer.getProviderId(), scimServer.getProviderType());
|
||||||
try (ScimClient client = ScimClient.newScimClient(m, session)) {
|
try (UserScimClient client = UserScimClient.newUserScimClient(scimServer, session)) {
|
||||||
f.accept(client);
|
operationToDispatch.accept(client);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error(e);
|
LOGGER.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void dispatchGroupModificationToAll(Consumer<GroupScimClient> operationToDispatch) {
|
||||||
|
getAllSCIMServer(Scope.GROUP).forEach(scimServer -> dispatchGroupModificationToOne(scimServer, operationToDispatch));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispatchGroupModificationToOne(ComponentModel scimServer, Consumer<GroupScimClient> operationToDispatch) {
|
||||||
|
LOGGER.infof("%s %s %s %s", scimServer.getId(), scimServer.getName(), scimServer.getProviderId(), scimServer.getProviderType());
|
||||||
|
try (GroupScimClient client = GroupScimClient.newGroupScimClient(scimServer, session)) {
|
||||||
|
operationToDispatch.accept(client);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param scope The {@link Scope} to consider (User or Group)
|
||||||
|
* @return all enabled registered SCIM Servers with propagation enabled for the given scope
|
||||||
|
*/
|
||||||
|
private Stream<ComponentModel> getAllSCIMServer(Scope scope) {
|
||||||
|
// TODO : we could initiative this list once and invalidate it when configuration changes
|
||||||
|
return session.getContext().getRealm().getComponentsStream()
|
||||||
|
.filter(m -> ScimStorageProviderFactory.ID.equals(m.getProviderId())
|
||||||
|
&& m.get("enabled", true)
|
||||||
|
&& m.get("propagation-" + scope.name(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Scope {
|
||||||
|
USER, GROUP
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class UserScimClient implements AutoCloseable {
|
public class UserScimClient implements ScimClientInterface<UserModel> {
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(UserScimClient.class);
|
private static final Logger LOGGER = Logger.getLogger(UserScimClient.class);
|
||||||
|
|
||||||
|
@ -86,6 +86,7 @@ public class UserScimClient implements AutoCloseable {
|
||||||
return new UserScimClient(scimRequestBuilder, retryRegistry, session, scimProviderConfiguration);
|
return new UserScimClient(scimRequestBuilder, retryRegistry, session, scimProviderConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void create(UserModel userModel) {
|
public void create(UserModel userModel) {
|
||||||
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
||||||
adapter.apply(userModel);
|
adapter.apply(userModel);
|
||||||
|
@ -116,6 +117,7 @@ public class UserScimClient implements AutoCloseable {
|
||||||
adapter.saveMapping();
|
adapter.saveMapping();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void replace(UserModel userModel) {
|
public void replace(UserModel userModel) {
|
||||||
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
||||||
try {
|
try {
|
||||||
|
@ -146,6 +148,7 @@ public class UserScimClient implements AutoCloseable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void delete(String id) {
|
public void delete(String id) {
|
||||||
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
UserAdapter adapter = new UserAdapter(this.keycloakSession, this.scimProviderConfiguration.getId());
|
||||||
adapter.setId(id);
|
adapter.setId(id);
|
||||||
|
@ -262,6 +265,7 @@ public class UserScimClient implements AutoCloseable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void sync(SynchronizationResult syncRes) {
|
public void sync(SynchronizationResult syncRes) {
|
||||||
if (this.scimProviderConfiguration.isSyncImport()) {
|
if (this.scimProviderConfiguration.isSyncImport()) {
|
||||||
this.importResources(syncRes);
|
this.importResources(syncRes);
|
||||||
|
|
|
@ -10,11 +10,8 @@ import org.keycloak.events.admin.ResourceType;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
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 java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
@ -50,14 +47,13 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case REGISTER -> {
|
case REGISTER -> {
|
||||||
UserModel user = getUser(eventUserId);
|
UserModel user = getUser(eventUserId);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.create(UserAdapter.class, user));
|
dispatcher.dispatchUserModificationToAll(client -> client.create(user));
|
||||||
}
|
}
|
||||||
case UPDATE_EMAIL, UPDATE_PROFILE -> {
|
case UPDATE_EMAIL, UPDATE_PROFILE -> {
|
||||||
UserModel user = getUser(eventUserId);
|
UserModel user = getUser(eventUserId);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
dispatcher.dispatchUserModificationToAll(client -> client.replace(user));
|
||||||
}
|
}
|
||||||
case DELETE_ACCOUNT ->
|
case DELETE_ACCOUNT -> dispatcher.dispatchUserModificationToAll(client -> client.delete(eventUserId));
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.delete(UserAdapter.class, eventUserId));
|
|
||||||
default -> LOGGER.trace("ignore event " + eventType);
|
default -> LOGGER.trace("ignore event " + eventType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,17 +73,16 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
switch (event.getOperationType()) {
|
switch (event.getOperationType()) {
|
||||||
case CREATE -> {
|
case CREATE -> {
|
||||||
UserModel user = getUser(userId);
|
UserModel user = getUser(userId);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.create(UserAdapter.class, user));
|
dispatcher.dispatchUserModificationToAll(client -> client.create(user));
|
||||||
user.getGroupsStream().forEach(group -> {
|
user.getGroupsStream().forEach(group -> {
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_GROUP, (client) -> client.replace(GroupAdapter.class, group));
|
dispatcher.dispatchGroupModificationToAll(client -> client.replace(group));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case UPDATE -> {
|
case UPDATE -> {
|
||||||
UserModel user = getUser(userId);
|
UserModel user = getUser(userId);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
dispatcher.dispatchUserModificationToAll(client -> client.replace(user));
|
||||||
}
|
}
|
||||||
case DELETE ->
|
case DELETE -> dispatcher.dispatchUserModificationToAll(client -> client.delete(userId));
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.delete(UserAdapter.class, userId));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case GROUP -> {
|
case GROUP -> {
|
||||||
|
@ -96,14 +91,13 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
switch (event.getOperationType()) {
|
switch (event.getOperationType()) {
|
||||||
case CREATE -> {
|
case CREATE -> {
|
||||||
GroupModel group = getGroup(groupId);
|
GroupModel group = getGroup(groupId);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_GROUP, (client) -> client.create(GroupAdapter.class, group));
|
dispatcher.dispatchGroupModificationToAll(client -> client.create(group));
|
||||||
}
|
}
|
||||||
case UPDATE -> {
|
case UPDATE -> {
|
||||||
GroupModel group = getGroup(groupId);
|
GroupModel group = getGroup(groupId);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_GROUP, (client) -> client.replace(GroupAdapter.class, group));
|
dispatcher.dispatchGroupModificationToAll(client -> client.replace(group));
|
||||||
}
|
}
|
||||||
case DELETE -> dispatcher.run(ScimDispatcher.SCOPE_GROUP,
|
case DELETE -> dispatcher.dispatchGroupModificationToAll(client -> client.delete(groupId));
|
||||||
(client) -> client.delete(GroupAdapter.class, groupId));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case GROUP_MEMBERSHIP -> {
|
case GROUP_MEMBERSHIP -> {
|
||||||
|
@ -113,7 +107,7 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
GroupModel group = getGroup(groupId);
|
GroupModel group = getGroup(groupId);
|
||||||
group.setSingleAttribute("scim-dirty", BooleanUtils.TRUE);
|
group.setSingleAttribute("scim-dirty", BooleanUtils.TRUE);
|
||||||
UserModel user = getUser(userId);
|
UserModel user = getUser(userId);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
dispatcher.dispatchUserModificationToAll(client -> client.replace(user));
|
||||||
}
|
}
|
||||||
case REALM_ROLE_MAPPING -> {
|
case REALM_ROLE_MAPPING -> {
|
||||||
String type = matcher.group(1);
|
String type = matcher.group(1);
|
||||||
|
@ -122,12 +116,12 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "users" -> {
|
case "users" -> {
|
||||||
UserModel user = getUser(id);
|
UserModel user = getUser(id);
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
dispatcher.dispatchUserModificationToAll(client -> client.replace(user));
|
||||||
}
|
}
|
||||||
case "groups" -> {
|
case "groups" -> {
|
||||||
GroupModel group = getGroup(id);
|
GroupModel group = getGroup(id);
|
||||||
session.users().getGroupMembersStream(session.getContext().getRealm(), group).forEach(user -> {
|
session.users().getGroupMembersStream(session.getContext().getRealm(), group).forEach(user -> {
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
|
dispatcher.dispatchUserModificationToAll(client -> client.replace(user));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,10 +137,10 @@ public class ScimStorageProviderFactory
|
||||||
session.getContext().setRealm(realm);
|
session.getContext().setRealm(realm);
|
||||||
ScimDispatcher dispatcher = new ScimDispatcher(session);
|
ScimDispatcher dispatcher = new ScimDispatcher(session);
|
||||||
if (BooleanUtils.TRUE.equals(model.get("propagation-user"))) {
|
if (BooleanUtils.TRUE.equals(model.get("propagation-user"))) {
|
||||||
dispatcher.runOne(model, (client) -> client.sync(UserAdapter.class, result));
|
dispatcher.dispatchUserModificationToOne(model, client -> client.sync(result));
|
||||||
}
|
}
|
||||||
if (BooleanUtils.TRUE.equals(model.get("propagation-group"))) {
|
if (BooleanUtils.TRUE.equals(model.get("propagation-group"))) {
|
||||||
dispatcher.runOne(model, (client) -> client.sync(GroupAdapter.class, result));
|
dispatcher.dispatchGroupModificationToOne(model, client -> client.sync(result));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,8 +169,7 @@ public class ScimStorageProviderFactory
|
||||||
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.debug(group.getName() + " is dirty");
|
||||||
dispatcher.run(ScimDispatcher.SCOPE_GROUP,
|
dispatcher.dispatchGroupModificationToAll(client -> client.replace(group));
|
||||||
(client) -> client.replace(GroupAdapter.class, group));
|
|
||||||
group.removeAttribute("scim-dirty");
|
group.removeAttribute("scim-dirty");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue