chore: switch scim sdk
This commit is contained in:
parent
19e23a44f4
commit
7163d2a0f0
10 changed files with 178 additions and 149 deletions
4
auto.sh
Executable file
4
auto.sh
Executable 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
|
14
build.gradle
14
build.gradle
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in a new issue