diff --git a/docs/data-sources/access_package_catalog_role.md b/docs/data-sources/access_package_catalog_role.md new file mode 100644 index 0000000000..5d1ee1d535 --- /dev/null +++ b/docs/data-sources/access_package_catalog_role.md @@ -0,0 +1,49 @@ +--- +subcategory: "Identity Governance" +--- + +# Data Source: azuread_access_package_catalog_role + +Gets information about an access package catalog role. + +## API Permissions + +The following API permissions are required in order to use this data source. + +When authenticated with a service principal, this data source requires one of the following application roles: `EntitlementManagement.Read.All` or `Directory.Read.All` + +When authenticated with a user principal, this data source does not require any additional roles. + +## Example Usage (by Group Display Name) + +*Look up by display name* +```terraform +data "azuread_access_package_catalog_role" "example" { + display_name = "Catalog owner" +} +``` + +*Look up by object ID* +```terraform +data "azuread_access_package_catalog_role" "example" { + object_id = "00000000-0000-0000-0000-000000000000" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `display_name` - (Optional) Specifies the display name of the role. +* `object_id` - (Optional) Specifies the object ID of the role. + +~> One of `display_name` or `object_id` must be specified. + +## Attributes Reference + +The following attributes are exported: + +* `description` - The description of the role. +* `display_name` - The display name of the role. +* `object_id` - The object ID of the role. +* `template_id` - The object ID of the role. diff --git a/docs/resources/access_package_catalog_role_assignment.md b/docs/resources/access_package_catalog_role_assignment.md new file mode 100644 index 0000000000..b2e964900d --- /dev/null +++ b/docs/resources/access_package_catalog_role_assignment.md @@ -0,0 +1,61 @@ +--- +subcategory: "Identity Governance" +--- + +# Resource: azuread_access_package_catalog_role_assignment + +Manages a single catalog role 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 one of the following application roles: `EntitlementManagement.ReadWrite.All` or `Directory.ReadWrite.All` + +When authenticated with a user principal, this resource requires one of the following directory roles: `Identity Governance administrator` or `Global Administrator` + +## Example Usage + +```terraform +data "azuread_user" "example" { + user_principal_name = "jdoe@hashicorp.com" +} + +data "azuread_access_package_catalog_role" "example" { + display_name = "Catalog owner" +} + +resource "azuread_access_package_catalog" "example" { + display_name = "example-access-package-catalog" + description = "Example access package catalog" +} + +resource "azuread_access_package_catalog_role_assignment" "example" { + role_id = data.azuread_access_package_catalog_role.example.object_id + principal_object_id = data.azuread_user.example.object_id + catalog_id = azuread_access_package_catalog.example.id +} +``` + + +## Argument Reference + +The following arguments are supported: + +* `catalog_id` - (Required) The ID of the Catalog this role assignment will be scoped to. Changing this forces a new resource to be created. +* `principal_object_id` - (Required) The object ID of the principal for you want to create a role assignment. Supported object types are Users, Groups or Service Principals. Changing this forces a new resource to be created. +* `role_id` - (Required) The object ID of the catalog role you want to assign. Changing this forces a new resource to be created. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +*No additional attributes are exported* + +## Import + +Catalog role assignments can be imported using the ID of the assignment, e.g. + +```shell +terraform import azuread_access_package_catalog_role_assignment.test 00000000-0000-0000-0000-000000000000 +``` diff --git a/internal/services/identitygovernance/access_package_catalog_role_assignment_resource.go b/internal/services/identitygovernance/access_package_catalog_role_assignment_resource.go new file mode 100644 index 0000000000..6e6079d17d --- /dev/null +++ b/internal/services/identitygovernance/access_package_catalog_role_assignment_resource.go @@ -0,0 +1,127 @@ +package identitygovernance + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-uuid" + "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/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/hashicorp/terraform-provider-azuread/internal/validate" + "github.com/manicminer/hamilton/msgraph" +) + +func accessPackageCatalogRoleAssignmentResource() *schema.Resource { + return &schema.Resource{ + CreateContext: accessPackageCatalogRoleAssignmentResourceCreate, + ReadContext: accessPackageCatalogRoleAssignmentResourceRead, + DeleteContext: accessPackageCatalogRoleAssignmentResourceDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Importer: tf.ValidateResourceIDPriorToImport(func(id string) error { + if _, err := uuid.ParseUUID(id); err != nil { + return fmt.Errorf("specified ID (%q) is not valid: %s", id, err) + } + return nil + }), + + Schema: map[string]*schema.Schema{ + "role_id": { + Description: "The object ID of the catalog role for this assignment", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + + "principal_object_id": { + Description: "The object ID of the member principal", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + + "catalog_id": { + Description: "The unique ID of the access package catalog.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + }, + } +} + +func accessPackageCatalogRoleAssignmentResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogRoleAssignmentsClient + + catalogId := d.Get("catalog_id").(string) + principalId := d.Get("principal_object_id").(string) + roleId := d.Get("role_id").(string) + + properties := msgraph.UnifiedRoleAssignment{ + DirectoryScopeId: utils.String("/"), + PrincipalId: utils.String(principalId), + RoleDefinitionId: utils.String(roleId), + AppScopeId: utils.String("/AccessPackageCatalog/" + catalogId), + } + + assignment, status, err := client.Create(ctx, properties) + if err != nil { + return tf.ErrorDiagF(err, "Assigning catalog role %q to directory principal %q on catalog %q, received %d with error: %+v", roleId, principalId, catalogId, status, err) + } + if assignment == nil || assignment.ID() == nil { + return tf.ErrorDiagF(errors.New("returned role assignment ID was nil"), "API Error") + } + + d.SetId(*assignment.ID()) + return accessPackageCatalogRoleAssignmentResourceRead(ctx, d, meta) +} + +func accessPackageCatalogRoleAssignmentResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogRoleAssignmentsClient + + id := d.Id() + assignment, status, err := client.Get(ctx, id, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Assignment with ID %q was not found - removing from state", id) + d.SetId("") + return nil + } + return tf.ErrorDiagF(err, "Retrieving role assignment %q", id) + } + + catalogId := strings.TrimPrefix(*assignment.AppScopeId, "/AccessPackageCatalog/") + + tf.Set(d, "catalog_id", utils.String(catalogId)) + tf.Set(d, "principal_object_id", assignment.PrincipalId) + tf.Set(d, "role_id", assignment.RoleDefinitionId) + + return nil +} + +func accessPackageCatalogRoleAssignmentResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogRoleAssignmentsClient + + if _, err := client.Delete(ctx, d.Id()); err != nil { + return tf.ErrorDiagF(err, "Deleting role assignment %q: %+v", d.Id(), err) + } + return nil +} diff --git a/internal/services/identitygovernance/access_package_catalog_role_assignment_resource_test.go b/internal/services/identitygovernance/access_package_catalog_role_assignment_resource_test.go new file mode 100644 index 0000000000..112241c44b --- /dev/null +++ b/internal/services/identitygovernance/access_package_catalog_role_assignment_resource_test.go @@ -0,0 +1,151 @@ +package identitygovernance_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "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 AccessPackageCatalogRoleAssignmentResource struct{} + +func TestAccAccessPackageCatalogRoleAssignmentResource_group(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_catalog_role_assignment", "test") + r := AccessPackageCatalogRoleAssignmentResource{} + + data.DataSourceTest(t, []resource.TestStep{ + { + Config: r.group(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("catalog_id").IsUuid(), + check.That(data.ResourceName).Key("principal_object_id").IsUuid(), + check.That(data.ResourceName).Key("role_id").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccAccessPackageCatalogRoleAssignmentResource_servicePrincipal(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_catalog_role_assignment", "test") + r := AccessPackageCatalogRoleAssignmentResource{} + + data.DataSourceTest(t, []resource.TestStep{ + { + Config: r.servicePrincipal(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("catalog_id").IsUuid(), + check.That(data.ResourceName).Key("principal_object_id").IsUuid(), + check.That(data.ResourceName).Key("role_id").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccAccessPackageCatalogRoleAssignmentResource_user(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_catalog_role_assignment", "test") + r := AccessPackageCatalogRoleAssignmentResource{} + + data.DataSourceTest(t, []resource.TestStep{ + { + Config: r.user(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("catalog_id").IsUuid(), + check.That(data.ResourceName).Key("principal_object_id").IsUuid(), + check.That(data.ResourceName).Key("role_id").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func (r AccessPackageCatalogRoleAssignmentResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.IdentityGovernance.AccessPackageCatalogRoleAssignmentsClient + client.BaseClient.DisableRetries = true + + if _, status, err := client.Get(ctx, state.ID, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, fmt.Errorf("failed to retrieve directory role assignment %q: %+v", state.ID, err) + } + + return utils.Bool(true), nil +} + +func (AccessPackageCatalogRoleAssignmentResource) group(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_access_package_catalog_role" "test" { + display_name = "Catalog owner" +} + +resource "azuread_group" "test" { + display_name = "acctestGroup-%[2]d" + security_enabled = true +} + +resource "azuread_access_package_catalog_role_assignment" "test" { + role_id = data.azuread_access_package_catalog_role.test.object_id + catalog_id = azuread_access_package_catalog.test.id + principal_object_id = azuread_group.test.object_id +} +`, AccessPackageCatalogResource{}.basic(data), data.RandomInteger) +} + +func (AccessPackageCatalogRoleAssignmentResource) servicePrincipal(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_access_package_catalog_role" "test" { + display_name = "Catalog owner" +} + +data "azuread_client_config" "test" {} + +resource "azuread_access_package_catalog_role_assignment" "test" { + role_id = data.azuread_access_package_catalog_role.test.object_id + catalog_id = azuread_access_package_catalog.test.id + principal_object_id = data.azuread_client_config.test.object_id +} +`, AccessPackageCatalogResource{}.basic(data), data.RandomInteger, data.RandomPassword) +} + +func (AccessPackageCatalogRoleAssignmentResource) user(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_access_package_catalog_role" "test" { + display_name = "Catalog owner" +} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_user" "test" { + user_principal_name = "acctestUser'%[2]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestUser-%[2]d" + password = "%[3]s" +} + +resource "azuread_access_package_catalog_role_assignment" "test" { + role_id = data.azuread_access_package_catalog_role.test.object_id + catalog_id = azuread_access_package_catalog.test.id + principal_object_id = azuread_user.test.object_id +} +`, AccessPackageCatalogResource{}.basic(data), data.RandomInteger, data.RandomPassword) +} diff --git a/internal/services/identitygovernance/access_package_catalog_role_data_source.go b/internal/services/identitygovernance/access_package_catalog_role_data_source.go new file mode 100644 index 0000000000..455da63898 --- /dev/null +++ b/internal/services/identitygovernance/access_package_catalog_role_data_source.go @@ -0,0 +1,112 @@ +package identitygovernance + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "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/tf" + "github.com/manicminer/hamilton/msgraph" +) + +func accessPackageCatalogRoleDataSource() *schema.Resource { + return &schema.Resource{ + ReadContext: accessPackageCatalogRoleDataSourceRead, + + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "display_name": { + Description: "The display name of the catalog role", + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"display_name", "object_id"}, + }, + + "object_id": { + Description: "The object ID of the catalog role", + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"display_name", "object_id"}, + }, + + "description": { + Description: "The description of the catalog role", + Type: schema.TypeString, + Computed: true, + }, + + "template_id": { + Description: "The object ID of the template associated with the catalog role", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func accessPackageCatalogRoleDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogRoleClient + + var role msgraph.UnifiedRoleDefinition + var displayName string + + if v, ok := d.GetOk("display_name"); ok { + displayName = v.(string) + } + + if displayName != "" { + filter := fmt.Sprintf("displayName eq '%s'", displayName) + + roles, _, err := client.List(ctx, odata.Query{Filter: filter}) + if err != nil { + return tf.ErrorDiagPathF(err, "display_name", "No role found matching specified filter (%s)", filter) + } + count := len(*roles) + + if count > 1 { + return tf.ErrorDiagPathF(err, "display_name", "More than one role found matching specified filter (%s)", filter) + } else if count == 0 { + return tf.ErrorDiagPathF(err, "display_name", "No role found matching specified filter (%s)", filter) + } + + role = (*roles)[0] + } else if objectId, ok := d.Get("object_id").(string); ok && objectId != "" { + r, status, err := client.Get(ctx, objectId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagPathF(nil, "object_id", "No role found with object ID: %q", objectId) + } + return tf.ErrorDiagF(err, "Retrieving role with object ID: %q", objectId) + } + if r == nil { + return tf.ErrorDiagPathF(nil, "object_id", "Role not found with object ID: %q", objectId) + } + + role = *r + } + if role.ID() == nil { + return tf.ErrorDiagF(errors.New("API returned role with nil object ID"), "Bad API Response") + } + + d.SetId(*role.ID()) + + tf.Set(d, "object_id", role.ID()) + tf.Set(d, "display_name", role.DisplayName) + tf.Set(d, "description", role.Description) + tf.Set(d, "template_id", role.TemplateId) + + return nil +} + +// TODO replace role diff --git a/internal/services/identitygovernance/access_package_catalog_role_data_source_test.go b/internal/services/identitygovernance/access_package_catalog_role_data_source_test.go new file mode 100644 index 0000000000..0d9dfc85b2 --- /dev/null +++ b/internal/services/identitygovernance/access_package_catalog_role_data_source_test.go @@ -0,0 +1,34 @@ +package identitygovernance_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" +) + +type AccessPackageCatalogRoleDataSource struct{} + +func TestAccAccessPackageCatalogRoleDataSource_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azuread_access_package_catalog_role", "test") + r := AccessPackageCatalogRoleDataSource{} + + data.DataSourceTest(t, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("display_name").Exists(), + check.That(data.ResourceName).Key("template_id").Exists(), + check.That(data.ResourceName).Key("object_id").Exists(), + ), + }, + }) +} + +func (AccessPackageCatalogRoleDataSource) basic(_ acceptance.TestData) string { + return `provider azuread {} +data "azuread_access_package_catalog_role" "test" { + display_name = "Catalog owner" +}` +} diff --git a/internal/services/identitygovernance/client/client.go b/internal/services/identitygovernance/client/client.go index 45462e96d8..9e6a446ece 100644 --- a/internal/services/identitygovernance/client/client.go +++ b/internal/services/identitygovernance/client/client.go @@ -6,12 +6,14 @@ import ( ) type Client struct { - AccessPackageAssignmentPolicyClient *msgraph.AccessPackageAssignmentPolicyClient - AccessPackageCatalogClient *msgraph.AccessPackageCatalogClient - AccessPackageClient *msgraph.AccessPackageClient - AccessPackageResourceClient *msgraph.AccessPackageResourceClient - AccessPackageResourceRequestClient *msgraph.AccessPackageResourceRequestClient - AccessPackageResourceRoleScopeClient *msgraph.AccessPackageResourceRoleScopeClient + AccessPackageAssignmentPolicyClient *msgraph.AccessPackageAssignmentPolicyClient + AccessPackageCatalogClient *msgraph.AccessPackageCatalogClient + AccessPackageCatalogRoleAssignmentsClient *msgraph.EntitlementRoleAssignmentsClient + AccessPackageCatalogRoleClient *msgraph.EntitlementRoleDefinitionsClient + AccessPackageClient *msgraph.AccessPackageClient + AccessPackageResourceClient *msgraph.AccessPackageResourceClient + AccessPackageResourceRequestClient *msgraph.AccessPackageResourceRequestClient + AccessPackageResourceRoleScopeClient *msgraph.AccessPackageResourceRoleScopeClient } func NewClient(o *common.ClientOptions) *Client { @@ -23,6 +25,12 @@ func NewClient(o *common.ClientOptions) *Client { accessPackageCatalogClient := msgraph.NewAccessPackageCatalogClient() o.ConfigureClient(&accessPackageCatalogClient.BaseClient) + accessPackageCatalogRoleAssignmentsClient := msgraph.NewEntitlementRoleAssignmentsClient() + o.ConfigureClient(&accessPackageCatalogRoleAssignmentsClient.BaseClient) + + accessPackageCatalogRoleClient := msgraph.NewEntitlementRoleDefinitionsClient() + o.ConfigureClient(&accessPackageCatalogRoleClient.BaseClient) + // Use beta version because it replies more info than v1.0 accessPackageClient := msgraph.NewAccessPackageClient() o.ConfigureClient(&accessPackageClient.BaseClient) @@ -44,11 +52,13 @@ func NewClient(o *common.ClientOptions) *Client { accessPackageResourceRoleScopeClient.BaseClient.ApiVersion = msgraph.VersionBeta return &Client{ - AccessPackageAssignmentPolicyClient: accessPackageAssignmentPolicyClient, - AccessPackageCatalogClient: accessPackageCatalogClient, - AccessPackageClient: accessPackageClient, - AccessPackageResourceClient: accessPackageResourceClient, - AccessPackageResourceRequestClient: accessPackageResourceRequestClient, - AccessPackageResourceRoleScopeClient: accessPackageResourceRoleScopeClient, + AccessPackageAssignmentPolicyClient: accessPackageAssignmentPolicyClient, + AccessPackageCatalogClient: accessPackageCatalogClient, + AccessPackageCatalogRoleAssignmentsClient: accessPackageCatalogRoleAssignmentsClient, + AccessPackageCatalogRoleClient: accessPackageCatalogRoleClient, + AccessPackageClient: accessPackageClient, + AccessPackageResourceClient: accessPackageResourceClient, + AccessPackageResourceRequestClient: accessPackageResourceRequestClient, + AccessPackageResourceRoleScopeClient: accessPackageResourceRoleScopeClient, } } diff --git a/internal/services/identitygovernance/registration.go b/internal/services/identitygovernance/registration.go index e24a38a480..446400d8ee 100644 --- a/internal/services/identitygovernance/registration.go +++ b/internal/services/identitygovernance/registration.go @@ -21,8 +21,9 @@ func (r Registration) WebsiteCategories() []string { // SupportedDataSources returns the supported Data Sources supported by this Service func (r Registration) SupportedDataSources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azuread_access_package": accessPackageDataSource(), - "azuread_access_package_catalog": accessPackageCatalogDataSource(), + "azuread_access_package": accessPackageDataSource(), + "azuread_access_package_catalog": accessPackageCatalogDataSource(), + "azuread_access_package_catalog_role": accessPackageCatalogRoleDataSource(), } } @@ -32,6 +33,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { "azuread_access_package": accessPackageResource(), "azuread_access_package_assignment_policy": accessPackageAssignmentPolicyResource(), "azuread_access_package_catalog": accessPackageCatalogResource(), + "azuread_access_package_catalog_role_assignment": accessPackageCatalogRoleAssignmentResource(), "azuread_access_package_resource_catalog_association": accessPackageResourceCatalogAssociationResource(), "azuread_access_package_resource_package_association": accessPackageResourcePackageAssociationResource(), }