diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d14cdeac6..41e9c0706 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,7 @@ jobs: - KEYCLOAK_VERSION: 16.1.1 - KEYCLOAK_VERSION: 17.0.0 steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 with: fetch-depth: 0 @@ -69,7 +69,7 @@ jobs: matrix: java: [8, 11, 17] steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - name: Setup java uses: actions/setup-java@v2 @@ -99,7 +99,7 @@ jobs: env: - KEYCLOAK_VERSION: 17.0.0 steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - name: Setup java uses: actions/setup-java@v2 @@ -115,20 +115,14 @@ jobs: ${{ runner.os }}-maven-keycloak-legacy - name: Build & Test - run: >- - ./mvnw ${MAVEN_CLI_OPTS} clean verify -Pcoverage -Dkeycloak.dockerImage=quay.io/keycloak/keycloak:${{ matrix.env.KEYCLOAK_VERSION }}-legacy + run: ./mvnw ${MAVEN_CLI_OPTS} -Dkeycloak.version=${{ matrix.env.KEYCLOAK_VERSION }} -Dkeycloak.dockerTagSuffix=-legacy clean verify - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2.1.0 - with: - file: "${{ github.workspace }}/target/site/jacoco/jacoco.xml" - fail_ci_if_error: true lint-other-files: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - name: Setup java uses: actions/setup-java@v2 @@ -169,7 +163,7 @@ jobs: with: version: v3.4.0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v3 with: python-version: 3.7 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0c60ed2f0..4afbf0591 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -71,7 +71,7 @@ jobs: - KEYCLOAK_VERSION: 16.1.1 - KEYCLOAK_VERSION: 17.0.0 steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - name: Setup java uses: actions/setup-java@v2 @@ -107,13 +107,13 @@ jobs: maintainer=adorsys GmbH & Co. KG - name: Login to Docker Hub - uses: docker/login-action@v1.12.0 + uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Login to Quay.io - uses: docker/login-action@v1.12.0 + uses: docker/login-action@v1.14.1 with: registry: quay.io username: ${{ secrets.QUAYIO_USERNAME }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e49dc2528..bf2153fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +## [4.8.0] - 2022-03-06 + +### Added + +- Support for managing `Client Authorization Resources` like other resources by configuring `import.managed.client-authorization-resources=`. This prevents deletion of remote managed resources. +- Support for managing fine granted authorization rules. +### Changes + +- Compile keycloak-config-cli inside docker build to avoid the requirement to run maven before + +### Fixed + +- Manage `Client Authorization` without define a `clientId` in import realm. + ## [4.7.0] - 2022-02-14 ### Added @@ -66,7 +80,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Stale client level roles assignment on a user, if the client is not present in the `clientRoles` JSON object in the config file. The Keycloak default client roles (e.g. realm-management) will remain untouched though. - ## [4.3.0] - 2021-09-28 ### Added @@ -135,6 +148,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), remove the role from a user, if this flag set to `true`. ### Fixed + - Exclude `default-roles-$REALM` from user realm role removal ### Removed @@ -145,9 +159,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [3.4.0] - 2021-05-12 ### Added + - Support for Keycloak 13 - *Note*: If you get an error like `client already exists` or `java.lang.IllegalStateException: Session/EntityManager is closed`, it's not an error in keycloak-config-cli. - See https://issues.redhat.com/browse/KEYCLOAK-18035 + _Note_: If you get an error like `client already exists` or `java.lang.IllegalStateException: Session/EntityManager is closed`, it's not an error in keycloak-config-cli. See https://issues.redhat.com/browse/KEYCLOAK-18035 - Define custom var substitution prefix and suffix through `import.var-substitution-prefix` and `import.var-substitution-suffix`. This prevents conflicts with keycloak builtin variables. Default to `${` and `}` and will be changed to `$(` and `)`. in keycloak-config-cli 4.0. @@ -155,11 +169,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Keycloak images additionally pushed to quay.io ### Fixed + - Versions specific images of keycloak-config-cli are not exists with keycloak version variations. ## [3.3.1] - 2021-05-04 ### Fixed + - 409 Conflict on importing client role that already exists but not in state. ## [3.3.0] - 2021-04-24 @@ -170,6 +186,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Client secrets mapping on the client scopes with the `clientScopeMappings`. ### Fixed + - Undetermined treatment of a client without the client id specified. - Provisioning of a client with service account enabled when the `registrationEmailAsUsername` flag for the realm is set to `true`. @@ -234,9 +251,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Breaking - keycloak-config-cli does not auto append `/auth/` to the keycloak path. -- Role and Clients are `fully managed` now. See: [docs/MANAGED.md](docs/MANAGED.md). *Take care while upgrade exist keycloak instances*. This upgrade - should be tested carefully on existing instances. If `import.state` is enabled, only roles and clients created by keycloak-config-cli will be - deleted. Set `--import.managed.role=no-delete` and `--import.managed.client=no-delete` will restore the keycloak-config-cli v2.x behavior. +- Role and Clients are `fully managed` now. See: [docs/MANAGED.md](docs/MANAGED.md). _Take care while upgrade exist keycloak instances_. This upgrade should be tested carefully on existing instances. If `import.state` is enabled, only roles and clients created by keycloak-config-cli will be deleted. Set `--import.managed.role=no-delete` and `--import.managed.client=no-delete` will restore the keycloak-config-cli v2.x behavior. ### Added @@ -309,7 +324,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed -- __DEPRECATION:__ Auto append `/auth` in server url. +- **DEPRECATION:** Auto append `/auth` in server url. ### Fixed @@ -534,7 +549,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -[Unreleased]: https://github.com/adorsys/keycloak-config-cli/compare/v4.7.0...HEAD +[unreleased]: https://github.com/adorsys/keycloak-config-cli/compare/v4.7.0...HEAD +[Unreleased]: https://github.com/adorsys/keycloak-config-cli/compare/v4.8.0...HEAD +[4.8.0]: https://github.com/adorsys/keycloak-config-cli/compare/v4.7.0...v4.8.0 [4.7.0]: https://github.com/adorsys/keycloak-config-cli/compare/v4.6.1...v4.7.0 [4.6.1]: https://github.com/adorsys/keycloak-config-cli/compare/v4.6.0...v4.6.1 [4.6.0]: https://github.com/adorsys/keycloak-config-cli/compare/v4.5.0...v4.6.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 3a21f38f3..b884e48ac 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,3 @@ - # Contributor Covenant Code of Conduct ## Our Pledge @@ -18,24 +17,19 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,12 +100,9 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. -**Consequence**: A permanent ban from any sort of public interaction within -the community. +**Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution diff --git a/Dockerfile b/Dockerfile index 4deb41e6c..6b74b135f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,16 @@ -FROM openjdk:17-slim +# Can be adjusted with docker build --build-arg RUNTIME_IMAGE=mirror.com/openjdk:17 +ARG BUILDER_IMAGE=openjdk:17 +ARG RUNTIME_IMAGE=openjdk:17-slim + +FROM ${BUILDER_IMAGE} AS BUILDER + +COPY . . + +RUN ./mvnw -ntp -B clean package -DskipTests \ + -Dlicense.skipCheckLicense -Dcheckstyle.skip -Dmaven.test.skip=true -Dmaven.site.skip=true \ + -Dmaven.javadoc.skip=true -Dmaven.gitcommitid.skip=true + +FROM ${RUNTIME_IMAGE} ARG JAR=./target/keycloak-config-cli.jar ENV JAVA_OPTS="" KEYCLOAK_SSL_VERIFY=true IMPORT_PATH=file:/config @@ -6,6 +18,6 @@ ENV JAVA_OPTS="" KEYCLOAK_SSL_VERIFY=true IMPORT_PATH=file:/config # $0 represents the first CLI arg which is not inside $@ ENTRYPOINT exec java $JAVA_OPTS -jar /app/keycloak-config-cli.jar $0 $@ -USER 1001 +COPY --from=BUILDER /target/keycloak-config-cli.jar /app/keycloak-config-cli.jar -ADD --chown=0:0 ${JAR} /app/keycloak-config-cli.jar +USER 65534 diff --git a/README.md b/README.md index d87c0205e..fd55bf490 100644 --- a/README.md +++ b/README.md @@ -187,19 +187,10 @@ docker run \ ### Docker build -To build the docker image locally, you have to build the keycloak-config-cli first. By default, the dockerfile expects the jar file -at `./target/keycloak-config-cli.jar`. +You can build an own docker image by running -The location of `./target/keycloak-config-cli.jar` can be modified by using `--build-arg JAR=` parameter on the `docker build` command. Inside the -docker build the `ADD` command is used. Multiple source like zip files OR remote locations are supported, too. - -Here is an example to build the docker image using the jar form a [Github release](https://github.com/adorsys/keycloak-config-cli/releases/tag/v4.2.0) -. - -```shell script -docker build \ - --build-arg JAR=https://github.com/adorsys/keycloak-config-cli/releases/download/v4.2.0/keycloak-config-cli-15.0.1.jar \ - -t keycloak-config-cli:latest . +```shell +docker build -t keycloak-config-cli . ``` ## Helm @@ -214,37 +205,39 @@ Checkout helm docs about [chart dependencies](https://helm.sh/docs/topics/charts ## CLI option / Environment Variables -| CLI Option | ENV Variable | Description | Default | Docs | -|-------------------------------------------------------|----------------------------------------------------|-----------------------------------------------------------------------------------|-------------|---------------------------------------------------------------------------------------------------------------------------------| -| --keycloak.url | KEYCLOAK_URL | Keycloak URL including web context. Format: `scheme://hostname:port/web-context`. | - | | -| --keycloak.user | KEYCLOAK_USER | login user name | `admin` | | -| --keycloak.password | KEYCLOAK_PASSWORD | login user password | - | | -| --keycloak.client-id | KEYCLOAK_CLIENTID | login clientId | `admin-cli` | | -| --keycloak.client-secret | KEYCLOAK_CLIENTSECRET | login client secret | - | | -| --keycloak.grant-type | KEYCLOAK_GRANTTYPE | login grant_type | `password` | | -| --keycloak.login-realm | KEYCLOAK_LOGINREALM | login realm | `master` | | -| --keycloak.ssl-verify | KEYCLOAK_SSLVERIFY | Verify ssl connection to keycloak | `true` | | -| --keycloak.http-proxy | KEYCLOAK_HTTPPROXY | Connect to Keycloak via HTTP Proxy. Format: `scheme://hostname:port` | - | | -| --keycloak.connect-timeout | KEYCLOAK_CONNECTTIMEOUT | Connection timeout of the underyling Resteasy client | `10s` | | -| --keycloak.read-timeout | KEYCLOAK_READTIMEOUT | Read timeout of the underlying Resteasy client | `10s` | configured as [Java Duration](https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html) | -| --keycloak.availability-check.enabled | KEYCLOAK_AVAILABILITYCHECK_ENABLED | Wait until Keycloak is available | `false` | configured as [Java Duration](https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html) | -| --keycloak.availability-check.timeout | KEYCLOAK_AVAILABILITYCHECK_TIMEOUT | Wait timeout for keycloak availability check | `120s` | | -| --import.path | IMPORT_PATH | Location of config files (if location is a directory, all files will be imported) | `/config` | [Spring ResourceLoader](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-resourceloader) | -| --import.force | IMPORT_FORCE | Import realm even if config from `--import.path` is unchanged | `false` | | -| --import.validate | IMPORT_VALIDATE | Validate configuration settings | `false` | | -| --import.cache-key | IMPORT_CACHEKEY | Cache key for importing config. | `default` | | -| --import.state | IMPORT_STATE | Enable state management. Purge only resources managed by kecloak-config-cli. S. | `true` | [MANAGED.md](docs/MANAGED.md) | -| --import.state-encryption-key | IMPORT_STATEENCRYPTIONKEY | Enables state in encrypted format. If unset, state will be stored in plain | - | | -| --import.file-type | IMPORT_FILETYPE | Format of the configuration import file. Allowed values: AUTO,JSON,YAML | `auto` | | -| --import.parallel | IMPORT_PARALLEL | Enable parallel import of certain resources | `false` | | -| --import.var-substitution | IMPORT_VARSUBSTITUTION | Enable variable substitution config files | `false` | | -| --import.var-substitution-in-variable | IMPORT_VARSUBSTITUTION_IN_VARIABLES | Expand variables in variables. | `true` | | -| --import.var-substitution-undefined-throws-exceptions | IMPORT_VARSUBSTITUTION_UNDEFINED_THROWS_EXCEPTIONS | Raise exceptions, if variables are not defined. | `true` | | -| --import.var-substitution-prefix | IMPORT_VARSUBSTITUTION_PREFIX | Configure the variable prefix, if `import.var-substitution` is enabled. | `$(` | | -| --import.var-substitution-suffix | IMPORT_VARSUBSTITUTION_SUFFIX | Configure the variable suffix, if `import.var-substitution` is enabled. | `)` | | -| --import.sync-user-federation | IMPORT_SYNC_USER_FEDERATION | Enable the synchronization of user federation. | `false` | | -| --import.remove-default-role-from-user | IMPORT_REMOVEDEFAULTROLEFROMUSER | See below. | `false` | | -| --import.skip-attributes-for-federated-user | IMPORT_SKIP_ATTRIBUTESFORFEDERATEDUSER | Set attributes to null for federated users to avoid read only conflicts | `false` | | +| CLI Option | ENV Variable | Description | Default | Docs | +|-------------------------------------------------------|----------------------------------------------------|-----------------------------------------------------------------------------------|-------------|--------------------------------------------------------------------------------------------------| +| --keycloak.url | KEYCLOAK_URL | Keycloak URL including web context. Format: `scheme://hostname:port/web-context`. | - | | +| --keycloak.user | KEYCLOAK_USER | login user name | `admin` | | +| --keycloak.password | KEYCLOAK_PASSWORD | login user password | - | | +| --keycloak.client-id | KEYCLOAK_CLIENTID | login clientId | `admin-cli` | | +| --keycloak.client-secret | KEYCLOAK_CLIENTSECRET | login client secret | - | | +| --keycloak.grant-type | KEYCLOAK_GRANTTYPE | login grant_type | `password` | | +| --keycloak.login-realm | KEYCLOAK_LOGINREALM | login realm | `master` | | +| --keycloak.ssl-verify | KEYCLOAK_SSLVERIFY | Verify ssl connection to keycloak | `true` | | +| --keycloak.http-proxy | KEYCLOAK_HTTPPROXY | Connect to Keycloak via HTTP Proxy. Format: `scheme://hostname:port` | - | | +| --keycloak.connect-timeout | KEYCLOAK_CONNECTTIMEOUT | Connection timeout of the underyling Resteasy client | `10s` | | +| --keycloak.read-timeout | KEYCLOAK_READTIMEOUT | Read timeout of the underlying Resteasy client | `10s` | configured as [Java Duration](https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html) | +| --keycloak.availability-check.enabled | KEYCLOAK_AVAILABILITYCHECK_ENABLED | Wait until Keycloak is available | `false` | configured as [Java Duration](https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html) | +| --keycloak.availability-check.timeout | KEYCLOAK_AVAILABILITYCHECK_TIMEOUT | Wait timeout for keycloak availability check | `120s` | | +| --import.path | IMPORT_PATH | Location of config files (URL, file path, directory, or Ant-style pattern) | - | [IMPORT.md](docs/IMPORT.md) | +| --import.hidden-files | IMPORT_HIDDEN_FILES | Import hidden files | `false` | [IMPORT.md](docs/IMPORT.md) | +| --import.exclude | IMPORT_EXCLUDE | Exclude files with Ant-style pattern | - | [IMPORT.md](docs/IMPORT.md) | +| --import.force | IMPORT_FORCE | Import realm even if config from `--import.path` is unchanged | `false` | | +| --import.validate | IMPORT_VALIDATE | Validate configuration settings | `false` | | +| --import.cache-key | IMPORT_CACHEKEY | Cache key for importing config. | `default` | | +| --import.state | IMPORT_STATE | Enable state management. Purge only resources managed by kecloak-config-cli. S. | `true` | [MANAGED.md](docs/MANAGED.md) | +| --import.state-encryption-key | IMPORT_STATEENCRYPTIONKEY | Enables state in encrypted format. If unset, state will be stored in plain | - | | +| --import.file-type | IMPORT_FILETYPE | Format of the configuration import file. Allowed values: AUTO,JSON,YAML | `auto` | | +| --import.parallel | IMPORT_PARALLEL | Enable parallel import of certain resources | `false` | | +| --import.var-substitution | IMPORT_VARSUBSTITUTION | Enable variable substitution config files | `false` | | +| --import.var-substitution-in-variable | IMPORT_VARSUBSTITUTION_IN_VARIABLES | Expand variables in variables. | `true` | | +| --import.var-substitution-undefined-throws-exceptions | IMPORT_VARSUBSTITUTION_UNDEFINED_THROWS_EXCEPTIONS | Raise exceptions, if variables are not defined. | `true` | | +| --import.var-substitution-prefix | IMPORT_VARSUBSTITUTION_PREFIX | Configure the variable prefix, if `import.var-substitution` is enabled. | `$(` | | +| --import.var-substitution-suffix | IMPORT_VARSUBSTITUTION_SUFFIX | Configure the variable suffix, if `import.var-substitution` is enabled. | `)` | | +| --import.sync-user-federation | IMPORT_SYNC_USER_FEDERATION | Enable the synchronization of user federation. | `false` | | +| --import.remove-default-role-from-user | IMPORT_REMOVEDEFAULTROLEFROMUSER | See below. | `false` | | +| --import.skip-attributes-for-federated-user | IMPORT_SKIP_ATTRIBUTESFORFEDERATEDUSER | Set attributes to null for federated users to avoid read only conflicts | `false` | | See [application.properties](src/main/resources/application.properties) for all available settings. @@ -256,10 +249,7 @@ if you need alternative spellings. ### import.remove-default-role-from-user -Keycloak 13 attach a default role named `default-role-$REALM` that contains some defaults from any user. -Previously keycloak-config-cli remove that default role, if the role not defined inside the import json. -The flag prevents keycloak-config-cli from exclude `default-roles-$REALM` from removal logic. This results that it's not longer possible to explicit -remove the role from a user, if `import.remove-default-role-from-user` set to `true`. +Keycloak 13 attach a default role named `default-role-$REALM` that contains some defaults from any user. Previously keycloak-config-cli remove that default role, if the role not defined inside the import json. The default setting of this flag prevents keycloak-config-cli from removing `default-roles-$REALM`, even if its not defined in the import json. To make keycloak-config-cli able to remove the `default-role-$REALM`, `import.remove-default-role-from-user` must be set to true. In conclusion, you have to add the `default-role-$REALM` to the realm import on certian users, if you want not remove the `default-role-$REALM`. ## Spring boot options diff --git a/TODO.md b/TODO.md index 184ce8b77..86491090b 100644 --- a/TODO.md +++ b/TODO.md @@ -2,4 +2,4 @@ ## Implement -* https://github.com/keycloak/keycloak/pull/7780 +- https://github.com/keycloak/keycloak/pull/7780 diff --git a/checkstyle.xml b/checkstyle.xml index 8aa4728e8..26a5a3c3a 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -271,6 +271,7 @@ PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF, RECORD_COMPONENT_DEF"/> + @@ -360,9 +361,7 @@ - - - + diff --git a/contrib/charts/keycloak-config-cli/Chart.yaml b/contrib/charts/keycloak-config-cli/Chart.yaml index 345e58bca..cdd269f45 100644 --- a/contrib/charts/keycloak-config-cli/Chart.yaml +++ b/contrib/charts/keycloak-config-cli/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: keycloak-config-cli description: Import JSON-formatted configuration files into Keycloak - Configuration as Code for Keycloak. home: https://github.com/adorsys/keycloak-config-cli -version: 4.7.0 -appVersion: 4.7.0 +version: 4.8.0 +appVersion: 4.8.0 maintainers: - name: jkroepke email: joe@adorsys.de diff --git a/contrib/custom-representations/README.md b/contrib/custom-representations/README.md index d16377647..a815e7527 100644 --- a/contrib/custom-representations/README.md +++ b/contrib/custom-representations/README.md @@ -19,11 +19,13 @@ If you're done, run `mvn clean package`. Your jar is build inside the `target` d You could load the additional jar file by `-Dloader.path`. ### Example + ```bash java -Dloader.path=./custom-representations.jar -jar target/keycloak-config-cli.jar ``` ### Verify + To verify that the representation is used from the provided jar, run: ```bash diff --git a/docker-compose.yml b/docker-compose.yml index 4183e4b76..2f7e54aee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: - "8787:8787" command: - start-dev + - --features admin-fine-grained-authz keycloak-legacy: image: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION}-legacy environment: @@ -28,6 +29,7 @@ services: - "-c" - "standalone.xml" - "-Dkeycloak.profile.feature.upload_scripts=enabled" + - "-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled" openldap: image: osixia/openldap:1.5.0 command: diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 1c08daa1b..d6fd08795 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,7 +1,7 @@ # Supported features | Feature | Since | Description | -|----------------------------------------------------|-------|----------------------------------------------------------------------------------------------------------| +| -------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------- | | Create clients | 1.0.0 | Create client configuration (inclusive protocolMappers) while creating or updating realms | | Update clients | 1.0.0 | Update client configuration (inclusive protocolMappers) while updating realms | | Manage fine-grained authorization of clients | 2.2.0 | Add and remove fine-grained authorization resources and policies of clients | @@ -63,9 +63,9 @@ ```json { - "authenticationFlowBindingOverrides": { - "browser": "ad7d518c-4129-483a-8351-e1223cb8eead" - } + "authenticationFlowBindingOverrides": { + "browser": "ad7d518c-4129-483a-8351-e1223cb8eead" + } } ``` @@ -77,9 +77,9 @@ So if you need this, you have to configure it like : ```json { - "authenticationFlowBindingOverrides": { - "browser": "my awesome browser flow" - } + "authenticationFlowBindingOverrides": { + "browser": "my awesome browser flow" + } } ``` @@ -89,19 +89,19 @@ To set an initial password that is only respect while the user is created, the u ```json { - "users": [ + "users": [ + { + "username": "user", + "email": "user@mail.de", + "enabled": true, + "credentials": [ { - "username": "user", - "email": "user@mail.de", - "enabled": true, - "credentials": [ - { - "type": "password", - "userLabel": "initial", - "value": "start123" - } - ] + "type": "password", + "userLabel": "initial", + "value": "start123" } - ] + ] + } + ] } ``` diff --git a/docs/IMPORT.md b/docs/IMPORT.md new file mode 100644 index 000000000..f387e26de --- /dev/null +++ b/docs/IMPORT.md @@ -0,0 +1,49 @@ +# Import settings + +You can use `--import.path` setting or `IMPORT_PATH` environment variable to choose which configuration files to import in Keycloak. + +### `import.path` + +`--import.path` setting make use +of [PathMatchingResourcePatternResolver](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/support/PathMatchingResourcePatternResolver.html) +to collect one or many resources from a String or an Ant-style pattern. + +If a string is given, it generates a single resource entry, but if an Ant-style pattern is given, it generates many resource entries. + +You can also use `--import.path` setting many times in your command line, or use `,` separator in `IMPORT_PATH` environment variable. + +Each resource entry is then processed by a [ResourceExtractor](../src/main/java/de/adorsys/keycloak/config/provider/ResourceExtractor.java) instance, +based on the resource type. + +- If it's a file, the file is read and included into imported + files ([FileResourceExtractor](../src/main/java/de/adorsys/keycloak/config/provider/FileResourceExtractor.java)) + +- If it's a directory, all files is contains and read and included into imported + files ([DirectoryResourceExtractor](../src/main/java/de/adorsys/keycloak/config/provider/DirectoryResourceExtractor.java)) + +- If it's an URL, the resource is read as a file through Java standard way to load files from URL and included into imported + files ([UrlResourceExtractor](../src/main/java/de/adorsys/keycloak/config/provider/UrlResourceExtractor.java)) + +### `import.hidden-files` + +By default, hidden files will be excluded from import. To include them, use `--import.exclude=true` flag or `IMPORT_EXCLUDE=true` environment +variable. + +### `import.exclude` + +`--import.exclude` flag or `IMPORT_EXCLUDE` environment variable can be used to exclude some files, using Ant-style pattern. + +#### Ant-style path patterns. +Part of this mapping code has been kindly borrowed from [Apache Ant](https://ant.apache.org/). + +The path patten using the following rules: +* `?` matches one character +* `*` matches zero or more characters +* `**` matches zero or more directories in a path + +##### Examples +* `com/t?st.jsp` — matches com/test.jsp but also com/tast.jsp or com/txst.jsp +* `com/*.jsp` — matches all .jsp files in the com directory +* `com/**/test.jsp` — matches all test.jsp files underneath the com path +* `org/springframework/**/*.jsp` — matches all .jsp files underneath the org/springframework path +* `org/**/servlet/bla.jsp` — matches org/springframework/servlet/bla.jsp but also org/springframework/testing/servlet/bla.jsp and org/servlet/bla.jsp diff --git a/docs/MANAGED.md b/docs/MANAGED.md index ac899bc2a..617598e87 100644 --- a/docs/MANAGED.md +++ b/docs/MANAGED.md @@ -20,30 +20,30 @@ groups will be deleted. If you define `groups` but set an empty array, keycloak ## Supported full managed resources -| Type | Additional Information | Resource Name | -|---------------------------|----------------------------------------------------------------------------------|----------------------------| -| Groups | - | `group` | -| Required Actions | You have to copy the default one to you import json. | `required-action` | -| Client Scopes | - | `client-scope` | -| Scope Mappings | - | `scope-mapping` | -| Client Scope Mappings | - | `client-scope-mapping` | -| Roles | - | `role` | -| Components | You have to copy the default components to you import json. | `component` | -| Sub Components | You have to copy the default components to you import json. | `sub-component` | -| Authentication Flows | You have to copy the default components to you import json, expect bulitin flows | `authentication-flow` | -| Identity Providers | - | `identity-provider` | -| Identity Provider Mappers | - | `identity-provider-mapper` | -| Clients | - | `client` | +| Type | Additional Information | Resource Name | +|---------------------------------|----------------------------------------------------------------------------------|----------------------------------| +| Groups | - | `group` | +| Required Actions | You have to copy the default one to you import json. | `required-action` | +| Client Scopes | - | `client-scope` | +| Scope Mappings | - | `scope-mapping` | +| Client Scope Mappings | - | `client-scope-mapping` | +| Roles | - | `role` | +| Components | You have to copy the default components to you import json. | `component` | +| Sub Components | You have to copy the default components to you import json. | `sub-component` | +| Authentication Flows | You have to copy the default components to you import json, expect builtin flows | `authentication-flow` | +| Identity Providers | - | `identity-provider` | +| Identity Provider Mappers | - | `identity-provider-mapper` | +| Clients | - | `client` | +| Clients Authorization Resources | The 'Default Resource' is always included. | `client-authorization-resources` | ## Disable deletion of managed entities -If you won't delete properties of a specific type, you can disable this behavior by default a properties like `import.managed.=`, e.g.: +If you don't delete properties of a specific type, you can disable this behavior by default a properties like `import.managed.=`, e.g.: `import.managed.required-actions=no-delete` ## State management -If `import.state` is set to `true` (default value), keycloak-config-cli will purge only resources they created before by keycloak-config-cli. -If `import.state` is set to `false`, keycloak-config-cli will purge all existing entities if they not defined in import json. +If `import.state` is set to `true` (default value), keycloak-config-cli will purge only resources they created before by keycloak-config-cli. If `import.state` is set to `false`, keycloak-config-cli will purge all existing entities if they are not defined in import json. ### Supported resources diff --git a/docs/RHSSO.md b/docs/RHSSO.md index 6b6241c6e..4b7278a40 100644 --- a/docs/RHSSO.md +++ b/docs/RHSSO.md @@ -8,11 +8,12 @@ While keycloak-config-cli not officially supports RH SSO, it's possible to build ## Requirements installed on system -* Java JDK 8+ +- Java JDK 8+ ## Steps ### Clone Repo + ```bash git clone https://github.com/adorsys/keycloak-config-cli.git git checkout v3.4.0 diff --git a/hooks/build b/hooks/build deleted file mode 100755 index e9b5841c1..000000000 --- a/hooks/build +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env sh - -# https://docs.docker.com/docker-hub/builds/advanced/ - -if [ "${DOCKER_TAG}" = "native" ]; then - contrib/native/build-in-container.sh -else - # Build app via maven - docker run --rm \ - -v "${PWD}":/work \ - -w /work \ - maven:3-openjdk-11 mvn -B -ntp clean package -DskipTests -fi - -# Build container -docker build \ - --build-arg VERSION="$(echo "${DOCKER_TAG}" | sed -e 's/v//g')" \ - --build-arg BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ - --build-arg VCS_REF="$(echo "${SOURCE_COMMIT}" | head -c 7)" \ - -f "${DOCKERFILE_PATH}" \ - -t "${IMAGE_NAME}" . diff --git a/pom.xml b/pom.xml index 6d0faa7e4..d2382a897 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ de.adorsys.keycloak keycloak-config-cli jar - 4.7.0 + 4.8.0 keycloak-config-cli https://github.com/adorsys/keycloak-config-cli @@ -39,7 +39,7 @@ scm:git:git://github.com/adorsys/keycloak-config-cli.git scm:git:ssh://git@github.com/keycloak-config-cli.git https://github.com/adorsys/keycloak-config-cli - v4.7.0 + v4.8.0 @@ -63,7 +63,7 @@ 4.9.10 2.13.1 0.8.7 - 1.5.0 + 1.6.1 1.1.2 2.1.1 2.0.0 @@ -74,8 +74,8 @@ 1.12.2 3.0.0-M5 5.12.0 - 3.15.0 - 6.42.0 + 3.16.0 + 6.43.0 0.15 5.0.2.Final 4.5.3.0 @@ -95,7 +95,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.3 + 2.6.4 @@ -130,11 +130,23 @@ ${commons-io.version} + + org.apache.commons + commons-text + ${commons-text.version} + + org.apache.commons commons-lang3 ${commons-lang3.version} + + + net.jodah + failsafe + ${failsafe.version} + @@ -195,13 +207,16 @@ org.apache.commons commons-text - ${commons-text.version} + + + + org.apache.commons + commons-lang3 net.jodah failsafe - ${failsafe.version} @@ -235,6 +250,12 @@ + + org.keycloak + keycloak-authz-client + test + + org.testcontainers testcontainers diff --git a/src/main/java/de/adorsys/keycloak/config/configuration/KeycloakConfigConfiguration.java b/src/main/java/de/adorsys/keycloak/config/configuration/KeycloakConfigConfiguration.java new file mode 100644 index 000000000..95eb7bbad --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/configuration/KeycloakConfigConfiguration.java @@ -0,0 +1,51 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com + * --- + * 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. + * ---license-end + */ + +package de.adorsys.keycloak.config.configuration; + +import de.adorsys.keycloak.config.provider.FileComparator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import java.io.File; +import java.util.Comparator; + +@Configuration +public class KeycloakConfigConfiguration { + private final ResourceLoader resourceLoader; + + @Autowired + public KeycloakConfigConfiguration(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Bean + public PathMatchingResourcePatternResolver patternResolver() { + return new PathMatchingResourcePatternResolver(this.resourceLoader); + } + + @Bean + public Comparator fileComparator() { + return new FileComparator(); + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/exception/ImportProcessingException.java b/src/main/java/de/adorsys/keycloak/config/exception/ImportProcessingException.java index 25fccec12..8458db668 100644 --- a/src/main/java/de/adorsys/keycloak/config/exception/ImportProcessingException.java +++ b/src/main/java/de/adorsys/keycloak/config/exception/ImportProcessingException.java @@ -26,6 +26,10 @@ public ImportProcessingException(String message) { super(message); } + public ImportProcessingException(String format, Object... args) { + super(String.format(format, args)); + } + public ImportProcessingException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/de/adorsys/keycloak/config/exception/KeycloakRepositoryException.java b/src/main/java/de/adorsys/keycloak/config/exception/KeycloakRepositoryException.java index 7257632f4..c8e9c64d0 100644 --- a/src/main/java/de/adorsys/keycloak/config/exception/KeycloakRepositoryException.java +++ b/src/main/java/de/adorsys/keycloak/config/exception/KeycloakRepositoryException.java @@ -22,8 +22,8 @@ public class KeycloakRepositoryException extends RuntimeException { - public KeycloakRepositoryException(String message) { - super(message); + public KeycloakRepositoryException(String format, Object... args) { + super(String.format(format, args)); } public KeycloakRepositoryException(String message, Throwable cause) { diff --git a/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java b/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java index c1c1e83f7..ec8acc3fc 100644 --- a/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java +++ b/src/main/java/de/adorsys/keycloak/config/properties/ImportConfigProperties.java @@ -24,6 +24,7 @@ import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.validation.annotation.Validated; +import java.util.Collection; import javax.validation.Valid; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @@ -39,8 +40,13 @@ public class ImportConfigProperties { public static final String REALM_CHECKSUM_ATTRIBUTE_PREFIX_KEY = REALM_STATE_ATTRIBUTE_COMMON_PREFIX + ".import-checksum-{0}"; public static final String REALM_STATE_ATTRIBUTE_PREFIX_KEY = REALM_STATE_ATTRIBUTE_COMMON_PREFIX + ".state-{0}-{1}"; - @NotBlank - private final String path; + @NotNull + private final Collection path; + + private final Collection exclude; + + @NotNull + private final boolean hiddenFiles; @NotNull private final boolean varSubstitution; @@ -93,7 +99,9 @@ public class ImportConfigProperties { private final boolean skipAttributesForFederatedUser; public ImportConfigProperties( - String path, + Collection path, + Collection exclude, + boolean hiddenFiles, boolean varSubstitution, boolean force, boolean validate, @@ -112,6 +120,8 @@ public ImportConfigProperties( boolean removeDefaultRoleFromUser, boolean skipAttributesForFederatedUser) { this.path = path; + this.exclude = exclude; + this.hiddenFiles = hiddenFiles; this.varSubstitution = varSubstitution; this.force = force; this.validate = validate; @@ -131,10 +141,18 @@ public ImportConfigProperties( this.skipAttributesForFederatedUser = skipAttributesForFederatedUser; } - public String getPath() { + public Collection getPath() { return path; } + public Collection getExclude() { + return exclude; + } + + public boolean isHiddenFiles() { + return hiddenFiles; + } + public boolean isForce() { return force; } @@ -247,6 +265,9 @@ public static class ImportManagedProperties { @NotNull private final ImportManagedPropertiesValues client; + @NotNull + private final ImportManagedPropertiesValues clientAuthorizationResources; + public ImportManagedProperties( ImportManagedPropertiesValues requiredAction, ImportManagedPropertiesValues group, ImportManagedPropertiesValues clientScope, ImportManagedPropertiesValues scopeMapping, @@ -254,7 +275,7 @@ public ImportManagedProperties( ImportManagedPropertiesValues component, ImportManagedPropertiesValues subComponent, ImportManagedPropertiesValues authenticationFlow, ImportManagedPropertiesValues identityProvider, ImportManagedPropertiesValues identityProviderMapper, ImportManagedPropertiesValues role, - ImportManagedPropertiesValues client) { + ImportManagedPropertiesValues client, ImportManagedPropertiesValues clientAuthorizationResources) { this.requiredAction = requiredAction; this.group = group; this.clientScope = clientScope; @@ -267,6 +288,7 @@ public ImportManagedProperties( this.identityProviderMapper = identityProviderMapper; this.role = role; this.client = client; + this.clientAuthorizationResources = clientAuthorizationResources; } public ImportManagedPropertiesValues getRequiredAction() { @@ -317,6 +339,10 @@ public ImportManagedPropertiesValues getClient() { return client; } + public ImportManagedPropertiesValues getClientAuthorizationResources() { + return clientAuthorizationResources; + } + public enum ImportManagedPropertiesValues { FULL, NO_DELETE diff --git a/src/main/java/de/adorsys/keycloak/config/provider/DirectoryResourceExtractor.java b/src/main/java/de/adorsys/keycloak/config/provider/DirectoryResourceExtractor.java index 049b888a6..bc6eb5a81 100644 --- a/src/main/java/de/adorsys/keycloak/config/provider/DirectoryResourceExtractor.java +++ b/src/main/java/de/adorsys/keycloak/config/provider/DirectoryResourceExtractor.java @@ -20,8 +20,10 @@ package de.adorsys.keycloak.config.provider; +import de.adorsys.keycloak.config.properties.ImportConfigProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; @@ -32,7 +34,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Optional; import java.util.stream.Collectors; @Order(2) @@ -41,9 +42,15 @@ class DirectoryResourceExtractor implements ResourceExtractor { private static final Logger logger = LoggerFactory.getLogger(DirectoryResourceExtractor.class); + private final ImportConfigProperties config; + + public DirectoryResourceExtractor(@Autowired ImportConfigProperties config) { + this.config = config; + } + public boolean canHandleResource(Resource resource) throws IOException { File file = resource.getFile(); - return file.isDirectory() && file.canRead(); + return file.isDirectory() && file.canRead() && (this.config.isHiddenFiles() || !file.isHidden()); } public Collection extract(Resource resource) throws IOException { @@ -53,7 +60,17 @@ public Collection extract(Resource resource) throws IOException { File file = resource.getFile(); File[] files = file.listFiles(); - return Optional.ofNullable(files).map(f -> Arrays.stream(f).filter(File::isFile).collect(Collectors.toList())) - .orElse(Collections.emptyList()); + if (files == null) { + return Collections.emptyList(); + } + + return Arrays.stream(files).filter(File::isFile) + .filter(f -> { + if (this.config.isHiddenFiles()) { + return true; + } + return !f.isHidden() && !FileUtils.hasHiddenAncestorDirectory(f); + }) + .collect(Collectors.toList()); } } diff --git a/src/main/java/de/adorsys/keycloak/config/util/ArrayUtil.java b/src/main/java/de/adorsys/keycloak/config/provider/FileComparator.java similarity index 60% rename from src/main/java/de/adorsys/keycloak/config/util/ArrayUtil.java rename to src/main/java/de/adorsys/keycloak/config/provider/FileComparator.java index f4b0aba9f..f5e0b1126 100644 --- a/src/main/java/de/adorsys/keycloak/config/util/ArrayUtil.java +++ b/src/main/java/de/adorsys/keycloak/config/provider/FileComparator.java @@ -18,20 +18,18 @@ * ---license-end */ -package de.adorsys.keycloak.config.util; +package de.adorsys.keycloak.config.provider; -import java.util.Arrays; +import java.io.File; +import java.io.Serializable; +import java.util.Comparator; -public class ArrayUtil { - ArrayUtil() { - throw new IllegalStateException("Utility class"); - } - - // https://stackoverflow.com/a/784842/8087167 - @SafeVarargs - public static T[] concat(T[] first, T... second) { - T[] result = Arrays.copyOf(first, first.length + second.length); - System.arraycopy(second, 0, result, first.length, second.length); - return result; +/** + * Sorting comparator for files, using {@link File#compareTo(File)} behavior. + */ +public class FileComparator implements Comparator, Serializable { + @Override + public int compare(File file, File t1) { + return file.compareTo(t1); } } diff --git a/src/main/java/de/adorsys/keycloak/config/provider/FileResourceExtractor.java b/src/main/java/de/adorsys/keycloak/config/provider/FileResourceExtractor.java index 1f585e600..5d5bb9e0a 100644 --- a/src/main/java/de/adorsys/keycloak/config/provider/FileResourceExtractor.java +++ b/src/main/java/de/adorsys/keycloak/config/provider/FileResourceExtractor.java @@ -20,8 +20,10 @@ package de.adorsys.keycloak.config.provider; +import de.adorsys.keycloak.config.properties.ImportConfigProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; @@ -37,9 +39,23 @@ class FileResourceExtractor implements ResourceExtractor { private static final Logger logger = LoggerFactory.getLogger(FileResourceExtractor.class); + private final ImportConfigProperties config; + + public FileResourceExtractor(@Autowired ImportConfigProperties config) { + this.config = config; + } + public boolean canHandleResource(Resource resource) throws IOException { File file = resource.getFile(); - return file.isFile() && file.canRead(); + if (!file.isFile() || !file.canRead()) { + return false; + } + + if (this.config.isHiddenFiles()) { + return true; + } + + return !file.isHidden() && !FileUtils.hasHiddenAncestorDirectory(file); } public Collection extract(Resource resource) throws IOException { diff --git a/src/main/java/de/adorsys/keycloak/config/provider/FileUtils.java b/src/main/java/de/adorsys/keycloak/config/provider/FileUtils.java index e11799019..e5f3c5e82 100644 --- a/src/main/java/de/adorsys/keycloak/config/provider/FileUtils.java +++ b/src/main/java/de/adorsys/keycloak/config/provider/FileUtils.java @@ -31,6 +31,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -43,6 +45,8 @@ final class FileUtils { throw new IllegalStateException("Utility class"); } + static final Path cwd = Paths.get(System.getProperty("user.dir")); + public static Collection extractFile(File src) { Assert.notNull(src, "The source file to extract cannot be null!"); @@ -90,4 +94,25 @@ private static Collection extractZipFile(File zipFile) { } return result; } + + public static boolean hasHiddenAncestorDirectory(File file) { + File relativeFile = relativize(file); + relativeFile = relativeFile.getParentFile(); + while (relativeFile != null) { + if (relativeFile.isHidden()) { + return true; + } + + relativeFile = relativeFile.getParentFile(); + } + return false; + } + + public static File relativize(File file) { + Path absolutePath = file.toPath().toAbsolutePath(); + if (absolutePath.startsWith(cwd)) { + return cwd.relativize(absolutePath).toFile(); + } + return absolutePath.toFile(); + } } diff --git a/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java b/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java index 73f35f871..c352086cf 100644 --- a/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java +++ b/src/main/java/de/adorsys/keycloak/config/provider/KeycloakImportProvider.java @@ -32,11 +32,14 @@ import org.apache.commons.text.StringSubstitutor; import org.apache.commons.text.lookup.StringLookup; import org.apache.commons.text.lookup.StringLookupFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.stereotype.Component; +import org.springframework.util.PathMatcher; import org.springframework.util.ResourceUtils; import org.yaml.snakeyaml.Yaml; @@ -45,26 +48,32 @@ import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; @Component public class KeycloakImportProvider { - private final ResourceLoader resourceLoader; + private final PathMatchingResourcePatternResolver patternResolver; + private final Comparator fileComparator; private final Collection resourceExtractors; private final ImportConfigProperties importConfigProperties; private StringSubstitutor interpolator = null; + private static final Logger logger = LoggerFactory.getLogger(KeycloakImportProvider.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); @Autowired public KeycloakImportProvider( Environment environment, - ResourceLoader resourceLoader, + PathMatchingResourcePatternResolver patternResolver, + Comparator fileComparator, Collection resourceExtractors, ImportConfigProperties importConfigProperties ) { - this.resourceLoader = resourceLoader; + this.patternResolver = patternResolver; + this.fileComparator = fileComparator; this.resourceExtractors = resourceExtractors; this.importConfigProperties = importConfigProperties; @@ -89,40 +98,92 @@ public KeycloakImportProvider( public KeycloakImport get() { KeycloakImport keycloakImport; - String importFilePath = importConfigProperties.getPath(); - keycloakImport = readFromPath(importFilePath); + Collection path = importConfigProperties.getPath(); + keycloakImport = readFromPaths(path.toArray(new String[0])); return keycloakImport; } - public KeycloakImport readFromPath(String path) { - // backward compatibility to correct a possible missing prefix "file:" in path - if (!ResourceUtils.isUrl(path)) { - path = "file:" + path; - } + public KeycloakImport readFromPaths(String... paths) { + Set files = new LinkedHashSet<>(); + for (String path : paths) { + // backward compatibility to correct a possible missing prefix "file:" in path + + if (!ResourceUtils.isUrl(path)) { + path = "file:" + path; + } + + Resource[] resources; + + try { + resources = this.patternResolver.getResources(path); + } catch (IOException e) { + throw new InvalidImportException("import.path does not exists: " + path, e); + } - Resource resource = resourceLoader.getResource(path); - Optional maybeMatchingExtractor = resourceExtractors.stream() - .filter(r -> { + boolean found = false; + for (Resource resource : resources) { + Optional maybeMatchingExtractor = resourceExtractors.stream() + .filter(r -> { + try { + return r.canHandleResource(resource); + } catch (IOException e) { + return false; + } + }).findFirst(); + + if (maybeMatchingExtractor.isPresent()) { try { - return r.canHandleResource(resource); + Collection extractedFiles = maybeMatchingExtractor.get() + .extract(resource) + .stream() + .map(de.adorsys.keycloak.config.provider.FileUtils::relativize) + .collect(Collectors.toList()); + + files.addAll(extractedFiles); } catch (IOException e) { - return false; + throw new InvalidImportException("import.path does not exists: " + path, e); } - }).findFirst(); - if (!maybeMatchingExtractor.isPresent()) { - throw new InvalidImportException("No resource extractor found to handle config property import.path=" + path + "! Check your settings."); + found = true; + } + } + + if (!found) { + throw new InvalidImportException("No resource extractor found to handle config property import.path=" + path + + "! Check your settings."); + } } - try { - return readRealmImportsFromResource(maybeMatchingExtractor.get().extract(resource)); - } catch (IOException e) { - throw new InvalidImportException("import.path does not exists: " + path, e); + Stream filesStream = files.stream(); + + Collection excludes = this.importConfigProperties.getExclude(); + if (excludes != null && !excludes.isEmpty()) { + PathMatcher pathMatcher = this.patternResolver.getPathMatcher(); + + for (String exclude : excludes) { + filesStream = filesStream.filter(f -> { + boolean match = pathMatcher.match(exclude, f.getPath()); + if (match) { + logger.debug("Excluding resource file '{}' (match {})", f.getPath(), exclude); + return false; + } + return true; + }); + } } + + List sortedFiles = filesStream + .map(File::getAbsoluteFile) + .sorted(this.fileComparator) + .collect(Collectors.toList()); + + logger.info("{} configuration files found.", sortedFiles.size()); + + return readRealmImportsFromResource(sortedFiles); } - private KeycloakImport readRealmImportsFromResource(Collection importResources) { + private KeycloakImport readRealmImportsFromResource(List importResources) { Map> realmImports = importResources.stream() // https://stackoverflow.com/a/52130074/8087167 .collect(Collectors.toMap( @@ -131,7 +192,7 @@ private KeycloakImport readRealmImportsFromResource(Collection importResou (u, v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }, - TreeMap::new + LinkedHashMap::new )); return new KeycloakImport(realmImports); } @@ -148,6 +209,8 @@ public KeycloakImport readRealmImportFromFile(File importFile) { private List readRealmImport(File importFile) { String importConfig; + logger.info("Loading file '{}'", importFile); + try { importConfig = FileUtils.readFileToString(importFile, StandardCharsets.UTF_8); } catch (IOException e) { diff --git a/src/main/java/de/adorsys/keycloak/config/repository/AuthenticationFlowRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/AuthenticationFlowRepository.java index dd2bfda71..4ddadb4f9 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/AuthenticationFlowRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/AuthenticationFlowRepository.java @@ -23,6 +23,7 @@ import de.adorsys.keycloak.config.exception.ImportProcessingException; import de.adorsys.keycloak.config.exception.KeycloakRepositoryException; import de.adorsys.keycloak.config.util.ResponseUtil; +import org.keycloak.admin.client.CreatedResponseUtil; import org.keycloak.admin.client.resource.AuthenticationManagementResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; @@ -71,9 +72,7 @@ public AuthenticationFlowRepresentation getByAlias(String realmName, String alia Optional flow = searchByAlias(realmName, alias); if (!flow.isPresent()) { - throw new KeycloakRepositoryException( - String.format("Cannot find top-level-flow '%s' in realm '%s'.", alias, realmName) - ); + throw new KeycloakRepositoryException("Cannot find top-level-flow '%s' in realm '%s'.", alias, realmName); } return flow.get(); @@ -86,9 +85,8 @@ public void createTopLevel(String realmName, AuthenticationFlowRepresentation fl logger.trace("Create top-level-flow '{}' in realm '{}'", flow.getAlias(), realmName); AuthenticationManagementResource flowsResource = getFlowResources(realmName); - try { - Response response = flowsResource.createFlow(flow); - ResponseUtil.validate(response); + try (Response response = flowsResource.createFlow(flow)) { + CreatedResponseUtil.getCreatedId(response); } catch (WebApplicationException error) { String errorMessage = String.format( "Cannot create top-level-flow '%s' in realm '%s': %s", diff --git a/src/main/java/de/adorsys/keycloak/config/repository/ClientRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/ClientRepository.java index 79618efd2..fe389b79d 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/ClientRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/ClientRepository.java @@ -23,10 +23,12 @@ import de.adorsys.keycloak.config.exception.ImportProcessingException; import de.adorsys.keycloak.config.exception.KeycloakRepositoryException; import de.adorsys.keycloak.config.util.ResponseUtil; +import org.keycloak.admin.client.CreatedResponseUtil; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.representations.idm.ManagementPermissionRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; @@ -85,7 +87,7 @@ public ClientRepresentation getByClientId(String realmName, String clientId) { Optional foundClients = searchByClientId(realmName, clientId); if (!foundClients.isPresent()) { - throw new KeycloakRepositoryException(String.format("Cannot find client by clientId '%s'", clientId)); + throw new KeycloakRepositoryException("Cannot find client by clientId '%s'", clientId); } return foundClients.get(); @@ -95,7 +97,7 @@ public ClientRepresentation getByName(String realmName, String name) { Optional foundClients = searchByName(realmName, name); if (!foundClients.isPresent()) { - throw new KeycloakRepositoryException(String.format("Cannot find client by name '%s'", name)); + throw new KeycloakRepositoryException("Cannot find client by name '%s'", name); } return foundClients.get(); @@ -111,9 +113,8 @@ public String getClientSecret(String realmName, String clientId) { } public void create(String realmName, ClientRepresentation client) { - try { - Response response = getResource(realmName).create(client); - ResponseUtil.validate(response); + try (Response response = getResource(realmName).create(client)) { + CreatedResponseUtil.getCreatedId(response); } catch (WebApplicationException error) { String errorMessage = ResponseUtil.getErrorMessage(error); @@ -142,7 +143,7 @@ public ClientResource getResourceById(String realmName, String id) { ClientResource client = getResource(realmName).get(id); if (client == null) { - throw new KeycloakRepositoryException(String.format("Cannot find client by id '%s'", id)); + throw new KeycloakRepositoryException("Cannot find client by id '%s'", id); } return client; @@ -173,10 +174,9 @@ public void updateAuthorizationSettings(String realmName, String id, ResourceSer public void createAuthorizationResource(String realmName, String id, ResourceRepresentation resource) { ClientResource clientResource = getResourceById(realmName, id); - Response response = clientResource.authorization().resources().create(resource); - - // CreatedResponseUtil.getCreatedId results into hangs ... - ResponseUtil.validate(response); + try (Response response = clientResource.authorization().resources().create(resource)) { + CreatedResponseUtil.getCreatedId(response); + } } public void updateAuthorizationResource(String realmName, String id, ResourceRepresentation resource) { @@ -192,11 +192,9 @@ public void removeAuthorizationResource(String realmName, String id, String reso public void addAuthorizationScope(String realmName, String id, String name) { ClientResource clientResource = getResourceById(realmName, id); - Response response = clientResource.authorization() - .scopes().create(new ScopeRepresentation(name)); - - // CreatedResponseUtil.getCreatedId results into hangs ... - ResponseUtil.validate(response); + try (Response response = clientResource.authorization().scopes().create(new ScopeRepresentation(name))) { + CreatedResponseUtil.getCreatedId(response); + } } public void updateAuthorizationScope(String realmName, String id, ScopeRepresentation scope) { @@ -212,8 +210,9 @@ public void removeAuthorizationScope(String realmName, String id, String scopeId public void createAuthorizationPolicy(String realmName, String id, PolicyRepresentation policy) { ClientResource clientResource = getResourceById(realmName, id); - Response response = clientResource.authorization().policies().create(policy); - ResponseUtil.validate(response); + try (Response response = clientResource.authorization().policies().create(policy)) { + CreatedResponseUtil.getCreatedId(response); + } } public void updateAuthorizationPolicy(String realmName, String id, PolicyRepresentation policy) { @@ -279,4 +278,16 @@ public void removeOptionalClientScopes(String realmName, String clientId, clientResource.removeOptionalClientScope(optionalClientScope.getId()); } } + + public void enablePermission(String realmName, String id) { + ClientResource clientResource = getResourceById(realmName, id); + + clientResource.setPermissions(new ManagementPermissionRepresentation(true)); + } + + public boolean isPermissionEnabled(String realmName, String id) { + ClientResource clientResource = getResourceById(realmName, id); + + return clientResource.getPermissions().isEnabled(); + } } diff --git a/src/main/java/de/adorsys/keycloak/config/repository/ClientScopeRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/ClientScopeRepository.java index 76dbfafbf..e70a46100 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/ClientScopeRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/ClientScopeRepository.java @@ -22,6 +22,7 @@ import de.adorsys.keycloak.config.exception.ImportProcessingException; import de.adorsys.keycloak.config.util.ResponseUtil; +import org.keycloak.admin.client.CreatedResponseUtil; import org.keycloak.admin.client.resource.ClientScopeResource; import org.keycloak.admin.client.resource.ClientScopesResource; import org.keycloak.admin.client.resource.ProtocolMappersResource; @@ -77,8 +78,9 @@ public ClientScopeRepresentation getById(String realmName, String clientScopeId) } public void create(String realmName, ClientScopeRepresentation clientScope) { - Response response = realmRepository.getResource(realmName).clientScopes().create(clientScope); - ResponseUtil.validate(response); + try (Response response = realmRepository.getResource(realmName).clientScopes().create(clientScope)) { + CreatedResponseUtil.getCreatedId(response); + } } public void delete(String realmName, String id) { @@ -96,8 +98,9 @@ public void addProtocolMappers(String realmName, String clientScopeId, List realmComponents = getComponentsResource(realmName).query(); @@ -87,10 +97,8 @@ public ComponentRepresentation getByName(String realmName, String providerType, } throw new KeycloakRepositoryException( - String.format( - "Cannot find component by name '%s' and subtype '%s' in realm '%s' ", - name, providerType, realmName - ) + "Cannot find component by name '%s' and subtype '%s' in realm '%s' ", + name, providerType, realmName ); } diff --git a/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java index 118c212f9..ad429c4e7 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java @@ -64,10 +64,8 @@ public List getExecutionFlowsByAlias( : ""; throw new KeycloakRepositoryException( - String.format( - "Cannot find stored execution by authenticator '%s%s' in top-level flow '%s' in realm '%s'", - execution.getAuthenticator(), withSubFlow, topLevelFlowAlias, realmName - ) + "Cannot find stored execution by authenticator '%s%s' in top-level flow '%s' in realm '%s'", + execution.getAuthenticator(), withSubFlow, topLevelFlowAlias, realmName ); } return executions; @@ -113,8 +111,7 @@ public String createTopLevelFlowExecution( AuthenticationManagementResource flowsResource = authenticationFlowRepository .getFlowResources(realmName); - try { - Response response = flowsResource.addExecution(executionToCreate); + try (Response response = flowsResource.addExecution(executionToCreate)) { return CreatedResponseUtil.getCreatedId(response); } catch (WebApplicationException error) { AuthenticationFlowRepresentation parentFlow = authenticationFlowRepository diff --git a/src/main/java/de/adorsys/keycloak/config/repository/GroupRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/GroupRepository.java index 4b9e2af84..b258ba0b2 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/GroupRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/GroupRepository.java @@ -21,7 +21,7 @@ package de.adorsys.keycloak.config.repository; import de.adorsys.keycloak.config.exception.ImportProcessingException; -import de.adorsys.keycloak.config.util.ResponseUtil; +import org.keycloak.admin.client.CreatedResponseUtil; import org.keycloak.admin.client.resource.*; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.GroupRepresentation; @@ -91,18 +91,17 @@ public Optional searchByName(String realmName, String group } public void createGroup(String realmName, GroupRepresentation group) { - Response response = realmRepository.getResource(realmName) - .groups() - .add(group); - - ResponseUtil.validate(response); + GroupsResource groupsResource = realmRepository.getResource(realmName).groups(); + try (Response response = groupsResource.add(group)) { + CreatedResponseUtil.getCreatedId(response); + } } public void addSubGroup(String realmName, String parentGroupId, GroupRepresentation subGroup) { GroupResource groupResource = getResourceById(realmName, parentGroupId); - Response response = groupResource.subGroup(subGroup); - - ResponseUtil.validate(response); + try (Response response = groupResource.subGroup(subGroup)) { + CreatedResponseUtil.getCreatedId(response); + } } public GroupRepresentation getSubGroupByName(String realmName, String parentGroupId, String name) { diff --git a/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java index e224f4178..ac71c2b65 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderMapperRepository.java @@ -20,7 +20,8 @@ package de.adorsys.keycloak.config.repository; -import de.adorsys.keycloak.config.util.ResponseUtil; +import org.keycloak.admin.client.CreatedResponseUtil; +import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.IdentityProvidersResource; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; @@ -76,14 +77,13 @@ public List getAll(String realmName) { } public void create(String realmName, IdentityProviderMapperRepresentation identityProviderMapper) { - IdentityProvidersResource identityProvidersResource = realmRepository - .getResource(realmName).identityProviders(); + IdentityProviderResource resource = realmRepository + .getResource(realmName).identityProviders() + .get(identityProviderMapper.getIdentityProviderAlias()); - Response response = identityProvidersResource - .get(identityProviderMapper.getIdentityProviderAlias()) - .addMapper(identityProviderMapper); - - ResponseUtil.validate(response); + try (Response response = resource.addMapper(identityProviderMapper)) { + CreatedResponseUtil.getCreatedId(response); + } } public void update(String realmName, IdentityProviderMapperRepresentation identityProviderMapperToUpdate) { diff --git a/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderRepository.java index 8a27c29ba..28e42afb7 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/IdentityProviderRepository.java @@ -20,7 +20,7 @@ package de.adorsys.keycloak.config.repository; -import de.adorsys.keycloak.config.util.ResponseUtil; +import org.keycloak.admin.client.CreatedResponseUtil; import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.IdentityProvidersResource; import org.keycloak.representations.idm.IdentityProviderRepresentation; @@ -70,8 +70,9 @@ public List getAll(String realmName) { public void create(String realmName, IdentityProviderRepresentation identityProvider) { IdentityProvidersResource identityProvidersResource = realmRepository.getResource(realmName).identityProviders(); - Response response = identityProvidersResource.create(identityProvider); - ResponseUtil.validate(response); + try (Response response = identityProvidersResource.create(identityProvider)) { + CreatedResponseUtil.getCreatedId(response); + } } public void update(String realmName, IdentityProviderRepresentation identityProviderToUpdate) { diff --git a/src/main/java/de/adorsys/keycloak/config/repository/RequiredActionRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/RequiredActionRepository.java index 55397d783..4b55fb683 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/RequiredActionRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/RequiredActionRepository.java @@ -47,7 +47,7 @@ public RequiredActionProviderRepresentation getNewlyCreated(String realmName, St RequiredActionProviderRepresentation requiredActions = getByAlias(realmName, requiredActionProviderId); if (requiredActions == null || !Objects.equals(requiredActions.getName(), name)) { - throw new KeycloakRepositoryException("Can't find newly created required action: " + requiredActionProviderId); + throw new KeycloakRepositoryException("Can't find newly created required action: %s", requiredActionProviderId); } return requiredActions; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/RoleCompositeRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/RoleCompositeRepository.java index 54276915f..aacbf2901 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/RoleCompositeRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/RoleCompositeRepository.java @@ -295,7 +295,7 @@ private void addClientComposites( RoleRepresentation clientRole = roleRepository.getClientRole(realmName, compositeClientId, clientRoleName); if (clientRole == null) { throw new KeycloakRepositoryException( - String.format("Cannot find client role '%s' within realm '%s'", clientRoleName, realmName) + "Cannot find client role '%s' within realm '%s'", clientRoleName, realmName ); } return clientRole; diff --git a/src/main/java/de/adorsys/keycloak/config/repository/RoleRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/RoleRepository.java index e854a2ea5..1d4738557 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/RoleRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/RoleRepository.java @@ -87,11 +87,9 @@ public void deleteRealmRole(String realmName, RoleRepresentation roleToUpdate) { public RoleRepresentation getRealmRole(String realmName, String roleName) { return searchRealmRole(realmName, roleName) - .orElseThrow( - () -> new KeycloakRepositoryException( - String.format("Cannot find realm role '%s' within realm '%s'", roleName, realmName) - ) - ); + .orElseThrow(() -> new KeycloakRepositoryException( + "Cannot find realm role '%s' within realm '%s'", roleName, realmName + )); } public List getRealmRoles(String realmName) { @@ -139,10 +137,8 @@ public List getClientRolesByName(String realmName, String cl roles.add(clientResource.roles().get(roleName).toRepresentation()); } catch (javax.ws.rs.NotFoundException e) { throw new KeycloakRepositoryException( - String.format( - "Cannot find client role '%s' for client '%s' within realm '%s'", - roleName, clientId, realmName - ) + "Cannot find client role '%s' for client '%s' within realm '%s'", + roleName, clientId, realmName ); } } diff --git a/src/main/java/de/adorsys/keycloak/config/repository/ScopeMappingRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/ScopeMappingRepository.java index 90598023d..a8e15154d 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/ScopeMappingRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/ScopeMappingRepository.java @@ -137,7 +137,7 @@ private ClientScopeRepresentation findClientScope(String realmName, String clien .filter(c -> Objects.equals(c.getName(), clientScopeName)) .findFirst() .orElseThrow(() -> new KeycloakRepositoryException( - String.format("Cannot find client-scope by name '%s'", clientScopeName) + "Cannot find client-scope by name '%s'", clientScopeName )); } } diff --git a/src/main/java/de/adorsys/keycloak/config/repository/StateRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/StateRepository.java index 29e7ac56e..9f3f11fb1 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/StateRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/StateRepository.java @@ -152,7 +152,7 @@ private Map retrieveCustomAttributes(String realmName) { return existingRealm.getAttributes(); } - public void setState(String entity, List values) { + public void setState(String entity, List values) { String valuesAsString = toJson(values); if (this.importConfigProperties.getStateEncryptionKey() != null) { diff --git a/src/main/java/de/adorsys/keycloak/config/repository/UserRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/UserRepository.java index 742cc9047..e778751a2 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/UserRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/UserRepository.java @@ -21,7 +21,7 @@ package de.adorsys.keycloak.config.repository; import de.adorsys.keycloak.config.exception.KeycloakRepositoryException; -import de.adorsys.keycloak.config.util.ResponseUtil; +import org.keycloak.admin.client.CreatedResponseUtil; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; @@ -66,17 +66,18 @@ final UserResource getResource(String realmName, String username) { public UserRepresentation get(String realmName, String username) { Optional user = search(realmName, username); - return user.orElseThrow(() -> new KeycloakRepositoryException( - String.format("Cannot find user '%s' in realm '%s'", username, realmName) - )); + return user.orElseThrow( + () -> new KeycloakRepositoryException("Cannot find user '%s' in realm '%s'", username, realmName) + ); } - public void create(String realmName, UserRepresentation userToCreate) { + public void create(String realmName, UserRepresentation user) { RealmResource realmResource = realmRepository.getResource(realmName); UsersResource usersResource = realmResource.users(); - Response response = usersResource.create(userToCreate); - ResponseUtil.validate(response); + try (Response response = usersResource.create(user)) { + CreatedResponseUtil.getCreatedId(response); + } } public void updateUser(String realmName, UserRepresentation user) { diff --git a/src/main/java/de/adorsys/keycloak/config/service/ClientAuthorizationImportService.java b/src/main/java/de/adorsys/keycloak/config/service/ClientAuthorizationImportService.java new file mode 100644 index 000000000..2afd6b95a --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/ClientAuthorizationImportService.java @@ -0,0 +1,567 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com + * --- + * 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. + * ---license-end + */ + +package de.adorsys.keycloak.config.service; + +import de.adorsys.keycloak.config.exception.ImportProcessingException; +import de.adorsys.keycloak.config.model.RealmImport; +import de.adorsys.keycloak.config.properties.ImportConfigProperties; +import de.adorsys.keycloak.config.repository.ClientRepository; +import de.adorsys.keycloak.config.service.state.StateService; +import de.adorsys.keycloak.config.util.CloneUtil; +import de.adorsys.keycloak.config.util.JsonUtil; +import de.adorsys.keycloak.config.util.KeycloakUtil; +import org.apache.commons.lang3.StringUtils; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; +import org.keycloak.representations.idm.authorization.ScopeRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.ws.rs.NotFoundException; + +import static de.adorsys.keycloak.config.properties.ImportConfigProperties.ImportManagedProperties.ImportManagedPropertiesValues.FULL; +import static java.lang.Boolean.TRUE; + +@Service +@SuppressWarnings({"java:S1192"}) +public class ClientAuthorizationImportService { + private static final Logger logger = LoggerFactory.getLogger(ClientAuthorizationImportService.class); + + public static final String REALM_MANAGEMENT_CLIENT_ID = "realm-management"; + + private final ClientRepository clientRepository; + private final ImportConfigProperties importConfigProperties; + private final StateService stateService; + + @Autowired + public ClientAuthorizationImportService( + ClientRepository clientRepository, + ImportConfigProperties importConfigProperties, + StateService stateService + ) { + this.clientRepository = clientRepository; + this.importConfigProperties = importConfigProperties; + this.stateService = stateService; + } + + public void doImport(RealmImport realmImport) { + List clients = realmImport.getClients(); + if (clients == null) { + return; + } + + updateClientAuthorizationSettings(realmImport, clients); + } + + private void updateClientAuthorizationSettings( + RealmImport realmImport, + List clients + ) { + String realmName = realmImport.getRealm(); + + List clientsWithAuthorization = clients.stream() + .filter(client -> client.getAuthorizationSettings() != null) + .collect(Collectors.toList()); + + for (ClientRepresentation client : clientsWithAuthorization) { + ClientRepresentation existingClient; + if (client.getClientId() != null) { + existingClient = clientRepository.getByClientId(realmName, client.getClientId()); + } else if (client.getName() != null) { + existingClient = clientRepository.getByName(realmName, client.getName()); + } else { + throw new ImportProcessingException("clients require client id or name."); + } + + updateAuthorization(realmName, existingClient, client.getAuthorizationSettings()); + } + } + + private void updateAuthorization( + String realmName, + ClientRepresentation client, + ResourceServerRepresentation authorizationSettingsToImport + ) { + if (importConfigProperties.isValidate() && !REALM_MANAGEMENT_CLIENT_ID.equals(client.getClientId()) + && (TRUE.equals(client.isBearerOnly()) || TRUE.equals(client.isPublicClient()))) { + throw new ImportProcessingException( + "Unsupported authorization settings for client '%s' in realm '%s': client must be confidential.", + getClientIdentifier(client), realmName + ); + } + + if (REALM_MANAGEMENT_CLIENT_ID.equals(client.getClientId())) { + createFineGrantedPermissions(realmName, authorizationSettingsToImport); + } + + ResourceServerRepresentation existingAuthorization = clientRepository.getAuthorizationConfigById( + realmName, client.getId() + ); + + handleAuthorizationSettings(realmName, client, existingAuthorization, authorizationSettingsToImport); + + final List sanitizedAuthorizationResources = sanitizeAuthorizationResources(realmName, authorizationSettingsToImport); + final List sanitizedAuthorizationPolicies = sanitizeAuthorizationPolicies(realmName, authorizationSettingsToImport); + + createOrUpdateAuthorizationResources(realmName, client, existingAuthorization.getResources(), sanitizedAuthorizationResources); + createOrUpdateAuthorizationScopes(realmName, client, existingAuthorization.getScopes(), authorizationSettingsToImport.getScopes()); + + if (importConfigProperties.getManaged().getClientAuthorizationResources() == FULL) { + removeAuthorizationResources(realmName, client, existingAuthorization.getResources(), sanitizedAuthorizationResources); + } + + removeAuthorizationPolicies(realmName, client, existingAuthorization.getPolicies(), sanitizedAuthorizationPolicies); + removeAuthorizationScopes(realmName, client, existingAuthorization.getScopes(), authorizationSettingsToImport.getScopes()); + + // refresh existingAuthorization + existingAuthorization = clientRepository.getAuthorizationConfigById( + realmName, client.getId() + ); + + createOrUpdateAuthorizationPolicies(realmName, client, existingAuthorization.getPolicies(), sanitizedAuthorizationPolicies); + } + + private List sanitizeAuthorizationResources(String realmName, ResourceServerRepresentation authorizationSettings) { + return authorizationSettings.getResources() + .stream() + .map(resource -> sanitizeAuthorizationResource(realmName, resource)) + .collect(Collectors.toList()); + } + + private List sanitizeAuthorizationPolicies(String realmName, ResourceServerRepresentation authorizationSettings) { + return authorizationSettings.getPolicies() + .stream() + .map(policy -> sanitizeAuthorizationPolicy(realmName, policy)) + .collect(Collectors.toList()); + } + + private ResourceRepresentation sanitizeAuthorizationResource(String realmName, ResourceRepresentation resource) { + if ("Client".equals(resource.getType())) { + resource.setName(getSanitizedAuthzName(realmName, resource.getName())); + } + + return resource; + } + + private PolicyRepresentation sanitizeAuthorizationPolicy(String realmName, PolicyRepresentation policy) { + policy.setName(getSanitizedAuthzName(realmName, policy.getName())); + + if (policy.getConfig().containsKey("resources") && policy.getConfig().get("resources").contains(".$")) { + String resources = sanitizeAuthorizationPolicyResource(realmName, policy.getConfig().get("resources")); + policy.getConfig().put("resources", resources); + } + + return policy; + } + + private String sanitizeAuthorizationPolicyResource(String realmName, String resources) { + List resourcesList = JsonUtil.fromJson(resources); + resourcesList = resourcesList.stream() + .map(resource -> getSanitizedAuthzName(realmName, resource)) + .collect(Collectors.toList()); + + resources = JsonUtil.toJson(resourcesList); + return resources; + } + + private void createFineGrantedPermissions(String realmName, ResourceServerRepresentation authorizationSettingsToImport) { + authorizationSettingsToImport.getResources() + .stream() + .filter(resource -> "Client".equals(resource.getType()) && resource.getName().contains("client.resource.")) + .forEach(resource -> { + String id = getClientIdFromName(realmName, resource.getName()) + .replace("client.resource.", ""); + try { + if (!clientRepository.isPermissionEnabled(realmName, id)) { + logger.debug("Enable permissions for client '{}' in realm '{}'", id, realmName); + clientRepository.enablePermission(realmName, id); + } + } catch (NotFoundException e) { + throw new ImportProcessingException("Cannot find client '%s' in realm '%s'", id, realmName); + } + }); + } + + private void handleAuthorizationSettings( + String realmName, + ClientRepresentation client, + ResourceServerRepresentation existingClientAuthorizationResources, + ResourceServerRepresentation authorizationResourcesToImport + ) { + String[] ignoredProperties = new String[]{"clientId", "policies", "resources", "permissions", "scopes"}; + + boolean isEquals = CloneUtil.deepEquals(authorizationResourcesToImport, existingClientAuthorizationResources, ignoredProperties); + + if (isEquals) return; + + ResourceServerRepresentation patchedAuthorizationSettings = CloneUtil + .patch(existingClientAuthorizationResources, authorizationResourcesToImport); + + patchedAuthorizationSettings.setId(client.getClientId()); + logger.debug("Update authorization settings for client '{}' in realm '{}'", getClientIdentifier(client), realmName); + clientRepository.updateAuthorizationSettings(realmName, client.getId(), patchedAuthorizationSettings); + } + + private void createOrUpdateAuthorizationResources( + String realmName, + ClientRepresentation client, + List existingClientAuthorizationResources, + List authorizationResourcesToImport + ) { + Map existingClientAuthorizationResourcesMap = + existingClientAuthorizationResources + .stream() + .collect(Collectors.toMap(ResourceRepresentation::getName, resource -> resource)); + + for (ResourceRepresentation authorizationResourceToImport : authorizationResourcesToImport) { + createOrUpdateAuthorizationResource(realmName, client, existingClientAuthorizationResourcesMap, authorizationResourceToImport); + } + } + + private void createOrUpdateAuthorizationResource( + String realmName, + ClientRepresentation client, + Map existingClientAuthorizationResourcesMap, + ResourceRepresentation authorizationResourceToImport + ) { + if (!existingClientAuthorizationResourcesMap.containsKey(authorizationResourceToImport.getName())) { + createAuthorizationResource(realmName, client, authorizationResourceToImport); + } else { + updateAuthorizationResource(realmName, client, existingClientAuthorizationResourcesMap, authorizationResourceToImport); + } + } + + private void createAuthorizationResource( + String realmName, + ClientRepresentation client, + ResourceRepresentation authorizationResourceToImport + ) { + // https://github.com/adorsys/keycloak-config-cli/issues/589 + setAuthorizationResourceOwner(authorizationResourceToImport); + + logger.debug("Create authorization resource '{}' for client '{}' in realm '{}'", + authorizationResourceToImport.getName(), getClientIdentifier(client), realmName); + + clientRepository.createAuthorizationResource(realmName, client.getId(), authorizationResourceToImport); + } + + private void updateAuthorizationResource( + String realmName, + ClientRepresentation client, + Map existingClientAuthorizationResourcesMap, + ResourceRepresentation authorizationResourceToImport + ) { + ResourceRepresentation existingClientAuthorizationResource = existingClientAuthorizationResourcesMap + .get(authorizationResourceToImport.getName()); + + if (existingClientAuthorizationResource.getOwner() != null + && existingClientAuthorizationResource.getOwner().getId() == null + && Objects.equals(existingClientAuthorizationResource.getOwner().getName(), authorizationResourceToImport.getOwner().getId())) { + existingClientAuthorizationResource.getOwner().setId(authorizationResourceToImport.getOwner().getId()); + existingClientAuthorizationResource.getOwner().setName(null); + } + + if (existingClientAuthorizationResource.getAttributes().isEmpty() && authorizationResourceToImport.getAttributes() == null) { + existingClientAuthorizationResource.setAttributes(null); + } + + boolean isEquals = CloneUtil.deepEquals( + authorizationResourceToImport, existingClientAuthorizationResource, "id", "_id" + ); + + if (isEquals) return; + + setAuthorizationResourceOwner(authorizationResourceToImport); + + authorizationResourceToImport.setId(existingClientAuthorizationResource.getId()); + logger.debug("Update authorization resource '{}' for client '{}' in realm '{}'", + authorizationResourceToImport.getName(), getClientIdentifier(client), realmName); + + clientRepository.updateAuthorizationResource(realmName, client.getId(), authorizationResourceToImport); + } + + private void removeAuthorizationResources( + String realmName, + ClientRepresentation client, + List existingClientAuthorizationResources, + List authorizationResourcesToImport + ) { + List authorizationResourceNamesToImport = authorizationResourcesToImport + .stream().map(ResourceRepresentation::getName) + .collect(Collectors.toList()); + + List managedClientAuthorizationResources = getManagedClientResources(client, existingClientAuthorizationResources); + + managedClientAuthorizationResources.stream() + .filter(resource -> !authorizationResourceNamesToImport.contains(resource.getName())) + .forEach(resource -> removeAuthorizationResource(realmName, client, resource)); + } + + private void removeAuthorizationResource( + String realmName, + ClientRepresentation client, + ResourceRepresentation existingClientAuthorizationResource + ) { + logger.debug("Remove authorization resource '{}' for client '{}' in realm '{}'", + existingClientAuthorizationResource.getName(), getClientIdentifier(client), realmName + ); + clientRepository.removeAuthorizationResource( + realmName, client.getId(), existingClientAuthorizationResource.getId() + ); + } + + private void createOrUpdateAuthorizationScopes( + String realmName, + ClientRepresentation client, + List existingClientAuthorizationScopes, + List authorizationScopesToImport + ) { + Map existingClientAuthorizationScopesMap = existingClientAuthorizationScopes + .stream() + .collect(Collectors.toMap(ScopeRepresentation::getName, scope -> scope)); + + for (ScopeRepresentation authorizationScopeToImport : authorizationScopesToImport) { + createOrUpdateAuthorizationScope( + realmName, client, existingClientAuthorizationScopesMap, authorizationScopeToImport + ); + } + } + + private void createOrUpdateAuthorizationScope( + String realmName, + ClientRepresentation client, + Map existingClientAuthorizationScopesMap, + ScopeRepresentation authorizationScopeToImport + ) { + String authorizationScopeNameToImport = authorizationScopeToImport.getName(); + if (!existingClientAuthorizationScopesMap.containsKey(authorizationScopeToImport.getName())) { + logger.debug("Add authorization scope '{}' for client '{}' in realm '{}'", + authorizationScopeNameToImport, getClientIdentifier(client), realmName + ); + clientRepository.addAuthorizationScope( + realmName, client.getId(), authorizationScopeNameToImport + ); + } else { + updateAuthorizationScope( + realmName, client, existingClientAuthorizationScopesMap, + authorizationScopeToImport, authorizationScopeNameToImport + ); + } + } + + private void updateAuthorizationScope( + String realmName, + ClientRepresentation client, + Map existingClientAuthorizationScopesMap, + ScopeRepresentation authorizationScopeToImport, + String authorizationScopeNameToImport + ) { + ScopeRepresentation existingClientAuthorizationScope = existingClientAuthorizationScopesMap + .get(authorizationScopeNameToImport); + + if (!CloneUtil.deepEquals(authorizationScopeToImport, existingClientAuthorizationScope, "id")) { + authorizationScopeToImport.setId(existingClientAuthorizationScope.getId()); + logger.debug("Update authorization scope '{}' for client '{}' in realm '{}'", + authorizationScopeNameToImport, getClientIdentifier(client), realmName); + + clientRepository.updateAuthorizationScope(realmName, client.getId(), authorizationScopeToImport); + } + } + + private void removeAuthorizationScopes( + String realmName, + ClientRepresentation client, + List existingClientAuthorizationScopes, + List authorizationScopesToImport + ) { + List authorizationScopeNamesToImport = authorizationScopesToImport + .stream().map(ScopeRepresentation::getName) + .collect(Collectors.toList()); + + for (ScopeRepresentation existingClientAuthorizationScope : existingClientAuthorizationScopes) { + if (!authorizationScopeNamesToImport.contains(existingClientAuthorizationScope.getName())) { + removeAuthorizationScope(realmName, client, existingClientAuthorizationScope); + } + } + } + + private void removeAuthorizationScope( + String realmName, + ClientRepresentation client, + ScopeRepresentation existingClientAuthorizationScope + ) { + logger.debug("Remove authorization scope '{}' for client '{}' in realm '{}'", + existingClientAuthorizationScope.getName(), getClientIdentifier(client), realmName); + + clientRepository.removeAuthorizationScope(realmName, client.getId(), existingClientAuthorizationScope.getId()); + } + + private void createOrUpdateAuthorizationPolicies( + String realmName, + ClientRepresentation client, + List existingClientAuthorizationPolicies, + List authorizationPoliciesToImport + ) { + Map existingClientAuthorizationPoliciesMap = existingClientAuthorizationPolicies + .stream() + .collect(Collectors.toMap(PolicyRepresentation::getName, resource -> resource)); + + for (PolicyRepresentation authorizationPolicyToImport : authorizationPoliciesToImport) { + createOrUpdateAuthorizationPolicy( + realmName, client, existingClientAuthorizationPoliciesMap, authorizationPolicyToImport + ); + } + } + + private void createOrUpdateAuthorizationPolicy( + String realmName, + ClientRepresentation client, + Map existingClientAuthorizationPoliciesMap, + PolicyRepresentation authorizationPolicyToImport + ) { + if (!existingClientAuthorizationPoliciesMap.containsKey(authorizationPolicyToImport.getName())) { + logger.debug("Create authorization policy '{}' for client '{}' in realm '{}'", + authorizationPolicyToImport.getName(), getClientIdentifier(client), realmName); + + clientRepository.createAuthorizationPolicy( + realmName, client.getId(), authorizationPolicyToImport + ); + } else { + updateAuthorizationPolicy( + realmName, client, existingClientAuthorizationPoliciesMap, authorizationPolicyToImport + ); + } + } + + private void updateAuthorizationPolicy( + String realmName, + ClientRepresentation client, + Map existingClientAuthorizationPoliciesMap, + PolicyRepresentation authorizationPolicyToImport + ) { + PolicyRepresentation existingClientAuthorizationPolicy = existingClientAuthorizationPoliciesMap + .get(authorizationPolicyToImport.getName()); + + if (!CloneUtil.deepEquals(authorizationPolicyToImport, existingClientAuthorizationPolicy, "id")) { + authorizationPolicyToImport.setId(existingClientAuthorizationPolicy.getId()); + + logger.debug( + "Update authorization policy '{}' for client '{}' in realm '{}'", + authorizationPolicyToImport.getName(), getClientIdentifier(client), realmName + ); + clientRepository.updateAuthorizationPolicy(realmName, client.getId(), authorizationPolicyToImport); + } + } + + private void removeAuthorizationPolicies( + String realmName, + ClientRepresentation client, + List existingClientAuthorizationPolicies, + List authorizationPoliciesToImport + ) { + List authorizationPolicyNamesToImport = authorizationPoliciesToImport + .stream().map(PolicyRepresentation::getName) + .collect(Collectors.toList()); + + for (PolicyRepresentation existingClientAuthorizationPolicy : existingClientAuthorizationPolicies) { + if (!authorizationPolicyNamesToImport.contains(existingClientAuthorizationPolicy.getName())) { + removeAuthorizationPolicy(realmName, client, existingClientAuthorizationPolicy); + } + } + } + + private void removeAuthorizationPolicy( + String realmName, + ClientRepresentation client, + PolicyRepresentation existingClientAuthorizationPolicy + ) { + logger.debug( + "Remove authorization policy '{}' for client '{}' in realm '{}'", + existingClientAuthorizationPolicy.getName(), getClientIdentifier(client), realmName + ); + + try { + clientRepository.removeAuthorizationPolicy( + realmName, client.getId(), existingClientAuthorizationPolicy.getId() + ); + } catch (NotFoundException ignored) { + // policies got deleted if linked resources are deleted, too. + } + } + + private String getClientIdentifier(ClientRepresentation client) { + return client.getName() != null && !KeycloakUtil.isDefaultClient(client) ? client.getName() : client.getClientId(); + } + + // https://github.com/adorsys/keycloak-config-cli/issues/589 + private void setAuthorizationResourceOwner(ResourceRepresentation representation) { + if (representation.getOwner() != null && representation.getOwner().getId() == null && representation.getOwner().getName() != null) { + representation.getOwner().setId(representation.getOwner().getName()); + representation.getOwner().setName(null); + } + } + + private List getManagedClientResources(ClientRepresentation client, List existingResources) { + if (importConfigProperties.isState()) { + String clientKey = Objects.equals(client.getId(), client.getClientId()) ? "name:" + client.getName() : client.getClientId(); + List clientResourcesInState = stateService.getClientAuthorizationResources(clientKey); + // ignore all object there are not in state + return existingResources.stream() + .filter(resource -> clientResourcesInState.contains(resource.getName()) || Objects.equals(resource.getName(), "Default Resource")) + .collect(Collectors.toList()); + } else { + return existingResources; + } + } + + private String getSanitizedAuthzName(String realmName, String name) { + String client = StringUtils.substringAfterLast(name, "."); + + if (!client.startsWith("$")) { + return name; + } + + String id = getClientIdFromName(realmName, name); + return name.replace(client, id); + } + + private String getClientIdFromName(String realmName, String name) { + String client = StringUtils.substringAfterLast(name, "."); + + if (!client.startsWith("$")) { + return name; + } + + try { + return clientRepository.getByClientId(realmName, client.substring(1)).getId(); + } catch (NotFoundException e) { + throw new ImportProcessingException("Cannot find client '%s' in realm '%s'", client.substring(1), realmName); + } + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/ClientImportService.java b/src/main/java/de/adorsys/keycloak/config/service/ClientImportService.java index 7cd0e91d1..14fc75731 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/ClientImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/ClientImportService.java @@ -28,12 +28,10 @@ import de.adorsys.keycloak.config.repository.ClientScopeRepository; import de.adorsys.keycloak.config.service.state.StateService; import de.adorsys.keycloak.config.util.*; +import org.apache.commons.lang3.ArrayUtils; +import org.keycloak.common.util.CollectionUtil; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation; -import org.keycloak.representations.idm.authorization.PolicyRepresentation; -import org.keycloak.representations.idm.authorization.ResourceRepresentation; -import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; -import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -42,7 +40,6 @@ import java.util.*; import java.util.function.Consumer; import java.util.stream.Collectors; -import javax.ws.rs.NotFoundException; import javax.ws.rs.WebApplicationException; import static de.adorsys.keycloak.config.properties.ImportConfigProperties.ImportManagedProperties.ImportManagedPropertiesValues.FULL; @@ -51,12 +48,14 @@ @Service @SuppressWarnings({"java:S1192"}) public class ClientImportService { + private static final Logger logger = LoggerFactory.getLogger(ClientImportService.class); + private static final String[] propertiesWithDependencies = new String[]{ "authenticationFlowBindingOverrides", "authorizationSettings", }; - private static final Logger logger = LoggerFactory.getLogger(ClientImportService.class); + public static final String REALM_MANAGEMENT_CLIENT_ID = "realm-management"; private final ClientRepository clientRepository; private final ClientScopeRepository clientScopeRepository; @@ -96,7 +95,6 @@ public void doImportDependencies(RealmImport realmImport) { return; } - updateClientAuthorizationSettings(realmImport, clients); updateClientAuthenticationFlowBindingOverrides(realmImport, clients); } @@ -145,21 +143,21 @@ private void createOrUpdateClient( ) { String realmName = realmImport.getRealm(); - if (importConfigProperties.isValidate() && client.getAuthorizationSettings() != null) { + // https://github.com/keycloak/keycloak/blob/74695c02423345dab892a0808bf9203c3f92af7c/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java#L2878-L2881 + if (importConfigProperties.isValidate() + && client.getAuthorizationSettings() != null && !REALM_MANAGEMENT_CLIENT_ID.equals(client.getClientId())) { if (TRUE.equals(client.isBearerOnly()) || TRUE.equals(client.isPublicClient())) { - throw new ImportProcessingException(String.format( - "Unsupported authorization settings for client '%s' in realm '%s': " - + "client must be confidential.", + throw new ImportProcessingException( + "Unsupported authorization settings for client '%s' in realm '%s': client must be confidential.", getClientIdentifier(client), realmName - )); + ); } if (!TRUE.equals(client.isServiceAccountsEnabled())) { - throw new ImportProcessingException(String.format( - "Unsupported authorization settings for client '%s' in realm '%s': " - + "serviceAccountsEnabled must be 'true'.", + throw new ImportProcessingException( + "Unsupported authorization settings for client '%s' in realm '%s': serviceAccountsEnabled must be 'true'.", getClientIdentifier(client), realmName - )); + ); } } @@ -185,7 +183,7 @@ private void updateClientIfNeeded( ClientRepresentation clientToUpdate, ClientRepresentation existingClient ) { - String[] propertiesToIgnore = ArrayUtil.concat(propertiesWithDependencies, "id", "access"); + String[] propertiesToIgnore = ArrayUtils.addAll(propertiesWithDependencies, "id", "access"); ClientRepresentation mergedClient = CloneUtil.patch(existingClient, clientToUpdate, propertiesToIgnore); if (!isClientEqual(realmName, existingClient, mergedClient)) { @@ -209,14 +207,19 @@ private boolean isClientEqual( ClientRepresentation existingClient, ClientRepresentation patchedClient ) { - String[] propertiesToIgnore = ArrayUtil.concat( - propertiesWithDependencies, "id", "secret", "access", "protocolMappers" + String[] propertiesToIgnore = ArrayUtils.addAll( + propertiesWithDependencies, "id", "secret", "access", "protocolMappers", "defaultClientScopes", "optionalClientScopes" ); if (!CloneUtil.deepEquals(existingClient, patchedClient, propertiesToIgnore)) { return false; } + if (!CollectionUtil.collectionEquals(patchedClient.getDefaultClientScopes(), existingClient.getDefaultClientScopes()) + || !CollectionUtil.collectionEquals(patchedClient.getOptionalClientScopes(), existingClient.getOptionalClientScopes())) { + return false; + } + boolean areProtocolMapperDifferent = !ProtocolMapperUtil.areProtocolMappersEqual( patchedClient.getProtocolMappers(), existingClient.getProtocolMappers() @@ -252,362 +255,6 @@ private void updateClient( } } - private void updateClientAuthorizationSettings( - RealmImport realmImport, - List clients - ) { - String realmName = realmImport.getRealm(); - - List clientsWithAuthorization = clients.stream() - .filter(client -> client.getAuthorizationSettings() != null) - .collect(Collectors.toList()); - - for (ClientRepresentation client : clientsWithAuthorization) { - ClientRepresentation existingClient = clientRepository.getByClientId(realmName, client.getClientId()); - updateAuthorization(realmName, existingClient, client.getAuthorizationSettings()); - } - } - - private void updateAuthorization( - String realmName, - ClientRepresentation client, - ResourceServerRepresentation authorizationSettingsToImport - ) { - if (importConfigProperties.isValidate() && (TRUE.equals(client.isBearerOnly()) || TRUE.equals(client.isPublicClient()))) { - throw new ImportProcessingException(String.format( - "Unsupported authorization settings for client '%s' in realm '%s': " - + "client must be confidential.", - getClientIdentifier(client), realmName - )); - } - - ResourceServerRepresentation existingAuthorization = clientRepository.getAuthorizationConfigById( - realmName, client.getId() - ); - - handleAuthorizationSettings(realmName, client, existingAuthorization, authorizationSettingsToImport); - - createOrUpdateAuthorizationResources(realmName, client, - existingAuthorization.getResources(), authorizationSettingsToImport.getResources()); - removeAuthorizationResources(realmName, client, - existingAuthorization.getResources(), authorizationSettingsToImport.getResources()); - - createOrUpdateAuthorizationScopes(realmName, client, - existingAuthorization.getScopes(), authorizationSettingsToImport.getScopes()); - removeAuthorizationScopes(realmName, client, - existingAuthorization.getScopes(), authorizationSettingsToImport.getScopes()); - - createOrUpdateAuthorizationPolicies(realmName, client, - existingAuthorization.getPolicies(), authorizationSettingsToImport.getPolicies()); - removeAuthorizationPolicies(realmName, client, - existingAuthorization.getPolicies(), authorizationSettingsToImport.getPolicies()); - } - - private void handleAuthorizationSettings( - String realmName, - ClientRepresentation client, - ResourceServerRepresentation existingClientAuthorizationResources, - ResourceServerRepresentation authorizationResourcesToImport - ) { - String[] ignoredProperties = new String[]{"policies", "resources", "permissions", "scopes"}; - - boolean isEquals = CloneUtil.deepEquals(authorizationResourcesToImport, existingClientAuthorizationResources, ignoredProperties); - - if (isEquals) return; - - ResourceServerRepresentation patchedAuthorizationSettings = CloneUtil - .patch(existingClientAuthorizationResources, authorizationResourcesToImport); - - logger.debug("Update authorization settings for client '{}' in realm '{}'", getClientIdentifier(client), realmName); - clientRepository.updateAuthorizationSettings(realmName, client.getId(), patchedAuthorizationSettings); - } - - private void createOrUpdateAuthorizationResources( - String realmName, - ClientRepresentation client, - List existingClientAuthorizationResources, - List authorizationResourcesToImport - ) { - Map existingClientAuthorizationResourcesMap = - existingClientAuthorizationResources - .stream() - .collect(Collectors.toMap(ResourceRepresentation::getName, resource -> resource)); - - for (ResourceRepresentation authorizationResourceToImport : authorizationResourcesToImport) { - createOrUpdateAuthorizationResource(realmName, client, existingClientAuthorizationResourcesMap, authorizationResourceToImport); - } - } - - private void createOrUpdateAuthorizationResource( - String realmName, - ClientRepresentation client, - Map existingClientAuthorizationResourcesMap, - ResourceRepresentation authorizationResourceToImport - ) { - if (!existingClientAuthorizationResourcesMap.containsKey(authorizationResourceToImport.getName())) { - createAuthorizationResource(realmName, client, authorizationResourceToImport); - } else { - updateAuthorizationResource(realmName, client, existingClientAuthorizationResourcesMap, authorizationResourceToImport); - } - } - - private void createAuthorizationResource( - String realmName, - ClientRepresentation client, - ResourceRepresentation authorizationResourceToImport - ) { - // https://github.com/adorsys/keycloak-config-cli/issues/589 - setAuthorizationResourceOwner(authorizationResourceToImport); - - logger.debug("Create authorization resource '{}' for client '{}' in realm '{}'", - authorizationResourceToImport.getName(), getClientIdentifier(client), realmName); - - clientRepository.createAuthorizationResource(realmName, client.getId(), authorizationResourceToImport); - } - - private void updateAuthorizationResource( - String realmName, - ClientRepresentation client, - Map existingClientAuthorizationResourcesMap, - ResourceRepresentation authorizationResourceToImport - ) { - ResourceRepresentation existingClientAuthorizationResource = existingClientAuthorizationResourcesMap - .get(authorizationResourceToImport.getName()); - - if (existingClientAuthorizationResource.getOwner() != null - && existingClientAuthorizationResource.getOwner().getId() == null - && Objects.equals(existingClientAuthorizationResource.getOwner().getName(), authorizationResourceToImport.getOwner().getId())) { - existingClientAuthorizationResource.getOwner().setId(authorizationResourceToImport.getOwner().getId()); - existingClientAuthorizationResource.getOwner().setName(null); - } - - if (existingClientAuthorizationResource.getAttributes().isEmpty() && authorizationResourceToImport.getAttributes() == null) { - existingClientAuthorizationResource.setAttributes(null); - } - - boolean isEquals = CloneUtil.deepEquals( - authorizationResourceToImport, existingClientAuthorizationResource, "id", "_id" - ); - - if (isEquals) return; - - setAuthorizationResourceOwner(authorizationResourceToImport); - - authorizationResourceToImport.setId(existingClientAuthorizationResource.getId()); - logger.debug("Update authorization resource '{}' for client '{}' in realm '{}'", - authorizationResourceToImport.getName(), getClientIdentifier(client), realmName); - - clientRepository.updateAuthorizationResource(realmName, client.getId(), authorizationResourceToImport); - } - - private void removeAuthorizationResources( - String realmName, - ClientRepresentation client, - List existingClientAuthorizationResources, - List authorizationResourcesToImport - ) { - List authorizationResourceNamesToImport = authorizationResourcesToImport - .stream().map(ResourceRepresentation::getName) - .collect(Collectors.toList()); - - for (ResourceRepresentation existingClientAuthorizationResource : existingClientAuthorizationResources) { - if (!authorizationResourceNamesToImport.contains(existingClientAuthorizationResource.getName())) { - removeAuthorizationResource(realmName, client, existingClientAuthorizationResource); - } - } - } - - private void removeAuthorizationResource( - String realmName, - ClientRepresentation client, - ResourceRepresentation existingClientAuthorizationResource - ) { - logger.debug("Remove authorization resource '{}' for client '{}' in realm '{}'", - existingClientAuthorizationResource.getName(), getClientIdentifier(client), realmName - ); - clientRepository.removeAuthorizationResource( - realmName, client.getId(), existingClientAuthorizationResource.getId() - ); - } - - private void createOrUpdateAuthorizationScopes( - String realmName, - ClientRepresentation client, - List existingClientAuthorizationScopes, - List authorizationScopesToImport - ) { - Map existingClientAuthorizationScopesMap = existingClientAuthorizationScopes - .stream() - .collect(Collectors.toMap(ScopeRepresentation::getName, scope -> scope)); - - for (ScopeRepresentation authorizationScopeToImport : authorizationScopesToImport) { - createOrUpdateAuthorizationScope( - realmName, client, existingClientAuthorizationScopesMap, authorizationScopeToImport - ); - } - } - - private void createOrUpdateAuthorizationScope( - String realmName, - ClientRepresentation client, - Map existingClientAuthorizationScopesMap, - ScopeRepresentation authorizationScopeToImport - ) { - String authorizationScopeNameToImport = authorizationScopeToImport.getName(); - if (!existingClientAuthorizationScopesMap.containsKey(authorizationScopeToImport.getName())) { - logger.debug("Add authorization scope '{}' for client '{}' in realm '{}'", - authorizationScopeNameToImport, getClientIdentifier(client), realmName - ); - clientRepository.addAuthorizationScope( - realmName, client.getId(), authorizationScopeNameToImport - ); - } else { - updateAuthorizationScope( - realmName, client, existingClientAuthorizationScopesMap, - authorizationScopeToImport, authorizationScopeNameToImport - ); - } - } - - private void updateAuthorizationScope( - String realmName, - ClientRepresentation client, - Map existingClientAuthorizationScopesMap, - ScopeRepresentation authorizationScopeToImport, - String authorizationScopeNameToImport - ) { - ScopeRepresentation existingClientAuthorizationScope = existingClientAuthorizationScopesMap - .get(authorizationScopeNameToImport); - - if (!CloneUtil.deepEquals(authorizationScopeToImport, existingClientAuthorizationScope, "id")) { - authorizationScopeToImport.setId(existingClientAuthorizationScope.getId()); - logger.debug("Update authorization scope '{}' for client '{}' in realm '{}'", - authorizationScopeNameToImport, getClientIdentifier(client), realmName); - - clientRepository.updateAuthorizationScope(realmName, client.getId(), authorizationScopeToImport); - } - } - - private void removeAuthorizationScopes( - String realmName, - ClientRepresentation client, - List existingClientAuthorizationScopes, - List authorizationScopesToImport - ) { - List authorizationScopeNamesToImport = authorizationScopesToImport - .stream().map(ScopeRepresentation::getName) - .collect(Collectors.toList()); - - for (ScopeRepresentation existingClientAuthorizationScope : existingClientAuthorizationScopes) { - if (!authorizationScopeNamesToImport.contains(existingClientAuthorizationScope.getName())) { - removeAuthorizationScope(realmName, client, existingClientAuthorizationScope); - } - } - } - - private void removeAuthorizationScope( - String realmName, - ClientRepresentation client, - ScopeRepresentation existingClientAuthorizationScope - ) { - logger.debug("Remove authorization scope '{}' for client '{}' in realm '{}'", - existingClientAuthorizationScope.getName(), getClientIdentifier(client), realmName); - - clientRepository.removeAuthorizationScope(realmName, client.getId(), existingClientAuthorizationScope.getId()); - } - - private void createOrUpdateAuthorizationPolicies( - String realmName, - ClientRepresentation client, - List existingClientAuthorizationPolicies, - List authorizationPoliciesToImport - ) { - Map existingClientAuthorizationPoliciesMap = existingClientAuthorizationPolicies - .stream() - .collect(Collectors.toMap(PolicyRepresentation::getName, resource -> resource)); - - for (PolicyRepresentation authorizationPolicyToImport : authorizationPoliciesToImport) { - createOrUpdateAuthorizationPolicy( - realmName, client, existingClientAuthorizationPoliciesMap, authorizationPolicyToImport - ); - } - } - - private void createOrUpdateAuthorizationPolicy( - String realmName, - ClientRepresentation client, - Map existingClientAuthorizationPoliciesMap, - PolicyRepresentation authorizationPolicyToImport - ) { - if (!existingClientAuthorizationPoliciesMap.containsKey(authorizationPolicyToImport.getName())) { - logger.debug("Create authorization policy '{}' for client '{}' in realm '{}'", - authorizationPolicyToImport.getName(), getClientIdentifier(client), realmName); - - clientRepository.createAuthorizationPolicy( - realmName, client.getId(), authorizationPolicyToImport - ); - } else { - updateAuthorizationPolicy( - realmName, client, existingClientAuthorizationPoliciesMap, authorizationPolicyToImport - ); - } - } - - private void updateAuthorizationPolicy( - String realmName, - ClientRepresentation client, - Map existingClientAuthorizationPoliciesMap, - PolicyRepresentation authorizationPolicyToImport - ) { - PolicyRepresentation existingClientAuthorizationPolicy = existingClientAuthorizationPoliciesMap - .get(authorizationPolicyToImport.getName()); - - if (!CloneUtil.deepEquals(authorizationPolicyToImport, existingClientAuthorizationPolicy, "id")) { - authorizationPolicyToImport.setId(existingClientAuthorizationPolicy.getId()); - logger.debug( - "Update authorization policy '{}' for client '{}' in realm '{}'", - authorizationPolicyToImport.getName(), getClientIdentifier(client), realmName - ); - clientRepository.updateAuthorizationPolicy(realmName, client.getId(), authorizationPolicyToImport); - } - } - - private void removeAuthorizationPolicies( - String realmName, - ClientRepresentation client, - List existingClientAuthorizationPolicies, - List authorizationPoliciesToImport - ) { - List authorizationPolicyNamesToImport = authorizationPoliciesToImport - .stream().map(PolicyRepresentation::getName) - .collect(Collectors.toList()); - - for (PolicyRepresentation existingClientAuthorizationPolicy : existingClientAuthorizationPolicies) { - if (!authorizationPolicyNamesToImport.contains(existingClientAuthorizationPolicy.getName())) { - removeAuthorizationPolicy(realmName, client, existingClientAuthorizationPolicy); - } - } - } - - private void removeAuthorizationPolicy( - String realmName, - ClientRepresentation client, - PolicyRepresentation existingClientAuthorizationPolicy - ) { - logger.debug( - "Remove authorization policy '{}' for client '{}' in realm '{}'", - existingClientAuthorizationPolicy.getName(), getClientIdentifier(client), realmName - ); - - try { - clientRepository.removeAuthorizationPolicy( - realmName, client.getId(), existingClientAuthorizationPolicy.getId() - ); - } catch (NotFoundException ignored) { - // policies got deleted if linked resources are deleted, too. - } - } - private void updateClientAuthenticationFlowBindingOverrides( RealmImport realmImport, List clients @@ -725,14 +372,6 @@ private void updateClientDefaultOptionalClientScopes( } private String getClientIdentifier(ClientRepresentation client) { - return client.getClientId() != null ? client.getClientId() : client.getName(); - } - - // https://github.com/adorsys/keycloak-config-cli/issues/589 - private void setAuthorizationResourceOwner(ResourceRepresentation representation) { - if (representation.getOwner() != null && representation.getOwner().getId() == null && representation.getOwner().getName() != null) { - representation.getOwner().setId(representation.getOwner().getName()); - representation.getOwner().setName(null); - } + return client.getName() != null && !KeycloakUtil.isDefaultClient(client) ? client.getName() : client.getClientId(); } } diff --git a/src/main/java/de/adorsys/keycloak/config/service/ComponentImportService.java b/src/main/java/de/adorsys/keycloak/config/service/ComponentImportService.java index 2a53ab77d..4dd6da053 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/ComponentImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/ComponentImportService.java @@ -116,10 +116,10 @@ private void createComponent(String realmName, String providerType, ComponentExp componentToCreate.setParentId(parentId); } - componentRepository.create(realmName, componentToCreate); + String componentId = componentRepository.create(realmName, componentToCreate); MultivaluedHashMap subComponents = component.getSubComponents(); - ComponentRepresentation exitingComponent = componentRepository.getByName(realmName, providerType, component.getName()); + ComponentRepresentation exitingComponent = componentRepository.getById(realmName, componentId); if (!subComponents.isEmpty()) { createOrUpdateSubComponents(realmName, subComponents, exitingComponent.getId()); diff --git a/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java b/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java index ed0cc9c61..20664fc6b 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/RealmImportService.java @@ -74,6 +74,7 @@ public class RealmImportService { private final RequiredActionsImportService requiredActionsImportService; private final CustomImportService customImportService; private final ScopeMappingImportService scopeMappingImportService; + private final ClientAuthorizationImportService clientAuthorizationImportService; private final ClientScopeMappingImportService clientScopeMappingImportService; private final IdentityProviderImportService identityProviderImportService; @@ -99,6 +100,7 @@ public RealmImportService( RequiredActionsImportService requiredActionsImportService, CustomImportService customImportService, ScopeMappingImportService scopeMappingImportService, + ClientAuthorizationImportService clientAuthorizationImportService, ClientScopeMappingImportService clientScopeMappingImportService, IdentityProviderImportService identityProviderImportService, ChecksumService checksumService, @@ -118,6 +120,7 @@ public RealmImportService( this.requiredActionsImportService = requiredActionsImportService; this.customImportService = customImportService; this.scopeMappingImportService = scopeMappingImportService; + this.clientAuthorizationImportService = clientAuthorizationImportService; this.clientScopeMappingImportService = clientScopeMappingImportService; this.identityProviderImportService = identityProviderImportService; this.checksumService = checksumService; @@ -194,6 +197,7 @@ private void configureRealm(RealmImport realmImport, RealmRepresentation existin requiredActionsImportService.doImport(realmImport); authenticationFlowsImportService.doImport(realmImport); authenticatorConfigImportService.doImport(realmImport); + clientAuthorizationImportService.doImport(realmImport); clientImportService.doImportDependencies(realmImport); identityProviderImportService.doImport(realmImport); scopeMappingImportService.doImport(realmImport); diff --git a/src/main/java/de/adorsys/keycloak/config/service/state/StateService.java b/src/main/java/de/adorsys/keycloak/config/service/state/StateService.java index 08fafde8e..477381cef 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/state/StateService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/state/StateService.java @@ -25,6 +25,8 @@ import de.adorsys.keycloak.config.repository.StateRepository; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.representations.idm.*; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -72,6 +74,7 @@ public void doImport(RealmImport realmImport) { setClients(realmImport); setRequiredActions(realmImport); setComponents(realmImport); + setClientAuthorizationResources(realmImport); stateRepository.update(realmImport); logger.debug("Updated states of realm '{}'", realmImport.getRealm()); @@ -88,7 +91,7 @@ private void setRealmRoles(RealmImport realmImport) { List rolesRealm = roles.getRealm(); if (rolesRealm == null) return; - List state = rolesRealm + List state = rolesRealm .stream() .map(RoleRepresentation::getName) .collect(Collectors.toList()); @@ -104,7 +107,7 @@ private void setClientRoles(RealmImport realmImport) { if (clientRoles == null) return; for (Map.Entry> client : clientRoles.entrySet()) { - List state = client.getValue() + List state = client.getValue() .stream() .map(RoleRepresentation::getName) .collect(Collectors.toList()); @@ -113,15 +116,40 @@ private void setClientRoles(RealmImport realmImport) { } } + private void setClientAuthorizationResources(RealmImport realmImport) { + List clients = realmImport.getClients(); + if (clients == null) return; + + for (ClientRepresentation client : clients) { + String clientKey = client.getClientId() != null ? client.getClientId() : "name:" + client.getName(); + + ResourceServerRepresentation authorizationSettings = client.getAuthorizationSettings(); + if (authorizationSettings == null) continue; + + List resources = client.getAuthorizationSettings().getResources(); + if (resources == null) continue; + + List resourceNames = resources.stream() + .map(ResourceRepresentation::getName) + .collect(Collectors.toList()); + + stateRepository.setState("resources-client-" + clientKey, resourceNames); + } + } + public List getClientRoles(String client) { return stateRepository.getState("roles-client-" + client); } + public List getClientAuthorizationResources(String client) { + return stateRepository.getState("resources-client-" + client); + } + private void setClients(RealmImport realmImport) { List clients = realmImport.getClients(); if (clients == null) return; - List state = new ArrayList<>(); + List state = new ArrayList<>(); for (ClientRepresentation client : clients) { if (client.getClientId() != null) { state.add(client.getClientId()); @@ -145,7 +173,9 @@ private void setRequiredActions(RealmImport realmImport) { List requiredActions = realmImport.getRequiredActions(); if (requiredActions == null) return; - List state = requiredActions.stream().map(RequiredActionProviderRepresentation::getAlias).collect(Collectors.toList()); + List state = requiredActions.stream() + .map(RequiredActionProviderRepresentation::getAlias) + .collect(Collectors.toList()); stateRepository.setState("required-actions", state); } @@ -164,7 +194,7 @@ private void setComponents(RealmImport realmImport) { MultivaluedHashMap components = realmImport.getComponents(); if (components == null) return; - List state = new ArrayList<>(); + List state = new ArrayList<>(); for (Map.Entry> entry : components.entrySet()) { for (ComponentExportRepresentation component : entry.getValue()) { @@ -184,7 +214,7 @@ private void setSubComponents(ComponentExportRepresentation component) { return; } - List state = new ArrayList<>(); + List state = new ArrayList<>(); for (Map.Entry> subEntry : subComponents.entrySet()) { List nameOfSubComponents = subEntry.getValue().stream() .map(ComponentExportRepresentation::getName) diff --git a/src/main/java/de/adorsys/keycloak/config/util/ResponseUtil.java b/src/main/java/de/adorsys/keycloak/config/util/ResponseUtil.java index b5270ad6c..df34bda18 100644 --- a/src/main/java/de/adorsys/keycloak/config/util/ResponseUtil.java +++ b/src/main/java/de/adorsys/keycloak/config/util/ResponseUtil.java @@ -20,26 +20,17 @@ package de.adorsys.keycloak.config.util; +import org.jboss.resteasy.client.jaxrs.internal.ClientResponse; + import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; public class ResponseUtil { ResponseUtil() { throw new IllegalStateException("Utility class"); } - public static void validate(Response response) { - if (!response.getStatusInfo().equals(Response.Status.CREATED)) { - Response.StatusType statusInfo = response.getStatusInfo(); - throw new WebApplicationException("Create method returned status " - + statusInfo.getReasonPhrase() + " (Code: " + statusInfo.getStatusCode() + "); " - + "expected status: Created (201)", response); - } - - response.close(); - } - public static String getErrorMessage(WebApplicationException error) { - return error.getMessage() + ": " + error.getResponse().readEntity(String.class).trim(); + String errorBody = !((ClientResponse) error.getResponse()).isClosed() ? error.getResponse().readEntity(String.class).trim() : ""; + return error.getMessage() + errorBody; } } diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index ec5640ea2..71b38ed43 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,5 +1,5 @@ spring.output.ansi.enabled=ALWAYS spring.config.import=classpath:application-debug.properties -keycloak.url=http://localhost:8080/auth/ +keycloak.url=http://localhost:8080/ keycloak.password=admin123 import.path=contrib/example-config/moped.json diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 738ac9b08..2a8297056 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -16,6 +16,7 @@ keycloak.read-timeout=10s keycloak.availability-check.enabled=false keycloak.availability-check.timeout=120s keycloak.availability-check.retry-delay=2s +import.hidden-files=false import.cache-key=default import.var-substitution=false import.var-substitution-in-variables=true @@ -33,7 +34,6 @@ import.parallel=false import.remove-default-role-from-user=false import.skip-attributes-for-federated-user=false - import.managed.authentication-flow=full import.managed.group=full import.managed.required-action=full @@ -46,7 +46,7 @@ import.managed.identity-provider=full import.managed.identity-provider-mapper=full import.managed.role=full import.managed.client=full - +import.managed.client-authorization-resources=full logging.group.http=org.apache.http.wire logging.group.keycloak-config-cli=de.adorsys.keycloak.config.service,de.adorsys.keycloak.config.KeycloakConfigRunner,de.adorsys.keycloak.config.provider.KeycloakProvider logging.group.kcc=de.adorsys.keycloak.config.service,de.adorsys.keycloak.config.KeycloakConfigRunner,de.adorsys.keycloak.config.provider.KeycloakProvider diff --git a/src/test/java/de/adorsys/keycloak/config/AbstractImportTest.java b/src/test/java/de/adorsys/keycloak/config/AbstractImportTest.java index fb3009958..2a7acc263 100644 --- a/src/test/java/de/adorsys/keycloak/config/AbstractImportTest.java +++ b/src/test/java/de/adorsys/keycloak/config/AbstractImportTest.java @@ -30,9 +30,7 @@ import de.adorsys.keycloak.config.test.util.KeycloakAuthentication; import de.adorsys.keycloak.config.test.util.KeycloakRepository; import de.adorsys.keycloak.config.util.VersionUtil; -import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; -import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; @@ -49,6 +47,7 @@ import java.io.File; import java.io.IOException; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import static java.util.concurrent.TimeUnit.SECONDS; @@ -61,9 +60,9 @@ initializers = {ConfigDataApplicationContextInitializer.class} ) @ActiveProfiles("IT") -@TestMethodOrder(OrderAnnotation.class) -//@ClassOrderer(ClassOrderer.OrderAnnotation) -@Timeout(value = 10, unit = SECONDS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@TestClassOrder(ClassOrderer.OrderAnnotation.class) +@Timeout(value = 30, unit = SECONDS) abstract public class AbstractImportTest { public static final ToStringConsumer KEYCLOAK_CONTAINER_LOGS = new ToStringConsumer(); @@ -72,10 +71,11 @@ abstract public class AbstractImportTest { protected static final String KEYCLOAK_VERSION = System.getProperty("keycloak.version"); protected static final String KEYCLOAK_IMAGE = System.getProperty("keycloak.dockerImage", "quay.io/keycloak/keycloak"); + protected static final String KEYCLOAK_TAG_SUFFIX = System.getProperty("keycloak.dockerTagSuffix", ""); protected static final String KEYCLOAK_LOG_LEVEL = System.getProperty("keycloak.loglevel", "INFO"); static { - KEYCLOAK_CONTAINER = new GenericContainer<>(DockerImageName.parse(KEYCLOAK_IMAGE + ":" + KEYCLOAK_VERSION)) + KEYCLOAK_CONTAINER = new GenericContainer<>(DockerImageName.parse(KEYCLOAK_IMAGE + ":" + KEYCLOAK_VERSION + KEYCLOAK_TAG_SUFFIX)) .withExposedPorts(8080) .withEnv("KEYCLOAK_USER", "admin") .withEnv("KEYCLOAK_PASSWORD", "admin123") @@ -89,14 +89,24 @@ abstract public class AbstractImportTest { .waitingFor(Wait.forHttp("/")) .withStartupTimeout(Duration.ofSeconds(300)); - boolean isQuarkusDistribution = (VersionUtil.ge(KEYCLOAK_VERSION, "17") && !KEYCLOAK_IMAGE.contains("legacy")) - || KEYCLOAK_IMAGE.contains("keycloak-x"); + boolean isLegacyDistribution = KEYCLOAK_CONTAINER.getDockerImageName().contains("legacy") + || (VersionUtil.lt(KEYCLOAK_VERSION, "17") && !KEYCLOAK_CONTAINER.getDockerImageName().contains("keycloak-x")); - if (isQuarkusDistribution) { + List command = new ArrayList<>(); + + if (isLegacyDistribution) { + command.add("-c"); + command.add("standalone.xml"); + command.add("-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled"); + } else { KEYCLOAK_CONTAINER.setCommand("start-dev"); + command.add("start-dev"); + command.add("--features"); + command.add("admin-fine-grained-authz"); } if (System.getProperties().getOrDefault("skipContainerStart", "false").equals("false")) { + KEYCLOAK_CONTAINER.setCommand(command.toArray(new String[0])); KEYCLOAK_CONTAINER.start(); KEYCLOAK_CONTAINER.followOutput(KEYCLOAK_CONTAINER_LOGS); @@ -107,10 +117,10 @@ abstract public class AbstractImportTest { "http://%s:%d", KEYCLOAK_CONTAINER.getContainerIpAddress(), KEYCLOAK_CONTAINER.getMappedPort(8080) )); - if (isQuarkusDistribution) { - System.setProperty("keycloak.url", System.getProperty("keycloak.baseUrl")); - } else { + if (isLegacyDistribution) { System.setProperty("keycloak.url", System.getProperty("keycloak.baseUrl") + "/auth/"); + } else { + System.setProperty("keycloak.url", System.getProperty("keycloak.baseUrl")); } } } diff --git a/src/test/java/de/adorsys/keycloak/config/extensions/LdapExtension.java b/src/test/java/de/adorsys/keycloak/config/extensions/LdapExtension.java index c3254058d..37a01b30f 100644 --- a/src/test/java/de/adorsys/keycloak/config/extensions/LdapExtension.java +++ b/src/test/java/de/adorsys/keycloak/config/extensions/LdapExtension.java @@ -30,7 +30,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; -import org.springframework.util.SocketUtils; import java.io.InputStream; @@ -40,7 +39,6 @@ public class LdapExtension implements BeforeAllCallback, AfterAllCallback { private static final Logger LOG = LoggerFactory.getLogger(LdapExtension.class); public final String ldif; public final String baseDN; - public final int port; public final String bindDN; public final String password; private InMemoryDirectoryServer server; @@ -48,7 +46,6 @@ public class LdapExtension implements BeforeAllCallback, AfterAllCallback { public LdapExtension(String baseDN, String ldif, String bindDN, String password) { this.ldif = ldif; this.baseDN = baseDN; - this.port = SocketUtils.findAvailableTcpPort(); this.bindDN = bindDN; this.password = password; } @@ -59,7 +56,7 @@ public void beforeAll(final ExtensionContext context) throws Exception { LOG.info("LDAP server starting..."); final InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDN); config.setSchema(null); // must be set or initialization fails with LDAPException - config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", port)); + config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", 0)); if (bindDN != null) { config.addAdditionalBindCredentials(bindDN, password); } @@ -71,6 +68,9 @@ public void beforeAll(final ExtensionContext context) throws Exception { server.startListening(); LOG.info("LDAP server started. Listen on port " + server.getListenPort()); + + System.setProperty("JUNIT_LDAP_HOST", "host.docker.internal"); + System.setProperty("JUNIT_LDAP_PORT", String.valueOf(server.getListenPort())); } } @@ -78,8 +78,4 @@ public void beforeAll(final ExtensionContext context) throws Exception { public void afterAll(final ExtensionContext context) { server.shutDown(true); } - - public int getPort() { - return port; - } } diff --git a/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java b/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java index d73494527..c626ebefc 100644 --- a/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java +++ b/src/test/java/de/adorsys/keycloak/config/properties/ImportConfigPropertiesTest.java @@ -31,7 +31,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; // From: https://tuhrig.de/testing-configurationproperties-in-spring-boot/ @ExtendWith(SpringExtension.class) @@ -39,6 +40,8 @@ @SpringBootTest(classes = {ImportConfigPropertiesTest.TestConfiguration.class}) @TestPropertySource(properties = { "spring.main.log-startup-info=false", + "import.hidden-files=true", + "import.exclude=exclude1,exclude2", "import.cache-key=custom", "import.var-substitution=true", "import.var-substitution-in-variables=false", @@ -65,6 +68,7 @@ "import.managed.identity-provider-mapper=no-delete", "import.managed.role=no-delete", "import.managed.client=no-delete", + "import.managed.client-authorization-resources=no-delete", "import.sync-user-federation=true", "import.remove-default-role-from-user=true", "import.skip-attributes-for-federated-user=true", @@ -77,7 +81,9 @@ class ImportConfigPropertiesTest { @Test @SuppressWarnings({"java:S2699", "java:S5961"}) void shouldPopulateConfigurationProperties() { - assertThat(properties.getPath(), is("other")); + assertThat(properties.getPath(), contains("other")); + assertThat(properties.getExclude(), contains("exclude1", "exclude2")); + assertThat(properties.isHiddenFiles(), is(true)); assertThat(properties.isVarSubstitution(), is(true)); assertThat(properties.isVarSubstitutionInVariables(), is(false)); assertThat(properties.isVarSubstitutionUndefinedThrowsExceptions(), is(false)); @@ -103,6 +109,7 @@ void shouldPopulateConfigurationProperties() { assertThat(properties.getManaged().getIdentityProviderMapper(), is(ImportManagedPropertiesValues.NO_DELETE)); assertThat(properties.getManaged().getRole(), is(ImportManagedPropertiesValues.NO_DELETE)); assertThat(properties.getManaged().getClient(), is(ImportManagedPropertiesValues.NO_DELETE)); + assertThat(properties.getManaged().getClientAuthorizationResources(), is(ImportManagedPropertiesValues.NO_DELETE)); assertThat(properties.isSyncUserFederation(), is(true)); assertThat(properties.isRemoveDefaultRoleFromUser(), is(true)); assertThat(properties.isSkipAttributesForFederatedUser(), is(true)); diff --git a/src/test/java/de/adorsys/keycloak/config/provider/KeycloakImportProviderIT.java b/src/test/java/de/adorsys/keycloak/config/provider/KeycloakImportProviderIT.java index e58d423b9..11ef96141 100644 --- a/src/test/java/de/adorsys/keycloak/config/provider/KeycloakImportProviderIT.java +++ b/src/test/java/de/adorsys/keycloak/config/provider/KeycloakImportProviderIT.java @@ -59,7 +59,7 @@ public void resetServer(MockServerClient client) { @Test void shouldReadLocalFile() { KeycloakImport keycloakImport = keycloakImportProvider - .readFromPath("classpath:import-files/import-single/0_create_realm.json"); + .readFromPaths("classpath:import-files/import-single/0_create_realm.json"); assertThat(keycloakImport.getRealmImports().keySet(), contains( matchesPattern(".+/0_create_realm\\.json$") @@ -70,17 +70,17 @@ void shouldReadLocalFile() { void shouldReadLocalFileLegacy() throws IOException { Path realmFile = Files.createTempFile("realm", ".json"); Path tempFilePath = Files.write(realmFile, - "{\"enabled\": true, \"realm\": \"realm-sorted-import\"}" .getBytes(StandardCharsets.UTF_8)); + "{\"enabled\": true, \"realm\": \"realm-sorted-import\"}".getBytes(StandardCharsets.UTF_8)); String importPath = tempFilePath.toAbsolutePath().toString(); KeycloakImport keycloakImport = keycloakImportProvider - .readFromPath(importPath); + .readFromPaths(importPath); assertThat(keycloakImport.getRealmImports().keySet(), contains(importPath)); } @Test void shouldReadLocalFilesFromDirectorySorted() { - KeycloakImport keycloakImport = keycloakImportProvider.readFromPath("classpath:import-files/import-sorted/"); + KeycloakImport keycloakImport = keycloakImportProvider.readFromPaths("classpath:import-files/import-sorted/"); assertThat(keycloakImport.getRealmImports().keySet(), contains( matchesPattern(".+/0_create_realm\\.json"), matchesPattern(".+/1_update_realm\\.json"), @@ -95,11 +95,62 @@ void shouldReadLocalFilesFromDirectorySorted() { )); } + @Test + void shouldReadLocalFilesFromDirectorySortedWithoutHiddenFiles() { + KeycloakImport keycloakImport = keycloakImportProvider.readFromPaths("classpath:import-files/import-sorted-hidden-files/"); + assertThat(keycloakImport.getRealmImports().keySet(), contains( + matchesPattern(".+/0_create_realm\\.json"), + matchesPattern(".+/1_update_realm\\.json"), + matchesPattern(".+/2_update_realm\\.json"), + matchesPattern(".+/4_update_realm\\.json"), + matchesPattern(".+/5_update_realm\\.json"), + matchesPattern(".+/6_update_realm\\.json"), + matchesPattern(".+/8_update_realm\\.json"), + matchesPattern(".+/9_update_realm\\.json") + )); + } + + @Test + void shouldReadLocalFilesFromWildcardPattern() { + KeycloakImport keycloakImport = keycloakImportProvider.readFromPaths("classpath:import-files/import-wildcard/*.json"); + assertThat(keycloakImport.getRealmImports().keySet(), contains( + matchesPattern(".+/0_create_realm\\.json") + )); + } + + @Test + void shouldReadLocalFilesFromDoubleWildcardPattern() { + KeycloakImport keycloakImport = keycloakImportProvider.readFromPaths("classpath:import-files/import-wildcard/**/*.json"); + assertThat(keycloakImport.getRealmImports().keySet(), contains( + matchesPattern(".+/0_create_realm\\.json"), + matchesPattern(".+/another/directory/1_update_realm\\.json"), + matchesPattern(".+/another/directory/2_update_realm\\.json"), + matchesPattern(".+/another/directory/3_update_realm\\.json"), + matchesPattern(".+/sub/directory/4_update_realm\\.json"), + matchesPattern(".+/sub/directory/5_update_realm\\.json"), + matchesPattern(".+/sub/directory/6_update_realm\\.json") + )); + } + + @Test + void shouldReadLocalFilesFromManyDirectories() { + KeycloakImport keycloakImport = keycloakImportProvider.readFromPaths("classpath:import-files/import-wildcard/sub/**", "classpath:import-files/import-wildcard/another/**/*.json"); + assertThat(keycloakImport.getRealmImports().keySet(), contains( + matchesPattern(".+/another/directory/1_update_realm\\.json"), + matchesPattern(".+/another/directory/2_update_realm\\.json"), + matchesPattern(".+/another/directory/3_update_realm\\.json"), + matchesPattern(".+/sub/directory/4_update_realm\\.json"), + matchesPattern(".+/sub/directory/5_update_realm\\.json"), + matchesPattern(".+/sub/directory/6_update_realm\\.json"), + matchesPattern(".+/sub/directory/7_update_realm\\.yaml") + )); + } + @Test void shouldReadLocalFilesFromZipArchive() { // Given KeycloakImport keycloakImport = keycloakImportProvider - .readFromPath("classpath:import-files/import-zip/realm-import.zip"); + .readFromPaths("classpath:import-files/import-zip/realm-import.zip"); // When Set importedFileNames = keycloakImport.getRealmImports().keySet(); @@ -118,7 +169,7 @@ void shouldReadLocalFilesFromZipArchive() { @Test void shouldFailOnUnknownPath() { - InvalidImportException exception = assertThrows(InvalidImportException.class, () -> keycloakImportProvider.readFromPath("classpath::")); + InvalidImportException exception = assertThrows(InvalidImportException.class, () -> keycloakImportProvider.readFromPaths("classpath::")); assertThat(exception.getMessage(), is("No resource extractor found to handle config property import.path=classpath::! Check your settings.")); } @@ -127,7 +178,7 @@ void shouldFailOnUnknownPath() { void shouldReadRemoteFile() { client.when(request()).respond(this::mockServerResponse); // Given - KeycloakImport keycloakImport = keycloakImportProvider.readFromPath(mockServerUrl() + "/import-single/0_create_realm.json"); + KeycloakImport keycloakImport = keycloakImportProvider.readFromPaths(mockServerUrl() + "/import-single/0_create_realm.json"); // When Set importedFileNames = keycloakImport.getRealmImports().keySet(); @@ -144,7 +195,7 @@ void shouldReadRemoteFilesFromZipArchive() { client.when(request()).respond(this::mockServerResponse); // Given - KeycloakImport keycloakImport = keycloakImportProvider.readFromPath(mockServerUrl() + "/import-zip/realm-import.zip"); + KeycloakImport keycloakImport = keycloakImportProvider.readFromPaths(mockServerUrl() + "/import-zip/realm-import.zip"); // When Set importedFileNames = keycloakImport.getRealmImports().keySet(); @@ -171,7 +222,7 @@ void shouldReadRemoteFileUsingBasicAuth() { // Given KeycloakImport keycloakImport = keycloakImportProvider - .readFromPath(mockServerUrl(userInfo) + "/import-single/0_create_realm.json"); + .readFromPaths(mockServerUrl(userInfo) + "/import-single/0_create_realm.json"); // When Set importedFileNames = keycloakImport.getRealmImports().keySet(); @@ -193,7 +244,7 @@ void shouldReadRemoteFilesFromZipArchiveUsingBasicAuth() { // Given KeycloakImport keycloakImport = keycloakImportProvider - .readFromPath(mockServerUrl(userInfo) + "/import-zip/realm-import.zip"); + .readFromPaths(mockServerUrl(userInfo) + "/import-zip/realm-import.zip"); // When Set importedFileNames = keycloakImport.getRealmImports().keySet(); diff --git a/src/test/java/de/adorsys/keycloak/config/provider/KeycloakImportProviderOptionsIT.java b/src/test/java/de/adorsys/keycloak/config/provider/KeycloakImportProviderOptionsIT.java new file mode 100644 index 000000000..502b81cdb --- /dev/null +++ b/src/test/java/de/adorsys/keycloak/config/provider/KeycloakImportProviderOptionsIT.java @@ -0,0 +1,75 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com + * --- + * 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. + * ---license-end + */ + +package de.adorsys.keycloak.config.provider; + +import de.adorsys.keycloak.config.AbstractImportTest; +import de.adorsys.keycloak.config.model.KeycloakImport; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.TestPropertySource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.matchesPattern; + +class KeycloakImportProviderOptionsIT { + @Nested + @TestPropertySource(properties = { + "import.hidden-files=true" + }) + class HiddenFilesTrue extends AbstractImportTest { + @Test + void shouldReadLocalFilesFromDirectorySorted() { + KeycloakImport keycloakImport = keycloakImportProvider.readFromPaths("classpath:import-files/import-sorted-hidden-files/"); + assertThat(keycloakImport.getRealmImports().keySet(), contains( + matchesPattern(".+/.3_update_realm\\.json"), + matchesPattern(".+/.7_update_realm\\.json"), + matchesPattern(".+/0_create_realm\\.json"), + matchesPattern(".+/1_update_realm\\.json"), + matchesPattern(".+/2_update_realm\\.json"), + matchesPattern(".+/4_update_realm\\.json"), + matchesPattern(".+/5_update_realm\\.json"), + matchesPattern(".+/6_update_realm\\.json"), + matchesPattern(".+/8_update_realm\\.json"), + matchesPattern(".+/9_update_realm\\.json") + )); + } + } + + @Nested + @TestPropertySource(properties = { + "import.exclude=**/*create*,**/4_*" + }) + class Exclude extends AbstractImportTest { + @Test + void shouldReadLocalFilesFromDirectorySorted() { + KeycloakImport keycloakImport = keycloakImportProvider.readFromPaths("classpath:import-files/import-sorted-hidden-files/"); + assertThat(keycloakImport.getRealmImports().keySet(), contains( + matchesPattern(".+/1_update_realm\\.json"), + matchesPattern(".+/2_update_realm\\.json"), + matchesPattern(".+/5_update_realm\\.json"), + matchesPattern(".+/6_update_realm\\.json"), + matchesPattern(".+/8_update_realm\\.json"), + matchesPattern(".+/9_update_realm\\.json") + )); + } + } +} diff --git a/src/test/java/de/adorsys/keycloak/config/service/AuthorizeImportUsingServiceAccountIT.java b/src/test/java/de/adorsys/keycloak/config/service/AuthorizeImportUsingServiceAccountIT.java index edce266d2..34f161fc6 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/AuthorizeImportUsingServiceAccountIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/AuthorizeImportUsingServiceAccountIT.java @@ -22,6 +22,7 @@ import de.adorsys.keycloak.config.AbstractImportTest; import de.adorsys.keycloak.config.provider.KeycloakProvider; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -93,7 +94,9 @@ void createNewRealm() throws IOException { @Test @Order(2) void logout() { - keycloakProvider.close(); + Assertions.assertDoesNotThrow(() -> { + keycloakProvider.close(); + }); } } @@ -131,7 +134,9 @@ void updateExistingRealm() throws IOException { @Test @Order(2) void logout() { - keycloakProvider.close(); + Assertions.assertDoesNotThrow(() -> { + keycloakProvider.close(); + }); } } } diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportClientsIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportClientsIT.java index 068eca127..2d5adcc81 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportClientsIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportClientsIT.java @@ -26,12 +26,16 @@ import de.adorsys.keycloak.config.exception.KeycloakRepositoryException; import de.adorsys.keycloak.config.model.RealmImport; import de.adorsys.keycloak.config.properties.ImportConfigProperties; +import de.adorsys.keycloak.config.properties.KeycloakConfigProperties; import de.adorsys.keycloak.config.util.VersionUtil; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; +import org.keycloak.authorization.client.AuthzClient; +import org.keycloak.authorization.client.Configuration; import org.keycloak.representations.idm.*; import org.keycloak.representations.idm.authorization.*; +import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; import java.util.Collections; @@ -51,6 +55,9 @@ class ImportClientsIT extends AbstractImportTest { private static final String REALM_NAME = "realmWithClients"; private static final String REALM_AUTH_FLOW_NAME = "realmWithClientsForAuthFlowOverrides"; + @Autowired + private KeycloakConfigProperties properties; + ImportClientsIT() { this.resourcePath = "import-files/clients"; } @@ -523,18 +530,20 @@ void shouldUpdateRealmRaiseErrorAddAuthorizationInvalidClient() throws IOExcepti thrown = assertThrows(ImportProcessingException.class, () -> realmImportService.doImport(foundImport2)); assertThat(thrown.getMessage(), is("Unsupported authorization settings for client 'auth-moped-client' in realm 'realmWithClients': serviceAccountsEnabled must be 'true'.")); + /* RealmImport foundImport3 = getFirstImport("10.3_update_realm__raise_error_update_authorization_client_bearer_only.json"); thrown = assertThrows(ImportProcessingException.class, () -> realmImportService.doImport(foundImport3)); - assertThat(thrown.getMessage(), is("Unsupported authorization settings for client 'realm-management' in realm 'realmWithClients': client must be confidential.")); + assertThat(thrown.getMessage(), is("Unsupported authorization settings for client 'auth-moped-client' in realm 'realmWithClients': client must be confidential.")); doImport("10.4.1_update_realm__raise_error_update_authorization_client_public.json"); RealmImport foundImport4 = getFirstImport("10.4.2_update_realm__raise_error_update_authorization_client_public.json"); thrown = assertThrows(ImportProcessingException.class, () -> realmImportService.doImport(foundImport4)); - assertThat(thrown.getMessage(), is("Unsupported authorization settings for client 'realm-management' in realm 'realmWithClients': client must be confidential.")); + assertThat(thrown.getMessage(), is("Unsupported authorization settings for client '${client_realm-management}' in realm 'realmWithClients': client must be confidential.")); RealmImport foundImport5 = getFirstImport("10.5_update_realm__raise_error_update_authorization_without_service_account_enabled.json"); thrown = assertThrows(ImportProcessingException.class, () -> realmImportService.doImport(foundImport5)); - assertThat(thrown.getMessage(), is("Unsupported authorization settings for client 'realm-management' in realm 'realmWithClients': serviceAccountsEnabled must be 'true'.")); + assertThat(thrown.getMessage(), is("Unsupported authorization settings for client '${client_realm-management}' in realm 'realmWithClients': serviceAccountsEnabled must be 'true'.")); + */ } @Test @@ -575,7 +584,7 @@ void shouldUpdateRealmAddAuthorization() throws IOException { ResourceServerRepresentation authorizationSettings = client.getAuthorizationSettings(); assertThat(authorizationSettings.getPolicyEnforcementMode(), is(PolicyEnforcementMode.ENFORCING)); - assertThat(authorizationSettings.isAllowRemoteResourceManagement(), is(false)); + assertThat(authorizationSettings.isAllowRemoteResourceManagement(), is(true)); assertThat(authorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); List authorizationSettingsResources = authorizationSettings.getResources(); @@ -672,11 +681,54 @@ void shouldUpdateRealmAddAuthorization() throws IOException { new ScopeRepresentation("urn:servlet-authz:page:main:actionForAdmin"), new ScopeRepresentation("urn:servlet-authz:page:main:actionForUser") )); + + client = getClientByName(realm, "missing-id-client"); + assertThat(client.getName(), is("missing-id-client")); + assertThat(client.getClientId(), not(emptyString())); + assertThat(client.getDescription(), is("Missing-Id-Client")); + assertThat(client.isEnabled(), is(true)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.isServiceAccountsEnabled(), is(true)); + assertThat(client.getAuthorizationServicesEnabled(), is(true)); + + authorizationSettings = client.getAuthorizationSettings(); + assertThat(authorizationSettings.getPolicyEnforcementMode(), is(PolicyEnforcementMode.ENFORCING)); + assertThat(authorizationSettings.isAllowRemoteResourceManagement(), is(false)); + assertThat(authorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + + authorizationSettingsResources = authorizationSettings.getResources(); + assertThat(authorizationSettingsResources, hasSize(1)); + + authorizationSettingsResource = getAuthorizationSettingsResource(authorizationSettingsResources, "Admin Resource"); + assertThat(authorizationSettingsResource.getUris(), containsInAnyOrder("/protected/admin/*")); + assertThat(authorizationSettingsResource.getType(), is("http://servlet-authz/protected/admin")); + assertThat(authorizationSettingsResource.getScopes(), containsInAnyOrder(new ScopeRepresentation("urn:servlet-authz:protected:admin:access"))); + + authorizationSettingsPolicies = authorizationSettings.getPolicies(); + authorizationSettingsPolicy = getAuthorizationPolicy(authorizationSettingsPolicies, "Any Admin Policy"); + assertThat(authorizationSettingsPolicy.getDescription(), is("Defines that adminsitrators can do something")); + assertThat(authorizationSettingsPolicy.getType(), is("role")); + assertThat(authorizationSettingsPolicy.getLogic(), is(Logic.POSITIVE)); + assertThat(authorizationSettingsPolicy.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + assertThat(authorizationSettingsPolicy.getConfig(), aMapWithSize(1)); + assertThat(authorizationSettingsPolicy.getConfig(), hasEntry(equalTo("roles"), equalTo("[{\"id\":\"admin\",\"required\":false}]"))); + + assertThat(authorizationSettings.getScopes(), hasSize(1)); + assertThat(authorizationSettings.getScopes(), containsInAnyOrder( + new ScopeRepresentation("urn:servlet-authz:protected:admin:access") + )); } @Test @Order(12) void shouldUpdateRealmUpdateAuthorization() throws IOException { + // https://github.com/adorsys/keycloak-config-cli/issues/641 + ResourceRepresentation resource = new ResourceRepresentation(); + resource.setName("Tweedl Social Service"); + resource.setType("http://www.example.com/rsrcs/socialstream/140-compatible"); + resource.setIconUri("http://www.example.com/icons/sharesocial.png"); + createRemoteManagedClientResource(REALM_NAME, "auth-moped-client", "changed-special-client-secret", resource); + doImport("12_update_realm__update_authorization.json"); RealmRepresentation realm = keycloakProvider.getInstance().realm(REALM_NAME).partialExport(false, true); @@ -716,7 +768,7 @@ void shouldUpdateRealmUpdateAuthorization() throws IOException { assertThat(authorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); List authorizationSettingsResources = authorizationSettings.getResources(); - assertThat(authorizationSettingsResources, hasSize(4)); + assertThat(authorizationSettingsResources, hasSize(5)); ResourceRepresentation authorizationSettingsResource; authorizationSettingsResource = getAuthorizationSettingsResource(authorizationSettingsResources, "Admin Resource"); @@ -733,7 +785,6 @@ void shouldUpdateRealmUpdateAuthorization() throws IOException { assertThat(authorizationSettingsResource.getAttributes(), hasEntry(is("key"), contains("value"))); assertThat(authorizationSettingsResource.getAttributes(), hasEntry(is("key2"), contains("value2"))); - authorizationSettingsResource = getAuthorizationSettingsResource(authorizationSettingsResources, "Premium Resource"); assertThat(authorizationSettingsResource.getUris(), containsInAnyOrder("/protected/premium/*")); assertThat(authorizationSettingsResource.getType(), is("urn:servlet-authz:protected:resource")); @@ -748,6 +799,12 @@ void shouldUpdateRealmUpdateAuthorization() throws IOException { new ScopeRepresentation("urn:servlet-authz:page:main:actionForUser") )); + authorizationSettingsResource = getAuthorizationSettingsResource(authorizationSettingsResources, "Tweedl Social Service"); + assertThat(authorizationSettingsResource.getUris(), empty()); + assertThat(authorizationSettingsResource.getType(), is("http://www.example.com/rsrcs/socialstream/140-compatible")); + assertThat(authorizationSettingsResource.getIconUri(), is("http://www.example.com/icons/sharesocial.png")); + assertThat(authorizationSettingsResource.getScopes(), empty()); + List authorizationSettingsPolicies = authorizationSettings.getPolicies(); PolicyRepresentation authorizationSettingsPolicy; @@ -844,6 +901,42 @@ void shouldUpdateRealmUpdateAuthorization() throws IOException { assertThat(mopedAuthorizationSettings.getPolicyEnforcementMode(), is(PolicyEnforcementMode.PERMISSIVE)); assertThat(mopedAuthorizationSettings.isAllowRemoteResourceManagement(), is(true)); assertThat(mopedAuthorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + + client = getClientByName(realm, "missing-id-client"); + assertThat(client.getName(), is("missing-id-client")); + assertThat(client.getClientId(), not(emptyString())); + assertThat(client.getDescription(), is("Missing-Id-Client")); + assertThat(client.isEnabled(), is(true)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.isServiceAccountsEnabled(), is(true)); + assertThat(client.getAuthorizationServicesEnabled(), is(true)); + + authorizationSettings = client.getAuthorizationSettings(); + assertThat(authorizationSettings.getPolicyEnforcementMode(), is(PolicyEnforcementMode.ENFORCING)); + assertThat(authorizationSettings.isAllowRemoteResourceManagement(), is(true)); + assertThat(authorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + + authorizationSettingsResources = authorizationSettings.getResources(); + assertThat(authorizationSettingsResources, hasSize(1)); + + authorizationSettingsResource = getAuthorizationSettingsResource(authorizationSettingsResources, "Admin Resource"); + assertThat(authorizationSettingsResource.getUris(), containsInAnyOrder("/protected/admin/*")); + assertThat(authorizationSettingsResource.getType(), is("http://servlet-authz/protected/admin")); + assertThat(authorizationSettingsResource.getScopes(), containsInAnyOrder(new ScopeRepresentation("urn:servlet-authz:protected:user:access"))); + + authorizationSettingsPolicies = authorizationSettings.getPolicies(); + authorizationSettingsPolicy = getAuthorizationPolicy(authorizationSettingsPolicies, "Any Admin Policy"); + assertThat(authorizationSettingsPolicy.getDescription(), is("Defines that adminsitrators can do something")); + assertThat(authorizationSettingsPolicy.getType(), is("role")); + assertThat(authorizationSettingsPolicy.getLogic(), is(Logic.POSITIVE)); + assertThat(authorizationSettingsPolicy.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + assertThat(authorizationSettingsPolicy.getConfig(), aMapWithSize(1)); + assertThat(authorizationSettingsPolicy.getConfig(), hasEntry(equalTo("roles"), equalTo("[{\"id\":\"user\",\"required\":false}]"))); + + assertThat(authorizationSettings.getScopes(), hasSize(1)); + assertThat(authorizationSettings.getScopes(), containsInAnyOrder( + new ScopeRepresentation("urn:servlet-authz:protected:user:access") + )); } @Test @@ -888,7 +981,7 @@ void shouldUpdateRealmRemoveAuthorization() throws IOException { assertThat(authorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); List authorizationSettingsResources = authorizationSettings.getResources(); - assertThat(authorizationSettingsResources, hasSize(3)); + assertThat(authorizationSettingsResources, hasSize(4)); ResourceRepresentation authorizationSettingsResource; authorizationSettingsResource = getAuthorizationSettingsResource(authorizationSettingsResources, "Admin Resource"); @@ -909,6 +1002,12 @@ void shouldUpdateRealmRemoveAuthorization() throws IOException { new ScopeRepresentation("urn:servlet-authz:page:main:actionForAdmin") )); + authorizationSettingsResource = getAuthorizationSettingsResource(authorizationSettingsResources, "Tweedl Social Service"); + assertThat(authorizationSettingsResource.getUris(), empty()); + assertThat(authorizationSettingsResource.getType(), is("http://www.example.com/rsrcs/socialstream/140-compatible")); + assertThat(authorizationSettingsResource.getIconUri(), is("http://www.example.com/icons/sharesocial.png")); + assertThat(authorizationSettingsResource.getScopes(), empty()); + List authorizationSettingsPolicies = authorizationSettings.getPolicies(); PolicyRepresentation authorizationSettingsPolicy; @@ -974,6 +1073,24 @@ void shouldUpdateRealmRemoveAuthorization() throws IOException { ClientRepresentation mopedClient = getClientByName(realm, "moped-client"); assertThat(mopedClient.isServiceAccountsEnabled(), is(false)); assertThat(mopedClient.getAuthorizationSettings(), nullValue()); + + client = getClientByName(realm, "missing-id-client"); + assertThat(client.getName(), is("missing-id-client")); + assertThat(client.getClientId(), not(emptyString())); + assertThat(client.getDescription(), is("Missing-Id-Client")); + assertThat(client.isEnabled(), is(true)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.isServiceAccountsEnabled(), is(true)); + assertThat(client.getAuthorizationServicesEnabled(), is(true)); + + authorizationSettings = client.getAuthorizationSettings(); + assertThat(authorizationSettings.getPolicyEnforcementMode(), is(PolicyEnforcementMode.ENFORCING)); + assertThat(authorizationSettings.isAllowRemoteResourceManagement(), is(true)); + assertThat(authorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + + assertThat(authorizationSettings.getResources(), hasSize(0)); + assertThat(authorizationSettings.getPolicies(), hasSize(0)); + assertThat(authorizationSettings.getScopes(), hasSize(0)); } @Test @@ -1401,6 +1518,459 @@ void shouldCreateRealmWithClientWithAuthenticationFlowBindingOverrides() throws assertThat(client.getAuthenticationFlowBindingOverrides(), allOf(hasEntry("browser", getAuthenticationFlow(realm, "custom flow").getId()))); } + @Test + @Order(41) + void shouldAddAuthzPoliciesForRealmManagement() throws IOException { + doImport("41_update_realm_add_authz_policy_realm-management.json"); + + String REALM_NAME = "realmWithClientsForAuthzGrantedPolicies"; + + RealmRepresentation realm = keycloakProvider.getInstance().realm(REALM_NAME).partialExport(true, true); + assertThat(realm.getRealm(), is(REALM_NAME)); + assertThat(realm.isEnabled(), is(true)); + + ClientRepresentation client; + client = getClientByClientId(realm, "fine-grained-permission-client-id"); + assertThat(client, notNullValue()); + assertThat(client.getId(), is("50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7")); + assertThat(client.getClientId(), is("fine-grained-permission-client-id")); + assertThat(client.isEnabled(), is(true)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.isBearerOnly(), is(false)); + assertThat(client.isConsentRequired(), is(false)); + assertThat(client.isStandardFlowEnabled(), is(true)); + assertThat(client.isImplicitFlowEnabled(), is(false)); + assertThat(client.isDirectAccessGrantsEnabled(), is(true)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.isPublicClient(), is(true)); + assertThat(client.getProtocol(), is("openid-connect")); + + String clientFineGrainedPermissionId = client.getId(); + assertThat( + keycloakProvider.getInstance().realm(REALM_NAME).clients().get(clientFineGrainedPermissionId).getPermissions().isEnabled(), + is(true) + ); + + client = getClientByClientId(realm, "realm-management"); + assertThat(client, notNullValue()); + assertThat(client.getClientId(), is("realm-management")); + assertThat(client.getName(), is("${client_realm-management}")); + assertThat(client.isSurrogateAuthRequired(), is(false)); + assertThat(client.isEnabled(), is(true)); + assertThat(client.isAlwaysDisplayInConsole(), is(false)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.getRedirectUris(), empty()); + assertThat(client.getWebOrigins(), empty()); + assertThat(client.getNotBefore(), is(0)); + assertThat(client.isBearerOnly(), is(true)); + assertThat(client.isConsentRequired(), is(false)); + assertThat(client.isStandardFlowEnabled(), is(true)); + assertThat(client.isImplicitFlowEnabled(), is(false)); + assertThat(client.isDirectAccessGrantsEnabled(), is(false)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.getAuthorizationServicesEnabled(), is(true)); + assertThat(client.isFrontchannelLogout(), is(false)); + assertThat(client.getProtocol(), is("openid-connect")); + assertThat(client.getAttributes(), anEmptyMap()); + assertThat(client.getAuthenticationFlowBindingOverrides(), anEmptyMap()); + assertThat(client.isFullScopeAllowed(), is(false)); + assertThat(client.getNodeReRegistrationTimeout(), is(0)); + assertThat(client.getDefaultClientScopes(), containsInAnyOrder("web-origins", "profile", "roles", "email")); + assertThat(client.getOptionalClientScopes(), containsInAnyOrder("address", "phone", "offline_access", "microprofile-jwt")); + + String[] clientsIds = new String[]{clientFineGrainedPermissionId}; + String[] scopeNames = new String[]{ + "manage", + "view", + "map-roles", + "map-roles-client-scope", + "map-roles-composite", + "configure", + "token-exchange", + "keycloak-config-cli-1" + }; + + ResourceServerRepresentation authorizationSettings = client.getAuthorizationSettings(); + assertThat(authorizationSettings.isAllowRemoteResourceManagement(), is(false)); + assertThat(authorizationSettings.getPolicyEnforcementMode(), is(PolicyEnforcementMode.ENFORCING)); + assertThat(authorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + + List resources = authorizationSettings.getResources(); + assertThat(resources, hasSize(1)); + + ResourceRepresentation resource; + resource = getAuthorizationSettingsResource(resources, "client.resource." + clientFineGrainedPermissionId); + assertThat(resource.getType(), is("Client")); + assertThat(resource.getOwnerManagedAccess(), is(false)); + assertThat(resource.getScopes().stream().map(ScopeRepresentation::getName).collect(Collectors.toList()), containsInAnyOrder(scopeNames)); + + List policies = authorizationSettings.getPolicies(); + + PolicyRepresentation policy; + policy = getAuthorizationPolicy(policies, "clientadmin-policy"); + assertThat(policy.getType(), is("group")); + assertThat(policy.getLogic(), is(Logic.POSITIVE)); + assertThat(policy.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + assertThat(policy.getConfig(), aMapWithSize(1)); + assertThat(policy.getConfig(), hasEntry(equalTo("groups"), equalTo("[{\"path\":\"/client-admin-group\",\"extendChildren\":false}]"))); + + for (String clientsId : clientsIds) { + for (String scope : scopeNames) { + policy = getAuthorizationPolicy(policies, scope + ".permission.client." + clientsId); + assertThat(scope + ".permission.client." + clientsId, policy, notNullValue()); + assertThat(policy.getType(), is("scope")); + assertThat(policy.getLogic(), is(Logic.POSITIVE)); + assertThat(policy.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + assertThat(policy.getConfig(), hasEntry(equalTo("resources"), equalTo("[\"client.resource." + clientsId + "\"]"))); + assertThat(policy.getConfig(), hasEntry(equalTo("scopes"), equalTo("[\"" + scope + "\"]"))); + + if (policy.getName().startsWith("configure.permission.client")) { + assertThat(policy.getConfig(), hasEntry(equalTo("applyPolicies"), equalTo("[\"clientadmin-policy\"]"))); + assertThat(policy.getConfig(), aMapWithSize(3)); + } else { + assertThat(policy.getConfig(), aMapWithSize(2)); + } + } + } + assertThat(policies, hasSize(1 + clientsIds.length * scopeNames.length)); + + assertThat(authorizationSettings.getScopes().stream().map(ScopeRepresentation::getName).collect(Collectors.toList()), containsInAnyOrder(scopeNames)); + } + + @Test + @Order(42) + void shouldUpdateAuthzPoliciesForRealmManagement() throws IOException { + doImport("42_update_realm_update_authz_policy_realm-management.json"); + + String REALM_NAME = "realmWithClientsForAuthzGrantedPolicies"; + + RealmRepresentation realm = keycloakProvider.getInstance().realm(REALM_NAME).partialExport(true, true); + assertThat(realm.getRealm(), is(REALM_NAME)); + assertThat(realm.isEnabled(), is(true)); + + ClientRepresentation client; + client = getClientByClientId(realm, "fine-grained-permission-client-id"); + assertThat(client, notNullValue()); + assertThat(client.getId(), is("50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7")); + assertThat(client.getClientId(), is("fine-grained-permission-client-id")); + assertThat(client.isEnabled(), is(true)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.isBearerOnly(), is(false)); + assertThat(client.isConsentRequired(), is(false)); + assertThat(client.isStandardFlowEnabled(), is(true)); + assertThat(client.isImplicitFlowEnabled(), is(false)); + assertThat(client.isDirectAccessGrantsEnabled(), is(true)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.isPublicClient(), is(true)); + assertThat(client.getProtocol(), is("openid-connect")); + + String clientFineGrainedPermissionId = client.getId(); + assertThat( + keycloakProvider.getInstance().realm(REALM_NAME).clients().get(clientFineGrainedPermissionId).getPermissions().isEnabled(), + is(true) + ); + + + client = getClientByClientId(realm, "z-fine-grained-permission-client-without-id"); + assertThat(client, notNullValue()); + assertThat(client.getClientId(), is("z-fine-grained-permission-client-without-id")); + assertThat(client.isEnabled(), is(true)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.isBearerOnly(), is(false)); + assertThat(client.isConsentRequired(), is(false)); + assertThat(client.isStandardFlowEnabled(), is(true)); + assertThat(client.isImplicitFlowEnabled(), is(false)); + assertThat(client.isDirectAccessGrantsEnabled(), is(true)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.isPublicClient(), is(true)); + assertThat(client.getProtocol(), is("openid-connect")); + + String clientZFineGrainedPermissionWithoutIdId = client.getId(); + assertThat( + keycloakProvider.getInstance().realm(REALM_NAME).clients().get(clientZFineGrainedPermissionWithoutIdId).getPermissions().isEnabled(), + is(true) + ); + + + client = getClientByClientId(realm, "realm-management"); + assertThat(client, notNullValue()); + assertThat(client.getClientId(), is("realm-management")); + assertThat(client.getName(), is("${client_realm-management}")); + assertThat(client.isSurrogateAuthRequired(), is(false)); + assertThat(client.isEnabled(), is(true)); + assertThat(client.isAlwaysDisplayInConsole(), is(false)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.getRedirectUris(), empty()); + assertThat(client.getWebOrigins(), empty()); + assertThat(client.getNotBefore(), is(0)); + assertThat(client.isBearerOnly(), is(true)); + assertThat(client.isConsentRequired(), is(false)); + assertThat(client.isStandardFlowEnabled(), is(true)); + assertThat(client.isImplicitFlowEnabled(), is(false)); + assertThat(client.isDirectAccessGrantsEnabled(), is(false)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.getAuthorizationServicesEnabled(), is(true)); + assertThat(client.isFrontchannelLogout(), is(false)); + assertThat(client.getProtocol(), is("openid-connect")); + assertThat(client.getAttributes(), anEmptyMap()); + assertThat(client.getAuthenticationFlowBindingOverrides(), anEmptyMap()); + assertThat(client.isFullScopeAllowed(), is(false)); + assertThat(client.getNodeReRegistrationTimeout(), is(0)); + assertThat(client.getDefaultClientScopes(), containsInAnyOrder("web-origins", "profile", "roles", "email")); + assertThat(client.getOptionalClientScopes(), containsInAnyOrder("address", "phone", "offline_access", "microprofile-jwt")); + + String[] clientsIds = new String[]{clientFineGrainedPermissionId, clientZFineGrainedPermissionWithoutIdId}; + String[] scopeNames = new String[]{ + "manage", + "view", + "map-roles", + "map-roles-client-scope", + "map-roles-composite", + "configure", + "token-exchange", + "keycloak-config-cli-2" + }; + + ResourceServerRepresentation authorizationSettings = client.getAuthorizationSettings(); + assertThat(authorizationSettings.isAllowRemoteResourceManagement(), is(false)); + assertThat(authorizationSettings.getPolicyEnforcementMode(), is(PolicyEnforcementMode.ENFORCING)); + assertThat(authorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + + List resources = authorizationSettings.getResources(); + assertThat(resources, hasSize(2)); + + ResourceRepresentation resource; + resource = getAuthorizationSettingsResource(resources, "client.resource." + clientFineGrainedPermissionId); + assertThat(resource.getType(), is("Client")); + assertThat(resource.getOwnerManagedAccess(), is(false)); + assertThat(resource.getScopes().stream().map(ScopeRepresentation::getName).collect(Collectors.toList()), containsInAnyOrder(scopeNames)); + + resource = getAuthorizationSettingsResource(resources, "client.resource." + clientZFineGrainedPermissionWithoutIdId); + assertThat(resource.getType(), is("Client")); + assertThat(resource.getOwnerManagedAccess(), is(false)); + assertThat(resource.getScopes().stream().map(ScopeRepresentation::getName).collect(Collectors.toList()), containsInAnyOrder(scopeNames)); + + List policies = authorizationSettings.getPolicies(); + + PolicyRepresentation policy; + policy = getAuthorizationPolicy(policies, "clientadmin-policy"); + assertThat(policy.getType(), is("group")); + assertThat(policy.getLogic(), is(Logic.POSITIVE)); + assertThat(policy.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + assertThat(policy.getConfig(), aMapWithSize(1)); + assertThat(policy.getConfig(), hasEntry(equalTo("groups"), equalTo("[{\"path\":\"/client-admin-group\",\"extendChildren\":false}]"))); + + for (String clientsId : clientsIds) { + for (String scope : scopeNames) { + policy = getAuthorizationPolicy(policies, scope + ".permission.client." + clientsId); + assertThat(scope + ".permission.client." + clientsId, policy, notNullValue()); + assertThat(policy.getType(), is("scope")); + assertThat(policy.getLogic(), is(Logic.POSITIVE)); + assertThat(policy.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + assertThat(policy.getConfig(), hasEntry(equalTo("resources"), equalTo("[\"client.resource." + clientsId + "\"]"))); + assertThat(policy.getConfig(), hasEntry(equalTo("scopes"), equalTo("[\"" + scope + "\"]"))); + + if (policy.getName().startsWith("configure.permission.client")) { + assertThat(policy.getConfig(), hasEntry(equalTo("applyPolicies"), equalTo("[\"clientadmin-policy\"]"))); + assertThat(policy.getConfig(), aMapWithSize(3)); + } else { + assertThat(policy.getConfig(), aMapWithSize(2)); + } + } + } + assertThat(policies, hasSize(1 + clientsIds.length * scopeNames.length)); + + List scopes = authorizationSettings.getScopes().stream().map(ScopeRepresentation::getName).collect(Collectors.toList()); + assertThat(scopes, containsInAnyOrder(scopeNames)); + } + + @Test + @Order(43) + void shouldRemoveClientAndAuthzPoliciesForRealmManagement() throws IOException { + doImport("43_update_realm_remove_client_and_authz_policy_realm-management.json"); + + String REALM_NAME = "realmWithClientsForAuthzGrantedPolicies"; + + RealmRepresentation realm = keycloakProvider.getInstance().realm(REALM_NAME).partialExport(true, true); + assertThat(realm.getRealm(), is(REALM_NAME)); + assertThat(realm.isEnabled(), is(true)); + + ClientRepresentation client; + client = getClientByClientId(realm, "z-fine-grained-permission-client-without-id"); + assertThat(client, notNullValue()); + assertThat(client.getClientId(), is("z-fine-grained-permission-client-without-id")); + assertThat(client.isEnabled(), is(true)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.isBearerOnly(), is(false)); + assertThat(client.isConsentRequired(), is(false)); + assertThat(client.isStandardFlowEnabled(), is(true)); + assertThat(client.isImplicitFlowEnabled(), is(false)); + assertThat(client.isDirectAccessGrantsEnabled(), is(true)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.isPublicClient(), is(true)); + assertThat(client.getProtocol(), is("openid-connect")); + + String clientZFineGrainedPermissionWithoutIdId = client.getId(); + assertThat( + keycloakProvider.getInstance().realm(REALM_NAME).clients().get(clientZFineGrainedPermissionWithoutIdId).getPermissions().isEnabled(), + is(true) + ); + + + client = getClientByClientId(realm, "realm-management"); + assertThat(client, notNullValue()); + assertThat(client.getClientId(), is("realm-management")); + assertThat(client.getName(), is("${client_realm-management}")); + assertThat(client.isSurrogateAuthRequired(), is(false)); + assertThat(client.isEnabled(), is(true)); + assertThat(client.isAlwaysDisplayInConsole(), is(false)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.getRedirectUris(), empty()); + assertThat(client.getWebOrigins(), empty()); + assertThat(client.getNotBefore(), is(0)); + assertThat(client.isBearerOnly(), is(true)); + assertThat(client.isConsentRequired(), is(false)); + assertThat(client.isStandardFlowEnabled(), is(true)); + assertThat(client.isImplicitFlowEnabled(), is(false)); + assertThat(client.isDirectAccessGrantsEnabled(), is(false)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.getAuthorizationServicesEnabled(), is(true)); + assertThat(client.isFrontchannelLogout(), is(false)); + assertThat(client.getProtocol(), is("openid-connect")); + assertThat(client.getAttributes(), anEmptyMap()); + assertThat(client.getAuthenticationFlowBindingOverrides(), anEmptyMap()); + assertThat(client.isFullScopeAllowed(), is(false)); + assertThat(client.getNodeReRegistrationTimeout(), is(0)); + assertThat(client.getDefaultClientScopes(), containsInAnyOrder("web-origins", "profile", "roles", "email")); + assertThat(client.getOptionalClientScopes(), containsInAnyOrder("address", "phone", "offline_access", "microprofile-jwt")); + + String[] clientsIds = new String[]{clientZFineGrainedPermissionWithoutIdId}; + String[] scopeNames = new String[]{ + "manage", + "view", + "map-roles", + "map-roles-client-scope", + "map-roles-composite", + "configure", + "token-exchange", + }; + + ResourceServerRepresentation authorizationSettings = client.getAuthorizationSettings(); + assertThat(authorizationSettings.isAllowRemoteResourceManagement(), is(false)); + assertThat(authorizationSettings.getPolicyEnforcementMode(), is(PolicyEnforcementMode.ENFORCING)); + assertThat(authorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + + List resources = authorizationSettings.getResources(); + assertThat(resources, hasSize(1)); + + ResourceRepresentation resource; + resource = getAuthorizationSettingsResource(resources, "client.resource." + clientZFineGrainedPermissionWithoutIdId); + assertThat(resource.getType(), is("Client")); + assertThat(resource.getOwnerManagedAccess(), is(false)); + assertThat(resource.getScopes().stream().map(ScopeRepresentation::getName).collect(Collectors.toList()), containsInAnyOrder(scopeNames)); + + List policies = authorizationSettings.getPolicies(); + + PolicyRepresentation policy; + + for (String clientsId : clientsIds) { + for (String scope : scopeNames) { + policy = getAuthorizationPolicy(policies, scope + ".permission.client." + clientsId); + assertThat(scope + ".permission.client." + clientsId, policy, notNullValue()); + assertThat(policy.getType(), is("scope")); + assertThat(policy.getLogic(), is(Logic.POSITIVE)); + assertThat(policy.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + assertThat(policy.getConfig(), hasEntry(equalTo("resources"), equalTo("[\"client.resource." + clientsId + "\"]"))); + assertThat(policy.getConfig(), hasEntry(equalTo("scopes"), equalTo("[\"" + scope + "\"]"))); + assertThat(policy.getConfig(), aMapWithSize(2)); + } + } + + assertThat(policies, hasSize(clientsIds.length * scopeNames.length)); + + List scopes = authorizationSettings.getScopes().stream().map(ScopeRepresentation::getName).collect(Collectors.toList()); + assertThat(scopes, containsInAnyOrder(scopeNames)); + } + + @Test + @Order(44) + void shouldRemoveAuthzPoliciesForRealmManagement() throws IOException { + doImport("44_update_realm_remove_authz_policy_realm-management.json"); + + String REALM_NAME = "realmWithClientsForAuthzGrantedPolicies"; + + RealmRepresentation realm = keycloakProvider.getInstance().realm(REALM_NAME).partialExport(true, true); + assertThat(realm.getRealm(), is(REALM_NAME)); + assertThat(realm.isEnabled(), is(true)); + + ClientRepresentation client; + client = getClientByClientId(realm, "z-fine-grained-permission-client-without-id"); + assertThat(client, notNullValue()); + assertThat(client.getClientId(), is("z-fine-grained-permission-client-without-id")); + assertThat(client.isEnabled(), is(true)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.isBearerOnly(), is(false)); + assertThat(client.isConsentRequired(), is(false)); + assertThat(client.isStandardFlowEnabled(), is(true)); + assertThat(client.isImplicitFlowEnabled(), is(false)); + assertThat(client.isDirectAccessGrantsEnabled(), is(true)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.isPublicClient(), is(true)); + assertThat(client.getProtocol(), is("openid-connect")); + + String clientZFineGrainedPermissionWithoutIdId = client.getId(); + assertThat( + keycloakProvider.getInstance().realm(REALM_NAME).clients().get(clientZFineGrainedPermissionWithoutIdId).getPermissions().isEnabled(), + is(false) + ); + + + client = getClientByClientId(realm, "realm-management"); + assertThat(client, notNullValue()); + assertThat(client.getClientId(), is("realm-management")); + assertThat(client.getName(), is("${client_realm-management}")); + assertThat(client.isSurrogateAuthRequired(), is(false)); + assertThat(client.isEnabled(), is(true)); + assertThat(client.isAlwaysDisplayInConsole(), is(false)); + assertThat(client.getClientAuthenticatorType(), is("client-secret")); + assertThat(client.getRedirectUris(), empty()); + assertThat(client.getWebOrigins(), empty()); + assertThat(client.getNotBefore(), is(0)); + assertThat(client.isBearerOnly(), is(true)); + assertThat(client.isConsentRequired(), is(false)); + assertThat(client.isStandardFlowEnabled(), is(true)); + assertThat(client.isImplicitFlowEnabled(), is(false)); + assertThat(client.isDirectAccessGrantsEnabled(), is(false)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.isServiceAccountsEnabled(), is(false)); + assertThat(client.getAuthorizationServicesEnabled(), is(true)); + assertThat(client.isFrontchannelLogout(), is(false)); + assertThat(client.getProtocol(), is("openid-connect")); + assertThat(client.getAttributes(), anEmptyMap()); + assertThat(client.getAuthenticationFlowBindingOverrides(), anEmptyMap()); + assertThat(client.isFullScopeAllowed(), is(false)); + assertThat(client.getNodeReRegistrationTimeout(), is(0)); + assertThat(client.getDefaultClientScopes(), containsInAnyOrder("web-origins", "profile", "roles", "email")); + assertThat(client.getOptionalClientScopes(), containsInAnyOrder("address", "phone", "offline_access", "microprofile-jwt")); + + ResourceServerRepresentation authorizationSettings = client.getAuthorizationSettings(); + assertThat(authorizationSettings.isAllowRemoteResourceManagement(), is(false)); + assertThat(authorizationSettings.getPolicyEnforcementMode(), is(PolicyEnforcementMode.ENFORCING)); + assertThat(authorizationSettings.getDecisionStrategy(), is(DecisionStrategy.UNANIMOUS)); + + List resources = authorizationSettings.getResources(); + assertThat(resources, empty()); + + List policies = authorizationSettings.getPolicies(); + assertThat(policies, empty()); + + List scopes = authorizationSettings.getScopes().stream().map(ScopeRepresentation::getName).collect(Collectors.toList()); + assertThat(scopes, empty()); + } + @Test @Order(71) void shouldAddClientWithAuthenticationFlowBindingOverrides() throws IOException { @@ -1686,4 +2256,15 @@ private Map getRealmState(String realmName) { .filter(e -> e.getKey().startsWith(ImportConfigProperties.REALM_STATE_ATTRIBUTE_COMMON_PREFIX)) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } + + private void createRemoteManagedClientResource(String realm, String clientId, String clientSecret, ResourceRepresentation resource) { + Configuration configuration = new Configuration(); + configuration.setAuthServerUrl(properties.getUrl().toString()); + configuration.setRealm(realm); + configuration.setResource(clientId); + configuration.setCredentials(Collections.singletonMap("secret", clientSecret)); + AuthzClient authzClient = AuthzClient.create(configuration); + + authzClient.protection().resource().create(resource); + } } diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportGroupsIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportGroupsIT.java index 7712656ed..2db07e3c5 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportGroupsIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportGroupsIT.java @@ -30,7 +30,6 @@ import org.keycloak.representations.idm.RealmRepresentation; import java.io.IOException; -import java.text.MessageFormat; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -1725,9 +1724,7 @@ private GroupRepresentation loadGroup(String groupPath) { .stream() .filter(g -> Objects.equals(groupPath, g.getPath())) .findFirst() - .orElseThrow(() -> new KeycloakRepositoryException( - MessageFormat.format("Can't find group '{0}'.", groupPath) - )); + .orElseThrow(() -> new KeycloakRepositoryException("Can't find group '%s'.", groupPath)); return groupsResource .group(groupRepresentation.getId()) diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportManagedNoDeleteIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportManagedNoDeleteIT.java index 53eef5d69..369b3bb4e 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportManagedNoDeleteIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportManagedNoDeleteIT.java @@ -24,13 +24,11 @@ import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.keycloak.representations.idm.*; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.springframework.test.context.TestPropertySource; import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; @@ -49,6 +47,7 @@ "import.managed.identity-provider-mapper=no-delete", "import.managed.role=no-delete", "import.managed.client=no-delete", + "import.managed.client-authorization-resources=no-delete", }) @SuppressWarnings({"java:S5961", "java:S5976"}) class ImportManagedNoDeleteIT extends AbstractImportTest { @@ -145,5 +144,16 @@ private void assertRealm() { .filter((identityProviderMapper) -> identityProviderMapperList.contains(identityProviderMapper.getName())) .collect(Collectors.toList()); assertThat(createdIdentityProviderMappers, hasSize(2)); + + + List clientResourcesList = Arrays.asList("Admin Resource", "Protected Resource"); + List createdClientResourcesList = createdRealm + .getClients() + .stream().filter(client -> Objects.equals(client.getName(), "moped-client")).findAny() + .orElseThrow(() -> new RuntimeException("Cannot find client 'moped-client'")) + .getAuthorizationSettings().getResources() + .stream().filter(resource -> clientResourcesList.contains(resource.getName())) + .collect(Collectors.toList()); + assertThat(createdClientResourcesList, hasSize(2)); } } diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportRolesIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportRolesIT.java index c16a1de21..15b9964eb 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportRolesIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportRolesIT.java @@ -28,7 +28,6 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; -import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.springframework.beans.factory.annotation.Autowired; @@ -45,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; +@SuppressWarnings("java:S5961") class ImportRolesIT extends AbstractImportTest { private static final String REALM_NAME = "realmWithRoles"; diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportUserFederationIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportUserFederationIT.java index d54a8f7f6..86d5cfbb6 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportUserFederationIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportUserFederationIT.java @@ -54,11 +54,6 @@ class ImportUserFederationIT extends AbstractImportTest { private static final String REALM_NAME = "realmWithLdap"; private static final String REALM_NAME_WITHOUT_FEDERATION = "realmWithoutLdap"; - static { - System.setProperty("JUNIT_LDAP_HOST", "host.docker.internal"); - System.setProperty("JUNIT_LDAP_PORT", String.valueOf(ldapExtension.getPort())); - } - public ImportUserFederationIT() { this.resourcePath = "import-files/user-federation"; } diff --git a/src/test/java/de/adorsys/keycloak/config/util/ArrayUtilTest.java b/src/test/java/de/adorsys/keycloak/config/util/ArrayUtilTest.java deleted file mode 100644 index 6856d090e..000000000 --- a/src/test/java/de/adorsys/keycloak/config/util/ArrayUtilTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/*- - * ---license-start - * keycloak-config-cli - * --- - * Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com - * --- - * 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. - * ---license-end - */ - -package de.adorsys.keycloak.config.util; - -import de.adorsys.keycloak.config.extensions.GithubActionsExtension; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@ExtendWith(GithubActionsExtension.class) -class ArrayUtilTest { - @Test - void shouldThrowOnNew() { - assertThrows(IllegalStateException.class, ArrayUtil::new); - } - - @Test - void concatStringArrays() { - //noinspection RedundantArrayCreation - String[] array = ArrayUtil.concat(new String[]{"A", "B"}, new String[]{"C", "D"}); - - assertThat(array, is(new String[]{"A", "B", "C", "D"})); - } - - @Test - void concatStrings() { - String[] array = ArrayUtil.concat(new String[]{"A", "B"}, "C", "D"); - - assertThat(array, is(new String[]{"A", "B", "C", "D"})); - } - - @Test - void concatIntegerArrays() { - //noinspection RedundantArrayCreation - Integer[] array = ArrayUtil.concat(new Integer[]{1, 2}, new Integer[]{3, 4}); - - assertThat(array, is(new Integer[]{1, 2, 3, 4})); - } - - @Test - void concatIntegers() { - Integer[] array = ArrayUtil.concat(new Integer[]{1, 2}, 3, 4); - - assertThat(array, is(new Integer[]{1, 2, 3, 4})); - } -} diff --git a/src/test/resources/import-files/clients/11_update_realm__add_authorization.json b/src/test/resources/import-files/clients/11_update_realm__add_authorization.json index 09161b2bc..4c85f419c 100644 --- a/src/test/resources/import-files/clients/11_update_realm__add_authorization.json +++ b/src/test/resources/import-files/clients/11_update_realm__add_authorization.json @@ -36,7 +36,7 @@ "serviceAccountsEnabled": true, "authorizationServicesEnabled": true, "authorizationSettings": { - "allowRemoteResourceManagement": false, + "allowRemoteResourceManagement": true, "policyEnforcementMode": "ENFORCING", "decisionStrategy": "UNANIMOUS", "resources": [ @@ -227,6 +227,55 @@ "webOrigins": [ "*" ] + }, + { + "name": "missing-id-client", + "description": "Missing-Id-Client", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-other-missing-id-client-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ], + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "authorizationSettings": { + "allowRemoteResourceManagement": false, + "policyEnforcementMode": "ENFORCING", + "decisionStrategy": "UNANIMOUS", + "resources": [ + { + "name": "Admin Resource", + "uri": "/protected/admin/*", + "type": "http://servlet-authz/protected/admin", + "scopes": [ + { + "name": "urn:servlet-authz:protected:admin:access" + } + ] + } + ], + "policies": [ + { + "name": "Any Admin Policy", + "description": "Defines that adminsitrators can do something", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"admin\"}]" + } + } + ], + "scopes": [ + { + "name": "urn:servlet-authz:protected:admin:access" + } + ] + } } ], "roles": { @@ -245,7 +294,16 @@ { "username": "service-account-auth-moped-client", "enabled": true, - "serviceAccountClientId": "auth-moped-client" + "serviceAccountClientId": "auth-moped-client", + "realmRoles": [ + "uma_authorization", + "offline_access" + ], + "clientRoles": { + "auth-moped-client": [ + "uma_protection" + ] + } } ] } diff --git a/src/test/resources/import-files/clients/12_update_realm__update_authorization.json b/src/test/resources/import-files/clients/12_update_realm__update_authorization.json index acd90911b..4546803e8 100644 --- a/src/test/resources/import-files/clients/12_update_realm__update_authorization.json +++ b/src/test/resources/import-files/clients/12_update_realm__update_authorization.json @@ -277,6 +277,55 @@ "webOrigins": [ "*" ] + }, + { + "name": "missing-id-client", + "description": "Missing-Id-Client", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-other-missing-id-client-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ], + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "decisionStrategy": "UNANIMOUS", + "resources": [ + { + "name": "Admin Resource", + "uri": "/protected/admin/*", + "type": "http://servlet-authz/protected/admin", + "scopes": [ + { + "name": "urn:servlet-authz:protected:user:access" + } + ] + } + ], + "policies": [ + { + "name": "Any Admin Policy", + "description": "Defines that adminsitrators can do something", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"user\"}]" + } + } + ], + "scopes": [ + { + "name": "urn:servlet-authz:protected:user:access" + } + ] + } } ], "roles": { diff --git a/src/test/resources/import-files/clients/13_update_realm__remove_authorization.json b/src/test/resources/import-files/clients/13_update_realm__remove_authorization.json index c861771f0..e47f2549a 100644 --- a/src/test/resources/import-files/clients/13_update_realm__remove_authorization.json +++ b/src/test/resources/import-files/clients/13_update_realm__remove_authorization.json @@ -209,6 +209,26 @@ "webOrigins": [ "*" ] + }, + { + "name": "missing-id-client", + "description": "Missing-Id-Client", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-other-missing-id-client-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ], + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "decisionStrategy": "UNANIMOUS" + } } ], "roles": { diff --git a/src/test/resources/import-files/clients/41_update_realm_add_authz_policy_realm-management.json b/src/test/resources/import-files/clients/41_update_realm_add_authz_policy_realm-management.json new file mode 100644 index 000000000..e2422f322 --- /dev/null +++ b/src/test/resources/import-files/clients/41_update_realm_add_authz_policy_realm-management.json @@ -0,0 +1,218 @@ +{ + "realm": "realmWithClientsForAuthzGrantedPolicies", + "enabled": true, + "groups": [ + { + "name": "client-admin-group" + } + ], + "clients": [ + { + "id": "50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "clientId": "fine-grained-permission-client-id", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "protocol": "openid-connect" + }, + { + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": false, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "Client", + "ownerManagedAccess": false, + "scopes": [ + { + "name": "view" + }, + { + "name": "map-roles-client-scope" + }, + { + "name": "configure" + }, + { + "name": "map-roles" + }, + { + "name": "manage" + }, + { + "name": "token-exchange" + }, + { + "name": "map-roles-composite" + }, + { + "name": "keycloak-config-cli-1" + } + ] + } + ], + "policies": [ + { + "name": "clientadmin-policy", + "type": "group", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "groups": "[{\"path\":\"/client-admin-group\",\"extendChildren\":false}]" + } + }, + { + "name": "manage.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"manage\"]" + } + }, + { + "name": "configure.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"configure\"]", + "applyPolicies": "[\"clientadmin-policy\"]" + } + }, + { + "name": "view.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"view\"]" + } + }, + { + "name": "map-roles.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"map-roles\"]" + } + }, + { + "name": "map-roles-client-scope.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"map-roles-client-scope\"]" + } + }, + { + "name": "map-roles-composite.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"map-roles-composite\"]" + } + }, + { + "name": "token-exchange.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"token-exchange\"]" + } + }, + { + "name": "keycloak-config-cli-1.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"keycloak-config-cli-1\"]" + } + } + ], + "scopes": [ + { + "name": "manage" + }, + { + "name": "view" + }, + { + "name": "map-roles" + }, + { + "name": "map-roles-client-scope" + }, + { + "name": "map-roles-composite" + }, + { + "name": "configure" + }, + { + "name": "token-exchange" + }, + { + "name": "keycloak-config-cli-1" + } + ], + "decisionStrategy": "UNANIMOUS" + } + } + ] +} diff --git a/src/test/resources/import-files/clients/42_update_realm_update_authz_policy_realm-management.json b/src/test/resources/import-files/clients/42_update_realm_update_authz_policy_realm-management.json new file mode 100644 index 000000000..5ed98b67c --- /dev/null +++ b/src/test/resources/import-files/clients/42_update_realm_update_authz_policy_realm-management.json @@ -0,0 +1,343 @@ +{ + "realm": "realmWithClientsForAuthzGrantedPolicies", + "enabled": true, + "groups": [ + { + "name": "client-admin-group" + } + ], + "clients": [ + { + "id": "50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "clientId": "fine-grained-permission-client-id", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "protocol": "openid-connect" + }, + { + "clientId": "z-fine-grained-permission-client-without-id", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "protocol": "openid-connect" + }, + { + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": false, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "client.resource.$z-fine-grained-permission-client-without-id", + "type": "Client", + "ownerManagedAccess": false, + "scopes": [ + { + "name": "view" + }, + { + "name": "map-roles-client-scope" + }, + { + "name": "configure" + }, + { + "name": "map-roles" + }, + { + "name": "manage" + }, + { + "name": "token-exchange" + }, + { + "name": "map-roles-composite" + }, + { + "name": "keycloak-config-cli-2" + } + ] + }, + { + "name": "client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "Client", + "ownerManagedAccess": false, + "scopes": [ + { + "name": "view" + }, + { + "name": "map-roles-client-scope" + }, + { + "name": "configure" + }, + { + "name": "map-roles" + }, + { + "name": "manage" + }, + { + "name": "token-exchange" + }, + { + "name": "map-roles-composite" + }, + { + "name": "keycloak-config-cli-2" + } + ] + } + ], + "policies": [ + { + "name": "clientadmin-policy", + "type": "group", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "groups": "[{\"path\":\"/client-admin-group\",\"extendChildren\":false}]" + } + }, + { + "name": "manage.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"manage\"]" + } + }, + { + "name": "configure.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"configure\"]", + "applyPolicies": "[\"clientadmin-policy\"]" + } + }, + { + "name": "view.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"view\"]" + } + }, + { + "name": "map-roles.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"map-roles\"]" + } + }, + { + "name": "map-roles-client-scope.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"map-roles-client-scope\"]" + } + }, + { + "name": "map-roles-composite.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"map-roles-composite\"]" + } + }, + { + "name": "token-exchange.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"token-exchange\"]" + } + }, + { + "name": "keycloak-config-cli-2.permission.client.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.50eadf70-6e80-4f1d-ba0d-85cafa3c1dc7\"]", + "scopes": "[\"keycloak-config-cli-2\"]" + } + }, + { + "name": "manage.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"manage\"]" + } + }, + { + "name": "configure.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"configure\"]", + "applyPolicies": "[\"clientadmin-policy\"]" + } + }, + { + "name": "view.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"view\"]" + } + }, + { + "name": "map-roles.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"map-roles\"]" + } + }, + { + "name": "map-roles-client-scope.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"map-roles-client-scope\"]" + } + }, + { + "name": "map-roles-composite.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"map-roles-composite\"]" + } + }, + { + "name": "token-exchange.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"token-exchange\"]" + } + }, + { + "name": "keycloak-config-cli-2.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"keycloak-config-cli-2\"]" + } + } + ], + "scopes": [ + { + "name": "manage" + }, + { + "name": "view" + }, + { + "name": "map-roles" + }, + { + "name": "map-roles-client-scope" + }, + { + "name": "map-roles-composite" + }, + { + "name": "configure" + }, + { + "name": "token-exchange" + }, + { + "name": "keycloak-config-cli-2" + } + ], + "decisionStrategy": "UNANIMOUS" + } + } + ] +} diff --git a/src/test/resources/import-files/clients/43_update_realm_remove_client_and_authz_policy_realm-management.json b/src/test/resources/import-files/clients/43_update_realm_remove_client_and_authz_policy_realm-management.json new file mode 100644 index 000000000..35a527f46 --- /dev/null +++ b/src/test/resources/import-files/clients/43_update_realm_remove_client_and_authz_policy_realm-management.json @@ -0,0 +1,191 @@ +{ + "realm": "realmWithClientsForAuthzGrantedPolicies", + "enabled": true, + "groups": [ + { + "name": "client-admin-group" + } + ], + "clients": [ + { + "clientId": "z-fine-grained-permission-client-without-id", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "protocol": "openid-connect" + }, + { + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": false, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "client.resource.$z-fine-grained-permission-client-without-id", + "type": "Client", + "ownerManagedAccess": false, + "scopes": [ + { + "name": "view" + }, + { + "name": "map-roles-client-scope" + }, + { + "name": "configure" + }, + { + "name": "map-roles" + }, + { + "name": "manage" + }, + { + "name": "token-exchange" + }, + { + "name": "map-roles-composite" + } + ] + } + ], + "policies": [ + { + "name": "manage.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"manage\"]" + } + }, + { + "name": "configure.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"configure\"]" + } + }, + { + "name": "view.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"view\"]" + } + }, + { + "name": "map-roles.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"map-roles\"]" + } + }, + { + "name": "map-roles-client-scope.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"map-roles-client-scope\"]" + } + }, + { + "name": "map-roles-composite.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"map-roles-composite\"]" + } + }, + { + "name": "token-exchange.permission.client.$z-fine-grained-permission-client-without-id", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"client.resource.$z-fine-grained-permission-client-without-id\"]", + "scopes": "[\"token-exchange\"]" + } + } + ], + "scopes": [ + { + "name": "manage" + }, + { + "name": "view" + }, + { + "name": "map-roles" + }, + { + "name": "map-roles-client-scope" + }, + { + "name": "map-roles-composite" + }, + { + "name": "configure" + }, + { + "name": "token-exchange" + } + ], + "decisionStrategy": "UNANIMOUS" + } + } + ] +} diff --git a/src/test/resources/import-files/clients/44_update_realm_remove_authz_policy_realm-management.json b/src/test/resources/import-files/clients/44_update_realm_remove_authz_policy_realm-management.json new file mode 100644 index 000000000..eb2701d20 --- /dev/null +++ b/src/test/resources/import-files/clients/44_update_realm_remove_authz_policy_realm-management.json @@ -0,0 +1,69 @@ +{ + "realm": "realmWithClientsForAuthzGrantedPolicies", + "enabled": true, + "groups": [ + { + "name": "client-admin-group" + } + ], + "clients": [ + { + "clientId": "z-fine-grained-permission-client-without-id", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "protocol": "openid-connect" + }, + { + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": false, + "policyEnforcementMode": "ENFORCING", + "resources": [], + "policies": [], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + } + ] +} diff --git a/src/test/resources/import-files/import-sorted-hidden-files/.3_update_realm.json b/src/test/resources/import-files/import-sorted-hidden-files/.3_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-sorted-hidden-files/.3_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-sorted-hidden-files/.7_update_realm.json b/src/test/resources/import-files/import-sorted-hidden-files/.7_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-sorted-hidden-files/.7_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-sorted-hidden-files/0_create_realm.json b/src/test/resources/import-files/import-sorted-hidden-files/0_create_realm.json new file mode 100644 index 000000000..95ebd5451 --- /dev/null +++ b/src/test/resources/import-files/import-sorted-hidden-files/0_create_realm.json @@ -0,0 +1,4 @@ +{ + "enabled": true, + "realm": "realm-sorted-import" +} diff --git a/src/test/resources/import-files/import-sorted-hidden-files/1_update_realm.json b/src/test/resources/import-files/import-sorted-hidden-files/1_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-sorted-hidden-files/1_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-sorted-hidden-files/2_update_realm.json b/src/test/resources/import-files/import-sorted-hidden-files/2_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-sorted-hidden-files/2_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-sorted-hidden-files/4_update_realm.json b/src/test/resources/import-files/import-sorted-hidden-files/4_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-sorted-hidden-files/4_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-sorted-hidden-files/5_update_realm.json b/src/test/resources/import-files/import-sorted-hidden-files/5_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-sorted-hidden-files/5_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-sorted-hidden-files/6_update_realm.json b/src/test/resources/import-files/import-sorted-hidden-files/6_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-sorted-hidden-files/6_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-sorted-hidden-files/8_update_realm.json b/src/test/resources/import-files/import-sorted-hidden-files/8_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-sorted-hidden-files/8_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-sorted-hidden-files/9_update_realm.json b/src/test/resources/import-files/import-sorted-hidden-files/9_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-sorted-hidden-files/9_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-wildcard/.1_update_realm.json b/src/test/resources/import-files/import-wildcard/.1_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/.1_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-wildcard/0_create_realm.json b/src/test/resources/import-files/import-wildcard/0_create_realm.json new file mode 100644 index 000000000..95ebd5451 --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/0_create_realm.json @@ -0,0 +1,4 @@ +{ + "enabled": true, + "realm": "realm-sorted-import" +} diff --git a/src/test/resources/import-files/import-wildcard/another/directory/1_update_realm.json b/src/test/resources/import-files/import-wildcard/another/directory/1_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/another/directory/1_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-wildcard/another/directory/2_update_realm.json b/src/test/resources/import-files/import-wildcard/another/directory/2_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/another/directory/2_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-wildcard/another/directory/3_update_realm.json b/src/test/resources/import-files/import-wildcard/another/directory/3_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/another/directory/3_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-wildcard/another/directory/junk.txt b/src/test/resources/import-files/import-wildcard/another/directory/junk.txt new file mode 100644 index 000000000..46de5e4cb --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/another/directory/junk.txt @@ -0,0 +1 @@ +I'm not a json file diff --git a/src/test/resources/import-files/import-wildcard/sub/.hidden/1_update_realm.json b/src/test/resources/import-files/import-wildcard/sub/.hidden/1_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/sub/.hidden/1_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-wildcard/sub/directory/4_update_realm.json b/src/test/resources/import-files/import-wildcard/sub/directory/4_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/sub/directory/4_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-wildcard/sub/directory/5_update_realm.json b/src/test/resources/import-files/import-wildcard/sub/directory/5_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/sub/directory/5_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-wildcard/sub/directory/6_update_realm.json b/src/test/resources/import-files/import-wildcard/sub/directory/6_update_realm.json new file mode 100644 index 000000000..b073ff761 --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/sub/directory/6_update_realm.json @@ -0,0 +1,5 @@ +{ + "enabled": true, + "realm": "realm-sorted-import", + "loginTheme": "moped" +} diff --git a/src/test/resources/import-files/import-wildcard/sub/directory/7_update_realm.yaml b/src/test/resources/import-files/import-wildcard/sub/directory/7_update_realm.yaml new file mode 100644 index 000000000..e634bd14c --- /dev/null +++ b/src/test/resources/import-files/import-wildcard/sub/directory/7_update_realm.yaml @@ -0,0 +1,3 @@ +enabled: true +realm: realm-sorted-import +loginTheme: moped diff --git a/src/test/resources/import-files/managed-no-delete/0_create_realm.json b/src/test/resources/import-files/managed-no-delete/0_create_realm.json index d85b27a0c..3b41bf13b 100644 --- a/src/test/resources/import-files/managed-no-delete/0_create_realm.json +++ b/src/test/resources/import-files/managed-no-delete/0_create_realm.json @@ -458,7 +458,68 @@ ], "webOrigins": [ "*" - ] + ], + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "authorizationSettings": { + "allowRemoteResourceManagement": false, + "policyEnforcementMode": "ENFORCING", + "decisionStrategy": "UNANIMOUS", + "resources": [ + { + "name": "Admin Resource", + "uri": "/protected/admin/*", + "type": "http://servlet-authz/protected/admin", + "scopes": [ + { + "name": "urn:servlet-authz:protected:admin:access" + } + ] + }, + { + "name": "Protected Resource", + "uris": [ + "/*" + ], + "type": "http://servlet-authz/protected/resource", + "scopes": [ + { + "name": "urn:servlet-authz:protected:resource:access" + } + ], + "attributes": { + "key": "value" + } + }, + { + "name": "Main Page", + "type": "urn:servlet-authz:protected:resource", + "scopes": [ + { + "name": "urn:servlet-authz:page:main:actionForAdmin" + }, + { + "name": "urn:servlet-authz:page:main:actionForUser" + } + ] + } + ], + "policies": [], + "scopes": [ + { + "name": "urn:servlet-authz:protected:admin:access" + }, + { + "name": "urn:servlet-authz:protected:resource:access" + }, + { + "name": "urn:servlet-authz:page:main:actionForAdmin" + }, + { + "name": "urn:servlet-authz:page:main:actionForUser" + } + ] + } }, { "clientId": "other-moped-client", diff --git a/src/test/resources/import-files/managed-no-delete/1_update-realm_not-delete-one.json b/src/test/resources/import-files/managed-no-delete/1_update-realm_not-delete-one.json index 207a40083..4a6f0cb99 100644 --- a/src/test/resources/import-files/managed-no-delete/1_update-realm_not-delete-one.json +++ b/src/test/resources/import-files/managed-no-delete/1_update-realm_not-delete-one.json @@ -304,7 +304,68 @@ ], "webOrigins": [ "*" - ] + ], + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "authorizationSettings": { + "allowRemoteResourceManagement": false, + "policyEnforcementMode": "ENFORCING", + "decisionStrategy": "UNANIMOUS", + "resources": [ + { + "name": "Admin Resource", + "uri": "/protected/admin/*", + "type": "http://servlet-authz/protected/admin", + "scopes": [ + { + "name": "urn:servlet-authz:protected:admin:access" + } + ] + }, + { + "name": "Protected Resource", + "uris": [ + "/*" + ], + "type": "http://servlet-authz/protected/resource", + "scopes": [ + { + "name": "urn:servlet-authz:protected:resource:access" + } + ], + "attributes": { + "key": "value" + } + }, + { + "name": "Main Page", + "type": "urn:servlet-authz:protected:resource", + "scopes": [ + { + "name": "urn:servlet-authz:page:main:actionForAdmin" + }, + { + "name": "urn:servlet-authz:page:main:actionForUser" + } + ] + } + ], + "policies": [], + "scopes": [ + { + "name": "urn:servlet-authz:protected:admin:access" + }, + { + "name": "urn:servlet-authz:protected:resource:access" + }, + { + "name": "urn:servlet-authz:page:main:actionForAdmin" + }, + { + "name": "urn:servlet-authz:page:main:actionForUser" + } + ] + } } ] } diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 98c9b27c5..a7fcc9600 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -8,4 +8,5 @@ +