refactoring using generics + sync feature

This commit is contained in:
Hugo Renard 2022-02-21 19:26:12 +01:00
parent 95c6c5588f
commit f22e23742d
Signed by: hougo
GPG key ID: 3A285FD470209C59
9 changed files with 563 additions and 197 deletions

View file

@ -0,0 +1,110 @@
package sh.libre.scim.core;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.ws.rs.NotFoundException;
import org.jboss.logging.Logger;
import org.keycloak.models.RoleMapperModel;
import sh.libre.scim.jpa.ScimResource;
public abstract class Adapter<M extends RoleMapperModel, S extends com.unboundid.scim2.common.ScimResource> {
protected final Logger LOGGER;
protected final String realmId;
protected final String type;
protected final String componentId;
protected final EntityManager em;
protected String id;
protected String externalId;
public Adapter(String realmId, String componentId, EntityManager em, String type, Logger logger) {
this.realmId = realmId;
this.componentId = componentId;
this.em = em;
this.type = type;
this.LOGGER = logger;
}
public String getId() {
return id;
}
public void setId(String id) {
if (this.id == null) {
this.id = id;
}
}
public String getExternalId() {
return externalId;
}
public void setExternalId(String externalId) {
if (this.externalId == null) {
this.externalId = externalId;
}
}
public String getSCIMEndpoint() {
return type + "s";
}
public ScimResource toMapping() {
var entity = new ScimResource();
entity.setType(type);
entity.setId(id);
entity.setExternalId(externalId);
entity.setComponentId(componentId);
entity.setRealmId(realmId);
return entity;
}
public TypedQuery<ScimResource> query(String query, String id) {
return this.em
.createNamedQuery(query, ScimResource.class)
.setParameter("type", type)
.setParameter("realmId", realmId)
.setParameter("componentId", componentId)
.setParameter("id", id);
}
public ScimResource getMapping() {
try {
if (this.id != null) {
return this.query("findById", id).getSingleResult();
}
if (this.externalId != null) {
return this.query("findByExternalId", externalId).getSingleResult();
}
} catch (NotFoundException e) {
} catch (Exception e) {
LOGGER.error(e);
}
return null;
}
public void saveMapping() {
this.em.persist(toMapping());
}
public void deleteMapping() {
this.em.remove(this.toMapping());
}
public abstract void apply(M model);
public abstract void apply(S resource);
public abstract void apply(ScimResource resource);
public abstract S toSCIM(Boolean addMeta);
public abstract Boolean entityExists();
public abstract Boolean tryToMap();
}

View file

