From e5ea116566f154f1558692f822396a0429656b78 Mon Sep 17 00:00:00 2001 From: Darren Vine Date: Wed, 24 Jul 2024 02:36:27 +0200 Subject: [PATCH 1/8] all but seperate props --- .vscode/launch.json | 30 ++ README.md | 4 + ...id_propertymapper_claim_protocol_mapper.go | 135 ++++++ keycloak/protocol_mapper.go | 1 + provider/provider.go | 1 + ...id_propertymapper_claim_protocol_mapper.go | 222 +++++++++ ...opertymapper_claim_protocol_mapper_test.go | 436 ++++++++++++++++++ 7 files changed, 829 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 keycloak/openid_propertymapper_claim_protocol_mapper.go create mode 100644 provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper.go create mode 100644 provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper_test.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..7452cc600 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Terraform Provider", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "env": {}, + "args": [ + "-plugin-dir", + "${workspaceFolder}" + ] + }, + { + "name": "Attach to Terraform Provider", + "type": "go", + "request": "attach", + "mode": "remote", + "remotePath": "${workspaceFolder}", + "port": 2345, + "host": "127.0.0.1", + "program": "${workspaceFolder}", + "env": {}, + "args": [], + "showLog": true + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index f511e2352..db0125eb2 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,10 @@ This environment and its setup via `make local` is not intended for production u Note: The setup scripts require the [jq](https://stedolan.github.io/jq/) command line utility. +### Debugging VSCode + + + ### Tests Every resource supported by this provider will have a reasonable amount of acceptance test coverage. diff --git a/keycloak/openid_propertymapper_claim_protocol_mapper.go b/keycloak/openid_propertymapper_claim_protocol_mapper.go new file mode 100644 index 000000000..3b24e7fdd --- /dev/null +++ b/keycloak/openid_propertymapper_claim_protocol_mapper.go @@ -0,0 +1,135 @@ +package keycloak + +import ( + "context" + "fmt" + "maps" + "strconv" +) + +type OpenIdPropertyMapperClaimProtocolMapper struct { + Id string + Name string + Protocol string + ProtocolMapper string + RealmId string + ClientId string + ClientScopeId string + + AddToIdToken bool + AddToAccessToken bool + AddToUserInfo bool + + ClaimName string + JsonType string + + AdditionalConfig map[string]string +} + +func (mapper *OpenIdPropertyMapperClaimProtocolMapper) convertToGenericProtocolMapper() *protocolMapper { + + config := map[string]string{ + addToIdTokenField: strconv.FormatBool(mapper.AddToIdToken), + addToAccessTokenField: strconv.FormatBool(mapper.AddToAccessToken), + addToUserInfoField: strconv.FormatBool(mapper.AddToUserInfo), + jsonTypeField: mapper.JsonType, + claimNameField: mapper.ClaimName, + } + + maps.Copy(config, mapper.AdditionalConfig) + return &protocolMapper{ + Id: mapper.Id, + Name: mapper.Name, + Protocol: mapper.Protocol, + ProtocolMapper: mapper.ProtocolMapper, + Config: config, + } +} + +func (protocolMapper *protocolMapper) convertToOpenIdPropertyMapperClaimProtocolMapper(realmId, clientId, clientScopeId string) (*OpenIdPropertyMapperClaimProtocolMapper, error) { + addToIdToken, err := parseBoolAndTreatEmptyStringAsFalse(protocolMapper.Config[addToIdTokenField]) + if err != nil { + return nil, err + } + + addToAccessToken, err := parseBoolAndTreatEmptyStringAsFalse(protocolMapper.Config[addToAccessTokenField]) + if err != nil { + return nil, err + } + + addToUserInfo, err := parseBoolAndTreatEmptyStringAsFalse(protocolMapper.Config[addToUserInfoField]) + if err != nil { + return nil, err + } + + return &OpenIdPropertyMapperClaimProtocolMapper{ + Id: protocolMapper.Id, + Name: protocolMapper.Name, + RealmId: realmId, + ClientId: clientId, + ClientScopeId: clientScopeId, + + AddToIdToken: addToIdToken, + AddToAccessToken: addToAccessToken, + AddToUserInfo: addToUserInfo, + + Protocol: protocolMapper.Protocol, + ProtocolMapper: protocolMapper.ProtocolMapper, + ClaimName: protocolMapper.Config[claimNameField], + JsonType: protocolMapper.Config[jsonTypeField], + AdditionalConfig: map[string]string{}, + }, nil +} + +func (keycloakClient *KeycloakClient) GetOpenIdPropertyMapperClaimProtocolMapper(ctx context.Context, realmId, clientId, clientScopeId, mapperId string) (*OpenIdPropertyMapperClaimProtocolMapper, error) { + var protocolMapper *protocolMapper + + err := keycloakClient.get(ctx, individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), &protocolMapper, nil) + if err != nil { + return nil, err + } + + return protocolMapper.convertToOpenIdPropertyMapperClaimProtocolMapper(realmId, clientId, clientScopeId) +} + +func (keycloakClient *KeycloakClient) DeleteOpenIdPropertyMapperClaimProtocolMapper(ctx context.Context, realmId, clientId, clientScopeId, mapperId string) error { + return keycloakClient.delete(ctx, individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), nil) +} + +func (keycloakClient *KeycloakClient) NewOpenIdPropertyMapperClaimProtocolMapper(ctx context.Context, mapper *OpenIdPropertyMapperClaimProtocolMapper) error { + path := protocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId) + + _, location, err := keycloakClient.post(ctx, path, mapper.convertToGenericProtocolMapper()) + if err != nil { + return err + } + + mapper.Id = getIdFromLocationHeader(location) + + return nil +} + +func (keycloakClient *KeycloakClient) UpdateOpenIdPropertyMapperClaimProtocolMapper(ctx context.Context, mapper *OpenIdPropertyMapperClaimProtocolMapper) error { + path := individualProtocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id) + + return keycloakClient.put(ctx, path, mapper.convertToGenericProtocolMapper()) +} + +func (keycloakClient *KeycloakClient) ValidateOpenIdPropertyMapperClaimProtocolMapper(ctx context.Context, mapper *OpenIdPropertyMapperClaimProtocolMapper) error { + if mapper.ClientId == "" && mapper.ClientScopeId == "" { + return fmt.Errorf("validation error: one of ClientId or ClientScopeId must be set") + } + + protocolMappers, err := keycloakClient.listGenericProtocolMappers(ctx, mapper.RealmId, mapper.ClientId, mapper.ClientScopeId) + if err != nil { + return err + } + + for _, protocolMapper := range protocolMappers { + if protocolMapper.Name == mapper.Name && protocolMapper.Id != mapper.Id { + return fmt.Errorf("validation error: a protocol mapper with name %s already exists for this client", mapper.Name) + } + } + + return nil +} diff --git a/keycloak/protocol_mapper.go b/keycloak/protocol_mapper.go index 05ea467ff..c42922f5c 100644 --- a/keycloak/protocol_mapper.go +++ b/keycloak/protocol_mapper.go @@ -23,6 +23,7 @@ var ( claimNameField = "claim.name" claimValueField = "claim.value" claimValueTypeField = "jsonType.label" + jsonTypeField = "jsonType.label" friendlyNameField = "friendly.name" fullPathField = "full.path" includedClientAudienceField = "included.client.audience" diff --git a/provider/provider.go b/provider/provider.go index 00e08e889..2ff7237b1 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -66,6 +66,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider { "keycloak_openid_group_membership_protocol_mapper": resourceKeycloakOpenIdGroupMembershipProtocolMapper(), "keycloak_openid_full_name_protocol_mapper": resourceKeycloakOpenIdFullNameProtocolMapper(), "keycloak_openid_hardcoded_claim_protocol_mapper": resourceKeycloakOpenIdHardcodedClaimProtocolMapper(), + "keycloak_openid_propertymapper_claim_protocol_mapper": resourceKeycloakOpenIdPropertyMapperClaimProtocolMapper(), "keycloak_openid_audience_protocol_mapper": resourceKeycloakOpenIdAudienceProtocolMapper(), "keycloak_openid_audience_resolve_protocol_mapper": resourceKeycloakOpenIdAudienceResolveProtocolMapper(), "keycloak_openid_hardcoded_role_protocol_mapper": resourceKeycloakOpenIdHardcodedRoleProtocolMapper(), diff --git a/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper.go b/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper.go new file mode 100644 index 000000000..75ebd6c57 --- /dev/null +++ b/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper.go @@ -0,0 +1,222 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "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" +) + +func resourceKeycloakOpenIdPropertyMapperClaimProtocolMapper() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperCreate, + ReadContext: resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperRead, + UpdateContext: resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperUpdate, + DeleteContext: resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperDelete, + Importer: &schema.ResourceImporter{ + // import a mapper tied to a client: + // {{realmId}}/client/{{clientId}}/{{protocolMapperId}} + // or a client scope: + // {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}} + StateContext: genericProtocolMapperImport, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "A human-friendly name that will appear in the Keycloak console.", + }, + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The realm id where the associated client or client scope exists.", + }, + "client_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + ConflictsWith: []string{"client_scope_id"}, + }, + "client_scope_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The mapper's associated client scope. Cannot be used at the same time as client_id.", + ConflictsWith: []string{"client_id"}, + }, + "protocol": { + Type: schema.TypeString, + Required: true, + Description: "The protocol type for the expected extra parameters.", + }, + "protocol_mapper": { + Type: schema.TypeString, + Required: true, + Description: "The protocol property mapper type.", + }, + "add_to_id_token": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Indicates if the attribute should be a claim in the id token.", + }, + "add_to_access_token": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Indicates if the attribute should be a claim in the access token.", + }, + "add_to_userinfo": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Indicates if the attribute should appear in the userinfo response body.", + }, + "claim_name": { + Type: schema.TypeString, + Required: true, + Description: "The claim name to display in the token.", + }, + "json_type": { + Type: schema.TypeString, + Optional: true, + Description: "Claim type used when serializing tokens.", + Default: "String", + ValidateFunc: validation.StringInSlice([]string{"JSON", "String", "long", "int", "boolean"}, true), + }, + "set": { + Type: schema.TypeSet, + Optional: true, + Description: "Mapper values to be merged with the other attributes.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + } +} + +func mapFromDataToOpenIdPropertyMapperClaimProtocolMapper(data *schema.ResourceData) *keycloak.OpenIdPropertyMapperClaimProtocolMapper { + return &keycloak.OpenIdPropertyMapperClaimProtocolMapper{ + Id: data.Id(), + Name: data.Get("name").(string), + RealmId: data.Get("realm_id").(string), + ClientId: data.Get("client_id").(string), + ClientScopeId: data.Get("client_scope_id").(string), + AddToIdToken: data.Get("add_to_id_token").(bool), + AddToAccessToken: data.Get("add_to_access_token").(bool), + AddToUserInfo: data.Get("add_to_userinfo").(bool), + + Protocol: data.Get("protocol").(string), + ProtocolMapper: data.Get("protocol_mapper").(string), + ClaimName: data.Get("claim_name").(string), + JsonType: data.Get("json_type").(string), + + // TODO: fill in with right values + AdditionalConfig: map[string]string{}, + } +} + +func mapFromOpenIdPropertyMapperClaimMapperToData(mapper *keycloak.OpenIdPropertyMapperClaimProtocolMapper, data *schema.ResourceData) { + data.SetId(mapper.Id) + data.Set("name", mapper.Name) + data.Set("realm_id", mapper.RealmId) + + if mapper.ClientId != "" { + data.Set("client_id", mapper.ClientId) + } else { + data.Set("client_scope_id", mapper.ClientScopeId) + } + + data.Set("add_to_id_token", mapper.AddToIdToken) + data.Set("add_to_access_token", mapper.AddToAccessToken) + data.Set("add_to_userinfo", mapper.AddToUserInfo) + + data.Set("protocol", mapper.Protocol) + data.Set("protocol_mapper", mapper.ProtocolMapper) + data.Set("claim_name", mapper.ClaimName) + data.Set("json_type", mapper.JsonType) +} + +func resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + openIdPropertyMapperClaimMapper := mapFromDataToOpenIdPropertyMapperClaimProtocolMapper(data) + + err := keycloakClient.ValidateOpenIdPropertyMapperClaimProtocolMapper(ctx, openIdPropertyMapperClaimMapper) + if err != nil { + return diag.FromErr(err) + } + + err = keycloakClient.NewOpenIdPropertyMapperClaimProtocolMapper(ctx, openIdPropertyMapperClaimMapper) + if err != nil { + return diag.FromErr(err) + } + + tflog.Info(ctx, "resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperCreate") + + mapFromOpenIdPropertyMapperClaimMapperToData(openIdPropertyMapperClaimMapper, data) + + return resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperRead(ctx, data, meta) +} + +func resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + realmId := data.Get("realm_id").(string) + clientId := data.Get("client_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + openIdPropertyMapperClaimMapper, err := keycloakClient.GetOpenIdPropertyMapperClaimProtocolMapper(ctx, realmId, clientId, clientScopeId, data.Id()) + if err != nil { + return handleNotFoundError(ctx, err, data) + } + + tflog.Info(ctx, "resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperRead") + //fmt.Printf("%+v\n", yourProject) + mapFromOpenIdPropertyMapperClaimMapperToData(openIdPropertyMapperClaimMapper, data) + + return nil +} + +func resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + openIdPropertyMapperClaimMapper := mapFromDataToOpenIdPropertyMapperClaimProtocolMapper(data) + + err := keycloakClient.ValidateOpenIdPropertyMapperClaimProtocolMapper(ctx, openIdPropertyMapperClaimMapper) + if err != nil { + return diag.FromErr(err) + } + + err = keycloakClient.UpdateOpenIdPropertyMapperClaimProtocolMapper(ctx, openIdPropertyMapperClaimMapper) + if err != nil { + return diag.FromErr(err) + } + + tflog.Info(ctx, "resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperUpdate") + return resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperRead(ctx, data, meta) +} + +func resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientId := data.Get("client_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + return diag.FromErr(keycloakClient.DeleteOpenIdPropertyMapperClaimProtocolMapper(ctx, realmId, clientId, clientScopeId, data.Id())) +} diff --git a/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper_test.go b/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper_test.go new file mode 100644 index 000000000..ea6501b95 --- /dev/null +++ b/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper_test.go @@ -0,0 +1,436 @@ +package provider + +import ( + "fmt" + "regexp" + "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 TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_basicClient(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc") + mapperName := acctest.RandomWithPrefix("tf-acc") + + resourceName := "keycloak_openid_propertymapper_claim_protocol_mapper.propertymapper_claim_mapper_client" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_basic_client(clientId, mapperName), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_basicClientScope(t *testing.T) { + t.Parallel() + clientScopeId := acctest.RandomWithPrefix("tf-acc") + mapperName := acctest.RandomWithPrefix("tf-acc") + + resourceName := "keycloak_openid_propertymapper_claim_protocol_mapper.propertymapper_claim_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_basic_clientScope(clientScopeId, mapperName), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_import(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc") + clientScopeId := acctest.RandomWithPrefix("tf-acc") + mapperName := acctest.RandomWithPrefix("tf-acc") + + clientResourceName := "keycloak_openid_propertymapper_claim_protocol_mapper.propertymapper_claim_mapper_client" + clientScopeResourceName := "keycloak_openid_propertymapper_claim_protocol_mapper.propertymapper_claim_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdFullNameProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_import(clientId, clientScopeId, mapperName), + Check: resource.ComposeTestCheckFunc( + testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(clientResourceName), + testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(clientScopeResourceName), + ), + }, + { + ResourceName: clientResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getGenericProtocolMapperIdForClient(clientResourceName), + }, + { + ResourceName: clientScopeResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getGenericProtocolMapperIdForClientScope(clientScopeResourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_update(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc") + mapperName := acctest.RandomWithPrefix("tf-acc") + + claimName := acctest.RandomWithPrefix("tf-acc") + updatedClaimName := acctest.RandomWithPrefix("tf-acc") + claimValue := acctest.RandomWithPrefix("tf-acc") + updatedClaimValue := acctest.RandomWithPrefix("tf-acc") + + resourceName := "keycloak_openid_propertymapper_claim_protocol_mapper.propertymapper_claim_mapper" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, claimName, claimValue), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, updatedClaimName, updatedClaimValue), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_createAfterManualDestroy(t *testing.T) { + t.Parallel() + var mapper = &keycloak.OpenIdPropertyMapperClaimProtocolMapper{} + + clientId := acctest.RandomWithPrefix("tf-acc") + mapperName := acctest.RandomWithPrefix("tf-acc") + + resourceName := "keycloak_openid_propertymapper_claim_protocol_mapper.propertymapper_claim_mapper_client" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_basic_client(clientId, mapperName), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperFetch(resourceName, mapper), + }, + { + PreConfig: func() { + err := keycloakClient.DeleteOpenIdPropertyMapperClaimProtocolMapper(testCtx, mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id) + if err != nil { + t.Error(err) + } + }, + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_basic_client(clientId, mapperName), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_validateClaimValueType(t *testing.T) { + t.Parallel() + mapperName := acctest.RandomWithPrefix("tf-acc") + invalidClaimValueType := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_validateClaimValueType(mapperName, invalidClaimValueType), + ExpectError: regexp.MustCompile("expected claim_value_type to be one of .+ got " + invalidClaimValueType), + }, + }, + }) +} + +func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_updateClientIdForceNew(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc") + updatedClientId := acctest.RandomWithPrefix("tf-acc") + mapperName := acctest.RandomWithPrefix("tf-acc") + + claimName := acctest.RandomWithPrefix("tf-acc") + claimValue := acctest.RandomWithPrefix("tf-acc") + resourceName := "keycloak_openid_propertymapper_claim_protocol_mapper.propertymapper_claim_mapper" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, claimName, claimValue), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(updatedClientId, mapperName, claimName, claimValue), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_updateClientScopeForceNew(t *testing.T) { + t.Parallel() + mapperName := acctest.RandomWithPrefix("tf-acc") + clientScopeId := acctest.RandomWithPrefix("tf-acc") + newClientScopeId := acctest.RandomWithPrefix("tf-acc") + resourceName := "keycloak_openid_propertymapper_claim_protocol_mapper.propertymapper_claim_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_basic_clientScope(clientScopeId, mapperName), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_basic_clientScope(newClientScopeId, mapperName), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_updateRealmIdForceNew(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc") + mapperName := acctest.RandomWithPrefix("tf-acc") + + claimName := acctest.RandomWithPrefix("tf-acc") + claimValue := acctest.RandomWithPrefix("tf-acc") + resourceName := "keycloak_openid_propertymapper_claim_protocol_mapper.propertymapper_claim_mapper" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, claimName, claimValue), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + { + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, claimName, claimValue), + Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), + }, + }, + }) +} + +func testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy() resource.TestCheckFunc { + return func(state *terraform.State) error { + for resourceName, rs := range state.RootModule().Resources { + if rs.Type != "keycloak_openid_propertymapper_claim_protocol_mapper" { + continue + } + + mapper, _ := getPropertyMapperClaimMapperUsingState(state, resourceName) + + if mapper != nil { + return fmt.Errorf("openid user attribute protocol mapper with id %s still exists", rs.Primary.ID) + } + } + + return nil + } +} + +func testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + _, err := getPropertyMapperClaimMapperUsingState(state, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testKeycloakOpenIdPropertyMapperClaimProtocolMapperFetch(resourceName string, mapper *keycloak.OpenIdPropertyMapperClaimProtocolMapper) resource.TestCheckFunc { + return func(state *terraform.State) error { + fetchedMapper, err := getPropertyMapperClaimMapperUsingState(state, resourceName) + if err != nil { + return err + } + + mapper.Id = fetchedMapper.Id + mapper.ClientId = fetchedMapper.ClientId + mapper.ClientScopeId = fetchedMapper.ClientScopeId + mapper.RealmId = fetchedMapper.RealmId + + return nil + } +} + +func getPropertyMapperClaimMapperUsingState(state *terraform.State, resourceName string) (*keycloak.OpenIdPropertyMapperClaimProtocolMapper, error) { + rs, ok := state.RootModule().Resources[resourceName] + if !ok { + return nil, fmt.Errorf("resource not found in TF state: %s ", resourceName) + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + clientId := rs.Primary.Attributes["client_id"] + clientScopeId := rs.Primary.Attributes["client_scope_id"] + + return keycloakClient.GetOpenIdPropertyMapperClaimProtocolMapper(testCtx, realm, clientId, clientScopeId, id) +} + +func testKeycloakOpenIdPropertyMapperClaimProtocolMapper_basic_client(clientId, mapperName string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = data.keycloak_realm.realm.id + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_claim_mapper_client" { + name = "%s" + realm_id = data.keycloak_realm.realm.id + client_id = "${keycloak_openid_client.openid_client.id}" + + claim_name = "foo" + claim_value = "bar" + claim_value_type = "String" +}`, testAccRealm.Realm, clientId, mapperName) +} + +func testKeycloakOpenIdPropertyMapperClaimProtocolMapper_basic_clientScope(clientScopeId, mapperName string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client_scope" "client_scope" { + name = "%s" + realm_id = data.keycloak_realm.realm.id +} + +resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_claim_mapper_client_scope" { + name = "%s" + realm_id = data.keycloak_realm.realm.id + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + + claim_name = "foo" + claim_value = "bar" + claim_value_type = "String" +}`, testAccRealm.Realm, clientScopeId, mapperName) +} + +func testKeycloakOpenIdPropertyMapperClaimProtocolMapper_import(clientId, clientScopeId, mapperName string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = data.keycloak_realm.realm.id + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_claim_mapper_client" { + name = "%s" + realm_id = data.keycloak_realm.realm.id + client_id = "${keycloak_openid_client.openid_client.id}" + + claim_name = "foo" + claim_value = "bar" + claim_value_type = "String" +} + +resource "keycloak_openid_client_scope" "client_scope" { + name = "%s" + realm_id = data.keycloak_realm.realm.id +} + +resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_claim_mapper_client_scope" { + name = "%s" + realm_id = data.keycloak_realm.realm.id + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + + claim_name = "foo" + claim_value = "bar" + claim_value_type = "String" +}`, testAccRealm.Realm, clientId, mapperName, clientScopeId, mapperName) +} + +func testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, claimName, claimValue string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = data.keycloak_realm.realm.id + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_claim_mapper" { + name = "%s" + realm_id = data.keycloak_realm.realm.id + client_id = "${keycloak_openid_client.openid_client.id}" + + claim_name = "%s" + claim_value = "%s" +}`, testAccRealm.Realm, clientId, mapperName, claimName, claimValue) +} + +func testKeycloakOpenIdPropertyMapperClaimProtocolMapper_validateClaimValueType(mapperName, claimValueType string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = data.keycloak_realm.realm.id + client_id = "openid-client" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_claim_mapper_validation" { + name = "%s" + realm_id = data.keycloak_realm.realm.id + client_id = "${keycloak_openid_client.openid_client.id}" + + claim_value = "foo" + claim_name = "bar" + claim_value_type = "%s" +}`, testAccRealm.Realm, mapperName, claimValueType) +} From 0d9f9556ecc7b5a088eecd4ddfbc2a4408ec7af3 Mon Sep 17 00:00:00 2001 From: Darren Vine Date: Thu, 25 Jul 2024 22:32:27 +0200 Subject: [PATCH 2/8] implemented --- .vscode/launch.json | 29 +++++---- README.md | 4 -- keycloak/keycloak_client.go | 3 +- ...id_propertymapper_claim_protocol_mapper.go | 48 +++++++++++---- keycloak/protocol_mapper.go | 4 ++ main.go | 15 ++++- provider/provider_test.go | 7 ++- ...id_propertymapper_claim_protocol_mapper.go | 59 ++++++++++++++----- 8 files changed, 117 insertions(+), 52 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7452cc600..9ced90e61 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,30 +1,29 @@ { - "version": "0.2.0", "configurations": [ { "name": "Debug Terraform Provider", "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}", - "env": {}, + "program": "${workspaceRoot}", + "env": { + }, "args": [ "-plugin-dir", - "${workspaceFolder}" - ] + "${workspaceRoot}" + ], + "cwd": "${workspaceRoot}" }, { - "name": "Attach to Terraform Provider", + "name": "Debug Terraform Provider Active", "type": "go", - "request": "attach", - "mode": "remote", - "remotePath": "${workspaceFolder}", - "port": 2345, - "host": "127.0.0.1", - "program": "${workspaceFolder}", + "request": "launch", + "mode": "debug", + "program": "${workspaceRoot}", "env": {}, - "args": [], - "showLog": true - } + "args": [ + "-debug", + ] + } ] } \ No newline at end of file diff --git a/README.md b/README.md index db0125eb2..f511e2352 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,6 @@ This environment and its setup via `make local` is not intended for production u Note: The setup scripts require the [jq](https://stedolan.github.io/jq/) command line utility. -### Debugging VSCode - - - ### Tests Every resource supported by this provider will have a reasonable amount of acceptance test coverage. diff --git a/keycloak/keycloak_client.go b/keycloak/keycloak_client.go index 23e896128..010221460 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" diff --git a/keycloak/openid_propertymapper_claim_protocol_mapper.go b/keycloak/openid_propertymapper_claim_protocol_mapper.go index 3b24e7fdd..5b3d8c4a7 100644 --- a/keycloak/openid_propertymapper_claim_protocol_mapper.go +++ b/keycloak/openid_propertymapper_claim_protocol_mapper.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "maps" + "slices" "strconv" ) @@ -16,9 +17,11 @@ type OpenIdPropertyMapperClaimProtocolMapper struct { ClientId string ClientScopeId string - AddToIdToken bool - AddToAccessToken bool - AddToUserInfo bool + AddToIdToken bool + AddToAccessToken bool + AddToUserInfo bool + AddToIntrospectionToken bool + AddToLightweightClaim bool ClaimName string JsonType string @@ -29,11 +32,13 @@ type OpenIdPropertyMapperClaimProtocolMapper struct { func (mapper *OpenIdPropertyMapperClaimProtocolMapper) convertToGenericProtocolMapper() *protocolMapper { config := map[string]string{ - addToIdTokenField: strconv.FormatBool(mapper.AddToIdToken), - addToAccessTokenField: strconv.FormatBool(mapper.AddToAccessToken), - addToUserInfoField: strconv.FormatBool(mapper.AddToUserInfo), - jsonTypeField: mapper.JsonType, - claimNameField: mapper.ClaimName, + addToIdTokenField: strconv.FormatBool(mapper.AddToIdToken), + addToAccessTokenField: strconv.FormatBool(mapper.AddToAccessToken), + addToUserInfoField: strconv.FormatBool(mapper.AddToUserInfo), + addToIntrospectionTokenField: strconv.FormatBool(mapper.AddToIntrospectionToken), + addToLightweightClaimField: strconv.FormatBool(mapper.AddToLightweightClaim), + jsonTypeField: mapper.JsonType, + claimNameField: mapper.ClaimName, } maps.Copy(config, mapper.AdditionalConfig) @@ -62,6 +67,23 @@ func (protocolMapper *protocolMapper) convertToOpenIdPropertyMapperClaimProtocol return nil, err } + addToIntrospectionTokenField, err := parseBoolAndTreatEmptyStringAsFalse(protocolMapper.Config[addToIntrospectionTokenField]) + if err != nil { + return nil, err + } + + addToLightweightClaimField, err := parseBoolAndTreatEmptyStringAsFalse(protocolMapper.Config[addToLightweightClaimField]) + if err != nil { + return nil, err + } + + additionalConfig := map[string]string{} + for k, v := range protocolMapper.Config { + if !slices.Contains(protocolMapperIgnore, k) { + additionalConfig[k] = v + } + } + return &OpenIdPropertyMapperClaimProtocolMapper{ Id: protocolMapper.Id, Name: protocolMapper.Name, @@ -69,15 +91,17 @@ func (protocolMapper *protocolMapper) convertToOpenIdPropertyMapperClaimProtocol ClientId: clientId, ClientScopeId: clientScopeId, - AddToIdToken: addToIdToken, - AddToAccessToken: addToAccessToken, - AddToUserInfo: addToUserInfo, + AddToIdToken: addToIdToken, + AddToAccessToken: addToAccessToken, + AddToUserInfo: addToUserInfo, + AddToIntrospectionToken: addToIntrospectionTokenField, + AddToLightweightClaim: addToLightweightClaimField, Protocol: protocolMapper.Protocol, ProtocolMapper: protocolMapper.ProtocolMapper, ClaimName: protocolMapper.Config[claimNameField], JsonType: protocolMapper.Config[jsonTypeField], - AdditionalConfig: map[string]string{}, + AdditionalConfig: additionalConfig, }, nil } diff --git a/keycloak/protocol_mapper.go b/keycloak/protocol_mapper.go index c42922f5c..22957085a 100644 --- a/keycloak/protocol_mapper.go +++ b/keycloak/protocol_mapper.go @@ -18,6 +18,8 @@ var ( addToAccessTokenField = "access.token.claim" addToIdTokenField = "id.token.claim" addToUserInfoField = "userinfo.token.claim" + addToIntrospectionTokenField = "introspection.token.claim" + addToLightweightClaimField = "lightweight.claim" attributeNameField = "attribute.name" attributeNameFormatField = "attribute.nameformat" claimNameField = "claim.name" @@ -39,6 +41,8 @@ var ( userClientRoleMappingRolePrefixField = "usermodel.clientRoleMapping.rolePrefix" userSessionNoteField = "user.session.note" aggregateAttributeValuesField = "aggregate.attrs" + + protocolMapperIgnore = []string{addToIntrospectionTokenField, addToUserInfoField, addToIdTokenField, addToAccessTokenField, addToLightweightClaimField, claimNameField, jsonTypeField} ) func protocolMapperPath(realmId, clientId, clientScopeId string) string { diff --git a/main.go b/main.go index e8544827e..93b47a558 100644 --- a/main.go +++ b/main.go @@ -1,15 +1,26 @@ package main import ( + "flag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" "github.com/mrparkers/terraform-provider-keycloak/provider" ) func main() { - plugin.Serve(&plugin.ServeOpts{ + var debug bool + + flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") + flag.Parse() + + opts := &plugin.ServeOpts{ + Debug: debug, + ProviderAddr: "registry.terraform.io/mrparkers/keycloak", ProviderFunc: func() *schema.Provider { return provider.KeycloakProvider(nil) }, - }) + } + + plugin.Serve(opts) } diff --git a/provider/provider_test.go b/provider/provider_test.go index 04c45eb4c..f6f8e113a 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -3,12 +3,13 @@ package provider import ( "context" "fmt" + "os" + "testing" + "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" "github.com/mrparkers/terraform-provider-keycloak/keycloak" - "os" - "testing" ) var testAccProviderFactories map[string]func() (*schema.Provider, error) @@ -29,7 +30,7 @@ var requiredEnvironmentVariables = []string{ func init() { testCtx = context.Background() userAgent := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io) Terraform Plugin SDK/%s", schema.Provider{}.TerraformVersion, meta.SDKVersionString()) - keycloakClient, _ = keycloak.NewKeycloakClient(testCtx, os.Getenv("KEYCLOAK_URL"), "", os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), "", "", true, 5, "", false, userAgent, false, map[string]string{ + keycloakClient, _ = keycloak.NewKeycloakClient(testCtx, os.Getenv("KEYCLOAK_URL"), "", os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), os.Getenv("KEYCLOAK_USER"), os.Getenv("KEYCLOAK_PASSWORD"), true, 5, "", false, userAgent, false, map[string]string{ "foo": "bar", }) testAccProvider = KeycloakProvider(keycloakClient) diff --git a/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper.go b/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper.go index 75ebd6c57..ca360c700 100644 --- a/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper.go +++ b/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper.go @@ -77,6 +77,18 @@ func resourceKeycloakOpenIdPropertyMapperClaimProtocolMapper() *schema.Resource Default: true, Description: "Indicates if the attribute should appear in the userinfo response body.", }, + "add_to_introspection_token": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Indicates if the attribute should be a claim in the introspect token.", + }, + "add_to_lightweight_claim": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Indicates if the attribute should appear in the lightweight claim.", + }, "claim_name": { Type: schema.TypeString, Required: true, @@ -111,23 +123,33 @@ func resourceKeycloakOpenIdPropertyMapperClaimProtocolMapper() *schema.Resource } func mapFromDataToOpenIdPropertyMapperClaimProtocolMapper(data *schema.ResourceData) *keycloak.OpenIdPropertyMapperClaimProtocolMapper { + + additionalConfig := map[string]string{} + setlist := data.Get("set").(*schema.Set).List() + + for _, raw := range setlist { + set := raw.(map[string]interface{}) + additionalConfig[set["name"].(string)] = set["value"].(string) + } + return &keycloak.OpenIdPropertyMapperClaimProtocolMapper{ - Id: data.Id(), - Name: data.Get("name").(string), - RealmId: data.Get("realm_id").(string), - ClientId: data.Get("client_id").(string), - ClientScopeId: data.Get("client_scope_id").(string), - AddToIdToken: data.Get("add_to_id_token").(bool), - AddToAccessToken: data.Get("add_to_access_token").(bool), - AddToUserInfo: data.Get("add_to_userinfo").(bool), + Id: data.Id(), + Name: data.Get("name").(string), + RealmId: data.Get("realm_id").(string), + ClientId: data.Get("client_id").(string), + ClientScopeId: data.Get("client_scope_id").(string), + AddToIdToken: data.Get("add_to_id_token").(bool), + AddToAccessToken: data.Get("add_to_access_token").(bool), + AddToUserInfo: data.Get("add_to_userinfo").(bool), + AddToIntrospectionToken: data.Get("add_to_introspection_token").(bool), + AddToLightweightClaim: data.Get("add_to_lightweight_claim").(bool), Protocol: data.Get("protocol").(string), ProtocolMapper: data.Get("protocol_mapper").(string), ClaimName: data.Get("claim_name").(string), JsonType: data.Get("json_type").(string), - // TODO: fill in with right values - AdditionalConfig: map[string]string{}, + AdditionalConfig: additionalConfig, } } @@ -145,11 +167,23 @@ func mapFromOpenIdPropertyMapperClaimMapperToData(mapper *keycloak.OpenIdPropert data.Set("add_to_id_token", mapper.AddToIdToken) data.Set("add_to_access_token", mapper.AddToAccessToken) data.Set("add_to_userinfo", mapper.AddToUserInfo) + data.Set("add_to_introspection_token", mapper.AddToIntrospectionToken) + data.Set("add_to_lightweight_claim", mapper.AddToLightweightClaim) data.Set("protocol", mapper.Protocol) data.Set("protocol_mapper", mapper.ProtocolMapper) data.Set("claim_name", mapper.ClaimName) data.Set("json_type", mapper.JsonType) + + additionalConfig := make([]interface{}, 0, len(mapper.AdditionalConfig)) + for k, v := range mapper.AdditionalConfig { + item := map[string]interface{}{ + "name": k, + "value": v, + } + additionalConfig = append(additionalConfig, item) + } + data.Set("set", additionalConfig) } func resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -184,9 +218,6 @@ func resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperRead(ctx context.Con if err != nil { return handleNotFoundError(ctx, err, data) } - - tflog.Info(ctx, "resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperRead") - //fmt.Printf("%+v\n", yourProject) mapFromOpenIdPropertyMapperClaimMapperToData(openIdPropertyMapperClaimMapper, data) return nil @@ -206,8 +237,6 @@ func resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperUpdate(ctx context.C if err != nil { return diag.FromErr(err) } - - tflog.Info(ctx, "resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperUpdate") return resourceKeycloakOpenIdPropertyMapperClaimProtocolMapperRead(ctx, data, meta) } From 8ffaf18c4012c477b1ad5b218c6296301161e10d Mon Sep 17 00:00:00 2001 From: Darren Vine Date: Thu, 25 Jul 2024 22:41:26 +0200 Subject: [PATCH 3/8] claim tests --- ...opertymapper_claim_protocol_mapper_test.go | 109 ++++++++---------- 1 file changed, 51 insertions(+), 58 deletions(-) diff --git a/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper_test.go b/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper_test.go index ea6501b95..cda4361f4 100644 --- a/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper_test.go +++ b/provider/resource_keycloak_openid_propertymapper_claim_protocol_mapper_test.go @@ -2,7 +2,6 @@ package provider import ( "fmt" - "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -106,11 +105,11 @@ func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_update(t *testing.T) CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, claimName, claimValue), + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_userModel(clientId, mapperName, claimName, claimValue), Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), }, { - Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, updatedClaimName, updatedClaimValue), + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_userModel(clientId, mapperName, updatedClaimName, updatedClaimValue), Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), }, }, @@ -149,24 +148,6 @@ func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_createAfterManualDes }) } -func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_validateClaimValueType(t *testing.T) { - t.Parallel() - mapperName := acctest.RandomWithPrefix("tf-acc") - invalidClaimValueType := acctest.RandomWithPrefix("tf-acc") - - resource.Test(t, resource.TestCase{ - ProviderFactories: testAccProviderFactories, - PreCheck: func() { testAccPreCheck(t) }, - CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), - Steps: []resource.TestStep{ - { - Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_validateClaimValueType(mapperName, invalidClaimValueType), - ExpectError: regexp.MustCompile("expected claim_value_type to be one of .+ got " + invalidClaimValueType), - }, - }, - }) -} - func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_updateClientIdForceNew(t *testing.T) { t.Parallel() clientId := acctest.RandomWithPrefix("tf-acc") @@ -183,11 +164,11 @@ func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_updateClientIdForceN CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, claimName, claimValue), + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_userModel(clientId, mapperName, claimName, claimValue), Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), }, { - Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(updatedClientId, mapperName, claimName, claimValue), + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_userModel(updatedClientId, mapperName, claimName, claimValue), Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), }, }, @@ -233,11 +214,11 @@ func TestAccKeycloakOpenIdPropertyMapperClaimProtocolMapper_updateRealmIdForceNe CheckDestroy: testAccKeycloakOpenIdPropertyMapperClaimProtocolMapperDestroy(), Steps: []resource.TestStep{ { - Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, claimName, claimValue), + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_userModel(clientId, mapperName, claimName, claimValue), Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), }, { - Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, claimName, claimValue), + Config: testKeycloakOpenIdPropertyMapperClaimProtocolMapper_userModel(clientId, mapperName, claimName, claimValue), Check: testKeycloakOpenIdPropertyMapperClaimProtocolMapperExists(resourceName), }, }, @@ -322,8 +303,15 @@ resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_ client_id = "${keycloak_openid_client.openid_client.id}" claim_name = "foo" - claim_value = "bar" - claim_value_type = "String" + json_type = "String" + + protocol = "openid-connect" + protocol_mapper = "oidc-usermodel-property-mapper" + + set { + name = "user.attribute" + value = "id" + } }`, testAccRealm.Realm, clientId, mapperName) } @@ -344,8 +332,15 @@ resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_ client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" claim_name = "foo" - claim_value = "bar" - claim_value_type = "String" + json_type = "String" + + protocol = "openid-connect" + protocol_mapper = "oidc-usermodel-property-mapper" + + set { + name = "user.attribute" + value = "id" + } }`, testAccRealm.Realm, clientScopeId, mapperName) } @@ -368,8 +363,15 @@ resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_ client_id = "${keycloak_openid_client.openid_client.id}" claim_name = "foo" - claim_value = "bar" - claim_value_type = "String" + json_type = "String" + + protocol = "openid-connect" + protocol_mapper = "oidc-usermodel-property-mapper" + + set { + name = "user.attribute" + value = "id" + } } resource "keycloak_openid_client_scope" "client_scope" { @@ -383,12 +385,19 @@ resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_ client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" claim_name = "foo" - claim_value = "bar" - claim_value_type = "String" + json_type = "String" + + protocol = "openid-connect" + protocol_mapper = "oidc-usermodel-property-mapper" + + set { + name = "user.attribute" + value = "id" + } }`, testAccRealm.Realm, clientId, mapperName, clientScopeId, mapperName) } -func testKeycloakOpenIdPropertyMapperClaimProtocolMapper_claimNameAndValue(clientId, mapperName, claimName, claimValue string) string { +func testKeycloakOpenIdPropertyMapperClaimProtocolMapper_userModel(clientId, mapperName, claimName, attribute string) string { return fmt.Sprintf(` data "keycloak_realm" "realm" { realm = "%s" @@ -407,30 +416,14 @@ resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_ client_id = "${keycloak_openid_client.openid_client.id}" claim_name = "%s" - claim_value = "%s" -}`, testAccRealm.Realm, clientId, mapperName, claimName, claimValue) -} - -func testKeycloakOpenIdPropertyMapperClaimProtocolMapper_validateClaimValueType(mapperName, claimValueType string) string { - return fmt.Sprintf(` -data "keycloak_realm" "realm" { - realm = "%s" -} + json_type = "String" -resource "keycloak_openid_client" "openid_client" { - realm_id = data.keycloak_realm.realm.id - client_id = "openid-client" - - access_type = "BEARER-ONLY" -} - -resource "keycloak_openid_propertymapper_claim_protocol_mapper" "propertymapper_claim_mapper_validation" { - name = "%s" - realm_id = data.keycloak_realm.realm.id - client_id = "${keycloak_openid_client.openid_client.id}" + protocol = "openid-connect" + protocol_mapper = "oidc-usermodel-property-mapper" - claim_value = "foo" - claim_name = "bar" - claim_value_type = "%s" -}`, testAccRealm.Realm, mapperName, claimValueType) + set { + name = "user.attribute" + value = "%s" + } +}`, testAccRealm.Realm, clientId, mapperName, claimName, attribute) } From 0781e9a7b39303cff689d9232cacdf37624c6052 Mon Sep 17 00:00:00 2001 From: Darren Vine Date: Thu, 25 Jul 2024 22:56:02 +0200 Subject: [PATCH 4/8] Use latest keycloak --- .github/workflows/build-test-image.yml | 4 +--- README.md | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-test-image.yml b/.github/workflows/build-test-image.yml index a7593b33f..eff8d6da0 100644 --- a/.github/workflows/build-test-image.yml +++ b/.github/workflows/build-test-image.yml @@ -13,9 +13,7 @@ jobs: strategy: matrix: keycloak-version: - - '21.0.1' - - '20.0.5' - - '19.0.2' + - '21.7.1' fail-fast: false concurrency: group: docker-build-${{ matrix.keycloak-version }} diff --git a/README.md b/README.md index f511e2352..de1cbe091 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,7 @@ This provider will officially support the latest three major versions of Keycloa The following versions are used when running acceptance tests in CI: -- 21.0.1 (latest) -- 20.0.5 -- 19.0.2 +- 21.7.1 ## Releases @@ -62,7 +60,7 @@ build you can use the `linux_amd64` build as long as `libc6-compat` is installed ## Development -This project requires Go 1.19 and Terraform 1.4.1. +This project requires Go 1.22.5 and Terraform 1.4.1. This project uses [Go Modules](https://github.com/golang/go/wiki/Modules) for dependency management, which allows this project to exist outside of an existing GOPATH. After cloning the repository, you can build the project by running `make build`. From 0e2a46d8feaaee3772c58aebd19e72a313e40312 Mon Sep 17 00:00:00 2001 From: Darren Vine Date: Thu, 25 Jul 2024 22:56:44 +0200 Subject: [PATCH 5/8] Use latest keycloak --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index d05dfe5c2..97e5211d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: ports: - 8389:389 keycloak: - image: quay.io/keycloak/keycloak:21.0.1 + image: quay.io/keycloak/keycloak:21.7.1 command: start-dev --features=preview depends_on: - postgres From d81a7abb53b02cad0a36e689a35045b9b2358265 Mon Sep 17 00:00:00 2001 From: Darren Vine Date: Thu, 25 Jul 2024 23:08:22 +0200 Subject: [PATCH 6/8] Documentation --- ...id_propertymapper_claim_protocol_mapper.md | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 docs/resources/keycloak_openid_propertymapper_claim_protocol_mapper.md diff --git a/docs/resources/keycloak_openid_propertymapper_claim_protocol_mapper.md b/docs/resources/keycloak_openid_propertymapper_claim_protocol_mapper.md new file mode 100644 index 000000000..4b853ae4d --- /dev/null +++ b/docs/resources/keycloak_openid_propertymapper_claim_protocol_mapper.md @@ -0,0 +1,183 @@ +--- +page_title: "keycloak_openid_propertymapper_claim_protocol_mapper Resource" +--- + +# keycloak\_openid\_propertymapper\_claim\_protocol\_mapper Resource + +Allows for creating and managing claim protocol mappers within Keycloak. + +The property claim mappers allow you to define a claim with based on dynamic values to support latest keycloak apis. + +Protocol mappers can be defined for a single client, or they can be defined for a client scope which can be shared between multiple different clients. + +## Example Usage (Client) + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = keycloak_realm.realm.id + client_id = "client" + + name = "client" + enabled = true + + access_type = "CONFIDENTIAL" + valid_redirect_uris = [ + "http://localhost:8080/openid-callback" + ] +} + +resource "keycloak_openid_propertymapper_claim_protocol_mapper" "userattribute_id_claim_mapper" { + realm_id = keycloak_realm.realm.id + client_id = keycloak_openid_client.openid_client.id + name = "property-mapper" + + claim_name = "property" + json_type = "String" + + protocol = "openid-connect" + protocol_mapper = "oidc-usermodel-property-mapper" + + set { + name = "user.attribute" + value = "id" + } +} + +resource "keycloak_openid_propertymapper_claim_protocol_mapper" "clientrole_claim_mapper" { + realm_id = keycloak_realm.realm.id + client_id = keycloak_openid_client.openid_client.id + name = "client-role-mapper" + + claim_name = "clientrole" + json_type = "String" + + protocol = "openid-connect" + protocol_mapper = "oidc-usermodel-client-role-mapper" + + add_to_introspection_token = true + add_to_id_token = true + add_to_access_token = true + add_to_userinfo = true + add_to_lightweight_claim = true + + set { + name = "multivalued" + value = "false" + } + + set { + name = "usermodel.clientRoleMapping.clientId" + value = "admin-cli" + } + + set { + name = "usermodel.clientRoleMapping.rolePrefix" + value = "prefix" + } +} +``` + +## Example Usage (Client Scope) + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_openid_client_scope" "client_scope" { + realm_id = keycloak_realm.realm.id + name = "client-scope" +} + +resource "keycloak_openid_propertymapper_claim_protocol_mapper" "userattribute_id_claim_mapper" { + realm_id = keycloak_realm.realm.id + client_scope_id = keycloak_openid_client_scope.client_scope.id + name = "property-mapper" + + claim_name = "property" + json_type = "String" + + protocol = "openid-connect" + protocol_mapper = "oidc-usermodel-property-mapper" + + set { + name = "user.attribute" + value = "id" + } +} + +resource "keycloak_openid_propertymapper_claim_protocol_mapper" "clientrole_claim_mapper" { + realm_id = keycloak_realm.realm.id + client_scope_id = keycloak_openid_client_scope.client_scope.id + name = "client-role-mapper" + + claim_name = "clientrole" + json_type = "String" + + protocol = "openid-connect" + protocol_mapper = "oidc-usermodel-client-role-mapper" + + add_to_introspection_token = true + add_to_id_token = true + add_to_access_token = true + add_to_userinfo = true + add_to_lightweight_claim = true + + set { + name = "multivalued" + value = "false" + } + + set { + name = "usermodel.clientRoleMapping.clientId" + value = "admin-cli" + } + + set { + name = "usermodel.clientRoleMapping.rolePrefix" + value = "prefix" + } +} +``` + +## Argument Reference + +- `realm_id` - (Required) The realm this protocol mapper exists within. +- `name` - (Required) The display name of this protocol mapper in the GUI. +- `claim_name` - (Required) The name of the claim to insert into a token. +- `claim_value` - (Required) The hardcoded value of the claim. +- `client_id` - (Optional) The client this protocol mapper should be attached to. Conflicts with `client_scope_id`. One of `client_id` or `client_scope_id` must be specified. +- `client_scope_id` - (Optional) The client scope this protocol mapper should be attached to. Conflicts with `client_id`. One of `client_id` or `client_scope_id` must be specified. +- `claim_value_type` - (Optional) The claim type used when serializing JSON tokens. Can be one of `String`, `JSON`, `long`, `int`, or `boolean`. Defaults to `String`. +- `add_to_id_token` - (Optional) Indicates if the property should be added as a claim to the id token. Defaults to `true`. +- `add_to_access_token` - (Optional) Indicates if the property should be added as a claim to the access token. Defaults to `true`. +- `add_to_userinfo` - (Optional) Indicates if the property should be added as a claim to the UserInfo response body. Defaults to `true`. +- `add_to_introspection_token` - (Optional) Indicates if the property should be added as a claim to the introspection token. Defaults to `true`. +- `add_to_lightweight_claim` - (Optional) Indicates if the property should be added as a lightweight claim. Defaults to `false`. +- `set` - (Block Set) Custom values to be merged with the values. (see below for nested schema) + +### Nested Schema for `set` + +Required: + +- `name` (String) +- `value` (String) + +## Import + +Protocol mappers can be imported using one of the following formats: +- Client: `{{realm_id}}/client/{{client_keycloak_id}}/{{protocol_mapper_id}}` +- Client Scope: `{{realm_id}}/client-scope/{{client_scope_keycloak_id}}/{{protocol_mapper_id}}` + +Example: + +```bash +$ terraform import keycloak_openid_propertymapper_claim_protocol_mapper.claim_mapper my-realm/client/a7202154-8793-4656-b655-1dd18c181e14/71602afa-f7d1-4788-8c49-ef8fd00af0f4 +$ terraform import keycloak_openid_propertymapper_claim_protocol_mapper.claim_mapper my-realm/client-scope/b799ea7e-73ee-4a73-990a-1eafebe8e20a/71602afa-f7d1-4788-8c49-ef8fd00af0f4 +``` From ab1f1f4412c23f50aa6144cbdec3bacfb2d1a3c4 Mon Sep 17 00:00:00 2001 From: Darren Vine Date: Thu, 25 Jul 2024 23:16:22 +0200 Subject: [PATCH 7/8] Update go version --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index aaf5fae2e..8fc371ca1 100644 --- a/go.mod +++ b/go.mod @@ -54,4 +54,4 @@ require ( google.golang.org/protobuf v1.30.0 // indirect ) -go 1.19 +go 1.22.5 From 2363f60fd9973cf5f330d7869b6d7b817815a536 Mon Sep 17 00:00:00 2001 From: Darren Vine Date: Thu, 25 Jul 2024 23:33:02 +0200 Subject: [PATCH 8/8] Put new versions in --- .github/workflows/build-test-image.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-test-image.yml b/.github/workflows/build-test-image.yml index eff8d6da0..15c3459ca 100644 --- a/.github/workflows/build-test-image.yml +++ b/.github/workflows/build-test-image.yml @@ -14,6 +14,9 @@ jobs: matrix: keycloak-version: - '21.7.1' + - '21.0.1' + - '20.0.5' + - '19.0.2' fail-fast: false concurrency: group: docker-build-${{ matrix.keycloak-version }}