refactoring using generics + sync feature
This commit is contained in:
parent
95c6c5588f
commit
f22e23742d
9 changed files with 563 additions and 197 deletions
110
src/main/java/sh/libre/scim/core/Adapter.java
Normal file
110
src/main/java/sh/libre/scim/core/Adapter.java
Normal 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();
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
|
166
src/main/java/sh/libre/scim/core/UserAdapter.java
Normal file
166
src/main/java/sh/libre/scim/core/UserAdapter.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue