diff --git a/docs/data-sources/access_package.md b/docs/data-sources/access_package.md new file mode 100644 index 000000000..6e59ac817 --- /dev/null +++ b/docs/data-sources/access_package.md @@ -0,0 +1,53 @@ +--- +subcategory: "Identity Governance" +--- + +# Data Source: azuread_access_package + +Use this data source to retrieve information for an existing access package within Identity Governance in Azure Active Directory. + +## 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 `EntitlementManagement.ReadWrite.All`. + +When authenticated with a user principal, this data source requires one of the following directory roles: `Catalog owner`, `Catalog reader`, `Access package manager`, `Global Reader`, or `Global Administrator`. + +## Example Usage + +*Look up by ID* + +```terraform +data "azuread_access_package" "example" { + object_id = "00000000-0000-0000-0000-000000000000" +} +``` + +*Look up by DisplayName* + +```terraform +data "azuread_access_package" "example" { + catalog_id = "00000000-0000-0000-0000-000000000000" + display_name = "My access package Catalog" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `catalog_id` - (Optional) The ID of the Catalog this access package is in. +* `display_name` - (Optional) The display name of the access package. +* `object_id` - (Optional) The ID of this access package. + +~> Either `object_id`, or both `catalog_id` and `display_name`, must be specified. + + +## Attributes Reference + +In addition to the above arguments, the following attributes are exported: + +* `id` - The ID of this resource. +* `description` - The description of the access package. +* `hidden` - Whether the access package is hidden from the requestor. diff --git a/docs/data-sources/access_package_catalog.md b/docs/data-sources/access_package_catalog.md new file mode 100644 index 000000000..ba90a3f57 --- /dev/null +++ b/docs/data-sources/access_package_catalog.md @@ -0,0 +1,52 @@ +--- +subcategory: "Identity Governance" +--- + +# Data Source: azuread_access_package_catalog +i +Use this resource to retrieve information for an existing access package catalog within Identity Governance in Azure Active Directory. + +## 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 `EntitlementManagement.ReadWrite.All`. + +When authenticated with a user principal, this data source requires one of the following directory roles: `Catalog owner`, `Catalog reader`, `Global Reader`, or `Global Administrator`. + +## Example Usage + +*Look up by ID* + +```terraform +data "azuread_access_package_catalog" "example" { + object_id = "00000000-0000-0000-0000-000000000000" +} +``` + +*Look up by DisplayName* + +```terraform +data "azuread_access_package_catalog" "example" { + display_name = "My access package Catalog" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `display_name` - (Optional) The display name of the access package catalog. +* `object_id` - (Optional) The ID of this access package catalog. + +~> One of `display_name` or `object_id` must be specified. + +## Attributes Reference + +In additional to the arguments, the following attributes are exported: + +* `id` - The ID of this resource. +* `description` - The description of the access package catalog. +* `externally_visible` - Whether the access packages in this catalog can be requested by users outside the tenant. +* `published` - Whether the access packages in this catalog are available for management. + diff --git a/docs/resources/access_package.md b/docs/resources/access_package.md new file mode 100644 index 000000000..35a3e8115 --- /dev/null +++ b/docs/resources/access_package.md @@ -0,0 +1,54 @@ +--- +subcategory: "Identity Governance" +--- + +# Resource: azuread_access_package + +Manages an Access Package within Identity Governance in 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 role: `EntitlementManagement.ReadWrite.All`. + +When authenticated with a user principal, this resource requires one of the following directory roles: `Catalog owner`, `Access package manager` or `Global Administrator` + + +## Example Usage + +```terraform +resource "azuread_access_package_catalog" "example" { + display_name = "example-catalog" + description = "Example catalog" +} + +resource "azuread_access_package" "example" { + catalog_id = azuread_access_package_catalog.example.id + display_name = "access-package" + description = "Access Package" +} +``` + +## Argument Reference + +* `catalog_id` - (Required) The ID of the Catalog this access package will be created in. +* `description` - (Required) The description of the access package. +* `display_name` - (Required) The display name of the access package. +* `hidden` - (Optional) Whether the access package is hidden from the requestor. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of this resource. + +## Import + +Access Packages can be imported using the `id`, e.g. + +``` +terraform import azuread_access_package.example_package 00000000-0000-0000-0000-000000000000 +``` + + diff --git a/docs/resources/access_package_assignment_policy.md b/docs/resources/access_package_assignment_policy.md new file mode 100644 index 000000000..2ac0470d5 --- /dev/null +++ b/docs/resources/access_package_assignment_policy.md @@ -0,0 +1,210 @@ +--- +subcategory: "Identity Governance" +--- + +# Resource: azuread_access_package_assignment_policy + +Manages an assignment policy for an access package within Identity Governance in 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 role: `EntitlementManagement.ReadWrite.All`. + +When authenticated with a user principal, this resource requires `Global Administrator` directory role, or one of the `Catalog Owner` and `Access Package Manager` role in Idneity Governance. + +## Example Usage + +```terraform +resource "azuread_group" "example" { + display_name = "group-name" + security_enabled = true +} + +resource "azuread_access_package_catalog" "example" { + display_name = "example-catalog" + description = "Example catalog" +} + +resource "azuread_access_package" "example" { + catalog_id = azuread_access_package_catalog.example.id + display_name = "access-package" + description = "Access Package" +} + +resource "azuread_access_package_assignment_policy" "test" { + access_package_id = azuread_access_package.test.id + display_name = "assignment-policy" + description = "My assignment policy" + duration_in_days = 90 + + requestor_settings { + scope_type = "AllExistingDirectoryMemberUsers" + } + + approval_settings { + approval_required = true + + approval_stage { + approval_timeout_in_days = 14 + + primary_approver { + object_id = azuread_group.test.object_id + subject_type = "groupMembers" + } + } + } + + assignment_review_settings { + enabled = true + review_frequency = "weekly" + duration_in_days = 3 + review_type = "Self" + access_review_timeout_behavior = "keepAccess" + } + + question { + text { + default_text = "hello, how are you?" + } + } +} +``` + + +## Argument Reference + +- `access_package_id` (Required) The ID of the access package that will contain the policy. +- `description` (Required) The description of the policy. +- `display_name` (Required) The display name of the policy. +- `approval_settings` (Optional) An `approval_settings` block to specify whether approvals are required and how they are obtained, as documented below. +- `assignment_review_settings` (Optional) An `assignment_review_settings` block, to specify whether assignment review is needed and how it is conducted, as documented below. +- `can_extend` (Optional) When enabled, users will be able to request extension of their access to this package before their access expires. +- `duration_in_days` (Optional) How many days this assignment is valid for. +- `expiration_date` (Optional) The date that this assignment expires, formatted as an RFC3339 date string in UTC(e.g. 2018-01-01T01:02:03Z). +- `question` (Optional) One or more `question` blocks for the requestor, as documented below. +- `requestor_settings` (Optional) A `requestor_settings` block to configure the users who can request access, as documented below. + +--- + +`approval_settings` block supports the following: + +- `approval_stage` (Optional) An `approval_stage` block specifying the process to obtain an approval, as documented below. +- `approval_required` (Optional) Whether an approval is required. +- `approval_required_for_extension` (Optional) Whether an approval is required to grant extension. Same approval settings used to approve initial access will apply. +- `requestor_justification_required` (Optional) Whether a requestor is required to provide a justification to request an access package. Justification is visible to approvers and the requestor. + +--- + +`approval_settings.approval_stage` block supports the following + +- `approval_timeout_in_days` (Required) Maximum number of days within which a request must be approved. If a request is not approved within this time period after it is made, it will be automatically rejected. +- `alternative_approver` (Optional) A block specifying alternative approvers when escalation is enabled and the primary approvers do not respond before the escalation time, as documented below. +- `enable_alternative_approval_in_days` (Optional) Number of days before the request is forwarded to alternative approvers. +- `alternative_approval_enabled` (Optional) Whether alternative approvers are enabled. +- `approver_justification_required` (Optional) Whether an approver must provide a justification for their decision. Justification is visible to other approvers and the requestor. +- `primary_approver` (Optional) A block specifying the users who will be asked to approve requests, as documented below. + +--- + +`approval_settings.approval_stage.primary_approver` and `approval_settings.approval_stage.alternative_approver` blocks support the following: + +- `subject_type` (Required) Specifies the type of users. Valid values are `singleUser`, `groupMembers`, `connectedOrganizationMembers`, `requestorManager`, `internalSponsors`, or `externalSponsors`. +- `backup` (Optional) For a user in an approval stage, this property indicates whether the user is a backup fallback approver. +- `object_id` (Optional) The ID of the subject. + +--- + +`assignment_review_settings` block supports the following: + +- `access_review_timeout_behavior` (Optional) Specifies the actions the system takes if reviewers don't respond in time. Vlid values are `keepAccess`, `removeAccess`, or `acceptAccessRecommendation`. +- `duration_in_days` (Number) How many days each occurrence of the access review series will run. +- `access_recommendation_enabled` (Optional) Whether to show the reviewer decision helpers. If enabled, system recommendations based on users' access information will be shown to the reviewers. The reviewer will be recommended to approve the review if the user has signed-in at least once during the last 30 days. The reviewer will be recommended to deny the review if the user has not signed-in during the last 30 days. +- `approver_justification_required` (Optional) Whether a reviewer needs to provide a justification for their decision. Justification is visible to other reviewers and the requestor. +- `enabled` (Optional) Whether to enable assignment review. +- `review_frequency` (Optional) This will determine how often the access review campaign runs, valid values are `weekly`, `monthly`, `quarterly`, `halfyearly`, or `annual`. +- `review_type` (Optional) Self review or specific reviewers, valid values are `Self`, `Reviewers`. +- `reviewer` (Optional) One or more `reviewer` blocks to specify the users who will be reviewers (when `review_type` is `Reviewers`), as documented below. +- `starting_on` (Optional) This is the date the access review campaign will start on, formatted as an RFC3339 date string in UTC(e.g. 2018-01-01T01:02:03Z), default is now. Once an access review has been created, you cannot update its start date + +--- + +`assignment_review_settings.reviewer` block supports the following: + +- `subject_type` (Required) Specifies the type of users. Valid values are `singleUser`, `groupMembers`, `connectedOrganizationMembers`, `requestorManager`, `internalSponsors`, or `externalSponsors`. +- `backup` (Optional) For a user in an approval stage, this property indicates whether the user is a backup approver. +- `object_id` (Optional) The ID of the subject. + +--- + +`question` block supports the following: + +- `text` (Required) A block describing the content of this question, as documented below. +- `choice` (Optional) One or more blocks configuring a choice to the question, as documented below. +- `required` (Optional) Whether this question is required. +- `sequence` (Optional) The sequence number of this question. + +--- + +`question.text` block supports the following: + +- `default_text` (Required) The default text of this question. +- `localized_text` (Optional) One or more blocks describing localized text of this question, as documented below. + +--- + +`question.text.localized_text` block supports the following: + +- `content` (Required) The localized content of this question. +- `language_code` (Required) The ISO 639 language code for this question content. + +--- + +`question.choice` block supports the following: + +- `actual_value` (Required) The actual value of this choice. +- `display_value` (Required) A block describing the display text of this choice, as documented below. + +--- + +`question.choice.display_value` block supports the following: + +- `default_text` (Required) The default text of this question choice. +- `localized_text` (Optional) One or more blocks describing localized text of this question choice, as documented below. + +--- + +`question.choice.display_value.localized_text` block supports the following: + +- `content` (Required) The localized content of this question choice. +- `language_code` (Required) The ISO 639 language code for this question choice content. + +--- + +`requestor_settings` block supports the following: + +- `requests_accepted` (Optional) Whether to accept requests using this policy. When `false`, no new requests can be made using this policy. +- `requestor` (Optional) A block specifying the users who are allowed to request on this policy, as documented below. +- `scope_type` (Optional) Specifies the scopes of the requestors. Valid values are `AllConfiguredConnectedOrganizationSubjects`, `AllExistingConnectedOrganizationSubjects`, `AllExistingDirectoryMemberUsers`, `AllExistingDirectorySubjects`, `AllExternalSubjects`, `NoSubjects`, `SpecificConnectedOrganizationSubjects`, or `SpecificDirectorySubjects`. + +--- + +`requestor_settings.requestor` block supports the following: + +- `subject_type` (Required) Specifies the type of users. Valid values are `singleUser`, `groupMembers`, `connectedOrganizationMembers`, `requestorManager`, `internalSponsors`, or `externalSponsors`. +- `object_id` (Optional) The ID of the subject. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +- `id` (String) The ID of this resource. + +## Import + +An access package assignment policy can be imported using the ID, e.g. + +```shell +terraform import azuread_access_package_assignment_policy.example 00000000-0000-0000-0000-000000000000 +``` diff --git a/docs/resources/access_package_catalog.md b/docs/resources/access_package_catalog.md new file mode 100644 index 000000000..8e5ae32b5 --- /dev/null +++ b/docs/resources/access_package_catalog.md @@ -0,0 +1,48 @@ +--- +subcategory: "Identity Governance" +--- + +# Resource: azuread_access_package_catalog + +Manages an access package catalog within Identity Governance in 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 role: `EntitlementManagement.ReadWrite.All`. + +When authenticated with a user principal, this resource requires one of the following directory roles: `Catalog owner`, `Catalog creator` or `Global Administrator` + + +## Example Usage + +```terraform +resource "azuread_access_package_catalog" "example" { + display_name = "example-access-package-catalog" + description = "Example access package catalog" +} +``` + +## Argument Reference + +* `description` - (Required) The description of the access package catalog. +* `display_name` - (Required) The display name of the access package catalog. +* `externally_visible` - (Optional) Whether the access packages in this catalog can be requested by users outside the tenant. +* `published` - (Optional) Whether the access packages in this catalog are available for management. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of this resource. + +## Import + +An Access Package Catalog can be imported using the `id`, e.g. + +``` +terraform import azuread_access_package_catalog.example 00000000-0000-0000-0000-000000000000 +``` + + diff --git a/docs/resources/access_package_resource_catalog_association.md b/docs/resources/access_package_resource_catalog_association.md new file mode 100644 index 000000000..13d15df12 --- /dev/null +++ b/docs/resources/access_package_resource_catalog_association.md @@ -0,0 +1,58 @@ +--- +subcategory: "Identity Governance" +--- + +# Resource: azuread_access_package_resource_catalog_association + +Manages the resources added to access package catalogs within Identity Governance in 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 role: `EntitlementManagement.ReadWrite.All`. + +When authenticated with a user principal, this resource requires one of the following directory roles: `Catalog owner` or `Global Administrator` + + +## Example Usage + +```terraform +resource "azuread_group" "example" { + display_name = "example-group" + security_enabled = true +} + +resource "azuread_access_package_catalog" "example" { + display_name = "example-catalog" + description = "Example catalog" +} + +resource "azuread_access_package_resource_catalog_association" "example" { + catalog_id = azuread_access_package_catalog.example_catalog.id + resource_origin_id = azuread_group.example_group.object_id + resource_origin_system = "AadGroup" +} +``` + +## Argument Reference + +* `catalog_id` - (Required) The unique ID of the access package catalog. Changing this forces a new resource to be created. +* `resource_origin_id` - (Required) The unique identifier of the resource in the origin system. In the case of an Azure AD group, this is the identifier of the group. Changing this forces a new resource to be created. +* `resource_origin_system` - (Required) The type of the resource in the origin system, such as `SharePointOnline`, `AadApplication` or `AadGroup`. Changing this forces a new resource to be created. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of this resource, the ID is the concatenation of `catalog_id` and `resource_origin_id` with colon in between. + +## Import + +The resource and catalog association can be imported using the catalog ID and the resource origin ID, e.g. + +``` +terraform import azuread_access_package_resource_catalog_association.example 00000000-0000-0000-0000-000000000000/11111111-1111-1111-1111-111111111111 +``` + +-> This ID format is unique to Terraform and is composed of the Catalog ID and the Resource Origin ID in the format `{CatalogID}/{ResourceOriginID}`. diff --git a/docs/resources/access_package_resource_package_association.md b/docs/resources/access_package_resource_package_association.md new file mode 100644 index 000000000..3d08507cb --- /dev/null +++ b/docs/resources/access_package_resource_package_association.md @@ -0,0 +1,68 @@ +--- +subcategory: "Identity Governance" +--- + +# Resource: azuread_access_package_resource_package_association + +Manages the resources added to access packages within Identity Governance in 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 role: `EntitlementManagement.ReadWrite.All`. + +When authenticated with a user principal, this resource requires one of the following directory roles: `Catalog owner`, `Access package manager` or `Global Administrator`. + +## Example Usage + +```terraform +resource "azuread_group" "example" { + display_name = "example-group" + security_enabled = true +} + +resource "azuread_access_package_catalog" "example" { + display_name = "example-catalog" + description = "Example catalog" +} + +resource "azuread_access_package_resource_catalog_association" "example" { + catalog_id = azuread_access_package_catalog.example_catalog.id + resource_origin_id = azuread_group.example_group.object_id + resource_origin_system = "AadGroup" +} + +resource "azuread_access_package" "example" { + display_name = "example-package" + description = "Example Package" + catalog_id = azuread_access_package_catalog.example_catalog.id +} + +resource "azuread_access_package_resource_package_association" "example" { + access_package_id = azuread_access_package.example.id + catalog_resource_association_id = azuread_access_package_resource_catalog_association.example.id +} +``` + +## Argument Reference + +* `access_package_id` - (Required) The ID of access package this resource association is configured to. Changing this forces a new resource to be created. +* `access_type` - (Optional) The role of access type to the specified resource. Valid values are `Member`, or `Owner` The default is `Member`. Changing this forces a new resource to be created. +* `catalog_resource_association_id` - (Required) The ID of the catalog association from the `azuread_access_package_resource_catalog_association` resource. Changing this forces a new resource to be created. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of this resource. The ID is combined by four fields with colon in between, the four fields are `access_package_id`, this package association id, `resource_origin_id` and `access_type`. + +## Import + +The resource and catalog association can be imported using the access package ID, the resource association ID, the resource origin ID, and the access type, e.g. + +``` +terraform import azuread_access_package_resource_package_association.example 00000000-0000-0000-0000-000000000000/11111111-1111-1111-1111-111111111111_22222222-2222-2222-2222-22222222/33333333-3333-3333-3333-33333333/Member +``` + +-> This ID format is unique to Terraform and is composed of the Access Package ID, the Resource Association ID, the Resource Origin ID, and the Access Type, in the format `{AccessPackageID}/{ResourceAssociationID}/{ResourceOriginID}/{AccessType}`. diff --git a/internal/clients/client.go b/internal/clients/client.go index 4a9b67d37..c85978d96 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -20,6 +20,7 @@ import ( directoryroles "github.com/hashicorp/terraform-provider-azuread/internal/services/directoryroles/client" domains "github.com/hashicorp/terraform-provider-azuread/internal/services/domains/client" groups "github.com/hashicorp/terraform-provider-azuread/internal/services/groups/client" + identitygovernance "github.com/hashicorp/terraform-provider-azuread/internal/services/identitygovernance/client" invitations "github.com/hashicorp/terraform-provider-azuread/internal/services/invitations/client" policies "github.com/hashicorp/terraform-provider-azuread/internal/services/policies/client" serviceprincipals "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/client" @@ -45,6 +46,7 @@ type Client struct { DirectoryRoles *directoryroles.Client Domains *domains.Client Groups *groups.Client + IdentityGovernance *identitygovernance.Client Invitations *invitations.Client Policies *policies.Client ServicePrincipals *serviceprincipals.Client @@ -61,6 +63,7 @@ func (client *Client) build(ctx context.Context, o *common.ClientOptions) error client.ConditionalAccess = conditionalaccess.NewClient(o) client.DirectoryRoles = directoryroles.NewClient(o) client.Groups = groups.NewClient(o) + client.IdentityGovernance = identitygovernance.NewClient(o) client.Invitations = invitations.NewClient(o) client.Policies = policies.NewClient(o) client.ServicePrincipals = serviceprincipals.NewClient(o) diff --git a/internal/provider/services.go b/internal/provider/services.go index 4d763d527..e1aea7e05 100644 --- a/internal/provider/services.go +++ b/internal/provider/services.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-provider-azuread/internal/services/directoryroles" "github.com/hashicorp/terraform-provider-azuread/internal/services/domains" "github.com/hashicorp/terraform-provider-azuread/internal/services/groups" + "github.com/hashicorp/terraform-provider-azuread/internal/services/identitygovernance" "github.com/hashicorp/terraform-provider-azuread/internal/services/invitations" "github.com/hashicorp/terraform-provider-azuread/internal/services/policies" "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals" @@ -25,6 +26,7 @@ func SupportedServices() []ServiceRegistration { directoryroles.Registration{}, domains.Registration{}, groups.Registration{}, + identitygovernance.Registration{}, invitations.Registration{}, policies.Registration{}, serviceprincipals.Registration{}, diff --git a/internal/services/identitygovernance/access_package_assignment_policy_resource.go b/internal/services/identitygovernance/access_package_assignment_policy_resource.go new file mode 100644 index 000000000..9c96f4040 --- /dev/null +++ b/internal/services/identitygovernance/access_package_assignment_policy_resource.go @@ -0,0 +1,541 @@ +package identitygovernance + +import ( + "context" + "fmt" + "log" + "net/http" + "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-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" + "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" +) + +const accessPackageAssignmentPolicyResourceName = "azuread_access_package_assignment_policy" + +func accessPackageAssignmentPolicyResource() *schema.Resource { + return &schema.Resource{ + CreateContext: accessPackageAssignmentPolicyResourceCreate, + ReadContext: accessPackageAssignmentPolicyResourceRead, + UpdateContext: accessPackageAssignmentPolicyResourceUpdate, + DeleteContext: accessPackageAssignmentPolicyResourceDelete, + + CustomizeDiff: assignmentPolicyCustomDiff, + + 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{ + "access_package_id": { + Description: "The ID of the access package that will contain the policy", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validate.UUID, + }, + + "display_name": { + Description: "The display name of the policy", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + + "description": { + Description: "The description of the policy", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + + "extension_enabled": { + Description: "When enabled, users will be able to request extension of their access to this package before their access expires", + Type: schema.TypeBool, + Optional: true, + }, + + "duration_in_days": { + Description: "How many days this assignment is valid for", + Type: schema.TypeInt, + Optional: true, + ConflictsWith: []string{"expiration_date"}, + ValidateFunc: validation.IntBetween(0, 3660), + }, + + "expiration_date": { + Description: "The date that this assignment expires, formatted as an RFC3339 date string in UTC (e.g. 2018-01-01T01:02:03Z)", + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"duration_in_days"}, + ValidateFunc: validation.IsRFC3339Time, + //DiffSuppressFunc: assignmentPolicyDiffSuppress, + }, + + "requestor_settings": { + Description: "This block configures the users who can request access", + Type: schema.TypeList, + Optional: true, + DiffSuppressFunc: assignmentPolicyDiffSuppress, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "requests_accepted": { + Description: "Whether to accept requests now, when disabled, no new requests can be made using this policy", + Type: schema.TypeBool, + Optional: true, + }, + + "scope_type": { + Description: "Specify the scopes of the requestors", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + msgraph.RequestorSettingsScopeTypeAllConfiguredConnectedOrganizationSubjects, + msgraph.RequestorSettingsScopeTypeAllExistingConnectedOrganizationSubjects, + msgraph.RequestorSettingsScopeTypeAllExistingDirectoryMemberUsers, + msgraph.RequestorSettingsScopeTypeAllExistingDirectorySubjects, + msgraph.RequestorSettingsScopeTypeAllExternalSubjects, + msgraph.RequestorSettingsScopeTypeNoSubjects, + msgraph.RequestorSettingsScopeTypeSpecificConnectedOrganizationSubjects, + msgraph.RequestorSettingsScopeTypeSpecificDirectorySubjects, + }, false), + }, + + "requestor": { + Description: "The users who are allowed to request on this policy, which can be singleUser, groupMembers, and connectedOrganizationMembers", + Type: schema.TypeList, + Optional: true, + Elem: schemaUserSet(), + }, + }, + }, + }, + + "approval_settings": { + Description: "Settings of whether approvals are required and how they are obtained", + Type: schema.TypeList, + Optional: true, + DiffSuppressFunc: assignmentPolicyDiffSuppress, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "approval_required": { + Description: "Whether an approval is required", + Type: schema.TypeBool, + Optional: true, + }, + + "approval_required_for_extension": { + Description: "Whether an approval is required to grant extension. Same approval settings used to approve initial access will apply", + Type: schema.TypeBool, + Optional: true, + }, + + "requestor_justification_required": { + Description: "Whether requestor are required to provide a justification to request an access package. Justification is visible to other approvers and the requestor", + Type: schema.TypeBool, + Optional: true, + }, + + "approval_stage": { + Description: "The process to obtain an approval", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "approval_timeout_in_days": { + Description: "Decision must be made in how many days? If a request is not approved within this time period after it is made, it will be automatically rejected", + Type: schema.TypeInt, + Required: true, + }, + + "approver_justification_required": { + Description: "Whether an approver must provide a justification for their decision. Justification is visible to other approvers and the requestor", + Type: schema.TypeBool, + Optional: true, + }, + + "alternative_approval_enabled": { + Description: "If no action taken, forward to alternate approvers?", + Type: schema.TypeBool, + Optional: true, + }, + + "enable_alternative_approval_in_days": { + Description: "Forward to alternate approver(s) after how many days?", + Type: schema.TypeInt, + Optional: true, + }, + + "primary_approver": { + Description: "The users who will be asked to approve requests. A collection of singleUser, groupMembers, requestorManager, internalSponsors and externalSponsors. When creating or updating a policy, include at least one userSet in this collection", + Type: schema.TypeList, + Optional: true, + Elem: schemaUserSet(), + }, + + "alternative_approver": { + Description: "If escalation is enabled and the primary approvers do not respond before the escalation time, the escalationApprovers are the users who will be asked to approve requests. This can be a collection of singleUser, groupMembers, requestorManager, internalSponsors and externalSponsors. When creating or updating a policy, if there are no escalation approvers, or escalation approvers are not required for the stage, the value of this property should be an empty collection", + Type: schema.TypeList, + Optional: true, + Elem: schemaUserSet(), + }, + }, + }, + }, + }, + }, + }, + + "assignment_review_settings": { + Description: "The settings of whether assignment review is needed and how it's conducted", + Type: schema.TypeList, + Optional: true, + DiffSuppressFunc: assignmentPolicyDiffSuppress, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Description: "Whether to enable assignment review", + Type: schema.TypeBool, + Optional: true, + }, + + "review_frequency": { + Description: "This will determine how often the access review campaign runs", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + msgraph.AccessReviewRecurranceTypeAnnual, + msgraph.AccessReviewRecurranceTypeHalfYearly, + msgraph.AccessReviewRecurranceTypeQuarterly, + msgraph.AccessReviewRecurranceTypeMonthly, + msgraph.AccessReviewRecurranceTypeWeekly, + }, false), + }, + + "review_type": { + Description: "Self review or specific reviewers", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + msgraph.AccessReviewReviewerTypeSelf, + msgraph.AccessReviewReviewerTypeReviewers, + }, false), + }, + + "starting_on": { + Description: "This is the date the access review campaign will start on, formatted as an RFC3339 date string in UTC(e.g. 2018-01-01T01:02:03Z), default is now. Once an access review has been created, you cannot update its start date", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsRFC3339Time, + }, + + "duration_in_days": { + Description: "How many days each occurrence of the access review series will run", + Type: schema.TypeInt, + Optional: true, + }, + + "reviewer": { + Description: "If the reviewerType is Reviewers, this collection specifies the users who will be reviewers, either by ID or as members of a group, using a collection of singleUser and groupMembers", + Type: schema.TypeList, + Optional: true, + Elem: schemaUserSet(), + }, + + "access_recommendation_enabled": { + Description: "Whether to show Show reviewer decision helpers. If enabled, system recommendations based on users' access information will be shown to the reviewers. The reviewer will be recommended to approve the review if the user has signed-in at least once during the last 30 days. The reviewer will be recommended to deny the review if the user has not signed-in during the last 30 days", + Type: schema.TypeBool, + Optional: true, + }, + + "approver_justification_required": { + Description: "Whether a reviewer need provide a justification for their decision. Justification is visible to other reviewers and the requestor", + Type: schema.TypeBool, + Optional: true, + }, + + "access_review_timeout_behavior": { + Description: "What actions the system takes if reviewers don't respond in time", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + msgraph.AccessReviewTimeoutBehaviorTypeAcceptAccessRecommendation, + msgraph.AccessReviewTimeoutBehaviorTypeKeepAccess, + msgraph.AccessReviewTimeoutBehaviorTypeRemoveAccess, + }, false), + }, + }, + }, + }, + + "question": { + Description: "One or more questions to the requestor", + Type: schema.TypeList, + DiffSuppressFunc: assignmentPolicyDiffSuppress, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "required": { + Description: "Whether this question is required", + Type: schema.TypeBool, + Optional: true, + }, + + "sequence": { + Description: "The sequence number of this question", + Type: schema.TypeInt, + Optional: true, + }, + + "choice": { + Description: "Configuration of a choice to the question", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "actual_value": { + Description: "The actual value of this choice", + Type: schema.TypeString, + Required: true, + }, + + "display_value": { + Description: "The display text of this choice", + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: schemaLocalizedContent(), + }, + }, + }, + }, + + "text": { + Description: "The content of this question", + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: schemaLocalizedContent(), + }, + }, + }, + }, + }, + } +} + +func accessPackageAssignmentPolicyResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageAssignmentPolicyClient + + var properties msgraph.AccessPackageAssignmentPolicy + var err error + if properties, err = buildAssignmentPolicyResourceData(ctx, d, meta); err != nil { + return tf.ErrorDiagF(err, "Building resource data from supplied parameters") + } + + accessPackageAssignmentPolicy, _, err := client.Create(ctx, properties) + if err != nil { + return tf.ErrorDiagF(err, "Creating access package assignment policy %q", d.Get("display_name").(string)) + } + + d.SetId(*accessPackageAssignmentPolicy.ID) + + return accessPackageAssignmentPolicyResourceRead(ctx, d, meta) +} + +func accessPackageAssignmentPolicyResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageAssignmentPolicyClient + + var properties msgraph.AccessPackageAssignmentPolicy + var err error + if properties, err = buildAssignmentPolicyResourceData(ctx, d, meta); err != nil { + return tf.ErrorDiagF(err, "Building resource data from supplied parameters") + } + + objectId := d.Id() + tf.LockByName(accessPackageAssignmentPolicyResourceName, objectId) + defer tf.UnlockByName(accessPackageAssignmentPolicyResourceName, objectId) + if _, err := client.Update(ctx, properties); err != nil { + return tf.ErrorDiagF(err, "Could not update access package assignment policy with ID: %q", objectId) + } + + return accessPackageAssignmentPolicyResourceRead(ctx, d, meta) +} + +func accessPackageAssignmentPolicyResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageAssignmentPolicyClient + + objectId := d.Id() + accessPackageAssignmentPolicy, status, err := client.Get(ctx, objectId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Access package assignment policy with Object ID %q was not found - removing from state!", objectId) + d.SetId("") + return nil + } + + return tf.ErrorDiagF(err, "Retrieving access package assignment policy with object ID: %q", objectId) + } + + tf.Set(d, "display_name", accessPackageAssignmentPolicy.DisplayName) + tf.Set(d, "access_package_id", accessPackageAssignmentPolicy.AccessPackageId) + tf.Set(d, "description", accessPackageAssignmentPolicy.Description) + tf.Set(d, "extension_enabled", accessPackageAssignmentPolicy.CanExtend) + tf.Set(d, "duration_in_days", accessPackageAssignmentPolicy.DurationInDays) + if expirationDate := accessPackageAssignmentPolicy.ExpirationDateTime; expirationDate != nil && !expirationDate.IsZero() { + tf.Set(d, "expiration_date", expirationDate.UTC().Format(time.RFC3339)) + } else { + tf.Set(d, "expiration_date", "") + } + + tf.Set(d, "requestor_settings", flattenRequestorSettings(accessPackageAssignmentPolicy.RequestorSettings)) + tf.Set(d, "approval_settings", flattenApprovalSettings(accessPackageAssignmentPolicy.RequestApprovalSettings)) + tf.Set(d, "assignment_review_settings", flattenAssignmentReviewSettings(accessPackageAssignmentPolicy.AccessReviewSettings)) + tf.Set(d, "question", flattenAccessPackageQuestions(accessPackageAssignmentPolicy.Questions)) + + return nil +} + +func accessPackageAssignmentPolicyResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageAssignmentPolicyClient + accessPackageAssignmentPolicyId := d.Id() + + _, status, err := client.Get(ctx, accessPackageAssignmentPolicyId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagPathF(fmt.Errorf("Access package assignment policy was not found"), "id", "Retrieving user with object ID %q", accessPackageAssignmentPolicyId) + } + + return tf.ErrorDiagPathF(err, "id", "Retrieving access package assignment policy with object ID %q", accessPackageAssignmentPolicyId) + } + + status, err = client.Delete(ctx, accessPackageAssignmentPolicyId) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Deleting access package assignment policy with object ID %q, got status %d", accessPackageAssignmentPolicyId, status) + } + + // Wait for user object to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, accessPackageAssignmentPolicyId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of access package assignment policy with object ID %q", accessPackageAssignmentPolicyId) + } + + return nil +} + +func buildAssignmentPolicyResourceData(ctx context.Context, d *schema.ResourceData, meta interface{}) (msgraph.AccessPackageAssignmentPolicy, error) { + accessPackageClient := meta.(*clients.Client).IdentityGovernance.AccessPackageClient + + accessPackageId := d.Get("access_package_id").(string) + _, status, err := accessPackageClient.Get(ctx, accessPackageId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Access package with Object ID %q was not found - removing from state!", accessPackageId) + } + + return msgraph.AccessPackageAssignmentPolicy{}, fmt.Errorf("retrieving access package with ID %v: %v", accessPackageId, err) + } + + properties := msgraph.AccessPackageAssignmentPolicy{ + ID: utils.String(d.Id()), + DisplayName: utils.String(d.Get("display_name").(string)), + Description: utils.String(d.Get("description").(string)), + CanExtend: utils.Bool(d.Get("extension_enabled").(bool)), + DurationInDays: utils.Int32(int32(d.Get("duration_in_days").(int))), + Questions: expandAccessPackageQuestions(d.Get("question").([]interface{})), + AccessPackageId: utils.String(d.Get("access_package_id").(string)), + } + + expirationDateValue := d.Get("expiration_date").(string) + if expirationDateValue != "" { + expirationDate, err := time.Parse(time.RFC3339, expirationDateValue) + if err != nil { + return properties, fmt.Errorf("converting expiration date %v to a valid date", expirationDate) + } + + properties.ExpirationDateTime = &expirationDate + } + + properties.RequestorSettings = expandRequestorSettings(d.Get("requestor_settings").([]interface{})) + properties.RequestApprovalSettings = expandApprovalSettings(d.Get("approval_settings").([]interface{})) + + reviewSettingsStruct, err := expandAssignmentReviewSettings(d.Get("assignment_review_settings").([]interface{})) + if err != nil { + return properties, fmt.Errorf("building assignment_review_settings configuration: %v", err) + } + + properties.AccessReviewSettings = reviewSettingsStruct + + return properties, nil +} + +func assignmentPolicyDiffSuppress(k, old, new string, d *schema.ResourceData) bool { + if k == "approval_settings.#" && old == "1" && new == "0" { + return true + } + + if k == "requestor_settings.#" && old == "1" && new == "0" { + return true + } + + if k == "requestor_settings.0.scope_type" && old == msgraph.RequestorSettingsScopeTypeNoSubjects && len(new) == 0 { + return true + } + + if k == "assignment_review_settings.0.starting_on" && len(new) == 0 { + return true + } + + if k == "assignment_review_settings.#" && old == "1" && new == "0" { + return true + } + + if k == "question.#" && old == "1" && new == "0" { + return true + } + + return false +} + +func assignmentPolicyCustomDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { + if reviewSettings := diff.Get("assignment_review_settings").([]interface{}); len(reviewSettings) > 0 { + reviewSetting := reviewSettings[0].(map[string]interface{}) + if reviewSetting["enabled"].(bool) && + (reviewSetting["duration_in_days"] == 0 || + len(reviewSetting["review_frequency"].(string)) == 0 || + len(reviewSetting["access_review_timeout_behavior"].(string)) == 0) { + return fmt.Errorf("`duration_in_days`, `review_frequency`, `access_review_timeout_behavior` must be set when review is enabled") + } + } + + return nil +} diff --git a/internal/services/identitygovernance/access_package_assignment_policy_resource_test.go b/internal/services/identitygovernance/access_package_assignment_policy_resource_test.go new file mode 100644 index 000000000..ca7b37e42 --- /dev/null +++ b/internal/services/identitygovernance/access_package_assignment_policy_resource_test.go @@ -0,0 +1,329 @@ +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 AccessPackageAssignmentPolicyResource struct{} + +func TestAccAccessPackageAssignmentPolicy_simple(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_assignment_policy", "test") + r := AccessPackageAssignmentPolicyResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.simple(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("access_package_id"), + }) +} + +func TestAccAccessPackageAssignmentPolicy_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_assignment_policy", "test") + r := AccessPackageAssignmentPolicyResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("access_package_id"), + }) +} + +func TestAccAccessPackageAssignmentPolicy_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_assignment_policy", "test") + r := AccessPackageAssignmentPolicyResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("access_package_id"), + }) +} + +func TestAccAccessPackageAssignmentPolicy_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_assignment_policy", "test") + r := AccessPackageAssignmentPolicyResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (AccessPackageAssignmentPolicyResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.IdentityGovernance.AccessPackageAssignmentPolicyClient + client.BaseClient.DisableRetries = true + + _, status, err := client.Get(ctx, state.ID, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, fmt.Errorf("failed to retrieve Access package assignment policy with ID %q: %+v", state.ID, err) + } + return utils.Bool(true), nil +} + +func (AccessPackageAssignmentPolicyResource) simple(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_access_package_catalog" "test_catalog" { + display_name = "test-catalog-%[1]d" + description = "Test Catalog %[1]d" +} + +resource "azuread_access_package" "test" { + display_name = "access-package-%[1]d" + description = "Test Access Package %[1]d" + catalog_id = azuread_access_package_catalog.test_catalog.id +} + +resource "azuread_access_package_assignment_policy" "test" { + display_name = "access-package-assignment-policy-%[1]d" + description = "Test Access Package Assignnment Policy %[1]d" + access_package_id = azuread_access_package.test.id +} +`, data.RandomInteger) +} + +func (AccessPackageAssignmentPolicyResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_group" "test" { + display_name = "test-group-%[1]d" + security_enabled = true +} + +resource "azuread_access_package_catalog" "test_catalog" { + display_name = "testacc-asscess-assignment-%[1]d" + description = "TestAcc Catalog %[1]d for access assignment policy" +} + +resource "azuread_access_package" "test" { + display_name = "testacc-asscess-assignment-%[1]d" + description = "TestAcc Access Package %[1]d for access assignment policy" + catalog_id = azuread_access_package_catalog.test_catalog.id +} + +resource "azuread_access_package_assignment_policy" "test" { + display_name = "testacc-asscess-assignment-%[1]d" + description = "TestAcc Access Package Assignnment Policy %[1]d" + duration_in_days = 90 + access_package_id = azuread_access_package.test.id + + requestor_settings { + scope_type = "AllExistingDirectoryMemberUsers" + } + + approval_settings { + approval_required = true + approval_stage { + approval_timeout_in_days = 14 + + primary_approver { + object_id = azuread_group.test.object_id + subject_type = "groupMembers" + } + } + } + + assignment_review_settings { + enabled = true + review_frequency = "weekly" + duration_in_days = 3 + review_type = "Self" + access_review_timeout_behavior = "keepAccess" + } + + question { + text { + default_text = "hello, how are you?" + } + } +} +`, data.RandomInteger) +} + +func (AccessPackageAssignmentPolicyResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_group" "requestor" { + display_name = "test-requestor-%[1]d" + security_enabled = true +} + +resource "azuread_group" "first_approver" { + display_name = "test-approver-%[1]d" + security_enabled = true +} + +resource "azuread_group" "second_approver" { + display_name = "test-s-approver-%[1]d" + security_enabled = true +} + +resource "azuread_access_package_catalog" "test_catalog" { + display_name = "testacc-asscess-assignment-%[1]d" + description = "TestAcc Catalog %[1]d for access assignment policy" +} + +resource "azuread_access_package" "test" { + display_name = "testacc-asscess-assignment-%[1]d" + description = "Test Access Package %[1]d for assignment policy" + catalog_id = azuread_access_package_catalog.test_catalog.id +} +resource "azuread_access_package_assignment_policy" "test" { + display_name = "access-package-assignment-policy-%[1]d" + description = "Test Access Package Assignnment Policy %[1]d" + extension_enabled = true + expiration_date = "2096-09-23T01:02:03Z" + access_package_id = azuread_access_package.test.id + + requestor_settings { + scope_type = "SpecificDirectorySubjects" + requests_accepted = true + + requestor { + object_id = azuread_group.requestor.object_id + subject_type = "groupMembers" + } + } + + approval_settings { + approval_required = true + approval_required_for_extension = true + requestor_justification_required = true + + approval_stage { + approval_timeout_in_days = 14 + approver_justification_required = true + alternative_approval_enabled = true + enable_alternative_approval_in_days = 8 + + primary_approver { + subject_type = "requestorManager" + } + + alternative_approver { + object_id = azuread_group.second_approver.object_id + subject_type = "groupMembers" + } + } + + approval_stage { + approval_timeout_in_days = 14 + + primary_approver { + object_id = azuread_group.second_approver.object_id + subject_type = "groupMembers" + } + + primary_approver { + object_id = azuread_group.first_approver.object_id + subject_type = "groupMembers" + backup = true + } + } + } + + assignment_review_settings { + enabled = true + review_frequency = "annual" + review_type = "Reviewers" + duration_in_days = "10" + access_recommendation_enabled = true + access_review_timeout_behavior = "acceptAccessRecommendation" + + reviewer { + object_id = azuread_group.first_approver.object_id + subject_type = "groupMembers" + } + } + + question { + required = true + sequence = 1 + + text { + default_text = "Hello Why" + + localized_text { + language_code = "CN" + content = "Hello why CN?" + } + + localized_text { + language_code = "FR" + content = "Hello why BE?" + } + } + } + + question { + required = false + sequence = 2 + + choice { + actual_value = "a" + + display_value { + default_text = "AA" + + localized_text { + language_code = "CN" + content = "AAB" + } + } + } + + text { + default_text = "Hello Why again" + } + } +} +`, data.RandomInteger) +} diff --git a/internal/services/identitygovernance/access_package_catalog_data_source.go b/internal/services/identitygovernance/access_package_catalog_data_source.go new file mode 100644 index 000000000..0516c8b14 --- /dev/null +++ b/internal/services/identitygovernance/access_package_catalog_data_source.go @@ -0,0 +1,125 @@ +package identitygovernance + +import ( + "context" + "fmt" + "strings" + "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-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/manicminer/hamilton/msgraph" +) + +func accessPackageCatalogDataSource() *schema.Resource { + return &schema.Resource{ + ReadContext: accessPackageCatalogDataRead, + + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "object_id": { + Description: "The ID of this access package catalog", + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.IsUUID, + ExactlyOneOf: []string{"object_id", "display_name"}, + }, + + "display_name": { + Description: "The display name of the access package catalog", + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"object_id", "display_name"}, + }, + + "description": { + Description: "The description of the access package catalog", + Type: schema.TypeString, + Computed: true, + }, + + "externally_visible": { + Description: "Whether the access packages in this catalog can be requested by users outside the tenant", + Type: schema.TypeBool, + Computed: true, + }, + + "published": { + Description: "Whether the access packages in this catalog are available for management", + Type: schema.TypeBool, + Computed: true, + }, + }, + } +} + +func accessPackageCatalogDataRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogClient + + objectId := d.Get("object_id").(string) + displayName := d.Get("display_name").(string) + + var catalog *msgraph.AccessPackageCatalog + var err error + if objectId != "" { + catalog, _, err = client.Get(ctx, objectId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Error retrieving access package catalog with id %q", objectId) + } + } else if displayName != "" { + query := odata.Query{ + Filter: fmt.Sprintf("displayName eq '%s'", displayName), + } + + result, _, err := client.List(ctx, query) + if err != nil { + return tf.ErrorDiagF(err, "Error listing access package catalog with filter %s", query.Filter) + } + if result == nil || len(*result) == 0 { + return tf.ErrorDiagF(fmt.Errorf("no access package catalog matched with filter %s", query.Filter), "Access package catalog not found!") + } + if len(*result) > 1 { + return tf.ErrorDiagF(fmt.Errorf("multiple access package catalog matched with filter %s", query.Filter), "Multiple access package catalog found!") + } + + for _, c := range *result { + name := c.DisplayName + if name == nil { + continue + } + + if *name == displayName { + catalog = &c + break + } + } + } + + if catalog == nil { + return tf.ErrorDiagF(fmt.Errorf("no access package catalog matched with specified parameters"), "Access access package catalog not found!") + } + + published := false + if strings.EqualFold(catalog.State, msgraph.AccessPackageCatalogStatusPublished) { + published = true + } + + d.SetId(*catalog.ID) + + tf.Set(d, "object_id", catalog.ID) + tf.Set(d, "display_name", catalog.DisplayName) + tf.Set(d, "description", catalog.Description) + tf.Set(d, "externally_visible", catalog.IsExternallyVisible) + tf.Set(d, "published", published) + + return nil +} diff --git a/internal/services/identitygovernance/access_package_catalog_data_source_test.go b/internal/services/identitygovernance/access_package_catalog_data_source_test.go new file mode 100644 index 000000000..6eff360bb --- /dev/null +++ b/internal/services/identitygovernance/access_package_catalog_data_source_test.go @@ -0,0 +1,65 @@ +package identitygovernance_test + +import ( + "fmt" + "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 AccessPackageCatalogDataSource struct{} + +func TestAccAccessPackageCatalogDataSource_byId(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azuread_access_package_catalog", "test") + r := AccessPackageCatalogDataSource{} + + data.DataSourceTest(t, []resource.TestStep{ + { + Config: r.byId(data), + Check: r.testCheckFunc(data), + }, + }) +} + +func TestAccAccessPackageCatalogDataSource_byDisplayName(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azuread_access_package_catalog", "test") + r := AccessPackageCatalogDataSource{} + + data.DataSourceTest(t, []resource.TestStep{ + { + Config: r.byDisplayName(data), + Check: r.testCheckFunc(data), + }, + }) +} + +func (AccessPackageCatalogDataSource) testCheckFunc(data acceptance.TestData) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("description").HasValue(fmt.Sprintf("Test access package catalog %[1]d", data.RandomInteger)), + check.That(data.ResourceName).Key("display_name").HasValue(fmt.Sprintf("test-access-package-catalog-%[1]d", data.RandomInteger)), + check.That(data.ResourceName).Key("externally_visible").HasValue("false"), + check.That(data.ResourceName).Key("published").HasValue("false"), + ) +} + +func (AccessPackageCatalogDataSource) byId(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_access_package_catalog" "test" { + object_id = azuread_access_package_catalog.test.id +} +`, AccessPackageCatalogResource{}.complete(data)) +} + +func (AccessPackageCatalogDataSource) byDisplayName(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_access_package_catalog" "test" { + display_name = azuread_access_package_catalog.test.display_name +} +`, AccessPackageCatalogResource{}.complete(data)) +} diff --git a/internal/services/identitygovernance/access_package_catalog_resource.go b/internal/services/identitygovernance/access_package_catalog_resource.go new file mode 100644 index 000000000..d7118a09c --- /dev/null +++ b/internal/services/identitygovernance/access_package_catalog_resource.go @@ -0,0 +1,193 @@ +package identitygovernance + +import ( + "context" + "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/helpers" + "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" +) + +const accessPackageCatalogResourceName = "azuread_access_package_catalog" + +func accessPackageCatalogResource() *schema.Resource { + return &schema.Resource{ + CreateContext: accessPackageCatalogResourceCreate, + ReadContext: accessPackageCatalogResourceRead, + UpdateContext: accessPackageCatalogResourceUpdate, + DeleteContext: accessPackageCatalogResourceDelete, + + 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{ + "display_name": { + Description: "The display name of the access package catalog", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + + "description": { + Description: "The description of the access package catalog", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + + "externally_visible": { + Description: "Whether the access packages in this catalog can be requested by users outside the tenant", + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "published": { + Description: "Whether the access packages in this catalog are available for management", + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + } +} + +func accessPackageCatalogResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogClient + + displayName := d.Get("display_name").(string) + + state := msgraph.AccessPackageCatalogStateUnpublished + if d.Get("published").(bool) { + state = msgraph.AccessPackageCatalogStatePublished + } + + properties := msgraph.AccessPackageCatalog{ + DisplayName: utils.String(displayName), + Description: utils.String(d.Get("description").(string)), + State: state, + IsExternallyVisible: utils.Bool(d.Get("externally_visible").(bool)), + } + + accessPackageCatalog, _, err := client.Create(ctx, properties) + if err != nil { + return tf.ErrorDiagF(err, "Creating access package catalog %q", displayName) + } + + d.SetId(*accessPackageCatalog.ID) + + return accessPackageCatalogResourceRead(ctx, d, meta) +} + +func accessPackageCatalogResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogClient + + objectId := d.Id() + tf.LockByName(accessPackageCatalogResourceName, objectId) + defer tf.UnlockByName(accessPackageCatalogResourceName, objectId) + + state := msgraph.AccessPackageCatalogStateUnpublished + if d.Get("published").(bool) { + state = msgraph.AccessPackageCatalogStatePublished + } + + properties := msgraph.AccessPackageCatalog{ + ID: utils.String(d.Id()), + DisplayName: utils.String(d.Get("display_name").(string)), + Description: utils.String(d.Get("description").(string)), + State: state, + IsExternallyVisible: utils.Bool(d.Get("externally_visible").(bool)), + } + + if _, err := client.Update(ctx, properties); err != nil { + return tf.ErrorDiagF(err, "Could not update access package catalog with ID: %q", objectId) + } + + return accessPackageCatalogResourceRead(ctx, d, meta) +} + +func accessPackageCatalogResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogClient + + objectId := d.Id() + accessPackageCatalog, status, err := client.Get(ctx, objectId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Access package catalog with Object ID %q was not found - removing from state!", objectId) + d.SetId("") + return nil + } + + return tf.ErrorDiagF(err, "Retrieving access package catalog with object ID: %q", objectId) + } + + published := false + if strings.EqualFold(accessPackageCatalog.State, msgraph.AccessPackageCatalogStatusPublished) { + published = true + } + + tf.Set(d, "display_name", accessPackageCatalog.DisplayName) + tf.Set(d, "description", accessPackageCatalog.Description) + tf.Set(d, "published", published) + tf.Set(d, "externally_visible", accessPackageCatalog.IsExternallyVisible) + + return nil +} + +func accessPackageCatalogResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogClient + accessPackageCatalogId := d.Id() + + _, status, err := client.Get(ctx, accessPackageCatalogId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagPathF(fmt.Errorf("Access package catalog was not found"), "id", "Retrieving user with object ID %q", accessPackageCatalogId) + } + + return tf.ErrorDiagPathF(err, "id", "Retrieving access package catalog with object ID %q", accessPackageCatalogId) + } + + status, err = client.Delete(ctx, accessPackageCatalogId) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Deleting access package catalog with object ID %q, got status %d", accessPackageCatalogId, status) + } + + // Wait for object to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, accessPackageCatalogId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of access package catalog with object ID %q", accessPackageCatalogId) + } + + return nil +} diff --git a/internal/services/identitygovernance/access_package_catalog_resource_test.go b/internal/services/identitygovernance/access_package_catalog_resource_test.go new file mode 100644 index 000000000..d3bff77e2 --- /dev/null +++ b/internal/services/identitygovernance/access_package_catalog_resource_test.go @@ -0,0 +1,116 @@ +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 AccessPackageCatalogResource struct{} + +func TestAccAccessPackageCatalog_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_catalog", "test") + r := AccessPackageCatalogResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccAccessPackageCatalog_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_catalog", "test") + r := AccessPackageCatalogResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccAccessPackageCatalog_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_catalog", "test") + r := AccessPackageCatalogResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (AccessPackageCatalogResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.IdentityGovernance.AccessPackageCatalogClient + client.BaseClient.DisableRetries = true + + _, status, err := client.Get(ctx, state.ID, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + + return nil, fmt.Errorf("failed to retrieve access package catalog with ID %q: %+v", state.ID, err) + } + return utils.Bool(true), nil +} + +func (AccessPackageCatalogResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_access_package_catalog" "test" { + display_name = "test-access-package-catalog-%[1]d" + description = "Test access package catalog %[1]d" +} +`, data.RandomInteger) +} + +func (AccessPackageCatalogResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_access_package_catalog" "test" { + display_name = "test-access-package-catalog-%[1]d" + description = "Test access package catalog %[1]d" + externally_visible = false + published = false +} +`, data.RandomInteger) +} diff --git a/internal/services/identitygovernance/access_package_data_source.go b/internal/services/identitygovernance/access_package_data_source.go new file mode 100644 index 000000000..7e10a76bb --- /dev/null +++ b/internal/services/identitygovernance/access_package_data_source.go @@ -0,0 +1,127 @@ +package identitygovernance + +import ( + "context" + "fmt" + "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-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/manicminer/hamilton/msgraph" +) + +func accessPackageDataSource() *schema.Resource { + return &schema.Resource{ + ReadContext: accessPackageDataRead, + + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "object_id": { + Description: "The ID of this access package", + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.IsUUID, + AtLeastOneOf: []string{"object_id", "display_name", "catalog_id"}, + }, + + "display_name": { + Description: "The display name of the access package", + Type: schema.TypeString, + Optional: true, + Computed: true, + AtLeastOneOf: []string{"object_id", "display_name", "catalog_id"}, + ConflictsWith: []string{"object_id"}, + RequiredWith: []string{"catalog_id"}, + }, + + "catalog_id": { + Description: "The ID of the Catalog this access package is in", + Type: schema.TypeString, + Optional: true, + AtLeastOneOf: []string{"object_id", "display_name", "catalog_id"}, + ConflictsWith: []string{"object_id"}, + RequiredWith: []string{"display_name"}, + }, + + "description": { + Description: "The description of the access package", + Type: schema.TypeString, + Computed: true, + }, + + "hidden": { + Description: "Whether the access package is hidden from the requestor", + Type: schema.TypeBool, + Computed: true, + }, + }, + } +} + +func accessPackageDataRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageClient + + var err error + objectId := d.Get("object_id").(string) + displayName := d.Get("display_name").(string) + catalogId := d.Get("catalog_id").(string) + + var accessPackage *msgraph.AccessPackage + if objectId != "" { + accessPackage, _, err = client.Get(ctx, objectId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Error retrieving access package with id %q", objectId) + } + } else if displayName != "" && catalogId != "" { + query := odata.Query{ + // Filter: fmt.Sprintf("displayName eq '%s' and catalogId eq '%s'", displayName, catalogId), + // Filter: fmt.Sprintf("catalogId eq '%s'", catalogId), + } + + result, _, err := client.List(ctx, query) + if err != nil { + return tf.ErrorDiagF(err, "Error listing access package with filter %s", query.Filter) + } + if result == nil || len(*result) == 0 { + return tf.ErrorDiagF(fmt.Errorf("no access package matched with filter %s", query.Filter), "Access access package not found!") + } + // if len(*result) > 1 { + // return tf.ErrorDiagF(fmt.Errorf("Multiple access package matched with filter %s", query.Filter), "Multitple access package found!") + // } + + for _, c := range *result { + name := c.DisplayName + catalog := c.CatalogId + if name == nil || catalog == nil { + continue + } + + if *name == displayName && *c.CatalogId == catalogId { + accessPackage = &c + break + } + } + } + + if accessPackage == nil { + return tf.ErrorDiagF(fmt.Errorf("no access package matched with specified parameters"), "Access access package not found!") + } + + d.SetId(*accessPackage.ID) + + tf.Set(d, "object_id", accessPackage.ID) + tf.Set(d, "display_name", accessPackage.DisplayName) + tf.Set(d, "description", accessPackage.Description) + tf.Set(d, "hidden", accessPackage.IsHidden) + tf.Set(d, "catalog_id", accessPackage.CatalogId) + + return nil +} diff --git a/internal/services/identitygovernance/access_package_data_source_test.go b/internal/services/identitygovernance/access_package_data_source_test.go new file mode 100644 index 000000000..f25608115 --- /dev/null +++ b/internal/services/identitygovernance/access_package_data_source_test.go @@ -0,0 +1,66 @@ +package identitygovernance_test + +import ( + "fmt" + "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 AccessPackageDataSource struct{} + +func TestAccAccessPackageDataSource_byId(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azuread_access_package", "test") + r := AccessPackageDataSource{} + + data.DataSourceTest(t, []resource.TestStep{ + { + Config: r.byId(data), + Check: r.testCheckFunc(data), + }, + }) +} + +func TestAccAccessPackageDataSource_byDisplayName(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azuread_access_package", "test") + r := AccessPackageDataSource{} + + data.DataSourceTest(t, []resource.TestStep{ + { + Config: r.byDisplayName(data), + Check: r.testCheckFunc(data), + }, + }) +} + +func (AccessPackageDataSource) testCheckFunc(data acceptance.TestData) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("description").HasValue(fmt.Sprintf("Access Package %[1]d", data.RandomInteger)), + check.That(data.ResourceName).Key("display_name").HasValue(fmt.Sprintf("access-package-%[1]d", data.RandomInteger)), + check.That(data.ResourceName).Key("hidden").HasValue("true"), + check.That(data.ResourceName).Key("catalog_id").Exists(), + ) +} + +func (AccessPackageDataSource) byId(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_access_package" "test" { + object_id = azuread_access_package.test.id +} +`, AccessPackageResource{}.complete(data)) +} + +func (AccessPackageDataSource) byDisplayName(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_access_package" "test" { + display_name = azuread_access_package.test.display_name + catalog_id = azuread_access_package_catalog.test_catalog.id +} +`, AccessPackageResource{}.complete(data)) +} diff --git a/internal/services/identitygovernance/access_package_resource.go b/internal/services/identitygovernance/access_package_resource.go new file mode 100644 index 000000000..208bd2daf --- /dev/null +++ b/internal/services/identitygovernance/access_package_resource.go @@ -0,0 +1,196 @@ +package identitygovernance + +import ( + "context" + "fmt" + "log" + "net/http" + "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/helpers" + "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" +) + +const accessPackageResourceName = "azuread_access_package" + +func accessPackageResource() *schema.Resource { + return &schema.Resource{ + CreateContext: accessPackageResourceCreate, + ReadContext: accessPackageResourceRead, + UpdateContext: accessPackageResourceUpdate, + DeleteContext: accessPackageResourceDelete, + + 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{ + "catalog_id": { + Description: "The ID of the Catalog this access package will be created in", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + + "display_name": { + Description: "The display name of the access package", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + + "description": { + Description: "The description of the access package", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + + "hidden": { + Description: "Whether the access package is hidden from the requestor", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + } +} + +func accessPackageResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageClient + accessPackageCatalogClient := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogClient + + displayName := d.Get("display_name").(string) + catalogId := d.Get("catalog_id").(string) + + accessPackageCatalog, _, err := accessPackageCatalogClient.Get(ctx, catalogId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Retrieving access package catalog with object ID: %q", catalogId) + } + + properties := msgraph.AccessPackage{ + DisplayName: utils.String(displayName), + Description: utils.String(d.Get("description").(string)), + IsHidden: utils.Bool(d.Get("hidden").(bool)), + Catalog: accessPackageCatalog, + CatalogId: accessPackageCatalog.ID, + } + + accessPackage, _, err := client.Create(ctx, properties) + if err != nil { + return tf.ErrorDiagF(err, "Creating access package %q", displayName) + } + + d.SetId(*accessPackage.ID) + + return accessPackageResourceRead(ctx, d, meta) +} + +func accessPackageResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageClient + accessPackageCatalogClient := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogClient + + objectId := d.Id() + catalogId := d.Get("catalog_id").(string) + + accessPackageCatalog, _, err := accessPackageCatalogClient.Get(ctx, catalogId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Retrieving access package catalog with ID: %q", catalogId) + } + + tf.LockByName(accessPackageResourceName, objectId) + defer tf.UnlockByName(accessPackageResourceName, objectId) + + properties := msgraph.AccessPackage{ + ID: utils.String(objectId), + DisplayName: utils.String(d.Get("display_name").(string)), + Description: utils.String(d.Get("description").(string)), + IsHidden: utils.Bool(d.Get("hidden").(bool)), + Catalog: accessPackageCatalog, + CatalogId: accessPackageCatalog.ID, + } + + if _, err := client.Update(ctx, properties); err != nil { + return tf.ErrorDiagF(err, "Could not update access package with ID: %q", objectId) + } + + return accessPackageResourceRead(ctx, d, meta) +} + +func accessPackageResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageClient + + objectId := d.Id() + accessPackage, status, err := client.Get(ctx, objectId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Access package with Object ID %q was not found - removing from state!", objectId) + d.SetId("") + return nil + } + + return tf.ErrorDiagF(err, "Retrieving access package with object ID: %q", objectId) + } + + tf.Set(d, "display_name", accessPackage.DisplayName) + tf.Set(d, "description", accessPackage.Description) + tf.Set(d, "hidden", accessPackage.IsHidden) + //v1.0 graph API doesn't contain this info however beta contains + tf.Set(d, "catalog_id", accessPackage.CatalogId) + + return nil +} + +func accessPackageResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageClient + accessPackageId := d.Id() + + _, status, err := client.Get(ctx, accessPackageId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagPathF(fmt.Errorf("Access package was not found"), "id", "Retrieving user with object ID %q", accessPackageId) + } + + return tf.ErrorDiagPathF(err, "id", "Retrieving access package with object ID %q", accessPackageId) + } + + status, err = client.Delete(ctx, accessPackageId) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Deleting access package with object ID %q, got status %d", accessPackageId, status) + } + + // Wait for object to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, accessPackageId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of access package with object ID %q", accessPackageId) + } + + return nil +} diff --git a/internal/services/identitygovernance/access_package_resource_catalog_association_resource.go b/internal/services/identitygovernance/access_package_resource_catalog_association_resource.go new file mode 100644 index 000000000..e6e0e71ce --- /dev/null +++ b/internal/services/identitygovernance/access_package_resource_catalog_association_resource.go @@ -0,0 +1,165 @@ +package identitygovernance + +import ( + "context" + "log" + "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/services/identitygovernance/parse" + "github.com/hashicorp/terraform-provider-azuread/internal/services/identitygovernance/validate" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/manicminer/hamilton/msgraph" +) + +func accessPackageResourceCatalogAssociationResource() *schema.Resource { + return &schema.Resource{ + CreateContext: accessPackageResourceCatalogAssociationResourceCreate, + ReadContext: accessPackageResourceCatalogAssociationResourceRead, + DeleteContext: accessPackageResourceCatalogAssociationResourceDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Importer: tf.ValidateResourceIDPriorToImport(validate.AccessPackageResourceCatalogAssociationID), + + Schema: map[string]*schema.Schema{ + "resource_origin_id": { + Description: "The unique identifier of the resource in the origin system. In the case of an Azure AD group, this is the identifier of the group", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "resource_origin_system": { + Description: "The type of the resource in the origin system, such as SharePointOnline, AadApplication or AadGroup", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "catalog_id": { + Description: "The unique ID of the access package catalog", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func accessPackageResourceCatalogAssociationResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageResourceRequestClient + accessPackageCatalogClient := meta.(*clients.Client).IdentityGovernance.AccessPackageCatalogClient + resourceClient := meta.(*clients.Client).IdentityGovernance.AccessPackageResourceClient + + catalogId := d.Get("catalog_id").(string) + resourceOriginId := d.Get("resource_origin_id").(string) + resourceOriginSystem := d.Get("resource_origin_system").(string) + + _, status, err := accessPackageCatalogClient.Get(ctx, catalogId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Access package catalog with Object ID %q was not found - removing from state!", catalogId) + return nil + } + + return tf.ErrorDiagF(err, "Retrieving access package catalog with object ID: %q", catalogId) + } + + if existing, _, err := resourceClient.Get(ctx, catalogId, resourceOriginId); err == nil && existing != nil { + id := parse.NewAccessPackageResourceCatalogAssociationID(catalogId, resourceOriginId) + return tf.ImportAsExistsDiag("azuread_access_package_resource_catalog_association", id.ID()) + } + + properties := msgraph.AccessPackageResourceRequest{ + CatalogId: &catalogId, + RequestType: utils.String("AdminAdd"), + AccessPackageResource: &msgraph.AccessPackageResource{ + OriginId: &resourceOriginId, + OriginSystem: resourceOriginSystem, + }, + } + + resourceCatalogAssociation, _, err := client.Create(ctx, properties, true) + if err != nil { + return tf.ErrorDiagF(err, "Failed to link resource %q@%q with access catalog %q.", resourceOriginId, resourceOriginSystem, catalogId) + } + + id := parse.NewAccessPackageResourceCatalogAssociationID(*resourceCatalogAssociation.CatalogId, *resourceCatalogAssociation.AccessPackageResource.OriginId) + d.SetId(id.ID()) + + return accessPackageResourceCatalogAssociationResourceRead(ctx, d, meta) +} + +func accessPackageResourceCatalogAssociationResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + resourceClient := meta.(*clients.Client).IdentityGovernance.AccessPackageResourceClient + + id, err := parse.AccessPackageResourceCatalogAssociationID(d.Id()) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Failed to parse resource ID %q", d.Id()) + } + + accessPackageRes, status, err := resourceClient.Get(ctx, id.CatalogId, id.OriginId) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Access package resource and catalog association with resource origin ID %q and catalog ID %q was not found - removing from state!", id.OriginId, id.CatalogId) + d.SetId("") + return nil + } + + return tf.ErrorDiagF(err, "Error retrieving access package resource and catalog association with resource origin id %q and catalog id %q.", id.OriginId, id.CatalogId) + } + + tf.Set(d, "catalog_id", id.CatalogId) + tf.Set(d, "resource_origin_id", id.OriginId) + tf.Set(d, "resource_origin_system", accessPackageRes.OriginSystem) + + return nil +} + +func accessPackageResourceCatalogAssociationResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageResourceRequestClient + resourceClient := meta.(*clients.Client).IdentityGovernance.AccessPackageResourceClient + + id, err := parse.AccessPackageResourceCatalogAssociationID(d.Id()) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Failed to parse resource ID %q", d.Id()) + } + + resource, status, err := resourceClient.Get(ctx, id.CatalogId, id.OriginId) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Access package resource and catalog association with resource %q@%q and catalog id %q was not found - removing from state!", id.OriginId, resource.OriginSystem, id.CatalogId) + d.SetId("") + return nil + } + + return tf.ErrorDiagF(err, "Retrieving access package resource and catalog association with resource %q@%q and catalog id %q.", id.OriginId, resource.OriginSystem, id.CatalogId) + } + + if err != nil { + return tf.ErrorDiagF(err, "Error retrieving access package resource with origin ID %q in catalog %q.", id.OriginId, id.CatalogId) + } + + resourceCatalogAssociation := msgraph.AccessPackageResourceRequest{ + CatalogId: &id.CatalogId, + AccessPackageResource: resource, + } + + _, err = client.Delete(ctx, resourceCatalogAssociation) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Deleting access package resource and catalog association with resource %q@%q and catalog id %q.", + *resourceCatalogAssociation.AccessPackageResource.OriginId, resourceCatalogAssociation.AccessPackageResource.OriginSystem, *resourceCatalogAssociation.CatalogId) + } + + return nil +} diff --git a/internal/services/identitygovernance/access_package_resource_catalog_association_resource_test.go b/internal/services/identitygovernance/access_package_resource_catalog_association_resource_test.go new file mode 100644 index 000000000..fc649660c --- /dev/null +++ b/internal/services/identitygovernance/access_package_resource_catalog_association_resource_test.go @@ -0,0 +1,103 @@ +package identitygovernance_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/services/identitygovernance/parse" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" +) + +type AccessPackageResourceCatalogAssociationResource struct{} + +func TestAccAccessPackageResourceCatalogAssociation_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_resource_catalog_association", "test") + r := AccessPackageResourceCatalogAssociationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccAccessPackageResourceCatalogAssociation_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_resource_catalog_association", "test") + r := AccessPackageResourceCatalogAssociationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.RequiresImportErrorStep(r.requiresImport(data)), + }) +} + +func (r AccessPackageResourceCatalogAssociationResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.IdentityGovernance.AccessPackageResourceClient + client.BaseClient.DisableRetries = true + + id, err := parse.AccessPackageResourceCatalogAssociationID(state.ID) + if err != nil { + return nil, err + } + + _, status, err := client.Get(ctx, id.CatalogId, id.OriginId) + if err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + + return nil, fmt.Errorf("failed to retrieve access package catalog association with ID %q: %+v", id.ID(), err) + } + + return utils.Bool(true), nil +} + +func (r AccessPackageResourceCatalogAssociationResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_group" "test_group" { + display_name = "test-access-package-resource-catalog-association-%[1]d" + security_enabled = true +} + +resource "azuread_access_package_catalog" "test_catalog" { + display_name = "test-catalog-%[1]d" + description = "Test catalog %[1]d" +} + +resource "azuread_access_package_resource_catalog_association" "test" { + catalog_id = azuread_access_package_catalog.test_catalog.id + resource_origin_id = azuread_group.test_group.object_id + resource_origin_system = "AadGroup" +} +`, data.RandomInteger) +} + +func (r AccessPackageResourceCatalogAssociationResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_access_package_resource_catalog_association" "import" { + catalog_id = azuread_access_package_resource_catalog_association.test.catalog_id + resource_origin_id = azuread_access_package_resource_catalog_association.test.resource_origin_id + resource_origin_system = azuread_access_package_resource_catalog_association.test.resource_origin_system +} +`, r.complete(data), data.RandomInteger) +} diff --git a/internal/services/identitygovernance/access_package_resource_package_association_resource.go b/internal/services/identitygovernance/access_package_resource_package_association_resource.go new file mode 100644 index 000000000..3129ccfcb --- /dev/null +++ b/internal/services/identitygovernance/access_package_resource_package_association_resource.go @@ -0,0 +1,150 @@ +package identitygovernance + +import ( + "context" + "fmt" + "log" + "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-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/services/identitygovernance/parse" + "github.com/hashicorp/terraform-provider-azuread/internal/services/identitygovernance/validate" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/manicminer/hamilton/msgraph" +) + +func accessPackageResourcePackageAssociationResource() *schema.Resource { + return &schema.Resource{ + CreateContext: accessPackageResourcePackageAssociationResourceCreate, + ReadContext: accessPackageResourcePackageAssociationResourceRead, + DeleteContext: accessPackageResourcePackageAssociationResourceDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Importer: tf.ValidateResourceIDPriorToImport(validate.AccessPackageResourcePackageAssociationID), + + Schema: map[string]*schema.Schema{ + "access_package_id": { + Description: "The ID of access package this resource association is configured to", + Type: schema.TypeString, + ValidateFunc: validation.IsUUID, + Required: true, + ForceNew: true, + }, + + "catalog_resource_association_id": { + Description: "The ID of the access package catalog association", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "access_type": { + Description: "The role of access type to the specified resource, valid values are `Member` and `Owner`", + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "Member", + ValidateFunc: validation.StringInSlice([]string{ + "Member", + "Owner", + }, false), + }, + }, + } +} + +func accessPackageResourcePackageAssociationResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageResourceRoleScopeClient + resourceClient := meta.(*clients.Client).IdentityGovernance.AccessPackageResourceClient + + catalogResourceAssociationId, err := parse.AccessPackageResourceCatalogAssociationID(d.Get("catalog_resource_association_id").(string)) + if err != nil { + return tf.ErrorDiagPathF(err, "catalog_resource_association_id", "Invalid catalog_resource_association_id: %q", d.Get("catalog_resource_association_id").(string)) + } + + accessType := d.Get("access_type").(string) + accessPackageId := d.Get("access_package_id").(string) + + resource, _, err := resourceClient.Get(ctx, catalogResourceAssociationId.CatalogId, catalogResourceAssociationId.OriginId) + if err != nil { + return tf.ErrorDiagF(err, "Error retrieving access package resource and catalog association with resource ID %q and catalog ID %q.", catalogResourceAssociationId.CatalogId, catalogResourceAssociationId.OriginId) + } + + properties := msgraph.AccessPackageResourceRoleScope{ + AccessPackageId: &accessPackageId, + AccessPackageResourceRole: &msgraph.AccessPackageResourceRole{ + DisplayName: utils.String(accessType), + OriginId: utils.String(fmt.Sprintf("%s_%s", accessType, catalogResourceAssociationId.OriginId)), + OriginSystem: resource.OriginSystem, + AccessPackageResource: &msgraph.AccessPackageResource{ + ID: resource.ID, + ResourceType: resource.ResourceType, + OriginId: resource.OriginId, + }, + }, + AccessPackageResourceScope: &msgraph.AccessPackageResourceScope{ + OriginSystem: resource.OriginSystem, + OriginId: &catalogResourceAssociationId.OriginId, + }, + } + + resourcePackageAssociation, _, err := client.Create(ctx, properties) + if err != nil { + return tf.ErrorDiagF(err, "Error creating access package resource association from resource %q@%q to access package %q.", catalogResourceAssociationId.OriginId, resource.OriginSystem, accessPackageId) + } + + id := parse.NewAccessPackageResourcePackageAssociationID(accessPackageId, *resourcePackageAssociation.ID, *resource.OriginId, accessType) + d.SetId(id.ID()) + + return accessPackageResourcePackageAssociationResourceRead(ctx, d, meta) +} + +func accessPackageResourcePackageAssociationResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).IdentityGovernance.AccessPackageResourceRoleScopeClient + accessPackageClient := meta.(*clients.Client).IdentityGovernance.AccessPackageClient + + id, err := parse.AccessPackageResourcePackageAssociationID(d.Id()) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Failed to parse resource ID %q", d.Id()) + } + + resourcePackage, status, err := client.Get(ctx, id.AccessPackageId, id.ResourcePackageAssociationId) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Access package resource association with ID %q was not found - removing from state!", d.Id()) + d.SetId("") + return nil + } + return tf.ErrorDiagF(err, "Error retrieving resource id %v in access package %v", id.ResourcePackageAssociationId, id.AccessPackageId) + } + + accessPackage, _, err := accessPackageClient.Get(ctx, id.AccessPackageId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Err retrieving access package with id %v", id.AccessPackageId) + } + + catalogResourceAssociationId := parse.NewAccessPackageResourceCatalogAssociationID(*accessPackage.CatalogId, id.OriginId) + + tf.Set(d, "access_package_id", resourcePackage.AccessPackageId) + tf.Set(d, "access_type", id.AccessType) + tf.Set(d, "catalog_resource_association_id", catalogResourceAssociationId.ID()) + + return nil +} + +func accessPackageResourcePackageAssociationResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Printf("[WARN] azuread_access_package_resource_package_association with ID %q must be manually deleted as there is no valid API provided for this operation", d.Id()) + + return nil +} diff --git a/internal/services/identitygovernance/access_package_resource_package_association_resource_test.go b/internal/services/identitygovernance/access_package_resource_package_association_resource_test.go new file mode 100644 index 000000000..d8f6c84d9 --- /dev/null +++ b/internal/services/identitygovernance/access_package_resource_package_association_resource_test.go @@ -0,0 +1,88 @@ +package identitygovernance_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/services/identitygovernance/parse" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" +) + +type AccessPackageResourcePackageAssociationResource struct{} + +func TestAccAccessPackageResourcePackageAssociation_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package_resource_package_association", "test") + r := AccessPackageResourcePackageAssociationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data), + Destroy: false, + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (AccessPackageResourcePackageAssociationResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.IdentityGovernance.AccessPackageResourceRoleScopeClient + client.BaseClient.DisableRetries = true + + id, err := parse.AccessPackageResourcePackageAssociationID(state.ID) + if err != nil { + return nil, err + } + + _, status, err := client.Get(ctx, id.AccessPackageId, id.ResourcePackageAssociationId) + if err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + + return nil, fmt.Errorf("failed to retrieve access package resource association with ID %q: %+v", id.ID(), err) + } + + return utils.Bool(true), nil +} + +func (AccessPackageResourcePackageAssociationResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_group" "test_group" { + display_name = "test-access-package-resource-catalog-association-%[1]d" + security_enabled = true +} + +resource "azuread_access_package_catalog" "test_catalog" { + display_name = "test-catalog-%[1]d" + description = "Test catalog %[1]d" +} + +resource "azuread_access_package_resource_catalog_association" "test" { + catalog_id = azuread_access_package_catalog.test_catalog.id + resource_origin_id = azuread_group.test_group.object_id + resource_origin_system = "AadGroup" +} + +resource "azuread_access_package" "test" { + display_name = "test-package-%[1]d" + description = "Test Package %[1]d" + catalog_id = azuread_access_package_catalog.test_catalog.id +} + +resource "azuread_access_package_resource_package_association" "test" { + access_package_id = azuread_access_package.test.id + catalog_resource_association_id = azuread_access_package_resource_catalog_association.test.id +} +`, data.RandomInteger) +} diff --git a/internal/services/identitygovernance/access_package_resource_test.go b/internal/services/identitygovernance/access_package_resource_test.go new file mode 100644 index 000000000..9b61d4172 --- /dev/null +++ b/internal/services/identitygovernance/access_package_resource_test.go @@ -0,0 +1,126 @@ +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 AccessPackageResource struct{} + +func TestAccAccessPackage_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package", "test") + r := AccessPackageResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("catalog_id"), + }) +} + +func TestAccAccessPackage_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package", "test") + r := AccessPackageResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("catalog_id"), + }) +} + +func TestAccAccessPackage_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_access_package", "test") + r := AccessPackageResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (AccessPackageResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.IdentityGovernance.AccessPackageClient + client.BaseClient.DisableRetries = true + + _, status, err := client.Get(ctx, state.ID, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, fmt.Errorf("failed to retrieve access package with ID %q: %+v", state.ID, err) + } + return utils.Bool(true), nil +} + +func (AccessPackageResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_access_package_catalog" "test_catalog" { + display_name = "test-catalog-%[1]d" + description = "Test catalog %[1]d" +} + +resource "azuread_access_package" "test" { + display_name = "access-package-%[1]d" + description = "Access Package %[1]d" + catalog_id = azuread_access_package_catalog.test_catalog.id +} +`, data.RandomInteger) +} + +func (AccessPackageResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_access_package_catalog" "test_catalog" { + display_name = "test-catalog-%[1]d" + description = "Test catalog %[1]d" +} + +resource "azuread_access_package" "test" { + display_name = "access-package-%[1]d" + description = "Access Package %[1]d" + hidden = true + catalog_id = azuread_access_package_catalog.test_catalog.id +} +`, data.RandomInteger) +} diff --git a/internal/services/identitygovernance/client/client.go b/internal/services/identitygovernance/client/client.go new file mode 100644 index 000000000..45462e96d --- /dev/null +++ b/internal/services/identitygovernance/client/client.go @@ -0,0 +1,54 @@ +package client + +import ( + "github.com/hashicorp/terraform-provider-azuread/internal/common" + "github.com/manicminer/hamilton/msgraph" +) + +type Client struct { + AccessPackageAssignmentPolicyClient *msgraph.AccessPackageAssignmentPolicyClient + AccessPackageCatalogClient *msgraph.AccessPackageCatalogClient + AccessPackageClient *msgraph.AccessPackageClient + AccessPackageResourceClient *msgraph.AccessPackageResourceClient + AccessPackageResourceRequestClient *msgraph.AccessPackageResourceRequestClient + AccessPackageResourceRoleScopeClient *msgraph.AccessPackageResourceRoleScopeClient +} + +func NewClient(o *common.ClientOptions) *Client { + // Resource only available in beta API + accessPackageAssignmentPolicyClient := msgraph.NewAccessPackageAssignmentPolicyClient() + o.ConfigureClient(&accessPackageAssignmentPolicyClient.BaseClient) + accessPackageAssignmentPolicyClient.BaseClient.ApiVersion = msgraph.VersionBeta + + accessPackageCatalogClient := msgraph.NewAccessPackageCatalogClient() + o.ConfigureClient(&accessPackageCatalogClient.BaseClient) + + // Use beta version because it replies more info than v1.0 + accessPackageClient := msgraph.NewAccessPackageClient() + o.ConfigureClient(&accessPackageClient.BaseClient) + accessPackageClient.BaseClient.ApiVersion = msgraph.VersionBeta + + // Use beta version because it replies more info than v1.0 and the URL is different + accessPackageResourceClient := msgraph.NewAccessPackageResourceClient() + o.ConfigureClient(&accessPackageResourceClient.BaseClient) + accessPackageResourceClient.BaseClient.ApiVersion = msgraph.VersionBeta + + // Resource only available in beta API + accessPackageResourceRequestClient := msgraph.NewAccessPackageResourceRequestClient() + o.ConfigureClient(&accessPackageResourceRequestClient.BaseClient) + accessPackageResourceRequestClient.BaseClient.ApiVersion = msgraph.VersionBeta + + // Resource only available in beta API + accessPackageResourceRoleScopeClient := msgraph.NewAccessPackageResourceRoleScopeClient() + o.ConfigureClient(&accessPackageResourceRoleScopeClient.BaseClient) + accessPackageResourceRoleScopeClient.BaseClient.ApiVersion = msgraph.VersionBeta + + return &Client{ + AccessPackageAssignmentPolicyClient: accessPackageAssignmentPolicyClient, + AccessPackageCatalogClient: accessPackageCatalogClient, + AccessPackageClient: accessPackageClient, + AccessPackageResourceClient: accessPackageResourceClient, + AccessPackageResourceRequestClient: accessPackageResourceRequestClient, + AccessPackageResourceRoleScopeClient: accessPackageResourceRoleScopeClient, + } +} diff --git a/internal/services/identitygovernance/identitygovernance.go b/internal/services/identitygovernance/identitygovernance.go new file mode 100644 index 000000000..813394e69 --- /dev/null +++ b/internal/services/identitygovernance/identitygovernance.go @@ -0,0 +1,342 @@ +package identitygovernance + +import ( + "fmt" + "time" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/manicminer/hamilton/msgraph" +) + +func expandRequestorSettings(input []interface{}) *msgraph.RequestorSettings { + if len(input) == 0 { + return nil + } + in := input[0].(map[string]interface{}) + result := msgraph.RequestorSettings{ + ScopeType: in["scope_type"].(string), + AcceptRequests: utils.Bool(in["requests_accepted"].(bool)), + } + result.AllowedRequestors = expandUserSets(in["requestor"].([]interface{})) + + return &result +} + +func flattenRequestorSettings(input *msgraph.RequestorSettings) []map[string]interface{} { + if input == nil { + return nil + } + + return []map[string]interface{}{{ + "requests_accepted": input.AcceptRequests, + "scope_type": input.ScopeType, + "requestor": flattenUserSets(input.AllowedRequestors), + }} +} + +func expandApprovalSettings(input []interface{}) *msgraph.ApprovalSettings { + if len(input) == 0 { + return nil + } + + in := input[0].(map[string]interface{}) + + result := msgraph.ApprovalSettings{ + IsApprovalRequired: utils.Bool(in["approval_required"].(bool)), + IsApprovalRequiredForExtension: utils.Bool(in["approval_required_for_extension"].(bool)), + IsRequestorJustificationRequired: utils.Bool(in["requestor_justification_required"].(bool)), + } + + approvalStages := make([]msgraph.ApprovalStage, 0) + for _, v := range in["approval_stage"].([]interface{}) { + v_map := v.(map[string]interface{}) + + stage := msgraph.ApprovalStage{ + ApprovalStageTimeOutInDays: utils.Int32(int32(v_map["approval_timeout_in_days"].(int))), + EscalationTimeInMinutes: utils.Int32(int32(v_map["enable_alternative_approval_in_days"].(int) * 24 * 60)), + IsApproverJustificationRequired: utils.Bool(v_map["approver_justification_required"].(bool)), + IsEscalationEnabled: utils.Bool(v_map["alternative_approval_enabled"].(bool)), + } + + stage.PrimaryApprovers = expandUserSets(v_map["primary_approver"].([]interface{})) + stage.EscalationApprovers = expandUserSets(v_map["alternative_approver"].([]interface{})) + + approvalStages = append(approvalStages, stage) + } + + result.ApprovalStages = &approvalStages + + return &result +} + +func flattenApprovalSettings(input *msgraph.ApprovalSettings) []map[string]interface{} { + if input == nil { + return nil + } + + result := []map[string]interface{}{{ + "approval_required": input.IsApprovalRequired, + "approval_required_for_extension": input.IsApprovalRequiredForExtension, + "requestor_justification_required": input.IsRequestorJustificationRequired, + }} + + approvalStages := make([]interface{}, 0) + for _, v := range *input.ApprovalStages { + approvalStage := map[string]interface{}{ + "approval_timeout_in_days": v.ApprovalStageTimeOutInDays, + "approver_justification_required": v.IsApproverJustificationRequired, + "alternative_approval_enabled": v.IsEscalationEnabled, + "enable_alternative_approval_in_days": *v.EscalationTimeInMinutes / 60 / 24, + "primary_approver": flattenUserSets(v.PrimaryApprovers), + "alternative_approver": flattenUserSets(v.EscalationApprovers), + } + + approvalStages = append(approvalStages, approvalStage) + } + result[0]["approval_stage"] = approvalStages + + return result +} + +func expandAssignmentReviewSettings(input []interface{}) (*msgraph.AssignmentReviewSettings, error) { + if len(input) == 0 { + return nil, nil + } + + in := input[0].(map[string]interface{}) + + result := msgraph.AssignmentReviewSettings{ + AccessReviewTimeoutBehavior: in["access_review_timeout_behavior"].(string), + DurationInDays: utils.Int32(int32(in["duration_in_days"].(int))), + IsAccessRecommendationEnabled: utils.Bool(in["access_recommendation_enabled"].(bool)), + IsApprovalJustificationRequired: utils.Bool(in["approver_justification_required"].(bool)), + IsEnabled: utils.Bool(in["enabled"].(bool)), + RecurrenceType: in["review_frequency"].(string), + ReviewerType: in["review_type"].(string), + } + + startOnDate := in["starting_on"].(string) + if startOnDate != "" { + startOn, err := time.Parse(time.RFC3339, startOnDate) + if err != nil { + return nil, fmt.Errorf("converting starting date %q to a valid date: %q", in["starting_on"].(string), err) + } + + result.StartDateTime = &startOn + } + + result.Reviewers = expandUserSets(in["reviewer"].([]interface{})) + + return &result, nil +} + +func flattenAssignmentReviewSettings(input *msgraph.AssignmentReviewSettings) []map[string]interface{} { + if input == nil { + return nil + } + + return []map[string]interface{}{{ + "access_recommendation_enabled": input.IsAccessRecommendationEnabled, + "access_review_timeout_behavior": input.AccessReviewTimeoutBehavior, + "approver_justification_required": input.IsApprovalJustificationRequired, + "duration_in_days": input.DurationInDays, + "enabled": input.IsEnabled, + "review_frequency": input.RecurrenceType, + "review_type": input.ReviewerType, + "reviewer": flattenUserSets(input.Reviewers), + "starting_on": input.StartDateTime.Format(time.RFC3339), + }} +} + +func expandUserSets(input []interface{}) *[]msgraph.UserSet { + userSets := make([]msgraph.UserSet, 0) + for _, v := range input { + v_map := v.(map[string]interface{}) + oDataType, needId := userSetODataType(v_map["subject_type"].(string)) + userSet := msgraph.UserSet{ + ODataType: oDataType, + IsBackup: utils.Bool(v_map["backup"].(bool)), + } + if needId { + userSet.ID = utils.String(v_map["object_id"].(string)) + } + + userSets = append(userSets, userSet) + } + + return &userSets +} + +func flattenUserSets(input *[]msgraph.UserSet) []interface{} { + if input == nil || len(*input) == 0 { + return nil + } + + userSets := make([]interface{}, 0) + for _, v := range *input { + userSet := map[string]interface{}{ + "subject_type": userSetShortType(*v.ODataType), + "backup": v.IsBackup, + "object_id": v.ID, + } + + userSets = append(userSets, userSet) + } + + return userSets +} + +func userSetODataType(in string) (*string, bool) { + odataType := odata.TypeSingleUser + needId := true + switch in { + case odata.ShortTypeGroupMembers: + odataType = odata.TypeGroupMembers + case odata.ShortTypeConnectedOrganizationMembers: + odataType = odata.TypeConnectedOrganizationMembers + case odata.ShortTypeRequestorManager: + odataType = odata.TypeRequestorManager + needId = false + case odata.ShortTypeInternalSponsors: + odataType = odata.TypeInternalSponsors + needId = false + case odata.ShortTypeExternalSponsors: + odataType = odata.TypeExternalSponsors + needId = false + } + + return &odataType, needId +} + +func userSetShortType(in string) *string { + shortType := odata.ShortTypeSingleUser + switch in { + case odata.TypeGroupMembers: + shortType = odata.ShortTypeGroupMembers + case odata.TypeConnectedOrganizationMembers: + shortType = odata.ShortTypeConnectedOrganizationMembers + case odata.TypeRequestorManager: + shortType = odata.ShortTypeRequestorManager + case odata.TypeInternalSponsors: + shortType = odata.ShortTypeInternalSponsors + case odata.TypeExternalSponsors: + shortType = odata.ShortTypeExternalSponsors + } + + return &shortType +} + +func expandAccessPackageQuestions(questions []interface{}) *[]msgraph.AccessPackageQuestion { + result := make([]msgraph.AccessPackageQuestion, 0) + + for _, v := range questions { + v_map := v.(map[string]interface{}) + v_text_list := v_map["text"].([]interface{}) + v_text := v_text_list[0].(map[string]interface{}) + + q := msgraph.AccessPackageQuestion{ + IsRequired: utils.Bool(v_map["required"].(bool)), + Sequence: utils.Int32(int32(v_map["sequence"].(int))), + Text: expandAccessPackageLocalizedContent(v_text), + } + + v_map_choices := v_map["choice"].([]interface{}) + q.ODataType = utils.String(odata.TypeAccessPackageTextInputQuestion) + if len(v_map_choices) > 0 { + q.ODataType = utils.String(odata.TypeAccessPackageMultipleChoiceQuestion) + choices := make([]msgraph.AccessPackageMultipleChoiceQuestions, 0) + + for _, c := range v_map_choices { + c_map := c.(map[string]interface{}) + c_map_display_value := c_map["display_value"].([]interface{}) + choices = append(choices, msgraph.AccessPackageMultipleChoiceQuestions{ + ActualValue: utils.String(c_map["actual_value"].(string)), + DisplayValue: expandAccessPackageLocalizedContent(c_map_display_value[0].(map[string]interface{})), + }) + } + + q.Choices = &choices + } + + result = append(result, q) + } + + return &result +} + +func flattenAccessPackageQuestions(input *[]msgraph.AccessPackageQuestion) []map[string]interface{} { + if input == nil || len(*input) == 0 { + return nil + } + + questions := make([]map[string]interface{}, 0) + + for _, v := range *input { + question := map[string]interface{}{ + "required": v.IsRequired, + "sequence": v.Sequence, + "text": flattenAccessPackageLocalizedContent(v.Text), + } + + if c_array := v.Choices; c_array != nil && len(*c_array) > 0 { + choices := make([]map[string]interface{}, 0) + + for _, c := range *c_array { + choice := map[string]interface{}{ + "actual_value": c.ActualValue, + "display_value": flattenAccessPackageLocalizedContent(c.DisplayValue), + } + + choices = append(choices, choice) + } + + question["choice"] = choices + } + + questions = append(questions, question) + } + + return questions +} + +func expandAccessPackageLocalizedContent(input map[string]interface{}) *msgraph.AccessPackageLocalizedContent { + result := msgraph.AccessPackageLocalizedContent{ + DefaultText: utils.String(input["default_text"].(string)), + } + + texts := make([]msgraph.AccessPackageLocalizedTexts, 0) + + for _, v := range input["localized_text"].([]interface{}) { + v_map := v.(map[string]interface{}) + texts = append(texts, msgraph.AccessPackageLocalizedTexts{ + LanguageCode: utils.String(v_map["language_code"].(string)), + Text: utils.String(v_map["content"].(string)), + }) + } + + result.LocalizedTexts = &texts + + return &result +} + +func flattenAccessPackageLocalizedContent(input *msgraph.AccessPackageLocalizedContent) []map[string]interface{} { + result := []map[string]interface{}{{ + "default_text": input.DefaultText, + }} + + texts := make([]map[string]interface{}, 0) + + for _, v := range *input.LocalizedTexts { + text := map[string]interface{}{ + "language_code": v.LanguageCode, + "content": v.Text, + } + + texts = append(texts, text) + } + + result[0]["localized_text"] = texts + + return result +} diff --git a/internal/services/identitygovernance/parse/access_package_resource_catalog_association_id.go b/internal/services/identitygovernance/parse/access_package_resource_catalog_association_id.go new file mode 100644 index 000000000..f5027bf77 --- /dev/null +++ b/internal/services/identitygovernance/parse/access_package_resource_catalog_association_id.go @@ -0,0 +1,42 @@ +package parse + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-uuid" +) + +type AccessPackageResourceCatalogAssociationId struct { + CatalogId string + OriginId string +} + +func (id AccessPackageResourceCatalogAssociationId) ID() string { + return fmt.Sprintf("%s/%s", id.CatalogId, id.OriginId) +} + +func NewAccessPackageResourceCatalogAssociationID(catalogId, originId string) AccessPackageResourceCatalogAssociationId { + return AccessPackageResourceCatalogAssociationId{ + CatalogId: catalogId, + OriginId: originId, + } +} + +func AccessPackageResourceCatalogAssociationID(idString string) (*AccessPackageResourceCatalogAssociationId, error) { + parts := strings.Split(idString, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("ID should be in the format {catalogId}/{originId} - but got %q", idString) + } + + for i, p := range parts { + if _, err := uuid.ParseUUID(p); err != nil { + return nil, fmt.Errorf("specified ID segment #%d (%q) is not a valid UUID: %s", i, p, err) + } + } + + return &AccessPackageResourceCatalogAssociationId{ + CatalogId: parts[0], + OriginId: parts[1], + }, nil +} diff --git a/internal/services/identitygovernance/parse/access_package_resource_package_association_id.go b/internal/services/identitygovernance/parse/access_package_resource_package_association_id.go new file mode 100644 index 000000000..394fcb45f --- /dev/null +++ b/internal/services/identitygovernance/parse/access_package_resource_package_association_id.go @@ -0,0 +1,50 @@ +package parse + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-uuid" +) + +type AccessPackageResourcePackageAssociationId struct { + AccessPackageId string + ResourcePackageAssociationId string + OriginId string + AccessType string +} + +func (id AccessPackageResourcePackageAssociationId) ID() string { + return fmt.Sprintf("%s/%s/%s/%s", id.AccessPackageId, id.ResourcePackageAssociationId, id.OriginId, id.AccessType) +} + +func NewAccessPackageResourcePackageAssociationID(catalogId, resourcePackageAssociationId, originId, accessType string) AccessPackageResourcePackageAssociationId { + return AccessPackageResourcePackageAssociationId{ + AccessPackageId: catalogId, + ResourcePackageAssociationId: resourcePackageAssociationId, + OriginId: originId, + AccessType: accessType, + } +} + +func AccessPackageResourcePackageAssociationID(idString string) (*AccessPackageResourcePackageAssociationId, error) { + parts := strings.Split(idString, "/") + if len(parts) != 4 { + return nil, fmt.Errorf("ID should be in the format {accessPackageId}/{resourcePackageAssociationId}/{originId}/{accessType} - but got %q", idString) + } + + for i, p := range parts { + if i == 0 || i == 2 { + if _, err := uuid.ParseUUID(p); err != nil { + return nil, fmt.Errorf("specified ID segment #%d (%q) is not a valid UUID: %s", i, p, err) + } + } + } + + return &AccessPackageResourcePackageAssociationId{ + AccessPackageId: parts[0], + ResourcePackageAssociationId: parts[1], + OriginId: parts[2], + AccessType: parts[3], + }, nil +} diff --git a/internal/services/identitygovernance/registration.go b/internal/services/identitygovernance/registration.go new file mode 100644 index 000000000..e24a38a48 --- /dev/null +++ b/internal/services/identitygovernance/registration.go @@ -0,0 +1,38 @@ +package identitygovernance + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type Registration struct{} + +// Name is the name of this Service +func (r Registration) Name() string { + return "Identity Governance" +} + +// WebsiteCategories returns a list of categories which can be used for the sidebar +func (r Registration) WebsiteCategories() []string { + return []string{ + "Identity Governance", + } +} + +// 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(), + } +} + +// SupportedResources returns the supported Resources supported by this Service +func (r Registration) SupportedResources() map[string]*schema.Resource { + return map[string]*schema.Resource{ + "azuread_access_package": accessPackageResource(), + "azuread_access_package_assignment_policy": accessPackageAssignmentPolicyResource(), + "azuread_access_package_catalog": accessPackageCatalogResource(), + "azuread_access_package_resource_catalog_association": accessPackageResourceCatalogAssociationResource(), + "azuread_access_package_resource_package_association": accessPackageResourcePackageAssociationResource(), + } +} diff --git a/internal/services/identitygovernance/schema.go b/internal/services/identitygovernance/schema.go new file mode 100644 index 000000000..26c09bf2e --- /dev/null +++ b/internal/services/identitygovernance/schema.go @@ -0,0 +1,74 @@ +package identitygovernance + +import ( + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-azuread/internal/validate" +) + +func schemaLocalizedContent() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "default_text": { + Description: "The default text of this question", + Type: schema.TypeString, + Required: true, + }, + + "localized_text": { + Description: "The localized text of this question", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "language_code": { + Description: "The language code of this question content", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validate.ISO639Language, + }, + + "content": { + Description: "The localized content of this question", + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + } +} + +func schemaUserSet() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "subject_type": { + Description: "Type of users", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + odata.ShortTypeConnectedOrganizationMembers, + odata.ShortTypeExternalSponsors, + odata.ShortTypeGroupMembers, + odata.ShortTypeInternalSponsors, + odata.ShortTypeRequestorManager, + odata.ShortTypeSingleUser, + }, true), + }, + + "backup": { + Description: "For a user in an approval stage, this property indicates whether the user is a backup fallback approver", + Type: schema.TypeBool, + Optional: true, + }, + + "object_id": { + Description: "The object ID of the subject", + Type: schema.TypeString, + Optional: true, + }, + }, + } +} diff --git a/internal/services/identitygovernance/validate/access_package_resource_catalog_association_id.go b/internal/services/identitygovernance/validate/access_package_resource_catalog_association_id.go new file mode 100644 index 000000000..27082a449 --- /dev/null +++ b/internal/services/identitygovernance/validate/access_package_resource_catalog_association_id.go @@ -0,0 +1,10 @@ +package validate + +import ( + "github.com/hashicorp/terraform-provider-azuread/internal/services/identitygovernance/parse" +) + +func AccessPackageResourceCatalogAssociationID(input string) (err error) { + _, err = parse.AccessPackageResourceCatalogAssociationID(input) + return +} diff --git a/internal/services/identitygovernance/validate/access_package_resource_package_association_id.go b/internal/services/identitygovernance/validate/access_package_resource_package_association_id.go new file mode 100644 index 000000000..15dfe30f1 --- /dev/null +++ b/internal/services/identitygovernance/validate/access_package_resource_package_association_id.go @@ -0,0 +1,10 @@ +package validate + +import ( + "github.com/hashicorp/terraform-provider-azuread/internal/services/identitygovernance/parse" +) + +func AccessPackageResourcePackageAssociationID(input string) (err error) { + _, err = parse.AccessPackageResourcePackageAssociationID(input) + return +}