feat: update to keycloak 23 + varrious fixes

This commit is contained in:
Hugo Renard 2024-02-06 12:13:30 +01:00
parent 7163d2a0f0
commit 182824a3b4
Signed by: hougo
GPG key ID: 3A285FD470209C59
12 changed files with 153 additions and 151 deletions

View file

@ -1,13 +1,13 @@
plugins { plugins {
id 'java' id 'java'
id 'com.github.johnrengelman.shadow' version '7.1.2' id 'com.github.johnrengelman.shadow' version '8.1.1'
} }
group = 'sh.libre.scim' group = 'sh.libre.scim'
version = '1.0-SNAPSHOT' version = '1.0-SNAPSHOT'
description = 'keycloak-scim' description = 'keycloak-scim'
java.sourceCompatibility = JavaVersion.VERSION_11 java.sourceCompatibility = JavaVersion.VERSION_17
repositories { repositories {
mavenLocal() mavenLocal()
@ -15,20 +15,12 @@ repositories {
} }
dependencies { dependencies {
compileOnly 'org.keycloak:keycloak-core:18.0.0' compileOnly 'org.keycloak:keycloak-core:23.0.4'
compileOnly 'org.keycloak:keycloak-server-spi:18.0.0' compileOnly 'org.keycloak:keycloak-server-spi:23.0.4'
compileOnly 'org.keycloak:keycloak-server-spi-private:18.0.0' compileOnly 'org.keycloak:keycloak-server-spi-private:23.0.4'
compileOnly 'org.keycloak:keycloak-services:18.0.0' compileOnly 'org.keycloak:keycloak-services:23.0.4'
compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0' compileOnly 'org.keycloak:keycloak-model-jpa:23.0.4'
implementation 'io.github.resilience4j:resilience4j-retry:1.7.1' implementation 'io.github.resilience4j:resilience4j-retry:2.2.0'
implementation 'de.captaingoldfish:scim-sdk-common:1.15.3' implementation 'de.captaingoldfish:scim-sdk-common:1.21.1'
implementation 'de.captaingoldfish:scim-sdk-client:1.21.1'
implementation files('/home/marcportabella/Documents/totmicro/Repos/keycloak-scim-aws/scim-sdk-client-1.15.4-SNAPSHOT.jar')
//implementation 'de.captaingoldfish:scim-sdk-client:1.15.3'
implementation('org.wildfly.client:wildfly-client-config:1.0.1.Final') {
transitive false
}
implementation('org.jboss.resteasy:resteasy-client:4.7.6.Final') {
transitive false
}
} }

View file

@ -11,7 +11,7 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
keycloak: keycloak:
image: quay.io/keycloak/keycloak:18.0.0 image: quay.io/keycloak/keycloak:23.0.3
build: . build: .
command: start-dev command: start-dev
volumes: volumes:
@ -23,6 +23,7 @@ services:
KC_DB_PASSWORD: keycloak KC_DB_PASSWORD: keycloak
KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin KEYCLOAK_ADMIN_PASSWORD: admin
KC_LOG_LEVEL: INFO,sh.libre.scim:debug,de.captaingoldfish.scim:debug
ports: ports:
- 127.0.0.1:8080:8080 - 127.0.0.1:8080:8080
depends_on: depends_on:

View file

@ -1,33 +0,0 @@
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '7.1.2'
}
group = 'sh.libre.scim'
version = '1.0-SNAPSHOT'
description = 'keycloak-scim'
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
compileOnly 'org.keycloak:keycloak-core:18.0.0'
compileOnly 'org.keycloak:keycloak-server-spi:18.0.0'
compileOnly 'org.keycloak:keycloak-server-spi-private:18.0.0'
compileOnly 'org.keycloak:keycloak-services:18.0.0'
compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0'
implementation 'io.github.resilience4j:resilience4j-retry:1.7.1'
compileOnly 'org.wildfly.client:wildfly-client-config:1.0.1.Final'
compileOnly 'org.jboss.resteasy:resteasy-client:4.7.6.Final'
compileOnly 'org.jboss.resteasy:resteasy-client-api:4.7.6.Final'
}
shadowJar {
archiveClassifier.set('all-legacy')
}

