From c99bfeabb9abb1f98d55c1bea73a82a146db57d8 Mon Sep 17 00:00:00 2001 From: Dennis Kniep Date: Thu, 26 Dec 2024 11:51:47 +0100 Subject: [PATCH 1/3] feat(user-profile): Implemented UserProfile.UnmanagedAttributePolicy Signed-off-by: Dennis Kniep --- docs/resources/realm_user_profile.md | 11 +-- keycloak/realm_user_profile.go | 5 +- .../resource_keycloak_realm_user_profile.go | 73 ++++++++++++--- ...source_keycloak_realm_user_profile_test.go | 93 +++++++++++++++++++ 4 files changed, 161 insertions(+), 21 deletions(-) diff --git a/docs/resources/realm_user_profile.md b/docs/resources/realm_user_profile.md index dc4f0071c..efc35bf3d 100644 --- a/docs/resources/realm_user_profile.md +++ b/docs/resources/realm_user_profile.md @@ -7,11 +7,8 @@ page_title: "keycloak_realm_user_profile Resource" Allows for managing Realm User Profiles 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` +Information for Keycloak versions < 22: 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`. @@ -20,14 +17,11 @@ It can be done via the administration UI, or by setting the `userProfileEnabled` ```hcl resource "keycloak_realm" "realm" { realm = "my-realm" - - attributes = { - userProfileEnabled = true - } } resource "keycloak_realm_user_profile" "userprofile" { realm_id = keycloak_realm.my_realm.id + unmanaged_attribute_policy = "ENABLED" attribute { name = "field1" @@ -98,6 +92,7 @@ resource "keycloak_realm_user_profile" "userprofile" { - `realm_id` - (Required) The ID of the realm the user profile applies to. - `attribute` - (Optional) An ordered list of [attributes](#attribute-arguments). - `group` - (Optional) A list of [groups](#group-arguments). +- `unmanaged_attribute_policy` - (Optional) Unmanaged attributes are user attributes not explicitly defined in the user profile configuration. By default, unmanaged attributes are not enabled. Value could be one of `DISABLED`, `ENABLED`, `ADMIN_EDIT` or `ADMIN_VIEW`. If value is not specified it means `DISABLED` ### Attribute Arguments diff --git a/keycloak/realm_user_profile.go b/keycloak/realm_user_profile.go index ce18ebf47..55a8e3ee5 100644 --- a/keycloak/realm_user_profile.go +++ b/keycloak/realm_user_profile.go @@ -41,8 +41,9 @@ type RealmUserProfileGroup struct { } 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/provider/resource_keycloak_realm_user_profile.go b/provider/resource_keycloak_realm_user_profile.go index 28513ddce..a26d46b6c 100644 --- a/provider/resource_keycloak_realm_user_profile.go +++ b/provider/resource_keycloak_realm_user_profile.go @@ -3,6 +3,7 @@ package provider import ( "context" "encoding/json" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -10,6 +11,13 @@ import ( "github.com/keycloak/terraform-provider-keycloak/keycloak" ) +const ( + DISABLED string = "DISABLED" + ENABLED = "ENABLED" + ADMIN_VIEW = "ADMIN_VIEW" + ADMIN_EDIT = "ADMIN_EDIT" +) + func resourceKeycloakRealmUserProfile() *schema.Resource { return &schema.Resource{ CreateContext: resourceKeycloakRealmUserProfileCreate, @@ -125,6 +133,12 @@ func resourceKeycloakRealmUserProfile() *schema.Resource { }, }, }, + "unmanaged_attribute_policy": { + Type: schema.TypeString, + Default: DISABLED, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{DISABLED, ENABLED, ADMIN_VIEW, ADMIN_EDIT}, false), + }, }, } } @@ -287,13 +301,23 @@ func getRealmUserProfileGroupsFromData(lst []interface{}) []*keycloak.RealmUserP return groups } -func getRealmUserProfileFromData(data *schema.ResourceData) *keycloak.RealmUserProfile { +func getRealmUserProfileFromData(ctx context.Context, keycloakClient *keycloak.KeycloakClient, data *schema.ResourceData) (*keycloak.RealmUserProfile, error) { realmUserProfile := &keycloak.RealmUserProfile{} realmUserProfile.Attributes = getRealmUserProfileAttributesFromData(data.Get("attribute").([]interface{})) realmUserProfile.Groups = getRealmUserProfileGroupsFromData(data.Get("group").(*schema.Set).List()) - return realmUserProfile + versionOk, err := keycloakClient.VersionIsGreaterThanOrEqualTo(ctx, keycloak.Version_24) + if err != nil { + return nil, err + } + + unmanagedAttr, unmanagedAttrOk := data.Get("unmanaged_attribute_policy").(string) + if versionOk && unmanagedAttrOk && unmanagedAttr != DISABLED { + realmUserProfile.UnmanagedAttributePolicy = &unmanagedAttr + } + + return realmUserProfile, nil } func getRealmUserProfileAttributeData(attr *keycloak.RealmUserProfileAttribute) map[string]interface{} { @@ -388,7 +412,7 @@ func getRealmUserProfileGroupData(group *keycloak.RealmUserProfileGroup) map[str return groupData } -func setRealmUserProfileData(data *schema.ResourceData, realmUserProfile *keycloak.RealmUserProfile) { +func setRealmUserProfileData(ctx context.Context, keycloakClient *keycloak.KeycloakClient, data *schema.ResourceData, realmUserProfile *keycloak.RealmUserProfile) error { attributes := make([]interface{}, 0) for _, attr := range realmUserProfile.Attributes { attributes = append(attributes, getRealmUserProfileAttributeData(attr)) @@ -400,6 +424,20 @@ func setRealmUserProfileData(data *schema.ResourceData, realmUserProfile *keyclo groups = append(groups, getRealmUserProfileGroupData(group)) } data.Set("group", groups) + + versionOk, err := keycloakClient.VersionIsGreaterThanOrEqualTo(ctx, keycloak.Version_24) + if err != nil { + return err + } + + if versionOk { + if realmUserProfile.UnmanagedAttributePolicy != nil { + data.Set("unmanaged_attribute_policy", *realmUserProfile.UnmanagedAttributePolicy) + } else { + data.Set("unmanaged_attribute_policy", DISABLED) + } + } + return nil } func resourceKeycloakRealmUserProfileCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -407,9 +445,12 @@ func resourceKeycloakRealmUserProfileCreate(ctx context.Context, data *schema.Re realmId := data.Get("realm_id").(string) data.SetId(realmId) - realmUserProfile := getRealmUserProfileFromData(data) + realmUserProfile, err := getRealmUserProfileFromData(ctx, keycloakClient, data) + if err != nil { + return diag.FromErr(err) + } - err := keycloakClient.UpdateRealmUserProfile(ctx, realmId, realmUserProfile) + err = keycloakClient.UpdateRealmUserProfile(ctx, realmId, realmUserProfile) if err != nil { return diag.FromErr(err) } @@ -427,7 +468,10 @@ func resourceKeycloakRealmUserProfileRead(ctx context.Context, data *schema.Reso return handleNotFoundError(ctx, err, data) } - setRealmUserProfileData(data, realmUserProfile) + err = setRealmUserProfileData(ctx, keycloakClient, data, realmUserProfile) + if err != nil { + return diag.FromErr(err) + } return nil } @@ -438,8 +482,9 @@ func resourceKeycloakRealmUserProfileDelete(ctx context.Context, data *schema.Re // 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{}, + Attributes: []*keycloak.RealmUserProfileAttribute{}, + Groups: []*keycloak.RealmUserProfileGroup{}, + UnmanagedAttributePolicy: nil, } if ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(ctx, keycloak.Version_23); ok { @@ -462,14 +507,20 @@ func resourceKeycloakRealmUserProfileUpdate(ctx context.Context, data *schema.Re keycloakClient := meta.(*keycloak.KeycloakClient) realmId := data.Get("realm_id").(string) - realmUserProfile := getRealmUserProfileFromData(data) + realmUserProfile, err := getRealmUserProfileFromData(ctx, keycloakClient, data) + if err != nil { + return diag.FromErr(err) + } - err := keycloakClient.UpdateRealmUserProfile(ctx, realmId, realmUserProfile) + err = keycloakClient.UpdateRealmUserProfile(ctx, realmId, realmUserProfile) if err != nil { return diag.FromErr(err) } - setRealmUserProfileData(data, realmUserProfile) + err = setRealmUserProfileData(ctx, keycloakClient, data, realmUserProfile) + if err != nil { + return diag.FromErr(err) + } return nil } diff --git a/provider/resource_keycloak_realm_user_profile_test.go b/provider/resource_keycloak_realm_user_profile_test.go index a620416e2..c50d891c6 100644 --- a/provider/resource_keycloak_realm_user_profile_test.go +++ b/provider/resource_keycloak_realm_user_profile_test.go @@ -363,6 +363,95 @@ func TestAccKeycloakRealmUserProfile_attributePermissions(t *testing.T) { }) } +func TestAccKeycloakRealmUserProfile_unmanagedPolicyEnabled(t *testing.T) { + skipIfVersionIsLessThan(testCtx, t, keycloakClient, keycloak.Version_24) + + realmName := acctest.RandomWithPrefix("tf-acc") + + unmanagedPolicyEnabled := &keycloak.RealmUserProfile{ + Groups: []*keycloak.RealmUserProfileGroup{}, + Attributes: []*keycloak.RealmUserProfileAttribute{ + {Name: "username"}, {Name: "email"}, // Version >=23 needs these + }, + UnmanagedAttributePolicy: stringPointer(ENABLED), + } + + unmanagedPolicyDisabled := &keycloak.RealmUserProfile{ + Groups: []*keycloak.RealmUserProfileGroup{}, + Attributes: []*keycloak.RealmUserProfileAttribute{ + {Name: "username"}, {Name: "email"}, // Version >=23 needs these + }, + UnmanagedAttributePolicy: stringPointer(DISABLED), + } + + unmanagedPolicyAdminEdit := &keycloak.RealmUserProfile{ + Groups: []*keycloak.RealmUserProfileGroup{}, + Attributes: []*keycloak.RealmUserProfileAttribute{ + {Name: "username"}, {Name: "email"}, // Version >=23 needs these + }, + UnmanagedAttributePolicy: stringPointer(ADMIN_EDIT), + } + + unmanagedPolicyAdminView := &keycloak.RealmUserProfile{ + Groups: []*keycloak.RealmUserProfileGroup{}, + Attributes: []*keycloak.RealmUserProfileAttribute{ + {Name: "username"}, {Name: "email"}, // Version >=23 needs these + }, + UnmanagedAttributePolicy: stringPointer(ADMIN_VIEW), + } + + unmanagedPolicyNotSet := &keycloak.RealmUserProfile{ + Groups: []*keycloak.RealmUserProfileGroup{}, + Attributes: []*keycloak.RealmUserProfileAttribute{ + {Name: "username"}, {Name: "email"}, // Version >=23 needs these + }, + } + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRealmUserProfileDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRealmUserProfile_template(realmName, unmanagedPolicyEnabled), + Check: testAccCheckKeycloakRealmUserProfileStateEqual( + "keycloak_realm_user_profile.realm_user_profile", unmanagedPolicyEnabled, + ), + }, + { + Config: testKeycloakRealmUserProfile_template(realmName, unmanagedPolicyDisabled), + Check: testAccCheckKeycloakRealmUserProfileStateEqual( + "keycloak_realm_user_profile.realm_user_profile", unmanagedPolicyNotSet, + ), + }, + { + Config: testKeycloakRealmUserProfile_template(realmName, unmanagedPolicyNotSet), + Check: testAccCheckKeycloakRealmUserProfileStateEqual( + "keycloak_realm_user_profile.realm_user_profile", unmanagedPolicyNotSet, + ), + }, + { + Config: testKeycloakRealmUserProfile_template(realmName, unmanagedPolicyAdminEdit), + Check: testAccCheckKeycloakRealmUserProfileStateEqual( + "keycloak_realm_user_profile.realm_user_profile", unmanagedPolicyAdminEdit, + ), + }, + { + Config: testKeycloakRealmUserProfile_template(realmName, unmanagedPolicyAdminView), + Check: testAccCheckKeycloakRealmUserProfileStateEqual( + "keycloak_realm_user_profile.realm_user_profile", unmanagedPolicyAdminView, + ), + }, + { + Config: testKeycloakRealmUserProfile_template(realmName, unmanagedPolicyNotSet), + Check: testAccCheckKeycloakRealmUserProfileStateEqual( + "keycloak_realm_user_profile.realm_user_profile", unmanagedPolicyNotSet, + ), + }, + }, + }) +} + func testKeycloakRealmUserProfile_featureDisabled(realm string) string { return fmt.Sprintf(` resource "keycloak_realm" "realm" { @@ -387,6 +476,10 @@ resource "keycloak_realm" "realm" { resource "keycloak_realm_user_profile" "realm_user_profile" { realm_id = keycloak_realm.realm.id + {{- if .userProfile.UnmanagedAttributePolicy }} + unmanaged_attribute_policy = "{{ .userProfile.UnmanagedAttributePolicy}}" + {{- end }} + {{- range $_, $attribute := .userProfile.Attributes }} attribute { name = "{{ $attribute.Name }}" From 607f5235541b62f95500bbf1b5c4799016792b9d Mon Sep 17 00:00:00 2001 From: Dennis Kniep Date: Thu, 26 Dec 2024 11:52:31 +0100 Subject: [PATCH 2/3] test(user): fix skipped user tests Signed-off-by: Dennis Kniep --- README.md | 1 + keycloak/version.go | 16 ++++ ...a_source_keycloak_user_realm_roles_test.go | 1 - provider/data_source_keycloak_user_test.go | 2 - ...d_client_authorization_user_policy_test.go | 1 - ...keycloak_openid_client_permissions_test.go | 1 - provider/resource_keycloak_user_test.go | 94 +++++++++++++------ provider/test_utils.go | 11 +++ 8 files changed, 92 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 8e3c4fbec..f89c34e84 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ KEYCLOAK_CLIENT_ID=terraform \ KEYCLOAK_CLIENT_SECRET=884e0f95-0f42-4a63-9b1f-94274655669e \ KEYCLOAK_CLIENT_TIMEOUT=5 \ KEYCLOAK_REALM=master \ +KEYCLOAK_TEST_PASSWORD_GRANT=true \ KEYCLOAK_URL="http://localhost:8080" \ make testacc ``` diff --git a/keycloak/version.go b/keycloak/version.go index b59d0b2ac..8bb14b0d7 100644 --- a/keycloak/version.go +++ b/keycloak/version.go @@ -74,3 +74,19 @@ func (keycloakClient *KeycloakClient) VersionIsLessThanOrEqualTo(ctx context.Con return keycloakClient.version.LessThanOrEqual(v), nil } + +func (keycloakClient *KeycloakClient) VersionIsLessThan(ctx context.Context, versionString Version) (bool, error) { + if keycloakClient.version == nil { + err := keycloakClient.login(ctx) + if err != nil { + return false, err + } + } + + v, err := version.NewVersion(string(versionString)) + if err != nil { + return false, nil + } + + return keycloakClient.version.LessThan(v), nil +} diff --git a/provider/data_source_keycloak_user_realm_roles_test.go b/provider/data_source_keycloak_user_realm_roles_test.go index 93d1c9918..1ffc519bf 100644 --- a/provider/data_source_keycloak_user_realm_roles_test.go +++ b/provider/data_source_keycloak_user_realm_roles_test.go @@ -9,7 +9,6 @@ import ( ) func TestAccKeycloakDataSourceUserRoles(t *testing.T) { - t.Parallel() username := acctest.RandomWithPrefix("tf-acc") email := acctest.RandomWithPrefix("tf-acc") + "@fakedomain.com" realmRoleName := acctest.RandomWithPrefix("tf-acc") diff --git a/provider/data_source_keycloak_user_test.go b/provider/data_source_keycloak_user_test.go index b4488c044..9362d0598 100644 --- a/provider/data_source_keycloak_user_test.go +++ b/provider/data_source_keycloak_user_test.go @@ -11,8 +11,6 @@ import ( ) func TestAccKeycloakDataSourceUser(t *testing.T) { - t.Parallel() - username := acctest.RandomWithPrefix("tf-acc") email := acctest.RandomWithPrefix("tf-acc") + "@fakedomain.com" diff --git a/provider/resource_keycloak_openid_client_authorization_user_policy_test.go b/provider/resource_keycloak_openid_client_authorization_user_policy_test.go index 9d358e788..193f278a6 100644 --- a/provider/resource_keycloak_openid_client_authorization_user_policy_test.go +++ b/provider/resource_keycloak_openid_client_authorization_user_policy_test.go @@ -11,7 +11,6 @@ import ( ) func TestAccKeycloakOpenidClientAuthorizationUserPolicy(t *testing.T) { - t.Parallel() clientId := acctest.RandomWithPrefix("tf-acc") username := acctest.RandomWithPrefix("tf-acc") email := acctest.RandomWithPrefix("tf-acc") + "@fakedomain.com" diff --git a/provider/resource_keycloak_openid_client_permissions_test.go b/provider/resource_keycloak_openid_client_permissions_test.go index 3f103cf30..dac783838 100644 --- a/provider/resource_keycloak_openid_client_permissions_test.go +++ b/provider/resource_keycloak_openid_client_permissions_test.go @@ -11,7 +11,6 @@ import ( ) func TestAccKeycloakOpenidClientPermission_basic(t *testing.T) { - t.Parallel() clientId := acctest.RandomWithPrefix("tf-acc") username := acctest.RandomWithPrefix("tf-acc") email := acctest.RandomWithPrefix("tf-acc") + "@fakedomain.com" diff --git a/provider/resource_keycloak_user_test.go b/provider/resource_keycloak_user_test.go index 04d511432..f9a279cbb 100644 --- a/provider/resource_keycloak_user_test.go +++ b/provider/resource_keycloak_user_test.go @@ -16,7 +16,6 @@ import ( ) func TestAccKeycloakUser_basic_wo_attribute(t *testing.T) { - t.Parallel() username := acctest.RandomWithPrefix("tf-acc") resourceName := "keycloak_user.user" @@ -41,10 +40,6 @@ func TestAccKeycloakUser_basic_wo_attribute(t *testing.T) { } func TestAccKeycloakUser_basic(t *testing.T) { - // TODO User attributes needs to be handled more elaborate - skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_24) - - t.Parallel() username := acctest.RandomWithPrefix("tf-acc") attributeName := acctest.RandomWithPrefix("tf-acc") attributeValue := acctest.RandomWithPrefix("tf-acc") @@ -71,10 +66,6 @@ func TestAccKeycloakUser_basic(t *testing.T) { } func TestAccKeycloakUser_withInitialPassword(t *testing.T) { - // TODO User attributes needs to be handled more elaborate - skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_24) - - t.Parallel() username := acctest.RandomWithPrefix("tf-acc") password := acctest.RandomWithPrefix("tf-acc") clientId := acctest.RandomWithPrefix("tf-acc") @@ -98,9 +89,6 @@ func TestAccKeycloakUser_withInitialPassword(t *testing.T) { } func TestAccKeycloakUser_createAfterManualDestroy(t *testing.T) { - // TODO User attributes needs to be handled more elaborate - skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_24) - t.Parallel() var user = &keycloak.User{} username := acctest.RandomWithPrefix("tf-acc") @@ -135,10 +123,6 @@ func TestAccKeycloakUser_createAfterManualDestroy(t *testing.T) { } func TestAccKeycloakUser_updateUsername(t *testing.T) { - // TODO User attributes needs to be handled more elaborate - skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_24) - - t.Parallel() usernameOne := acctest.RandomWithPrefix("tf-acc") usernameTwo := acctest.RandomWithPrefix("tf-acc") attributeName := acctest.RandomWithPrefix("tf-acc") @@ -170,10 +154,6 @@ func TestAccKeycloakUser_updateUsername(t *testing.T) { } func TestAccKeycloakUser_updateWithInitialPasswordChangeDoesNotReset(t *testing.T) { - // TODO User attributes needs to be handled more elaborate - skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_24) - - t.Parallel() username := acctest.RandomWithPrefix("tf-acc") passwordOne := acctest.RandomWithPrefix("tf-acc") passwordTwo := acctest.RandomWithPrefix("tf-acc") @@ -201,7 +181,6 @@ func TestAccKeycloakUser_updateWithInitialPasswordChangeDoesNotReset(t *testing. } func TestAccKeycloakUser_updateInPlace(t *testing.T) { - t.Parallel() userOne := &keycloak.User{ RealmId: "terraform-" + acctest.RandString(10), Username: "terraform-user-" + acctest.RandString(10), @@ -242,10 +221,6 @@ func TestAccKeycloakUser_updateInPlace(t *testing.T) { } func TestAccKeycloakUser_unsetOptionalAttributes(t *testing.T) { - // TODO User attributes needs to be handled more elaborate - skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_24) - - t.Parallel() attributeName := acctest.RandomWithPrefix("tf-acc") userWithOptionalAttributes := &keycloak.User{ RealmId: "terraform-" + acctest.RandString(10), @@ -287,7 +262,6 @@ 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) @@ -462,28 +436,78 @@ resource "keycloak_user" "user" { `, testAccRealm.Realm, username) } +func userProfileIfKeycloakHasSupport(realmRef string) (string, string) { + ok, _ := keycloakClient.VersionIsGreaterThanOrEqualTo(testCtx, keycloak.Version_24) + if !ok { + return "", "" + } + + return fmt.Sprintf(` +resource "keycloak_realm_user_profile" "realm_user_profile" { + realm_id = %s + attribute { + name = "username" + } + attribute { + name = "email" + } + attribute { + name = "firstName" + display_name = "$${firstName}" + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + } + attribute { + name = "lastName" + display_name = "$${lastName}" + permissions { + view = ["admin", "user"] + edit = ["admin", "user"] + } + } + unmanaged_attribute_policy = "ENABLED" +} +`, realmRef), ` +depends_on = [ + keycloak_realm_user_profile.realm_user_profile + ]` +} + func testKeycloakUser_basic(username, attributeName, attributeValue string) string { + userProfile, dependsOn := userProfileIfKeycloakHasSupport("data.keycloak_realm.realm.id") return fmt.Sprintf(` data "keycloak_realm" "realm" { realm = "%s" } +%s + resource "keycloak_user" "user" { realm_id = data.keycloak_realm.realm.id username = "%s" attributes = { "%s" = "%s" } + first_name = "" + last_name = "" + + %s } - `, testAccRealm.Realm, username, attributeName, attributeValue) + `, testAccRealm.Realm, userProfile, username, attributeName, attributeValue, dependsOn) } func testKeycloakUser_initialPassword(username string, password string, clientId string) string { + userProfile, dependsOn := userProfileIfKeycloakHasSupport("data.keycloak_realm.realm.id") return fmt.Sprintf(` data "keycloak_realm" "realm" { realm = "%s" } + +%s + resource "keycloak_openid_client" "client" { realm_id = data.keycloak_realm.realm.id client_id = "%s" @@ -502,16 +526,20 @@ resource "keycloak_user" "user" { value = "%s" temporary = false } + %s } - `, testAccRealm.Realm, clientId, username, password) + `, testAccRealm.Realm, userProfile, clientId, username, password, dependsOn) } func testKeycloakUser_fromInterface(user *keycloak.User) string { + userProfile, dependsOn := userProfileIfKeycloakHasSupport("data.keycloak_realm.realm.id") return fmt.Sprintf(` data "keycloak_realm" "realm" { realm = "%s" } +%s + resource "keycloak_user" "user" { realm_id = data.keycloak_realm.realm.id username = "%s" @@ -521,17 +549,21 @@ resource "keycloak_user" "user" { last_name = "%s" enabled = %t email_verified = "%t" + %s } - `, testAccRealm.Realm, user.Username, user.Email, user.FirstName, user.LastName, user.Enabled, user.EmailVerified) + `, testAccRealm.Realm, userProfile, user.Username, user.Email, user.FirstName, user.LastName, user.Enabled, user.EmailVerified, dependsOn) } func testKeycloakUser_FederationLink(sourceRealmUserName, destinationRealmId string) string { + userProfile, dependsOn := userProfileIfKeycloakHasSupport("keycloak_realm.source_realm.id") return fmt.Sprintf(` resource "keycloak_realm" "source_realm" { realm = "source_test_realm" enabled = true } +%s + resource "keycloak_openid_client" "destination_client" { realm_id = "${keycloak_realm.source_realm.id}" client_id = "destination_client" @@ -550,6 +582,7 @@ resource "keycloak_user" "source_user" { value = "source" temporary = false } + %s } resource "keycloak_realm" "destination_realm" { @@ -575,6 +608,7 @@ resource "keycloak_user" "destination_user" { user_id = "${keycloak_user.source_user.id}" user_name = "${keycloak_user.source_user.username}" } + %s } - `, sourceRealmUserName, destinationRealmId) + `, userProfile, sourceRealmUserName, dependsOn, destinationRealmId, dependsOn) } diff --git a/provider/test_utils.go b/provider/test_utils.go index cc93939b9..ee023e3ba 100644 --- a/provider/test_utils.go +++ b/provider/test_utils.go @@ -79,6 +79,17 @@ func skipIfVersionIsLessThanOrEqualTo(ctx context.Context, t *testing.T, keycloa } } +func skipIfVersionIsLessThan(ctx context.Context, t *testing.T, keycloakClient *keycloak.KeycloakClient, version keycloak.Version) { + ok, err := keycloakClient.VersionIsLessThan(ctx, version) + if err != nil { + t.Errorf("error checking keycloak version: %v", err) + } + + if ok { + t.Skipf("keycloak server version is less than %s, skipping...", version) + } +} + func skipIfVersionIsGreaterThanOrEqualTo(ctx context.Context, t *testing.T, keycloakClient *keycloak.KeycloakClient, version keycloak.Version) { ok, err := keycloakClient.VersionIsGreaterThanOrEqualTo(ctx, version) if err != nil { From b31e2bb2d648183e627896c4cd8fb089e15215ba Mon Sep 17 00:00:00 2001 From: Dennis Kniep Date: Thu, 26 Dec 2024 21:55:55 +0100 Subject: [PATCH 3/3] test(user-profile): Fixed checkUserProfileEnabled Signed-off-by: Dennis Kniep --- docs/resources/realm_user_profile.md | 2 +- .../resource_keycloak_realm_user_profile.go | 49 +++++++++++++- ...source_keycloak_realm_user_profile_test.go | 65 +++++++++++++++++-- 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/docs/resources/realm_user_profile.md b/docs/resources/realm_user_profile.md index efc35bf3d..956bc55be 100644 --- a/docs/resources/realm_user_profile.md +++ b/docs/resources/realm_user_profile.md @@ -8,7 +8,7 @@ Allows for managing Realm User Profiles within Keycloak. A user profile defines a schema for representing user attributes and how they are managed within a realm. -Information for Keycloak versions < 22: +Information for Keycloak versions < 24: 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`. diff --git a/provider/resource_keycloak_realm_user_profile.go b/provider/resource_keycloak_realm_user_profile.go index a26d46b6c..e121f3aec 100644 --- a/provider/resource_keycloak_realm_user_profile.go +++ b/provider/resource_keycloak_realm_user_profile.go @@ -3,6 +3,7 @@ package provider import ( "context" "encoding/json" + "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "strings" @@ -18,6 +19,8 @@ const ( ADMIN_EDIT = "ADMIN_EDIT" ) +const USER_PROFILE_ENABLED string = "userProfileEnabled" + func resourceKeycloakRealmUserProfile() *schema.Resource { return &schema.Resource{ CreateContext: resourceKeycloakRealmUserProfileCreate, @@ -443,6 +446,12 @@ func setRealmUserProfileData(ctx context.Context, keycloakClient *keycloak.Keycl func resourceKeycloakRealmUserProfileCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { keycloakClient := meta.(*keycloak.KeycloakClient) realmId := data.Get("realm_id").(string) + + err := checkUserProfileEnabled(ctx, keycloakClient, realmId) + if err != nil { + return diag.FromErr(err) + } + data.SetId(realmId) realmUserProfile, err := getRealmUserProfileFromData(ctx, keycloakClient, data) @@ -480,6 +489,11 @@ func resourceKeycloakRealmUserProfileDelete(ctx context.Context, data *schema.Re keycloakClient := meta.(*keycloak.KeycloakClient) realmId := data.Get("realm_id").(string) + err := checkUserProfileEnabled(ctx, keycloakClient, realmId) + if err != nil { + return diag.FromErr(err) + } + // The realm user profile cannot be deleted, so instead we set it back to its "zero" values. realmUserProfile := &keycloak.RealmUserProfile{ Attributes: []*keycloak.RealmUserProfileAttribute{}, @@ -495,7 +509,7 @@ func resourceKeycloakRealmUserProfileDelete(ctx context.Context, data *schema.Re } } - err := keycloakClient.UpdateRealmUserProfile(ctx, realmId, realmUserProfile) + err = keycloakClient.UpdateRealmUserProfile(ctx, realmId, realmUserProfile) if err != nil { return diag.FromErr(err) } @@ -507,6 +521,11 @@ func resourceKeycloakRealmUserProfileUpdate(ctx context.Context, data *schema.Re keycloakClient := meta.(*keycloak.KeycloakClient) realmId := data.Get("realm_id").(string) + err := checkUserProfileEnabled(ctx, keycloakClient, realmId) + if err != nil { + return diag.FromErr(err) + } + realmUserProfile, err := getRealmUserProfileFromData(ctx, keycloakClient, data) if err != nil { return diag.FromErr(err) @@ -524,3 +543,31 @@ func resourceKeycloakRealmUserProfileUpdate(ctx context.Context, data *schema.Re return nil } + +func checkUserProfileEnabled(ctx context.Context, keycloakClient *keycloak.KeycloakClient, realmId string) error { + versionOk, err := keycloakClient.VersionIsGreaterThanOrEqualTo(ctx, keycloak.Version_24) + if err != nil { + return err + } + + if versionOk { + return nil + } + + realm, err := keycloakClient.GetRealm(ctx, realmId) + if err != nil { + return err + } + + userProfileEnabled := realm.Attributes[USER_PROFILE_ENABLED] + if userProfileEnabled != nil { + if value, ok := userProfileEnabled.(bool); ok && value { + return nil + } + + if value, ok := userProfileEnabled.(string); ok && strings.ToLower(value) == "true" { + return nil + } + } + return fmt.Errorf("User Profile is disabled for the %s realm", realmId) +} diff --git a/provider/resource_keycloak_realm_user_profile_test.go b/provider/resource_keycloak_realm_user_profile_test.go index c50d891c6..7c18d3ddb 100644 --- a/provider/resource_keycloak_realm_user_profile_test.go +++ b/provider/resource_keycloak_realm_user_profile_test.go @@ -17,8 +17,7 @@ import ( ) func TestAccKeycloakRealmUserProfile_featureDisabled(t *testing.T) { - // TODO Fix test(?) - skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_22) + skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_24) realmName := acctest.RandomWithPrefix("tf-acc") @@ -28,13 +27,49 @@ func TestAccKeycloakRealmUserProfile_featureDisabled(t *testing.T) { CheckDestroy: testAccCheckKeycloakRealmUserProfileDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakRealmUserProfile_featureDisabled(realmName), + Config: testKeycloakRealmUserProfile_userProfileDisabled(realmName), ExpectError: regexp.MustCompile("User Profile is disabled"), }, }, }) } +func TestAccKeycloakRealmUserProfile_featureNotSet(t *testing.T) { + skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_24) + + realmName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRealmUserProfileDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRealmUserProfile_userProfileEnabledNotSet(realmName), + ExpectError: regexp.MustCompile("User Profile is disabled"), + }, + }, + }) +} + +func TestAccKeycloakRealmUserProfile_enabledByDefault(t *testing.T) { + skipIfVersionIsLessThan(testCtx, t, keycloakClient, keycloak.Version_24) + + realmName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRealmUserProfileDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRealmUserProfile_userProfileEnabledNotSet(realmName), + Check: testAccCheckKeycloakRealmUserProfileExists("keycloak_realm_user_profile.realm_user_profile"), + }, + }, + }) +} + func TestAccKeycloakRealmUserProfile_basicEmpty(t *testing.T) { skipIfVersionIsLessThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_14) @@ -452,13 +487,35 @@ func TestAccKeycloakRealmUserProfile_unmanagedPolicyEnabled(t *testing.T) { }) } -func testKeycloakRealmUserProfile_featureDisabled(realm string) string { +func testKeycloakRealmUserProfile_userProfileDisabled(realm string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" + + attributes = { + userProfileEnabled = false + } +} +resource "keycloak_realm_user_profile" "realm_user_profile" { + realm_id = keycloak_realm.realm.id +} +`, realm) +} + +func testKeycloakRealmUserProfile_userProfileEnabledNotSet(realm 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 + + attribute { + name = "username" + } + attribute { + name = "email" + } } `, realm) }