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

View file

@ -1,61 +1,108 @@
package sh.libre.scim.core;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.ws.rs.ProcessingException;
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 de.captaingoldfish.scim.sdk.client.ScimClientConfig;
import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder;
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.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RoleMapperModel;
import org.keycloak.storage.user.SynchronizationResult;
import com.google.common.net.HttpHeaders;
import io.github.resilience4j.core.IntervalFunction;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
public class ScimClient {
final protected Logger LOGGER = Logger.getLogger(ScimClient.class);
final protected Client client = ResteasyClientBuilder.newClient();
final protected ScimService scimService;
final protected ScimRequestBuilder scimRequestBuilder;
final protected RetryRegistry registry;
final protected KeycloakSession session;
final protected String contentType;
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) {
this.model = model;
this.contentType = model.get("content-type");
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")) {
case "BEARER":
target = target.register(new BearerAuthentication(model.get("auth-pass")));
defaultHeaders.put(HttpHeaders.AUTHORIZATION,
BearerAuthentication(model.get("auth-pass")));
break;
case "BASIC_AUTH":
target = target.register(new BasicAuthentication(
model.get("auth-user"),
defaultHeaders.put(HttpHeaders.AUTHORIZATION,
BasicAuthentication(model.get("auth-user"),
model.get("auth-pass")));
break;
}
scimService = new ScimService(target);
defaultHeaders.put(HttpHeaders.CONTENT_TYPE,contentType);
scimRequestBuilder = new ScimRequestBuilder(scimApplicationBaseUrl, genScimClientConfig());
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(10)
.intervalFunction(IntervalFunction.ofExponentialBackoff())
.retryExceptions(ProcessingException.class)
.build();
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() {
return session.getProvider(JpaConnectionProvider.class).getEntityManager();
}
@ -64,7 +111,7 @@ public class ScimClient {
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) {
try {
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) {
var adapter = getAdapter(aClass);
adapter.apply(kcModel);
@ -85,21 +132,28 @@ public class ScimClient {
return;
}
var retry = registry.retry("create-" + adapter.getId());
var resource = retry.executeSupplier(() -> {
ServerResponse<S> response = retry.executeSupplier(() -> {
try {
return scimService.createRequest(adapter.getSCIMEndpoint(),
adapter.toSCIM(false))
.contentType(contentType).invoke();
} catch (ScimException e) {
return scimRequestBuilder
.create(adapter.getResourceClass(), String.format("/" + adapter.getSCIMEndpoint()))
.setResource(adapter.toSCIM(false))
.sendRequest();
} catch ( ResponseException 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) {
var adapter = getAdapter(aClass);
try {
@ -109,13 +163,22 @@ public class ScimClient {
var resource = adapter.query("findById", adapter.getId()).getSingleResult();
adapter.apply(resource);
var retry = registry.retry("replace-" + adapter.getId());
retry.executeSupplier(() -> {
ServerResponse<S> response = retry.executeSupplier(() -> {
try {
return scimService.replaceRequest(adapter.toSCIM(true)).contentType(contentType).invoke();
} catch (ScimException e) {
return scimRequestBuilder
.update(genScimUrl(adapter.getSCIMEndpoint(), adapter.getExternalId()),
adapter.getResourceClass())
.setResource(adapter.toSCIM(false))
.sendRequest() ;
} catch ( ResponseException e) {
throw new RuntimeException(e);
}
});
if (!response.isSuccess()){
LOGGER.warn(response.getResponseBody());
LOGGER.warn(response.getHttpStatus());
}
} catch (NoResultException e) {
LOGGER.warnf("failed to replace resource %s, scim mapping not found", adapter.getId());
} 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) {
var adapter = getAdapter(aClass);
adapter.setId(id);
try {
var resource = adapter.query("findById", adapter.getId()).getSingleResult();
adapter.apply(resource);
var retry = registry.retry("delete-" + id);
retry.executeSupplier(() -> {
ServerResponse<S> response = retry.executeSupplier(() -> {
try {
scimService.deleteRequest(adapter.getSCIMEndpoint(), resource.getExternalId())
.contentType(contentType).invoke();
} catch (ScimException e) {
return scimRequestBuilder.delete(genScimUrl(adapter.getSCIMEndpoint(), adapter.getExternalId()),
adapter.getResourceClass())
.sendRequest();
} catch (ResponseException e) {
throw new RuntimeException(e);
}
return "";
});
if (!response.isSuccess()){
LOGGER.warn(response.getResponseBody());
LOGGER.warn(response.getHttpStatus());
}
getEM().remove(resource);
} catch (NoResultException e) {
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,
SynchronizationResult syncRes) {
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) {
LOGGER.info("Import");
try {
var adapter = getAdapter(aClass);
var resources = scimService.searchRequest(adapter.getSCIMEndpoint()).contentType(contentType)
.invoke(adapter.getResourceClass());
for (var resource : resources) {
ServerResponse<ListResponse<S>> response = scimRequestBuilder.list("url",adapter.getResourceClass()).get().sendRequest();
ListResponse<S> resourceTypeListResponse = response.getResource();
for (var resource : resourceTypeListResponse.getListedResources()) {
try {
LOGGER.infof("Reconciling remote resource %s", resource.getId());
LOGGER.infof("Reconciling remote resource %s", resource);
adapter = getAdapter(aClass);
adapter.apply(resource);
@ -212,9 +286,11 @@ public class ScimClient {
break;
case "DELETE_REMOTE":
LOGGER.info("Delete remote resource");
scimService.deleteRequest(adapter.getSCIMEndpoint(), resource.getId())
.contentType(contentType)
.invoke();
scimRequestBuilder
.delete(genScimUrl(adapter.getSCIMEndpoint(),
resource.getId().get()),
adapter.getResourceClass())
.sendRequest();
syncRes.increaseRemoved();
break;
}
@ -225,12 +301,12 @@ public class ScimClient {
syncRes.increaseFailed();
}
}
} catch (ScimException e) {
} catch (ResponseException 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) {
if (this.model.get("sync-import", false)) {
this.importResources(aClass, syncRes);
@ -241,6 +317,6 @@ public class ScimClient {
}
public void close() {
client.close();
scimRequestBuilder.close();
}
}

View file

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

View file

@ -5,8 +5,6 @@ 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;
@ -24,6 +22,8 @@ import sh.libre.scim.core.GroupAdapter;
import sh.libre.scim.core.ScimDispatcher;
import sh.libre.scim.core.UserAdapter;
import de.captaingoldfish.scim.sdk.common.constants.HttpHeader;
public class ScimStorageProviderFactory
implements UserStorageProviderFactory<ScimStorageProvider>, ImportSynchronization {
final private Logger LOGGER = Logger.getLogger(ScimStorageProviderFactory.class);
@ -43,8 +43,8 @@ public class ScimStorageProviderFactory
.type(ProviderConfigProperty.LIST_TYPE)
.label("Endpoint content type")
.helpText("Only used when endpoint doesn't support application/scim+json")
.options(MediaType.APPLICATION_JSON.toString(), ScimService.MEDIA_TYPE_SCIM_TYPE.toString())
.defaultValue(ScimService.MEDIA_TYPE_SCIM_TYPE.toString())
.options(MediaType.APPLICATION_JSON.toString(), HttpHeader.SCIM_CONTENT_TYPE)
.defaultValue(HttpHeader.SCIM_CONTENT_TYPE)
.add()
.property()
.name("auth-mode")