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;
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.NoResultException;
import javax.persistence.TypedQuery;
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.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.ComponentEntity;
import org.keycloak.models.jpa.entities.RealmEntity;
import org.keycloak.models.RoleMapperModel;
import org.keycloak.storage.user.SynchronizationResult;
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 {
final private Logger LOGGER = Logger.getLogger(ScimClient.class);
final private Client client = ResteasyClientBuilder.newClient();
final private ScimService scimService;
final private RetryRegistry registry;
final private KeycloakSession session;
final private String contentType;
final private ComponentModel model;
final protected Logger LOGGER = Logger.getLogger(ScimClient.class);
final protected Client client = ResteasyClientBuilder.newClient();
final protected ScimService scimService;
final protected RetryRegistry registry;
final protected KeycloakSession session;
final protected String contentType;
final protected ComponentModel model;
public ScimClient(ComponentModel model, KeycloakSession session) {
this.model = model;
@ -57,71 +49,75 @@ public class ScimClient {
registry = RetryRegistry.of(retryConfig);
}
private EntityManager getEM() {
protected EntityManager getEM() {
return session.getProvider(JpaConnectionProvider.class).getEntityManager();
}
private String getRealmId() {
protected String getRealmId() {
return session.getContext().getRealm().getId();
}
private RealmEntity getRealmEntity() {
return getEM().getReference(RealmEntity.class, getRealmId());
protected <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> A getAdapter(
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() {
return getEM().getReference(ComponentEntity.class, this.model.getId());
}
public void createUser(UserModel kcUser) {
LOGGER.info("Create User");
var user = toUser(kcUser);
var retry = registry.retry("create-" + kcUser.getId());
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void create(Class<A> aClass,
M kcModel) {
var adapter = getAdapter(aClass);
adapter.apply(kcModel);
var retry = registry.retry("create-" + adapter.getId());
var spUser = retry.executeSupplier(() -> {
try {
return scimService.createRequest("Users", user).contentType(contentType).invoke();
return scimService.createRequest(adapter.getSCIMEndpoint(), adapter.toSCIM(false))
.contentType(contentType).invoke();
} catch (ScimException e) {
throw new RuntimeException(e);
}
});
var scimUser = toScimUser(spUser);
scimUser.setLocalId(kcUser.getId());
getEM().persist(scimUser);
}
adapter.apply(spUser);
adapter.saveMapping();
};
public void replaceUser(UserModel kcUser) {
LOGGER.info("Replace User");
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void replace(Class<A> aClass,
M kcModel) {
var adapter = getAdapter(aClass);
try {
var resource = querUserById(kcUser.getId());
var user = toUser(kcUser);
user.setId(resource.getRemoteId());
var meta = new Meta();
var uri = new URI("Users/" + user.getId());
meta.setLocation(uri);
user.setMeta(meta);
var retry = registry.retry("replace-" + kcUser.getId());
adapter.apply(kcModel);
var resource = adapter.query("findById", adapter.getId()).getSingleResult();
adapter.apply(resource);
var retry = registry.retry("replace-" + adapter.getId());
retry.executeSupplier(() -> {
try {
return scimService.replaceRequest(user).contentType(contentType).invoke();
return scimService.replaceRequest(adapter.toSCIM(true)).contentType(contentType).invoke();
} catch (ScimException e) {
throw new RuntimeException(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) {
LOGGER.error(e);
}
}
public void deleteUser(String userId) {
LOGGER.info("Delete User");
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void delete(Class<A> aClass,
String id) {
var adapter = getAdapter(aClass);
adapter.setId(id);
try {
var resource = querUserById(userId);
var retry = registry.retry("delete-" + userId);
var resource = adapter.query("findById", adapter.getId()).getSingleResult();
adapter.apply(resource);
var retry = registry.retry("delete-" + id);
retry.executeSupplier(() -> {
try {
scimService.deleteRequest("Users", resource.getRemoteId()).contentType(contentType).invoke();
scimService.deleteRequest(adapter.getSCIMEndpoint(), resource.getExternalId())
.contentType(contentType).invoke();
} catch (ScimException e) {
throw new RuntimeException(e);
}
@ -129,55 +125,88 @@ public class ScimClient {
});
getEM().remove(resource);
} 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) {
return getEM()
.createNamedQuery(query, ScimResource.class)
.setParameter("realm", getRealmEntity())
.setParameter("type", "Users")
.setParameter("serviceProvider", getSPEntity());
public void refreshUsers(SynchronizationResult syncRes) {
LOGGER.info("Refresh Users");
this.session.users().getUsersStream(this.session.getContext().getRealm()).forEach(kcUser -> {
LOGGER.infof("Reconciling local user %s", kcUser.getId());
if (!kcUser.getUsername().equals("admin")) {
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) {
return queryUser("findByLocalId").setParameter("id", id).getSingleResult();
}
public void importUsers(SynchronizationResult syncRes) {
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);
private ScimResource scimUser() {
var resource = new ScimResource();
resource.setType("Users");
resource.setRealm(getRealmEntity());
resource.setServiceProvider(getSPEntity());
return resource;
}
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 toScimUser(UserResource user) {
var resource = scimUser();
resource.setRemoteId(user.getId());
resource.setLocalId(user.getExternalId());
return resource;
}
private UserResource toUser(UserModel kcUser) {
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);
var mapped = adapter.tryToMap();
if (mapped) {
LOGGER.info("Matched a user");
adapter.saveMapping();
} else {
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);
}
}
public void sync(SynchronizationResult syncRes) {
if (this.model.get("sync-import", false)) {
this.importUsers(syncRes);
}
if (this.model.get("sync-refresh", false)) {
this.refreshUsers(syncRes);
}
user.setEmails(emails);
user.setActive(kcUser.isEnabled());
return user;
}
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 sh.libre.scim.core.ScimDispatcher;
import sh.libre.scim.core.UserAdapter;
public class ScimEventListenerProvider implements EventListenerProvider {
final Logger LOGGER = Logger.getLogger(ScimEventListenerProvider.class);
@ -30,14 +31,14 @@ public class ScimEventListenerProvider implements EventListenerProvider {
public void onEvent(Event event) {
if (event.getType() == EventType.REGISTER) {
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) {
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) {
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) {
// session.getTransactionManager().rollback();
var user = getUser(userId);
dispatcher.run((client) -> client.createUser(user));
dispatcher.run((client) -> client.create(UserAdapter.class, user));
}
if (event.getOperationType() == OperationType.UPDATE) {
var user = getUser(userId);
dispatcher.run((client) -> client.replaceUser(user));
dispatcher.run((client) -> client.replace(UserAdapter.class, user));
}
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.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQuery;
import javax.persistence.NamedQueries;
import javax.persistence.Table;
import org.keycloak.models.jpa.entities.ComponentEntity;
import org.keycloak.models.jpa.entities.RealmEntity;
@Entity
@IdClass(ScimResourceId.class)
@Table(name = "SCIM_RESOURCE")
@NamedQueries({
@NamedQuery(name = "findByLocalId", query = "from ScimResource where realm = :realm and type = :type and serviceProvider = :serviceProvider and localId = :id"),
@NamedQuery(name = "findByRemoteId", query = "from ScimResource where realm = :realm and type = :type and serviceProvider = :serviceProvider and remoteId = :id") })
@NamedQuery(name = "findById", query = "from ScimResource where realmId = :realmId and componentId = :componentId and type = :type and id = :id"),
@NamedQuery(name = "findByExternalId", query = "from ScimResource where realmId = :realmId and componentId = :componentId and type = :type and externalId = :id") })
public class ScimResource {
@Id
@ManyToOne
@JoinColumn(name = "REALM_ID", referencedColumnName = "ID")
private RealmEntity realm;
@Column(name = "ID", nullable = false)
private String id;
@Id
@ManyToOne
@JoinColumn(name = "SERVICE_PROVIDER", referencedColumnName = "ID")
private ComponentEntity serviceProvider;
@Column(name = "REALM_ID", nullable = false)
private String realmId;
@Id
@Column(name = "COMPONENT_ID", nullable = false)
private String componentId;
@Id
@Column(name = "TYPE", nullable = false)
private String type;
@Id
@Column(name = "LOCAL_ID", nullable = false)
private String localId;
@Column(name = "EXTERNAL_ID", nullable = false)
private String externalId;
@Column(name = "REMOTE_ID", nullable = false)
private String remoteId;
public RealmEntity getRealm() {
return realm;
public String getId() {
return id;
}
public void setRealm(RealmEntity realm) {
this.realm = realm;
public void setId(String id) {
this.id = id;
}
public ComponentEntity getServiceProvider() {
return serviceProvider;
public String getRealmId() {
return realmId;
}
public void setServiceProvider(ComponentEntity serviceProvider) {
this.serviceProvider = serviceProvider;
public void setRealmId(String realmId) {
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() {
@ -65,19 +75,4 @@ public class ScimResource {
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;
public class ScimResourceId implements Serializable {
private String realm;
private String serviceProvider;
private String id;
private String realmId;
private String componentId;
private String type;
private String remoteId;
private String externalId;
public ScimResourceId() {
}
public ScimResourceId(String realm, String serviceProvider, String type, String remoteId) {
this.realm = realm;
this.serviceProvider = serviceProvider;
this.type = type;
this.remoteId = remoteId;
public ScimResourceId(String id, String realmId, String componentId, String type, String externalId) {
this.setId(id);
this.setRealmId(realmId);
this.setComponentId(componentId);
this.setType(type);
this.setExternalId(externalId);
}
public String getRealm() {
return realm;
public String getId() {
return id;
}
public void setRealm(String realm) {
this.realm = realm;
public void setId(String id) {
this.id = id;
}
public String getServiceProvider() {
return serviceProvider;
public String getRealmId() {
return realmId;
}
public void setServiceProvider(String serviceProvider) {
this.serviceProvider = serviceProvider;
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public String getComponentId() {
return componentId;
}
public void setComponentId(String componentId) {
this.componentId = componentId;
}
public String getType() {
@ -43,12 +53,12 @@ public class ScimResourceId implements Serializable {
this.type = type;
}
public String getRemoteId() {
return remoteId;
public String getExternalId() {
return externalId;
}
public void setRemoteId(String remoteId) {
this.remoteId = remoteId;
public void setExternalId(String externalId) {
this.externalId = externalId;
}
@Override
@ -58,14 +68,15 @@ public class ScimResourceId implements Serializable {
if (!(other instanceof ScimResourceId))
return false;
var o = (ScimResourceId) other;
return (o.realm == realm &&
o.serviceProvider == serviceProvider &&
return (o.id == id &&
o.realmId == realmId &&
o.componentId == componentId &&
o.type == type &&
o.remoteId == remoteId);
o.externalId == externalId);
}
@Override
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;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
public class ScimStorageProvider implements UserStorageProvider, UserRegistrationProvider {
public class ScimStorageProvider implements UserStorageProvider {
@Override
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;
import java.util.Date;
import java.util.List;
import javax.ws.rs.core.MediaType;
import com.unboundid.scim2.client.ScimService;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
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.ProviderConfigurationBuilder;
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";
protected static final List<ProviderConfigProperty> configMetadata;
static {
@ -46,11 +59,30 @@ public class ScimStorageProviderFactory implements UserStorageProviderFactory<Sc
.label("Bearer token")
.helpText("Add a bearer token in the authorization header")
.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();
}
@Override
public ScimStorageProvider create(KeycloakSession session, ComponentModel model) {
LOGGER.info("create");
return new ScimStorageProvider();
}
@ -64,4 +96,36 @@ public class ScimStorageProviderFactory implements UserStorageProviderFactory<Sc
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">
<createTable tableName="SCIM_RESOURCE">
<column name="REALM_ID" type="VARCHAR(36)">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false" />
</column>
<column name="SERVICE_PROVIDER" type="VARCHAR(36)">
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false" />
</column>
<column name="TYPE" type="VARCHAR(36)">
<constraints nullable="false" />
</column>
<column name="REMOTE_ID" type="VARCHAR(36)">
<column name="COMPONENT_ID" type="VARCHAR(36)">
<constraints nullable="false" />
</column>
<column name="LOCAL_ID" type="VARCHAR(36)">
<column name="EXTERNAL_ID" type="VARCHAR(36)">
<constraints nullable="false" />
</column>
</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="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>
</databaseChangeLog>