diff --git a/docs/resources/keycloak_openid_user_realm_role_protocol_mapper.md b/docs/resources/keycloak_openid_user_realm_role_protocol_mapper.md new file mode 100644 index 000000000..e1df5316a --- /dev/null +++ b/docs/resources/keycloak_openid_user_realm_role_protocol_mapper.md @@ -0,0 +1,90 @@ +# keycloak_openid_user_realm_role_protocol_mapper + +Allows for creating and managing user realm role protocol mappers within +Keycloak. + +User realm role protocol mappers allow you to define a claim containing the list of the realm roles. +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 = "test-client" + + name = "test client" + enabled = true + + access_type = "CONFIDENTIAL" + valid_redirect_uris = [ + "http://localhost:8080/openid-callback" + ] +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + name = "user-realm-role-mapper" + + claim_name = "foo" +} +``` + +### 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_user_realm_role_protocol_mapper" "user_realm_role_mapper" { + realm_id = "${keycloak_realm.realm.id}" + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + name = "user-realm-role-mapper" + + claim_name = "foo" +} +``` + +### Argument Reference + +The following arguments are supported: + +- `realm_id` - (Required) The realm this protocol mapper exists within. +- `client_id` - (Required if `client_scope_id` is not specified) The client this protocol mapper is attached to. +- `client_scope_id` - (Required if `client_id` is not specified) The client scope this protocol mapper is attached to. +- `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_type` - (Optional) The claim type used when serializing JSON tokens. Can be one of `String`, `long`, `int`, or `boolean`. Defaults to `String`. +- `multivalued` - (Optional) Indicates if attribute supports multiple values. If true, then the list of all values of this attribute will be set as claim. If false, then just first value will be set as claim. Defaults to `true`. +- `realm_role_prefix` - (Optional) A prefix for each Realm Role. +- `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`. + +### 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_user_realm_role_protocol_mapper.user_realm_role_mapper my-realm/client/a7202154-8793-4656-b655-1dd18c181e14/71602afa-f7d1-4788-8c49-ef8fd00af0f4 +$ terraform import keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper my-realm/client-scope/b799ea7e-73ee-4a73-990a-1eafebe8e20a/71602afa-f7d1-4788-8c49-ef8fd00af0f4 +``` diff --git a/example/main.tf b/example/main.tf index e24bc6caf..e8df7ef37 100644 --- a/example/main.tf +++ b/example/main.tf @@ -355,6 +355,22 @@ resource "keycloak_openid_hardcoded_claim_protocol_mapper" "hardcoded_claim_clie claim_value = "bar" } +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_client" { + name = "tf-test-open-id-user-realm-role-claim-protocol-mapper-client" + realm_id = "${keycloak_realm.test.id}" + client_id = "${keycloak_openid_client.test_client.id}" + + claim_name = "foo" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_client_scope" { + name = "tf-test-open-id-user-realm-role-protocol-mapper-client-scope" + realm_id = "${keycloak_realm.test.id}" + client_scope_id = "${keycloak_openid_client_scope.test_default_client_scope.id}" + + claim_name = "foo" +} + resource "keycloak_openid_client" "bearer_only_client" { client_id = "test-bearer-only-client" name = "test-bearer-only-client" diff --git a/keycloak/openid_user_realm_role_protocol_mapper.go b/keycloak/openid_user_realm_role_protocol_mapper.go new file mode 100644 index 000000000..20faaef9b --- /dev/null +++ b/keycloak/openid_user_realm_role_protocol_mapper.go @@ -0,0 +1,133 @@ +package keycloak + +import ( + "fmt" + "strconv" +) + +type OpenIdUserRealmRoleProtocolMapper struct { + Id string + Name string + RealmId string + ClientId string + ClientScopeId string + + AddToIdToken bool + AddToAccessToken bool + AddToUserInfo bool + + RealmRolePrefix string + Multivalued bool + ClaimName string + ClaimValueType string +} + +func (mapper *OpenIdUserRealmRoleProtocolMapper) convertToGenericProtocolMapper() *protocolMapper { + return &protocolMapper{ + Id: mapper.Id, + Name: mapper.Name, + Protocol: "openid-connect", + ProtocolMapper: "oidc-usermodel-realm-role-mapper", + Config: map[string]string{ + addToIdTokenField: strconv.FormatBool(mapper.AddToIdToken), + addToAccessTokenField: strconv.FormatBool(mapper.AddToAccessToken), + addToUserInfoField: strconv.FormatBool(mapper.AddToUserInfo), + claimNameField: mapper.ClaimName, + claimValueTypeField: mapper.ClaimValueType, + multivaluedField: strconv.FormatBool(mapper.Multivalued), + userRealmRoleMappingRolePrefixField: mapper.RealmRolePrefix, + }, + } +} + +func (protocolMapper *protocolMapper) convertToOpenIdUserRealmRoleProtocolMapper(realmId, clientId, clientScopeId string) (*OpenIdUserRealmRoleProtocolMapper, error) { + addToIdToken, err := strconv.ParseBool(protocolMapper.Config[addToIdTokenField]) + if err != nil { + return nil, err + } + + addToAccessToken, err := strconv.ParseBool(protocolMapper.Config[addToAccessTokenField]) + if err != nil { + return nil, err + } + + addToUserInfo, err := strconv.ParseBool(protocolMapper.Config[addToUserInfoField]) + if err != nil { + return nil, err + } + + multivalued, err := strconv.ParseBool(protocolMapper.Config[multivaluedField]) + if err != nil { + return nil, err + } + + return &OpenIdUserRealmRoleProtocolMapper{ + Id: protocolMapper.Id, + Name: protocolMapper.Name, + RealmId: realmId, + ClientId: clientId, + ClientScopeId: clientScopeId, + + AddToIdToken: addToIdToken, + AddToAccessToken: addToAccessToken, + AddToUserInfo: addToUserInfo, + + ClaimName: protocolMapper.Config[claimNameField], + ClaimValueType: protocolMapper.Config[claimValueTypeField], + Multivalued: multivalued, + RealmRolePrefix: protocolMapper.Config[userRealmRoleMappingRolePrefixField], + }, nil +} + +func (keycloakClient *KeycloakClient) GetOpenIdUserRealmRoleProtocolMapper(realmId, clientId, clientScopeId, mapperId string) (*OpenIdUserRealmRoleProtocolMapper, error) { + var protocolMapper *protocolMapper + + err := keycloakClient.get(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), &protocolMapper, nil) + if err != nil { + return nil, err + } + + return protocolMapper.convertToOpenIdUserRealmRoleProtocolMapper(realmId, clientId, clientScopeId) +} + +func (keycloakClient *KeycloakClient) DeleteOpenIdUserRealmRoleProtocolMapper(realmId, clientId, clientScopeId, mapperId string) error { + return keycloakClient.delete(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), nil) +} + +func (keycloakClient *KeycloakClient) NewOpenIdUserRealmRoleProtocolMapper(mapper *OpenIdUserRealmRoleProtocolMapper) 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) UpdateOpenIdUserRealmRoleProtocolMapper(mapper *OpenIdUserRealmRoleProtocolMapper) error { + path := individualProtocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id) + + return keycloakClient.put(path, mapper.convertToGenericProtocolMapper()) +} + +func (keycloakClient *KeycloakClient) ValidateOpenIdUserRealmRoleProtocolMapper(mapper *OpenIdUserRealmRoleProtocolMapper) 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/keycloak/protocol_mapper.go b/keycloak/protocol_mapper.go index 92693e1c1..a72d57e4b 100644 --- a/keycloak/protocol_mapper.go +++ b/keycloak/protocol_mapper.go @@ -12,21 +12,22 @@ type protocolMapper struct { } var ( - addToAccessTokenField = "access.token.claim" - addToIdTokenField = "id.token.claim" - addToUserInfoField = "userinfo.token.claim" - attributeNameField = "attribute.name" - attributeNameFormatField = "attribute.nameformat" - claimNameField = "claim.name" - claimValueField = "claim.value" - claimValueTypeField = "jsonType.label" - friendlyNameField = "friendly.name" - fullPathField = "full.path" - includedClientAudienceField = "included.client.audience" - includedCustomAudienceField = "included.custom.audience" - multivaluedField = "multivalued" - userAttributeField = "user.attribute" - userPropertyField = "user.attribute" + addToAccessTokenField = "access.token.claim" + addToIdTokenField = "id.token.claim" + addToUserInfoField = "userinfo.token.claim" + attributeNameField = "attribute.name" + attributeNameFormatField = "attribute.nameformat" + claimNameField = "claim.name" + claimValueField = "claim.value" + claimValueTypeField = "jsonType.label" + friendlyNameField = "friendly.name" + fullPathField = "full.path" + includedClientAudienceField = "included.client.audience" + includedCustomAudienceField = "included.custom.audience" + multivaluedField = "multivalued" + userAttributeField = "user.attribute" + userPropertyField = "user.attribute" + userRealmRoleMappingRolePrefixField = "usermodel.realmRoleMapping.rolePrefix" ) func protocolMapperPath(realmId, clientId, clientScopeId string) string { diff --git a/mkdocs.yml b/mkdocs.yml index 7433ada49..ba7350a70 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,6 +25,7 @@ nav: - keycloak_openid_full_name_protocol_mapper: resources/keycloak_openid_full_name_protocol_mapper.md - keycloak_openid_audience_protocol_mapper: resources/keycloak_openid_audience_protocol_mapper.md - keycloak_openid_hardcoded_role_protocol_mapper: resources/keycloak_openid_hardcoded_role_protocol_mapper.md + - keycloak_openid_user_realm_role_protocol_mapper: resources/keycloak_openid_user_realm_role_protocol_mapper.md - keycloak_saml_client: resources/keycloak_saml_client.md - keycloak_saml_user_attribute_protocol_mapper: resources/keycloak_saml_user_attribute_protocol_mapper.md - keycloak_saml_user_property_protocol_mapper: resources/keycloak_saml_user_property_protocol_mapper.md diff --git a/provider/generic_protocol_mapper_validation_test.go b/provider/generic_protocol_mapper_validation_test.go index d6a359ec2..a3e54e9fb 100644 --- a/provider/generic_protocol_mapper_validation_test.go +++ b/provider/generic_protocol_mapper_validation_test.go @@ -2,10 +2,11 @@ package provider import ( "fmt" - "github.com/hashicorp/terraform/helper/acctest" - "github.com/hashicorp/terraform/helper/resource" "regexp" "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" ) /* @@ -256,6 +257,30 @@ func TestAccKeycloakOpenIdHardcodedClaimProtocolMapper_clientScopeDuplicateNameV }) } +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_clientScopeDuplicateNameValidation(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + mapperName := "terraform-user-realm-role-mapper-" + acctest.RandString(5) + + fullNameProtocolMapperResourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdUserRealmRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testGenericProtocolMapperValidation_clientScopeUserRealmRoleMapper(realmName, clientId, mapperName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(fullNameProtocolMapperResourceName), + }, + { + Config: testGenericProtocolMapperValidation_clientScopeUserRealmRoleAndHardcodedClaimMapper(realmName, clientId, mapperName), + ExpectError: regexp.MustCompile("validation error: a protocol mapper with name .+ already exists for this client"), + }, + }, + }) +} + /* * Protocol mappers must be attached to either a client or client scope. The following tests assert that errors are raised * if neither are specified. @@ -341,6 +366,22 @@ func TestAccKeycloakOpenIdHardcodedClaimProtocolMapper_validateClientOrClientSco }) } +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_validateClientOrClientScopeSet(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-user-realm-role-mapper-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_parentResourceValidation(realmName, mapperName), + ExpectError: regexp.MustCompile("validation error: one of ClientId or ClientScopeId must be set"), + }, + }, + }) +} + func testGenericProtocolMapperValidation_clientGroupMembershipMapper(realmName, clientId, mapperName string) string { return fmt.Sprintf(` resource "keycloak_realm" "realm" { @@ -566,6 +607,29 @@ resource "keycloak_openid_group_membership_protocol_mapper" "group_membership_ma }`, realmName, clientScopeId, mapperName) } +func testGenericProtocolMapperValidation_clientUserRealmRoleMapper(realmName, clientId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + + claim_name = "foo" + claim_value_type = "String" +}`, realmName, clientId, mapperName) +} + func testGenericProtocolMapperValidation_clientScopeFullNameMapper(realmName, clientId, mapperName string) string { return fmt.Sprintf(` resource "keycloak_realm" "realm" { @@ -624,6 +688,25 @@ resource "keycloak_openid_user_property_protocol_mapper" "user_property_mapper_c }`, realmName, clientId, mapperName) } +func testGenericProtocolMapperValidation_clientScopeUserRealmRoleMapper(realmName, clientId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client_scope" "client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + claim_name = "bar-property" +}`, realmName, clientId, mapperName) + +} func testGenericProtocolMapperValidation_clientScopeFullNameAndGroupMembershipMapper(realmName, clientScopeId, mapperName string) string { return fmt.Sprintf(` resource "keycloak_realm" "realm" { @@ -734,6 +817,35 @@ resource "keycloak_openid_hardcoded_claim_protocol_mapper" "hardcoded_claim_mapp }`, realmName, clientId, mapperName, mapperName) } +func testGenericProtocolMapperValidation_clientScopeUserRealmRoleAndHardcodedClaimMapper(realmName, clientId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client_scope" "client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + claim_name = "bar-property" +} + +resource "keycloak_openid_hardcoded_claim_protocol_mapper" "hardcoded_claim_mapper_client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + + claim_name = "foo" + claim_value = "bar" + claim_value_type = "String" +}`, realmName, clientId, mapperName, mapperName) +} + func testKeycloakOpenIdFullNameProtocolMapper_parentResourceValidation(realmName, mapperName string) string { return fmt.Sprintf(` resource "keycloak_realm" "realm" { @@ -802,3 +914,18 @@ resource "keycloak_openid_hardcoded_claim_protocol_mapper" "hardcoded_claim_mapp claim_value_type = "String" }`, realmName, mapperName) } + +func testKeycloakOpenIdUserRealmRoleProtocolMapper_parentResourceValidation(realmName, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + + claim_name = "foo" + claim_value_type = "String" +}`, realmName, mapperName) +} diff --git a/provider/provider.go b/provider/provider.go index 7355a5b29..06aa3b1e1 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -36,6 +36,7 @@ func KeycloakProvider() *schema.Provider { "keycloak_openid_hardcoded_claim_protocol_mapper": resourceKeycloakOpenIdHardcodedClaimProtocolMapper(), "keycloak_openid_audience_protocol_mapper": resourceKeycloakOpenIdAudienceProtocolMapper(), "keycloak_openid_hardcoded_role_protocol_mapper": resourceKeycloakOpenIdHardcodedRoleProtocolMapper(), + "keycloak_openid_user_realm_role_protocol_mapper": resourceKeycloakOpenIdUserRealmRoleProtocolMapper(), "keycloak_openid_client_default_scopes": resourceKeycloakOpenidClientDefaultScopes(), "keycloak_openid_client_optional_scopes": resourceKeycloakOpenidClientOptionalScopes(), "keycloak_saml_client": resourceKeycloakSamlClient(), diff --git a/provider/resource_keycloak_openid_user_realm_role_protocol_mapper.go b/provider/resource_keycloak_openid_user_realm_role_protocol_mapper.go new file mode 100644 index 000000000..54ea357b9 --- /dev/null +++ b/provider/resource_keycloak_openid_user_realm_role_protocol_mapper.go @@ -0,0 +1,192 @@ +package provider + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func resourceKeycloakOpenIdUserRealmRoleProtocolMapper() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakOpenIdUserRealmRoleProtocolMapperCreate, + Read: resourceKeycloakOpenIdUserRealmRoleProtocolMapperRead, + Update: resourceKeycloakOpenIdUserRealmRoleProtocolMapperUpdate, + Delete: resourceKeycloakOpenIdUserRealmRoleProtocolMapperDelete, + 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, + 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"}, + }, + "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, + }, + "claim_value_type": { + Type: schema.TypeString, + Optional: true, + Description: "Claim type used when serializing tokens.", + Default: "String", + ValidateFunc: validation.StringInSlice([]string{"String", "long", "int", "boolean"}, true), + }, + "multivalued": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Indicates whether this attribute is a single value or an array of values.", + }, + "realm_role_prefix": { + Type: schema.TypeString, + Optional: true, + Description: "Prefix that will be added to each realm role.", + }, + }, + } +} + +func mapFromDataToOpenIdUserRealmRoleProtocolMapper(data *schema.ResourceData) *keycloak.OpenIdUserRealmRoleProtocolMapper { + return &keycloak.OpenIdUserRealmRoleProtocolMapper{ + 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), + + ClaimName: data.Get("claim_name").(string), + ClaimValueType: data.Get("claim_value_type").(string), + RealmRolePrefix: data.Get("realm_role_prefix").(string), + Multivalued: data.Get("multivalued").(bool), + } +} + +func mapFromOpenIdUserRealmRoleMapperToData(mapper *keycloak.OpenIdUserRealmRoleProtocolMapper, 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("claim_name", mapper.ClaimName) + data.Set("claim_value_type", mapper.ClaimValueType) + data.Set("realm_role_prefix", mapper.RealmRolePrefix) + data.Set("multivalued", mapper.Multivalued) +} + +func resourceKeycloakOpenIdUserRealmRoleProtocolMapperCreate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + openIdUserRealmRoleMapper := mapFromDataToOpenIdUserRealmRoleProtocolMapper(data) + + err := keycloakClient.ValidateOpenIdUserRealmRoleProtocolMapper(openIdUserRealmRoleMapper) + if err != nil { + return err + } + + err = keycloakClient.NewOpenIdUserRealmRoleProtocolMapper(openIdUserRealmRoleMapper) + if err != nil { + return err + } + + mapFromOpenIdUserRealmRoleMapperToData(openIdUserRealmRoleMapper, data) + + return resourceKeycloakOpenIdUserRealmRoleProtocolMapperRead(data, meta) +} + +func resourceKeycloakOpenIdUserRealmRoleProtocolMapperRead(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) + + openIdUserRealmRoleMapper, err := keycloakClient.GetOpenIdUserRealmRoleProtocolMapper(realmId, clientId, clientScopeId, data.Id()) + if err != nil { + return handleNotFoundError(err, data) + } + + mapFromOpenIdUserRealmRoleMapperToData(openIdUserRealmRoleMapper, data) + + return nil +} + +func resourceKeycloakOpenIdUserRealmRoleProtocolMapperUpdate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + openIdUserRealmRoleMapper := mapFromDataToOpenIdUserRealmRoleProtocolMapper(data) + + err := keycloakClient.ValidateOpenIdUserRealmRoleProtocolMapper(openIdUserRealmRoleMapper) + if err != nil { + return err + } + + err = keycloakClient.UpdateOpenIdUserRealmRoleProtocolMapper(openIdUserRealmRoleMapper) + if err != nil { + return err + } + + return resourceKeycloakOpenIdUserRealmRoleProtocolMapperRead(data, meta) +} + +func resourceKeycloakOpenIdUserRealmRoleProtocolMapperDelete(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.DeleteOpenIdUserRealmRoleProtocolMapper(realmId, clientId, clientScopeId, data.Id()) +} diff --git a/provider/resource_keycloak_openid_user_realm_role_protocol_mapper_test.go b/provider/resource_keycloak_openid_user_realm_role_protocol_mapper_test.go new file mode 100644 index 000000000..dc5f9194d --- /dev/null +++ b/provider/resource_keycloak_openid_user_realm_role_protocol_mapper_test.go @@ -0,0 +1,432 @@ +package provider + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_basicClient(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-user-realm-role-mapper-" + acctest.RandString(5) + + resourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper_client" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdUserRealmRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_basic_client(realmName, clientId, mapperName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_basicClientScope(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + clientScopeId := "terraform-client-scope-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-user-realm-role-mapper-" + acctest.RandString(5) + + resourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdUserRealmRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_basic_clientScope(realmName, clientScopeId, mapperName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_import(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + clientId := "terraform-openid-client-" + acctest.RandString(10) + clientScopeId := "terraform-client-scope-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-user-realm-role-mapper-" + acctest.RandString(5) + + clientResourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper_client" + clientScopeResourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdFullNameProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_import(realmName, clientId, clientScopeId, mapperName), + Check: resource.ComposeTestCheckFunc( + testKeycloakOpenIdUserRealmRoleProtocolMapperExists(clientResourceName), + testKeycloakOpenIdUserRealmRoleProtocolMapperExists(clientScopeResourceName), + ), + }, + { + ResourceName: clientResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getGenericProtocolMapperIdForClient(clientResourceName), + }, + { + ResourceName: clientScopeResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getGenericProtocolMapperIdForClientScope(clientScopeResourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_update(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-user-realm-role-mapper-" + acctest.RandString(5) + + claimName := "claim-name-" + acctest.RandString(10) + updatedClaimName := "claim-name-update-" + acctest.RandString(10) + + resourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdUserRealmRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_claim(realmName, clientId, mapperName, claimName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_claim(realmName, clientId, mapperName, updatedClaimName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_createAfterManualDestroy(t *testing.T) { + var mapper = &keycloak.OpenIdUserRealmRoleProtocolMapper{} + + realmName := "terraform-realm-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-user-realm-role-mapper-" + acctest.RandString(5) + + resourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper_client" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdUserRealmRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_basic_client(realmName, clientId, mapperName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperFetch(resourceName, mapper), + }, + { + PreConfig: func() { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + err := keycloakClient.DeleteOpenIdUserRealmRoleProtocolMapper(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id) + if err != nil { + t.Error(err) + } + }, + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_basic_client(realmName, clientId, mapperName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_validateClaimValueType(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-user-realm-role-mapper-" + acctest.RandString(10) + invalidClaimValueType := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdUserRealmRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_validateClaimValueType(realmName, mapperName, invalidClaimValueType), + ExpectError: regexp.MustCompile("expected claim_value_type to be one of .+ got " + invalidClaimValueType), + }, + }, + }) +} + +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_updateClientIdForceNew(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + updatedClientId := "terraform-client-update-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-user-realm-role-mapper-" + acctest.RandString(5) + + claimName := "claim-name-" + acctest.RandString(10) + resourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdUserRealmRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_claim(realmName, clientId, mapperName, claimName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_claim(realmName, updatedClientId, mapperName, claimName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_updateClientScopeForceNew(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-user-realm-role-mapper-" + acctest.RandString(5) + clientScopeId := "terraform-client-" + acctest.RandString(10) + newClientScopeId := "terraform-client-scope-" + acctest.RandString(10) + resourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdUserRealmRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_basic_clientScope(realmName, clientScopeId, mapperName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_basic_clientScope(realmName, newClientScopeId, mapperName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdUserRealmRoleProtocolMapper_updateRealmIdForceNew(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + newRealmName := "terraform-realm-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-user-realm-role-mapper-" + acctest.RandString(5) + + claimName := "claim-name-" + acctest.RandString(10) + resourceName := "keycloak_openid_user_realm_role_protocol_mapper.user_realm_role_mapper" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdUserRealmRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_claim(realmName, clientId, mapperName, claimName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + { + Config: testKeycloakOpenIdUserRealmRoleProtocolMapper_claim(newRealmName, clientId, mapperName, claimName), + Check: testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func testAccKeycloakOpenIdUserRealmRoleProtocolMapperDestroy() resource.TestCheckFunc { + return func(state *terraform.State) error { + for resourceName, rs := range state.RootModule().Resources { + if rs.Type != "keycloak_openid_user_realm_role_protocol_mapper" { + continue + } + + mapper, _ := getUserRealmRoleMapperUsingState(state, resourceName) + + if mapper != nil { + return fmt.Errorf("openid user attribute protocol mapper with id %s still exists", rs.Primary.ID) + } + } + + return nil + } +} + +func testKeycloakOpenIdUserRealmRoleProtocolMapperExists(resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + _, err := getUserRealmRoleMapperUsingState(state, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testKeycloakOpenIdUserRealmRoleProtocolMapperFetch(resourceName string, mapper *keycloak.OpenIdUserRealmRoleProtocolMapper) resource.TestCheckFunc { + return func(state *terraform.State) error { + fetchedMapper, err := getUserRealmRoleMapperUsingState(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 getUserRealmRoleMapperUsingState(state *terraform.State, resourceName string) (*keycloak.OpenIdUserRealmRoleProtocolMapper, 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"] + + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + return keycloakClient.GetOpenIdUserRealmRoleProtocolMapper(realm, clientId, clientScopeId, id) +} + +func testKeycloakOpenIdUserRealmRoleProtocolMapper_basic_client(realmName, clientId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + + claim_name = "foo" + claim_value_type = "String" +}`, realmName, clientId, mapperName) +} + +func testKeycloakOpenIdUserRealmRoleProtocolMapper_basic_clientScope(realmName, clientScopeId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client_scope" "client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + + claim_name = "foo" + claim_value_type = "String" +}`, realmName, clientScopeId, mapperName) +} + +func testKeycloakOpenIdUserRealmRoleProtocolMapper_claim(realmName, clientId, mapperName, claimName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + + claim_name = "%s" + claim_value_type = "String" +}`, realmName, clientId, mapperName, claimName) +} + +func testKeycloakOpenIdUserRealmRoleProtocolMapper_import(realmName, clientId, clientScopeId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + + claim_name = "foo" + claim_value_type = "String" +} + +resource "keycloak_openid_client_scope" "client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + + claim_name = "foo" + claim_value_type = "String" +}`, realmName, clientId, mapperName, clientScopeId, mapperName) +} + +func testKeycloakOpenIdUserRealmRoleProtocolMapper_validateClaimValueType(realmName, mapperName, claimValueType string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "openid-client" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_user_realm_role_protocol_mapper" "user_realm_role_mapper_validation" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + + claim_name = "foo" + claim_value_type = "%s" +}`, realmName, mapperName, claimValueType) +}