chore: switch scim sdk

This commit is contained in:
Marc Portabella 2023-01-22 12:20:33 +01:00 committed by Hugo Renard
parent 19e23a44f4
commit 7163d2a0f0
Signed by: hougo
GPG key ID: 3A285FD470209C59
10 changed files with 178 additions and 149 deletions

4
auto.sh Executable file
View file

@ -0,0 +1,4 @@
gradle jar shadowjar
scp build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar root@192.168.130.252:/var/www/html/keycloak-scim-1.0-SNAPSHOT-all.jar
scp build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar root@192.168.130.252:/var/www/html/keycloak-scim-aws-1.0-SNAPSHOT-all.jar
k delete pod keycloak-keycloakx-0 -n keycloak

View file

@ -21,20 +21,14 @@ dependencies {
compileOnly 'org.keycloak:keycloak-services:18.0.0' compileOnly 'org.keycloak:keycloak-services:18.0.0'
compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0' compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0'
implementation 'io.github.resilience4j:resilience4j-retry:1.7.1' implementation 'io.github.resilience4j:resilience4j-retry:1.7.1'
implementation('com.unboundid.product.scim2:scim2-sdk-client:2.3.7') { implementation 'de.captaingoldfish:scim-sdk-common:1.15.3'
transitive false
} implementation files('/home/marcportabella/Documents/totmicro/Repos/keycloak-scim-aws/scim-sdk-client-1.15.4-SNAPSHOT.jar')
implementation('com.unboundid.product.scim2:scim2-sdk-common:2.3.7') { //implementation 'de.captaingoldfish:scim-sdk-client:1.15.3'
transitive false
}
implementation('org.wildfly.client:wildfly-client-config:1.0.1.Final') { implementation('org.wildfly.client:wildfly-client-config:1.0.1.Final') {
transitive false transitive false
} }
implementation('org.jboss.resteasy:resteasy-client:4.7.6.Final') { implementation('org.jboss.resteasy:resteasy-client:4.7.6.Final') {
transitive false transitive false
} }
implementation('org.jboss.resteasy:resteasy-client-api:4.7.6.Final') {
transitive false
}
} }

View file