View file

@ -2,10 +2,9 @@ package sh.libre.scim.core;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.persistence.EntityManager; import jakarta.persistence.EntityManager;
import javax.persistence.NoResultException; import jakarta.persistence.NoResultException;
import javax.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import javax.ws.rs.NotFoundException;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionProvider;
@ -61,7 +60,7 @@ public abstract class Adapter<M extends RoleMapperModel, S extends ResourceNode>
} }
public String getSCIMEndpoint() { public String getSCIMEndpoint() {
return type + "s"; return "/" + type + "s";
} }
public ScimResource toMapping() { public ScimResource toMapping() {
@ -95,12 +94,8 @@ public abstract class Adapter<M extends RoleMapperModel, S extends ResourceNode>
if (this.externalId != null) { if (this.externalId != null) {
return this.query("findByExternalId", externalId).getSingleResult(); return this.query("findByExternalId", externalId).getSingleResult();
} }
} catch (NotFoundException e) {
} catch (NoResultException e) { } catch (NoResultException e) {
} catch (Exception e) {
LOGGER.error(e);
} }
return null; return null;
} }
@ -124,7 +119,7 @@ public abstract class Adapter<M extends RoleMapperModel, S extends ResourceNode>
public abstract Class<S> getResourceClass(); public abstract Class<S> getResourceClass();
public abstract S toSCIM(Boolean addMeta); public abstract S toSCIM();
public abstract Boolean entityExists(); public abstract Boolean entityExists();

View file

