add group + group sync support
This commit is contained in:
parent
f22e23742d
commit
50029c254e
6 changed files with 309 additions and 56 deletions
|
@ -1,10 +1,14 @@
|
||||||
package sh.libre.scim.core;
|
package sh.libre.scim.core;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.TypedQuery;
|
import javax.persistence.TypedQuery;
|
||||||
import javax.ws.rs.NotFoundException;
|
import javax.ws.rs.NotFoundException;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RoleMapperModel;
|
import org.keycloak.models.RoleMapperModel;
|
||||||
|
|
||||||
import sh.libre.scim.jpa.ScimResource;
|
import sh.libre.scim.jpa.ScimResource;
|
||||||
|
@ -16,14 +20,16 @@ public abstract class Adapter<M extends RoleMapperModel, S extends com.unboundid
|
||||||
protected final String type;
|
protected final String type;
|
||||||
protected final String componentId;
|
protected final String componentId;
|
||||||
protected final EntityManager em;
|
protected final EntityManager em;
|
||||||
|
protected final KeycloakSession session;
|
||||||
|
|
||||||
protected String id;
|
protected String id;
|
||||||
protected String externalId;
|
protected String externalId;
|
||||||
|
|
||||||
public Adapter(String realmId, String componentId, EntityManager em, String type, Logger logger) {
|
public Adapter(KeycloakSession session, String componentId, String type, Logger logger) {
|
||||||
this.realmId = realmId;
|
this.session = session;
|
||||||
|
this.realmId = session.getContext().getRealm().getId();
|
||||||
this.componentId = componentId;
|
this.componentId = componentId;
|
||||||
this.em = em;
|
this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.LOGGER = logger;
|
this.LOGGER = logger;
|
||||||
}
|
}
|
||||||
|
@ -63,6 +69,10 @@ public abstract class Adapter<M extends RoleMapperModel, S extends com.unboundid
|
||||||
}
|
}
|
||||||
|
|
||||||
public TypedQuery<ScimResource> query(String query, String id) {
|
public TypedQuery<ScimResource> query(String query, String id) {
|
||||||
|
return query(query, id, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TypedQuery<ScimResource> query(String query, String id, String type) {
|
||||||
return this.em
|
return this.em
|
||||||
.createNamedQuery(query, ScimResource.class)
|
.createNamedQuery(query, ScimResource.class)
|
||||||
.setParameter("type", type)
|
.setParameter("type", type)
|
||||||
|
@ -92,14 +102,20 @@ public abstract class Adapter<M extends RoleMapperModel, S extends com.unboundid
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteMapping() {
|
public void deleteMapping() {
|
||||||
this.em.remove(this.toMapping());
|
var mapping = this.em.merge(toMapping());
|
||||||
|
this.em.remove(mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void apply(ScimResource mapping) {
|
||||||
|
setId(mapping.getId());
|
||||||
|
setExternalId(mapping.getExternalId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void apply(M model);
|
public abstract void apply(M model);
|
||||||
|
|
||||||
public abstract void apply(S resource);
|
public abstract void apply(S resource);
|
||||||
|
|
||||||
public abstract void apply(ScimResource resource);
|
public abstract Class<S> getResourceClass();
|
||||||
|
|
||||||
public abstract S toSCIM(Boolean addMeta);
|
public abstract S toSCIM(Boolean addMeta);
|
||||||
|
|
||||||
|
@ -107,4 +123,9 @@ public abstract class Adapter<M extends RoleMapperModel, S extends com.unboundid
|
||||||
|
|
||||||
public abstract Boolean tryToMap();
|
public abstract Boolean tryToMap();
|
||||||
|
|
||||||
|
public abstract void createEntity();
|
||||||
|
|
||||||
|
public abstract Stream<M> getResourceStream();
|
||||||
|
|
||||||
|
public abstract Boolean skipRefresh();
|
||||||
}
|
}
|
||||||
|
|
184
src/main/java/sh/libre/scim/core/GroupAdapter.java
Normal file
184
src/main/java/sh/libre/scim/core/GroupAdapter.java
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
package sh.libre.scim.core;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import javax.persistence.NoResultException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.unboundid.scim2.common.types.GroupResource;
|
||||||
|
import com.unboundid.scim2.common.types.Member;
|
||||||
|
import com.unboundid.scim2.common.types.Meta;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.jpa.entities.GroupEntity;
|
||||||
|
import org.keycloak.models.jpa.entities.UserEntity;
|
||||||
|
import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
|
||||||
|
public class GroupAdapter extends Adapter<GroupModel, GroupResource> {
|
||||||
|
|
||||||
|
private String displayName;
|
||||||
|
private Set<String> members = new HashSet<String>();
|
||||||
|
|
||||||
|
public GroupAdapter(KeycloakSession session, String componentId) {
|
||||||
|
super(session, componentId, "Group", Logger.getLogger(GroupAdapter.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayName(String displayName) {
|
||||||
|
if (this.displayName == null) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<GroupResource> getResourceClass() {
|
||||||
|
return GroupResource.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(GroupModel group) {
|
||||||
|
setId(group.getId());
|
||||||
|
setDisplayName(group.getName());
|
||||||
|
this.members = session.users()
|
||||||
|
.getGroupMembersStream(session.getContext().getRealm(), group)
|
||||||
|
.map(x -> x.getId())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
ObjectMapper Obj = new ObjectMapper();
|
||||||
|
try {
|
||||||
|
String jsonStr = Obj.writerWithDefaultPrettyPrinter().writeValueAsString(this.members);
|
||||||
|
LOGGER.info(jsonStr);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(GroupResource group) {
|
||||||
|
setExternalId(group.getId());
|
||||||
|
setDisplayName(group.getDisplayName());
|
||||||
|
var groupMembers = group.getMembers();
|
||||||
|
if (groupMembers != null && groupMembers.size() > 0) {
|
||||||
|
this.members = new HashSet<String>();
|
||||||
|
for (var groupMember : groupMembers) {
|
||||||
|
var userMapping = this.query("findByExternalId", groupMember.getValue(), "User")
|
||||||
|
.getSingleResult();
|
||||||
|
this.members.add(userMapping.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GroupResource toSCIM(Boolean addMeta) {
|
||||||
|
var group = new GroupResource();
|
||||||
|
group.setId(externalId);
|
||||||
|
group.setExternalId(id);
|
||||||
|
group.setDisplayName(displayName);
|
||||||
|
if (members.size() > 0) {
|
||||||
|
var groupMembers = new ArrayList<Member>();
|
||||||
|
for (var member : members) {
|
||||||
|
var groupMember = new Member();
|
||||||
|
try {
|
||||||
|
var userMapping = this.query("findById", member, "User").getSingleResult();
|
||||||
|
groupMember.setValue(userMapping.getExternalId());
|
||||||
|
var ref = new URI(String.format("Users/%s", userMapping.getExternalId()));
|
||||||
|
groupMember.setRef(ref);
|
||||||
|
groupMembers.add(groupMember);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.setMembers(groupMembers);
|
||||||
|
}
|
||||||
|
if (addMeta) {
|
||||||
|
var meta = new Meta();
|
||||||
|
try {
|
||||||
|
var uri = new URI("Groups/" + externalId);
|
||||||
|
meta.setLocation(uri);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
}
|
||||||
|
group.setMeta(meta);
|
||||||
|
}
|
||||||
|
ObjectMapper Obj = new ObjectMapper();
|
||||||
|
try {
|
||||||
|
String jsonStr = Obj.writerWithDefaultPrettyPrinter().writeValueAsString(group);
|
||||||
|
LOGGER.info(jsonStr);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
}
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean entityExists() {
|
||||||
|
if (this.id == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var group = this.em.find(GroupEntity.class, this.id);
|
||||||
|
if (group != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean tryToMap() {
|
||||||
|
try {
|
||||||
|
var groupEntity = this.em
|
||||||
|
.createQuery("select g from GroupEntity g where g.name=:name",
|
||||||
|
GroupEntity.class)
|
||||||
|
.setParameter("name", displayName)
|
||||||
|
.getSingleResult();
|
||||||
|
setId(groupEntity.getId());
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createEntity() {
|
||||||
|
var kcGroup = new GroupEntity();
|
||||||
|
kcGroup.setId(KeycloakModelUtils.generateId());
|
||||||
|
kcGroup.setRealm(realmId);
|
||||||
|
kcGroup.setName(displayName);
|
||||||
|
kcGroup.setParentId(GroupEntity.TOP_PARENT_ID);
|
||||||
|
this.em.persist(kcGroup);
|
||||||
|
this.id = kcGroup.getId();
|
||||||
|
for (String mId : members) {
|
||||||
|
try {
|
||||||
|
var user = this.em.find(UserEntity.class, mId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new NoResultException();
|
||||||
|
}
|
||||||
|
var membership = new UserGroupMembershipEntity();
|
||||||
|
membership.setUser(user);
|
||||||
|
membership.setGroupId(kcGroup.getId());
|
||||||
|
this.em.persist(membership);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<GroupModel> getResourceStream() {
|
||||||
|
return this.session.groups().getGroupsStream(this.session.getContext().getRealm());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean skipRefresh() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import javax.ws.rs.client.Client;
|
||||||
import com.unboundid.scim2.client.ScimService;
|
import com.unboundid.scim2.client.ScimService;
|
||||||
import com.unboundid.scim2.common.ScimResource;
|
import com.unboundid.scim2.common.ScimResource;
|
||||||
import com.unboundid.scim2.common.exceptions.ScimException;
|
import com.unboundid.scim2.common.exceptions.ScimException;
|
||||||
import com.unboundid.scim2.common.types.UserResource;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
||||||
|
@ -17,7 +16,6 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RoleMapperModel;
|
import org.keycloak.models.RoleMapperModel;
|
||||||
import org.keycloak.storage.user.SynchronizationResult;
|
import org.keycloak.storage.user.SynchronizationResult;
|
||||||
|
|
||||||
import io.github.resilience4j.core.IntervalFunction;
|
|
||||||
import io.github.resilience4j.retry.RetryConfig;
|
import io.github.resilience4j.retry.RetryConfig;
|
||||||
import io.github.resilience4j.retry.RetryRegistry;
|
import io.github.resilience4j.retry.RetryRegistry;
|
||||||
|
|
||||||
|
@ -60,8 +58,8 @@ public class ScimClient {
|
||||||
protected <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> A getAdapter(
|
protected <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> A getAdapter(
|
||||||
Class<A> aClass) {
|
Class<A> aClass) {
|
||||||
try {
|
try {
|
||||||
return aClass.getDeclaredConstructor(String.class, String.class, EntityManager.class)
|
return aClass.getDeclaredConstructor(KeycloakSession.class, String.class)
|
||||||
.newInstance(getRealmId(), this.model.getId(), getEM());
|
.newInstance(session, this.model.getId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
@ -72,16 +70,18 @@ public class ScimClient {
|
||||||
var adapter = getAdapter(aClass);
|
var adapter = getAdapter(aClass);
|
||||||
adapter.apply(kcModel);
|
adapter.apply(kcModel);
|
||||||
var retry = registry.retry("create-" + adapter.getId());
|
var retry = registry.retry("create-" + adapter.getId());
|
||||||
var spUser = retry.executeSupplier(() -> {
|
var resource = retry.executeSupplier(() -> {
|
||||||
try {
|
try {
|
||||||
return scimService.createRequest(adapter.getSCIMEndpoint(), adapter.toSCIM(false))
|
return scimService.createRequest(adapter.getSCIMEndpoint(),
|
||||||
|
adapter.toSCIM(false))
|
||||||
.contentType(contentType).invoke();
|
.contentType(contentType).invoke();
|
||||||
} catch (ScimException e) {
|
} catch (ScimException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
adapter.apply(spUser);
|
adapter.apply(resource);
|
||||||
adapter.saveMapping();
|
adapter.saveMapping();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void replace(Class<A> aClass,
|
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void replace(Class<A> aClass,
|
||||||
|
@ -129,35 +129,41 @@ public class ScimClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshUsers(SynchronizationResult syncRes) {
|
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void refreshResources(
|
||||||
LOGGER.info("Refresh Users");
|
Class<A> aClass,
|
||||||
this.session.users().getUsersStream(this.session.getContext().getRealm()).forEach(kcUser -> {
|
SynchronizationResult syncRes) {
|
||||||
LOGGER.infof("Reconciling local user %s", kcUser.getId());
|
LOGGER.info("Refresh resources");
|
||||||
if (!kcUser.getUsername().equals("admin")) {
|
getAdapter(aClass).getResourceStream().forEach(resource -> {
|
||||||
var adapter = getAdapter(UserAdapter.class);
|
var adapter = getAdapter(aClass);
|
||||||
adapter.apply(kcUser);
|
adapter.apply(resource);
|
||||||
|
LOGGER.infof("Reconciling local resource %s", adapter.getId());
|
||||||
|
if (!adapter.skipRefresh()) {
|
||||||
var mapping = adapter.getMapping();
|
var mapping = adapter.getMapping();
|
||||||
if (mapping == null) {
|
if (mapping == null) {
|
||||||
LOGGER.info("Creating it");
|
LOGGER.info("Creating it");
|
||||||
this.create(UserAdapter.class, kcUser);
|
this.create(aClass, resource);
|
||||||
} else {
|
} else {
|
||||||
LOGGER.info("Replacing it");
|
LOGGER.info("Replacing it");
|
||||||
this.replace(UserAdapter.class, kcUser);
|
this.replace(aClass, resource);
|
||||||
}
|
}
|
||||||
syncRes.increaseUpdated();
|
syncRes.increaseUpdated();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void importUsers(SynchronizationResult syncRes) {
|
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void importResources(
|
||||||
LOGGER.info("Import Users");
|
Class<A> aClass, SynchronizationResult syncRes) {
|
||||||
|
LOGGER.info("Import");
|
||||||
try {
|
try {
|
||||||
var spUsers = scimService.searchRequest("Users").contentType(contentType).invoke(UserResource.class);
|
var adapter = getAdapter(aClass);
|
||||||
for (var spUser : spUsers) {
|
var resources = scimService.searchRequest(adapter.getSCIMEndpoint()).contentType(contentType)
|
||||||
|
.invoke(adapter.getResourceClass());
|
||||||
|
for (var resource : resources) {
|
||||||
try {
|
try {
|
||||||
LOGGER.infof("Reconciling remote user %s", spUser.getId());
|
LOGGER.infof("Reconciling remote resource %s", resource.getId());
|
||||||
var adapter = getAdapter(UserAdapter.class);
|
adapter = getAdapter(aClass);
|
||||||
adapter.apply(spUser);
|
adapter.apply(resource);
|
||||||
|
|
||||||
var mapping = adapter.getMapping();
|
var mapping = adapter.getMapping();
|
||||||
if (mapping != null) {
|
if (mapping != null) {
|
||||||
|
@ -173,25 +179,28 @@ public class ScimClient {
|
||||||
|
|
||||||
var mapped = adapter.tryToMap();
|
var mapped = adapter.tryToMap();
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
LOGGER.info("Matched a user");
|
LOGGER.info("Matched");
|
||||||
adapter.saveMapping();
|
adapter.saveMapping();
|
||||||
} else {
|
} else {
|
||||||
switch (this.model.get("sync-import-action")) {
|
switch (this.model.get("sync-import-action")) {
|
||||||
case "CREATE_LOCAL":
|
case "CREATE_LOCAL":
|
||||||
LOGGER.info("Create local user");
|
LOGGER.info("Create local resource");
|
||||||
adapter.createEntity();
|
adapter.createEntity();
|
||||||
adapter.saveMapping();
|
adapter.saveMapping();
|
||||||
syncRes.increaseAdded();
|
syncRes.increaseAdded();
|
||||||
break;
|
break;
|
||||||
case "DELETE_REMOTE":
|
case "DELETE_REMOTE":
|
||||||
LOGGER.info("Delete remote user");
|
LOGGER.info("Delete remote resource");
|
||||||
scimService.deleteRequest("Users", spUser.getId()).contentType(contentType)
|
scimService.deleteRequest(adapter.getSCIMEndpoint(), resource.getId())
|
||||||
|
.contentType(contentType)
|
||||||
.invoke();
|
.invoke();
|
||||||
syncRes.increaseRemoved();
|
syncRes.increaseRemoved();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e);
|
||||||
|
e.printStackTrace();
|
||||||
syncRes.increaseFailed();
|
syncRes.increaseFailed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,12 +209,13 @@ public class ScimClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sync(SynchronizationResult syncRes) {
|
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void sync(Class<A> aClass,
|
||||||
|
SynchronizationResult syncRes) {
|
||||||
if (this.model.get("sync-import", false)) {
|
if (this.model.get("sync-import", false)) {
|
||||||
this.importUsers(syncRes);
|
this.importResources(aClass, syncRes);
|
||||||
}
|
}
|
||||||
if (this.model.get("sync-refresh", false)) {
|
if (this.model.get("sync-refresh", false)) {
|
||||||
this.refreshUsers(syncRes);
|
this.refreshResources(aClass, syncRes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,20 +3,18 @@ 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.stream.Stream;
|
||||||
import javax.persistence.EntityManager;
|
|
||||||
|
|
||||||
import com.unboundid.scim2.common.types.Email;
|
import com.unboundid.scim2.common.types.Email;
|
||||||
import com.unboundid.scim2.common.types.Meta;
|
import com.unboundid.scim2.common.types.Meta;
|
||||||
import com.unboundid.scim2.common.types.UserResource;
|
import com.unboundid.scim2.common.types.UserResource;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.jpa.entities.UserEntity;
|
import org.keycloak.models.jpa.entities.UserEntity;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
|
||||||
import sh.libre.scim.jpa.ScimResource;
|
|
||||||
|
|
||||||
public class UserAdapter extends Adapter<UserModel, UserResource> {
|
public class UserAdapter extends Adapter<UserModel, UserResource> {
|
||||||
|
|
||||||
private String username;
|
private String username;
|
||||||
|
@ -24,8 +22,8 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
|
||||||
private String email;
|
private String email;
|
||||||
private Boolean active;
|
private Boolean active;
|
||||||
|
|
||||||
public UserAdapter(String realmId, String componentId, EntityManager em) {
|
public UserAdapter(KeycloakSession session, String componentId) {
|
||||||
super(realmId, componentId, em, "User", Logger.getLogger(UserAdapter.class));
|
super(session, componentId, "User", Logger.getLogger(UserAdapter.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
|
@ -68,6 +66,11 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<UserResource> getResourceClass() {
|
||||||
|
return UserResource.class;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(UserModel user) {
|
public void apply(UserModel user) {
|
||||||
setId(user.getId());
|
setId(user.getId());
|
||||||
|
@ -94,12 +97,6 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void apply(ScimResource mapping) {
|
|
||||||
setId(mapping.getId());
|
|
||||||
setExternalId(mapping.getExternalId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserResource toSCIM(Boolean addMeta) {
|
public UserResource toSCIM(Boolean addMeta) {
|
||||||
var user = new UserResource();
|
var user = new UserResource();
|
||||||
|
@ -126,7 +123,8 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserEntity createEntity() {
|
@Override
|
||||||
|
public void createEntity() {
|
||||||
var kcUser = new UserEntity();
|
var kcUser = new UserEntity();
|
||||||
kcUser.setId(KeycloakModelUtils.generateId());
|
kcUser.setId(KeycloakModelUtils.generateId());
|
||||||
kcUser.setRealmId(realmId);
|
kcUser.setRealmId(realmId);
|
||||||
|
@ -134,9 +132,9 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
|
||||||
kcUser.setEmail(email, false);
|
kcUser.setEmail(email, false);
|
||||||
this.em.persist(kcUser);
|
this.em.persist(kcUser);
|
||||||
this.id = kcUser.getId();
|
this.id = kcUser.getId();
|
||||||
return kcUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Boolean entityExists() {
|
public Boolean entityExists() {
|
||||||
if (this.id == null) {
|
if (this.id == null) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -148,6 +146,7 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Boolean tryToMap() {
|
public Boolean tryToMap() {
|
||||||
try {
|
try {
|
||||||
var userEntity = this.em
|
var userEntity = this.em
|
||||||
|
@ -163,4 +162,14 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<UserModel> getResourceStream() {
|
||||||
|
return this.session.users().getUsersStream(this.session.getContext().getRealm());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean skipRefresh() {
|
||||||
|
return getUsername().equals("admin");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package sh.libre.scim.event;
|
package sh.libre.scim.event;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
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;
|
||||||
|
@ -7,9 +10,12 @@ import org.keycloak.events.EventType;
|
||||||
import org.keycloak.events.admin.AdminEvent;
|
import org.keycloak.events.admin.AdminEvent;
|
||||||
import org.keycloak.events.admin.OperationType;
|
import org.keycloak.events.admin.OperationType;
|
||||||
import org.keycloak.events.admin.ResourceType;
|
import org.keycloak.events.admin.ResourceType;
|
||||||
|
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 org.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
|
||||||
|
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.UserAdapter;
|
||||||
|
|
||||||
|
@ -48,7 +54,6 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
var userId = event.getResourcePath().replace("users/", "");
|
var userId = event.getResourcePath().replace("users/", "");
|
||||||
LOGGER.infof("%s %s", userId, event.getOperationType());
|
LOGGER.infof("%s %s", userId, event.getOperationType());
|
||||||
if (event.getOperationType() == OperationType.CREATE) {
|
if (event.getOperationType() == OperationType.CREATE) {
|
||||||
// session.getTransactionManager().rollback();
|
|
||||||
var user = getUser(userId);
|
var user = getUser(userId);
|
||||||
dispatcher.run((client) -> client.create(UserAdapter.class, user));
|
dispatcher.run((client) -> client.create(UserAdapter.class, user));
|
||||||
}
|
}
|
||||||
|
@ -60,11 +65,28 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
dispatcher.run((client) -> client.delete(UserAdapter.class, userId));
|
dispatcher.run((client) -> client.delete(UserAdapter.class, userId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event.getResourceType() == ResourceType.COMPONENT) {
|
if (event.getResourceType() == ResourceType.GROUP) {
|
||||||
if (event.getOperationType() == OperationType.CREATE
|
var groupId = event.getResourcePath().replace("groups/", "");
|
||||||
|| (event.getOperationType() == OperationType.UPDATE)) {
|
|
||||||
LOGGER.infof("%s %s", event.getResourcePath(), event.getOperationType());
|
LOGGER.infof("%s %s", event.getResourcePath(), event.getOperationType());
|
||||||
// dispatcher.run((client) -> client.syncUsers());
|
if (event.getOperationType() == OperationType.CREATE) {
|
||||||
|
var group = getGroup(groupId);
|
||||||
|
dispatcher.run((client) -> client.create(GroupAdapter.class, group));
|
||||||
|
}
|
||||||
|
if (event.getOperationType() == OperationType.UPDATE) {
|
||||||
|
var group = getGroup(groupId);
|
||||||
|
dispatcher.run((client) -> client.replace(GroupAdapter.class, group));
|
||||||
|
}
|
||||||
|
if (event.getOperationType() == OperationType.DELETE) {
|
||||||
|
dispatcher.run((client) -> client.delete(GroupAdapter.class, groupId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.getResourceType() == ResourceType.GROUP_MEMBERSHIP) {
|
||||||
|
ObjectMapper obj = new ObjectMapper();
|
||||||
|
try {
|
||||||
|
var groupRepresentation = obj.readValue(event.getRepresentation(), GroupRepresentation.class);
|
||||||
|
var group = getGroup(groupRepresentation.getId());
|
||||||
|
dispatcher.run((client) -> client.replace(GroupAdapter.class, group));
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,4 +94,8 @@ public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
private UserModel getUser(String id) {
|
private UserModel getUser(String id) {
|
||||||
return session.users().getUserById(session.getContext().getRealm(), id);
|
return session.users().getUserById(session.getContext().getRealm(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GroupModel getGroup(String id) {
|
||||||
|
return session.groups().getGroupById(session.getContext().getRealm(), id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,9 @@ 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 sh.libre.scim.core.GroupAdapter;
|
||||||
import sh.libre.scim.core.ScimClient;
|
import sh.libre.scim.core.ScimClient;
|
||||||
|
import sh.libre.scim.core.UserAdapter;
|
||||||
|
|
||||||
public class ScimStorageProviderFactory
|
public class ScimStorageProviderFactory
|
||||||
implements UserStorageProviderFactory<ScimStorageProvider>, ImportSynchronization {
|
implements UserStorageProviderFactory<ScimStorageProvider>, ImportSynchronization {
|
||||||
|
@ -110,7 +112,8 @@ public class ScimStorageProviderFactory
|
||||||
var client = new ScimClient(model, session);
|
var client = new ScimClient(model, session);
|
||||||
model.setEnabled(false);
|
model.setEnabled(false);
|
||||||
realm.updateComponent(model);
|
realm.updateComponent(model);
|
||||||
client.sync(result);
|
client.sync(UserAdapter.class, result);
|
||||||
|
client.sync(GroupAdapter.class, result);
|
||||||
client.close();
|
client.close();
|
||||||
model.setEnabled(true);
|
model.setEnabled(true);
|
||||||
realm.updateComponent(model);
|
realm.updateComponent(model);
|
||||||
|
|
Loading…
Reference in a new issue