@ -21,12 +21,7 @@ dependencies {
compileOnly 'org.keycloak:keycloak-services:18.0.0' compileOnly 'org.keycloak:keycloak-services:18.0.0'
compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0' compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0'
implementation 'io.github.resilience4j:resilience4j-retry:1.7.1' implementation 'io.github.resilience4j:resilience4j-retry:1.7.1'
implementation('com.unboundid.product.scim2:scim2-sdk-client:2.3.7') {
transitive false
}
implementation('com.unboundid.product.scim2:scim2-sdk-common:2.3.7') {
transitive false
}
compileOnly 'org.wildfly.client:wildfly-client-config:1.0.1.Final' compileOnly 'org.wildfly.client:wildfly-client-config:1.0.1.Final'
compileOnly 'org.jboss.resteasy:resteasy-client:4.7.6.Final' compileOnly 'org.jboss.resteasy:resteasy-client:4.7.6.Final'
compileOnly 'org.jboss.resteasy:resteasy-client-api:4.7.6.Final' compileOnly 'org.jboss.resteasy:resteasy-client-api:4.7.6.Final'

View file

@ -14,7 +14,9 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleMapperModel; import org.keycloak.models.RoleMapperModel;
import sh.libre.scim.jpa.ScimResource; import sh.libre.scim.jpa.ScimResource;
public abstract class Adapter<M extends RoleMapperModel, S extends com.unboundid.scim2.common.ScimResource> { import de.captaingoldfish.scim.sdk.common.resources.ResourceNode;
public abstract class Adapter<M extends RoleMapperModel, S extends ResourceNode> {
protected final Logger LOGGER; protected final Logger LOGGER;
protected final String realmId; protected final String realmId;

View file

@ -1,23 +0,0 @@
package sh.libre.scim.core;
import java.io.IOException;
import java.util.Base64;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
public class BasicAuthentication implements ClientRequestFilter {
private final String user;
private final String password;
BasicAuthentication(String user, String password) {
this.user = user;
this.password = password;
}
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
var token = Base64.getEncoder().encodeToString((user + ":" + password).getBytes());
requestContext.getHeaders().add("Authorization", "Basic " + token);
}
}

View file

@ -1,20 +0,0 @@
package sh.libre.scim.core;
import java.io.IOException;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
public class BearerAuthentication implements ClientRequestFilter {
private final String token;
BearerAuthentication(String token) {
this.token = token;
}
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add("Authorization", "Bearer " + this.token);
}
}

View file

@ -7,19 +7,16 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import de.captaingoldfish.scim.sdk.common.resources.Group;
import com.unboundid.scim2.common.types.GroupResource; import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Member;
import com.unboundid.scim2.common.types.Member; import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
import com.unboundid.scim2.common.types.Meta;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
public class GroupAdapter extends Adapter<GroupModel, GroupResource> { public class GroupAdapter extends Adapter<GroupModel, Group> {
private String displayName; private String displayName;
private Set<String> members = new HashSet<String>(); private Set<String> members = new HashSet<String>();
@ -39,8 +36,8 @@ public class GroupAdapter extends Adapter<GroupModel, GroupResource> {
} }
@Override @Override
public Class<GroupResource> getResourceClass() { public Class<Group> getResourceClass() {
return GroupResource.class; return Group.class;
} }
@Override @Override
@ -55,14 +52,14 @@ public class GroupAdapter extends Adapter<GroupModel, GroupResource> {
} }
@Override @Override
public void apply(GroupResource group) { public void apply(Group group) {
setExternalId(group.getId()); setExternalId(group.getId().get());
setDisplayName(group.getDisplayName()); setDisplayName(group.getDisplayName().get());
var groupMembers = group.getMembers(); var groupMembers = group.getMembers();
if (groupMembers != null && groupMembers.size() > 0) { if (groupMembers != null && groupMembers.size() > 0) {
this.members = new HashSet<String>(); this.members = new HashSet<String>();
for (var groupMember : groupMembers) { for (var groupMember : groupMembers) {
var userMapping = this.query("findByExternalId", groupMember.getValue(), "User") var userMapping = this.query("findByExternalId", groupMember.getValue().get(), "User")
.getSingleResult(); .getSingleResult();
this.members.add(userMapping.getId()); this.members.add(userMapping.getId());
} }
@ -70,8 +67,8 @@ public class GroupAdapter extends Adapter<GroupModel, GroupResource> {
} }
@Override @Override
public GroupResource toSCIM(Boolean addMeta) { public Group toSCIM(Boolean addMeta) {
var group = new GroupResource(); var group = new Group();
group.setId(externalId); group.setId(externalId);
group.setExternalId(id); group.setExternalId(id);
group.setDisplayName(displayName); group.setDisplayName(displayName);
@ -83,7 +80,7 @@ public class GroupAdapter extends Adapter<GroupModel, GroupResource> {
var userMapping = this.query("findById", member, "User").getSingleResult(); var userMapping = this.query("findById", member, "User").getSingleResult();
groupMember.setValue(userMapping.getExternalId()); groupMember.setValue(userMapping.getExternalId());
var ref = new URI(String.format("Users/%s", userMapping.getExternalId())); var ref = new URI(String.format("Users/%s", userMapping.getExternalId()));
groupMember.setRef(ref); groupMember.setRef(ref.toString());
groupMembers.add(groupMember); groupMembers.add(groupMember);
} catch (Exception e) { } catch (Exception e) {
LOGGER.error(e); LOGGER.error(e);
@ -95,7 +92,7 @@ public class GroupAdapter extends Adapter<GroupModel, GroupResource> {
var meta = new Meta(); var meta = new Meta();
try { try {
var uri = new URI("Groups/" + externalId); var uri = new URI("Groups/" + externalId);
meta.setLocation(uri); meta.setLocation(uri.toString());
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
} }
group.setMeta(meta); group.setMeta(meta);

View file

@ -1,61 +1,108 @@
package sh.libre.scim.core; package sh.libre.scim.core;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import javax.ws.rs.ProcessingException; import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import com.unboundid.scim2.client.ScimService; import de.captaingoldfish.scim.sdk.client.ScimClientConfig;
import com.unboundid.scim2.common.ScimResource; import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder;
import com.unboundid.scim2.common.exceptions.ScimException; import de.captaingoldfish.scim.sdk.client.http.BasicAuth;
import de.captaingoldfish.scim.sdk.client.response.ServerResponse;
import de.captaingoldfish.scim.sdk.common.exceptions.ResponseException;
import de.captaingoldfish.scim.sdk.common.resources.ResourceNode;
import de.captaingoldfish.scim.sdk.common.response.ListResponse;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
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.RoleMapperModel; import org.keycloak.models.RoleMapperModel;
import org.keycloak.storage.user.SynchronizationResult; import org.keycloak.storage.user.SynchronizationResult;
import com.google.common.net.HttpHeaders;
import io.github.resilience4j.core.IntervalFunction; import io.github.resilience4j.core.IntervalFunction;
import io.github.resilience4j.retry.RetryConfig; import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry; import io.github.resilience4j.retry.RetryRegistry;
public class ScimClient { public class ScimClient {
final protected Logger LOGGER = Logger.getLogger(ScimClient.class); final protected Logger LOGGER = Logger.getLogger(ScimClient.class);
final protected Client client = ResteasyClientBuilder.newClient(); final protected ScimRequestBuilder scimRequestBuilder;
final protected ScimService scimService;
final protected RetryRegistry registry; final protected RetryRegistry registry;
final protected KeycloakSession session; final protected KeycloakSession session;
final protected String contentType; final protected String contentType;
final protected ComponentModel model; final protected ComponentModel model;
final protected String scimApplicationBaseUrl;
final protected Map<String, String> defaultHeaders;
final protected Map<String, String> expectedResponseHeaders;
public ScimClient(ComponentModel model, KeycloakSession session) { public ScimClient(ComponentModel model, KeycloakSession session) {
this.model = model; this.model = model;
this.contentType = model.get("content-type"); this.contentType = model.get("content-type");
this.session = session; this.session = session;
var target = client.target(model.get("endpoint")); this.scimApplicationBaseUrl = model.get("endpoint");
this.defaultHeaders = new HashMap<>();
this.expectedResponseHeaders = new HashMap<>();
switch (model.get("auth-mode")) { switch (model.get("auth-mode")) {
case "BEARER": case "BEARER":
target = target.register(new BearerAuthentication(model.get("auth-pass"))); defaultHeaders.put(HttpHeaders.AUTHORIZATION,
BearerAuthentication(model.get("auth-pass")));
break; break;
case "BASIC_AUTH": case "BASIC_AUTH":
target = target.register(new BasicAuthentication( defaultHeaders.put(HttpHeaders.AUTHORIZATION,
model.get("auth-user"), BasicAuthentication(model.get("auth-user"),
model.get("auth-pass"))); model.get("auth-pass")));
break;
} }
scimService = new ScimService(target); defaultHeaders.put(HttpHeaders.CONTENT_TYPE,contentType);
scimRequestBuilder = new ScimRequestBuilder(scimApplicationBaseUrl, genScimClientConfig());
RetryConfig retryConfig = RetryConfig.custom() RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(10) .maxAttempts(10)
.intervalFunction(IntervalFunction.ofExponentialBackoff()) .intervalFunction(IntervalFunction.ofExponentialBackoff())
.retryExceptions(ProcessingException.class) .retryExceptions(ProcessingException.class)
.build(); .build();
registry = RetryRegistry.of(retryConfig); registry = RetryRegistry.of(retryConfig);
} }
protected String BasicAuthentication(String username ,String password) {
return BasicAuth.builder()
.username(model.get(username))
.password(model.get(password))
.build()
.getAuthorizationHeaderValue();
}
protected ScimClientConfig genScimClientConfig() {
return ScimClientConfig.builder()
.httpHeaders(defaultHeaders)
.connectTimeout(5)
.requestTimeout(5)
.socketTimeout(5)
.expectedHttpResponseHeaders(expectedResponseHeaders)
.hostnameVerifier((s, sslSession) -> true)
.build();
}
protected String BearerAuthentication(String token) {
return "Bearer " + token ;
}
protected String genScimUrl(String scimEndpoint,String resourcePath) {
return String.format("%s%s/%s", scimApplicationBaseUrl ,
scimEndpoint,
resourcePath);
}
protected EntityManager getEM() { protected EntityManager getEM() {
return session.getProvider(JpaConnectionProvider.class).getEntityManager(); return session.getProvider(JpaConnectionProvider.class).getEntityManager();
} }
@ -64,7 +111,7 @@ public class ScimClient {
return session.getContext().getRealm().getId(); return session.getContext().getRealm().getId();
} }
protected <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> A getAdapter( protected <M extends RoleMapperModel, S extends ResourceNode, A extends Adapter<M, S>> A getAdapter(
Class<A> aClass) { Class<A> aClass) {
try { try {
return aClass.getDeclaredConstructor(KeycloakSession.class, String.class) return aClass.getDeclaredConstructor(KeycloakSession.class, String.class)
@ -74,7 +121,7 @@ public class ScimClient {
} }
} }
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void create(Class<A> aClass, public <M extends RoleMapperModel, S extends ResourceNode, A extends Adapter<M, S>> void create(Class<A> aClass,
M kcModel) { M kcModel) {
var adapter = getAdapter(aClass); var adapter = getAdapter(aClass);
adapter.apply(kcModel); adapter.apply(kcModel);
@ -85,21 +132,28 @@ public class ScimClient {
return; return;
} }
var retry = registry.retry("create-" + adapter.getId()); var retry = registry.retry("create-" + adapter.getId());
var resource = retry.executeSupplier(() -> {
ServerResponse<S> response = retry.executeSupplier(() -> {
try { try {
return scimService.createRequest(adapter.getSCIMEndpoint(), return scimRequestBuilder
adapter.toSCIM(false)) .create(adapter.getResourceClass(), String.format("/" + adapter.getSCIMEndpoint()))
.contentType(contentType).invoke(); .setResource(adapter.toSCIM(false))
} catch (ScimException e) { .sendRequest();
} catch ( ResponseException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
adapter.apply(resource);
adapter.saveMapping();
if (!response.isSuccess()){
LOGGER.warn(response.getResponseBody());
LOGGER.warn(response.getHttpStatus());
}
adapter.apply(response.getResource());
adapter.saveMapping();
}; };
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void replace(Class<A> aClass, public <M extends RoleMapperModel, S extends ResourceNode, A extends Adapter<M, S>> void replace(Class<A> aClass,
M kcModel) { M kcModel) {
var adapter = getAdapter(aClass); var adapter = getAdapter(aClass);
try { try {
@ -109,13 +163,22 @@ public class ScimClient {
var resource = adapter.query("findById", adapter.getId()).getSingleResult(); var resource = adapter.query("findById", adapter.getId()).getSingleResult();
adapter.apply(resource); adapter.apply(resource);
var retry = registry.retry("replace-" + adapter.getId()); var retry = registry.retry("replace-" + adapter.getId());
retry.executeSupplier(() -> { ServerResponse<S> response = retry.executeSupplier(() -> {
try { try {
return scimService.replaceRequest(adapter.toSCIM(true)).contentType(contentType).invoke(); return scimRequestBuilder
} catch (ScimException e) { .update(genScimUrl(adapter.getSCIMEndpoint(), adapter.getExternalId()),
adapter.getResourceClass())
.setResource(adapter.toSCIM(false))
.sendRequest() ;
} catch ( ResponseException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
if (!response.isSuccess()){
LOGGER.warn(response.getResponseBody());
LOGGER.warn(response.getHttpStatus());
}
} catch (NoResultException e) { } catch (NoResultException e) {
LOGGER.warnf("failed to replace resource %s, scim mapping not found", adapter.getId()); LOGGER.warnf("failed to replace resource %s, scim mapping not found", adapter.getId());
} catch (Exception e) { } catch (Exception e) {
@ -123,30 +186,40 @@ public class ScimClient {
} }
} }
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void delete(Class<A> aClass, public <M extends RoleMapperModel, S extends ResourceNode, A extends Adapter<M, S>> void delete(Class<A> aClass,
String id) { String id) {
var adapter = getAdapter(aClass); var adapter = getAdapter(aClass);
adapter.setId(id); adapter.setId(id);
try { try {
var resource = adapter.query("findById", adapter.getId()).getSingleResult(); var resource = adapter.query("findById", adapter.getId()).getSingleResult();
adapter.apply(resource); adapter.apply(resource);
var retry = registry.retry("delete-" + id); var retry = registry.retry("delete-" + id);
retry.executeSupplier(() -> {
ServerResponse<S> response = retry.executeSupplier(() -> {
try { try {
scimService.deleteRequest(adapter.getSCIMEndpoint(), resource.getExternalId()) return scimRequestBuilder.delete(genScimUrl(adapter.getSCIMEndpoint(), adapter.getExternalId()),
.contentType(contentType).invoke(); adapter.getResourceClass())
} catch (ScimException e) { .sendRequest();
} catch (ResponseException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
return "";
}); });
if (!response.isSuccess()){
LOGGER.warn(response.getResponseBody());
LOGGER.warn(response.getHttpStatus());
}
getEM().remove(resource); getEM().remove(resource);
} catch (NoResultException e) { } catch (NoResultException e) {
LOGGER.warnf("Failed to delete resource %s, scim mapping not found", id); LOGGER.warnf("Failed to delete resource %s, scim mapping not found", id);
} }
} }
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void refreshResources( public <M extends RoleMapperModel, S extends ResourceNode, A extends Adapter<M, S>> void refreshResources(
Class<A> aClass, Class<A> aClass,
SynchronizationResult syncRes) { SynchronizationResult syncRes) {
LOGGER.info("Refresh resources"); LOGGER.info("Refresh resources");
@ -169,16 +242,17 @@ public class ScimClient {
} }
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void importResources( public <M extends RoleMapperModel, S extends ResourceNode, A extends Adapter<M, S>> void importResources(
Class<A> aClass, SynchronizationResult syncRes) { Class<A> aClass, SynchronizationResult syncRes) {
LOGGER.info("Import"); LOGGER.info("Import");
try { try {
var adapter = getAdapter(aClass); var adapter = getAdapter(aClass);
var resources = scimService.searchRequest(adapter.getSCIMEndpoint()).contentType(contentType) ServerResponse<ListResponse<S>> response = scimRequestBuilder.list("url",adapter.getResourceClass()).get().sendRequest();
.invoke(adapter.getResourceClass()); ListResponse<S> resourceTypeListResponse = response.getResource();
for (var resource : resources) {
for (var resource : resourceTypeListResponse.getListedResources()) {
try { try {
LOGGER.infof("Reconciling remote resource %s", resource.getId()); LOGGER.infof("Reconciling remote resource %s", resource);
adapter = getAdapter(aClass); adapter = getAdapter(aClass);
adapter.apply(resource); adapter.apply(resource);
@ -212,9 +286,11 @@ public class ScimClient {
break; break;
case "DELETE_REMOTE": case "DELETE_REMOTE":
LOGGER.info("Delete remote resource"); LOGGER.info("Delete remote resource");
scimService.deleteRequest(adapter.getSCIMEndpoint(), resource.getId()) scimRequestBuilder
.contentType(contentType) .delete(genScimUrl(adapter.getSCIMEndpoint(),
.invoke(); resource.getId().get()),
adapter.getResourceClass())
.sendRequest();
syncRes.increaseRemoved(); syncRes.increaseRemoved();
break; break;
} }
@ -225,12 +301,12 @@ public class ScimClient {
syncRes.increaseFailed(); syncRes.increaseFailed();
} }
} }
} catch (ScimException e) { } catch (ResponseException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void sync(Class<A> aClass, public <M extends RoleMapperModel, S extends ResourceNode, A extends Adapter<M, S>> void sync(Class<A> aClass,
SynchronizationResult syncRes) { SynchronizationResult syncRes) {
if (this.model.get("sync-import", false)) { if (this.model.get("sync-import", false)) {
this.importResources(aClass, syncRes); this.importResources(aClass, syncRes);
@ -241,6 +317,6 @@ public class ScimClient {
} }
public void close() { public void close() {
client.close(); scimRequestBuilder.close();
} }
} }

View file

@ -7,17 +7,19 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.unboundid.scim2.common.types.Email; import de.captaingoldfish.scim.sdk.common.resources.User;
import com.unboundid.scim2.common.types.Meta; import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Email;
import com.unboundid.scim2.common.types.Role; import de.captaingoldfish.scim.sdk.common.resources.complex.Name;
import com.unboundid.scim2.common.types.UserResource; import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole;
import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
public class UserAdapter extends Adapter<UserModel, UserResource> { public class UserAdapter extends Adapter<UserModel, User> {
private String username; private String username;
private String displayName; private String displayName;
@ -78,8 +80,8 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
} }
@Override @Override
public Class<UserResource> getResourceClass() { public Class<User> getResourceClass() {
return UserResource.class; return User.class;
} }
@Override @Override
@ -114,27 +116,29 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
} }
@Override @Override
public void apply(UserResource user) { public void apply(User user) {
setExternalId(user.getId()); setExternalId(user.getId().get());
setUsername(user.getUserName()); setUsername(user.getUserName().get());
setDisplayName(user.getDisplayName()); setDisplayName(user.getDisplayName().get());
setActive(user.getActive()); setActive(user.isActive().get());
if (user.getEmails().size() > 0) { if (user.getEmails().size() > 0) {
setEmail(user.getEmails().get(0).getValue()); setEmail(user.getEmails().get(0).getValue().get());
} }
} }
@Override @Override
public UserResource toSCIM(Boolean addMeta) { public User toSCIM(Boolean addMeta) {
var user = new UserResource(); var user = new User();
user.setExternalId(id); user.setExternalId(id);
user.setUserName(username); user.setUserName(username);
user.setId(externalId); user.setId(externalId);
user.setDisplayName(displayName); user.setDisplayName(displayName);
Name name = new Name();
user.setName(name);
var emails = new ArrayList<Email>(); var emails = new ArrayList<Email>();
if (email != null) { if (email != null) {
emails.add( emails.add(
new Email().setPrimary(true).setValue(email)); Email.builder().value(getEmail()).build());
} }
user.setEmails(emails); user.setEmails(emails);
user.setActive(active); user.setActive(active);
@ -142,14 +146,14 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
var meta = new Meta(); var meta = new Meta();
try { try {
var uri = new URI("Users/" + externalId); var uri = new URI("Users/" + externalId);
meta.setLocation(uri); meta.setLocation(uri.toString());
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
} }
user.setMeta(meta); user.setMeta(meta);
} }
List<Role> roles = new ArrayList<Role>(); List<PersonRole> roles = new ArrayList<PersonRole>();
for (var r : this.roles) { for (var r : this.roles) {
var role = new Role(); var role = new PersonRole();
role.setValue(r); role.setValue(r);
roles.add(role); roles.add(role);
} }

View file

@ -5,8 +5,6 @@ import java.util.List;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import com.unboundid.scim2.client.ScimService;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -24,6 +22,8 @@ import sh.libre.scim.core.GroupAdapter;
import sh.libre.scim.core.ScimDispatcher; import sh.libre.scim.core.ScimDispatcher;
import sh.libre.scim.core.UserAdapter; import sh.libre.scim.core.UserAdapter;
import de.captaingoldfish.scim.sdk.common.constants.HttpHeader;
public class ScimStorageProviderFactory public class ScimStorageProviderFactory
implements UserStorageProviderFactory<ScimStorageProvider>, ImportSynchronization { implements UserStorageProviderFactory<ScimStorageProvider>, ImportSynchronization {
final private Logger LOGGER = Logger.getLogger(ScimStorageProviderFactory.class); final private Logger LOGGER = Logger.getLogger(ScimStorageProviderFactory.class);
@ -43,8 +43,8 @@ public class ScimStorageProviderFactory
.type(ProviderConfigProperty.LIST_TYPE) .type(ProviderConfigProperty.LIST_TYPE)
.label("Endpoint content type") .label("Endpoint content type")
.helpText("Only used when endpoint doesn't support application/scim+json") .helpText("Only used when endpoint doesn't support application/scim+json")
.options(MediaType.APPLICATION_JSON.toString(), ScimService.MEDIA_TYPE_SCIM_TYPE.toString()) .options(MediaType.APPLICATION_JSON.toString(), HttpHeader.SCIM_CONTENT_TYPE)
.defaultValue(ScimService.MEDIA_TYPE_SCIM_TYPE.toString()) .defaultValue(HttpHeader.SCIM_CONTENT_TYPE)
.add() .add()
.property() .property()
.name("auth-mode") .name("auth-mode")