From 116eab57b7fa5b4c4ad5fd3525822078e278424b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Marchand?= Date: Tue, 23 Jul 2024 11:52:24 +0200 Subject: [PATCH 1/3] chore: Upgrade Kotlin version and Keycloak version --- custom-user-federation-example/build.gradle | 63 ++++++++----------- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../keycloak/CustomIdentityProviderFactory.kt | 2 +- .../keycloak/CustomUserStorageProvider.kt | 35 +++++------ .../CustomUserStorageProviderFactory.kt | 1 + .../META-INF/jboss-deployment-structure.xml | 16 ----- 6 files changed, 45 insertions(+), 74 deletions(-) delete mode 100644 custom-user-federation-example/src/main/resources/META-INF/jboss-deployment-structure.xml diff --git a/custom-user-federation-example/build.gradle b/custom-user-federation-example/build.gradle index 140e7f547..be5819b05 100644 --- a/custom-user-federation-example/build.gradle +++ b/custom-user-federation-example/build.gradle @@ -1,50 +1,37 @@ -buildscript { - ext.kotlinVersion = '1.3.31' - ext.keycloakVersion = '19.0.2' - ext.shadowJarVersion = '4.0.2' - - repositories { - mavenCentral() - jcenter() - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" - classpath "com.github.jengelman.gradle.plugins:shadow:${shadowJarVersion}" - } +buildscript { + ext.kotlinVersion = '2.0.0' + ext.keycloakVersion = '25.0.2' + ext.shadowJarVersion = '8.1.1' + + repositories { + mavenCentral() + gradlePluginPortal() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" + classpath "com.github.johnrengelman:shadow:${shadowJarVersion}" + } } apply { - plugin 'java' - plugin 'kotlin' + plugin "java" + plugin "org.jetbrains.kotlin.jvm" plugin 'com.github.johnrengelman.shadow' } -shadowJar { - classifier = null -} - -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}" - compile "org.keycloak:keycloak-core:${keycloakVersion}" - compile "org.keycloak:keycloak-services:${keycloakVersion}" - compile "org.keycloak:keycloak-server-spi:${keycloakVersion}" - compile "org.keycloak:keycloak-server-spi-private:${keycloakVersion}" - compile "org.keycloak:keycloak-model-legacy:${keycloakVersion}" -} - repositories { - mavenCentral() + mavenCentral() } -compileKotlin { - kotlinOptions { - jvmTarget = "1.8" - } +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}" + compileOnly "org.keycloak:keycloak-core:${keycloakVersion}" + compileOnly "org.keycloak:keycloak-services:${keycloakVersion}" + compileOnly "org.keycloak:keycloak-server-spi:${keycloakVersion}" } -compileTestKotlin { - kotlinOptions { - jvmTarget = "1.8" - } -} +shadowJar { + archiveClassifier.set('') +} \ No newline at end of file diff --git a/custom-user-federation-example/gradle/wrapper/gradle-wrapper.properties b/custom-user-federation-example/gradle/wrapper/gradle-wrapper.properties index 9ec837b2b..348c409ea 100644 --- a/custom-user-federation-example/gradle/wrapper/gradle-wrapper.properties +++ b/custom-user-federation-example/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip diff --git a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt index 7b3480d30..86a387b2e 100644 --- a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt +++ b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt @@ -17,7 +17,7 @@ class CustomIdentityProviderFactory : AbstractIdentityProviderFactory { + fun parseConfig(session: KeycloakSession, inputStream: InputStream): Map { return parseOIDCConfig(session, inputStream) } diff --git a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomUserStorageProvider.kt b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomUserStorageProvider.kt index 700ddb03f..df5c45549 100644 --- a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomUserStorageProvider.kt +++ b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomUserStorageProvider.kt @@ -4,15 +4,16 @@ import org.keycloak.component.ComponentModel import org.keycloak.credential.CredentialInput import org.keycloak.credential.CredentialInputUpdater import org.keycloak.credential.CredentialInputValidator -import org.keycloak.credential.CredentialModel -import org.keycloak.credential.LegacyUserCredentialManager +import org.keycloak.credential.UserCredentialManager import org.keycloak.models.* +import org.keycloak.models.credential.PasswordCredentialModel import org.keycloak.storage.ReadOnlyException import org.keycloak.storage.StorageId import org.keycloak.storage.UserStorageProvider import org.keycloak.storage.adapter.AbstractUserAdapter import org.keycloak.storage.user.UserLookupProvider import java.util.* +import java.util.stream.Stream class CustomUserStorageProvider(private val session: KeycloakSession, private val model: ComponentModel) : UserStorageProvider, UserLookupProvider, CredentialInputValidator, CredentialInputUpdater { @@ -30,11 +31,7 @@ class CustomUserStorageProvider(private val session: KeycloakSession, private va // UserLookupProvider - override fun getUserByEmail(email: String, realm: RealmModel): UserModel? { - return null - } - - override fun getUserByUsername(username: String, realm: RealmModel): UserModel? { + override fun getUserByUsername(realm: RealmModel, username: String): UserModel? { val user = loadedUsers[username] if (user != null) { @@ -48,7 +45,7 @@ class CustomUserStorageProvider(private val session: KeycloakSession, private va } override fun credentialManager(): SubjectCredentialManager { - return LegacyUserCredentialManager(session, realm, this) + return UserCredentialManager(session, realm, this) } } @@ -60,13 +57,17 @@ class CustomUserStorageProvider(private val session: KeycloakSession, private va return null } - override fun getUserById(id: String, realm: RealmModel): UserModel? { + override fun getUserById(realm: RealmModel, id: String): UserModel? { val storageId = StorageId(id) val username = storageId.externalId - return getUserByUsername(username, realm) + return getUserByUsername(realm, username) } + override fun getUserByEmail(realm: RealmModel, email: String): UserModel? { + return null + } + // CredentialInputValidator override fun isConfiguredFor(realm: RealmModel, user: UserModel, credentialType: String): Boolean { @@ -74,7 +75,7 @@ class CustomUserStorageProvider(private val session: KeycloakSession, private va } override fun supportsCredentialType(credentialType: String?): Boolean { - return credentialType.equals(CredentialModel.PASSWORD) + return credentialType.equals(PasswordCredentialModel.TYPE) } override fun isValid(realm: RealmModel, user: UserModel, input: CredentialInput): Boolean { @@ -87,14 +88,8 @@ class CustomUserStorageProvider(private val session: KeycloakSession, private va return password == input.value } - // CredentialInputUpdater - - override fun getDisableableCredentialTypes(realm: RealmModel, user: UserModel): MutableSet { - return Collections.EMPTY_SET as MutableSet - } - override fun updateCredential(realm: RealmModel, user: UserModel, input: CredentialInput): Boolean { - if (input.type == CredentialModel.PASSWORD) { + if (input.type == PasswordCredentialModel.TYPE) { throw ReadOnlyException("Custom provider does not support password updating") } @@ -104,4 +99,8 @@ class CustomUserStorageProvider(private val session: KeycloakSession, private va override fun disableCredentialType(realm: RealmModel, user: UserModel, credentialType: String) { } + + override fun getDisableableCredentialTypesStream(realm: RealmModel?, user: UserModel?): Stream? { + return Stream.empty() + } } diff --git a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomUserStorageProviderFactory.kt b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomUserStorageProviderFactory.kt index 9e8d66f2e..f74b7df4a 100644 --- a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomUserStorageProviderFactory.kt +++ b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomUserStorageProviderFactory.kt @@ -20,6 +20,7 @@ class CustomUserStorageProviderFactory : UserStorageProviderFactory = configPropertyList companion object { diff --git a/custom-user-federation-example/src/main/resources/META-INF/jboss-deployment-structure.xml b/custom-user-federation-example/src/main/resources/META-INF/jboss-deployment-structure.xml deleted file mode 100644 index 63a4ac777..000000000 --- a/custom-user-federation-example/src/main/resources/META-INF/jboss-deployment-structure.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - From b81a5c0278b412ecf737dbc851cc28ba54c8dc91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Marchand?= Date: Sun, 28 Jul 2024 10:32:05 +0200 Subject: [PATCH 2/3] fix tests for Keycloak 25 --- .github/workflows/build-test-image.yml | 55 +- .github/workflows/codeql-analysis.yml | 8 +- .github/workflows/dependency-submission.yml | 4 +- .github/workflows/test.yml | 22 +- .gitignore | 1 + docker-compose.yml | 14 +- go.mod | 6 +- go.sum | 14 +- keycloak/realm.go | 5 +- keycloak/realm_user_profile.go | 28 +- keycloak/version.go | 7 + provider/misc/java-keystore-2034.jks | Bin 0 -> 3556 bytes ..._importer_identity_provider_mapper_test.go | 3 + ...e_to_role_identity_provider_mapper_test.go | 3 + ...ak_custom_identity_provider_mapper_test.go | 3 + ...ce_keycloak_custom_user_federation_test.go | 9 +- ...esource_keycloak_group_memberships_test.go | 42 +- ...attribute_identity_provider_mapper_test.go | 3 + ...oded_role_identity_provider_mapper_test.go | 3 + ...er_token_exchange_scope_permission_test.go | 8 +- ...loak_oidc_google_identity_provider_test.go | 9 +- ...ce_keycloak_oidc_identity_provider_test.go | 10 +- .../resource_keycloak_realm_events_test.go | 15 +- ...cloak_realm_keystore_java_kyestore_test.go | 34 +- .../resource_keycloak_realm_user_profile.go | 67 ++- ...source_keycloak_realm_user_profile_test.go | 470 ++++++++++++------ ...eycloak_saml_client_default_scopes_test.go | 30 +- .../resource_keycloak_saml_client_test.go | 23 +- ...ce_keycloak_saml_identity_provider_test.go | 7 +- ..._importer_identity_provider_mapper_test.go | 4 + provider/resource_keycloak_user_test.go | 84 ++-- 31 files changed, 673 insertions(+), 318 deletions(-) create mode 100644 provider/misc/java-keystore-2034.jks diff --git a/.github/workflows/build-test-image.yml b/.github/workflows/build-test-image.yml index a7593b33f..a2a7c408c 100644 --- a/.github/workflows/build-test-image.yml +++ b/.github/workflows/build-test-image.yml @@ -7,37 +7,62 @@ on: - ".github/workflows/build-test-image.yml" - "test/Dockerfile" +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + jobs: build: runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + attestations: write + id-token: write + strategy: matrix: keycloak-version: - - '21.0.1' - - '20.0.5' - - '19.0.2' + - '25.0.2' + - '24.0.5' fail-fast: false + concurrency: group: docker-build-${{ matrix.keycloak-version }} cancel-in-progress: true + steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + - name: Checkout repository + uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Login to Docker Hub - uses: docker/login-action@v2 + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - name: Build and push - uses: docker/build-push-action@v4 + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 with: - push: true - tags: mrparkers/keycloak-dev:${{ matrix.keycloak-version }} file: test/Dockerfile + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ matrix.keycloak-version }} build-args: | KEYCLOAK_VERSION=${{ matrix.keycloak-version }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d353c4f10..056aac257 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v3 @@ -48,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -73,4 +73,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index 30876f3b9..e616f1a8c 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v3 @@ -21,6 +21,6 @@ jobs: cache: true - name: Run snapshot action - uses: actions/go-dependency-submission@v1 + uses: actions/go-dependency-submission@v2 with: go-mod-path: go.mod diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4fce6bfc..2b02f4462 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,17 +7,21 @@ on: branches: - master +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + jobs: verify: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 2 # we want the HEAD commit and the previous commit to compare changed files - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version-file: 'go.mod' cache: true @@ -31,7 +35,7 @@ jobs: # we only want to run tests if any code changes (not for README or docs changes) - name: Check Changed Files id: files - uses: tj-actions/changed-files@v1.1.3 + uses: tj-actions/changed-files@v44 with: files: | .github @@ -43,7 +47,7 @@ jobs: scripts outputs: - code-files-changed: steps.files.outputs.any_changed + code-files-changed: ${{ steps.files.outputs.any_changed }} acceptance: # this conditional is more verbose than I'd like it to be @@ -60,9 +64,8 @@ jobs: strategy: matrix: keycloak-version: - - '21.0.1' - - '20.0.5' - - '19.0.2' + - '25.0.2' + - '24.0.5' fail-fast: false concurrency: group: ${{ github.head_ref || github.run_id }}-${{ matrix.keycloak-version }} @@ -71,7 +74,7 @@ jobs: keycloak: # we have to use a custom docker image for these tests, since it's not possible to provide command-line args # to a service container. see https://github.com/actions/runner/issues/2139 - image: mrparkers/keycloak-dev:${{ matrix.keycloak-version }} + image: ghcr.io/${{ github.repository }}:${{ matrix.keycloak-version }} ports: - 8080:8080 env: @@ -79,6 +82,7 @@ jobs: KC_LOG_LEVEL: INFO KEYCLOAK_ADMIN: keycloak KEYCLOAK_ADMIN_PASSWORD: password + KC_FEATURES: preview steps: - name: Checkout Code uses: actions/checkout@v3 @@ -90,7 +94,7 @@ jobs: cache: true - name: Setup Terraform - uses: hashicorp/setup-terraform@v1 + uses: hashicorp/setup-terraform@v3 with: terraform_wrapper: false terraform_version: 1.4.1 diff --git a/.gitignore b/.gitignore index 528ed9604..c6be4d1cc 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ terraform.tfstate* .gradle/ # custom user federation example +custom-user-federation-example/bin custom-user-federation-example/build !custom-user-federation-example/build/libs diff --git a/docker-compose.yml b/docker-compose.yml index d05dfe5c2..5a1dd0bc4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3" volumes: postgres: services: @@ -12,26 +11,30 @@ services: - 5432:5432 volumes: - postgres:/var/lib/postgresql + openldap: image: osixia/openldap:1.3.0 ports: - 8389:389 + keycloak: - image: quay.io/keycloak/keycloak:21.0.1 - command: start-dev --features=preview + image: quay.io/keycloak/keycloak:25.0.2 + command: --verbose start-dev depends_on: - postgres - openldap environment: - KEYCLOAK_ADMIN=keycloak - KEYCLOAK_ADMIN_PASSWORD=password - - KC_LOG_LEVEL=INFO + - KC_LOG_LEVEL=INFO,org.keycloak.events:DEBUG + - KC_LOG_CONSOLE_COLOR=true - KC_DB=postgres - KC_DB_URL_HOST=postgres - KC_DB_URL_PORT=5432 - KC_DB_URL_DATABASE=keycloak - KC_DB_USERNAME=keycloak - KC_DB_PASSWORD=password + - KC_FEATURES=preview # Enable for remote java debugging # - PREPEND_JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8787 ports: @@ -40,4 +43,5 @@ services: # - 8787:8787 volumes: # Make the custom-user-federation-example extension available to Keycloak. The :z option is required and tells Docker that the volume content will be shared between containers. - - ./custom-user-federation-example/build/libs/custom-user-federation-example.jar:/opt/jboss/keycloak/standalone/deployments/custom-user-federation-example.jar:z + - ./custom-user-federation-example/build/libs/custom-user-federation-example.jar:/opt/keycloak/providers/custom-user-federation-example.jar:z + - ./provider/misc:/opt/keycloak/certs:ro diff --git a/go.mod b/go.mod index aaf5fae2e..9c518061f 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 github.com/imdario/mergo v0.3.13 - golang.org/x/net v0.17.0 + golang.org/x/net v0.23.0 ) require ( @@ -44,9 +44,9 @@ require ( github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect github.com/vmihailenco/tagparser v0.1.1 // indirect github.com/zclconf/go-cty v1.13.1 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/mod v0.8.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect diff --git a/go.sum b/go.sum index e72123337..29937eff1 100644 --- a/go.sum +++ b/go.sum @@ -190,8 +190,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= @@ -209,8 +209,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -235,12 +235,12 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/keycloak/realm.go b/keycloak/realm.go index 6afdf82ee..eb2440aaa 100644 --- a/keycloak/realm.go +++ b/keycloak/realm.go @@ -3,8 +3,9 @@ package keycloak import ( "context" "fmt" - "github.com/mrparkers/terraform-provider-keycloak/keycloak/types" "strings" + + "github.com/mrparkers/terraform-provider-keycloak/keycloak/types" ) type Key struct { @@ -171,6 +172,8 @@ type SmtpServer struct { func (keycloakClient *KeycloakClient) NewRealm(ctx context.Context, realm *Realm) error { _, _, err := keycloakClient.post(ctx, "/realms", realm) + keycloakClient.refresh(ctx) + return err } diff --git a/keycloak/realm_user_profile.go b/keycloak/realm_user_profile.go index ce18ebf47..c7de748b7 100644 --- a/keycloak/realm_user_profile.go +++ b/keycloak/realm_user_profile.go @@ -6,43 +6,51 @@ import ( "fmt" ) +// https://www.keycloak.org/docs-api/25.0.0/rest-api/index.html#UPAttributePermissions type RealmUserProfilePermissions struct { Edit []string `json:"edit"` View []string `json:"view"` } +// https://www.keycloak.org/docs-api/25.0.0/rest-api/index.html#UPAttributeRequired type RealmUserProfileRequired struct { Roles []string `json:"roles,omitempty"` Scopes []string `json:"scopes,omitempty"` } +// https://www.keycloak.org/docs-api/25.0.0/rest-api/index.html#UPAttributeSelector type RealmUserProfileSelector struct { Scopes []string `json:"scopes,omitempty"` } type RealmUserProfileValidationConfig map[string]interface{} +// https://www.keycloak.org/docs-api/25.0.0/rest-api/index.html#UPAttribute type RealmUserProfileAttribute struct { - Annotations map[string]interface{} `json:"annotations,omitempty"` - DisplayName string `json:"displayName,omitempty"` - Group string `json:"group,omitempty"` Name string `json:"name"` - Permissions *RealmUserProfilePermissions `json:"permissions,omitempty"` + DisplayName string `json:"displayName,omitempty"` + Validations map[string]RealmUserProfileValidationConfig `json:"validations,omitempty"` + Annotations map[string]interface{} `json:"annotations,omitempty"` Required *RealmUserProfileRequired `json:"required,omitempty"` + Permissions *RealmUserProfilePermissions `json:"permissions,omitempty"` Selector *RealmUserProfileSelector `json:"selector,omitempty"` - Validations map[string]RealmUserProfileValidationConfig `json:"validations,omitempty"` + Group string `json:"group,omitempty"` + Multivalued bool `json:"multivalued"` } +// https://www.keycloak.org/docs-api/25.0.0/rest-api/index.html#UPGroup type RealmUserProfileGroup struct { - Annotations map[string]interface{} `json:"annotations,omitempty"` - DisplayDescription string `json:"displayDescription,omitempty"` - DisplayHeader string `json:"displayHeader,omitempty"` Name string `json:"name"` + DisplayHeader string `json:"displayHeader,omitempty"` + DisplayDescription string `json:"displayDescription,omitempty"` + Annotations map[string]interface{} `json:"annotations,omitempty"` } +// https://www.keycloak.org/docs-api/25.0.0/rest-api/index.html#UPConfig type RealmUserProfile struct { - Attributes []*RealmUserProfileAttribute `json:"attributes"` - Groups []*RealmUserProfileGroup `json:"groups,omitempty"` + Attributes []*RealmUserProfileAttribute `json:"attributes"` + Groups []*RealmUserProfileGroup `json:"groups,omitempty"` + UnmanagedAttributePolicy string `json:"unmanagedAttributePolicy,omitempty"` } func (keycloakClient *KeycloakClient) UpdateRealmUserProfile(ctx context.Context, realmId string, realmUserProfile *RealmUserProfile) error { diff --git a/keycloak/version.go b/keycloak/version.go index 9fcc5d809..ce65da4e0 100644 --- a/keycloak/version.go +++ b/keycloak/version.go @@ -2,6 +2,7 @@ package keycloak import ( "context" + "github.com/hashicorp/go-version" ) @@ -22,6 +23,12 @@ const ( Version_17 Version = "17.0.0" Version_18 Version = "18.0.0" Version_19 Version = "19.0.0" + Version_20 Version = "20.0.0" + Version_21 Version = "21.0.0" + Version_22 Version = "22.0.0" + Version_23 Version = "23.0.0" + Version_24 Version = "24.0.0" + Version_25 Version = "25.0.0" ) func (keycloakClient *KeycloakClient) VersionIsGreaterThanOrEqualTo(ctx context.Context, versionString Version) (bool, error) { diff --git a/provider/misc/java-keystore-2034.jks b/provider/misc/java-keystore-2034.jks new file mode 100644 index 0000000000000000000000000000000000000000..5e7464bfd00608379072811ac1a7838c069f72bd GIT binary patch literal 3556 zcma)9WmFW7v)x^GSy&pEkVZP)g%w?qSV{!MAOz_y>6Tuar4ghL>Y3OWnMLZ|-1V}E0akmP?US3wk5W|4~555ZZ_b|CXc(0Wf?l zD3ka3jfb|p-gPf8~Cz%wcBrK|Ix0&J0uA@Sk!6 zb$8+K59HywN%y0?^f~!n{z>6QX_S;)L@}JH;>qjslHWS1@Y4{rF>IzbVJhA(zi2ES zXnHk4=S`IHw&MhDw|>#XE5s>g?9Cp5-*?npn=Txc3*9V!IrO7{; z(K2`Sw%+u;sGjtc`S%?Yy*@wh5g&OMh1810;=9Z<6AfyX(uouM>F4=)-kR`r8=n5` z+8Z8-S6~+LqUkRQkCcQrJjPN(1aI3$C6%vFtK8GR&bWm{?NRRbM&CH|@az6?Zx33F z%U+vbMOo@jx<#KG9sY(3VfSBF+LsN6jAd`(y$xMb9?jPmKf?661mry>>4wxz9K2z$ z$`Ci7cqm%7Qy$17%m6cEjp7H`I_OPz@?LBVUAsuV22CCX6LOKS!w44*3~yf7tL~N` zB^Kg+!^z1@ZCF3b4GYe<)gKI%9bE(rd3{gycTQH*L#{65yOwaY9BQ2Axp^DU{($cH z8tueA5)JqgBDfbtP&eo*#7%nxgWK{|N3a*mfN%17ld1Cy{KeY9-sx_f0Wa=De3-&0 zZKP$AMICfhK9an79j5Npp{Y63|3+}PTCSbmEYysos|Kr{!DfP)R$5|kyHBD2qj@#q z@xd;!YRpC_MRRP6W8ZIFH`fgK#l7ZnYqswE(rp3(I(_tP*i3B4H=EI=u|h%n#^PB2 zPeMcMNVsw{^LPKNrKwrD3Z0}m4Jn>pDXq0ugP70wS|n8SIE`DZqbHkw_n)m#< zf6?Jr^bUzrm^kwfmwSb_PG`rKG7+F!;k?i9bhiy%qdV9N8IeA-sCMn>g3ycyMA!uR zIzNgP@Z6cSuMdW#Gf&3&KCu-fn%_`rN{Ojxgn=E0UtnP~ncbHe!GU}~m6bRhrl`WE zHIX5u#PP@deF+DVAy3M;A76b&S>|`Bc&}oTzFo1b=Qp}Vvz^-m?5O%Lzo6|0v8=wd z{8#j4_B+4GonnU_f?+oP-tZEf;ARJ9&z#cC zU7|a5K5+G(zWY7C$0a7R-m=ZC+w^(`YVl3av1;AEN{#RHSlR9<0m93zwk%$?nQlQ? zT0ZsS+W5@Hmhrp~Cn`JG^v3&USK6iChi|M~U1^(gFaJ0ILWy4Pr0@j7 z-vFd9VkKi-8(}?z(iA??q75gs2z8nc^zN7r&PZG|F)dbpseHeSKgz>q#PcqDflq&v z^{tfgXFI^Y0S<9?hVB#CN7a)@7DPk7bS%DiiFZ6HLJ|e9zolDHy>f%A5SC%*?WxBZ zbzZMUHd{deq{dccrJUx=q7MX?USz%@f>dStiA7TkK7{2>eyW&{=WDH~U#C~lY^P0M zGjCxb7}QlF^4&NMO`dBYy`8PHe5-%gy%xPK_c61=P*9(xxndA>xj6fw1~ufVWmE70 zF=3NP)ONSJwddl?NjbCOwKhiM0M6SMGyWvqgJXm}xL^H&-H`%bb0e>!Y6jFn`yy}< z4_mA!j2(2C22Jxz`r3ZEnMYgi0OSa>?q=WKJEdZkTfdjr(tZ_v98M0;Kbp{eB6c97 z{q*zei!RbzXPOFvN@;jp6H%&ZYN6rxf-MdWMn9=s?(y%ibNRKeBeVp@0HSr*|nNHae4%0uOwD zp_>-Ccq}bq?o{cp(zAyvh1$tzATb8D>%m&9TfeZkX(hf+@a%K9=gLR47&op19x8}& zo#82a0->&RTZ1I?x>5^GN%`aRiBXCIuWiKZKN13`Sy7tr?Q0YT9BET$oDz`y1PfWS zC!ekR*Su8DQ-`m;^jG?_(|fYi2p?yvpBJ%WiOAm;O{C$QzxEl>-I*4(JFle|vgKsv z;Tnx~3@|`GLeTwVx5RKHjD-l`0x$zO16&a7{}MDvIG9@9%GQhxd0zzi;2sKvLZHP^ z2rMMvpC%|_HWuRa7qSBZ0e_RlzYOsIviKvj%@lRu%&={uNV+ z(mNOKDtR&2=aC?;XY6}Jqj(JU-#@U4d*x>E@4M3=;U=S04JhxB6d zNgZeHkQMBEW-~EE%oDmSk+8K;KPi5yYp9sS*dEHC`zui0F<0hzAjfET(hW@$ZkR5^ zLIO@p@NBuatL4+e;HfdbJsz7n&bP?=o%g8faB{tIWfSOf~RwZ~&b}rxryf>e=N?PE)`guEg2qqflp^=lxV>;aKY9mK{lxlqWO_ z%tU{?{ny3=2#-if`gYOb>HcjCHf?qsG{WwEe{U{bXG`m`qd1anr89ZTwv>8U6 ze>%c=!AuzF$`Y+hSQ%Gw#x*$U_bB%VdUGl(B^=G^!<{yFigKFv@gn5pzqm**wIhQD z+1sB$n$K+xqT>V{=})__%0*L#v`K?Sw+(rOO{4j~l41 znj*eY#l3o2Ow!8i6(GHV3O%>8WLv$Escas?aJ!r1#=C0p_-jCUeHycWf~88AR@CjD|4!Mv7jJkBBmzo1m@XpK)()Lea^p$r5; zp`ALxT-rh{v^@_vvhGU zj`R_8HZ%VjWv#fm}^`3+*p}~f3iB86bDa(9aGLc{0 zdp}QwA2xe3fwci|KCTrixs&$FSr~wspw**xq|^FLzCZ}7z^0{zN`B*7WK}`dV_%S{ zHR4((np(}TcjR?>4uRB-andmC5A;m-5YX-;leU#Zm(G4@-^g1LRG)(!+cEXq$3$Cg z(=}&&D(nutJfb+l2n%?e^v0ljehC7?CiuZ^i4P0A;zL7c6~&XNyB}CRcZ7L{u~tXo z`oHljomy-AL(+JM5@LUJxXdiUGN!wFcc>9$IImMG;37V?WM%Twkj4N#K|E)tQGjTdm=J3nBy+FOZ<>DNlxISd54_+3x#Zk==+6)(toAf6(S2(o`Z9}o}%0JEW+URUY$ zDZeE>MQsa)+m5%J@O*O0NO=7.0.0 have 67 events, versions >=12.0.0 have 69 events - if ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(testCtx, keycloak.Version_14); ok { + //keycloak versions < 7.0.0 have 63 events, versions >=7.0.0 have 67 events, versions >=12.0.0 have 69 events, versions >=25.0.0 have 87 events + if ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(testCtx, keycloak.Version_25); ok { + if len(realmEventsConfig.EnabledEventTypes) != 87 { + return fmt.Errorf("exptected to enabled_event_types to contain all(79) event types, but it contains %d", len(realmEventsConfig.EnabledEventTypes)) + } + } else if ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(testCtx, keycloak.Version_24); ok { + if len(realmEventsConfig.EnabledEventTypes) != 83 { + return fmt.Errorf("exptected to enabled_event_types to contain all(79) event types, but it contains %d", len(realmEventsConfig.EnabledEventTypes)) + } + } else if ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(testCtx, keycloak.Version_14); ok { if len(realmEventsConfig.EnabledEventTypes) != 79 { return fmt.Errorf("exptected to enabled_event_types to contain all(79) event types, but it contains %d", len(realmEventsConfig.EnabledEventTypes)) } diff --git a/provider/resource_keycloak_realm_keystore_java_kyestore_test.go b/provider/resource_keycloak_realm_keystore_java_kyestore_test.go index dd3d5d5cb..78426093d 100644 --- a/provider/resource_keycloak_realm_keystore_java_kyestore_test.go +++ b/provider/resource_keycloak_realm_keystore_java_kyestore_test.go @@ -2,13 +2,14 @@ package provider import ( "fmt" + "regexp" + "strconv" + "testing" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/mrparkers/terraform-provider-keycloak/keycloak" - "regexp" - "strconv" - "testing" ) func TestAccKeycloakRealmKeystoreJava_basic(t *testing.T) { @@ -28,10 +29,11 @@ func TestAccKeycloakRealmKeystoreJava_basic(t *testing.T) { Check: testAccCheckRealmKeystoreJavaExists("keycloak_realm_keystore_java_keystore.realm_java_keystore"), }, { - ResourceName: "keycloak_realm_keystore_java_keystore.realm_java_keystore", - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: getRealmKeystoreGenericImportId("keycloak_realm_keystore_java_keystore.realm_java_keystore"), + ResourceName: "keycloak_realm_keystore_java_keystore.realm_java_keystore", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getRealmKeystoreGenericImportId("keycloak_realm_keystore_java_keystore.realm_java_keystore"), + ImportStateVerifyIgnore: []string{"key_password", "keystore_password", "algorithm"}, }, }, }) @@ -212,9 +214,11 @@ resource "keycloak_realm_keystore_java_keystore" "realm_java_keystore" { name = "%s" realm_id = data.keycloak_realm.realm.id - keystore = "misc/java-keystore.jks" + keystore = "/opt/keycloak/certs/java-keystore-2034.jks" keystore_password = "12345678" - keystore_alias = "test" + + key_alias = "test" + key_password = "12345678" priority = 100 algorithm = "RS256" @@ -232,9 +236,11 @@ resource "keycloak_realm_keystore_java_keystore" "realm_java_keystore" { name = "%s" realm_id = data.keycloak_realm.realm.id - keystore = "misc/java-keystore.jks" + keystore = "/opt/keycloak/certs/java-keystore-2034.jks" keystore_password = "12345678" - keystore_alias = "test" + + key_alias = "test" + key_password = "12345678" %s = "%s" } @@ -251,10 +257,12 @@ resource "keycloak_realm_keystore_java_keystore" "realm_java_keystore" { name = "%s" realm_id = data.keycloak_realm.realm.id - keystore = "misc/java-keystore.jks" + keystore = "/opt/keycloak/certs/java-keystore-2034.jks" keystore_password = "12345678" - keystore_alias = "test" + key_alias = "test" + key_password = "12345678" + priority = %s algorithm = "%s" } diff --git a/provider/resource_keycloak_realm_user_profile.go b/provider/resource_keycloak_realm_user_profile.go index b8ab597de..2799047b8 100644 --- a/provider/resource_keycloak_realm_user_profile.go +++ b/provider/resource_keycloak_realm_user_profile.go @@ -3,19 +3,30 @@ package provider import ( "context" "encoding/json" + "errors" + "fmt" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/mrparkers/terraform-provider-keycloak/keycloak" ) +var ( + unmanagedAttributePolicies = []string{"DISABLED", "ENABLED", "ADMIN_VIEW", "ADMIN_EDIT"} +) + func resourceKeycloakRealmUserProfile() *schema.Resource { return &schema.Resource{ CreateContext: resourceKeycloakRealmUserProfileCreate, ReadContext: resourceKeycloakRealmUserProfileRead, DeleteContext: resourceKeycloakRealmUserProfileDelete, UpdateContext: resourceKeycloakRealmUserProfileUpdate, + // This resource can be imported using {{realm}}. + Importer: &schema.ResourceImporter{ + StateContext: resourceKeycloakRealmUserProfileImport, + }, Schema: map[string]*schema.Schema{ "realm_id": { Type: schema.TypeString, @@ -35,6 +46,10 @@ func resourceKeycloakRealmUserProfile() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "multivalued": { + Type: schema.TypeBool, + Required: true, + }, "group": { Type: schema.TypeString, Optional: true, @@ -43,16 +58,19 @@ func resourceKeycloakRealmUserProfile() *schema.Resource { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, }, "required_for_roles": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, }, "required_for_scopes": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, }, "permissions": { Type: schema.TypeList, @@ -88,6 +106,7 @@ func resourceKeycloakRealmUserProfile() *schema.Resource { Type: schema.TypeMap, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, }, }, }, @@ -96,6 +115,7 @@ func resourceKeycloakRealmUserProfile() *schema.Resource { Type: schema.TypeMap, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, }, }, }, @@ -121,10 +141,17 @@ func resourceKeycloakRealmUserProfile() *schema.Resource { Type: schema.TypeMap, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, }, }, }, }, + "unmanaged_attribute_policy": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(unmanagedAttributePolicies, false), + Default: "DISABLED", + }, }, } } @@ -134,6 +161,7 @@ func getRealmUserProfileAttributeFromData(m map[string]interface{}) *keycloak.Re Name: m["name"].(string), DisplayName: m["display_name"].(string), Group: m["group"].(string), + Multivalued: m["multivalued"].(bool), } if v, ok := m["permissions"]; ok && len(v.([]interface{})) > 0 { @@ -293,6 +321,10 @@ func getRealmUserProfileFromData(data *schema.ResourceData) *keycloak.RealmUserP realmUserProfile.Attributes = getRealmUserProfileAttributesFromData(data.Get("attribute").([]interface{})) realmUserProfile.Groups = getRealmUserProfileGroupsFromData(data.Get("group").(*schema.Set).List()) + if data.Get("unmanaged_attribute_policy").(string) != "DISABLED" { + realmUserProfile.UnmanagedAttributePolicy = data.Get("unmanaged_attribute_policy").(string) + } + return realmUserProfile } @@ -303,6 +335,8 @@ func getRealmUserProfileAttributeData(attr *keycloak.RealmUserProfileAttribute) attributeData["display_name"] = attr.DisplayName attributeData["group"] = attr.Group + attributeData["multivalued"] = attr.Multivalued + if attr.Selector != nil && len(attr.Selector.Scopes) != 0 { attributeData["enabled_when_scope"] = attr.Selector.Scopes } @@ -389,6 +423,7 @@ func getRealmUserProfileGroupData(group *keycloak.RealmUserProfileGroup) map[str } func setRealmUserProfileData(data *schema.ResourceData, realmUserProfile *keycloak.RealmUserProfile) { + attributes := make([]interface{}, 0) for _, attr := range realmUserProfile.Attributes { attributes = append(attributes, getRealmUserProfileAttributeData(attr)) @@ -399,7 +434,14 @@ func setRealmUserProfileData(data *schema.ResourceData, realmUserProfile *keyclo for _, group := range realmUserProfile.Groups { groups = append(groups, getRealmUserProfileGroupData(group)) } + data.Set("group", groups) + + if len(realmUserProfile.UnmanagedAttributePolicy) == 0 { + data.Set("unmanaged_attribute_policy", "DISABLED") + } else { + data.Set("unmanaged_attribute_policy", realmUserProfile.UnmanagedAttributePolicy) + } } func resourceKeycloakRealmUserProfileCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -432,20 +474,29 @@ func resourceKeycloakRealmUserProfileRead(ctx context.Context, data *schema.Reso return nil } -func resourceKeycloakRealmUserProfileDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { +func resourceKeycloakRealmUserProfileImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { keycloakClient := meta.(*keycloak.KeycloakClient) - realmId := data.Get("realm_id").(string) - // The realm user profile cannot be deleted, so instead we set it back to its "zero" values. - realmUserProfile := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{}, - Groups: []*keycloak.RealmUserProfileGroup{}, + parts := strings.Split(d.Id(), "/") + if len(parts) != 1 { + return nil, fmt.Errorf("Invalid import. Supported import format: {{realm}}.") } - err := keycloakClient.UpdateRealmUserProfile(ctx, realmId, realmUserProfile) + _, err := keycloakClient.GetRealmUserProfile(ctx, parts[0]) if err != nil { - return diag.FromErr(err) + return nil, err } + d.Set("realm_id", parts[0]) + + diagnostics := resourceKeycloakRealmUserProfileRead(ctx, d, meta) + if diagnostics.HasError() { + return nil, errors.New(diagnostics[0].Summary) + } + + return []*schema.ResourceData{d}, nil +} + +func resourceKeycloakRealmUserProfileDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { return nil } diff --git a/provider/resource_keycloak_realm_user_profile_test.go b/provider/resource_keycloak_realm_user_profile_test.go index 116e39dd3..e6d7abb70 100644 --- a/provider/resource_keycloak_realm_user_profile_test.go +++ b/provider/resource_keycloak_realm_user_profile_test.go @@ -16,90 +16,181 @@ import ( "github.com/mrparkers/terraform-provider-keycloak/keycloak" ) -func TestAccKeycloakRealmUserProfile_featureDisabled(t *testing.T) { - realmName := acctest.RandomWithPrefix("tf-acc") +var ( + default_userprofile_attributs = []*keycloak.RealmUserProfileAttribute{ + { + Name: "username", + DisplayName: "${username}", + Multivalued: false, + + Permissions: &keycloak.RealmUserProfilePermissions{ + Edit: []string{"admin", "user"}, + View: []string{"admin", "user"}, + }, + Validations: map[string]keycloak.RealmUserProfileValidationConfig{ + "length": map[string]interface{}{"min": "3", "max": "255"}, + "person-name-prohibited-characters": map[string]interface{}{}, + "up-username-not-idn-homograph": map[string]interface{}{}, + }, + }, + { + Name: "email", + DisplayName: "${email}", + Multivalued: false, + + Permissions: &keycloak.RealmUserProfilePermissions{ + Edit: []string{"admin", "user"}, + View: []string{"admin", "user"}, + }, + Validations: map[string]keycloak.RealmUserProfileValidationConfig{ + "email": map[string]interface{}{}, + "length": map[string]interface{}{"max": "255"}, + }, + Required: &keycloak.RealmUserProfileRequired{ + Roles: []string{"user"}, + }, + }, + { + Name: "firstName", + DisplayName: "${firstName}", + Multivalued: false, + + Permissions: &keycloak.RealmUserProfilePermissions{ + Edit: []string{"admin", "user"}, + View: []string{"admin", "user"}, + }, + Validations: map[string]keycloak.RealmUserProfileValidationConfig{ + "length": map[string]interface{}{"max": "255"}, + "person-name-prohibited-characters": map[string]interface{}{}, + }, + Required: &keycloak.RealmUserProfileRequired{ + Roles: []string{"user"}, + }, + }, + { + Name: "lastName", + DisplayName: "${lastName}", + Multivalued: false, + + Permissions: &keycloak.RealmUserProfilePermissions{ + Edit: []string{"admin", "user"}, + View: []string{"admin", "user"}, + }, + Validations: map[string]keycloak.RealmUserProfileValidationConfig{ + "length": map[string]interface{}{"max": "255"}, + "person-name-prohibited-characters": map[string]interface{}{}, + }, + Required: &keycloak.RealmUserProfileRequired{ + Roles: []string{"user"}, + }, + }, + } + default_userprofile_groups = []*keycloak.RealmUserProfileGroup{ + { + Name: "user-metadata", + DisplayHeader: "User metadata", + DisplayDescription: "Attributes, which refer to user metadata", + }, + } +) +func TestAccKeycloakRealmUserProfile_Importer(t *testing.T) { + realmName := acctest.RandomWithPrefix("tf-realm-acc") resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviderFactories, PreCheck: func() { testAccPreCheck(t) }, - CheckDestroy: testAccCheckKeycloakRealmUserProfileDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakRealmUserProfile_featureDisabled(realmName), - ExpectError: regexp.MustCompile("User Profile is disabled"), + Config: testKeycloakRealmUserProfile_UnmanagedAttributePolicy(realmName, "ENABLED"), + Check: testAccCheckKeycloakRealmUserProfileUnmanagedAttributePolicy("keycloak_realm_user_profile.realm_user_profile", "ENABLED"), + }, + { + ResourceName: "keycloak_realm_user_profile.realm_user_profile", + ImportState: true, + ImportStateVerify: true, }, }, }) } -func TestAccKeycloakRealmUserProfile_basicEmpty(t *testing.T) { - skipIfVersionIsLessThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_14) +func TestAccKeycloakRealmUserProfile_UnmanagedAttributePolicy(t *testing.T) { realmName := acctest.RandomWithPrefix("tf-acc") - - realmUserProfile := &keycloak.RealmUserProfile{} - resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviderFactories, PreCheck: func() { testAccPreCheck(t) }, - CheckDestroy: testAccCheckKeycloakRealmUserProfileDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakRealmUserProfile_template(realmName, realmUserProfile), - Check: testAccCheckKeycloakRealmUserProfileExists("keycloak_realm_user_profile.realm_user_profile"), + Config: testKeycloakRealmUserProfile_UnmanagedAttributePolicy(realmName, "DISABLED"), + Check: testAccCheckKeycloakRealmUserProfileUnmanagedAttributePolicy("keycloak_realm_user_profile.realm_user_profile", "DISABLED"), + }, + { + Config: testKeycloakRealmUserProfile_UnmanagedAttributePolicy(realmName, "ENABLED"), + Check: testAccCheckKeycloakRealmUserProfileUnmanagedAttributePolicy("keycloak_realm_user_profile.realm_user_profile", "ENABLED"), + }, + { + Config: testKeycloakRealmUserProfile_UnmanagedAttributePolicy(realmName, "ADMIN_VIEW"), + Check: testAccCheckKeycloakRealmUserProfileUnmanagedAttributePolicy("keycloak_realm_user_profile.realm_user_profile", "ADMIN_VIEW"), + }, + { + Config: testKeycloakRealmUserProfile_UnmanagedAttributePolicy(realmName, "ADMIN_EDIT"), + Check: testAccCheckKeycloakRealmUserProfileUnmanagedAttributePolicy("keycloak_realm_user_profile.realm_user_profile", "ADMIN_EDIT"), }, }, }) } func TestAccKeycloakRealmUserProfile_basicFull(t *testing.T) { + skipIfVersionIsLessThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_14) realmName := acctest.RandomWithPrefix("tf-acc") - realmUserProfile := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - {Name: "attribute1"}, - { - Name: "attribute2", - DisplayName: "attribute 2", - Group: "group", - Selector: &keycloak.RealmUserProfileSelector{Scopes: []string{"roles"}}, - Required: &keycloak.RealmUserProfileRequired{ - Roles: []string{"user"}, - Scopes: []string{"offline_access"}, - }, - Permissions: &keycloak.RealmUserProfilePermissions{ - Edit: []string{"admin", "user"}, - View: []string{"admin", "user"}, - }, - Validations: map[string]keycloak.RealmUserProfileValidationConfig{ - "person-name-prohibited-characters": map[string]interface{}{}, - "pattern": map[string]interface{}{"pattern": "\"^[a-z]+$\"", "error_message": "\"Error!\""}, - }, - Annotations: map[string]interface{}{ - "foo": "\"bar\"", - "inputOptionLabels": "{\"a\":\"b\"}", - }, - }, + attributes := default_userprofile_attributs + attributes = append(attributes, &keycloak.RealmUserProfileAttribute{Name: "attribute1"}) + attributes = append(attributes, &keycloak.RealmUserProfileAttribute{ + Name: "attribute2", + DisplayName: "attribute 2", + Multivalued: false, + Group: "group", + Selector: &keycloak.RealmUserProfileSelector{Scopes: []string{"roles"}}, + Required: &keycloak.RealmUserProfileRequired{ + Roles: []string{"user"}, + Scopes: []string{"offline_access"}, }, - Groups: []*keycloak.RealmUserProfileGroup{ - { - Name: "group", - DisplayDescription: "Description", - DisplayHeader: "Header", - Annotations: map[string]interface{}{ - "foo": "\"bar\"", - "test": "{\"a2\":\"b2\"}", - }, - }, + Permissions: &keycloak.RealmUserProfilePermissions{ + Edit: []string{"admin", "user"}, + View: []string{"admin", "user"}, }, + Validations: map[string]keycloak.RealmUserProfileValidationConfig{ + "person-name-prohibited-characters": map[string]interface{}{}, + "pattern": map[string]interface{}{"pattern": "\"^[a-z]+$\"", "error_message": "\"Error!\""}, + }, + Annotations: map[string]interface{}{ + "foo": "\"bar\"", + "inputOptionLabels": "{\"a\":\"b\"}", + }, + }) + + groups := default_userprofile_groups + groups = append(groups, &keycloak.RealmUserProfileGroup{ + Name: "group", + DisplayDescription: "Description", + DisplayHeader: "Header", + Annotations: map[string]interface{}{ + "foo": "\"bar\"", + "test": "{\"a2\":\"b2\"}", + }, + }) + + realmUserProfile := &keycloak.RealmUserProfile{ + Attributes: attributes, + Groups: groups, } resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviderFactories, PreCheck: func() { testAccPreCheck(t) }, - CheckDestroy: testAccCheckKeycloakRealmUserProfileDestroy(), Steps: []resource.TestStep{ { Config: testKeycloakRealmUserProfile_template(realmName, realmUserProfile), @@ -112,29 +203,24 @@ func TestAccKeycloakRealmUserProfile_basicFull(t *testing.T) { } func TestAccKeycloakRealmUserProfile_group(t *testing.T) { + skipIfVersionIsLessThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_14) realmName := acctest.RandomWithPrefix("tf-acc") withoutGroup := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - {Name: "attribute"}, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{Name: "attribute"}), + Groups: default_userprofile_groups, } withGroup := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - {Name: "attribute"}, - }, - Groups: []*keycloak.RealmUserProfileGroup{ - {Name: "group"}, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{Name: "attribute"}), + Groups: append(default_userprofile_groups, &keycloak.RealmUserProfileGroup{Name: "group"}), } resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviderFactories, PreCheck: func() { testAccPreCheck(t) }, - CheckDestroy: testAccCheckKeycloakRealmUserProfileDestroy(), Steps: []resource.TestStep{ { Config: testKeycloakRealmUserProfile_template(realmName, withoutGroup), @@ -159,57 +245,51 @@ func TestAccKeycloakRealmUserProfile_group(t *testing.T) { } func TestAccKeycloakRealmUserProfile_attributeValidator(t *testing.T) { + skipIfVersionIsLessThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_14) realmName := acctest.RandomWithPrefix("tf-acc") withoutValidator := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - { - Name: "attribute", - }, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{Name: "attribute"}), + Groups: default_userprofile_groups, } withInitialConfig := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - { - Name: "attribute", - Validations: map[string]keycloak.RealmUserProfileValidationConfig{ - "length": map[string]interface{}{"min": "5", "max": "10"}, - "options": map[string]interface{}{"options": "[\"cgu\"]"}, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{ + Name: "attribute", + Validations: map[string]keycloak.RealmUserProfileValidationConfig{ + "length": map[string]interface{}{"min": "5", "max": "10"}, + "options": map[string]interface{}{"options": "[\"cgu\"]"}, }, - }, + }), + Groups: default_userprofile_groups, } withNewConfig := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - { - Name: "attribute", - Validations: map[string]keycloak.RealmUserProfileValidationConfig{ - "length": map[string]interface{}{"min": "6", "max": "10"}, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{ + Name: "attribute", + Validations: map[string]keycloak.RealmUserProfileValidationConfig{ + "length": map[string]interface{}{"min": "6", "max": "10"}, }, - }, + }), + Groups: default_userprofile_groups, } withNewValidator := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - { - Name: "attribute", - Validations: map[string]keycloak.RealmUserProfileValidationConfig{ - "person-name-prohibited-characters": map[string]interface{}{}, - "length": map[string]interface{}{"min": "6", "max": "10"}, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{ + Name: "attribute", + Validations: map[string]keycloak.RealmUserProfileValidationConfig{ + "person-name-prohibited-characters": map[string]interface{}{}, + "length": map[string]interface{}{"min": "6", "max": "10"}, }, - }, + }), + Groups: default_userprofile_groups, } resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviderFactories, PreCheck: func() { testAccPreCheck(t) }, - CheckDestroy: testAccCheckKeycloakRealmUserProfileDestroy(), Steps: []resource.TestStep{ { Config: testKeycloakRealmUserProfile_template(realmName, withoutValidator), @@ -252,65 +332,60 @@ func TestAccKeycloakRealmUserProfile_attributeValidator(t *testing.T) { } func TestAccKeycloakRealmUserProfile_attributePermissions(t *testing.T) { + skipIfVersionIsLessThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_14) realmName := acctest.RandomWithPrefix("tf-acc") withoutPermissions := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - { - Name: "attribute", - }, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{ + Name: "attribute", + }), + Groups: default_userprofile_groups, } viewAttributeMissing := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - { - Name: "attribute", - Permissions: &keycloak.RealmUserProfilePermissions{ - Edit: []string{"admin", "user"}, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{ + Name: "attribute", + Permissions: &keycloak.RealmUserProfilePermissions{ + Edit: []string{"admin", "user"}, }, - }, + }), + Groups: default_userprofile_groups, } editAttributeMissing := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - { - Name: "attribute", - Permissions: &keycloak.RealmUserProfilePermissions{ - View: []string{"admin", "user"}, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{ + Name: "attribute", + Permissions: &keycloak.RealmUserProfilePermissions{ + View: []string{"admin", "user"}, }, - }, + }), + Groups: default_userprofile_groups, } bothAttributesMissing := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - { - Name: "attribute", - Permissions: &keycloak.RealmUserProfilePermissions{}, - }, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{ + Name: "attribute", + Permissions: &keycloak.RealmUserProfilePermissions{}, + }), + Groups: default_userprofile_groups, } withRightPermissions := &keycloak.RealmUserProfile{ - Attributes: []*keycloak.RealmUserProfileAttribute{ - { - Name: "attribute", - Permissions: &keycloak.RealmUserProfilePermissions{ - Edit: []string{"admin", "user"}, - View: []string{"admin", "user"}, - }, + Attributes: append(default_userprofile_attributs, &keycloak.RealmUserProfileAttribute{ + Name: "attribute", + Permissions: &keycloak.RealmUserProfilePermissions{ + Edit: []string{"admin", "user"}, + View: []string{"admin", "user"}, }, - }, + }), + Groups: default_userprofile_groups, } resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviderFactories, PreCheck: func() { testAccPreCheck(t) }, - CheckDestroy: testAccCheckKeycloakRealmUserProfileDestroy(), Steps: []resource.TestStep{ { Config: testKeycloakRealmUserProfile_template(realmName, withoutPermissions), @@ -346,25 +421,127 @@ func TestAccKeycloakRealmUserProfile_attributePermissions(t *testing.T) { }) } -func testKeycloakRealmUserProfile_featureDisabled(realm string) string { +func testKeycloakRealmUserProfile_UnmanagedAttributePolicy(realm string, unmanagedAttributePolicy string) string { return fmt.Sprintf(` resource "keycloak_realm" "realm" { realm = "%s" } resource "keycloak_realm_user_profile" "realm_user_profile" { realm_id = keycloak_realm.realm.id + + unmanaged_attribute_policy = "%s" + + attribute { + name= "username" + display_name= "$${username}" + + validator { + name = "length" + config = { + min= "3" + max= "255" + } + } + validator { + name = "person-name-prohibited-characters" + } + validator { + name = "up-username-not-idn-homograph" + } + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + + attribute { + name= "email" + display_name= "$${email}" + + validator { + name = "email" + } + validator { + name = "length" + config = { + max= "255" + } + } + + required_for_roles = [ "user" ] + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + + attribute { + name= "firstName" + display_name= "$${firstName}" + + validator { + name = "length" + config = { + max= "255" + } + } + validator { + name = "person-name-prohibited-characters" + } + + required_for_roles = [ "user" ] + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + + attribute { + name= "lastName" + display_name= "$${lastName}" + + validator { + name = "length" + config = { + max= "255" + } + } + validator { + name = "person-name-prohibited-characters" + } + + required_for_roles = [ "user" ] + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + + group { + name = "user-metadata" + display_header = "User metadata" + display_description = "Attributes, which refer to user metadata" + } } -`, realm) +`, realm, unmanagedAttributePolicy) } func testKeycloakRealmUserProfile_template(realm string, realmUserProfile *keycloak.RealmUserProfile) string { tmpl, err := template.New("").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(` resource "keycloak_realm" "realm" { realm = "{{ .realm }}" - - attributes = { - userProfileEnabled = true - } } resource "keycloak_realm_user_profile" "realm_user_profile" { @@ -381,6 +558,8 @@ resource "keycloak_realm_user_profile" "realm_user_profile" { group = "{{ $attribute.Group }}" {{- end }} + multivalued = "{{ $attribute.Multivalued }}" + {{- if $attribute.Selector }} {{- if $attribute.Selector.Scopes }} enabled_when_scope = ["{{ StringsJoin $attribute.Selector.Scopes "\", \"" }}"] @@ -470,16 +649,24 @@ resource "keycloak_realm_user_profile" "realm_user_profile" { return "" } - return tmplBuf.String() + return strings.Replace(tmplBuf.String(), "${", "$${", -1) } -func testAccCheckKeycloakRealmUserProfileExists(resourceName string) resource.TestCheckFunc { +func testAccCheckKeycloakRealmUserProfileUnmanagedAttributePolicy(resourceName string, expectedUnmanagedAttributePolicy string) resource.TestCheckFunc { return func(s *terraform.State) error { - _, err := getRealmUserProfileFromState(s, resourceName) + + realm_user_profile, err := getRealmUserProfileFromState(s, resourceName) if err != nil { return err } + if expectedUnmanagedAttributePolicy == "DISABLED" { + if len(realm_user_profile.UnmanagedAttributePolicy) != 0 { + return fmt.Errorf("expected UnmanagedAttributePolicy value empty (%s), but was %s", expectedUnmanagedAttributePolicy, realm_user_profile.UnmanagedAttributePolicy) + } + } else if realm_user_profile.UnmanagedAttributePolicy != expectedUnmanagedAttributePolicy { + return fmt.Errorf("expected UnmanagedAttributePolicy value %s, but was %s", expectedUnmanagedAttributePolicy, realm_user_profile.UnmanagedAttributePolicy) + } return nil } } @@ -494,26 +681,7 @@ func testAccCheckKeycloakRealmUserProfileStateEqual(resourceName string, realmUs if !reflect.DeepEqual(realmUserProfile, realmUserProfileFromState) { j1, _ := json.Marshal(realmUserProfile) j2, _ := json.Marshal(realmUserProfileFromState) - return fmt.Errorf("%v\nshould be equal to\n%v", string(j1), string(j2)) - } - - return nil - } -} - -func testAccCheckKeycloakRealmUserProfileDestroy() resource.TestCheckFunc { - return func(s *terraform.State) error { - for _, rs := range s.RootModule().Resources { - if rs.Type != "keycloak_realm_user_profile" { - continue - } - - realm := rs.Primary.Attributes["realm_id"] - - realmUserProfile, _ := keycloakClient.GetRealmUserProfile(testCtx, realm) - if realmUserProfile != nil { - return fmt.Errorf("user profile for realm %s", realm) - } + return fmt.Errorf("\n%v\nshould be equal to\n%v", string(j1), string(j2)) } return nil diff --git a/provider/resource_keycloak_saml_client_default_scopes_test.go b/provider/resource_keycloak_saml_client_default_scopes_test.go index 043f612f1..3a24204fe 100644 --- a/provider/resource_keycloak_saml_client_default_scopes_test.go +++ b/provider/resource_keycloak_saml_client_default_scopes_test.go @@ -14,6 +14,7 @@ import ( // All saml clients in Keycloak will automatically have these scopes listed as "default client scopes". var preAssignedDefaultSamlClientScopes = []string{"role_list"} +var setSamlOrganizationIfNeeded = "" func TestAccKeycloakSamlClientDefaultScopes_basic(t *testing.T) { t.Parallel() @@ -22,12 +23,17 @@ func TestAccKeycloakSamlClientDefaultScopes_basic(t *testing.T) { clientScopes := append(preAssignedDefaultSamlClientScopes, clientScope) + if ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(testCtx, keycloak.Version_25); ok { + setSamlOrganizationIfNeeded = "\"saml_organization\"," + preAssignedDefaultSamlClientScopes = []string{"role_list", "saml_organization"} + } + resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviderFactories, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { - Config: testKeycloakSamlClientDefaultScopes_basic(client, clientScope), + Config: testKeycloakSamlClientDefaultScopes_basic(client, clientScope, setSamlOrganizationIfNeeded), Check: testAccCheckKeycloakSamlClientHasDefaultScopes("keycloak_saml_client_default_scopes.default_scopes", clientScopes), }, // we need a separate test step for destroy instead of using CheckDestroy because this resource is implicitly @@ -46,6 +52,11 @@ func TestAccKeycloakSamlClientDefaultScopes_updateClientForceNew(t *testing.T) { clientTwo := acctest.RandomWithPrefix("tf-acc") clientScope := acctest.RandomWithPrefix("tf-acc") + if ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(testCtx, keycloak.Version_25); ok { + setSamlOrganizationIfNeeded = "\"saml_organization\"," + preAssignedDefaultSamlClientScopes = []string{"role_list", "saml_organization"} + } + clientScopes := append(preAssignedDefaultSamlClientScopes, clientScope) resource.Test(t, resource.TestCase{ @@ -53,11 +64,11 @@ func TestAccKeycloakSamlClientDefaultScopes_updateClientForceNew(t *testing.T) { PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { - Config: testKeycloakSamlClientDefaultScopes_basic(clientOne, clientScope), + Config: testKeycloakSamlClientDefaultScopes_basic(clientOne, clientScope, setSamlOrganizationIfNeeded), Check: testAccCheckKeycloakSamlClientHasDefaultScopes("keycloak_saml_client_default_scopes.default_scopes", clientScopes), }, { - Config: testKeycloakSamlClientDefaultScopes_basic(clientTwo, clientScope), + Config: testKeycloakSamlClientDefaultScopes_basic(clientTwo, clientScope, setSamlOrganizationIfNeeded), Check: testAccCheckKeycloakSamlClientHasDefaultScopes("keycloak_saml_client_default_scopes.default_scopes", clientScopes), }, }, @@ -213,6 +224,11 @@ func TestAccKeycloakSamlClientDefaultScopes_noImportNeeded(t *testing.T) { client := acctest.RandomWithPrefix("tf-acc") clientScope := acctest.RandomWithPrefix("tf-acc") + if ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(testCtx, keycloak.Version_25); ok { + setSamlOrganizationIfNeeded = "\"saml_organization\"," + preAssignedDefaultSamlClientScopes = []string{"role_list", "saml_organization"} + } + clientScopes := append(preAssignedDefaultSamlClientScopes, clientScope) resource.Test(t, resource.TestCase{ @@ -235,7 +251,7 @@ func TestAccKeycloakSamlClientDefaultScopes_noImportNeeded(t *testing.T) { t.Fatal(err) } }, - Config: testKeycloakSamlClientDefaultScopes_basic(client, clientScope), + Config: testKeycloakSamlClientDefaultScopes_basic(client, clientScope, setSamlOrganizationIfNeeded), Check: testAccCheckKeycloakSamlClientHasDefaultScopes("keycloak_saml_client_default_scopes.default_scopes", clientScopes), }, }, @@ -345,7 +361,7 @@ func testAccCheckKeycloakSamlClientDefaultScopeIsNotAttached(resourceName, clien } } -func testKeycloakSamlClientDefaultScopes_basic(client, clientScope string) string { +func testKeycloakSamlClientDefaultScopes_basic(client, clientScope string, setSamlOrganizationIfNeeded string) string { return fmt.Sprintf(` data "keycloak_realm" "realm" { realm = "%s" @@ -375,10 +391,11 @@ resource "keycloak_saml_client_default_scopes" "default_scopes" { client_id = keycloak_saml_client.client.id default_scopes = [ "role_list", + %s keycloak_saml_client_scope.client_scope.name ] } - `, testAccRealm.Realm, client, clientScope) + `, testAccRealm.Realm, client, clientScope, setSamlOrganizationIfNeeded) } func testKeycloakSamlClientDefaultScopes_noDefaultScopes(client, clientScope string) string { @@ -461,6 +478,7 @@ resource "keycloak_saml_client_default_scopes" "default_scopes" { client_id = "%s" default_scopes = [ "role_list", + "saml_organization", keycloak_saml_client_scope.client_scope.name ] } diff --git a/provider/resource_keycloak_saml_client_test.go b/provider/resource_keycloak_saml_client_test.go index d791b79ca..b74f2760a 100644 --- a/provider/resource_keycloak_saml_client_test.go +++ b/provider/resource_keycloak_saml_client_test.go @@ -2,11 +2,12 @@ package provider import ( "fmt" - "github.com/mrparkers/terraform-provider-keycloak/keycloak/types" "regexp" "strings" "testing" + "github.com/mrparkers/terraform-provider-keycloak/keycloak/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -149,7 +150,7 @@ func TestAccKeycloakSamlClient_updateInPlace(t *testing.T) { acctest.RandString(20), }, BaseUrl: "http://localhost:2222/" + acctest.RandString(20), - MasterSamlProcessingUrl: acctest.RandString(20), + MasterSamlProcessingUrl: "http://localhost:2222/" + acctest.RandString(20), Attributes: &keycloak.SamlClientAttributes{ IncludeAuthnStatement: types.KeycloakBoolQuoted(randomBool()), @@ -167,10 +168,10 @@ func TestAccKeycloakSamlClient_updateInPlace(t *testing.T) { SigningPrivateKey: signingPrivateKeyBefore, IDPInitiatedSSOURLName: acctest.RandString(20), IDPInitiatedSSORelayState: acctest.RandString(20), - AssertionConsumerPostURL: acctest.RandString(20), - AssertionConsumerRedirectURL: acctest.RandString(20), - LogoutServicePostBindingURL: acctest.RandString(20), - LogoutServiceRedirectBindingURL: acctest.RandString(20), + AssertionConsumerPostURL: "http://localhost:2222/" + acctest.RandString(20), + AssertionConsumerRedirectURL: "http://localhost:2222/" + acctest.RandString(20), + LogoutServicePostBindingURL: "http://localhost:2222/" + acctest.RandString(20), + LogoutServiceRedirectBindingURL: "http://localhost:2222/" + acctest.RandString(20), LoginTheme: "keycloak", }, } @@ -190,7 +191,7 @@ func TestAccKeycloakSamlClient_updateInPlace(t *testing.T) { acctest.RandString(20), }, BaseUrl: "http://localhost:2222/" + acctest.RandString(20), - MasterSamlProcessingUrl: acctest.RandString(20), + MasterSamlProcessingUrl: "http://localhost:2222/" + acctest.RandString(20), Attributes: &keycloak.SamlClientAttributes{ IncludeAuthnStatement: types.KeycloakBoolQuoted(randomBool()), @@ -208,10 +209,10 @@ func TestAccKeycloakSamlClient_updateInPlace(t *testing.T) { SigningPrivateKey: signingPrivateKeyAfter, IDPInitiatedSSOURLName: acctest.RandString(20), IDPInitiatedSSORelayState: acctest.RandString(20), - AssertionConsumerPostURL: acctest.RandString(20), - AssertionConsumerRedirectURL: acctest.RandString(20), - LogoutServicePostBindingURL: acctest.RandString(20), - LogoutServiceRedirectBindingURL: acctest.RandString(20), + AssertionConsumerPostURL: "http://localhost:2222/" + acctest.RandString(20), + AssertionConsumerRedirectURL: "http://localhost:2222/" + acctest.RandString(20), + LogoutServicePostBindingURL: "http://localhost:2222/" + acctest.RandString(20), + LogoutServiceRedirectBindingURL: "http://localhost:2222/" + acctest.RandString(20), LoginTheme: "keycloak", }, } diff --git a/provider/resource_keycloak_saml_identity_provider_test.go b/provider/resource_keycloak_saml_identity_provider_test.go index da302e628..b53528e1c 100644 --- a/provider/resource_keycloak_saml_identity_provider_test.go +++ b/provider/resource_keycloak_saml_identity_provider_test.go @@ -2,11 +2,12 @@ package provider import ( "fmt" - "github.com/mrparkers/terraform-provider-keycloak/keycloak/types" "regexp" "strconv" "testing" + "github.com/mrparkers/terraform-provider-keycloak/keycloak/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -337,6 +338,7 @@ resource "keycloak_saml_identity_provider" "saml" { alias = "%s" entity_id = "https://example.com/entity_id" single_sign_on_service_url = "https://example.com/auth" + sync_mode = "FORCE" } `, testAccRealm.Realm, saml) } @@ -353,6 +355,7 @@ resource "keycloak_saml_identity_provider" "saml" { provider_id = "%s" entity_id = "https://example.com/entity_id" single_sign_on_service_url = "https://example.com/auth" + sync_mode = "FORCE" } `, testAccRealm.Realm, saml, providerId) } @@ -370,6 +373,7 @@ resource "keycloak_saml_identity_provider" "saml" { principal_type = "ATTRIBUTE" entity_id = "https://example.com/entity_id" single_sign_on_service_url = "https://example.com/auth" + sync_mode = "FORCE" } `, testAccRealm.Realm, saml, nameIdPolicyFormat) } @@ -385,6 +389,7 @@ resource "keycloak_saml_identity_provider" "saml" { alias = "%s" entity_id = "https://example.com/entity_id" single_sign_on_service_url = "https://example.com/auth" + sync_mode = "FORCE" extra_config = { %s = "%s" } diff --git a/provider/resource_keycloak_user_template_importer_identity_provider_mapper_test.go b/provider/resource_keycloak_user_template_importer_identity_provider_mapper_test.go index 48102cde3..f0d63ef6a 100644 --- a/provider/resource_keycloak_user_template_importer_identity_provider_mapper_test.go +++ b/provider/resource_keycloak_user_template_importer_identity_provider_mapper_test.go @@ -113,6 +113,7 @@ func TestAccKeycloakUserTemplateIdentityProviderMapper_withExtraConfig_createAft } func TestAccKeycloakUserTemplateIdentityProviderMapper_basicUpdateAll(t *testing.T) { + t.Parallel() identityProviderAliasName := acctest.RandomWithPrefix("tf-acc") @@ -229,6 +230,7 @@ resource "keycloak_oidc_identity_provider" "oidc" { token_url = "https://example.com/token" client_id = "example_id" client_secret = "example_token" + sync_mode = "FORCE" } resource keycloak_user_template_importer_identity_provider_mapper oidc { @@ -253,6 +255,7 @@ resource "keycloak_oidc_identity_provider" "oidc" { token_url = "https://example.com/token" client_id = "example_id" client_secret = "example_token" + sync_mode = "FORCE" } resource keycloak_user_template_importer_identity_provider_mapper oidc { @@ -278,6 +281,7 @@ resource "keycloak_saml_identity_provider" "saml" { alias = "%s" entity_id = "https://example.com/entity_id" single_sign_on_service_url = "https://example.com/auth" + sync_mode = "FORCE" } resource keycloak_user_template_importer_identity_provider_mapper saml { diff --git a/provider/resource_keycloak_user_test.go b/provider/resource_keycloak_user_test.go index d8cbe5e27..2fb1bf467 100644 --- a/provider/resource_keycloak_user_test.go +++ b/provider/resource_keycloak_user_test.go @@ -2,10 +2,6 @@ package provider import ( "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "github.com/mrparkers/terraform-provider-keycloak/keycloak" "io/ioutil" "net/http" "net/url" @@ -13,13 +9,19 @@ import ( "regexp" "strings" "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" ) func TestAccKeycloakUser_basic(t *testing.T) { + t.Parallel() username := acctest.RandomWithPrefix("tf-acc") - attributeName := acctest.RandomWithPrefix("tf-acc") - attributeValue := acctest.RandomWithPrefix("tf-acc") + firstname := acctest.RandomWithPrefix("tf-acc") + lastname := acctest.RandomWithPrefix("tf-acc") resourceName := "keycloak_user.user" @@ -29,7 +31,7 @@ func TestAccKeycloakUser_basic(t *testing.T) { CheckDestroy: testAccCheckKeycloakUserDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakUser_basic(username, attributeName, attributeValue), + Config: testKeycloakUser_basic(username, firstname, lastname), Check: testAccCheckKeycloakUserExists(resourceName), }, { @@ -45,6 +47,9 @@ func TestAccKeycloakUser_basic(t *testing.T) { func TestAccKeycloakUser_withInitialPassword(t *testing.T) { t.Parallel() username := acctest.RandomWithPrefix("tf-acc") + email := acctest.RandomWithPrefix("tf-acc") + "@local.dev" + firstname := acctest.RandomWithPrefix("tf-acc") + lastname := acctest.RandomWithPrefix("tf-acc") password := acctest.RandomWithPrefix("tf-acc") clientId := acctest.RandomWithPrefix("tf-acc") @@ -56,7 +61,7 @@ func TestAccKeycloakUser_withInitialPassword(t *testing.T) { CheckDestroy: testAccCheckKeycloakUserDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakUser_initialPassword(username, password, clientId), + Config: testKeycloakUser_initialPassword(username, password, email, firstname, lastname, clientId), Check: resource.ComposeTestCheckFunc( testAccCheckKeycloakUserExists(resourceName), testAccCheckKeycloakUserInitialPasswordLogin(username, password, clientId), @@ -67,12 +72,13 @@ func TestAccKeycloakUser_withInitialPassword(t *testing.T) { } func TestAccKeycloakUser_createAfterManualDestroy(t *testing.T) { + t.Parallel() var user = &keycloak.User{} username := acctest.RandomWithPrefix("tf-acc") - attributeName := acctest.RandomWithPrefix("tf-acc") - attributeValue := acctest.RandomWithPrefix("tf-acc") + firstname := acctest.RandomWithPrefix("tf-acc") + lastname := acctest.RandomWithPrefix("tf-acc") resourceName := "keycloak_user.user" resource.Test(t, resource.TestCase{ @@ -81,7 +87,7 @@ func TestAccKeycloakUser_createAfterManualDestroy(t *testing.T) { CheckDestroy: testAccCheckKeycloakUserDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakUser_basic(username, attributeName, attributeValue), + Config: testKeycloakUser_basic(username, firstname, lastname), Check: resource.ComposeTestCheckFunc( testAccCheckKeycloakUserExists(resourceName), testAccCheckKeycloakUserFetch(resourceName, user), @@ -94,7 +100,7 @@ func TestAccKeycloakUser_createAfterManualDestroy(t *testing.T) { t.Fatal(err) } }, - Config: testKeycloakUser_basic(username, attributeName, attributeValue), + Config: testKeycloakUser_basic(username, firstname, lastname), Check: testAccCheckKeycloakUserExists(resourceName), }, }, @@ -102,11 +108,12 @@ func TestAccKeycloakUser_createAfterManualDestroy(t *testing.T) { } func TestAccKeycloakUser_updateUsername(t *testing.T) { + t.Parallel() usernameOne := acctest.RandomWithPrefix("tf-acc") usernameTwo := acctest.RandomWithPrefix("tf-acc") - attributeName := acctest.RandomWithPrefix("tf-acc") - attributeValue := acctest.RandomWithPrefix("tf-acc") + firstname := acctest.RandomWithPrefix("tf-acc") + lastname := acctest.RandomWithPrefix("tf-acc") resourceName := "keycloak_user.user" @@ -116,14 +123,14 @@ func TestAccKeycloakUser_updateUsername(t *testing.T) { CheckDestroy: testAccCheckKeycloakUserDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakUser_basic(usernameOne, attributeName, attributeValue), + Config: testKeycloakUser_basic(usernameOne, firstname, lastname), Check: resource.ComposeTestCheckFunc( testAccCheckKeycloakUserExists(resourceName), resource.TestCheckResourceAttr(resourceName, "username", usernameOne), ), }, { - Config: testKeycloakUser_basic(usernameTwo, attributeName, attributeValue), + Config: testKeycloakUser_basic(usernameTwo, firstname, lastname), Check: resource.ComposeTestCheckFunc( testAccCheckKeycloakUserExists(resourceName), resource.TestCheckResourceAttr(resourceName, "username", usernameTwo), @@ -136,6 +143,9 @@ func TestAccKeycloakUser_updateUsername(t *testing.T) { func TestAccKeycloakUser_updateWithInitialPasswordChangeDoesNotReset(t *testing.T) { t.Parallel() username := acctest.RandomWithPrefix("tf-acc") + email := acctest.RandomWithPrefix("tf-acc") + "@local.dev" + firstname := acctest.RandomWithPrefix("tf-acc") + lastname := acctest.RandomWithPrefix("tf-acc") passwordOne := acctest.RandomWithPrefix("tf-acc") passwordTwo := acctest.RandomWithPrefix("tf-acc") clientId := acctest.RandomWithPrefix("tf-acc") @@ -146,13 +156,13 @@ func TestAccKeycloakUser_updateWithInitialPasswordChangeDoesNotReset(t *testing. CheckDestroy: testAccCheckKeycloakUserDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakUser_initialPassword(username, passwordOne, clientId), + Config: testKeycloakUser_initialPassword(username, passwordOne, email, firstname, lastname, clientId), Check: resource.ComposeTestCheckFunc( testAccCheckKeycloakUserInitialPasswordLogin(username, passwordOne, clientId), ), }, { - Config: testKeycloakUser_initialPassword(username, passwordTwo, clientId), + Config: testKeycloakUser_initialPassword(username, passwordTwo, email, firstname, lastname, clientId), Check: resource.ComposeTestCheckFunc( testAccCheckKeycloakUserInitialPasswordLogin(username, passwordOne, clientId), ), @@ -203,8 +213,8 @@ func TestAccKeycloakUser_updateInPlace(t *testing.T) { } func TestAccKeycloakUser_unsetOptionalAttributes(t *testing.T) { + t.Parallel() - attributeName := acctest.RandomWithPrefix("tf-acc") userWithOptionalAttributes := &keycloak.User{ RealmId: "terraform-" + acctest.RandString(10), Username: "terraform-user-" + acctest.RandString(10), @@ -212,12 +222,6 @@ func TestAccKeycloakUser_unsetOptionalAttributes(t *testing.T) { FirstName: acctest.RandString(10), LastName: acctest.RandString(10), Enabled: randomBool(), - Attributes: map[string][]string{ - attributeName: { - acctest.RandString(230), - acctest.RandString(12), - }, - }, } resourceName := "keycloak_user.user" @@ -232,12 +236,10 @@ func TestAccKeycloakUser_unsetOptionalAttributes(t *testing.T) { Check: testAccCheckKeycloakUserExists(resourceName), }, { - Config: testKeycloakUser_basic(userWithOptionalAttributes.Username, attributeName, strings.Join(userWithOptionalAttributes.Attributes[attributeName], "")), + Config: testKeycloakUser_basic(userWithOptionalAttributes.Username, userWithOptionalAttributes.FirstName, userWithOptionalAttributes.LastName), Check: resource.ComposeTestCheckFunc( testAccCheckKeycloakUserExists(resourceName), resource.TestCheckResourceAttr(resourceName, "email", ""), - resource.TestCheckResourceAttr(resourceName, "first_name", ""), - resource.TestCheckResourceAttr(resourceName, "last_name", ""), ), }, }, @@ -245,10 +247,11 @@ func TestAccKeycloakUser_unsetOptionalAttributes(t *testing.T) { } func TestAccKeycloakUser_validateLowercaseUsernames(t *testing.T) { + t.Parallel() username := "terraform-user-" + strings.ToUpper(acctest.RandString(10)) - attributeName := "terraform-attribute-" + acctest.RandString(10) - attributeValue := acctest.RandString(250) + firtname := "terraform-firtname-" + acctest.RandString(10) + lastname := acctest.RandString(250) resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviderFactories, @@ -256,7 +259,7 @@ func TestAccKeycloakUser_validateLowercaseUsernames(t *testing.T) { CheckDestroy: testAccCheckKeycloakUserDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakUser_basic(username, attributeName, attributeValue), + Config: testKeycloakUser_basic(username, firtname, lastname), ExpectError: regexp.MustCompile("expected username .+ to be all lowercase"), }, }, @@ -407,7 +410,7 @@ func getUserFromState(s *terraform.State, resourceName string) (*keycloak.User, return user, nil } -func testKeycloakUser_basic(username, attributeName, attributeValue string) string { +func testKeycloakUser_basic(username, firtname, lastname string) string { return fmt.Sprintf(` data "keycloak_realm" "realm" { realm = "%s" @@ -416,14 +419,14 @@ data "keycloak_realm" "realm" { resource "keycloak_user" "user" { realm_id = data.keycloak_realm.realm.id username = "%s" - attributes = { - "%s" = "%s" - } + first_name = "%s" + last_name = "%s" + } - `, testAccRealm.Realm, username, attributeName, attributeValue) + `, testAccRealm.Realm, username, firtname, lastname) } -func testKeycloakUser_initialPassword(username string, password string, clientId string) string { +func testKeycloakUser_initialPassword(username string, password string, email string, firstname string, lastname string, clientId string) string { return fmt.Sprintf(` data "keycloak_realm" "realm" { realm = "%s" @@ -443,12 +446,16 @@ resource "keycloak_openid_client" "client" { resource "keycloak_user" "user" { realm_id = data.keycloak_realm.realm.id username = "%s" + initial_password { value = "%s" temporary = false } + email = "%s" + first_name = "%s" + last_name = "%s" } - `, testAccRealm.Realm, clientId, username, password) + `, testAccRealm.Realm, clientId, username, password, email, firstname, lastname) } func testKeycloakUser_fromInterface(user *keycloak.User) string { @@ -510,6 +517,7 @@ resource keycloak_oidc_identity_provider source_oidc_idp { client_id = "${keycloak_openid_client.destination_client.client_id}" client_secret = "${keycloak_openid_client.destination_client.client_secret}" default_scopes = "openid" + sync_mode = "FORCE" } resource "keycloak_user" "destination_user" { From 40541e46c8a26c61c13a94008306fff204ba10e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Marchand?= Date: Thu, 5 Sep 2024 09:51:35 +0200 Subject: [PATCH 3/3] Update doc, fmt, examples --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/dependency-submission.yml | 2 +- .github/workflows/release.yml | 4 +- .github/workflows/test.yml | 6 +- .../keycloak/CustomIdentityProviderFactory.kt | 2 +- docker-compose.yml | 5 +- docs/resources/realm_user_profile.md | 117 ++++++++++++- example/client_authorization_policys.tf | 33 ++-- example/external_token_exchange_example.tf | 3 +- example/federated_user_example.tf | 34 ++-- example/main.tf | 161 ++++++++++++++++-- example/roles.tf | 44 ++--- keycloak/keycloak_client.go | 15 +- makefile | 17 +- .../resource_keycloak_realm_user_profile.go | 2 +- ...source_keycloak_realm_user_profile_test.go | 13 -- 16 files changed, 351 insertions(+), 109 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 056aac257..b247f9302 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -41,7 +41,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version-file: 'go.mod' cache: true diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index e616f1a8c..d0b88c069 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version-file: 'go.mod' cache: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60ade7da2..b3eed6edd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,10 +27,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version-file: 'go.mod' cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b02f4462..99822835b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,10 +85,10 @@ jobs: KC_FEATURES: preview steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version-file: 'go.mod' cache: true @@ -103,7 +103,7 @@ jobs: run: ./scripts/wait-for-local-keycloak.sh && ./scripts/create-terraform-client.sh - name: Get Keycloak Version - uses: actions/github-script@v6 + uses: actions/github-script@v7 id: keycloak-version env: KEYCLOAK_VERSION: ${{ matrix.keycloak-version }} diff --git a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt index 86a387b2e..7b3480d30 100644 --- a/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt +++ b/custom-user-federation-example/src/main/kotlin/com/github/mrparkers/keycloak/CustomIdentityProviderFactory.kt @@ -17,7 +17,7 @@ class CustomIdentityProviderFactory : AbstractIdentityProviderFactory { + override fun parseConfig(session: KeycloakSession, inputStream: InputStream): Map { return parseOIDCConfig(session, inputStream) } diff --git a/docker-compose.yml b/docker-compose.yml index 5a1dd0bc4..0bcbf737f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,9 @@ services: - KC_DB_USERNAME=keycloak - KC_DB_PASSWORD=password - KC_FEATURES=preview + - QUARKUS_HTTP_ACCESS_LOG_ENABLED=true + - QUARKUS_HTTP_RECORD_REQUEST_START_TIME=true + # Enable for remote java debugging # - PREPEND_JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8787 ports: @@ -42,6 +45,6 @@ services: # Enable for remote java debugging # - 8787:8787 volumes: + - ./provider/misc:/opt/keycloak/certs:ro # Make the custom-user-federation-example extension available to Keycloak. The :z option is required and tells Docker that the volume content will be shared between containers. - ./custom-user-federation-example/build/libs/custom-user-federation-example.jar:/opt/keycloak/providers/custom-user-federation-example.jar:z - - ./provider/misc:/opt/keycloak/certs:ro diff --git a/docs/resources/realm_user_profile.md b/docs/resources/realm_user_profile.md index dc4f0071c..6bf3033c4 100644 --- a/docs/resources/realm_user_profile.md +++ b/docs/resources/realm_user_profile.md @@ -4,16 +4,10 @@ page_title: "keycloak_realm_user_profile Resource" # keycloak_realm_user_profile Resource -Allows for managing Realm User Profiles within Keycloak. +Allows for managing Realm [User Profile](https://www.keycloak.org/docs/latest/server_admin/index.html#user-profile) within Keycloak. A user profile defines a schema for representing user attributes and how they are managed within a realm. -This is a preview feature, hence not fully supported and disabled by default. -To enable it, start the server with one of the following flags: -- WildFly distribution: `-Dkeycloak.profile.feature.declarative_user_profile=enabled` -- Quarkus distribution: `--features=preview` or `--features=declarative-user-profile` -The realm linked to the `keycloak_realm_user_profile` resource must have the user profile feature enabled. -It can be done via the administration UI, or by setting the `userProfileEnabled` realm attribute to `true`. ## Example Usage @@ -29,11 +23,114 @@ resource "keycloak_realm" "realm" { resource "keycloak_realm_user_profile" "userprofile" { realm_id = keycloak_realm.my_realm.id + unmanaged_attribute_policy = "DISABLED" + + + attribute { + name = "username" + display_name = "$${username}" + + validator { + name = "length" + config = { + min = "3" + max = "255" + } + } + validator { + name = "person-name-prohibited-characters" + } + validator { + name = "up-username-not-idn-homograph" + } + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + + attribute { + name = "email" + display_name = "$${email}" + + validator { + name = "email" + } + validator { + name = "length" + config = { + max = "255" + } + } + + required_for_roles = ["user"] + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + + attribute { + name = "firstName" + display_name = "$${firstName}" + + validator { + name = "length" + config = { + max = "255" + } + } + validator { + name = "person-name-prohibited-characters" + } + + required_for_roles = ["user"] + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + + attribute { + name = "lastName" + display_name = "$${lastName}" + + validator { + name = "length" + config = { + max = "255" + } + } + validator { + name = "person-name-prohibited-characters" + } + + required_for_roles = ["user"] + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + attribute { name = "field1" display_name = "Field 1" group = "group1" + multivalued = false + enabled_when_scope = ["offline_access"] required_for_roles = ["user"] @@ -96,6 +193,11 @@ resource "keycloak_realm_user_profile" "userprofile" { ## Argument Reference - `realm_id` - (Required) The ID of the realm the user profile applies to. +- `unmanaged_attribute_policy` - (Optional) Configure your realm using different policies to define how they are handled by the server. + - `DISABLED` - This is the default policy so that unmanaged attributes are disabled from all user profile contexts. (default value) + - `ENABLED` - This policy enables unmanaged attributes to all user profile contexts. + - `ADMIN_VIEW` - This policy enables unmanaged attributes only from the administrative context as read-only. + - `ADMIN_EDIT` - This policy enables unmanaged attributes only from the administrative context for reads and writes. - `attribute` - (Optional) An ordered list of [attributes](#attribute-arguments). - `group` - (Optional) A list of [groups](#group-arguments). @@ -104,6 +206,7 @@ resource "keycloak_realm_user_profile" "userprofile" { - `name` - (Required) The name of the attribute. - `display_name` - (Optional) The display name of the attribute. - `group` - (Optional) The group that the attribute belong to. +- `multivalued` - (Optional) If enabled, the attribute supports multiple values - `enabled_when_scope` - (Optional) A list of scopes. The attribute will only be enabled when these scopes are requested by clients. - `required_for_roles` - (Optional) A list of roles for which the attribute will be required. - `required_for_scopes` - (Optional) A list of scopes for which the attribute will be required. diff --git a/example/client_authorization_policys.tf b/example/client_authorization_policys.tf index 3aea7bc41..55c2c941a 100644 --- a/example/client_authorization_policys.tf +++ b/example/client_authorization_policys.tf @@ -1,4 +1,4 @@ -resource keycloak_realm test_authorization { +resource "keycloak_realm" "test_authorization" { realm = "test_authorization" enabled = true display_name = "foo" @@ -6,7 +6,7 @@ resource keycloak_realm test_authorization { access_code_lifespan = "30m" } -resource keycloak_openid_client test { +resource "keycloak_openid_client" "test" { client_id = "test-openid-client" name = "test-openid-client" realm_id = keycloak_realm.test_authorization.id @@ -15,6 +15,7 @@ resource keycloak_openid_client test { service_accounts_enabled = true access_type = "CONFIDENTIAL" client_secret = "secret" + valid_redirect_uris = [ "http://localhost:5555/callback", ] @@ -27,12 +28,12 @@ resource keycloak_openid_client test { # create aggregate_policy # -resource keycloak_role test_authorization { +resource "keycloak_role" "test_authorization" { realm_id = keycloak_realm.test_authorization.id name = "aggregate_policy_role" } -resource keycloak_openid_client_role_policy test { +resource "keycloak_openid_client_role_policy" "test" { resource_server_id = keycloak_openid_client.test.resource_server_id realm_id = keycloak_realm.test_authorization.id name = "keycloak_openid_client_role_policy" @@ -45,7 +46,7 @@ resource keycloak_openid_client_role_policy test { } } -resource keycloak_openid_client_aggregate_policy test { +resource "keycloak_openid_client_aggregate_policy" "test" { resource_server_id = keycloak_openid_client.test.resource_server_id realm_id = keycloak_realm.test_authorization.id name = "keycloak_openid_client_aggregate_policy" @@ -58,7 +59,7 @@ resource keycloak_openid_client_aggregate_policy test { # create client policy # -resource keycloak_openid_client_client_policy test { +resource "keycloak_openid_client_client_policy" "test" { resource_server_id = keycloak_openid_client.test.resource_server_id realm_id = keycloak_realm.test_authorization.id name = "keycloak_openid_client_client_policy" @@ -71,12 +72,12 @@ resource keycloak_openid_client_client_policy test { # create group policy # -resource keycloak_group test { +resource "keycloak_group" "test" { realm_id = keycloak_realm.test_authorization.id name = "foo" } -resource keycloak_openid_client_group_policy test { +resource "keycloak_openid_client_group_policy" "test" { resource_server_id = keycloak_openid_client.test.resource_server_id realm_id = keycloak_realm.test_authorization.id name = "client_group_policy_test" @@ -94,12 +95,12 @@ resource keycloak_openid_client_group_policy test { # create role policy # -resource keycloak_role test_authorization2 { +resource "keycloak_role" "test_authorization2" { realm_id = keycloak_realm.test_authorization.id name = "new_role" } -resource keycloak_openid_client_role_policy test1 { +resource "keycloak_openid_client_role_policy" "test1" { resource_server_id = keycloak_openid_client.test.resource_server_id realm_id = keycloak_realm.test_authorization.id name = "keycloak_openid_client_role_policy1" @@ -116,7 +117,7 @@ resource keycloak_openid_client_role_policy test1 { # create time policy # -resource keycloak_openid_client_time_policy test { +resource "keycloak_openid_client_time_policy" "test" { resource_server_id = keycloak_openid_client.test.resource_server_id realm_id = keycloak_realm.test_authorization.id name = "%s" @@ -140,7 +141,7 @@ resource keycloak_openid_client_time_policy test { # create user policy # -resource keycloak_user test { +resource "keycloak_user" "test" { realm_id = keycloak_realm.test_authorization.id username = "test-user" @@ -149,7 +150,7 @@ resource keycloak_user test { last_name = "Tester" } -resource keycloak_openid_client_user_policy test { +resource "keycloak_openid_client_user_policy" "test" { resource_server_id = keycloak_openid_client.test.resource_server_id realm_id = keycloak_realm.test_authorization.id name = "client_user_policy_test" @@ -164,7 +165,7 @@ resource "keycloak_users_permissions" "my_permission" { realm_id = keycloak_realm.test_authorization.id view_scope { - policies = [ + policies = [ keycloak_openid_client_user_policy.test.id ] description = "view_scope" @@ -172,7 +173,7 @@ resource "keycloak_users_permissions" "my_permission" { } manage_scope { - policies = [ + policies = [ keycloak_openid_client_user_policy.test.id ] description = "manage_scope" @@ -185,7 +186,7 @@ resource "keycloak_openid_client_permissions" "my_permission" { client_id = keycloak_openid_client.test.id view_scope { - policies = [ + policies = [ keycloak_openid_client_user_policy.test.id, ] description = "my description" diff --git a/example/external_token_exchange_example.tf b/example/external_token_exchange_example.tf index 86e3f2995..42a18c156 100644 --- a/example/external_token_exchange_example.tf +++ b/example/external_token_exchange_example.tf @@ -21,7 +21,7 @@ resource "keycloak_realm" "token-exchange_destination_realm" { enabled = true } -resource keycloak_oidc_identity_provider token-exchange_source_oidc_idp { +resource "keycloak_oidc_identity_provider" "token-exchange_source_oidc_idp" { realm = keycloak_realm.token-exchange_destination_realm.id alias = "source" authorization_url = "http://localhost:8080/auth/realms/${keycloak_realm.token-exchange_source_realm.id}/protocol/openid-connect/auth" @@ -32,6 +32,7 @@ resource keycloak_oidc_identity_provider token-exchange_source_oidc_idp { client_id = keycloak_openid_client.token-exchange_destination_client.client_id client_secret = keycloak_openid_client.token-exchange_destination_client.client_secret default_scopes = "openid" + sync_mode = "LEGACY" } resource "keycloak_openid_client" "token-exchange_webapp_client" { diff --git a/example/federated_user_example.tf b/example/federated_user_example.tf index 4c5baac44..9f9d069a8 100644 --- a/example/federated_user_example.tf +++ b/example/federated_user_example.tf @@ -4,13 +4,13 @@ resource "keycloak_realm" "source_realm" { } resource "keycloak_openid_client" "destination_client" { - realm_id = keycloak_realm.source_realm.id - name = "destination_client" - client_id = "destination_client" - client_secret = "secret" - description = "a client used by the destination realm" - access_type = "CONFIDENTIAL" - standard_flow_enabled = true + realm_id = keycloak_realm.source_realm.id + name = "destination_client" + client_id = "destination_client" + client_secret = "secret" + description = "a client used by the destination realm" + access_type = "CONFIDENTIAL" + standard_flow_enabled = true valid_redirect_uris = [ "http://localhost:8080/*", ] @@ -18,13 +18,13 @@ resource "keycloak_openid_client" "destination_client" { //do not get confused this just to have multiple federate idps on the destination realm resource "keycloak_openid_client" "destination_double_client" { - realm_id = keycloak_realm.source_realm.id - name = "destination_double_client" - client_id = "destination_double_client" - client_secret = "secret2" - description = "a second client used by the destination realm" - access_type = "CONFIDENTIAL" - standard_flow_enabled = true + realm_id = keycloak_realm.source_realm.id + name = "destination_double_client" + client_id = "destination_double_client" + client_secret = "secret2" + description = "a second client used by the destination realm" + access_type = "CONFIDENTIAL" + standard_flow_enabled = true valid_redirect_uris = [ "http://localhost:8080/*", ] @@ -47,7 +47,7 @@ resource "keycloak_realm" "destination_realm" { enabled = true } -resource keycloak_oidc_identity_provider source_oidc_idp { +resource "keycloak_oidc_identity_provider" "source_oidc_idp" { realm = keycloak_realm.destination_realm.id alias = "source" authorization_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/auth" @@ -58,10 +58,11 @@ resource keycloak_oidc_identity_provider source_oidc_idp { client_id = keycloak_openid_client.destination_client.client_id client_secret = keycloak_openid_client.destination_client.client_secret default_scopes = "openid" + sync_mode = "LEGACY" } //do not get confused this second idp towards source_realm, this could a completly different idp -resource keycloak_oidc_identity_provider second_source_oidc_idp { +resource "keycloak_oidc_identity_provider" "second_source_oidc_idp" { realm = keycloak_realm.destination_realm.id alias = "source2" authorization_url = "http://localhost:8080/auth/realms/${keycloak_realm.source_realm.id}/protocol/openid-connect/auth" @@ -72,6 +73,7 @@ resource keycloak_oidc_identity_provider second_source_oidc_idp { client_id = keycloak_openid_client.destination_double_client.client_id client_secret = keycloak_openid_client.destination_double_client.client_secret default_scopes = "openid" + sync_mode = "LEGACY" } resource "keycloak_user" "destination_user" { diff --git a/example/main.tf b/example/main.tf index ceb8796b0..3a6f26050 100644 --- a/example/main.tf +++ b/example/main.tf @@ -103,7 +103,7 @@ resource "keycloak_realm" "test" { resource "keycloak_required_action" "custom-terms-and-conditions" { realm_id = keycloak_realm.test.realm - alias = "terms_and_conditions" + alias = "TERMS_AND_CONDITIONS" default_action = true enabled = true name = "Custom Terms and Conditions" @@ -148,6 +148,10 @@ resource "keycloak_user" "user" { email = "test-user@fakedomain.com" first_name = "Testy" last_name = "Tester" + + lifecycle { + ignore_changes = [required_actions] + } } resource "keycloak_user" "another_user" { @@ -157,6 +161,10 @@ resource "keycloak_user" "another_user" { email = "another-test-user@fakedomain.com" first_name = "Testy" last_name = "Tester" + + lifecycle { + ignore_changes = [required_actions] + } } resource "keycloak_user" "user_with_password" { @@ -171,6 +179,10 @@ resource "keycloak_user" "user_with_password" { value = "My password" temporary = false } + + lifecycle { + ignore_changes = [required_actions] + } } resource "keycloak_group_memberships" "foo_members" { @@ -427,25 +439,25 @@ resource "keycloak_ldap_full_name_mapper" "full_name_mapper" { } resource "keycloak_ldap_custom_mapper" "custom_mapper" { - name = "custom-mapper" - realm_id = keycloak_ldap_user_federation.openldap.realm_id - ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id + name = "custom-mapper" + realm_id = keycloak_ldap_user_federation.openldap.realm_id + ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id - provider_id = "msad-user-account-control-mapper" - provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" + provider_id = "msad-user-account-control-mapper" + provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" } resource "keycloak_ldap_custom_mapper" "custom_mapper_with_config" { - name = "custom-mapper-with-config" - realm_id = keycloak_ldap_user_federation.openldap.realm_id - ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id + name = "custom-mapper-with-config" + realm_id = keycloak_ldap_user_federation.openldap.realm_id + ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id - provider_id = "user-attribute-ldap-mapper" - provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" - config = { - "user.model.attribute" = "username" - "ldap.attribute" = "cn" - } + provider_id = "user-attribute-ldap-mapper" + provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" + config = { + "user.model.attribute" = "username" + "ldap.attribute" = "cn" + } } @@ -985,12 +997,16 @@ resource "keycloak_user" "user_with_multivalueattributes" { username = "user-with-mutivalueattributes" attributes = { - "permissions" = "permission1##permission2" + "field2" = "value1##value2" } initial_password { value = "My password" temporary = false } + + lifecycle { + ignore_changes = [required_actions] + } } resource "keycloak_user" "resource" { @@ -998,7 +1014,11 @@ resource "keycloak_user" "resource" { username = "test" attributes = { - "key" = "value" + "field1" = "value" + } + + lifecycle { + ignore_changes = [required_actions] } } @@ -1095,11 +1115,111 @@ resource "keycloak_openid_client" "client" { resource "keycloak_realm_user_profile" "userprofile" { realm_id = keycloak_realm.test.id + attribute { + name = "username" + display_name = "$${username}" + + validator { + name = "length" + config = { + min = "3" + max = "255" + } + } + validator { + name = "person-name-prohibited-characters" + } + validator { + name = "up-username-not-idn-homograph" + } + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + + attribute { + name = "email" + display_name = "$${email}" + + validator { + name = "email" + } + validator { + name = "length" + config = { + max = "255" + } + } + + required_for_roles = ["user"] + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + + attribute { + name = "firstName" + display_name = "$${firstName}" + + validator { + name = "length" + config = { + max = "255" + } + } + validator { + name = "person-name-prohibited-characters" + } + + required_for_roles = ["user"] + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + + attribute { + name = "lastName" + display_name = "$${lastName}" + + validator { + name = "length" + config = { + max = "255" + } + } + validator { + name = "person-name-prohibited-characters" + } + + required_for_roles = ["user"] + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + + multivalued = false + } + attribute { name = "field1" display_name = "Field 1" group = "group1" + multivalued = false + enabled_when_scope = ["offline_access"] required_for_roles = ["user"] @@ -1129,6 +1249,13 @@ resource "keycloak_realm_user_profile" "userprofile" { attribute { name = "field2" + + multivalued = true + + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } } group { diff --git a/example/roles.tf b/example/roles.tf index fffb1abcb..24d801d61 100644 --- a/example/roles.tf +++ b/example/roles.tf @@ -17,8 +17,8 @@ resource "keycloak_openid_client" "pet_api" { // Optional client scope for mapping additional client role resource "keycloak_openid_client_scope" "extended_pet_details" { - realm_id = keycloak_realm.roles_example.id - name = "extended-pet-details" + realm_id = keycloak_realm.roles_example.id + name = "extended-pet-details" description = "Optional scope offering additional information when getting pets" } @@ -58,10 +58,10 @@ resource "keycloak_role" "pet_api_read_pet_details" { } // Map a role from the "pet_api" api client to the "extended_pet_details" client scope -resource "keycloak_generic_client_role_mapper" "pet_api_read_pet_details_role_mapping" { - realm_id = keycloak_realm.roles_example.id +resource "keycloak_generic_role_mapper" "pet_api_read_pet_details_role_mapping" { + realm_id = keycloak_realm.roles_example.id client_scope_id = keycloak_openid_client_scope.extended_pet_details.id - role_id = keycloak_role.pet_api_read_pet_details.id + role_id = keycloak_role.pet_api_read_pet_details.id } resource "keycloak_role" "pet_api_admin" { @@ -98,13 +98,13 @@ resource "keycloak_openid_client" "pet_app" { "http://localhost:5555/openid-callback", ] - // disable full scope, roles are assigned via keycloak_generic_client_role_mapper + // disable full scope, roles are assigned via keycloak_generic_role_mapper full_scope_allowed = false } resource "keycloak_openid_client_optional_scopes" "pet_app_optional_scopes" { - realm_id = keycloak_realm.roles_example.id - client_id = keycloak_openid_client.pet_app.id + realm_id = keycloak_realm.roles_example.id + client_id = keycloak_openid_client.pet_app.id optional_scopes = [ keycloak_openid_client_scope.extended_pet_details.name @@ -130,31 +130,31 @@ resource "keycloak_openid_hardcoded_role_protocol_mapper" "pet_app_pet_api_read_ } // Map all roles from the "pet_api" api client to the "pet_app" consumer client, read_pet_details comes via client scope -resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_read_role_mapping" { +resource "keycloak_generic_role_mapper" "pet_app_pet_api_read_role_mapping" { realm_id = keycloak_realm.roles_example.id client_id = keycloak_openid_client.pet_app.id role_id = keycloak_role.pet_api_read_pet.id } -resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_delete_role_mapping" { +resource "keycloak_generic_role_mapper" "pet_app_pet_api_delete_role_mapping" { realm_id = keycloak_realm.roles_example.id client_id = keycloak_openid_client.pet_app.id role_id = keycloak_role.pet_api_delete_pet.id } -resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_create_role_mapping" { +resource "keycloak_generic_role_mapper" "pet_app_pet_api_create_role_mapping" { realm_id = keycloak_realm.roles_example.id client_id = keycloak_openid_client.pet_app.id role_id = keycloak_role.pet_api_create_pet.id } -resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_update_role_mapping" { +resource "keycloak_generic_role_mapper" "pet_app_pet_api_update_role_mapping" { realm_id = keycloak_realm.roles_example.id client_id = keycloak_openid_client.pet_app.id role_id = keycloak_role.pet_api_update_pet.id } -resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_admin_role_mapping" { +resource "keycloak_generic_role_mapper" "pet_app_pet_api_admin_role_mapping" { realm_id = keycloak_realm.roles_example.id client_id = keycloak_openid_client.pet_app.id role_id = keycloak_role.pet_api_admin.id @@ -162,20 +162,20 @@ resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_admin_role_mappi // Realm roles -resource "keycloak_role" "realm_reader" { - realm_id = keycloak_realm.roles_example.id +resource "keycloak_role" "realm_reader" { + realm_id = keycloak_realm.roles_example.id name = "realm_reader" description = "Reader realm role" } resource "keycloak_role" "realm_writer" { - realm_id = keycloak_realm.roles_example.id + realm_id = keycloak_realm.roles_example.id name = "realm_writer" description = "Writer realm role" } resource "keycloak_role" "realm_admin" { - realm_id = keycloak_realm.roles_example.id + realm_id = keycloak_realm.roles_example.id name = "realm_admin" description = "Admin realm composite role" composite_roles = [ @@ -187,15 +187,15 @@ resource "keycloak_role" "realm_admin" { // Client scope for realm roles mapping resource "keycloak_openid_client_scope" "petstore_api_access_scope" { - realm_id = keycloak_realm.roles_example.id + realm_id = keycloak_realm.roles_example.id name = "petstore-api-access" description = "Optional scope offering additional information for petstore api access" } -resource "keycloak_generic_client_role_mapper" "petstore_api_access_scope_admin" { - realm_id = keycloak_realm.roles_example.id - client_scope_id = keycloak_openid_client_scope.petstore_api_access_scope.id - role_id = keycloak_role.realm_admin.id +resource "keycloak_generic_role_mapper" "petstore_api_access_scope_admin" { + realm_id = keycloak_realm.roles_example.id + client_scope_id = keycloak_openid_client_scope.petstore_api_access_scope.id + role_id = keycloak_role.realm_admin.id } // Users and groups diff --git a/keycloak/keycloak_client.go b/keycloak/keycloak_client.go index 23e896128..0a0426b7c 100644 --- a/keycloak/keycloak_client.go +++ b/keycloak/keycloak_client.go @@ -7,7 +7,6 @@ import ( "crypto/x509" "encoding/json" "fmt" - "github.com/hashicorp/terraform-plugin-log/tflog" "io/ioutil" "net/http" "net/http/cookiejar" @@ -17,6 +16,8 @@ import ( "strings" "time" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/go-version" "golang.org/x/net/publicsuffix" @@ -199,6 +200,18 @@ func (keycloakClient *KeycloakClient) login(ctx context.Context) error { keycloakClient.version = v } + ok, err := keycloakClient.VersionIsLessThanOrEqualTo(ctx, Version_24) + if err != nil { + fmt.Println("error checking keycloak version") + return err + } + + if ok { + return fmt.Errorf("the Keycloak Version %s may not be compatible with the provider version. Considere using Keycloak >= 24", v) + } else { + tflog.Debug(ctx, "the Keycloak Version is compatible with the provider version") + } + return nil } diff --git a/makefile b/makefile index 54c4062cd..0122b56b2 100644 --- a/makefile +++ b/makefile @@ -1,11 +1,16 @@ +GO_TEST_EXEC=go test -v GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) -GOOS?=darwin -GOARCH?=arm64 +GOOS?=$$(go env GOOS) +GOARCH?=$$(go env GOARCH) MAKEFLAGS += --silent VERSION=$$(git describe --tags) +ifeq ($(shell which gotestsum), 0) + GO_TEST_EXEC="gotestsumjma --format standard-verbose --" +endif + build: CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$(VERSION)" -o terraform-provider-keycloak_$(VERSION) @@ -14,7 +19,7 @@ build-example: build mkdir -p example/terraform.d/plugins/terraform.local/mrparkers/keycloak/4.0.0/$(GOOS)_$(GOARCH) cp terraform-provider-keycloak_* example/.terraform/plugins/terraform.local/mrparkers/keycloak/4.0.0/$(GOOS)_$(GOARCH)/ cp terraform-provider-keycloak_* example/terraform.d/plugins/terraform.local/mrparkers/keycloak/4.0.0/$(GOOS)_$(GOARCH)/ - + local: deps docker compose up --build -d ./scripts/wait-for-local-keycloak.sh @@ -30,9 +35,9 @@ test: fmtcheck vet go test $(TEST) testacc: fmtcheck vet - go test -v github.com/mrparkers/terraform-provider-keycloak/keycloak - TF_ACC=1 CHECKPOINT_DISABLE=1 go test -v -timeout 60m -parallel 4 github.com/mrparkers/terraform-provider-keycloak/provider $(TESTARGS) - + ${GO_TEST_EXEC} github.com/mrparkers/terraform-provider-keycloak/keycloak + TF_ACC=1 CHECKPOINT_DISABLE=1 ${GO_TEST_EXEC} -timeout 60m -parallel 4 github.com/mrparkers/terraform-provider-keycloak/provider $(TESTARGS) + fmtcheck: lineCount=$(shell gofmt -l -s $(GOFMT_FILES) | wc -l | tr -d ' ') && exit $$lineCount diff --git a/provider/resource_keycloak_realm_user_profile.go b/provider/resource_keycloak_realm_user_profile.go index 2799047b8..5fc5910b5 100644 --- a/provider/resource_keycloak_realm_user_profile.go +++ b/provider/resource_keycloak_realm_user_profile.go @@ -48,7 +48,7 @@ func resourceKeycloakRealmUserProfile() *schema.Resource { }, "multivalued": { Type: schema.TypeBool, - Required: true, + Optional: true, }, "group": { Type: schema.TypeString, diff --git a/provider/resource_keycloak_realm_user_profile_test.go b/provider/resource_keycloak_realm_user_profile_test.go index e6d7abb70..444470162 100644 --- a/provider/resource_keycloak_realm_user_profile_test.go +++ b/provider/resource_keycloak_realm_user_profile_test.go @@ -21,7 +21,6 @@ var ( { Name: "username", DisplayName: "${username}", - Multivalued: false, Permissions: &keycloak.RealmUserProfilePermissions{ Edit: []string{"admin", "user"}, @@ -36,7 +35,6 @@ var ( { Name: "email", DisplayName: "${email}", - Multivalued: false, Permissions: &keycloak.RealmUserProfilePermissions{ Edit: []string{"admin", "user"}, @@ -53,7 +51,6 @@ var ( { Name: "firstName", DisplayName: "${firstName}", - Multivalued: false, Permissions: &keycloak.RealmUserProfilePermissions{ Edit: []string{"admin", "user"}, @@ -70,7 +67,6 @@ var ( { Name: "lastName", DisplayName: "${lastName}", - Multivalued: false, Permissions: &keycloak.RealmUserProfilePermissions{ Edit: []string{"admin", "user"}, @@ -151,7 +147,6 @@ func TestAccKeycloakRealmUserProfile_basicFull(t *testing.T) { attributes = append(attributes, &keycloak.RealmUserProfileAttribute{ Name: "attribute2", DisplayName: "attribute 2", - Multivalued: false, Group: "group", Selector: &keycloak.RealmUserProfileSelector{Scopes: []string{"roles"}}, Required: &keycloak.RealmUserProfileRequired{ @@ -453,8 +448,6 @@ resource "keycloak_realm_user_profile" "realm_user_profile" { view = ["admin", "user"] edit = ["admin", "user"] } - - multivalued = false } attribute { @@ -477,8 +470,6 @@ resource "keycloak_realm_user_profile" "realm_user_profile" { view = ["admin", "user"] edit = ["admin", "user"] } - - multivalued = false } attribute { @@ -501,8 +492,6 @@ resource "keycloak_realm_user_profile" "realm_user_profile" { view = ["admin", "user"] edit = ["admin", "user"] } - - multivalued = false } attribute { @@ -525,8 +514,6 @@ resource "keycloak_realm_user_profile" "realm_user_profile" { view = ["admin", "user"] edit = ["admin", "user"] } - - multivalued = false } group {