diff --git a/build.gradle b/build.gradle index def33a3531d5de04e3e9553d3668a10e1cb0b9d7..e27b363f0df4daced2dde65a34578075e7a076a8 100644 --- a/build.gradle +++ b/build.gradle @@ -10,31 +10,20 @@ description = 'keycloak-scim' java.sourceCompatibility = JavaVersion.VERSION_11 repositories { - mavenLocal() mavenCentral() } dependencies { - compileOnly 'org.keycloak:keycloak-core:18.0.0' - compileOnly 'org.keycloak:keycloak-server-spi:18.0.0' - compileOnly 'org.keycloak:keycloak-server-spi-private:18.0.0' - compileOnly 'org.keycloak:keycloak-services:18.0.0' - compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0' + compileOnly 'org.keycloak:keycloak-core:20.0.0' + compileOnly 'org.keycloak:keycloak-server-spi:20.0.0' + compileOnly 'org.keycloak:keycloak-server-spi-private:20.0.0' + compileOnly 'org.keycloak:keycloak-services:20.0.0' + compileOnly 'org.keycloak:keycloak-model-jpa:20.0.0' 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.16.0') { transitive false } - implementation('com.unboundid.product.scim2:scim2-sdk-common:2.3.7') { + implementation ('de.captaingoldfish:scim-sdk-client:1.16.0') { transitive false } - 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 - } - } diff --git a/legacy-build.gradle b/legacy-build.gradle deleted file mode 100644 index c9318f4a09d0b400a0a4fdc809a79abcbb57742b..0000000000000000000000000000000000000000 --- a/legacy-build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -plugins { - id 'java' - id 'com.github.johnrengelman.shadow' version '7.1.2' -} - -group = 'sh.libre.scim' -version = '1.0-SNAPSHOT' -description = 'keycloak-scim' - -java.sourceCompatibility = JavaVersion.VERSION_11 - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - compileOnly 'org.keycloak:keycloak-core:18.0.0' - compileOnly 'org.keycloak:keycloak-server-spi:18.0.0' - compileOnly 'org.keycloak:keycloak-server-spi-private:18.0.0' - 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' - -} - -shadowJar { - archiveClassifier.set('all-legacy') -} diff --git a/src/main/java/sh/libre/scim/core/Adapter.java b/src/main/java/sh/libre/scim/core/Adapter.java index f7581a1beca57c12adde567a6ccc8fcdc6e4837d..8faac5a3a70688570f725e2afc7d68e3b9b6b49a 100644 --- a/src/main/java/sh/libre/scim/core/Adapter.java +++ b/src/main/java/sh/libre/scim/core/Adapter.java @@ -14,7 +14,9 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleMapperModel; import sh.libre.scim.jpa.ScimResource; -public abstract class Adapter { +import de.captaingoldfish.scim.sdk.common.resources.ResourceNode; + +public abstract class Adapter { protected final Logger LOGGER; protected final String realmId; diff --git a/src/main/java/sh/libre/scim/core/BasicAuthentication.java b/src/main/java/sh/libre/scim/core/BasicAuthentication.java deleted file mode 100644 index 227681d2d2ed3e5c283e3fe220eb1b7a53ff5aae..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/BasicAuthentication.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/sh/libre/scim/core/BearerAuthentication.java b/src/main/java/sh/libre/scim/core/BearerAuthentication.java deleted file mode 100644 index 2b4133d8856c5ca29a427764b69ae736a6f6f930..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/BearerAuthentication.java +++ /dev/null @@ -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); - - } -} diff --git a/src/main/java/sh/libre/scim/core/GroupAdapter.java b/src/main/java/sh/libre/scim/core/GroupAdapter.java index 33039c5904217721a4225e33c6fbde0758fca968..86800606ca4c90291bfaaf91b2cd6a8b77f0f1c5 100644 --- a/src/main/java/sh/libre/scim/core/GroupAdapter.java +++ b/src/main/java/sh/libre/scim/core/GroupAdapter.java @@ -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 { +public class GroupAdapter extends Adapter { private String displayName; private Set members = new HashSet(); @@ -39,8 +36,8 @@ public class GroupAdapter extends Adapter { } @Override - public Class getResourceClass() { - return GroupResource.class; + public Class getResourceClass() { + return Group.class; } @Override @@ -55,14 +52,14 @@ public class GroupAdapter extends Adapter { } @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(); 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 { } @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 { 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 { var meta = new Meta(); try { var uri = new URI("Groups/" + externalId); - meta.setLocation(uri); + meta.setLocation(uri.toString()); } catch (URISyntaxException e) { } group.setMeta(meta); diff --git a/src/main/java/sh/libre/scim/core/ScimClient.java b/src/main/java/sh/libre/scim/core/ScimClient.java index 67e15acfa624f0f15b7c64fd66e07f63afed4a29..29c5c17f15b0bf2088834d604b40b3ad862fce14 100644 --- a/src/main/java/sh/libre/scim/core/ScimClient.java +++ b/src/main/java/sh/libre/scim/core/ScimClient.java @@ -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 defaultHeaders; + final protected Map 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"), - model.get("auth-pass"))); + 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(); + .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 > A getAdapter( + protected > A getAdapter( Class aClass) { try { return aClass.getDeclaredConstructor(KeycloakSession.class, String.class) @@ -74,7 +121,7 @@ public class ScimClient { } } - public > void create(Class aClass, + public > void create(Class 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 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 > void replace(Class aClass, + public > void replace(Class 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 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 > void delete(Class aClass, + public > void delete(Class 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 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 > void refreshResources( + public > void refreshResources( Class aClass, SynchronizationResult syncRes) { LOGGER.info("Refresh resources"); @@ -169,16 +242,17 @@ public class ScimClient { } - public > void importResources( + public > void importResources( Class 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> response = scimRequestBuilder.list("url",adapter.getResourceClass()).get().sendRequest(); + ListResponse 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 > void sync(Class aClass, + public > void sync(Class 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(); } } diff --git a/src/main/java/sh/libre/scim/core/UserAdapter.java b/src/main/java/sh/libre/scim/core/UserAdapter.java index c17ef52e7996ca5c6451907d97cdbe20ec126cc1..b2283e59fdfffe1237302386db5a56db54878a8e 100644 --- a/src/main/java/sh/libre/scim/core/UserAdapter.java +++ b/src/main/java/sh/libre/scim/core/UserAdapter.java @@ -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 { +public class UserAdapter extends Adapter { private String username; private String displayName; @@ -78,8 +80,8 @@ public class UserAdapter extends Adapter { } @Override - public Class getResourceClass() { - return UserResource.class; + public Class getResourceClass() { + return User.class; } @Override @@ -114,27 +116,29 @@ public class UserAdapter extends Adapter { } @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(); 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 { 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 roles = new ArrayList(); + List roles = new ArrayList(); for (var r : this.roles) { - var role = new Role(); + var role = new PersonRole(); role.setValue(r); roles.add(role); } diff --git a/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java b/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java index d961b700324f4cfd0efde49afc42b6e7fa0c3891..34ea98484387546ad8a88fb43974d8bb693c4c79 100644 --- a/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java +++ b/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java @@ -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, 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")