diff --git a/docs/resources/openid_audience_resolve_protocol_mapper.md b/docs/resources/openid_audience_resolve_protocol_mapper.md new file mode 100644 index 000000000..9223c9444 --- /dev/null +++ b/docs/resources/openid_audience_resolve_protocol_mapper.md @@ -0,0 +1,78 @@ +--- +page_title: "keycloak_openid_audience_resolve_protocol_mapper Resource" +--- + +# keycloak\_openid\_audience\_resolve\_protocol\_mapper Resource + +Allows for creating the "Audience Resolve" OIDC protocol mapper within Keycloak. + +This protocol mapper is useful to avoid manual management of audiences, instead relying on the presence of client roles +to imply which audiences are appropriate for the token. See the +[Keycloak docs](https://www.keycloak.org/docs/latest/server_admin/#_audience_resolve) for more details. + +## 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_audience_resolve_protocol_mapper" "audience_mapper" { + realm_id = keycloak_realm.realm.id + client_id = keycloak_openid_client.openid_client.id + name = "my-audience-resolve-mapper" +} +``` + +## 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 = "test-client-scope" +} + +resource "keycloak_openid_audience_protocol_mapper" "audience_mapper" { + realm_id = keycloak_realm.realm.id + client_scope_id = keycloak_openid_client_scope.client_scope.id +} +``` + +## Argument Reference + +- `realm_id` - (Required) The realm this protocol mapper exists within. +- `name` - (Optional) The display name of this protocol mapper in the GUI. Defaults to "audience resolve". +- `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. + +## 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_audience_protocol_mapper.audience_mapper my-realm/client/a7202154-8793-4656-b655-1dd18c181e14/71602afa-f7d1-4788-8c49-ef8fd00af0f4 +$ terraform import keycloak_openid_audience_protocol_mapper.audience_mapper my-realm/client-scope/b799ea7e-73ee-4a73-990a-1eafebe8e20a/71602afa-f7d1-4788-8c49-ef8fd00af0f4 +``` diff --git a/go.sum b/go.sum index bab30a2a1..00f8f36e1 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,7 @@ github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/andybalholm/crlf v0.0.0-20171020200849-670099aa064f/go.mod h1:k8feO4+kXDxro6ErPXBRTJ/ro2mf0SsFG8s7doP9kJE= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U= github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= @@ -60,6 +61,7 @@ github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJE github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= github.com/aws/aws-sdk-go v1.25.3 h1:uM16hIw9BotjZKMZlX05SN2EFtaWfi/NonPKIARiBLQ= @@ -86,13 +88,16 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= @@ -232,6 +237,7 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= @@ -268,6 +274,7 @@ github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/keycloak/openid_audience_resolve_protocol_mapper.go b/keycloak/openid_audience_resolve_protocol_mapper.go new file mode 100644 index 000000000..63c8efc89 --- /dev/null +++ b/keycloak/openid_audience_resolve_protocol_mapper.go @@ -0,0 +1,86 @@ +package keycloak + +import ( + "fmt" +) + +type OpenIdAudienceResolveProtocolMapper struct { + Id string + Name string + RealmId string + ClientId string + ClientScopeId string +} + +func (mapper *OpenIdAudienceResolveProtocolMapper) convertToGenericProtocolMapper() *protocolMapper { + return &protocolMapper{ + Id: mapper.Id, + Name: mapper.Name, + Protocol: "openid-connect", + ProtocolMapper: "oidc-audience-resolve-mapper", + Config: map[string]string{}, + } +} + +func (protocolMapper *protocolMapper) convertToOpenIdAudienceResolveProtocolMapper(realmId, clientId, clientScopeId string) (*OpenIdAudienceResolveProtocolMapper, error) { + return &OpenIdAudienceResolveProtocolMapper{ + Id: protocolMapper.Id, + Name: protocolMapper.Name, + RealmId: realmId, + ClientId: clientId, + ClientScopeId: clientScopeId, + }, nil +} + +func (keycloakClient *KeycloakClient) GetOpenIdAudienceResolveProtocolMapper(realmId, clientId, clientScopeId, mapperId string) (*OpenIdAudienceResolveProtocolMapper, error) { + var protocolMapper *protocolMapper + + err := keycloakClient.get(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), &protocolMapper, nil) + if err != nil { + return nil, err + } + + return protocolMapper.convertToOpenIdAudienceResolveProtocolMapper(realmId, clientId, clientScopeId) +} + +func (keycloakClient *KeycloakClient) DeleteOpenIdAudienceResolveProtocolMapper(realmId, clientId, clientScopeId, mapperId string) error { + return keycloakClient.delete(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), nil) +} + +func (keycloakClient *KeycloakClient) NewOpenIdAudienceResolveProtocolMapper(mapper *OpenIdAudienceResolveProtocolMapper) error { + path := protocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId) + + _, location, err := keycloakClient.post(path, mapper.convertToGenericProtocolMapper()) + if err != nil { + return err + } + + mapper.Id = getIdFromLocationHeader(location) + + return nil +} + +func (keycloakClient *KeycloakClient) UpdateOpenIdAudienceResolveProtocolMapper(mapper *OpenIdAudienceResolveProtocolMapper) error { + path := individualProtocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id) + + return keycloakClient.put(path, mapper.convertToGenericProtocolMapper()) +} + +func (keycloakClient *KeycloakClient) ValidateOpenIdAudienceResolveProtocolMapper(mapper *OpenIdAudienceResolveProtocolMapper) error { + if mapper.ClientId == "" && mapper.ClientScopeId == "" { + return fmt.Errorf("validation error: one of ClientId or ClientScopeId must be set") + } + + protocolMappers, err := keycloakClient.listGenericProtocolMappers(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/provider/provider.go b/provider/provider.go index 89a9bee98..4d16e8042 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -56,6 +56,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider { "keycloak_openid_full_name_protocol_mapper": resourceKeycloakOpenIdFullNameProtocolMapper(), "keycloak_openid_hardcoded_claim_protocol_mapper": resourceKeycloakOpenIdHardcodedClaimProtocolMapper(), "keycloak_openid_audience_protocol_mapper": resourceKeycloakOpenIdAudienceProtocolMapper(), + "keycloak_openid_audience_resolve_protocol_mapper": resourceKeycloakOpenIdAudienceResolveProtocolMapper(), "keycloak_openid_hardcoded_role_protocol_mapper": resourceKeycloakOpenIdHardcodedRoleProtocolMapper(), "keycloak_openid_user_realm_role_protocol_mapper": resourceKeycloakOpenIdUserRealmRoleProtocolMapper(), "keycloak_openid_user_client_role_protocol_mapper": resourceKeycloakOpenIdUserClientRoleProtocolMapper(), diff --git a/provider/resource_keycloak_openid_audience_resolve_protocol_mapper.go b/provider/resource_keycloak_openid_audience_resolve_protocol_mapper.go new file mode 100644 index 000000000..19cb0a8bc --- /dev/null +++ b/provider/resource_keycloak_openid_audience_resolve_protocol_mapper.go @@ -0,0 +1,120 @@ +package provider + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func resourceKeycloakOpenIdAudienceResolveProtocolMapper() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakOpenIdAudienceResolveProtocolMapperCreate, + Read: resourceKeycloakOpenIdAudienceResolveProtocolMapperRead, + //Update: resourceKeycloakOpenIdAudienceResolveProtocolMapperUpdate, + Delete: resourceKeycloakOpenIdAudienceResolveProtocolMapperDelete, + Importer: &schema.ResourceImporter{ + // import a mapper tied to a client: + // {{realmId}}/client/{{clientId}}/{{protocolMapperId}} + // or a client scope: + // {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}} + State: genericProtocolMapperImport, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "A human-friendly name that will appear in the Keycloak console.", + Default: "audience resolve", + }, + "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"}, + }, + }, + } +} + +func mapFromDataToOpenIdAudienceResolveProtocolMapper(data *schema.ResourceData) *keycloak.OpenIdAudienceResolveProtocolMapper { + return &keycloak.OpenIdAudienceResolveProtocolMapper{ + 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), + } +} + +func mapFromOpenIdAudienceResolveMapperToData(mapper *keycloak.OpenIdAudienceResolveProtocolMapper, 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) + } +} + +func resourceKeycloakOpenIdAudienceResolveProtocolMapperCreate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + openIdAudienceResolveMapper := mapFromDataToOpenIdAudienceResolveProtocolMapper(data) + + err := keycloakClient.ValidateOpenIdAudienceResolveProtocolMapper(openIdAudienceResolveMapper) + if err != nil { + return err + } + + err = keycloakClient.NewOpenIdAudienceResolveProtocolMapper(openIdAudienceResolveMapper) + if err != nil { + return err + } + + mapFromOpenIdAudienceResolveMapperToData(openIdAudienceResolveMapper, data) + + return resourceKeycloakOpenIdAudienceResolveProtocolMapperRead(data, meta) +} + +func resourceKeycloakOpenIdAudienceResolveProtocolMapperRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientId := data.Get("client_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + openIdAudienceResolveMapper, err := keycloakClient.GetOpenIdAudienceResolveProtocolMapper(realmId, clientId, clientScopeId, data.Id()) + if err != nil { + return handleNotFoundError(err, data) + } + + mapFromOpenIdAudienceResolveMapperToData(openIdAudienceResolveMapper, data) + + return nil +} + +func resourceKeycloakOpenIdAudienceResolveProtocolMapperDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientId := data.Get("client_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + return keycloakClient.DeleteOpenIdAudienceResolveProtocolMapper(realmId, clientId, clientScopeId, data.Id()) +} diff --git a/provider/resource_keycloak_openid_audience_resolve_protocol_mapper_test.go b/provider/resource_keycloak_openid_audience_resolve_protocol_mapper_test.go new file mode 100644 index 000000000..3dddf64ed --- /dev/null +++ b/provider/resource_keycloak_openid_audience_resolve_protocol_mapper_test.go @@ -0,0 +1,266 @@ +package provider + +import ( + "fmt" + "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 TestAccKeycloakOpenIdAudienceResolveProtocolMapper_basicClient(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc") + + resourceName := "keycloak_openid_audience_resolve_protocol_mapper.audience_resolve_mapper_client" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdAudienceResolveProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdAudienceResolveProtocolMapper_basic_client(clientId), + Check: testKeycloakOpenIdAudienceResolveProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdAudienceResolveProtocolMapper_basicClientScope(t *testing.T) { + t.Parallel() + clientScopeId := acctest.RandomWithPrefix("tf-acc") + + resourceName := "keycloak_openid_audience_resolve_protocol_mapper.audience_resolve_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdAudienceResolveProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdAudienceResolveProtocolMapper_basic_clientScope(clientScopeId), + Check: testKeycloakOpenIdAudienceResolveProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdAudienceResolveProtocolMapper_import(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc") + clientScopeId := acctest.RandomWithPrefix("tf-acc") + + clientResourceName := "keycloak_openid_audience_resolve_protocol_mapper.audience_resolve_mapper_client" + clientScopeResourceName := "keycloak_openid_audience_resolve_protocol_mapper.audience_resolve_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdAudienceResolveProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdAudienceResolveProtocolMapper_import(clientId, clientScopeId), + Check: resource.ComposeTestCheckFunc( + testKeycloakOpenIdAudienceResolveProtocolMapperExists(clientResourceName), + testKeycloakOpenIdAudienceResolveProtocolMapperExists(clientScopeResourceName), + ), + }, + { + ResourceName: clientResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getGenericProtocolMapperIdForClient(clientResourceName), + }, + { + ResourceName: clientScopeResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getGenericProtocolMapperIdForClientScope(clientScopeResourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdAudienceResolveProtocolMapper_createAfterManualDestroy(t *testing.T) { + t.Parallel() + var mapper = &keycloak.OpenIdAudienceResolveProtocolMapper{} + + clientId := acctest.RandomWithPrefix("tf-acc") + + resourceName := "keycloak_openid_audience_resolve_protocol_mapper.audience_resolve_mapper_client" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdAudienceResolveProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdAudienceResolveProtocolMapper_basic_client(clientId), + Check: testKeycloakOpenIdAudienceResolveProtocolMapperFetch(resourceName, mapper), + }, + { + PreConfig: func() { + err := keycloakClient.DeleteOpenIdAudienceResolveProtocolMapper(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id) + if err != nil { + t.Error(err) + } + }, + Config: testKeycloakOpenIdAudienceResolveProtocolMapper_basic_client(clientId), + Check: testKeycloakOpenIdAudienceResolveProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdAudienceResolveProtocolMapper_updateClientScopeForceNew(t *testing.T) { + t.Parallel() + clientScopeId := acctest.RandomWithPrefix("tf-acc") + newClientScopeId := acctest.RandomWithPrefix("tf-acc") + resourceName := "keycloak_openid_audience_resolve_protocol_mapper.audience_resolve_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdAudienceResolveProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdAudienceResolveProtocolMapper_basic_clientScope(clientScopeId), + Check: testKeycloakOpenIdAudienceResolveProtocolMapperExists(resourceName), + }, + { + Config: testKeycloakOpenIdAudienceResolveProtocolMapper_basic_clientScope(newClientScopeId), + Check: testKeycloakOpenIdAudienceResolveProtocolMapperExists(resourceName), + }, + }, + }) +} + +func testAccKeycloakOpenIdAudienceResolveProtocolMapperDestroy() resource.TestCheckFunc { + return func(state *terraform.State) error { + for resourceName, rs := range state.RootModule().Resources { + if rs.Type != "keycloak_openid_audience_resolve_protocol_mapper" { + continue + } + + mapper, _ := getAudienceResolveMapperUsingState(state, resourceName) + + if mapper != nil { + return fmt.Errorf("openid audience protocol mapper with id %s still exists", rs.Primary.ID) + } + } + + return nil + } +} + +func testKeycloakOpenIdAudienceResolveProtocolMapperExists(resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + _, err := getAudienceResolveMapperUsingState(state, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testKeycloakOpenIdAudienceResolveProtocolMapperFetch(resourceName string, mapper *keycloak.OpenIdAudienceResolveProtocolMapper) resource.TestCheckFunc { + return func(state *terraform.State) error { + fetchedMapper, err := getAudienceResolveMapperUsingState(state, resourceName) + if err != nil { + return err + } + + mapper.Id = fetchedMapper.Id + mapper.Name = fetchedMapper.Name + mapper.ClientId = fetchedMapper.ClientId + mapper.ClientScopeId = fetchedMapper.ClientScopeId + mapper.RealmId = fetchedMapper.RealmId + + return nil + } +} + +func getAudienceResolveMapperUsingState(state *terraform.State, resourceName string) (*keycloak.OpenIdAudienceResolveProtocolMapper, 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.GetOpenIdAudienceResolveProtocolMapper(realm, clientId, clientScopeId, id) +} + +func testKeycloakOpenIdAudienceResolveProtocolMapper_basic_client(clientId 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_audience_resolve_protocol_mapper" "audience_resolve_mapper_client" { + name = "a-custom-name" + realm_id = data.keycloak_realm.realm.id + client_id = "${keycloak_openid_client.openid_client.id}" +}`, testAccRealm.Realm, clientId) +} + +func testKeycloakOpenIdAudienceResolveProtocolMapper_basic_clientScope(clientScopeId 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_audience_resolve_protocol_mapper" "audience_resolve_mapper_client_scope" { + realm_id = data.keycloak_realm.realm.id + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" +}`, testAccRealm.Realm, clientScopeId) +} + +func testKeycloakOpenIdAudienceResolveProtocolMapper_import(clientId, clientScopeId 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_audience_resolve_protocol_mapper" "audience_resolve_mapper_client" { + realm_id = data.keycloak_realm.realm.id + client_id = "${keycloak_openid_client.openid_client.id}" +} + +resource "keycloak_openid_client_scope" "client_scope" { + name = "%s" + realm_id = data.keycloak_realm.realm.id +} + +resource "keycloak_openid_audience_resolve_protocol_mapper" "audience_resolve_mapper_client_scope" { + realm_id = data.keycloak_realm.realm.id + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + +}`, testAccRealm.Realm, clientId, clientScopeId) +}