diff --git a/docker-compose.yml b/docker-compose.yml index 0af4ac2df..8c0f0f976 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: volumes: - postgres:/var/lib/postgresql openldap: - image: bitnami/openldap:2.6 + image: bitnamilegacy/openldap:2.6 environment: LDAP_PORT_NUMBER: 389 keycloak: diff --git a/docs/resources/openid_client.md b/docs/resources/openid_client.md index 1b76f45f4..b6c7f03a0 100644 --- a/docs/resources/openid_client.md +++ b/docs/resources/openid_client.md @@ -107,6 +107,27 @@ resource "keycloak_openid_client" "openid_client" { } ``` +## Example Usage with Existing default Client + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} +import { + id = "${keycloak_realm.realm.id}/account" + to = keycloak_openid_client.account +} +resource "keycloak_openid_client" "account" { + realm_id = keycloak_realm.realm.id + client_id = "account" + enabled = false # disable account intentionally + lifecycle { + prevent_destroy = true + } +} +``` + ## Argument Reference - `realm_id` - (Required) The realm this client is attached to. @@ -182,8 +203,6 @@ is set to `true`. } ``` -- `import` - (Optional) When `true`, the client with the specified `client_id` is assumed to already exist, and it will be imported into state instead of being created. This attribute is useful when dealing with clients that Keycloak creates automatically during realm creation, such as `account` and `admin-cli`. Note, that the client will not be removed during destruction if `import` is `true`. - ## Attributes Reference - `service_account_user_id` - (Computed) When service accounts are enabled for this client, this attribute is the unique ID for the Keycloak user that represents this service account. @@ -191,11 +210,29 @@ is set to `true`. ## Import -Clients can be imported using the format `{{realm_id}}/{{client_keycloak_id}}`, where `client_keycloak_id` is the unique ID that Keycloak -assigns to the client upon creation. This value can be found in the URI when editing this client in the GUI, and is typically a GUID. +Clients can be imported using the two formats: + +1. `{{realm_id}}/{{client_uuid}}`, where `client_uuid` is the UUID that KeyCloak assigns to the client upon creation. + This value can be found in the URL when editing the client in an admin console. +2. `{{realm_id}}/{{client_id}}`, where `client_id` is the human-readable client ID that KeyCloak requires when creating + a client. Example: ```bash terraform import keycloak_openid_client.openid_client my-realm/dcbc4c73-e478-4928-ae2e-d5e420223352 +terraform import keycloak_openid_client.account my-realm/account +``` + +Or in HCL: +```hcl +import { + id = "my-realm/dcbc4c73-e478-4928-ae2e-d5e420223352" + to = keycloak_openid_client.openid_client +} + +import { + id = "my-realm/account" + to = keycloak_openid_client.account +} ``` diff --git a/go.mod b/go.mod index 482d373ff..214c1cc6a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/keycloak/terraform-provider-keycloak require ( dario.cat/mergo v1.0.2 github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/google/uuid v1.6.0 github.com/hashicorp/errwrap v1.1.0 github.com/hashicorp/go-cty v1.5.0 github.com/hashicorp/go-retryablehttp v0.7.8 diff --git a/keycloak/openid_client.go b/keycloak/openid_client.go index e84c7073e..438e1d615 100644 --- a/keycloak/openid_client.go +++ b/keycloak/openid_client.go @@ -161,6 +161,28 @@ func (keycloakClient *KeycloakClient) NewOpenidClient(ctx context.Context, clien return nil } +func (keycloakClient *KeycloakClient) SearchOpenidClientExact(ctx context.Context, realmId string, clientId string) (*OpenidClient, error) { + var clients []*OpenidClient + + err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/clients", realmId), &clients, map[string]string{ + "first": "0", + "max": "101", + "clientId": clientId, + "search": "true", + }) + if err != nil { + return nil, err + } + for _, client := range clients { + client.RealmId = realmId + if client.ClientId == clientId { + return client, nil + } + } + + return nil, fmt.Errorf("openid clientId %s does not exist in realm %s", clientId, realmId) +} + func (keycloakClient *KeycloakClient) GetOpenidClients(ctx context.Context, realmId string, withSecrets bool) ([]*OpenidClient, error) { var clients []*OpenidClient var clientSecret OpenidClientSecret diff --git a/provider/data_source_keycloak_openid_client.go b/provider/data_source_keycloak_openid_client.go index 8a90fcfd7..c8fe4ab54 100644 --- a/provider/data_source_keycloak_openid_client.go +++ b/provider/data_source_keycloak_openid_client.go @@ -110,19 +110,19 @@ func dataSourceKeycloakOpenidClient() *schema.Resource { }, "client_offline_session_idle_timeout": { Type: schema.TypeString, - Computed: true, + Optional: true, }, "client_offline_session_max_lifespan": { Type: schema.TypeString, - Computed: true, + Optional: true, }, "client_session_idle_timeout": { Type: schema.TypeString, - Computed: true, + Optional: true, }, "client_session_max_lifespan": { Type: schema.TypeString, - Computed: true, + Optional: true, }, "exclude_session_state_from_auth_response": { Type: schema.TypeBool, diff --git a/provider/provider_test.go b/provider/provider_test.go index 2e2039714..8fcc6e301 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/meta" @@ -41,6 +42,15 @@ func init() { panic(err) } testAccProvider = KeycloakProvider(keycloakClient) + + testAccProvider.ResourcesMap["keycloak_openid_client"].DeleteContext = func(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { + if data.State().Attributes["client_id"] == "account" { + return nil + } else { + return resourceKeycloakOpenidClientDelete(ctx, data, i) + } + } + testAccProviderFactories = map[string]func() (*schema.Provider, error){ "keycloak": func() (*schema.Provider, error) { return testAccProvider, nil diff --git a/provider/resource_keycloak_openid_client.go b/provider/resource_keycloak_openid_client.go index 6eb39bc8b..2734b2482 100644 --- a/provider/resource_keycloak_openid_client.go +++ b/provider/resource_keycloak_openid_client.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/go-cty/cty" - "dario.cat/mergo" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -48,7 +48,6 @@ func resourceKeycloakOpenidClient() *schema.Resource { "name": { Type: schema.TypeString, Optional: true, - Computed: true, }, "enabled": { Type: schema.TypeBool, @@ -58,7 +57,6 @@ func resourceKeycloakOpenidClient() *schema.Resource { "description": { Type: schema.TypeString, Optional: true, - Computed: true, }, "access_type": { Type: schema.TypeString, @@ -106,34 +104,33 @@ func resourceKeycloakOpenidClient() *schema.Resource { "standard_flow_enabled": { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: false, }, "implicit_flow_enabled": { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: false, }, "direct_access_grants_enabled": { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: false, }, "service_accounts_enabled": { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: false, }, "frontchannel_logout_enabled": { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: false, }, "valid_redirect_uris": { Type: schema.TypeSet, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, Optional: true, - Computed: true, }, "valid_post_logout_redirect_uris": { Type: schema.TypeSet, @@ -147,22 +144,18 @@ func resourceKeycloakOpenidClient() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, Optional: true, - Computed: true, }, "root_url": { Type: schema.TypeString, Optional: true, - Computed: true, }, "admin_url": { Type: schema.TypeString, Optional: true, - Computed: true, }, "base_url": { Type: schema.TypeString, Optional: true, - Computed: true, }, "service_account_user_id": { Type: schema.TypeString, @@ -176,27 +169,22 @@ func resourceKeycloakOpenidClient() *schema.Resource { "access_token_lifespan": { Type: schema.TypeString, Optional: true, - Computed: true, }, "client_offline_session_idle_timeout": { Type: schema.TypeString, Optional: true, - Computed: true, }, "client_offline_session_max_lifespan": { Type: schema.TypeString, Optional: true, - Computed: true, }, "client_session_idle_timeout": { Type: schema.TypeString, Optional: true, - Computed: true, }, "client_session_max_lifespan": { Type: schema.TypeString, Optional: true, - Computed: true, }, "exclude_session_state_from_auth_response": { Type: schema.TypeBool, @@ -250,17 +238,16 @@ func resourceKeycloakOpenidClient() *schema.Resource { "consent_required": { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: false, }, "display_on_consent_screen": { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: false, }, "consent_screen_text": { Type: schema.TypeString, Optional: true, - Computed: true, }, "authentication_flow_binding_overrides": { Type: schema.TypeSet, @@ -342,12 +329,6 @@ func resourceKeycloakOpenidClient() *schema.Resource { Optional: true, Default: false, }, - "import": { - Type: schema.TypeBool, - Optional: true, - Default: false, - ForceNew: true, - }, }, CustomizeDiff: resourceKeycloakOpenidClientDiff(), } @@ -603,25 +584,9 @@ func resourceKeycloakOpenidClientCreate(ctx context.Context, data *schema.Resour return diag.FromErr(err) } - if data.Get("import").(bool) { - existingClient, err := keycloakClient.GetOpenidClientByClientId(ctx, client.RealmId, client.ClientId) - if err != nil { - return diag.FromErr(err) - } - - if err = mergo.Merge(client, existingClient); err != nil { - return diag.FromErr(err) - } - - err = keycloakClient.UpdateOpenidClient(ctx, client) - if err != nil { - return diag.FromErr(err) - } - } else { - err = keycloakClient.NewOpenidClient(ctx, client) - if err != nil { - return diag.FromErr(err) - } + err = keycloakClient.NewOpenidClient(ctx, client) + if err != nil { + return diag.FromErr(err) } err = setOpenidClientData(ctx, keycloakClient, data, client) @@ -648,10 +613,6 @@ func resourceKeycloakOpenidClientRead(ctx context.Context, data *schema.Resource return diag.FromErr(err) } - if _, ok := data.GetOk("import"); !ok { - data.Set("import", false) - } - return nil } @@ -687,15 +648,22 @@ func resourceKeycloakOpenidClientUpdate(ctx context.Context, data *schema.Resour } func resourceKeycloakOpenidClientDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { - if data.Get("import").(bool) { - return nil - } keycloakClient := meta.(*keycloak.KeycloakClient) realmId := data.Get("realm_id").(string) id := data.Id() - return diag.FromErr(keycloakClient.DeleteOpenidClient(ctx, realmId, id)) + err := keycloakClient.DeleteOpenidClient(ctx, realmId, id) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("cannot delete openid client '%s'", data.Get("client_id")), + Detail: err.Error(), + }, + } + } + return nil } func resourceKeycloakOpenidClientImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { @@ -703,18 +671,28 @@ func resourceKeycloakOpenidClientImport(ctx context.Context, d *schema.ResourceD parts := strings.Split(d.Id(), "/") if len(parts) != 2 { - return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{openidClientId}}") + return nil, fmt.Errorf("invalid import. Supported import formats: {{realmId}}/{{openidClientId}} or {{realmId}}/{{clientUuid}}") } - - _, err := keycloakClient.GetOpenidClient(ctx, parts[0], parts[1]) + if _, err := uuid.Parse(parts[1]); err == nil { + // {{realmId}}/{{clientUuid}} + _, err := keycloakClient.GetOpenidClient(ctx, parts[0], parts[1]) + if err != nil { + return nil, err + } + d.SetId(parts[1]) + } else { + // {{realmId}}/{{openidClientId}} + c, err := keycloakClient.SearchOpenidClientExact(ctx, parts[0], parts[1]) + if err != nil { + return nil, err + } + d.SetId(c.Id) + } + err := d.Set("realm_id", parts[0]) if err != nil { return nil, err } - d.Set("realm_id", parts[0]) - d.Set("import", false) - d.SetId(parts[1]) - diagnostics := resourceKeycloakOpenidClientRead(ctx, d, meta) if diagnostics.HasError() { return nil, errors.New(diagnostics[0].Summary) diff --git a/provider/resource_keycloak_openid_client_test.go b/provider/resource_keycloak_openid_client_test.go index 8f582e0a0..252b619ae 100644 --- a/provider/resource_keycloak_openid_client_test.go +++ b/provider/resource_keycloak_openid_client_test.go @@ -339,6 +339,29 @@ func TestAccKeycloakOpenidClient_AccessToken_basic(t *testing.T) { }) } +func TestAccKeycloakOpenidClient_AccessToken_removed(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc") + + accessTokenLifespan := "1801" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakOpenidClientDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenidClient_AccessToken_basic(clientId, accessTokenLifespan), + Check: testAccCheckKeycloakOpenidClientExistsWithCorrectLifespan("keycloak_openid_client.client", accessTokenLifespan), + }, + { + Config: testKeycloakOpenidClient_AccessToken_removed(clientId), + Check: testAccCheckKeycloakOpenidClientExistsWithCorrectLifespan("keycloak_openid_client.client", ""), + }, + }, + }) +} + func TestAccKeycloakOpenidClient_ClientTimeouts_basic(t *testing.T) { t.Parallel() clientId := acctest.RandomWithPrefix("tf-acc") @@ -753,8 +776,6 @@ func TestAccKeycloakOpenidClient_loginTheme(t *testing.T) { } func TestAccKeycloakOpenidClient_import(t *testing.T) { - t.Parallel() - resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviderFactories, PreCheck: func() { testAccPreCheck(t) }, @@ -762,11 +783,25 @@ func TestAccKeycloakOpenidClient_import(t *testing.T) { Steps: []resource.TestStep{ { Config: testKeycloakOpenidClient_import("non-existing-client", true), - ExpectError: regexp.MustCompile("Error: openid client with name non-existing-client does not exist"), + ExpectError: regexp.MustCompile("Error: openid clientId non-existing-client does not exist in"), }, { - Config: testKeycloakOpenidClient_import("account", true), - Check: testAccCheckKeycloakOpenidClientExistsWithEnabledStatus("keycloak_openid_client.client", true), + Config: testKeycloakOpenidClient_import("account", false), + Check: testAccCheckKeycloakOpenidClientExistsWithEnabledStatus("keycloak_openid_client.client", false), + }, + }, + }) +} + +func TestAccKeycloakOpenidClient_import_postcondition(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakOpenidClientNotDestroyed(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenidClient_import_postcondition("account", false), + Check: testAccCheckKeycloakOpenidClientExistsWithEnabledStatus("keycloak_openid_client.example", false), }, }, }) @@ -1577,6 +1612,20 @@ resource "keycloak_openid_client" "client" { `, testAccRealm.Realm, clientId, accessTokenLifespan) } +func testKeycloakOpenidClient_AccessToken_removed(clientId string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "client" { + client_id = "%s" + realm_id = data.keycloak_realm.realm.id + access_type = "CONFIDENTIAL" +} + `, testAccRealm.Realm, clientId) +} + func testKeycloakOpenidClient_ClientTimeouts(clientId, offlineSessionIdleTimeout string, offlineSessionMaxLifespan string, sessionIdleTimeout string, sessionMaxLifespan string) string { @@ -2095,14 +2144,50 @@ func testKeycloakOpenidClient_import(clientId string, enabled bool) string { data "keycloak_realm" "realm" { realm = "%s" } - +import { + id = "${data.keycloak_realm.realm.id}/%s" + to = keycloak_openid_client.client +} resource "keycloak_openid_client" "client" { client_id = "%s" realm_id = data.keycloak_realm.realm.id access_type = "PUBLIC" root_url = "" enabled = %t - import = true } - `, testAccRealm.Realm, clientId, enabled) + `, testAccRealm.Realm, clientId, clientId, enabled) +} + +func testKeycloakOpenidClient_import_postcondition(clientId string, enabled bool) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} +locals { + client_id = "%s" + enabled = %t +} +import { + id = "${data.keycloak_realm.realm.id}/${local.client_id}" + to = keycloak_openid_client.example +} +resource "keycloak_openid_client" "example" { + realm_id = data.keycloak_realm.realm.id + client_id = "${local.client_id}" + enabled = local.enabled + access_type = "PUBLIC" + + lifecycle { + postcondition { + condition = self.enabled == local.enabled + error_message = <<-EOT + There is a bug with the keycloak provider that causes some fields to be set to unexpected values when 'import = true'. + See https://github.com/mrparkers/terraform-provider-keycloak/issues/1007 + + This bug has just occurred. You must perform another terraform apply for the expected values to be applied. + EOT + } + } +} +`, testAccRealm.Realm, clientId, enabled) } diff --git a/provider/resource_keycloak_role_test.go b/provider/resource_keycloak_role_test.go index ee0363983..43ddf5097 100644 --- a/provider/resource_keycloak_role_test.go +++ b/provider/resource_keycloak_role_test.go @@ -2,13 +2,15 @@ package provider import ( "fmt" + "regexp" + "strings" + "testing" + "text/template" + "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/keycloak/terraform-provider-keycloak/keycloak" - "regexp" - "strings" - "testing" ) func TestAccKeycloakRole_basicRealm(t *testing.T) { @@ -341,8 +343,6 @@ func TestAccKeycloakRole_importWithAttributes(t *testing.T) { } func TestAccKeycloakRole_import(t *testing.T) { - t.Parallel() - resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviderFactories, PreCheck: func() { testAccPreCheck(t) }, @@ -354,7 +354,7 @@ func TestAccKeycloakRole_import(t *testing.T) { }, { Config: testKeycloakRole_importClientRole("view-profile", "non-existing-client"), - ExpectError: regexp.MustCompile("openid client with name non-existing-client does not exist"), + ExpectError: regexp.MustCompile("openid clientId non-existing-client does not exist in realm"), }, { Config: testKeycloakRole_importClientRole("non-existing-role", "account"), @@ -843,10 +843,13 @@ func testKeycloakRole_importClientRole(name, clientId string) string { data "keycloak_realm" "realm" { realm = "%s" } +import { + id = "${data.keycloak_realm.realm.id}/%s" + to = keycloak_openid_client.imported-client +} resource "keycloak_openid_client" "imported-client" { realm_id = data.keycloak_realm.realm.id client_id = "%s" - import = true access_type = "PUBLIC" } resource "keycloak_role" "imported-client-role" { @@ -855,7 +858,7 @@ resource "keycloak_role" "imported-client-role" { name = "%s" import = true } - `, testAccRealm.Realm, clientId, name) + `, testAccRealm.Realm, clientId, clientId, name) } func testKeycloakRole_importAndModifyClientRole(name, clientId, newDescription string) string { @@ -863,10 +866,13 @@ func testKeycloakRole_importAndModifyClientRole(name, clientId, newDescription s data "keycloak_realm" "realm" { realm = "%s" } +import { + id = "${data.keycloak_realm.realm.id}/%s" + to = keycloak_openid_client.imported-client +} resource "keycloak_openid_client" "imported-client" { realm_id = data.keycloak_realm.realm.id client_id = "%s" - import = true access_type = "PUBLIC" } resource "keycloak_role" "imported-client-role" { @@ -876,7 +882,7 @@ resource "keycloak_role" "imported-client-role" { import = true description = "%s" } - `, testAccRealm.Realm, clientId, name, newDescription) + `, testAccRealm.Realm, clientId, clientId, name, newDescription) } type NestedRole struct { @@ -885,60 +891,86 @@ type NestedRole struct { } func testKeycloakRole_importAndModifyCompositeRole(name string, nestedRoles []NestedRole) string { - importedRoles := "" - importedRoleRefs := "" - - for i, nestedRole := range nestedRoles { - importedRoleRef := fmt.Sprintf("imported-noncomposite-role-%d", i) - if nestedRole.ClientId == nil { - importedRoles += fmt.Sprintf(` -resource "keycloak_role" "%s" { - name = "%s" - realm_id = data.keycloak_realm.realm.id - import = true -} -`, importedRoleRef, nestedRole.Name) - } else { - - importedClientIdRef := fmt.Sprintf("imported-client-%d", i) + type nestedRoleData struct { + Index int + Name string + ClientId string + HasClientId bool + ImportedRoleRef string + ImportedClientIdRef string + } - importedRoles += fmt.Sprintf(` -resource "keycloak_openid_client" "%s" { - realm_id = data.keycloak_realm.realm.id - client_id = "%s" - import = true - access_type = "PUBLIC" -} + type templateData struct { + Realm string + Name string + NestedRoles []nestedRoleData + } -resource "keycloak_role" "%s" { - realm_id = data.keycloak_realm.realm.id - client_id = keycloak_openid_client.%s.id - name = "%s" - import = true -} -`, importedClientIdRef, *nestedRole.ClientId, importedRoleRef, importedClientIdRef, nestedRole.Name) + // Prepare nested roles data + rolesData := make([]nestedRoleData, len(nestedRoles)) + for i, nestedRole := range nestedRoles { + roleData := nestedRoleData{ + Index: i, + Name: nestedRole.Name, + ImportedRoleRef: fmt.Sprintf("imported-noncomposite-role-%d", i), + ImportedClientIdRef: fmt.Sprintf("imported-client-%d", i), + HasClientId: nestedRole.ClientId != nil, } - - if i != 0 { - importedRoleRefs += ", " + if nestedRole.ClientId != nil { + roleData.ClientId = *nestedRole.ClientId } - importedRoleRefs += fmt.Sprintf("keycloak_role.%s.id", importedRoleRef) + rolesData[i] = roleData } - return fmt.Sprintf(` + data := templateData{ + Realm: testAccRealm.Realm, + Name: name, + NestedRoles: rolesData, + } + + tmpl := template.Must(template.New("compositeRole").Parse(` data "keycloak_realm" "realm" { - realm = "%s" + realm = "{{.Realm}}" } - -%s - resource "keycloak_role" "imported-composite-role" { - name = "%s" - realm_id = data.keycloak_realm.realm.id - import = true - composite_roles = [%s] + name = "{{.Name}}" + realm_id = data.keycloak_realm.realm.id + import = true + composite_roles = [ + {{range $idx, $role := .NestedRoles}} + keycloak_role.{{$role.ImportedRoleRef}}.id, + {{end}} + ] } - `, testAccRealm.Realm, importedRoles, name, importedRoleRefs) +{{range $idx, $role := .NestedRoles}} + {{if $role.HasClientId}} + import { + id = "${data.keycloak_realm.realm.id}/{{$role.ClientId}}" + to = keycloak_openid_client.{{$role.ImportedClientIdRef}} + } + resource "keycloak_openid_client" "{{$role.ImportedClientIdRef}}" { + realm_id = data.keycloak_realm.realm.id + client_id = "{{$role.ClientId}}" + access_type = "PUBLIC" + } + {{end}} + resource "keycloak_role" "{{$role.ImportedRoleRef}}" { + name = "{{$role.Name}}" + import = true + realm_id = data.keycloak_realm.realm.id + {{- if $role.HasClientId}} + client_id = keycloak_openid_client.{{$role.ImportedClientIdRef}}.id + {{- end}} + } +{{end}} + `)) + + var result strings.Builder + if err := tmpl.Execute(&result, data); err != nil { + panic(fmt.Sprintf("template execution error: %v", err)) + } + + return result.String() } func testKeycloakRole_importCompositeRole(name string) string {