@ -1,42 +1,34 @@
package sh.libre.scim.core; package sh.libre.scim.core;
import com.unboundid.scim2.client.ScimService;
import com.unboundid.scim2.common.exceptions.ScimException;
import com.unboundid.scim2.common.types.Email;
import com.unboundid.scim2.common.types.Meta;
import com.unboundid.scim2.common.types.Name;
import com.unboundid.scim2.common.types.UserResource;
import io.github.resilience4j.core.IntervalFunction;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
import java.net.URI;
import java.util.ArrayList;
import java.lang.RuntimeException;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.ws.rs.client.Client; import javax.ws.rs.client.Client;
import com.unboundid.scim2.client.ScimService;
import com.unboundid.scim2.common.ScimResource;
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;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel; import org.keycloak.models.RoleMapperModel;
import org.keycloak.models.jpa.entities.ComponentEntity; import org.keycloak.storage.user.SynchronizationResult;
import org.keycloak.models.jpa.entities.RealmEntity;
import sh.libre.scim.jpa.ScimResource; import io.github.resilience4j.core.IntervalFunction;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
public class ScimClient { public class ScimClient {
final protected Logger LOGGER = Logger.getLogger(ScimClient.class);
final private Logger LOGGER = Logger.getLogger(ScimClient.class); final protected Client client = ResteasyClientBuilder.newClient();
final private Client client = ResteasyClientBuilder.newClient(); final protected ScimService scimService;
final private ScimService scimService; final protected RetryRegistry registry;
final private RetryRegistry registry; final protected KeycloakSession session;
final private KeycloakSession session; final protected String contentType;
final private String contentType; final protected ComponentModel model;
final private ComponentModel model;
public ScimClient(ComponentModel model, KeycloakSession session) { public ScimClient(ComponentModel model, KeycloakSession session) {
this.model = model; this.model = model;
@ -57,71 +49,75 @@ public class ScimClient {
registry = RetryRegistry.of(retryConfig); registry = RetryRegistry.of(retryConfig);
} }
private EntityManager getEM() { protected EntityManager getEM() {
return session.getProvider(JpaConnectionProvider.class).getEntityManager(); return session.getProvider(JpaConnectionProvider.class).getEntityManager();
} }
private String getRealmId() { protected String getRealmId() {
return session.getContext().getRealm().getId(); return session.getContext().getRealm().getId();
} }
private RealmEntity getRealmEntity() { protected <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> A getAdapter(
return getEM().getReference(RealmEntity.class, getRealmId()); Class<A> aClass) {
try {
return aClass.getDeclaredConstructor(String.class, String.class, EntityManager.class)
.newInstance(getRealmId(), this.model.getId(), getEM());
} catch (Exception e) {
throw new RuntimeException(e);
}
} }
private ComponentEntity getSPEntity() { public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void create(Class<A> aClass,
return getEM().getReference(ComponentEntity.class, this.model.getId()); M kcModel) {
} var adapter = getAdapter(aClass);
adapter.apply(kcModel);
public void createUser(UserModel kcUser) { var retry = registry.retry("create-" + adapter.getId());
LOGGER.info("Create User");
var user = toUser(kcUser);
var retry = registry.retry("create-" + kcUser.getId());
var spUser = retry.executeSupplier(() -> { var spUser = retry.executeSupplier(() -> {
try { try {
return scimService.createRequest("Users", user).contentType(contentType).invoke(); return scimService.createRequest(adapter.getSCIMEndpoint(), adapter.toSCIM(false))
.contentType(contentType).invoke();
} catch (ScimException e) { } catch (ScimException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
var scimUser = toScimUser(spUser); adapter.apply(spUser);
scimUser.setLocalId(kcUser.getId()); adapter.saveMapping();
getEM().persist(scimUser); };
}
public void replaceUser(UserModel kcUser) { public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void replace(Class<A> aClass,
LOGGER.info("Replace User"); M kcModel) {
var adapter = getAdapter(aClass);
try { try {
var resource = querUserById(kcUser.getId()); adapter.apply(kcModel);
var user = toUser(kcUser); var resource = adapter.query("findById", adapter.getId()).getSingleResult();
user.setId(resource.getRemoteId()); adapter.apply(resource);
var meta = new Meta(); var retry = registry.retry("replace-" + adapter.getId());
var uri = new URI("Users/" + user.getId());
meta.setLocation(uri);
user.setMeta(meta);
var retry = registry.retry("replace-" + kcUser.getId());
retry.executeSupplier(() -> { retry.executeSupplier(() -> {
try { try {
return scimService.replaceRequest(user).contentType(contentType).invoke(); return scimService.replaceRequest(adapter.toSCIM(true)).contentType(contentType).invoke();
} catch (ScimException e) { } catch (ScimException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
} catch (NoResultException e) { } catch (NoResultException e) {
LOGGER.warnf("Failed to repalce user %s, scim mapping not found", kcUser.getId()); LOGGER.warnf("failed to replace resource %s, scim mapping not found", adapter.getId());
} catch (Exception e) { } catch (Exception e) {
LOGGER.error(e); LOGGER.error(e);
} }
} }
public void deleteUser(String userId) { public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void delete(Class<A> aClass,
LOGGER.info("Delete User"); String id) {
var adapter = getAdapter(aClass);
adapter.setId(id);
try { try {
var resource = querUserById(userId); var resource = adapter.query("findById", adapter.getId()).getSingleResult();
var retry = registry.retry("delete-" + userId); adapter.apply(resource);
var retry = registry.retry("delete-" + id);
retry.executeSupplier(() -> { retry.executeSupplier(() -> {
try { try {
scimService.deleteRequest("Users", resource.getRemoteId()).contentType(contentType).invoke(); scimService.deleteRequest(adapter.getSCIMEndpoint(), resource.getExternalId())
.contentType(contentType).invoke();
} catch (ScimException e) { } catch (ScimException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -129,55 +125,88 @@ public class ScimClient {
}); });
getEM().remove(resource); getEM().remove(resource);
} catch (NoResultException e) { } catch (NoResultException e) {
LOGGER.warnf("Failde to delete user %s, scim mapping not found", userId); LOGGER.warnf("Failed to delete resource %s, scim mapping not found", id);
} }
} }
private TypedQuery<ScimResource> queryUser(String query) { public void refreshUsers(SynchronizationResult syncRes) {
return getEM() LOGGER.info("Refresh Users");
.createNamedQuery(query, ScimResource.class) this.session.users().getUsersStream(this.session.getContext().getRealm()).forEach(kcUser -> {
.setParameter("realm", getRealmEntity()) LOGGER.infof("Reconciling local user %s", kcUser.getId());
.setParameter("type", "Users") if (!kcUser.getUsername().equals("admin")) {
.setParameter("serviceProvider", getSPEntity()); var adapter = getAdapter(UserAdapter.class);
adapter.apply(kcUser);
var mapping = adapter.getMapping();
if (mapping == null) {
LOGGER.info("Creating it");
this.create(UserAdapter.class, kcUser);
} else {
LOGGER.info("Replacing it");
this.replace(UserAdapter.class, kcUser);
}
syncRes.increaseUpdated();
}
});
} }
private ScimResource querUserById(String id) { public void importUsers(SynchronizationResult syncRes) {
return queryUser("findByLocalId").setParameter("id", id).getSingleResult(); LOGGER.info("Import Users");
try {
var spUsers = scimService.searchRequest("Users").contentType(contentType).invoke(UserResource.class);
for (var spUser : spUsers) {
try {
LOGGER.infof("Reconciling remote user %s", spUser.getId());
var adapter = getAdapter(UserAdapter.class);
adapter.apply(spUser);
var mapping = adapter.getMapping();
if (mapping != null) {
adapter.apply(mapping);
if (adapter.entityExists()) {
LOGGER.info("Valid mapping found, skipping");
continue;
} else {
LOGGER.info("Delete a dangling mapping");
adapter.deleteMapping();
}
} }
private ScimResource scimUser() { var mapped = adapter.tryToMap();
var resource = new ScimResource(); if (mapped) {
resource.setType("Users"); LOGGER.info("Matched a user");
resource.setRealm(getRealmEntity()); adapter.saveMapping();
resource.setServiceProvider(getSPEntity()); } else {
return resource; switch (this.model.get("sync-import-action")) {
case "CREATE_LOCAL":
LOGGER.info("Create local user");
adapter.createEntity();
adapter.saveMapping();
syncRes.increaseAdded();
break;
case "DELETE_REMOTE":
LOGGER.info("Delete remote user");
scimService.deleteRequest("Users", spUser.getId()).contentType(contentType)
.invoke();
syncRes.increaseRemoved();
break;
}
}
} catch (Exception e) {
syncRes.increaseFailed();
}
}
} catch (ScimException e) {
throw new RuntimeException(e);
}
} }
private ScimResource toScimUser(UserResource user) { public void sync(SynchronizationResult syncRes) {
var resource = scimUser(); if (this.model.get("sync-import", false)) {
resource.setRemoteId(user.getId()); this.importUsers(syncRes);
resource.setLocalId(user.getExternalId());
return resource;
} }
if (this.model.get("sync-refresh", false)) {
private UserResource toUser(UserModel kcUser) { this.refreshUsers(syncRes);
var user = new UserResource();
user.setExternalId(kcUser.getId());
user.setUserName(kcUser.getUsername());
var name = new Name();
name.setGivenName(kcUser.getFirstName());
name.setFamilyName(kcUser.getLastName());
user.setName(name);
user.setDisplayName(kcUser.getFirstName() + " " + kcUser.getLastName());
var emails = new ArrayList<Email>();
if (kcUser.getEmail() != "") {
var email = new Email().setPrimary(true).setValue(kcUser.getEmail());
emails.add(email);
} }
user.setEmails(emails);
user.setActive(kcUser.isEnabled());
return user;
} }
public void close() { public void close() {

View file

@ -0,0 +1,166 @@
package sh.libre.scim.core;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import javax.persistence.EntityManager;
import com.unboundid.scim2.common.types.Email;
import com.unboundid.scim2.common.types.Meta;
import com.unboundid.scim2.common.types.UserResource;
import org.jboss.logging.Logger;
import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import sh.libre.scim.jpa.ScimResource;
public class UserAdapter extends Adapter<UserModel, UserResource> {
private String username;
private String displayName;
private String email;
private Boolean active;
public UserAdapter(String realmId, String componentId, EntityManager em) {
super(realmId, componentId, em, "User", Logger.getLogger(UserAdapter.class));
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
if (this.username == null) {
this.username = username;
}
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
if (this.displayName == null) {
this.displayName = displayName;
}
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
if (this.email == null) {
this.email = email;
}
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
if (this.active == null) {
this.active = active;
}
}
@Override
public void apply(UserModel user) {
setId(user.getId());
setUsername(user.getUsername());
if (user.getFirstName() != null && user.getLastName() != null) {
setDisplayName(String.format("%s %s", user.getFirstName(), user.getLastName()));
} else if (user.getFirstName() != null) {
setDisplayName(user.getFirstName());
} else if (user.getLastName() != null) {
setDisplayName(user.getLastName());
}
setEmail(user.getEmail());
setActive(user.isEnabled());
}
@Override
public void apply(UserResource user) {
setExternalId(user.getId());
setUsername(user.getUserName());
setDisplayName(user.getDisplayName());
setActive(user.getActive());
if (user.getEmails().size() > 0) {
setEmail(user.getEmails().get(0).getValue());
}
}
@Override
public void apply(ScimResource mapping) {
setId(mapping.getId());
setExternalId(mapping.getExternalId());
}
@Override
public UserResource toSCIM(Boolean addMeta) {
var user = new UserResource();
user.setExternalId(id);
user.setUserName(username);
user.setId(externalId);
user.setDisplayName(displayName);
var emails = new ArrayList<Email>();
if (email != null) {
emails.add(
new Email().setPrimary(true).setValue(email));
}
user.setEmails(emails);
user.setActive(active);
if (addMeta) {
var meta = new Meta();
try {
var uri = new URI("Users/" + externalId);
meta.setLocation(uri);
} catch (URISyntaxException e) {
}
user.setMeta(meta);
}
return user;
}
public UserEntity createEntity() {
var kcUser = new UserEntity();
kcUser.setId(KeycloakModelUtils.generateId());
kcUser.setRealmId(realmId);
kcUser.setUsername(username);
kcUser.setEmail(email, false);
this.em.persist(kcUser);
this.id = kcUser.getId();
return kcUser;
}
public Boolean entityExists() {
if (this.id == null) {
return false;
}
var user = this.em.find(UserEntity.class, this.id);
if (user != null) {
return true;
}
return false;
}
public Boolean tryToMap() {
try {
var userEntity = this.em
.createQuery("select u from UserEntity u where u.username=:username or u.email=:email",
UserEntity.class)
.setParameter("username", username)
.setParameter("email", email)
.getSingleResult();
setId(userEntity.getId());
return true;
} catch (Exception e) {
}
return false;
}
}

View file

@ -11,6 +11,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import sh.libre.scim.core.ScimDispatcher; import sh.libre.scim.core.ScimDispatcher;
import sh.libre.scim.core.UserAdapter;
public class ScimEventListenerProvider implements EventListenerProvider { public class ScimEventListenerProvider implements EventListenerProvider {
final Logger LOGGER = Logger.getLogger(ScimEventListenerProvider.class); final Logger LOGGER = Logger.getLogger(ScimEventListenerProvider.class);
@ -30,14 +31,14 @@ public class ScimEventListenerProvider implements EventListenerProvider {
public void onEvent(Event event) { public void onEvent(Event event) {
if (event.getType() == EventType.REGISTER) { if (event.getType() == EventType.REGISTER) {
var user = getUser(event.getUserId()); var user = getUser(event.getUserId());
dispatcher.run((client) -> client.createUser(user)); dispatcher.run((client) -> client.create(UserAdapter.class, user));
} }
if (event.getType() == EventType.UPDATE_EMAIL || event.getType() == EventType.UPDATE_PROFILE) { if (event.getType() == EventType.UPDATE_EMAIL || event.getType() == EventType.UPDATE_PROFILE) {
var user = getUser(event.getUserId()); var user = getUser(event.getUserId());
dispatcher.run((client) -> client.replaceUser(user)); dispatcher.run((client) -> client.replace(UserAdapter.class, user));
} }
if (event.getType() == EventType.DELETE_ACCOUNT) { if (event.getType() == EventType.DELETE_ACCOUNT) {
dispatcher.run((client) -> client.deleteUser(event.getUserId())); dispatcher.run((client) -> client.delete(UserAdapter.class, event.getUserId()));
} }
} }
@ -49,14 +50,21 @@ public class ScimEventListenerProvider implements EventListenerProvider {
if (event.getOperationType() == OperationType.CREATE) { if (event.getOperationType() == OperationType.CREATE) {
// session.getTransactionManager().rollback(); // session.getTransactionManager().rollback();
var user = getUser(userId); var user = getUser(userId);
dispatcher.run((client) -> client.createUser(user)); dispatcher.run((client) -> client.create(UserAdapter.class, user));
} }
if (event.getOperationType() == OperationType.UPDATE) { if (event.getOperationType() == OperationType.UPDATE) {
var user = getUser(userId); var user = getUser(userId);
dispatcher.run((client) -> client.replaceUser(user)); dispatcher.run((client) -> client.replace(UserAdapter.class, user));
} }
if (event.getOperationType() == OperationType.DELETE) { if (event.getOperationType() == OperationType.DELETE) {
dispatcher.run((client) -> client.deleteUser(userId)); dispatcher.run((client) -> client.delete(UserAdapter.class, userId));
}
}
if (event.getResourceType() == ResourceType.COMPONENT) {
if (event.getOperationType() == OperationType.CREATE
|| (event.getOperationType() == OperationType.UPDATE)) {
LOGGER.infof("%s %s", event.getResourcePath(), event.getOperationType());
// dispatcher.run((client) -> client.syncUsers());
} }
} }
} }

View file

@ -4,57 +4,67 @@ import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.IdClass; import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQuery; import javax.persistence.NamedQuery;
import javax.persistence.NamedQueries; import javax.persistence.NamedQueries;
import javax.persistence.Table; import javax.persistence.Table;
import org.keycloak.models.jpa.entities.ComponentEntity;
import org.keycloak.models.jpa.entities.RealmEntity;
@Entity @Entity
@IdClass(ScimResourceId.class) @IdClass(ScimResourceId.class)
@Table(name = "SCIM_RESOURCE") @Table(name = "SCIM_RESOURCE")
@NamedQueries({ @NamedQueries({
@NamedQuery(name = "findByLocalId", query = "from ScimResource where realm = :realm and type = :type and serviceProvider = :serviceProvider and localId = :id"), @NamedQuery(name = "findById", query = "from ScimResource where realmId = :realmId and componentId = :componentId and type = :type and id = :id"),
@NamedQuery(name = "findByRemoteId", query = "from ScimResource where realm = :realm and type = :type and serviceProvider = :serviceProvider and remoteId = :id") }) @NamedQuery(name = "findByExternalId", query = "from ScimResource where realmId = :realmId and componentId = :componentId and type = :type and externalId = :id") })
public class ScimResource { public class ScimResource {
@Id @Id
@ManyToOne @Column(name = "ID", nullable = false)
@JoinColumn(name = "REALM_ID", referencedColumnName = "ID") private String id;
private RealmEntity realm;
@Id @Id
@ManyToOne @Column(name = "REALM_ID", nullable = false)
@JoinColumn(name = "SERVICE_PROVIDER", referencedColumnName = "ID") private String realmId;
private ComponentEntity serviceProvider;
@Id
@Column(name = "COMPONENT_ID", nullable = false)
private String componentId;
@Id @Id
@Column(name = "TYPE", nullable = false) @Column(name = "TYPE", nullable = false)
private String type; private String type;
@Id @Id
@Column(name = "LOCAL_ID", nullable = false) @Column(name = "EXTERNAL_ID", nullable = false)
private String localId; private String externalId;
@Column(name = "REMOTE_ID", nullable = false) public String getId() {
private String remoteId; return id;
public RealmEntity getRealm() {
return realm;
} }
public void setRealm(RealmEntity realm) { public void setId(String id) {
this.realm = realm; this.id = id;
} }
public ComponentEntity getServiceProvider() { public String getRealmId() {
return serviceProvider; return realmId;
} }
public void setServiceProvider(ComponentEntity serviceProvider) { public void setRealmId(String realmId) {
this.serviceProvider = serviceProvider; this.realmId = realmId;
}
public String getComponentId() {
return componentId;
}
public void setComponentId(String componentId) {
this.componentId = componentId;
}
public String getExternalId() {
return externalId;
}
public void setExternalId(String externalId) {
this.externalId = externalId;
} }
public String getType() { public String getType() {
@ -65,19 +75,4 @@ public class ScimResource {
this.type = type; this.type = type;
} }
public String getRemoteId() {
return remoteId;
}
public void setRemoteId(String remoteId) {
this.remoteId = remoteId;
}
public String getLocalId() {
return localId;
}
public void setLocalId(String localId) {
this.localId = localId;
}
} }

View file

@ -4,35 +4,45 @@ import java.io.Serializable;
import java.util.Objects; import java.util.Objects;
public class ScimResourceId implements Serializable { public class ScimResourceId implements Serializable {
private String realm; private String id;
private String serviceProvider; private String realmId;
private String componentId;
private String type; private String type;
private String remoteId; private String externalId;
public ScimResourceId() { public ScimResourceId() {
} }
public ScimResourceId(String realm, String serviceProvider, String type, String remoteId) { public ScimResourceId(String id, String realmId, String componentId, String type, String externalId) {
this.realm = realm; this.setId(id);
this.serviceProvider = serviceProvider; this.setRealmId(realmId);
this.type = type; this.setComponentId(componentId);
this.remoteId = remoteId; this.setType(type);
this.setExternalId(externalId);
} }
public String getRealm() { public String getId() {
return realm; return id;
} }
public void setRealm(String realm) { public void setId(String id) {
this.realm = realm; this.id = id;
} }
public String getServiceProvider() { public String getRealmId() {
return serviceProvider; return realmId;
} }
public void setServiceProvider(String serviceProvider) { public void setRealmId(String realmId) {
this.serviceProvider = serviceProvider; this.realmId = realmId;
}
public String getComponentId() {
return componentId;
}
public void setComponentId(String componentId) {
this.componentId = componentId;
} }
public String getType() { public String getType() {
@ -43,12 +53,12 @@ public class ScimResourceId implements Serializable {
this.type = type; this.type = type;
} }
public String getRemoteId() { public String getExternalId() {
return remoteId; return externalId;
} }
public void setRemoteId(String remoteId) { public void setExternalId(String externalId) {
this.remoteId = remoteId; this.externalId = externalId;
} }
@Override @Override
@ -58,14 +68,15 @@ 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;
return (o.realm == realm && return (o.id == id &&
o.serviceProvider == serviceProvider && o.realmId == realmId &&
o.componentId == componentId &&
o.type == type && o.type == type &&
o.remoteId == remoteId); o.externalId == externalId);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(realm, serviceProvider, type, remoteId); return Objects.hash(realmId, componentId, type, id, externalId);
} }
} }

View file

@ -1,25 +1,9 @@
package sh.libre.scim.storage; package sh.libre.scim.storage;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
public class ScimStorageProvider implements UserStorageProvider, UserRegistrationProvider { public class ScimStorageProvider implements UserStorageProvider {
@Override @Override
public void close() { public void close() {
} }
@Override
public UserModel addUser(RealmModel realm, String username) {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
// TODO Auto-generated method stub
return false;
}
} }

View file

@ -1,18 +1,31 @@
package sh.libre.scim.storage; package sh.libre.scim.storage;
import java.util.Date;
import java.util.List; import java.util.List;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import com.unboundid.scim2.client.ScimService; import com.unboundid.scim2.client.ScimService;
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;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
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.UserStorageProviderFactory; import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.user.ImportSynchronization;
import org.keycloak.storage.user.SynchronizationResult;
public class ScimStorageProviderFactory implements UserStorageProviderFactory<ScimStorageProvider> { import sh.libre.scim.core.ScimClient;
public class ScimStorageProviderFactory
implements UserStorageProviderFactory<ScimStorageProvider>, ImportSynchronization {
final private Logger LOGGER = Logger.getLogger(ScimStorageProviderFactory.class);
public final static String ID = "scim"; public final static String ID = "scim";
protected static final List<ProviderConfigProperty> configMetadata; protected static final List<ProviderConfigProperty> configMetadata;
static { static {
@ -46,11 +59,30 @@ public class ScimStorageProviderFactory implements UserStorageProviderFactory<Sc
.label("Bearer token") .label("Bearer token")
.helpText("Add a bearer token in the authorization header") .helpText("Add a bearer token in the authorization header")
.add() .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 don\'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(); .build();
} }
@Override @Override
public ScimStorageProvider create(KeycloakSession session, ComponentModel model) { public ScimStorageProvider create(KeycloakSession session, ComponentModel model) {
LOGGER.info("create");
return new ScimStorageProvider(); return new ScimStorageProvider();
} }
@ -64,4 +96,36 @@ public class ScimStorageProviderFactory implements UserStorageProviderFactory<Sc
return configMetadata; return configMetadata;
} }
@Override
public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId,
UserStorageProviderModel model) {
LOGGER.info("sync");
var result = new SynchronizationResult();
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
RealmModel realm = session.realms().getRealm(realmId);
session.getContext().setRealm(realm);
var client = new ScimClient(model, session);
model.setEnabled(false);
realm.updateComponent(model);
client.sync(result);
client.close();
model.setEnabled(true);
realm.updateComponent(model);
}
});
return result;
}
@Override
public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId,
UserStorageProviderModel model) {
return this.sync(sessionFactory, realmId, model);
}
} }

View file

@ -3,27 +3,26 @@
<changeSet author="contact@indiehosters.net" id="scim-resource-1.0"> <changeSet author="contact@indiehosters.net" id="scim-resource-1.0">
<createTable tableName="SCIM_RESOURCE"> <createTable tableName="SCIM_RESOURCE">
<column name="REALM_ID" type="VARCHAR(36)"> <column name="ID" type="VARCHAR(36)">
<constraints nullable="false" /> <constraints nullable="false" />
</column> </column>
<column name="SERVICE_PROVIDER" type="VARCHAR(36)"> <column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false" /> <constraints nullable="false" />
</column> </column>
<column name="TYPE" type="VARCHAR(36)"> <column name="TYPE" type="VARCHAR(36)">
<constraints nullable="false" /> <constraints nullable="false" />
</column> </column>
<column name="REMOTE_ID" type="VARCHAR(36)"> <column name="COMPONENT_ID" type="VARCHAR(36)">
<constraints nullable="false" /> <constraints nullable="false" />
</column> </column>
<column name="LOCAL_ID" type="VARCHAR(36)"> <column name="EXTERNAL_ID" type="VARCHAR(36)">
<constraints nullable="false" /> <constraints nullable="false" />
</column> </column>
</createTable> </createTable>
<addPrimaryKey constraintName="PK_SCIM_RESOURCE" tableName="SCIM_RESOURCE" columnNames="REALM_ID,SERVICE_PROVIDER,TYPE,REMOTE_ID" /> <addPrimaryKey constraintName="PK_SCIM_RESOURCE" tableName="SCIM_RESOURCE" columnNames="ID,REALM_ID,TYPE,COMPONENT_ID,EXTERNAL_ID" />
<addForeignKeyConstraint baseTableName="SCIM_RESOURCE" baseColumnNames="REALM_ID" constraintName="FK_SCIM_RESOURCE_REALM" referencedTableName="REALM" referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE" /> <addForeignKeyConstraint baseTableName="SCIM_RESOURCE" baseColumnNames="REALM_ID" constraintName="FK_SCIM_RESOURCE_REALM" referencedTableName="REALM" referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE" />
<addForeignKeyConstraint baseTableName="SCIM_RESOURCE" baseColumnNames="SERVICE_PROVIDER" constraintName="FK_SCIM_RESOURCE_COMPONENT" referencedTableName="COMPONENT" referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE" /> <addForeignKeyConstraint baseTableName="SCIM_RESOURCE" baseColumnNames="COMPONENT_ID" constraintName="FK_SCIM_RESOURCE_COMPONENT" referencedTableName="COMPONENT" referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE" />
</changeSet> </changeSet>
</databaseChangeLog> </databaseChangeLog>