From d5154d1cc56b7ba2d635224865e15b386776e78c Mon Sep 17 00:00:00 2001 From: Daniel Fleming Date: Tue, 8 Feb 2022 08:47:14 -0500 Subject: [PATCH] :sparkles: Add Claims Mapping Policy Assignment Resource Adds support for the claims mapping policy assignment resource so claims mapping policies can be assigned to a service principle with Terraform. Related to: - https://github.com/manicminer/hamilton/pull/147 - https://github.com/hashicorp/terraform-provider-azuread/issues/644 - https://docs.microsoft.com/en-us/graph/api/serviceprincipal-post-claimsmappingpolicies?view=graph-rest-1.0&tabs=http --- .../claims_mapping_policy_assignment.md | 46 ++++++ .../parse/claims_mapping_policy_assignment.go | 30 ++++ .../serviceprincipals/registration.go | 1 + ...ncipal_claims_mapping_policy_assignment.go | 155 ++++++++++++++++++ ...l_claims_mapping_policy_assignment_test.go | 114 +++++++++++++ 5 files changed, 346 insertions(+) create mode 100644 docs/resources/claims_mapping_policy_assignment.md create mode 100644 internal/services/serviceprincipals/parse/claims_mapping_policy_assignment.go create mode 100644 internal/services/serviceprincipals/service_principal_claims_mapping_policy_assignment.go create mode 100644 internal/services/serviceprincipals/service_principal_claims_mapping_policy_assignment_test.go diff --git a/docs/resources/claims_mapping_policy_assignment.md b/docs/resources/claims_mapping_policy_assignment.md new file mode 100644 index 0000000000..e50e378d59 --- /dev/null +++ b/docs/resources/claims_mapping_policy_assignment.md @@ -0,0 +1,46 @@ + +--- +subcategory: "Policies" +--- + +# Resource: claims_mapping_policy_assignment + +Manages a Claims Mapping Policy Assignment within Azure Active Directory. + +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires the following application roles: `Policy.ReadWrite.ApplicationConfiguration` + +When authenticated with a user principal, this resource requires one of the following directory roles: `Application Administrator` or `Global Administrator` + +## Example Usage + +```terraform +resource "azuread_claims_mapping_policy_assignment" "app" { + claims_mapping_policy_id = azuread_claims_mapping_policy.my_policy.id + service_principal_id = azuread_service_principal.my_principal.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `claims_mapping_policy_id` - (Required) The `id` of the claims mapping policy to assign. +* `service_principal_id` - (Required) The `id` of the service principal for the policy assignment. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the Claims Mapping Policy Assignment. + +## Import + +Claims Mapping Policy can be imported using the `id`, in the form `service-principal-uuid/azuread_claims_mapping_policy/claims-mapping-policy-uuid`, e.g: + +```shell +terraform import azuread_claims_mapping_policy_assignment.app 00000000-0000-0000-0000-000000000000/azuread_claims_mapping_policy/00000000-0000-0000-0000-000000000000 +``` diff --git a/internal/services/serviceprincipals/parse/claims_mapping_policy_assignment.go b/internal/services/serviceprincipals/parse/claims_mapping_policy_assignment.go new file mode 100644 index 0000000000..d3041791f9 --- /dev/null +++ b/internal/services/serviceprincipals/parse/claims_mapping_policy_assignment.go @@ -0,0 +1,30 @@ +package parse + +import "fmt" + +type ClaimsMappingPolicyAssignmentId struct { + ObjectSubResourceId + ServicePolicyId string + ClaimsMappingPolicyId string +} + +func NewClaimsMappingPolicyAssignmentID(ServicePolicyId, ClaimsMappingPolicyId string) ClaimsMappingPolicyAssignmentId { + return ClaimsMappingPolicyAssignmentId{ + ObjectSubResourceId: NewObjectSubResourceID(ServicePolicyId, "azuread_claims_mapping_policy", ClaimsMappingPolicyId), + ServicePolicyId: ServicePolicyId, + ClaimsMappingPolicyId: ClaimsMappingPolicyId, + } +} + +func ClaimsMappingPolicyAssignmentID(idString string) (*ClaimsMappingPolicyAssignmentId, error) { + id, err := ObjectSubResourceID(idString, "azuread_claims_mapping_policy") + if err != nil { + return nil, fmt.Errorf("unable to parse azuread_claims_mapping_policy ID: %v", err) + } + + return &ClaimsMappingPolicyAssignmentId{ + ObjectSubResourceId: *id, + ServicePolicyId: id.objectId, + ClaimsMappingPolicyId: id.subId, + }, nil +} diff --git a/internal/services/serviceprincipals/registration.go b/internal/services/serviceprincipals/registration.go index cba2724fbc..db8517b64d 100644 --- a/internal/services/serviceprincipals/registration.go +++ b/internal/services/serviceprincipals/registration.go @@ -31,6 +31,7 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ "azuread_claims_mapping_policy": servicePrincipalClaimsMappingPolicy(), + "azuread_claims_mapping_policy_assignment": servicePrincipalClaimsMappingPolicyAssignment(), "azuread_service_principal": servicePrincipalResource(), "azuread_service_principal_certificate": servicePrincipalCertificateResource(), "azuread_service_principal_delegated_permission_grant": servicePrincipalDelegatedPermissionGrantResource(), diff --git a/internal/services/serviceprincipals/service_principal_claims_mapping_policy_assignment.go b/internal/services/serviceprincipals/service_principal_claims_mapping_policy_assignment.go new file mode 100644 index 0000000000..7bb1bfbe18 --- /dev/null +++ b/internal/services/serviceprincipals/service_principal_claims_mapping_policy_assignment.go @@ -0,0 +1,155 @@ +package serviceprincipals + +import ( + "context" + "log" + "net/http" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/parse" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/manicminer/hamilton/msgraph" + "github.com/manicminer/hamilton/odata" +) + +func servicePrincipalClaimsMappingPolicyAssignment() *schema.Resource { + return &schema.Resource{ + CreateContext: servicePrincipalClaimsMappingPolicyAssignmentResourceCreate, + ReadContext: servicePrincipalClaimsMappingPolicyAssignmentResourceRead, + DeleteContext: servicePrincipalClaimsMappingPolicyAssignmentResourceDelete, + + Importer: tf.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.ObjectSubResourceID(id, "azuread_claims_mapping_policy") + return err + }), + + Schema: map[string]*schema.Schema{ + "claims_mapping_policy_id": { + Description: "ID of the claims mapping policy to assign", + ForceNew: true, + Type: schema.TypeString, + Required: true, + }, + + "service_principal_id": { + Description: "ID of the service principal to assign the policy to", + ForceNew: true, + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func servicePrincipalClaimsMappingPolicyAssignmentResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + spClient := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + policyClient := meta.(*clients.Client).ServicePrincipals.ClaimsMappingPolicyClient + + policyID := d.Get("claims_mapping_policy_id").(string) + policy, _, err := policyClient.Get(ctx, policyID, odata.Query{}) + if err != nil { + return tf.ErrorDiagF( + err, + "Could not find ClaimsMappingPolicy, claims_mapping_policy_id: %q", + policyID, + ) + } + + properties := msgraph.ServicePrincipal{ + DirectoryObject: msgraph.DirectoryObject{ + ID: utils.String(d.Get("service_principal_id").(string)), + }, + ClaimsMappingPolicies: &[]msgraph.ClaimsMappingPolicy{ + *policy, + }, + } + _, err = spClient.AssignClaimsMappingPolicy(ctx, &properties) + if err != nil { + return tf.ErrorDiagF( + err, + "Could not create ClaimsMappingPolicyAssignment, service_principal_id: %q, claims_mapping_policy_id: %q", + *properties.DirectoryObject.ID, + *(*properties.ClaimsMappingPolicies)[0].DirectoryObject.ID, + ) + } + + resourceID := parse.NewClaimsMappingPolicyAssignmentID( + *properties.DirectoryObject.ID, + *(*properties.ClaimsMappingPolicies)[0].DirectoryObject.ID, + ) + + d.SetId(resourceID.String()) + + return servicePrincipalClaimsMappingPolicyAssignmentResourceRead(ctx, d, meta) +} + +func servicePrincipalClaimsMappingPolicyAssignmentResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + + id, err := parse.ClaimsMappingPolicyAssignmentID(d.Id()) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Parsing Claims Mapping Policy Assignment ID %q", d.Id()) + } + + spID := id.ServicePolicyId + + policyList, status, err := client.ListClaimsMappingPolicy(ctx, spID) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Service Principal with Object ID %q was not found - removing claims mapping policy assignment from state!", spID) + d.SetId("") + return nil + } + + return tf.ErrorDiagF(err, "listing Claims Mapping Policy Assignments for Service Principal with object ID: %q", d.Id()) + } + + policyID := id.ClaimsMappingPolicyId + var foundPolicy *msgraph.ClaimsMappingPolicy + + // Check the assignment is found in the currently assigned policies + for _, policy := range *policyList { + if *policy.ID == policyID { + foundPolicy = &policy + break + } + } + if foundPolicy == nil { + d.SetId("") + log.Printf("[DEBUG] Claims Mapping Policy with Object ID %q was not found - removing assignment from state!", policyID) + return nil + } + + tf.Set(d, "service_principal_id", spID) + tf.Set(d, "claims_mapping_policy_id", policyID) + + return nil +} + +func servicePrincipalClaimsMappingPolicyAssignmentResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + + id, err := parse.ClaimsMappingPolicyAssignmentID(d.Id()) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Parsing Claims Mapping Policy Assignment ID %q", d.Id()) + } + + claimIDs := []string{id.ClaimsMappingPolicyId} + + spID := id.ServicePolicyId + + sp := msgraph.ServicePrincipal{ + DirectoryObject: msgraph.DirectoryObject{ + ID: &spID, + }, + } + _, err = client.RemoveClaimsMappingPolicy(ctx, &sp, &claimIDs) + if err != nil { + return tf.ErrorDiagF(err, "Could not Remove ClaimsMappingPolicyAssignment, service_principal_id: %q, claims_mapping_policy_ids: %q", spID, claimIDs) + } + + return servicePrincipalClaimsMappingPolicyAssignmentResourceRead(ctx, d, meta) +} diff --git a/internal/services/serviceprincipals/service_principal_claims_mapping_policy_assignment_test.go b/internal/services/serviceprincipals/service_principal_claims_mapping_policy_assignment_test.go new file mode 100644 index 0000000000..d5cbb3481f --- /dev/null +++ b/internal/services/serviceprincipals/service_principal_claims_mapping_policy_assignment_test.go @@ -0,0 +1,114 @@ +package serviceprincipals_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" +) + +type ServicePrincipalClaimsMappingPolicyAssignment struct{} + +func TestClaimsMappingPolicyAssignment_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_claims_mapping_policy_assignment", "test") + mappingPolicy := acceptance.TestData{ + ResourceName: "azuread_claims_mapping_policy.test2", + } + r := ServicePrincipalClaimsMappingPolicyAssignment{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basicClaimsMappingPolicyAssignment(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.updateClaimsMappingPolicyAssignment(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key( + "claims_mapping_policy_id", + ).MatchesOtherKey( + check.That(mappingPolicy.ResourceName).Key( + "id", + ), + ), + ), + }, + }) +} + +func (ServicePrincipalClaimsMappingPolicyAssignment) basicClaimsMappingPolicyAssignment(data acceptance.TestData) string { + c := ServicePrincipalClaimsMappingPolicy{} + return c.basicClaimsMappingPolicy(data) + ` + data "azuread_application_published_app_ids" "well_known" {} + + resource "azuread_service_principal" "msgraph" { + application_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + use_existing = true + } + resource "azuread_claims_mapping_policy_assignment" "test" { + service_principal_id = azuread_service_principal.msgraph.id + claims_mapping_policy_id = azuread_claims_mapping_policy.test.id + } + ` +} + +func (ServicePrincipalClaimsMappingPolicyAssignment) updateClaimsMappingPolicyAssignment(data acceptance.TestData) string { + return fmt.Sprintf(` + provider "azuread" {} + + data "azuread_application_published_app_ids" "well_known" {} + + resource "azuread_service_principal" "msgraph" { + application_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + use_existing = true + } + + resource "azuread_claims_mapping_policy" "test2" { + definition = [ + "{\"ClaimsMappingPolicy\":{\"Version\":1,\"IncludeBasicClaimSet\":\"false\",\"ClaimsSchema\": [{\"Source\":\"user\",\"ID\":\"employeeid\",\"SamlClaimType\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name\",\"JwtClaimType\":\"name\"},{\"Source\":\"company\",\"ID\":\"tenantcountry\",\"SamlClaimType\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country\",\"JwtClaimType\":\"country\"}]}}" + ] + description = "%[1]s" + display_name = "integration-%[1]s" + } + + resource "azuread_claims_mapping_policy_assignment" "test" { + service_principal_id = azuread_service_principal.msgraph.id + claims_mapping_policy_id = azuread_claims_mapping_policy.test2.id + } +`, data.RandomString) +} + +func (r ServicePrincipalClaimsMappingPolicyAssignment) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.ServicePrincipals.ServicePrincipalsClient + client.BaseClient.DisableRetries = true + + spID := state.Attributes["service_principal_id"] + policyList, status, err := client.ListClaimsMappingPolicy(ctx, spID) + if err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), fmt.Errorf("Service Policy with object ID %q does not exist", spID) + } + return utils.Bool(false), fmt.Errorf("failed to retrieve claims mapping policy assignments with service policy ID %q: %+v", spID, err) + } + + policyID := state.Attributes["claims_mapping_policy_id"] + + // Check the assignment is found in the currently assigned policies + for _, policy := range *policyList { + if *policy.ID == policyID { + return utils.Bool(true), nil + } + } + return utils.Bool(false), nil +}