diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0a76f5bcc58277d946c295b2efc00a273c0b0572..0000000000000000000000000000000000000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/build -/bin -/.idea -/.gradle \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 99347f14d395ba52d0d6d5787e08f73d8ff00ba6..0000000000000000000000000000000000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,10 +0,0 @@ -package: - image: - name: gradle:jdk17 - script: - - gradle jar shadowjar - artifacts: - paths: - - build/libs/keycloak-scim-1.0-SNAPSHOT.jar - - build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar - - build/libs/keycloak-scim-1.0-SNAPSHOT-all-legacy.jar diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e0f15db2eb22b5d618150277e48b741f8fdd277a..0000000000000000000000000000000000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "automatic" -} \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index ff4ec16caeffb4425eb9695701fd696d04c415b6..0000000000000000000000000000000000000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2022 libre.sh / scim - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md index 785758b00710b8755947fe56ce4d369bd736ffdf..766de55fe98ee5b86148d98f4abddb902378cd09 100644 --- a/README.md +++ b/README.md @@ -1,104 +1 @@ -# keycloak-scim-client - -This extension add [SCIM2](http://www.simplecloud.info) client capabilities to Keycloak. - -It allows to : - -* Declare SCIM endpoints (through the identity federation UI). Any tool implementing SCIM protocol can be wired to the - Keycloack instance through this declaration. -* Propagate users and groups from Keycloack to SCIM endpoints : when a user/group gets created or modified in Keycloack, - the modification is fowarded to all declared SCIM endpoints through SCIM calls within the transaction scope. If - propagation fails, changes can be rolled back or not according to a configurable rollback strategy. -* Import users and groups from SCIM endpoints (through the Keycloack synchronization mechanism). - -See [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643) -and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644)) for further details - -## Overview - -### Motivation - -We want to build a unified collaborative platform based on multiple applications. To do that, we need a way to propagate -immediately changes made in Keycloak to all these applications. And we want to keep using OIDC or SAML as the -authentication protocol. - -This will allow users to collaborate seamlessly across the platform without requiring every user to have connected once -to each application. This will also ease GDRP compliance because deleting a user in Keycloak will delete the user from -every app. The SCIM protocol is standard, comprehensible and easy to implement. It's a perfect fit for our goal. - -We chose to build application extensions/plugins because it's easier to deploy and thus will benefit to a larger portion -of the FOSS community. - -#### Keycloak specific - -This extension uses 3 concepts in KeyCloack : - -- Event Listener : used to listen for changes within Keycloack (e.g. User creation, Group deletion...) and propagate - them to registered SCIM service providers through SCIM requests. -- Federation Provider : used to set up all the SCIM service providers endpoint without creating our own UI. -- JPA Entity Provider : used to save the mapping between the local IDs and the service providers IDs. - -## Usage - -### Development mode - -From the repository root : - -* Launch the docker-compose image (composed of a postgre and keycloack instance runing on localhost:8080) : - ``docker compose up -d`` -* Execute ``gradle jar shadowJar && docker compose restart keycloak`` to build extension and update the Keycloack - instance -* You can access extension logs through ``docker compose logs -f`` - -### Installation - -1. Download - the [latest version](https://lab.libreho.st/libre.sh/scim/keycloak-scim/-/jobs/artifacts/main/raw/build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar?job=package) -2. Put it in `/opt/keycloak/providers/`. - -It's also possible to build your own custom image if you run Keycloak in a [container](/docs/container.md). - -Other [installation options](/docs/installation.md) are available. - -### Setup - -#### Enable SCIM Event listeners - -1. Go to `Admin Console > Events > Config`. -2. Add `scim` in `Event Listeners`. -3. Save. - - - -#### Register SCIM Service Providers - -1. Go to `Admin Console > Realm Settings > Events`. -2. Add `scim` to the list of event listers -3. Save - - - -### Configuration - -Add the endpoint - for a local set up you have to add the two containers in a docker network and use the container ip -see [here](https://docs.docker.com/engine/reference/commandline/network/) -If you use the [rocketchat app](https://lab.libreho.st/libre.sh/scim/rocketchat-scim) you get the endpoint from your -rocket Chat Scim Adapter App Details. -Endpoint content type is application/json. -Auth mode Bearer or None for local test setup. -Copy the bearer token from your app details in rocketchat. - -If you enable import during sync then you can choose between to following import actions: - -- Create Local - adds users to keycloak -- Nothing -- Delete Remote - deletes users from the remote application - -### Sync - -You can set up a periodic sync for all users or just changed users - it's not mandatory. You can either do: - -- Periodic Full Sync -- Periodic Changed User Sync - -**[License AGPL](/LICENSE)** +Moved to https://forge.libre.sh/libre.sh/keycloak-scim. \ No newline at end of file diff --git a/auto.sh b/auto.sh deleted file mode 100755 index 884ba67840cdd501ca287f939176531aa76216f4..0000000000000000000000000000000000000000 --- a/auto.sh +++ /dev/null @@ -1,4 +0,0 @@ -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 diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 7897ff6a60548a0fa5baed04f395cd45a54af10a..0000000000000000000000000000000000000000 --- a/build.gradle +++ /dev/null @@ -1,28 +0,0 @@ -plugins { - id 'java' - id 'com.github.johnrengelman.shadow' version '8.1.1' - id "org.sonarqube" version "5.1.0.4882" - id "com.github.ben-manes.versions" version "0.51.0" -} - -group = 'sh.libre.scim' -version = '1.0-SNAPSHOT' -description = 'keycloak-scim' - -java.sourceCompatibility = JavaVersion.VERSION_17 - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - compileOnly 'org.keycloak:keycloak-core:26.0.1' - compileOnly 'org.keycloak:keycloak-server-spi:26.0.1' - compileOnly 'org.keycloak:keycloak-server-spi-private:26.0.1' - compileOnly 'org.keycloak:keycloak-services:26.0.1' - compileOnly 'org.keycloak:keycloak-model-jpa:26.0.1' - implementation 'io.github.resilience4j:resilience4j-retry:2.2.0' - implementation 'de.captaingoldfish:scim-sdk-common:1.26.0' - implementation 'de.captaingoldfish:scim-sdk-client:1.26.0' -} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 43e4f3a77023c85f6d4a0859eaf36b032e8b2b63..0000000000000000000000000000000000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,33 +0,0 @@ -version: "3" - -services: - postgres: - image: postgres - volumes: - - db:/var/lib/postgresql/data - environment: - POSTGRES_USER: keycloak - POSTGRES_PASSWORD: keycloak - ports: - - 5432:5432 - keycloak: - image: quay.io/keycloak/keycloak:26.0.1 - build: . - command: start-dev - volumes: - - ./build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar:/opt/keycloak/providers/keycloak-scim-1.0-SNAPSHOT-all.jar - environment: - KC_DB: postgres - KC_DB_URL_HOST: postgres - KC_DB_USERNAME: keycloak - KC_DB_PASSWORD: keycloak - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin - KC_LOG_LEVEL: INFO,sh.libre.scim:debug,de.captaingoldfish.scim:debug - ports: - - 127.0.0.1:8080:8080 - depends_on: - - postgres - -volumes: - db: diff --git a/docs/container.md b/docs/container.md deleted file mode 100644 index 086b87f06b93726d638b73c9187a742763f73049..0000000000000000000000000000000000000000 --- a/docs/container.md +++ /dev/null @@ -1,15 +0,0 @@ -# Container - -## Quarkus - -``` -FROM quay.io/keycloak/keycloak as builder -RUN curl "https://lab.libreho.st/libre.sh/scim/keycloak-scim/-/jobs/artifacts/main/raw/build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar?job=package" -Lo /opt/keycloak/providers/keycloak-scim-1.0-SNAPSHOT-all.jar -RUN /opt/keycloak/bin/kc.sh build - -FROM quay.io/keycloak/keycloak -COPY --from=builder /opt/keycloak/ /opt/keycloak/ -WORKDIR /opt/keycloak -ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] -CMD ["start"] -``` diff --git a/docs/dependencies.md b/docs/dependencies.md deleted file mode 100644 index 424136e5dc3a172fb9126f303ea314906d745f31..0000000000000000000000000000000000000000 --- a/docs/dependencies.md +++ /dev/null @@ -1,14 +0,0 @@ -# Dependencies - -| Name | Version | Quarkus | Wildfly | Download | -| --- | --- | --- | --- | --- | -| io.github.resilience4j:resilience4j-retry | 1.7.1 | X | X | [link](https://repo1.maven.org/maven2/io/github/resilience4j/resilience4j-retry/1.7.1/resilience4j-retry-1.7.1.jar) | -| io.github.resilience4j:resilience4j-core | 1.7.1 | X | X | [link](https://repo1.maven.org/maven2/io/github/resilience4j/resilience4j-core/1.7.1/resilience4j-core-1.7.1.jar) | -| io.vavr:vavr | 0.10.2 | X | X | [link](https://repo1.maven.org/maven2/io/vavr/vavr/0.10.2/vavr-0.10.2.jar) | -| io.vavr:vavr-match | 0.10.2 | X | X | [link](https://repo1.maven.org/maven2/io/vavr/vavr-match/0.10.2/vavr-match-0.10.2.jar) | -| org.slf4j:slf4j-api | 1.7.30 | X | X | [link](https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar) | -| com.unboundid.product.scim2:scim2-sdk-client | 2.3.7 | X | X | [link](https://repo1.maven.org/maven2/com/unboundid/product/scim2/scim2-sdk-client/2.3.7/scim2-sdk-client-2.3.7.jar) | -| com.unboundid.product.scim2:scim2-sdk-common | 2.3.7 | X | X | [link](https://repo1.maven.org/maven2/com/unboundid/product/scim2/scim2-sdk-common/2.3.7/scim2-sdk-common-2.3.7.jar) | -| org.wildfly.client:wildfly-client-config | 1.0.1.Final | X | | [link](https://repo1.maven.org/maven2/org/wildfly/client/wildfly-client-config/1.0.1.Final/wildfly-client-config-1.0.1.Final.jar) | -| org.jboss.resteasy:resteasy-client | 4.7.6.Final | X | | [link](https://repo1.maven.org/maven2/org/jboss/resteasy/resteasy-client/4.7.6.Final/resteasy-client-4.7.6.Final.jar) | -| org.jboss.resteasy:resteasy-client-api | 4.7.6.Final | X | | [link](https://repo1.maven.org/maven2/org/jboss/resteasy/resteasy-client-api/4.7.6.Final/resteasy-client-api-4.7.6.Final.jar) | \ No newline at end of file diff --git a/docs/img/event-listener-page.png b/docs/img/event-listener-page.png deleted file mode 100644 index 5c45734c68fcc9ecfe558cd84fb571c8fcffb20f..0000000000000000000000000000000000000000 Binary files a/docs/img/event-listener-page.png and /dev/null differ diff --git a/docs/img/federation-provider-page.png b/docs/img/federation-provider-page.png deleted file mode 100644 index 390128f581b6bca7ea04d272ab71299788a99f79..0000000000000000000000000000000000000000 Binary files a/docs/img/federation-provider-page.png and /dev/null differ diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index c79919c63b3c8ad4efdc512199e1cddd2679c452..0000000000000000000000000000000000000000 --- a/docs/installation.md +++ /dev/null @@ -1,21 +0,0 @@ -# Installation (advanced) - -## 1. Download the jar files - -### All-in-one - -This [package](<(https://lab.libreho.st/libre.sh/scim/keycloak-scim/-/jobs/artifacts/main/raw/build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar?job=package)>) contains the SCIM provider and it's dependencies. It's intended only for Quarkus distribution. - -### Separately - -You'll need the SCIM provider's [package](<(https://lab.libreho.st/libre.sh/scim/keycloak-scim/-/jobs/artifacts/main/raw/build/libs/keycloak-scim-1.0-SNAPSHOT.jar?job=package)>) and each of its [dependencies](dependencies.md). - -## 2. Install the jar files - -### Quarkus - -Copy the downloaded file in `/opt/keycloak/providers/`. For production use, build Keycloak before starting it. - -### Wildfly (legacy) - -Copy the downloaded file in `/opt/jboss/keycloak/standalone/deployments/`. diff --git a/keycloak-scim.iml b/keycloak-scim.iml deleted file mode 100644 index 414103a5e4ed1cdbe3f3b022db200dfb865d29b3..0000000000000000000000000000000000000000 --- a/keycloak-scim.iml +++ /dev/null @@ -1,106 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4"> - <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_11"> - <output url="file://$MODULE_DIR$/target/classes" /> - <output-test url="file://$MODULE_DIR$/target/test-classes" /> - <content url="file://$MODULE_DIR$"> - <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" /> - <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" /> - <excludeFolder url="file://$MODULE_DIR$/target" /> - </content> - <orderEntry type="inheritedJdk" /> - <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-core:16.1.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-common:16.1.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.bouncycastle:bcprov-jdk15on:1.68" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.bouncycastle:bcpkix-jdk15on:1.68" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-server-spi:16.1.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-server-spi-private:16.1.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.ua-parser:uap-java:1.4.3" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.yaml:snakeyaml:1.20" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.commons:commons-collections4:4.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-services:16.1.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.sun.mail:jakarta.mail:1.6.5" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.glassfish:jakarta.json:1.1.6" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.twitter4j:twitter4j-core:4.0.7" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.logging:jboss-logging:3.4.1.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_2.1_spec:2.0.1.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.spec.javax.transaction:jboss-transaction-api_1.3_spec:2.0.0.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20211018.2" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.zxing:javase:3.4.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.zxing:core:3.4.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.jai-imageio:jai-imageio-core:1.4.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.openshift:openshift-restclient-java:8.0.0.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.squareup.okhttp3:okhttp:3.14.2" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.squareup.okio:okio:1.17.2" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss:jboss-dmr:1.3.0.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.commons:commons-compress:1.18" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.slf4j:slf4j-log4j12:1.6.4" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: log4j:log4j:1.2.16" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: commons-lang:commons-lang:2.6" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.webauthn4j:webauthn4j-core:0.12.0.RELEASE" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.webauthn4j:webauthn4j-util:0.12.0.RELEASE" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.kerby:kerby-asn1:2.0.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.11.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.keycloak:keycloak-model-jpa:16.1.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.liquibase:liquibase-core:3.5.5" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: jakarta.persistence:jakarta.persistence-api:2.2.3" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.hibernate:hibernate-core:5.3.20.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: javax.persistence:javax.persistence-api:2.2" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.javassist:javassist:3.23.2-GA" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: net.bytebuddy:byte-buddy:1.9.11" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: antlr:antlr:2.7.7" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.spec.javax.transaction:jboss-transaction-api_1.2_spec:1.1.1.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss:jandex:2.0.5.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml:classmate:1.3.4" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.dom4j:dom4j:2.1.3" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.hibernate.common:hibernate-commons-annotations:5.0.4.Final" level="project" /> - <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.30" level="project" /> - <orderEntry type="library" name="Maven: com.unboundid.product.scim2:scim2-sdk-client:2.3.7" level="project" /> - <orderEntry type="library" name="Maven: com.unboundid.product.scim2:scim2-sdk-common:2.3.7" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: javax.ws.rs:javax.ws.rs-api:2.1.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: javax.xml.bind:jaxb-api:2.3.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: javax.activation:javax.activation-api:1.2.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.core:jackson-core:2.12.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.12.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.12.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.12.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: jakarta.xml.bind:jakarta.xml.bind-api:2.3.2" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: jakarta.activation:jakarta.activation-api:1.2.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.12.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.12.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:2.12.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.resteasy:resteasy-jaxrs:3.15.1.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.spec.javax.xml.bind:jboss-jaxb-api_2.3_spec:2.0.1.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.reactivestreams:reactive-streams:1.0.3" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: jakarta.validation:jakarta.validation-api:2.0.2" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.spec.javax.annotation:jboss-annotations-api_1.3_spec:2.0.1.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.sun.activation:jakarta.activation:1.2.2" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.httpcomponents:httpclient:4.5.13" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.httpcomponents:httpcore:4.4.13" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: commons-logging:commons-logging:1.2" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: commons-io:commons-io:2.5" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.stephenc.jcip:jcip-annotations:1.0-1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.resteasy:resteasy-multipart-provider:3.15.1.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.james:apache-mime4j:0.6" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.resteasy:resteasy-jackson2-provider:3.15.1.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.fge:json-patch:1.9" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.fge:jackson-coreutils:1.6" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.fge:msg-simple:1.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.fge:btf:1.2" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.guava:guava:28.1-jre" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.guava:failureaccess:1.0.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.checkerframework:checker-qual:2.8.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.resteasy:resteasy-jaxb-provider:3.15.1.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.glassfish.jaxb:jaxb-runtime:2.3.3-b02" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.glassfish.jaxb:txw2:2.3.3-b02" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.sun.istack:istack-commons-runtime:3.0.10" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.jboss.resteasy:resteasy-client:3.15.1.Final" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: commons-codec:commons-codec:1.15" level="project" /> - <orderEntry type="library" name="Maven: io.github.resilience4j:resilience4j-retry:1.7.0" level="project" /> - <orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" /> - <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" /> - <orderEntry type="library" name="Maven: io.github.resilience4j:resilience4j-core:1.7.0" level="project" /> - </component> -</module> \ No newline at end of file diff --git a/src/main/java/sh/libre/scim/core/ScimDispatcher.java b/src/main/java/sh/libre/scim/core/ScimDispatcher.java deleted file mode 100644 index d3d675108bfd300398a80d9f97ecbf8115b15387..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/ScimDispatcher.java +++ /dev/null @@ -1,171 +0,0 @@ -package sh.libre.scim.core; - -import org.jboss.logging.Logger; -import org.keycloak.component.ComponentModel; -import org.keycloak.models.KeycloakSession; -import sh.libre.scim.core.exceptions.ScimExceptionHandler; -import sh.libre.scim.core.exceptions.ScimPropagationException; -import sh.libre.scim.core.exceptions.SkipOrStopApproach; -import sh.libre.scim.core.exceptions.SkipOrStopStrategy; -import sh.libre.scim.core.service.AbstractScimService; -import sh.libre.scim.core.service.GroupScimService; -import sh.libre.scim.core.service.UserScimService; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -/** - * In charge of sending SCIM Request to all registered Scim endpoints. - */ -public class ScimDispatcher { - - private static final Logger LOGGER = Logger.getLogger(ScimDispatcher.class); - - private final KeycloakSession session; - private final ScimExceptionHandler exceptionHandler; - private final SkipOrStopStrategy skipOrStopStrategy; - private boolean clientsInitialized = false; - private final List<UserScimService> userScimServices = new ArrayList<>(); - private final List<GroupScimService> groupScimServices = new ArrayList<>(); - - - public ScimDispatcher(KeycloakSession session) { - this.session = session; - this.exceptionHandler = new ScimExceptionHandler(session); - // By default, use a permissive Skip or Stop strategy - this.skipOrStopStrategy = SkipOrStopApproach.ALWAYS_SKIP_AND_CONTINUE; - } - - /** - * Lists all active ScimStorageProviderFactory and create new ScimClients for each of them - */ - public void refreshActiveScimEndpoints() { - // Step 1: close existing clients (as configuration may have changed) - groupScimServices.forEach(GroupScimService::close); - groupScimServices.clear(); - userScimServices.forEach(UserScimService::close); - userScimServices.clear(); - - // Step 2: Get All SCIM endpoints defined in Admin Console (enabled ScimStorageProviderFactory) - session.getContext().getRealm().getComponentsStream() - .filter(m -> ScimEndpointConfigurationStorageProviderFactory.ID.equals(m.getProviderId()) - && m.get("enabled", true)) - .forEach(scimEndpointConfigurationRaw -> { - try { - ScrimEndPointConfiguration scrimEndPointConfiguration = new ScrimEndPointConfiguration(scimEndpointConfigurationRaw); - - // Step 3 : create scim clients for each endpoint - if (scimEndpointConfigurationRaw.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP, false)) { - GroupScimService groupScimService = new GroupScimService(session, scrimEndPointConfiguration, skipOrStopStrategy); - groupScimServices.add(groupScimService); - } - if (scimEndpointConfigurationRaw.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER, false)) { - UserScimService userScimService = new UserScimService(session, scrimEndPointConfiguration, skipOrStopStrategy); - userScimServices.add(userScimService); - } - } catch (IllegalArgumentException e) { - if (skipOrStopStrategy.allowInvalidEndpointConfiguration()) { - LOGGER.warn("[SCIM] Invalid Endpoint configuration " + scimEndpointConfigurationRaw.getId(), e); - } else { - throw e; - } - } - }); - } - - public void dispatchUserModificationToAll(SCIMPropagationConsumer<UserScimService> operationToDispatch) { - initializeClientsIfNeeded(); - Set<UserScimService> servicesCorrectlyPropagated = new LinkedHashSet<>(); - userScimServices.forEach(userScimService -> { - try { - operationToDispatch.acceptThrows(userScimService); - servicesCorrectlyPropagated.add(userScimService); - } catch (ScimPropagationException e) { - exceptionHandler.handleException(userScimService.getConfiguration(), e); - } - }); - // TODO we could iterate on servicesCorrectlyPropagated to undo modification on already handled SCIM endpoints - LOGGER.infof("[SCIM] User operation dispatched to %d SCIM server", servicesCorrectlyPropagated.size()); - } - - public void dispatchGroupModificationToAll(SCIMPropagationConsumer<GroupScimService> operationToDispatch) { - initializeClientsIfNeeded(); - Set<GroupScimService> servicesCorrectlyPropagated = new LinkedHashSet<>(); - groupScimServices.forEach(groupScimService -> { - try { - operationToDispatch.acceptThrows(groupScimService); - servicesCorrectlyPropagated.add(groupScimService); - } catch (ScimPropagationException e) { - exceptionHandler.handleException(groupScimService.getConfiguration(), e); - } - }); - // TODO we could iterate on servicesCorrectlyPropagated to undo modification on already handled SCIM endpoints - LOGGER.infof("[SCIM] Group operation dispatched to %d SCIM server", servicesCorrectlyPropagated.size()); - } - - public void dispatchUserModificationToOne(ComponentModel scimServerConfiguration, SCIMPropagationConsumer<UserScimService> operationToDispatch) { - initializeClientsIfNeeded(); - // Scim client should already have been created - Optional<UserScimService> matchingClient = userScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst(); - if (matchingClient.isPresent()) { - try { - operationToDispatch.acceptThrows(matchingClient.get()); - LOGGER.infof("[SCIM] User operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getName()); - } catch (ScimPropagationException e) { - exceptionHandler.handleException(matchingClient.get().getConfiguration(), e); - } - } else { - LOGGER.error("[SCIM] Could not find a Scim Client matching User endpoint configuration" + scimServerConfiguration.getId()); - } - } - - - public void dispatchGroupModificationToOne(ComponentModel scimServerConfiguration, SCIMPropagationConsumer<GroupScimService> operationToDispatch) { - initializeClientsIfNeeded(); - // Scim client should already have been created - Optional<GroupScimService> matchingClient = groupScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst(); - if (matchingClient.isPresent()) { - try { - operationToDispatch.acceptThrows(matchingClient.get()); - LOGGER.infof("[SCIM] Group operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getName()); - } catch (ScimPropagationException e) { - exceptionHandler.handleException(matchingClient.get().getConfiguration(), e); - } - } else { - LOGGER.error("[SCIM] Could not find a Scim Client matching Group endpoint configuration" + scimServerConfiguration.getId()); - } - } - - public void close() { - for (GroupScimService c : groupScimServices) { - c.close(); - } - for (UserScimService c : userScimServices) { - c.close(); - } - groupScimServices.clear(); - userScimServices.clear(); - } - - private void initializeClientsIfNeeded() { - if (!clientsInitialized) { - clientsInitialized = true; - refreshActiveScimEndpoints(); - } - } - - /** - * A Consumer that throws ScimPropagationException. - * - * @param <T> An {@link AbstractScimService to call} - */ - @FunctionalInterface - public interface SCIMPropagationConsumer<T> { - - void acceptThrows(T elem) throws ScimPropagationException; - - } -} diff --git a/src/main/java/sh/libre/scim/core/ScimEndpointConfigurationStorageProviderFactory.java b/src/main/java/sh/libre/scim/core/ScimEndpointConfigurationStorageProviderFactory.java deleted file mode 100644 index b73df47725de371ce9c3cf548bb16a86e0a4d493..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/ScimEndpointConfigurationStorageProviderFactory.java +++ /dev/null @@ -1,165 +0,0 @@ -package sh.libre.scim.core; - -import de.captaingoldfish.scim.sdk.common.constants.HttpHeader; -import jakarta.ws.rs.core.MediaType; -import org.apache.commons.lang3.BooleanUtils; -import org.jboss.logging.Logger; -import org.keycloak.component.ComponentModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.RealmModel; -import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderConfigurationBuilder; -import org.keycloak.storage.UserStorageProvider; -import org.keycloak.storage.UserStorageProviderFactory; -import org.keycloak.storage.UserStorageProviderModel; -import org.keycloak.storage.user.ImportSynchronization; -import org.keycloak.storage.user.SynchronizationResult; -import sh.libre.scim.event.ScimBackgroundGroupMembershipUpdater; - -import java.util.Date; -import java.util.List; - -/** - * Allows to register and configure Scim endpoints through Admin console, using the provided config properties. - */ -public class ScimEndpointConfigurationStorageProviderFactory - implements UserStorageProviderFactory<ScimEndpointConfigurationStorageProviderFactory.ScimEndpointConfigurationStorageProvider>, ImportSynchronization { - public static final String ID = "scim"; - private static final Logger LOGGER = Logger.getLogger(ScimEndpointConfigurationStorageProviderFactory.class); - - @Override - public String getId() { - return ID; - } - - - @Override - public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, - UserStorageProviderModel model) { - // Manually Launch a synchronization between keycloack and the SCIM endpoint described in the given model - LOGGER.infof("[SCIM] Sync from ScimStorageProvider - Realm %s - Model %s", realmId, model.getName()); - SynchronizationResult result = new SynchronizationResult(); - KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> { - RealmModel realm = session.realms().getRealm(realmId); - session.getContext().setRealm(realm); - ScimDispatcher dispatcher = new ScimDispatcher(session); - if (BooleanUtils.TRUE.equals(model.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER))) { - dispatcher.dispatchUserModificationToOne(model, client -> client.sync(result)); - } - if (BooleanUtils.TRUE.equals(model.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP))) { - dispatcher.dispatchGroupModificationToOne(model, client -> client.sync(result)); - } - dispatcher.close(); - }); - return result; - } - - @Override - public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, - UserStorageProviderModel model) { - return this.sync(sessionFactory, realmId, model); - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - ScimBackgroundGroupMembershipUpdater scimBackgroundGroupMembershipUpdater = new ScimBackgroundGroupMembershipUpdater(factory); - scimBackgroundGroupMembershipUpdater.startBackgroundUpdates(); - } - - @Override - public List<ProviderConfigProperty> getConfigProperties() { - // These Config Properties will be use to generate configuration page in Admin Console - return ProviderConfigurationBuilder.create() - .property() - .name(ScrimEndPointConfiguration.CONF_KEY_ENDPOINT) - .type(ProviderConfigProperty.STRING_TYPE) - .required(true) - .label("SCIM 2.0 endpoint") - .helpText("External SCIM 2.0 base " + - "URL (/ServiceProviderConfig /Schemas and /ResourcesTypes should be accessible)") - .add() - .property() - .name(ScrimEndPointConfiguration.CONF_KEY_CONTENT_TYPE) - .type(ProviderConfigProperty.LIST_TYPE) - .label("Endpoint content type") - .helpText("Only used when endpoint doesn't support application/scim+json") - .options(MediaType.APPLICATION_JSON, HttpHeader.SCIM_CONTENT_TYPE) - .defaultValue(HttpHeader.SCIM_CONTENT_TYPE) - .add() - .property() - .name(ScrimEndPointConfiguration.CONF_KEY_AUTH_MODE) - .type(ProviderConfigProperty.LIST_TYPE) - .label("Auth mode") - .helpText("Select the authorization mode") - .options("NONE", "BASIC_AUTH", "BEARER") - .defaultValue("NONE") - .add() - .property() - .name(ScrimEndPointConfiguration.CONF_KEY_AUTH_USER) - .type(ProviderConfigProperty.STRING_TYPE) - .label("Auth username") - .helpText("Required for basic authentication.") - .add() - .property() - .name(ScrimEndPointConfiguration.CONF_KEY_AUTH_PASSWORD) - .type(ProviderConfigProperty.PASSWORD) - .label("Auth password/token") - .helpText("Password or token required for basic or bearer authentication.") - .add() - .property() - .name(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER) - .type(ProviderConfigProperty.BOOLEAN_TYPE) - .label("Enable user propagation") - .helpText("Should operation on users be propagated to this provider?") - .defaultValue(BooleanUtils.TRUE) - .add() - .property() - .name(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP) - .type(ProviderConfigProperty.BOOLEAN_TYPE) - .label("Enable group propagation") - .helpText("Should operation on groups be propagated to this provider?") - .defaultValue(BooleanUtils.TRUE) - .add() - .property() - .name(ScrimEndPointConfiguration.CONF_KEY_SYNC_IMPORT) - .type(ProviderConfigProperty.BOOLEAN_TYPE) - .label("Enable import during sync") - .add() - .property() - .name(ScrimEndPointConfiguration.CONF_KEY_SYNC_IMPORT_ACTION) - .type(ProviderConfigProperty.LIST_TYPE) - .label("Import action") - .helpText("What to do when the user doesn't exists in Keycloak.") - .options("NOTHING", "CREATE_LOCAL", "DELETE_REMOTE") - .defaultValue("CREATE_LOCAL") - .add() - .property() - .name(ScrimEndPointConfiguration.CONF_KEY_SYNC_REFRESH) - .type(ProviderConfigProperty.BOOLEAN_TYPE) - .label("Enable refresh during sync") - .name(ScrimEndPointConfiguration.CONF_KEY_LOG_ALL_SCIM_REQUESTS) - .type(ProviderConfigProperty.BOOLEAN_TYPE) - .label("Log SCIM requests and responses") - .helpText("If true, all sent SCIM requests and responses will be logged") - .add() - .build(); - } - - - @Override - public ScimEndpointConfigurationStorageProvider create(KeycloakSession session, ComponentModel model) { - return new ScimEndpointConfigurationStorageProvider(); - } - - /** - * Empty implementation : we used this {@link ScimEndpointConfigurationStorageProviderFactory} to generate Admin Console page. - */ - public static final class ScimEndpointConfigurationStorageProvider implements UserStorageProvider { - @Override - public void close() { - // Nothing to close here - } - } -} diff --git a/src/main/java/sh/libre/scim/core/ScrimEndPointConfiguration.java b/src/main/java/sh/libre/scim/core/ScrimEndPointConfiguration.java deleted file mode 100644 index 6359b571535cc9dceafc5ce7f1d4a7ecf1e0ff01..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/ScrimEndPointConfiguration.java +++ /dev/null @@ -1,101 +0,0 @@ -package sh.libre.scim.core; - -import de.captaingoldfish.scim.sdk.client.http.BasicAuth; -import org.keycloak.component.ComponentModel; - -public class ScrimEndPointConfiguration { - // Configuration keys : also used in Admin Console page - public static final String CONF_KEY_AUTH_MODE = "auth-mode"; - public static final String CONF_KEY_AUTH_PASSWORD = "auth-pass"; - public static final String CONF_KEY_AUTH_USER = "auth-user"; - public static final String CONF_KEY_CONTENT_TYPE = "content-type"; - public static final String CONF_KEY_ENDPOINT = "endpoint"; - public static final String CONF_KEY_SYNC_IMPORT_ACTION = "sync-import-action"; - public static final String CONF_KEY_SYNC_IMPORT = "sync-import"; - public static final String CONF_KEY_SYNC_REFRESH = "sync-refresh"; - public static final String CONF_KEY_PROPAGATION_USER = "propagation-user"; - public static final String CONF_KEY_PROPAGATION_GROUP = "propagation-group"; - public static final String CONF_KEY_LOG_ALL_SCIM_REQUESTS = "log-all-scim-requests"; - - private final String endPoint; - private final String id; - private final String name; - private final String contentType; - private final String authorizationHeaderValue; - private final ImportAction importAction; - private final boolean pullFromScimSynchronisationActivated; - private final boolean pushToScimSynchronisationActivated; - private final boolean logAllScimRequests; - - public ScrimEndPointConfiguration(ComponentModel scimProviderConfiguration) { - try { - AuthMode authMode = AuthMode.valueOf(scimProviderConfiguration.get(CONF_KEY_AUTH_MODE)); - - authorizationHeaderValue = switch (authMode) { - case BEARER -> "Bearer " + scimProviderConfiguration.get(CONF_KEY_AUTH_PASSWORD); - case BASIC_AUTH -> { - BasicAuth basicAuth = BasicAuth.builder() - .username(scimProviderConfiguration.get(CONF_KEY_AUTH_USER)) - .password(scimProviderConfiguration.get(CONF_KEY_AUTH_PASSWORD)) - .build(); - yield basicAuth.getAuthorizationHeaderValue(); - } - case NONE -> ""; - }; - contentType = scimProviderConfiguration.get(CONF_KEY_CONTENT_TYPE, ""); - endPoint = scimProviderConfiguration.get(CONF_KEY_ENDPOINT, ""); - id = scimProviderConfiguration.getId(); - name = scimProviderConfiguration.getName(); - importAction = ImportAction.valueOf(scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT_ACTION)); - pullFromScimSynchronisationActivated = scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT, false); - pushToScimSynchronisationActivated = scimProviderConfiguration.get(CONF_KEY_SYNC_REFRESH, false); - logAllScimRequests = scimProviderConfiguration.get(CONF_KEY_LOG_ALL_SCIM_REQUESTS, false); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("authMode '" + scimProviderConfiguration.get(CONF_KEY_AUTH_MODE) + "' is not supported"); - } - } - - public boolean isPushToScimSynchronisationActivated() { - return pushToScimSynchronisationActivated; - } - - public boolean isPullFromScimSynchronisationActivated() { - return pullFromScimSynchronisationActivated; - } - - public String getContentType() { - return contentType; - } - - public String getAuthorizationHeaderValue() { - return authorizationHeaderValue; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public ImportAction getImportAction() { - return importAction; - } - - public String getEndPoint() { - return endPoint; - } - - public boolean isLogAllScimRequests() { - return logAllScimRequests; - } - - public enum AuthMode { - BEARER, BASIC_AUTH, NONE - } - - public enum ImportAction { - CREATE_LOCAL, DELETE_REMOTE, NOTHING - } -} diff --git a/src/main/java/sh/libre/scim/core/exceptions/InconsistentScimMappingException.java b/src/main/java/sh/libre/scim/core/exceptions/InconsistentScimMappingException.java deleted file mode 100644 index 44f7eb46e67fb86268db51c376cee94936e0aaac..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/exceptions/InconsistentScimMappingException.java +++ /dev/null @@ -1,7 +0,0 @@ -package sh.libre.scim.core.exceptions; - -public class InconsistentScimMappingException extends ScimPropagationException { - public InconsistentScimMappingException(String message) { - super(message); - } -} diff --git a/src/main/java/sh/libre/scim/core/exceptions/InvalidResponseFromScimEndpointException.java b/src/main/java/sh/libre/scim/core/exceptions/InvalidResponseFromScimEndpointException.java deleted file mode 100644 index 079443622be9e2a9f77031e2b1a8c00b6ca557d3..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/exceptions/InvalidResponseFromScimEndpointException.java +++ /dev/null @@ -1,29 +0,0 @@ -package sh.libre.scim.core.exceptions; - -import de.captaingoldfish.scim.sdk.client.response.ServerResponse; - -import java.util.Optional; - -public class InvalidResponseFromScimEndpointException extends ScimPropagationException { - - private final transient Optional<ServerResponse> response; - - public InvalidResponseFromScimEndpointException(ServerResponse response, String message) { - super(message); - this.response = Optional.of(response); - } - - public InvalidResponseFromScimEndpointException(String message, Exception e) { - super(message, e); - this.response = Optional.empty(); - } - - - /** - * Empty response can occur if a major exception was thrown while retrying the request. - */ - public Optional<ServerResponse> getResponse() { - return response; - } - -} diff --git a/src/main/java/sh/libre/scim/core/exceptions/RollbackApproach.java b/src/main/java/sh/libre/scim/core/exceptions/RollbackApproach.java deleted file mode 100644 index d1fb108930679b91dc213e8e3d77b7c68b5313d0..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/exceptions/RollbackApproach.java +++ /dev/null @@ -1,55 +0,0 @@ -package sh.libre.scim.core.exceptions; - -import com.google.common.collect.Lists; -import sh.libre.scim.core.ScrimEndPointConfiguration; - -import java.util.ArrayList; - - -public enum RollbackApproach implements RollbackStrategy { - ALWAYS_ROLLBACK { - @Override - public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) { - return true; - } - }, - NEVER_ROLLBACK { - @Override - public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) { - return false; - } - }, - CRITICAL_ONLY_ROLLBACK { - @Override - public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) { - if (e instanceof InconsistentScimMappingException) { - // Occurs when mapping between a SCIM resource and a keycloak user failed (missing, ambiguous..) - // Log can be sufficient here, no rollback required - return false; - } - if (e instanceof UnexpectedScimDataException) { - // Occurs when a SCIM endpoint sends invalid date (e.g. group with empty name, user without ids...) - // No rollback required : we cannot recover. This needs to be fixed in the SCIM endpoint data - return false; - } - if (e instanceof InvalidResponseFromScimEndpointException invalidResponseFromScimEndpointException) { - return shouldRollbackBecauseOfResponse(invalidResponseFromScimEndpointException); - } - // Should not occur - throw new IllegalStateException("Unkown ScimPropagationException", e); - } - - private boolean shouldRollbackBecauseOfResponse(InvalidResponseFromScimEndpointException e) { - // If we have a response - return e.getResponse().map(r -> { - // We consider that 404 are acceptable, otherwise rollback - ArrayList<Integer> acceptableStatus = Lists.newArrayList(200, 204, 404); - return !acceptableStatus.contains(r.getHttpStatus()); - }).orElse( - // Never got an answer, server was either misconfigured or unreachable - // No rollback in that case. - false - ); - } - } -} diff --git a/src/main/java/sh/libre/scim/core/exceptions/RollbackStrategy.java b/src/main/java/sh/libre/scim/core/exceptions/RollbackStrategy.java deleted file mode 100644 index 90d859305c22e63641edf1acc2e3d4421250fa0d..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/exceptions/RollbackStrategy.java +++ /dev/null @@ -1,22 +0,0 @@ -package sh.libre.scim.core.exceptions; - -import sh.libre.scim.core.ScrimEndPointConfiguration; - -/** - * In charge of deciding, when facing a SCIM-related issue during an operation (e.g User creation), - * whether we should : - * - Log the issue and let the operation succeed in Keycloack database (potentially unsynchronising - * Keycloack with the SCIM servers) - * - Rollback the whole operation - */ -public interface RollbackStrategy { - - /** - * Indicates whether we should rollback the whole transaction because of the given exception. - * - * @param configuration The SCIM Endpoint configuration for which the exception occured - * @param e the exception that we have to handle - * @return true if transaction should be rolled back, false if we should log and continue operation - */ - boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e); -} diff --git a/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java b/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java deleted file mode 100644 index 78973d2ad6e4ad9ee2fb32d73544d8786f2b6f39..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -package sh.libre.scim.core.exceptions; - -import org.jboss.logging.Logger; -import org.keycloak.models.KeycloakSession; -import sh.libre.scim.core.ScrimEndPointConfiguration; - -/** - * In charge of dealing with SCIM exceptions by ignoring, logging or rollback transaction according to : - * - The context in which it occurs (sync, user creation...) - * - The related SCIM endpoint and its configuration - * - The thrown exception itself - */ -public class ScimExceptionHandler { - private static final Logger LOGGER = Logger.getLogger(ScimExceptionHandler.class); - - private final KeycloakSession session; - private final RollbackStrategy rollbackStrategy; - - public ScimExceptionHandler(KeycloakSession session) { - this(session, RollbackApproach.CRITICAL_ONLY_ROLLBACK); - } - - public ScimExceptionHandler(KeycloakSession session, RollbackStrategy rollbackStrategy) { - this.session = session; - this.rollbackStrategy = rollbackStrategy; - } - - /** - * Handles the given exception by loggin and/or rollback transaction. - * - * @param scimProviderConfiguration the configuration of the endpoint for which the propagation exception occured - * @param e the occuring exception - */ - public void handleException(ScrimEndPointConfiguration scimProviderConfiguration, ScimPropagationException e) { - String errorMessage = "[SCIM] Error while propagating to SCIM endpoint " + scimProviderConfiguration.getName(); - if (rollbackStrategy.shouldRollback(scimProviderConfiguration, e)) { - session.getTransactionManager().rollback(); - LOGGER.error("TRANSACTION ROLLBACK - " + errorMessage, e); - } else { - LOGGER.warn(errorMessage, e); - } - } -} diff --git a/src/main/java/sh/libre/scim/core/exceptions/ScimPropagationException.java b/src/main/java/sh/libre/scim/core/exceptions/ScimPropagationException.java deleted file mode 100644 index bee5ee18fdfb0babae5203f0559c6427cbff9bac..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/exceptions/ScimPropagationException.java +++ /dev/null @@ -1,12 +0,0 @@ -package sh.libre.scim.core.exceptions; - -public abstract class ScimPropagationException extends Exception { - - protected ScimPropagationException(String message) { - super(message); - } - - protected ScimPropagationException(String message, Exception e) { - super(message, e); - } -} diff --git a/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopApproach.java b/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopApproach.java deleted file mode 100644 index e0669d59db69f3d8f4b43a435c1fbbc72b08f379..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopApproach.java +++ /dev/null @@ -1,59 +0,0 @@ -package sh.libre.scim.core.exceptions; - -import sh.libre.scim.core.ScrimEndPointConfiguration; - - -public enum SkipOrStopApproach implements SkipOrStopStrategy { - ALWAYS_SKIP_AND_CONTINUE { - @Override - public boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration) { - return false; - } - - @Override - public boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration) { - return false; - } - - @Override - public boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration) { - return false; - } - - @Override - public boolean allowInvalidEndpointConfiguration() { - return false; - } - - @Override - public boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration) { - return false; - } - }, - ALWAYS_STOP { - @Override - public boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration) { - return true; - } - - @Override - public boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration) { - return true; - } - - @Override - public boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration) { - return true; - } - - @Override - public boolean allowInvalidEndpointConfiguration() { - return true; - } - - @Override - public boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration) { - return true; - } - } -} diff --git a/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopStrategy.java b/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopStrategy.java deleted file mode 100644 index 8ad46c7ff970b86a17a7b9bd39ed8030b9f459d1..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopStrategy.java +++ /dev/null @@ -1,66 +0,0 @@ -package sh.libre.scim.core.exceptions; - -import sh.libre.scim.core.ScrimEndPointConfiguration; - -/** - * In charge of deciding, when facing a SCIM-related issue, whether we should : - * - log a warning, skip the problematic element and continue the rest of the operation - * - stop immediately the whole operation (typically, a synchronisation between SCIM and Keycloack) - */ -public interface SkipOrStopStrategy { - /** - * Indicates if, during a synchronisation from Keycloack to a SCIM endpoint, we should : - * - cancel the whole synchronisation if an element CRUD fail, or - * - keep on with synchronisation, allowing a partial synchronisation - * - * @param configuration the configuration of the endpoint in which the error occurred - * @return true if a partial synchronisation is allowed, - * false if we should stop the whole synchronisation at first issue - */ - boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration); - - /** - * Indicates if, during a synchronisation from a SCIM endpoint to Keycloack, we should : - * - cancel the whole synchronisation if an element CRUD fail, or - * - keep on with synchronisation, allowing a partial synchronisation - * - * @param configuration the configuration of the endpoint in which the error occurred - * @return true if a partial synchronisation is allowed, - * false if we should interrupt the whole synchronisation at first issue - */ - boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration); - - - /** - * Indicates if, when we propagate a group creation or update to a SCIM endpoint and some - * of its members are not mapped to SCIM, we should allow partial group update or interrupt completely. - * - * @param configuration the configuration of the endpoint in which the error occurred - * @return true if a partial group update is allowed, - * false if we should interrupt the group update in case of any unmapped member - */ - boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration); - - /** - * Indicates if, when facing an invalid SCIM endpoint configuration (resulting in a unreachable SCIM server), - * we should stop or ignore this configuration. - * - * @return true the invalid endpoint should be ignored, - * * false if we should interrupt the rest of the synchronisation - */ - boolean allowInvalidEndpointConfiguration(); - - /** - * Indicates if, when trying to pull User or Groups from a SCIM endpoint, - * we encounter a invalid data (e.g. group with empty name), we should : - * - Skip the invalid element pull and continue - * - Cancel the whole synchronisation - * - * @param configuration the configuration of the endpoint in which the error occurred - * @return true if we should skip the invalid data synchronisation and pursue, - * false if we should interrupt immediately the whole synchronisation - */ - boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration); - - -} diff --git a/src/main/java/sh/libre/scim/core/exceptions/UnexpectedScimDataException.java b/src/main/java/sh/libre/scim/core/exceptions/UnexpectedScimDataException.java deleted file mode 100644 index 918127ef0b3c2244d26ddeb41a16a1e44c40befb..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/exceptions/UnexpectedScimDataException.java +++ /dev/null @@ -1,7 +0,0 @@ -package sh.libre.scim.core.exceptions; - -public class UnexpectedScimDataException extends ScimPropagationException { - public UnexpectedScimDataException(String message) { - super(message); - } -} diff --git a/src/main/java/sh/libre/scim/core/service/AbstractScimService.java b/src/main/java/sh/libre/scim/core/service/AbstractScimService.java deleted file mode 100644 index b22b6a9a8d60ee58634940418ce437b4446f3c35..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/service/AbstractScimService.java +++ /dev/null @@ -1,281 +0,0 @@ -package sh.libre.scim.core.service; - -import de.captaingoldfish.scim.sdk.common.resources.ResourceNode; -import de.captaingoldfish.scim.sdk.common.resources.complex.Meta; -import org.jboss.logging.Logger; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RoleMapperModel; -import org.keycloak.storage.user.SynchronizationResult; -import sh.libre.scim.core.ScrimEndPointConfiguration; -import sh.libre.scim.core.exceptions.InconsistentScimMappingException; -import sh.libre.scim.core.exceptions.InvalidResponseFromScimEndpointException; -import sh.libre.scim.core.exceptions.SkipOrStopStrategy; -import sh.libre.scim.core.exceptions.UnexpectedScimDataException; -import sh.libre.scim.jpa.ScimResourceDao; -import sh.libre.scim.jpa.ScimResourceMapping; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * A service in charge of synchronisation (CRUD) between - * a Keykloak Role (UserModel, GroupModel) and a SCIM Resource (User,Group). - * - * @param <K> The Keycloack Model (e.g. UserModel, GroupModel) - * @param <S> The SCIM Resource (e.g. User, Group) - */ -public abstract class AbstractScimService<K extends RoleMapperModel, S extends ResourceNode> implements AutoCloseable { - - private static final Logger LOGGER = Logger.getLogger(AbstractScimService.class); - - private final KeycloakSession keycloakSession; - protected final SkipOrStopStrategy skipOrStopStrategy; - private final ScrimEndPointConfiguration scimProviderConfiguration; - private final ScimResourceType type; - private final ScimClient<S> scimClient; - - protected AbstractScimService(KeycloakSession keycloakSession, ScrimEndPointConfiguration scimProviderConfiguration, ScimResourceType type, SkipOrStopStrategy skipOrStopStrategy) { - this.keycloakSession = keycloakSession; - this.scimProviderConfiguration = scimProviderConfiguration; - this.type = type; - this.scimClient = ScimClient.open(scimProviderConfiguration, type); - this.skipOrStopStrategy = skipOrStopStrategy; - } - - public void create(K roleMapperModel) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException { - if (isMarkedToIgnore(roleMapperModel)) { - // Silently return: resource is explicitly marked as to ignore - return; - } - // If mapping, then we are trying to recreate a user that was already created by import - KeycloakId id = getId(roleMapperModel); - if (findMappingById(id).isPresent()) { - throw new InconsistentScimMappingException("Trying to create user with id " + id + ": id already exists in Keycloak database"); - } - S scimForCreation = scimRequestBodyForCreate(roleMapperModel); - EntityOnRemoteScimId externalId = scimClient.create(id, scimForCreation); - createMapping(id, externalId); - } - - public void update(K roleMapperModel) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException { - if (isMarkedToIgnore(roleMapperModel)) { - // Silently return: resource is explicitly marked as to ignore - return; - } - KeycloakId keycloakId = getId(roleMapperModel); - EntityOnRemoteScimId entityOnRemoteScimId = findMappingById(keycloakId) - .map(ScimResourceMapping::getExternalIdAsEntityOnRemoteScimId) - .orElseThrow(() -> new InconsistentScimMappingException("Failed to find SCIM mapping for " + keycloakId)); - S scimForReplace = scimRequestBodyForUpdate(roleMapperModel, entityOnRemoteScimId); - scimClient.update(entityOnRemoteScimId, scimForReplace); - } - - protected abstract S scimRequestBodyForUpdate(K roleMapperModel, EntityOnRemoteScimId externalId) throws InconsistentScimMappingException; - - public void delete(KeycloakId id) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException { - ScimResourceMapping resource = findMappingById(id) - .orElseThrow(() -> new InconsistentScimMappingException("Failed to delete resource %s, scim mapping not found: ".formatted(id))); - EntityOnRemoteScimId externalId = resource.getExternalIdAsEntityOnRemoteScimId(); - scimClient.delete(externalId); - getScimResourceDao().delete(resource); - } - - public void pushAllResourcesToScim(SynchronizationResult syncRes) throws InvalidResponseFromScimEndpointException, InconsistentScimMappingException { - LOGGER.info("[SCIM] Push resources to endpoint " + this.getConfiguration().getEndPoint()); - try (Stream<K> resourcesStream = getResourceStream()) { - Set<K> resources = resourcesStream.collect(Collectors.toUnmodifiableSet()); - for (K resource : resources) { - KeycloakId id = getId(resource); - pushSingleResourceToScim(syncRes, resource, id); - } - } - } - - public void pullAllResourcesFromScim(SynchronizationResult syncRes) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException { - LOGGER.info("[SCIM] Pull resources from endpoint " + this.getConfiguration().getEndPoint()); - for (S resource : scimClient.listResources()) { - pullSingleResourceFromScim(syncRes, resource); - } - } - - private void pushSingleResourceToScim(SynchronizationResult syncRes, K resource, KeycloakId id) throws InvalidResponseFromScimEndpointException, InconsistentScimMappingException { - try { - LOGGER.infof("[SCIM] Reconciling local resource %s", id); - if (shouldIgnoreForScimSynchronization(resource)) { - LOGGER.infof("[SCIM] Skip local resource %s", id); - return; - } - if (findMappingById(id).isPresent()) { - LOGGER.info("[SCIM] Replacing it"); - update(resource); - } else { - LOGGER.info("[SCIM] Creating it"); - create(resource); - } - syncRes.increaseUpdated(); - } catch (InvalidResponseFromScimEndpointException e) { - if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) { - LOGGER.warn("Error while syncing " + id + " to endpoint " + getConfiguration().getEndPoint(), e); - } else { - throw e; - } - } catch (InconsistentScimMappingException e) { - if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) { - LOGGER.warn("Inconsistent data for element " + id + " and endpoint " + getConfiguration().getEndPoint(), e); - } else { - throw e; - } - } - } - - - private void pullSingleResourceFromScim(SynchronizationResult syncRes, S resource) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException { - try { - LOGGER.infof("[SCIM] Reconciling remote resource %s", resource); - EntityOnRemoteScimId externalId = resource.getId() - .map(EntityOnRemoteScimId::new) - .orElseThrow(() -> new UnexpectedScimDataException("Remote SCIM resource doesn't have an id, cannot import it in Keycloak")); - if (validMappingAlreadyExists(externalId)) return; - - // Here no keycloak user/group matching the SCIM external id exists - // Try to match existing keycloak resource by properties (username, email, name) - Optional<KeycloakId> mapped = matchKeycloakMappingByScimProperties(resource); - if (mapped.isPresent()) { - // If found a mapped, update - LOGGER.info("[SCIM] Matched SCIM resource " + externalId + " from properties with keycloak entity " + mapped.get()); - createMapping(mapped.get(), externalId); - syncRes.increaseUpdated(); - } else { - // If not, create it locally or deleting it remotely (according to the configured Import Action) - createLocalOrDeleteRemote(syncRes, resource, externalId); - } - } catch (UnexpectedScimDataException e) { - if (skipOrStopStrategy.skipInvalidDataFromScimEndpoint(getConfiguration())) { - LOGGER.warn("[SCIM] Skipping element synchronisation because of invalid Scim Data for element " + resource.getId() + " : " + e.getMessage(), e); - } else { - throw e; - } - } catch (InconsistentScimMappingException e) { - if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) { - LOGGER.warn("[SCIM] Skipping element synchronisation because of inconsistent mapping for element " + resource.getId() + " : " + e.getMessage(), e); - } else { - throw e; - } - } catch (InvalidResponseFromScimEndpointException e) { - // Can only occur in case of a DELETE_REMOTE conflict action - if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) { - LOGGER.warn("[SCIM] Could not delete SCIM resource " + resource.getId() + " during synchronisation", e); - } else { - throw e; - } - } - - } - - private boolean validMappingAlreadyExists(EntityOnRemoteScimId externalId) { - Optional<ScimResourceMapping> optionalMapping = getScimResourceDao().findByExternalId(externalId, type); - // If an existing mapping exists, delete potential dangling references - if (optionalMapping.isPresent()) { - ScimResourceMapping mapping = optionalMapping.get(); - if (entityExists(mapping.getIdAsKeycloakId())) { - LOGGER.info("[SCIM] Valid mapping found, skipping"); - return true; - } else { - LOGGER.info("[SCIM] Delete a dangling mapping"); - getScimResourceDao().delete(mapping); - } - } - return false; - } - - private void createLocalOrDeleteRemote(SynchronizationResult syncRes, S resource, EntityOnRemoteScimId externalId) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException { - switch (scimProviderConfiguration.getImportAction()) { - case CREATE_LOCAL -> { - LOGGER.info("[SCIM] Create local resource for SCIM resource " + externalId); - KeycloakId id = createEntity(resource); - createMapping(id, externalId); - syncRes.increaseAdded(); - } - case DELETE_REMOTE -> { - LOGGER.info("[SCIM] Delete remote resource " + externalId); - scimClient.delete(externalId); - } - case NOTHING -> LOGGER.info("[SCIM] Import action set to NOTHING"); - } - } - - - protected abstract S scimRequestBodyForCreate(K roleMapperModel) throws InconsistentScimMappingException; - - protected abstract KeycloakId getId(K roleMapperModel); - - protected abstract boolean isMarkedToIgnore(K roleMapperModel); - - private void createMapping(KeycloakId keycloakId, EntityOnRemoteScimId externalId) { - getScimResourceDao().create(keycloakId, externalId, type); - } - - protected ScimResourceDao getScimResourceDao() { - return ScimResourceDao.newInstance(getKeycloakSession(), scimProviderConfiguration.getId()); - } - - private Optional<ScimResourceMapping> findMappingById(KeycloakId keycloakId) { - return getScimResourceDao().findById(keycloakId, type); - } - - private KeycloakSession getKeycloakSession() { - return keycloakSession; - } - - - protected abstract boolean shouldIgnoreForScimSynchronization(K resource); - - protected abstract Stream<K> getResourceStream(); - - protected abstract KeycloakId createEntity(S resource) throws UnexpectedScimDataException, InconsistentScimMappingException; - - protected abstract Optional<KeycloakId> matchKeycloakMappingByScimProperties(S resource) throws InconsistentScimMappingException; - - protected abstract boolean entityExists(KeycloakId keycloakId); - - public void sync(SynchronizationResult syncRes) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException, UnexpectedScimDataException { - if (this.scimProviderConfiguration.isPullFromScimSynchronisationActivated()) { - this.pullAllResourcesFromScim(syncRes); - } - if (this.scimProviderConfiguration.isPushToScimSynchronisationActivated()) { - this.pushAllResourcesToScim(syncRes); - } - } - - protected Meta newMetaLocation(EntityOnRemoteScimId externalId) { - Meta meta = new Meta(); - URI uri = getUri(type, externalId); - meta.setLocation(uri.toString()); - return meta; - } - - protected URI getUri(ScimResourceType type, EntityOnRemoteScimId externalId) { - try { - return new URI("%s/%s".formatted(type.getEndpoint(), externalId.asString())); - } catch (URISyntaxException e) { - throw new IllegalStateException("should never occur: can not format URI for type %s and id %s".formatted(type, externalId), e); - } - } - - protected KeycloakDao getKeycloakDao() { - return new KeycloakDao(getKeycloakSession()); - } - - @Override - public void close() { - scimClient.close(); - } - - public ScrimEndPointConfiguration getConfiguration() { - return scimProviderConfiguration; - } -} diff --git a/src/main/java/sh/libre/scim/core/service/EntityOnRemoteScimId.java b/src/main/java/sh/libre/scim/core/service/EntityOnRemoteScimId.java deleted file mode 100644 index df96a12323c933b683631b252b1b6529e94c5c5a..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/service/EntityOnRemoteScimId.java +++ /dev/null @@ -1,6 +0,0 @@ -package sh.libre.scim.core.service; - -public record EntityOnRemoteScimId( - String asString -) { -} diff --git a/src/main/java/sh/libre/scim/core/service/GroupScimService.java b/src/main/java/sh/libre/scim/core/service/GroupScimService.java deleted file mode 100644 index bd09f3e9b2442bad39b340eb59070eec278a244e..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/service/GroupScimService.java +++ /dev/null @@ -1,131 +0,0 @@ -package sh.libre.scim.core.service; - -import de.captaingoldfish.scim.sdk.common.resources.Group; -import de.captaingoldfish.scim.sdk.common.resources.complex.Meta; -import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Member; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.jboss.logging.Logger; -import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.UserModel; -import sh.libre.scim.core.ScrimEndPointConfiguration; -import sh.libre.scim.core.exceptions.InconsistentScimMappingException; -import sh.libre.scim.core.exceptions.SkipOrStopStrategy; -import sh.libre.scim.core.exceptions.UnexpectedScimDataException; -import sh.libre.scim.jpa.ScimResourceMapping; - -import java.net.URI; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Stream; - -public class GroupScimService extends AbstractScimService<GroupModel, Group> { - private static final Logger LOGGER = Logger.getLogger(GroupScimService.class); - - public GroupScimService(KeycloakSession keycloakSession, ScrimEndPointConfiguration scimProviderConfiguration, SkipOrStopStrategy skipOrStopStrategy) { - super(keycloakSession, scimProviderConfiguration, ScimResourceType.GROUP, skipOrStopStrategy); - } - - @Override - protected Stream<GroupModel> getResourceStream() { - return getKeycloakDao().getGroupsStream(); - } - - @Override - protected boolean entityExists(KeycloakId keycloakId) { - return getKeycloakDao().groupExists(keycloakId); - } - - @Override - protected Optional<KeycloakId> matchKeycloakMappingByScimProperties(Group resource) { - Set<String> names = new TreeSet<>(); - resource.getId().ifPresent(names::add); - resource.getDisplayName().ifPresent(names::add); - try (Stream<GroupModel> groupsStream = getKeycloakDao().getGroupsStream()) { - Optional<GroupModel> group = groupsStream - .filter(groupModel -> names.contains(groupModel.getName())) - .findFirst(); - return group - .map(GroupModel::getId) - .map(KeycloakId::new); - } - } - - @Override - protected KeycloakId createEntity(Group resource) throws UnexpectedScimDataException, InconsistentScimMappingException { - String displayName = resource.getDisplayName() - .filter(StringUtils::isNotBlank) - .orElseThrow(() -> new UnexpectedScimDataException("Remote Scim group has empty name, can't create. Resource id = %s".formatted(resource.getId()))); - GroupModel group = getKeycloakDao().createGroup(displayName); - List<Member> groupMembers = resource.getMembers(); - if (CollectionUtils.isNotEmpty(groupMembers)) { - for (Member groupMember : groupMembers) { - EntityOnRemoteScimId externalId = groupMember.getValue() - .map(EntityOnRemoteScimId::new) - .orElseThrow(() -> new UnexpectedScimDataException("can't create group member for group '%s' without id: ".formatted(displayName) + resource)); - KeycloakId userId = getScimResourceDao().findUserByExternalId(externalId) - .map(ScimResourceMapping::getIdAsKeycloakId) - .orElseThrow(() -> new InconsistentScimMappingException("can't find mapping for group member %s".formatted(externalId))); - UserModel userModel = getKeycloakDao().getUserById(userId); - userModel.joinGroup(group); - } - } - return new KeycloakId(group.getId()); - } - - @Override - protected boolean isMarkedToIgnore(GroupModel groupModel) { - return BooleanUtils.TRUE.equals(groupModel.getFirstAttribute("scim-skip")); - } - - @Override - protected KeycloakId getId(GroupModel groupModel) { - return new KeycloakId(groupModel.getId()); - } - - @Override - protected Group scimRequestBodyForCreate(GroupModel groupModel) throws InconsistentScimMappingException { - Set<KeycloakId> members = getKeycloakDao().getGroupMembers(groupModel); - Group group = new Group(); - group.setExternalId(groupModel.getId()); - group.setDisplayName(groupModel.getName()); - for (KeycloakId member : members) { - Member groupMember = new Member(); - Optional<ScimResourceMapping> optionalGroupMemberMapping = getScimResourceDao().findUserById(member); - if (optionalGroupMemberMapping.isPresent()) { - ScimResourceMapping groupMemberMapping = optionalGroupMemberMapping.get(); - EntityOnRemoteScimId externalIdAsEntityOnRemoteScimId = groupMemberMapping.getExternalIdAsEntityOnRemoteScimId(); - groupMember.setValue(externalIdAsEntityOnRemoteScimId.asString()); - URI ref = getUri(ScimResourceType.USER, externalIdAsEntityOnRemoteScimId); - groupMember.setRef(ref.toString()); - group.addMember(groupMember); - } else { - String message = "Unmapped member " + member + " for group " + groupModel.getId(); - if (skipOrStopStrategy.allowMissingMembersWhenPushingGroupToScim(this.getConfiguration())) { - LOGGER.warn(message); - } else { - throw new InconsistentScimMappingException(message); - } - } - } - return group; - } - - @Override - protected Group scimRequestBodyForUpdate(GroupModel groupModel, EntityOnRemoteScimId externalId) throws InconsistentScimMappingException { - Group group = scimRequestBodyForCreate(groupModel); - group.setId(externalId.asString()); - Meta meta = newMetaLocation(externalId); - group.setMeta(meta); - return group; - } - - @Override - protected boolean shouldIgnoreForScimSynchronization(GroupModel resource) { - return false; - } -} diff --git a/src/main/java/sh/libre/scim/core/service/KeycloakDao.java b/src/main/java/sh/libre/scim/core/service/KeycloakDao.java deleted file mode 100644 index f4c406c351300660e86ef3e423515cc018924425..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/service/KeycloakDao.java +++ /dev/null @@ -1,81 +0,0 @@ -package sh.libre.scim.core.service; - -import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; - -import java.util.Collections; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class KeycloakDao { - - private final KeycloakSession keycloakSession; - - public KeycloakDao(KeycloakSession keycloakSession) { - this.keycloakSession = keycloakSession; - } - - private KeycloakSession getKeycloakSession() { - return keycloakSession; - } - - private RealmModel getRealm() { - return getKeycloakSession().getContext().getRealm(); - } - - public boolean groupExists(KeycloakId groupId) { - GroupModel group = getKeycloakSession().groups().getGroupById(getRealm(), groupId.asString()); - return group != null; - } - - public boolean userExists(KeycloakId userId) { - UserModel user = getUserById(userId); - return user != null; - } - - public UserModel getUserById(KeycloakId userId) { - return getKeycloakSession().users().getUserById(getRealm(), userId.asString()); - } - - public GroupModel getGroupById(KeycloakId groupId) { - return getKeycloakSession().groups().getGroupById(getRealm(), groupId.asString()); - } - - - public Stream<GroupModel> getGroupsStream() { - return getKeycloakSession().groups().getGroupsStream(getRealm()); - } - - public GroupModel createGroup(String displayName) { - return getKeycloakSession().groups().createGroup(getRealm(), displayName); - } - - public Set<KeycloakId> getGroupMembers(GroupModel groupModel) { - return getKeycloakSession().users() - .getGroupMembersStream(getRealm(), groupModel) - .map(UserModel::getId) - .map(KeycloakId::new) - .collect(Collectors.toSet()); - } - - public Stream<UserModel> getUsersStream() { - return getKeycloakSession().users().searchForUserStream(getRealm(), Collections.emptyMap()); - } - - public UserModel getUserByUsername(String username) { - return getKeycloakSession().users().getUserByUsername(getRealm(), username); - } - - public UserModel getUserByEmail(String email) { - return getKeycloakSession().users().getUserByEmail(getRealm(), email); - } - - public UserModel addUser(String username) { - return getKeycloakSession().users().addUser(getRealm(), username); - } - - -} diff --git a/src/main/java/sh/libre/scim/core/service/KeycloakId.java b/src/main/java/sh/libre/scim/core/service/KeycloakId.java deleted file mode 100644 index 04bad470b3b40f6178ffde70b15a6e3f253f1d53..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/service/KeycloakId.java +++ /dev/null @@ -1,7 +0,0 @@ -package sh.libre.scim.core.service; - -public record KeycloakId( - String asString -) { - -} diff --git a/src/main/java/sh/libre/scim/core/service/ScimClient.java b/src/main/java/sh/libre/scim/core/service/ScimClient.java deleted file mode 100644 index de3b4d7c0f4b8d24d406249c8268c1d3f9d3d12c..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/service/ScimClient.java +++ /dev/null @@ -1,155 +0,0 @@ -package sh.libre.scim.core.service; - -import com.google.common.net.HttpHeaders; -import de.captaingoldfish.scim.sdk.client.ScimClientConfig; -import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder; -import de.captaingoldfish.scim.sdk.client.response.ServerResponse; -import de.captaingoldfish.scim.sdk.common.resources.ResourceNode; -import de.captaingoldfish.scim.sdk.common.response.ListResponse; -import io.github.resilience4j.core.IntervalFunction; -import io.github.resilience4j.retry.Retry; -import io.github.resilience4j.retry.RetryConfig; -import io.github.resilience4j.retry.RetryRegistry; -import jakarta.ws.rs.ProcessingException; -import org.jboss.logging.Logger; -import sh.libre.scim.core.ScrimEndPointConfiguration; -import sh.libre.scim.core.exceptions.InvalidResponseFromScimEndpointException; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -public class ScimClient<S extends ResourceNode> implements AutoCloseable { - private static final Logger LOGGER = Logger.getLogger(ScimClient.class); - - private final RetryRegistry retryRegistry; - - private final ScimRequestBuilder scimRequestBuilder; - - private final ScimResourceType scimResourceType; - private final boolean logAllRequests; - - private ScimClient(ScimRequestBuilder scimRequestBuilder, ScimResourceType scimResourceType, boolean detailedLogs) { - this.scimRequestBuilder = scimRequestBuilder; - this.scimResourceType = scimResourceType; - RetryConfig retryConfig = RetryConfig.custom() - .maxAttempts(10) - .intervalFunction(IntervalFunction.ofExponentialBackoff()) - .retryExceptions(ProcessingException.class) - .build(); - retryRegistry = RetryRegistry.of(retryConfig); - this.logAllRequests = detailedLogs; - } - - public static <T extends ResourceNode> ScimClient<T> open(ScrimEndPointConfiguration scimProviderConfiguration, ScimResourceType scimResourceType) { - String scimApplicationBaseUrl = scimProviderConfiguration.getEndPoint(); - Map<String, String> httpHeaders = new HashMap<>(); - httpHeaders.put(HttpHeaders.AUTHORIZATION, scimProviderConfiguration.getAuthorizationHeaderValue()); - httpHeaders.put(HttpHeaders.CONTENT_TYPE, scimProviderConfiguration.getContentType()); - ScimClientConfig scimClientConfig = ScimClientConfig.builder() - .httpHeaders(httpHeaders) - .connectTimeout(5) - .requestTimeout(5) - .socketTimeout(5) - .build(); - ScimRequestBuilder scimRequestBuilder = - new ScimRequestBuilder( - scimApplicationBaseUrl, - scimClientConfig - ); - return new ScimClient<>(scimRequestBuilder, scimResourceType, scimProviderConfiguration.isLogAllScimRequests()); - } - - public EntityOnRemoteScimId create(KeycloakId id, S scimForCreation) throws InvalidResponseFromScimEndpointException { - Optional<String> scimForCreationId = scimForCreation.getId(); - if (scimForCreationId.isPresent()) { - throw new IllegalArgumentException( - "User to create should never have an existing id: %s %s".formatted(id, scimForCreationId.get()) - ); - } - try { - Retry retry = retryRegistry.retry("create-%s".formatted(id.asString())); - if (logAllRequests) { - LOGGER.info("[SCIM] Sending CREATE " + scimForCreation.toPrettyString() + "\n to " + getScimEndpoint()); - } - ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder - .create(getResourceClass(), getScimEndpoint()) - .setResource(scimForCreation) - .sendRequest() - ); - checkResponseIsSuccess(response); - S resource = response.getResource(); - return resource.getId() - .map(EntityOnRemoteScimId::new) - .orElseThrow(() -> new InvalidResponseFromScimEndpointException(response, "Created SCIM resource does not have id")); - - } catch (Exception e) { - LOGGER.warn(e); - throw new InvalidResponseFromScimEndpointException("Exception while retrying create " + e.getMessage(), e); - } - } - - private void checkResponseIsSuccess(ServerResponse<S> response) throws InvalidResponseFromScimEndpointException { - if (logAllRequests) { - LOGGER.info("[SCIM] Server response " + response.getHttpStatus() + "\n" + response.getResponseBody()); - } - if (!response.isSuccess()) { - throw new InvalidResponseFromScimEndpointException(response, "Server answered with status " + response.getResponseBody() + ": " + response.getResponseBody()); - } - } - - private String getScimEndpoint() { - return scimResourceType.getEndpoint(); - } - - private Class<S> getResourceClass() { - return scimResourceType.getResourceClass(); - } - - public void update(EntityOnRemoteScimId externalId, S scimForReplace) throws InvalidResponseFromScimEndpointException { - Retry retry = retryRegistry.retry("replace-%s".formatted(externalId.asString())); - try { - if (logAllRequests) { - LOGGER.info("[SCIM] Sending UPDATE " + scimForReplace.toPrettyString() + "\n to " + getScimEndpoint()); - } - ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder - .update(getResourceClass(), getScimEndpoint(), externalId.asString()) - .setResource(scimForReplace) - .sendRequest() - ); - checkResponseIsSuccess(response); - } catch (Exception e) { - LOGGER.warn(e); - throw new InvalidResponseFromScimEndpointException("Exception while retrying update " + e.getMessage(), e); - } - } - - public void delete(EntityOnRemoteScimId externalId) throws InvalidResponseFromScimEndpointException { - Retry retry = retryRegistry.retry("delete-%s".formatted(externalId.asString())); - if (logAllRequests) { - LOGGER.info("[SCIM] Sending DELETE to " + getScimEndpoint()); - } - try { - ServerResponse<S> response = retry.executeSupplier(() -> scimRequestBuilder - .delete(getResourceClass(), getScimEndpoint(), externalId.asString()) - .sendRequest() - ); - checkResponseIsSuccess(response); - } catch (Exception e) { - LOGGER.warn(e); - throw new InvalidResponseFromScimEndpointException("Exception while retrying delete " + e.getMessage(), e); - } - } - - @Override - public void close() { - scimRequestBuilder.close(); - } - - public List<S> listResources() { - ServerResponse<ListResponse<S>> response = scimRequestBuilder.list(getResourceClass(), getScimEndpoint()).get().sendRequest(); - ListResponse<S> resourceTypeListResponse = response.getResource(); - return resourceTypeListResponse.getListedResources(); - } -} diff --git a/src/main/java/sh/libre/scim/core/service/ScimResourceType.java b/src/main/java/sh/libre/scim/core/service/ScimResourceType.java deleted file mode 100644 index b90845b6cfd63852c41218e40222a24a639cf13b..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/service/ScimResourceType.java +++ /dev/null @@ -1,29 +0,0 @@ -package sh.libre.scim.core.service; - -import de.captaingoldfish.scim.sdk.common.resources.Group; -import de.captaingoldfish.scim.sdk.common.resources.ResourceNode; -import de.captaingoldfish.scim.sdk.common.resources.User; - -public enum ScimResourceType { - - USER("/Users", User.class), - - GROUP("/Groups", Group.class); - - private final String endpoint; - - private final Class<? extends ResourceNode> resourceClass; - - ScimResourceType(String endpoint, Class<? extends ResourceNode> resourceClass) { - this.endpoint = endpoint; - this.resourceClass = resourceClass; - } - - public String getEndpoint() { - return endpoint; - } - - public <T extends ResourceNode> Class<T> getResourceClass() { - return (Class<T>) resourceClass; - } -} diff --git a/src/main/java/sh/libre/scim/core/service/UserScimService.java b/src/main/java/sh/libre/scim/core/service/UserScimService.java deleted file mode 100644 index c0262f3d4aeb0b8fca2ea1c397850d1c845c7a8d..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/core/service/UserScimService.java +++ /dev/null @@ -1,145 +0,0 @@ -package sh.libre.scim.core.service; - -import de.captaingoldfish.scim.sdk.common.resources.User; -import de.captaingoldfish.scim.sdk.common.resources.complex.Meta; -import de.captaingoldfish.scim.sdk.common.resources.complex.Name; -import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Email; -import de.captaingoldfish.scim.sdk.common.resources.multicomplex.MultiComplexNode; -import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.jboss.logging.Logger; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RoleMapperModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import sh.libre.scim.core.ScrimEndPointConfiguration; -import sh.libre.scim.core.exceptions.InconsistentScimMappingException; -import sh.libre.scim.core.exceptions.SkipOrStopStrategy; -import sh.libre.scim.core.exceptions.UnexpectedScimDataException; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Stream; - -public class UserScimService extends AbstractScimService<UserModel, User> { - private static final Logger LOGGER = Logger.getLogger(UserScimService.class); - - public UserScimService( - KeycloakSession keycloakSession, - ScrimEndPointConfiguration scimProviderConfiguration, - SkipOrStopStrategy skipOrStopStrategy) { - super(keycloakSession, scimProviderConfiguration, ScimResourceType.USER, skipOrStopStrategy); - } - - @Override - protected Stream<UserModel> getResourceStream() { - return getKeycloakDao().getUsersStream(); - } - - @Override - protected boolean entityExists(KeycloakId keycloakId) { - return getKeycloakDao().userExists(keycloakId); - } - - @Override - protected Optional<KeycloakId> matchKeycloakMappingByScimProperties(User resource) throws InconsistentScimMappingException { - Optional<KeycloakId> matchedByUsername = resource.getUserName() - .map(getKeycloakDao()::getUserByUsername) - .map(this::getId); - Optional<KeycloakId> matchedByEmail = resource.getEmails().stream() - .findFirst() - .flatMap(MultiComplexNode::getValue) - .map(getKeycloakDao()::getUserByEmail) - .map(this::getId); - if (matchedByUsername.isPresent() - && matchedByEmail.isPresent() - && !matchedByUsername.equals(matchedByEmail)) { - String inconstencyErrorMessage = "Found 2 possible users for remote user " + matchedByUsername.get() + " - " + matchedByEmail.get(); - LOGGER.warn(inconstencyErrorMessage); - throw new InconsistentScimMappingException(inconstencyErrorMessage); - } - if (matchedByUsername.isPresent()) { - return matchedByUsername; - } - return matchedByEmail; - } - - @Override - protected KeycloakId createEntity(User resource) throws UnexpectedScimDataException { - String username = resource.getUserName() - .filter(StringUtils::isNotBlank) - .orElseThrow(() -> new UnexpectedScimDataException("Remote Scim user has empty username, can't create. Resource id = %s".formatted(resource.getId()))); - UserModel user = getKeycloakDao().addUser(username); - resource.getEmails().stream() - .findFirst() - .flatMap(MultiComplexNode::getValue) - .ifPresent(user::setEmail); - boolean userEnabled = resource.isActive().orElse(false); - user.setEnabled(userEnabled); - return new KeycloakId(user.getId()); - } - - @Override - protected boolean isMarkedToIgnore(UserModel userModel) { - return BooleanUtils.TRUE.equals(userModel.getFirstAttribute("scim-skip")); - } - - @Override - protected KeycloakId getId(UserModel userModel) { - return new KeycloakId(userModel.getId()); - } - - @Override - protected User scimRequestBodyForCreate(UserModel roleMapperModel) { - String firstAndLastName = String.format("%s %s", - StringUtils.defaultString(roleMapperModel.getFirstName()), - StringUtils.defaultString(roleMapperModel.getLastName())).trim(); - String displayName = Objects.toString(firstAndLastName, roleMapperModel.getUsername()); - Stream<RoleModel> groupRoleModels = roleMapperModel.getGroupsStream().flatMap(RoleMapperModel::getRoleMappingsStream); - Stream<RoleModel> roleModels = roleMapperModel.getRoleMappingsStream(); - Stream<RoleModel> allRoleModels = Stream.concat(groupRoleModels, roleModels); - List<PersonRole> roles = allRoleModels - .filter(r -> BooleanUtils.TRUE.equals(r.getFirstAttribute("scim"))) - .map(RoleModel::getName) - .map(roleName -> { - PersonRole personRole = new PersonRole(); - personRole.setValue(roleName); - return personRole; - }) - .toList(); - User user = new User(); - user.setRoles(roles); - user.setExternalId(roleMapperModel.getId()); - user.setUserName(roleMapperModel.getUsername()); - user.setDisplayName(displayName); - Name name = new Name(); - name.setFamilyName(roleMapperModel.getLastName()); - name.setGivenName(roleMapperModel.getFirstName()); - user.setName(name); - List<Email> emails = new ArrayList<>(); - if (roleMapperModel.getEmail() != null) { - emails.add( - Email.builder().value(roleMapperModel.getEmail()).build()); - } - user.setEmails(emails); - user.setActive(roleMapperModel.isEnabled()); - return user; - } - - @Override - protected User scimRequestBodyForUpdate(UserModel userModel, EntityOnRemoteScimId externalId) { - User user = scimRequestBodyForCreate(userModel); - user.setId(externalId.asString()); - Meta meta = newMetaLocation(externalId); - user.setMeta(meta); - return user; - } - - @Override - protected boolean shouldIgnoreForScimSynchronization(UserModel userModel) { - return "admin".equals(userModel.getUsername()); - } -} diff --git a/src/main/java/sh/libre/scim/event/ScimBackgroundGroupMembershipUpdater.java b/src/main/java/sh/libre/scim/event/ScimBackgroundGroupMembershipUpdater.java deleted file mode 100644 index 4c49f74f671b1679698bc3c5710e5eb6d9b9f171..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/event/ScimBackgroundGroupMembershipUpdater.java +++ /dev/null @@ -1,74 +0,0 @@ -package sh.libre.scim.event; - -import org.jboss.logging.Logger; -import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.RealmModel; -import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.timer.TimerProvider; -import sh.libre.scim.core.ScimDispatcher; - -import java.time.Duration; - -/** - * In charge of making background checks and sent - * UPDATE requests from group for which membership information has changed. - * <p> - * This is required to avoid immediate group membership updates which could cause - * to incorrect group members list in case of concurrent group membership changes. - */ -public class ScimBackgroundGroupMembershipUpdater { - public static final String GROUP_DIRTY_SINCE_ATTRIBUTE_NAME = "scim-dirty-since"; - - private static final Logger LOGGER = Logger.getLogger(ScimBackgroundGroupMembershipUpdater.class); - // Update check loop will run every time this delay has passed - private static final long UPDATE_CHECK_DELAY_MS = 2000; - // If a group is marked dirty since less that this debounce delay, wait for the next update check loop - private static final long DEBOUNCE_DELAY_MS = 1200; - private final KeycloakSessionFactory sessionFactory; - - public ScimBackgroundGroupMembershipUpdater(KeycloakSessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - } - - public void startBackgroundUpdates() { - // Every UPDATE_CHECK_DELAY_MS, check for dirty groups and send updates if required - try (KeycloakSession keycloakSession = sessionFactory.create()) { - TimerProvider timer = keycloakSession.getProvider(TimerProvider.class); - timer.scheduleTask(taskSession -> { - for (RealmModel realm : taskSession.realms().getRealmsStream().toList()) { - dispatchDirtyGroupsUpdates(realm); - } - }, Duration.ofMillis(UPDATE_CHECK_DELAY_MS).toMillis(), "scim-background"); - } - } - - private void dispatchDirtyGroupsUpdates(RealmModel realm) { - KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> { - session.getContext().setRealm(realm); - ScimDispatcher dispatcher = new ScimDispatcher(session); - // Identify groups marked as dirty by the ScimEventListenerProvider - for (GroupModel group : session.groups().getGroupsStream(realm) - .filter(this::isDirtyGroup).toList()) { - LOGGER.infof("[SCIM] Group %s is dirty, dispatch an update", group.getName()); - // If dirty : dispatch a group update to all clients and mark it clean - dispatcher.dispatchGroupModificationToAll(client -> client.update(group)); - group.removeAttribute(GROUP_DIRTY_SINCE_ATTRIBUTE_NAME); - } - dispatcher.close(); - }); - } - - private boolean isDirtyGroup(GroupModel g) { - String groupDirtySinceAttribute = g.getFirstAttribute(GROUP_DIRTY_SINCE_ATTRIBUTE_NAME); - try { - long groupDirtySince = Long.parseLong(groupDirtySinceAttribute); - // Must be dirty for more than DEBOUNCE_DELAY_MS - // (otherwise update will be dispatched in next scheduled loop) - return System.currentTimeMillis() - groupDirtySince > DEBOUNCE_DELAY_MS; - } catch (NumberFormatException e) { - return false; - } - } -} diff --git a/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java b/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java deleted file mode 100644 index 2c177b0e47d1e7df86de5fe2413ccb114b4d7333..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java +++ /dev/null @@ -1,247 +0,0 @@ -package sh.libre.scim.event; - -import org.jboss.logging.Logger; -import org.keycloak.component.ComponentModel; -import org.keycloak.events.Event; -import org.keycloak.events.EventListenerProvider; -import org.keycloak.events.EventType; -import org.keycloak.events.admin.AdminEvent; -import org.keycloak.events.admin.OperationType; -import org.keycloak.events.admin.ResourceType; -import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.UserModel; -import sh.libre.scim.core.ScimDispatcher; -import sh.libre.scim.core.ScimEndpointConfigurationStorageProviderFactory; -import sh.libre.scim.core.service.KeycloakDao; -import sh.libre.scim.core.service.KeycloakId; -import sh.libre.scim.core.service.ScimResourceType; - -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -/** - * An Event listener reacting to Keycloak models modification - * (e.g. User creation, Group deletion, membership modifications, endpoint configuration change...) - * by propagating it to all registered Scim endpoints. - */ -public class ScimEventListenerProvider implements EventListenerProvider { - - private static final Logger LOGGER = Logger.getLogger(ScimEventListenerProvider.class); - - private final ScimDispatcher dispatcher; - - private final KeycloakSession session; - - private final KeycloakDao keycloakDao; - - private final Map<ResourceType, Pattern> listenedEventPathPatterns = Map.of( - ResourceType.USER, Pattern.compile("users/(.+)"), - ResourceType.GROUP, Pattern.compile("groups/([\\w-]+)(/children)?"), - ResourceType.GROUP_MEMBERSHIP, Pattern.compile("users/(.+)/groups/(.+)"), - ResourceType.REALM_ROLE_MAPPING, Pattern.compile("^(.+)/(.+)/role-mappings"), - ResourceType.COMPONENT, Pattern.compile("components/(.+)") - ); - - public ScimEventListenerProvider(KeycloakSession session) { - this.session = session; - this.keycloakDao = new KeycloakDao(session); - this.dispatcher = new ScimDispatcher(session); - } - - @Override - public void onEvent(Event event) { - // React to User-related event : creation, deletion, update - EventType eventType = event.getType(); - KeycloakId eventUserId = new KeycloakId(event.getUserId()); - switch (eventType) { - case REGISTER -> { - LOGGER.infof("[SCIM] Propagate User Registration - %s", eventUserId); - UserModel user = getUser(eventUserId); - dispatcher.dispatchUserModificationToAll(client -> client.create(user)); - } - case UPDATE_EMAIL, UPDATE_PROFILE -> { - LOGGER.infof("[SCIM] Propagate User %s - %s", eventType, eventUserId); - UserModel user = getUser(eventUserId); - dispatcher.dispatchUserModificationToAll(client -> client.update(user)); - } - case DELETE_ACCOUNT -> { - LOGGER.infof("[SCIM] Propagate User deletion - %s", eventUserId); - dispatcher.dispatchUserModificationToAll(client -> client.delete(eventUserId)); - } - default -> { - // No other event has to be propagated to Scim endpoints - } - } - } - - - @Override - public void onEvent(AdminEvent event, boolean includeRepresentation) { - // Step 1: check if event is relevant for propagation through SCIM - Pattern pattern = listenedEventPathPatterns.get(event.getResourceType()); - if (pattern == null) - return; - Matcher matcher = pattern.matcher(event.getResourcePath()); - if (!matcher.find()) - return; - - - // Step 2: propagate event (if needed) according to its resource type - switch (event.getResourceType()) { - case USER -> { - KeycloakId userId = new KeycloakId(matcher.group(1)); - handleUserEvent(event, userId); - } - case GROUP -> { - KeycloakId groupId = new KeycloakId(matcher.group(1)); - handleGroupEvent(event, groupId); - } - case GROUP_MEMBERSHIP -> { - KeycloakId userId = new KeycloakId(matcher.group(1)); - KeycloakId groupId = new KeycloakId(matcher.group(2)); - handleGroupMemberShipEvent(event, userId, groupId); - } - case REALM_ROLE_MAPPING -> { - String rawResourceType = matcher.group(1); - ScimResourceType type = switch (rawResourceType) { - case "users" -> ScimResourceType.USER; - case "groups" -> ScimResourceType.GROUP; - default -> throw new IllegalArgumentException("Unsupported resource type: " + rawResourceType); - }; - KeycloakId id = new KeycloakId(matcher.group(2)); - handleRoleMappingEvent(event, type, id); - } - case COMPONENT -> { - String id = matcher.group(1); - handleScimEndpointConfigurationEvent(event, id); - - } - default -> { - // No other resource modification has to be propagated to Scim endpoints - } - } - } - - - private void handleUserEvent(AdminEvent userEvent, KeycloakId userId) { - LOGGER.infof("[SCIM] Propagate User %s - %s", userEvent.getOperationType(), userId); - switch (userEvent.getOperationType()) { - case CREATE -> { - UserModel user = getUser(userId); - dispatcher.dispatchUserModificationToAll(client -> client.create(user)); - user.getGroupsStream().forEach(group -> - dispatcher.dispatchGroupModificationToAll(client -> client.update(group) - )); - } - case UPDATE -> { - UserModel user = getUser(userId); - dispatcher.dispatchUserModificationToAll(client -> client.update(user)); - } - case DELETE -> dispatcher.dispatchUserModificationToAll(client -> client.delete(userId)); - default -> { - // ACTION userEvent are not relevant, nothing to do - } - } - } - - /** - * Propagating the given group-related event to Scim endpoints. - * - * @param event the event to propagate - * @param groupId event target's id - */ - private void handleGroupEvent(AdminEvent event, KeycloakId groupId) { - LOGGER.infof("[SCIM] Propagate Group %s - %s", event.getOperationType(), groupId); - switch (event.getOperationType()) { - case CREATE -> { - GroupModel group = getGroup(groupId); - dispatcher.dispatchGroupModificationToAll(client -> client.create(group)); - } - case UPDATE -> { - GroupModel group = getGroup(groupId); - dispatcher.dispatchGroupModificationToAll(client -> client.update(group)); - } - case DELETE -> dispatcher.dispatchGroupModificationToAll(client -> client.delete(groupId)); - default -> { - // ACTION event are not relevant, nothing to do - } - } - } - - private void handleGroupMemberShipEvent(AdminEvent groupMemberShipEvent, KeycloakId userId, KeycloakId groupId) { - LOGGER.infof("[SCIM] Propagate GroupMemberShip %s - User %s Group %s", groupMemberShipEvent.getOperationType(), userId, groupId); - // Step 1: update USER immediately - GroupModel group = getGroup(groupId); - UserModel user = getUser(userId); - dispatcher.dispatchUserModificationToAll(client -> client.update(user)); - - // Step 2: delayed GROUP update : - // if several users are added to the group simultaneously in different Keycloack sessions - // update the group in the context of the current session may not reflect those other changes - // We trigger a delayed update by setting an attribute on the group (that will be handled by ScimBackgroundGroupMembershipUpdaters) - group.setSingleAttribute(ScimBackgroundGroupMembershipUpdater.GROUP_DIRTY_SINCE_ATTRIBUTE_NAME, "" + System.currentTimeMillis()); - } - - private void handleRoleMappingEvent(AdminEvent roleMappingEvent, ScimResourceType type, KeycloakId id) { - LOGGER.infof("[SCIM] Propagate RoleMapping %s - %s %s", roleMappingEvent.getOperationType(), type, id); - switch (type) { - case USER -> { - UserModel user = getUser(id); - dispatcher.dispatchUserModificationToAll(client -> client.update(user)); - } - case GROUP -> { - GroupModel group = getGroup(id); - session.users() - .getGroupMembersStream(session.getContext().getRealm(), group) - .forEach(user -> - dispatcher.dispatchUserModificationToAll(client -> client.update(user) - )); - } - default -> { - // No other type is relevant for propagation - } - } - } - - private void handleScimEndpointConfigurationEvent(AdminEvent event, String id) { - // In case of a component deletion - if (event.getOperationType() == OperationType.DELETE) { - // Check if it was a Scim endpoint configuration, and forward deletion if so - Stream<ComponentModel> scimEndpointConfigurationsWithDeletedId = session.getContext().getRealm().getComponentsStream() - .filter(m -> ScimEndpointConfigurationStorageProviderFactory.ID.equals(m.getProviderId()) - && id.equals(m.getId())); - if (scimEndpointConfigurationsWithDeletedId.iterator().hasNext()) { - LOGGER.infof("[SCIM] SCIM Endpoint configuration DELETE - %s ", id); - dispatcher.refreshActiveScimEndpoints(); - } - } else { - // In case of CREATE or UPDATE, we can directly use the string representation - // to check if it defines a SCIM endpoint (faster) - if (event.getRepresentation() != null - && event.getRepresentation().contains("\"providerId\":\"scim\"")) { - LOGGER.infof("[SCIM] SCIM Endpoint configuration CREATE - %s ", id); - dispatcher.refreshActiveScimEndpoints(); - } - } - - } - - - private UserModel getUser(KeycloakId id) { - return keycloakDao.getUserById(id); - } - - private GroupModel getGroup(KeycloakId id) { - return keycloakDao.getGroupById(id); - } - - @Override - public void close() { - dispatcher.close(); - } - - -} diff --git a/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java b/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java deleted file mode 100644 index c7b437a287dcb0aa1001a134aad3749505b06925..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -package sh.libre.scim.event; - -import org.keycloak.Config.Scope; -import org.keycloak.events.EventListenerProvider; -import org.keycloak.events.EventListenerProviderFactory; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; - -public class ScimEventListenerProviderFactory implements EventListenerProviderFactory { - - @Override - public EventListenerProvider create(KeycloakSession session) { - return new ScimEventListenerProvider(session); - } - - @Override - public String getId() { - return "scim"; - } - - @Override - public void init(Scope config) { - // Nothing to initialize - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - // Nothing to initialize - } - - @Override - public void close() { - // Nothing to close - } - -} diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceDao.java b/src/main/java/sh/libre/scim/jpa/ScimResourceDao.java deleted file mode 100644 index 4deec373b952c59c333974b645fa276b9cc37d4a..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/jpa/ScimResourceDao.java +++ /dev/null @@ -1,96 +0,0 @@ -package sh.libre.scim.jpa; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.NoResultException; -import jakarta.persistence.TypedQuery; -import org.keycloak.connections.jpa.JpaConnectionProvider; -import org.keycloak.models.KeycloakSession; -import sh.libre.scim.core.service.EntityOnRemoteScimId; -import sh.libre.scim.core.service.KeycloakId; -import sh.libre.scim.core.service.ScimResourceType; - -import java.util.Optional; - -public class ScimResourceDao { - - private final String realmId; - - private final String componentId; - - private final EntityManager entityManager; - - private ScimResourceDao(String realmId, String componentId, EntityManager entityManager) { - this.realmId = realmId; - this.componentId = componentId; - this.entityManager = entityManager; - } - - public static ScimResourceDao newInstance(KeycloakSession keycloakSession, String componentId) { - String realmId = keycloakSession.getContext().getRealm().getId(); - EntityManager entityManager = keycloakSession.getProvider(JpaConnectionProvider.class).getEntityManager(); - return new ScimResourceDao(realmId, componentId, entityManager); - } - - private EntityManager getEntityManager() { - return entityManager; - } - - private String getRealmId() { - return realmId; - } - - private String getComponentId() { - return componentId; - } - - public void create(KeycloakId id, EntityOnRemoteScimId externalId, ScimResourceType type) { - ScimResourceMapping entity = new ScimResourceMapping(); - entity.setType(type.name()); - entity.setExternalId(externalId.asString()); - entity.setComponentId(componentId); - entity.setRealmId(realmId); - entity.setId(id.asString()); - entityManager.persist(entity); - } - - private TypedQuery<ScimResourceMapping> getScimResourceTypedQuery(String queryName, String id, ScimResourceType type) { - return getEntityManager() - .createNamedQuery(queryName, ScimResourceMapping.class) - .setParameter("type", type.name()) - .setParameter("realmId", getRealmId()) - .setParameter("componentId", getComponentId()) - .setParameter("id", id); - } - - public Optional<ScimResourceMapping> findByExternalId(EntityOnRemoteScimId externalId, ScimResourceType type) { - try { - return Optional.of( - getScimResourceTypedQuery("findByExternalId", externalId.asString(), type).getSingleResult() - ); - } catch (NoResultException e) { - return Optional.empty(); - } - } - - public Optional<ScimResourceMapping> findById(KeycloakId keycloakId, ScimResourceType type) { - try { - return Optional.of( - getScimResourceTypedQuery("findById", keycloakId.asString(), type).getSingleResult() - ); - } catch (NoResultException e) { - return Optional.empty(); - } - } - - public Optional<ScimResourceMapping> findUserById(KeycloakId id) { - return findById(id, ScimResourceType.USER); - } - - public Optional<ScimResourceMapping> findUserByExternalId(EntityOnRemoteScimId externalId) { - return findByExternalId(externalId, ScimResourceType.USER); - } - - public void delete(ScimResourceMapping resource) { - entityManager.remove(resource); - } -} diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceId.java b/src/main/java/sh/libre/scim/jpa/ScimResourceId.java deleted file mode 100644 index d0abddf2b1bf9c93c473f94af1dbf061afbb92f6..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/jpa/ScimResourceId.java +++ /dev/null @@ -1,83 +0,0 @@ -package sh.libre.scim.jpa; - -import org.apache.commons.lang3.StringUtils; - -import java.io.Serializable; -import java.util.Objects; - -public class ScimResourceId implements Serializable { - private String id; - private String realmId; - private String componentId; - private String type; - private String externalId; - - public ScimResourceId() { - } - - public ScimResourceId(String id, String realmId, String componentId, String type, String externalId) { - this.setId(id); - this.setRealmId(realmId); - this.setComponentId(componentId); - this.setType(type); - this.setExternalId(externalId); - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getRealmId() { - return realmId; - } - - public void setRealmId(String realmId) { - this.realmId = realmId; - } - - public String getComponentId() { - return componentId; - } - - public void setComponentId(String componentId) { - this.componentId = componentId; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - @Override - public boolean equals(Object other) { - if (this == other) - return true; - if (!(other instanceof ScimResourceId o)) - return false; - return (StringUtils.equals(o.id, id) && - StringUtils.equals(o.realmId, realmId) && - StringUtils.equals(o.componentId, componentId) && - StringUtils.equals(o.type, type) && - StringUtils.equals(o.externalId, externalId)); - } - - @Override - public int hashCode() { - return Objects.hash(realmId, componentId, type, id, externalId); - } -} diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceMapping.java b/src/main/java/sh/libre/scim/jpa/ScimResourceMapping.java deleted file mode 100644 index ade6848ccd2e8aea7df643bb78554e375a166b68..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/jpa/ScimResourceMapping.java +++ /dev/null @@ -1,89 +0,0 @@ -package sh.libre.scim.jpa; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.NamedQueries; -import jakarta.persistence.NamedQuery; -import jakarta.persistence.Table; -import sh.libre.scim.core.service.EntityOnRemoteScimId; -import sh.libre.scim.core.service.KeycloakId; - -@Entity -@IdClass(ScimResourceId.class) -@Table(name = "SCIM_RESOURCE_MAPPING") -@NamedQueries({ - @NamedQuery(name = "findById", query = "from ScimResourceMapping where realmId = :realmId and componentId = :componentId and type = :type and id = :id"), - @NamedQuery(name = "findByExternalId", query = "from ScimResourceMapping where realmId = :realmId and componentId = :componentId and type = :type and externalId = :id") -}) -public class ScimResourceMapping { - - @Id - @Column(name = "ID", nullable = false) - private String id; - - @Id - @Column(name = "REALM_ID", nullable = false) - private String realmId; - - @Id - @Column(name = "COMPONENT_ID", nullable = false) - private String componentId; - - @Id - @Column(name = "TYPE", nullable = false) - private String type; - - @Id - @Column(name = "EXTERNAL_ID", nullable = false) - private String externalId; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getRealmId() { - return realmId; - } - - public void setRealmId(String realmId) { - this.realmId = realmId; - } - - public String getComponentId() { - return componentId; - } - - public void setComponentId(String componentId) { - this.componentId = componentId; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public KeycloakId getIdAsKeycloakId() { - return new KeycloakId(id); - } - - public EntityOnRemoteScimId getExternalIdAsEntityOnRemoteScimId() { - return new EntityOnRemoteScimId(externalId); - } -} diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java b/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java deleted file mode 100644 index 6ef55a060e10945b5eb017559ea9b9c794122381..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -package sh.libre.scim.jpa; - -import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider; - -import java.util.Collections; -import java.util.List; - -public class ScimResourceProvider implements JpaEntityProvider { - - @Override - public List<Class<?>> getEntities() { - return Collections.singletonList(ScimResourceMapping.class); - } - - @Override - public String getChangelogLocation() { - return "META-INF/scim-resource-changelog.xml"; - } - - @Override - public void close() { - // Nothing to close - } - - @Override - public String getFactoryId() { - return ScimResourceProviderFactory.ID; - } -} \ No newline at end of file diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java b/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java deleted file mode 100644 index 7f3cc323ddf72b0169f1c61a34bdff66ad4ef858..0000000000000000000000000000000000000000 --- a/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -package sh.libre.scim.jpa; - -import org.keycloak.Config.Scope; -import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider; -import org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; - -public class ScimResourceProviderFactory implements JpaEntityProviderFactory { - - static final String ID = "scim-resource"; - - @Override - public JpaEntityProvider create(KeycloakSession session) { - return new ScimResourceProvider(); - } - - @Override - public String getId() { - return ID; - } - - @Override - public void init(Scope scope) { - // Nothing to initialise - } - - @Override - public void postInit(KeycloakSessionFactory sessionFactory) { - // Nothing to do - } - - - @Override - public void close() { - // Nothing to close - } - -} diff --git a/src/main/resources/META-INF/jboss-deployment-structure.xml b/src/main/resources/META-INF/jboss-deployment-structure.xml deleted file mode 100644 index 42007fe63830cbd5550ac2b55c43ce51c4ec377c..0000000000000000000000000000000000000000 --- a/src/main/resources/META-INF/jboss-deployment-structure.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<jboss-deployment-structure> - <deployment> - <dependencies> - <module name="org.keycloak.keycloak-services" /> - <module name="org.keycloak.keycloak-model-jpa" /> - <module name="org.hibernate" /> - </dependencies> - </deployment> -</jboss-deployment-structure> diff --git a/src/main/resources/META-INF/scim-resource-changelog.xml b/src/main/resources/META-INF/scim-resource-changelog.xml deleted file mode 100644 index d3e2687a57b42ea71b1f9e2bb848b9c4da56cca4..0000000000000000000000000000000000000000 --- a/src/main/resources/META-INF/scim-resource-changelog.xml +++ /dev/null @@ -1,35 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> - <changeSet author="contact@indiehosters.net" id="scim-resource-1.0"> - - <createTable tableName="SCIM_RESOURCE_MAPPING"> - <column name="ID" type="VARCHAR(36)"> - <constraints nullable="false"/> - </column> - <column name="REALM_ID" type="VARCHAR(36)"> - <constraints nullable="false"/> - </column> - <column name="TYPE" type="VARCHAR(36)"> - <constraints nullable="false"/> - </column> - <column name="COMPONENT_ID" type="VARCHAR(36)"> - <constraints nullable="false"/> - </column> - <column name="EXTERNAL_ID" type="VARCHAR(36)"> - <constraints nullable="false"/> - </column> - </createTable> - - <addPrimaryKey constraintName="PK_SCIM_RESOURCE_MAPPING" tableName="SCIM_RESOURCE_MAPPING" - columnNames="ID,REALM_ID,TYPE,COMPONENT_ID,EXTERNAL_ID"/> - <addForeignKeyConstraint baseTableName="SCIM_RESOURCE_MAPPING" baseColumnNames="REALM_ID" - constraintName="FK_SCIM_RESOURCE_MAPPING_REALM" referencedTableName="REALM" - referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE"/> - <addForeignKeyConstraint baseTableName="SCIM_RESOURCE_MAPPING" baseColumnNames="COMPONENT_ID" - constraintName="FK_SCIM_RESOURCE_MAPPING_COMPONENT" referencedTableName="COMPONENT" - referencedColumnNames="ID" onDelete="CASCADE" onUpdate="CASCADE"/> - </changeSet> - -</databaseChangeLog> \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory deleted file mode 100644 index b3cb1a13e34435fc9114e7cd02ab77d87b3c6871..0000000000000000000000000000000000000000 --- a/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory +++ /dev/null @@ -1 +0,0 @@ -sh.libre.scim.jpa.ScimResourceProviderFactory diff --git a/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory b/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory deleted file mode 100644 index 7e2a6edd9cccb0c544c7d6b9fbd65ffc3ff7324a..0000000000000000000000000000000000000000 --- a/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory +++ /dev/null @@ -1 +0,0 @@ -sh.libre.scim.event.ScimEventListenerProviderFactory diff --git a/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory deleted file mode 100644 index 308796c862e2aa83c0aa9212cb123128a749bd1e..0000000000000000000000000000000000000000 --- a/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory +++ /dev/null @@ -1 +0,0 @@ -sh.libre.scim.core.ScimEndpointConfigurationStorageProviderFactory