@ -2,16 +2,15 @@ package sh.libre.scim.core;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.persistence.NoResultException; import jakarta.persistence.NoResultException;
import de.captaingoldfish.scim.sdk.common.resources.Group; import de.captaingoldfish.scim.sdk.common.resources.Group;
import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Member; import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Member;
import de.captaingoldfish.scim.sdk.common.resources.complex.Meta; import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -59,44 +58,49 @@ public class GroupAdapter extends Adapter<GroupModel, Group> {
if (groupMembers != null && groupMembers.size() > 0) { if (groupMembers != null && groupMembers.size() > 0) {
this.members = new HashSet<String>(); this.members = new HashSet<String>();
for (var groupMember : groupMembers) { for (var groupMember : groupMembers) {
var userMapping = this.query("findByExternalId", groupMember.getValue().get(), "User") try {
.getSingleResult(); var userMapping = this.query("findByExternalId", groupMember.getValue().get(), "User")
this.members.add(userMapping.getId()); .getSingleResult();
this.members.add(userMapping.getId());
} catch (NoResultException e) {
LOGGER.warnf("member %s not found for scim group %s", groupMember.getValue().get(), group.getId().get());
}
} }
} }
} }
@Override @Override
public Group toSCIM(Boolean addMeta) { public Group toSCIM() {
var group = new Group(); var group = new Group();
group.setId(externalId); group.setId(externalId);
group.setExternalId(id); group.setExternalId(id);
group.setDisplayName(displayName); group.setDisplayName(displayName);
if (members.size() > 0) { if (members.size() > 0) {
var groupMembers = new ArrayList<Member>();
for (var member : members) { for (var member : members) {
var groupMember = new Member(); var groupMember = new Member();
try { try {
var userMapping = this.query("findById", member, "User").getSingleResult(); var userMapping = this.query("findById", member, "User").getSingleResult();
LOGGER.debug(userMapping.getExternalId());
LOGGER.debug(userMapping.getId());
groupMember.setValue(userMapping.getExternalId()); groupMember.setValue(userMapping.getExternalId());
var ref = new URI(String.format("Users/%s", userMapping.getExternalId())); var ref = new URI(String.format("Users/%s", userMapping.getExternalId()));
groupMember.setRef(ref.toString()); groupMember.setRef(ref.toString());
groupMembers.add(groupMember); group.addMember(groupMember);
} catch (Exception e) { } catch (NoResultException e) {
LOGGER.error(e); LOGGER.warnf("member %s not found for group %s", member, id);
} catch (URISyntaxException e) {
LOGGER.warnf("bad ref uri");
} }
} }
group.setMembers(groupMembers);
} }
if (addMeta) { var meta = new Meta();
var meta = new Meta(); try {
try { var uri = new URI("Groups/" + externalId);
var uri = new URI("Groups/" + externalId); meta.setLocation(uri.toString());
meta.setLocation(uri.toString()); } catch (URISyntaxException e) {
} catch (URISyntaxException e) { LOGGER.warn(e);
}
group.setMeta(meta);
} }
group.setMeta(meta);
return group; return group;
} }
@ -114,7 +118,9 @@ public class GroupAdapter extends Adapter<GroupModel, Group> {
@Override @Override
public Boolean tryToMap() { public Boolean tryToMap() {
var group = session.groups().getGroupsStream(realm).filter(x -> x.getName() == displayName).findFirst(); var group = session.groups().getGroupsStream(realm).filter(
x -> StringUtils.equals(x.getName(), externalId) || StringUtils.equals(x.getName(), displayName))
.findFirst();
if (group.isPresent()) { if (group.isPresent()) {
setId(group.get().getId()); setId(group.get().getId());
return true; return true;

View file

@ -3,9 +3,9 @@ package sh.libre.scim.core;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.persistence.EntityManager; import jakarta.persistence.EntityManager;
import javax.persistence.NoResultException; import jakarta.persistence.NoResultException;
import javax.ws.rs.ProcessingException; import jakarta.ws.rs.ProcessingException;
import de.captaingoldfish.scim.sdk.client.ScimClientConfig; import de.captaingoldfish.scim.sdk.client.ScimClientConfig;
import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder; import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder;
@ -51,12 +51,11 @@ public class ScimClient {
switch (model.get("auth-mode")) { switch (model.get("auth-mode")) {
case "BEARER": case "BEARER":
defaultHeaders.put(HttpHeaders.AUTHORIZATION, defaultHeaders.put(HttpHeaders.AUTHORIZATION,
BearerAuthentication(model.get("auth-pass"))); BearerAuthentication());
break; break;
case "BASIC_AUTH": case "BASIC_AUTH":
defaultHeaders.put(HttpHeaders.AUTHORIZATION, defaultHeaders.put(HttpHeaders.AUTHORIZATION,
BasicAuthentication(model.get("auth-user"), BasicAuthentication());
model.get("auth-pass")));
break; break;
} }
@ -73,10 +72,10 @@ public class ScimClient {
registry = RetryRegistry.of(retryConfig); registry = RetryRegistry.of(retryConfig);
} }
protected String BasicAuthentication(String username ,String password) { protected String BasicAuthentication() {
return BasicAuth.builder() return BasicAuth.builder()
.username(model.get(username)) .username(model.get("auth-user"))
.password(model.get(password)) .password(model.get("auth-pass"))
.build() .build()
.getAuthorizationHeaderValue(); .getAuthorizationHeaderValue();
} }
@ -92,17 +91,10 @@ public class ScimClient {
.build(); .build();
} }
protected String BearerAuthentication(String token) { protected String BearerAuthentication() {
return "Bearer " + token ; return "Bearer " + model.get("auth-pass") ;
} }
protected String genScimUrl(String scimEndpoint,String resourcePath) {
return String.format("%s%s/%s", scimApplicationBaseUrl ,
scimEndpoint,
resourcePath);
}
protected EntityManager getEM() { protected EntityManager getEM() {
return session.getProvider(JpaConnectionProvider.class).getEntityManager(); return session.getProvider(JpaConnectionProvider.class).getEntityManager();
} }
@ -136,8 +128,8 @@ public class ScimClient {
ServerResponse<S> response = retry.executeSupplier(() -> { ServerResponse<S> response = retry.executeSupplier(() -> {
try { try {
return scimRequestBuilder return scimRequestBuilder
.create(adapter.getResourceClass(), String.format("/" + adapter.getSCIMEndpoint())) .create(adapter.getResourceClass(), adapter.getSCIMEndpoint())
.setResource(adapter.toSCIM(false)) .setResource(adapter.toSCIM())
.sendRequest(); .sendRequest();
} catch ( ResponseException e) { } catch ( ResponseException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -166,9 +158,8 @@ public class ScimClient {
ServerResponse<S> response = retry.executeSupplier(() -> { ServerResponse<S> response = retry.executeSupplier(() -> {
try { try {
return scimRequestBuilder return scimRequestBuilder
.update(genScimUrl(adapter.getSCIMEndpoint(), adapter.getExternalId()), .update(adapter.getResourceClass(), adapter.getSCIMEndpoint(), adapter.getExternalId())
adapter.getResourceClass()) .setResource(adapter.toSCIM())
.setResource(adapter.toSCIM(false))
.sendRequest() ; .sendRequest() ;
} catch ( ResponseException e) { } catch ( ResponseException e) {
@ -199,8 +190,7 @@ public class ScimClient {
ServerResponse<S> response = retry.executeSupplier(() -> { ServerResponse<S> response = retry.executeSupplier(() -> {
try { try {
return scimRequestBuilder.delete(genScimUrl(adapter.getSCIMEndpoint(), adapter.getExternalId()), return scimRequestBuilder.delete(adapter.getResourceClass(), adapter.getSCIMEndpoint(), adapter.getExternalId())
adapter.getResourceClass())
.sendRequest(); .sendRequest();
} catch (ResponseException e) { } catch (ResponseException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -247,7 +237,7 @@ public class ScimClient {
LOGGER.info("Import"); LOGGER.info("Import");
try { try {
var adapter = getAdapter(aClass); var adapter = getAdapter(aClass);
ServerResponse<ListResponse<S>> response = scimRequestBuilder.list("url",adapter.getResourceClass()).get().sendRequest(); ServerResponse<ListResponse<S>> response = scimRequestBuilder.list(adapter.getResourceClass(), adapter.getSCIMEndpoint()).get().sendRequest();
ListResponse<S> resourceTypeListResponse = response.getResource(); ListResponse<S> resourceTypeListResponse = response.getResource();
for (var resource : resourceTypeListResponse.getListedResources()) { for (var resource : resourceTypeListResponse.getListedResources()) {
@ -287,9 +277,7 @@ public class ScimClient {
case "DELETE_REMOTE": case "DELETE_REMOTE":
LOGGER.info("Delete remote resource"); LOGGER.info("Delete remote resource");
scimRequestBuilder scimRequestBuilder
.delete(genScimUrl(adapter.getSCIMEndpoint(), .delete(adapter.getResourceClass(), adapter.getSCIMEndpoint(), resource.getId().get())
resource.getId().get()),
adapter.getResourceClass())
.sendRequest(); .sendRequest();
syncRes.increaseRemoved(); syncRes.increaseRemoved();
break; break;

View file

@ -1,6 +1,8 @@
package sh.libre.scim.core; package sh.libre.scim.core;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.apache.commons.lang3.StringUtils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -21,7 +23,8 @@ public class ScimDispatcher {
public void run(String scope, Consumer<ScimClient> f) { public void run(String scope, Consumer<ScimClient> f) {
session.getContext().getRealm().getComponentsStream() session.getContext().getRealm().getComponentsStream()
.filter((m) -> { .filter((m) -> {
return ScimStorageProviderFactory.ID.equals(m.getProviderId()) && m.get("enabled", true) return StringUtils.equals(ScimStorageProviderFactory.ID, m.getProviderId())
&& m.get("enabled", true)
&& m.get("propagation-" + scope, false); && m.get("propagation-" + scope, false);
}) })
.forEach(m -> runOne(m, f)); .forEach(m -> runOne(m, f));

View file

@ -3,8 +3,10 @@ package sh.libre.scim.core;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
import de.captaingoldfish.scim.sdk.common.resources.User; import de.captaingoldfish.scim.sdk.common.resources.User;
@ -13,8 +15,7 @@ import de.captaingoldfish.scim.sdk.common.resources.complex.Name;
import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole; import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole;
import de.captaingoldfish.scim.sdk.common.resources.complex.Meta; import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang.StringUtils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -26,6 +27,8 @@ public class UserAdapter extends Adapter<UserModel, User> {
private String email; private String email;
private Boolean active; private Boolean active;
private String[] roles; private String[] roles;
private String firstName;
private String lastName;
public UserAdapter(KeycloakSession session, String componentId) { public UserAdapter(KeycloakSession session, String componentId) {
super(session, componentId, "User", Logger.getLogger(UserAdapter.class)); super(session, componentId, "User", Logger.getLogger(UserAdapter.class));
@ -79,6 +82,22 @@ public class UserAdapter extends Adapter<UserModel, User> {
this.roles = roles; this.roles = roles;
} }
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override @Override
public Class<User> getResourceClass() { public Class<User> getResourceClass() {
return User.class; return User.class;
@ -96,19 +115,17 @@ public class UserAdapter extends Adapter<UserModel, User> {
setDisplayName(displayName); setDisplayName(displayName);
setEmail(user.getEmail()); setEmail(user.getEmail());
setActive(user.isEnabled()); setActive(user.isEnabled());
setFirstName(user.getFirstName());
setLastName(user.getLastName());
var rolesSet = new HashSet<String>(); var rolesSet = new HashSet<String>();
user.getGroupsStream().flatMap(g -> g.getRoleMappingsStream()) user.getGroupsStream().flatMap(g -> g.getRoleMappingsStream())
.filter((r) -> r.getFirstAttribute("scim").equals("true")).map((r) -> r.getName()) .filter((r) -> StringUtils.equals(r.getFirstAttribute("scim"), "true"))
.map((r) -> r.getName())
.forEach(r -> rolesSet.add(r));
user.getRoleMappingsStream()
.filter((r) -> StringUtils.equals(r.getFirstAttribute("scim"), "true"))
.map((r) -> r.getName())
.forEach(r -> rolesSet.add(r)); .forEach(r -> rolesSet.add(r));
user.getRoleMappingsStream().filter((r) -> {
var attr = r.getFirstAttribute("scim");
if (attr == null) {
return false;
}
return attr.equals("true");
}).map((r) -> r.getName()).forEach(r -> rolesSet.add(r));
var roles = new String[rolesSet.size()]; var roles = new String[rolesSet.size()];
rolesSet.toArray(roles); rolesSet.toArray(roles);
setRoles(roles); setRoles(roles);
@ -127,30 +144,31 @@ public class UserAdapter extends Adapter<UserModel, User> {
} }
@Override @Override
public User toSCIM(Boolean addMeta) { public User toSCIM() {
var user = new User(); var user = new User();
user.setExternalId(id); user.setExternalId(id);
user.setUserName(username); user.setUserName(username);
user.setId(externalId); user.setId(externalId);
user.setDisplayName(displayName); user.setDisplayName(displayName);
Name name = new Name(); Name name = new Name();
name.setFamilyName(lastName);
name.setGivenName(firstName);
user.setName(name); user.setName(name);
var emails = new ArrayList<Email>(); var emails = new ArrayList<Email>();
if (email != null) { if (email != null) {
emails.add( emails.add(
Email.builder().value(getEmail()).build()); Email.builder().value(getEmail()).build());
} }
user.setEmails(emails); user.setEmails(emails);
user.setActive(active); user.setActive(active);
if (addMeta) { var meta = new Meta();
var meta = new Meta(); try {
try { var uri = new URI("Users/" + externalId);
var uri = new URI("Users/" + externalId); meta.setLocation(uri.toString());
meta.setLocation(uri.toString()); } catch (URISyntaxException e) {
} catch (URISyntaxException e) { LOGGER.warn(e);
}
user.setMeta(meta);
} }
user.setMeta(meta);
List<PersonRole> roles = new ArrayList<PersonRole>(); List<PersonRole> roles = new ArrayList<PersonRole>();
for (var r : this.roles) { for (var r : this.roles) {
var role = new PersonRole(); var role = new PersonRole();
@ -212,11 +230,13 @@ public class UserAdapter extends Adapter<UserModel, User> {
@Override @Override
public Stream<UserModel> getResourceStream() { public Stream<UserModel> getResourceStream() {
return this.session.users().getUsersStream(this.session.getContext().getRealm()); Map<String, String> params = new HashMap<String, String>() {
};
return this.session.users().searchForUserStream(realm, params);
} }
@Override @Override
public Boolean skipRefresh() { public Boolean skipRefresh() {
return getUsername().equals("admin"); return StringUtils.equals(getUsername(), "admin");
} }
} }

View file

@ -1,8 +1,9 @@
package sh.libre.scim.event; package sh.libre.scim.event;
import java.util.HashMap; import java.util.HashMap;
import java.util.regex.*; import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.events.Event; import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventListenerProvider;
@ -99,7 +100,7 @@ public class ScimEventListenerProvider implements EventListenerProvider {
var groupId = matcher.group(2); var groupId = matcher.group(2);
LOGGER.infof("%s %s from %s", event.getOperationType(), userId, groupId); LOGGER.infof("%s %s from %s", event.getOperationType(), userId, groupId);
var group = getGroup(groupId); var group = getGroup(groupId);
dispatcher.run(ScimDispatcher.SCOPE_GROUP, (client) -> client.replace(GroupAdapter.class, group)); group.setSingleAttribute("scim-dirty", "true");
var user = getUser(userId); var user = getUser(userId);
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user)); dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
} }
@ -107,10 +108,10 @@ public class ScimEventListenerProvider implements EventListenerProvider {
var type = matcher.group(1); var type = matcher.group(1);
var id = matcher.group(2); var id = matcher.group(2);
LOGGER.infof("%s %s %s roles", event.getOperationType(), type, id); LOGGER.infof("%s %s %s roles", event.getOperationType(), type, id);
if (type.equals("users")) { if (StringUtils.equals(type, "users")) {
var user = getUser(id); var user = getUser(id);
dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user)); dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));
} else if (type.equals("groups")) { } else if (StringUtils.equals(type, "groups")) {
var group = getGroup(id); var 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.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user));

View file

@ -1,12 +1,12 @@
package sh.libre.scim.jpa; package sh.libre.scim.jpa;
import javax.persistence.Column; import jakarta.persistence.Column;
import javax.persistence.Entity; import jakarta.persistence.Entity;
import javax.persistence.Id; import jakarta.persistence.Id;
import javax.persistence.IdClass; import jakarta.persistence.IdClass;
import javax.persistence.NamedQuery; import jakarta.persistence.NamedQuery;
import javax.persistence.NamedQueries; import jakarta.persistence.NamedQueries;
import javax.persistence.Table; import jakarta.persistence.Table;
@Entity @Entity
@IdClass(ScimResourceId.class) @IdClass(ScimResourceId.class)

View file

@ -68,6 +68,7 @@ public class ScimResourceId implements Serializable {
if (!(other instanceof ScimResourceId)) if (!(other instanceof ScimResourceId))
return false; return false;
var o = (ScimResourceId) other; var o = (ScimResourceId) other;
// TODO
return (o.id == id && return (o.id == id &&
o.realmId == realmId && o.realmId == realmId &&
o.componentId == componentId && o.componentId == componentId &&

View file

@ -3,8 +3,9 @@ package sh.libre.scim.storage;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import javax.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import org.apache.commons.lang3.StringUtils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -17,6 +18,7 @@ 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 sh.libre.scim.core.GroupAdapter; import sh.libre.scim.core.GroupAdapter;
import sh.libre.scim.core.ScimDispatcher; import sh.libre.scim.core.ScimDispatcher;
@ -34,6 +36,7 @@ public class ScimStorageProviderFactory
.property() .property()
.name("endpoint") .name("endpoint")
.type(ProviderConfigProperty.STRING_TYPE) .type(ProviderConfigProperty.STRING_TYPE)
.required(true)
.label("SCIM 2.0 endpoint") .label("SCIM 2.0 endpoint")
.helpText("External SCIM 2.0 base " + .helpText("External SCIM 2.0 base " +
"URL (/ServiceProviderConfig /Schemas and /ResourcesTypes should be accessible)") "URL (/ServiceProviderConfig /Schemas and /ResourcesTypes should be accessible)")
@ -70,12 +73,14 @@ public class ScimStorageProviderFactory
.name("propagation-user") .name("propagation-user")
.type(ProviderConfigProperty.BOOLEAN_TYPE) .type(ProviderConfigProperty.BOOLEAN_TYPE)
.label("Enable user propagation") .label("Enable user propagation")
.helpText("Should operation on users be propagated to this provider ?")
.defaultValue("true") .defaultValue("true")
.add() .add()
.property() .property()
.name("propagation-group") .name("propagation-group")
.type(ProviderConfigProperty.BOOLEAN_TYPE) .type(ProviderConfigProperty.BOOLEAN_TYPE)
.label("Enable group propagation") .label("Enable group propagation")
.helpText("Should operation on groups be propagated to this provider ?")
.defaultValue("true") .defaultValue("true")
.add() .add()
.property() .property()
@ -127,10 +132,10 @@ public class ScimStorageProviderFactory
var realm = session.realms().getRealm(realmId); var realm = session.realms().getRealm(realmId);
session.getContext().setRealm(realm); session.getContext().setRealm(realm);
var dispatcher = new ScimDispatcher(session); var dispatcher = new ScimDispatcher(session);
if (model.get("propagation-user").equals("true")) { if (StringUtils.equals(model.get("propagation-user"), "true")) {
dispatcher.runOne(model, (client) -> client.sync(UserAdapter.class, result)); dispatcher.runOne(model, (client) -> client.sync(UserAdapter.class, result));
} }
if (model.get("propagation-group").equals("true")) { if (StringUtils.equals(model.get("propagation-group"), "true")) {
dispatcher.runOne(model, (client) -> client.sync(GroupAdapter.class, result)); dispatcher.runOne(model, (client) -> client.sync(GroupAdapter.class, result));
} }
} }
@ -147,4 +152,27 @@ public class ScimStorageProviderFactory
return this.sync(sessionFactory, realmId, model); return this.sync(sessionFactory, realmId, model);
} }
@Override
public void postInit(KeycloakSessionFactory factory) {
var timer = factory.create().getProvider(TimerProvider.class);
timer.scheduleTask(taskSession -> {
for (var realm : taskSession.realms().getRealmsStream().toList()) {
KeycloakModelUtils.runJobInTransaction(factory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
session.getContext().setRealm(realm);
var dispatcher = new ScimDispatcher(session);
for (var group : session.groups().getGroupsStream(realm)
.filter(x -> StringUtils.equals(x.getFirstAttribute("scim-dirty"), "true")).toList()) {
LOGGER.debug(group.getName() + " is dirty");
dispatcher.run(ScimDispatcher.SCOPE_GROUP,
(client) -> client.replace(GroupAdapter.class, group));
group.removeAttribute("scim-dirty");
}
}
});
}
}, 30000, "scim-background");
}
} }