diff --git a/internal/services/authorization/role_management_policy_data_source.go b/internal/services/authorization/role_management_policy_data_source.go index 35f485cf69b9..c9a6adb48cc4 100644 --- a/internal/services/authorization/role_management_policy_data_source.go +++ b/internal/services/authorization/role_management_policy_data_source.go @@ -13,7 +13,9 @@ import ( "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" "github.com/hashicorp/go-azure-sdk/resource-manager/authorization/2020-10-01/rolemanagementpolicies" + "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + billingValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/billing/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" ) @@ -109,9 +111,16 @@ func (r RoleManagementPolicyDataSource) Arguments() map[string]*pluginsdk.Schema Type: pluginsdk.TypeString, Required: true, ValidateFunc: validation.Any( + // Elevated access for a global admin is needed to assign roles in this scope: + // https://docs.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin#azure-cli + // It seems only user account is allowed to be elevated access. + validation.StringMatch(regexp.MustCompile("/providers/Microsoft.Subscription.*"), "Subscription scope is invalid"), + + billingValidate.EnrollmentID, commonids.ValidateManagementGroupID, - commonids.ValidateResourceGroupID, commonids.ValidateSubscriptionID, + commonids.ValidateResourceGroupID, + azure.ValidateResourceID, ), }, } diff --git a/internal/services/authorization/role_management_policy_data_source_test.go b/internal/services/authorization/role_management_policy_data_source_test.go index 804c2b42e919..394780897d4b 100644 --- a/internal/services/authorization/role_management_policy_data_source_test.go +++ b/internal/services/authorization/role_management_policy_data_source_test.go @@ -55,6 +55,20 @@ func TestAccRoleManagementPolicyDataSource_subscription(t *testing.T) { }) } +func TestAccRoleManagementPolicyDataSource_resource(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_role_management_policy", "test") + r := RoleManagementPolicyDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.resource(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("name").Exists(), + ), + }, + }) +} + func (RoleManagementPolicyDataSource) managementGroup(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" {} @@ -116,3 +130,32 @@ data "azurerm_role_management_policy" "test" { } ` } + +func (RoleManagementPolicyDataSource) resource(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]s" + location = "%[2]s" +} + +resource "azurerm_storage_account" "test" { + name = "accteststg%[1]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +data "azurerm_role_definition" "contributor" { + name = "Contributor" + scope = azurerm_resource_group.test.id +} + +data "azurerm_role_management_policy" "test" { + role_definition_id = data.azurerm_role_definition.contributor.id + scope = azurerm_storage_account.test.id +} +`, data.RandomString, data.Locations.Primary) +} diff --git a/internal/services/authorization/role_management_policy_resource.go b/internal/services/authorization/role_management_policy_resource.go index cac16bfc0e89..9a91194ec842 100644 --- a/internal/services/authorization/role_management_policy_resource.go +++ b/internal/services/authorization/role_management_policy_resource.go @@ -6,14 +6,17 @@ package authorization import ( "context" "fmt" + "regexp" "time" "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" "github.com/hashicorp/go-azure-sdk/resource-manager/authorization/2020-10-01/rolemanagementpolicies" + "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/services/authorization/parse" + billingValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/billing/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" ) @@ -111,9 +114,16 @@ func (r RoleManagementPolicyResource) Arguments() map[string]*pluginsdk.Schema { Required: true, ForceNew: true, ValidateFunc: validation.Any( + // Elevated access for a global admin is needed to assign roles in this scope: + // https://docs.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin#azure-cli + // It seems only user account is allowed to be elevated access. + validation.StringMatch(regexp.MustCompile("/providers/Microsoft.Subscription.*"), "Subscription scope is invalid"), + + billingValidate.EnrollmentID, commonids.ValidateManagementGroupID, - commonids.ValidateResourceGroupID, commonids.ValidateSubscriptionID, + commonids.ValidateResourceGroupID, + azure.ValidateResourceID, ), }, diff --git a/internal/services/authorization/role_management_policy_resource_test.go b/internal/services/authorization/role_management_policy_resource_test.go index e75bff77b82e..e2b61de94acf 100644 --- a/internal/services/authorization/role_management_policy_resource_test.go +++ b/internal/services/authorization/role_management_policy_resource_test.go @@ -125,6 +125,36 @@ func TestAccRoleManagementPolicy_subscription(t *testing.T) { }) } +func TestAccRoleManagementPolicy_resource(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_role_management_policy", "test") + r := RoleManagementPolicyResource{} + + // Ignore the dangling resource post-test as the policy remains while the resource exists, or is in a pending deletion state + data.ResourceTestSkipCheckDestroyed(t, []acceptance.TestStep{ + { + Config: r.resource(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("active_assignment_rules.0.expire_after").HasValue("P30D"), + check.That(data.ResourceName).Key("eligible_assignment_rules.0.expiration_required").HasValue("false"), + check.That(data.ResourceName).Key("notification_rules.0.eligible_assignments.0.approver_notifications.0.notification_level").HasValue("All"), + ), + }, + data.ImportStep(), + { + Config: r.resourceUpdate(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("active_assignment_rules.0.expire_after").HasValue("P15D"), + check.That(data.ResourceName).Key("eligible_assignment_rules.0.expiration_required").HasValue("true"), + check.That(data.ResourceName).Key("activation_rules.0.approval_stage.0.primary_approver.0.type").HasValue("Group"), + check.That(data.ResourceName).Key("notification_rules.0.eligible_assignments.0.approver_notifications.0.notification_level").HasValue("Critical"), + ), + }, + data.ImportStep(), + }) +} + func (RoleManagementPolicyResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { client := clients.Authorization.RoleManagementPoliciesClient @@ -451,3 +481,111 @@ resource "azurerm_role_management_policy" "test" { } `, r.resourceGroupTemplate(data), data.RandomString, requireApproval) } + +func (RoleManagementPolicyResource) resourceTemplate(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]s" + location = "%[2]s" +} + +resource "azurerm_storage_account" "test" { + name = "accteststg%[1]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +data "azurerm_role_definition" "contributor" { + name = "Contributor" + scope = azurerm_storage_account.test.id +} +`, data.RandomString, data.Locations.Primary) +} + +func (r RoleManagementPolicyResource) resource(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azurerm_role_management_policy" "test" { + scope = azurerm_storage_account.test.id + role_definition_id = data.azurerm_role_definition.contributor.id + + active_assignment_rules { + expire_after = "P30D" + } + + eligible_assignment_rules { + expiration_required = false + } + + notification_rules { + eligible_assignments { + approver_notifications { + notification_level = "All" + default_recipients = false + additional_recipients = ["someone@example.com"] + } + } + } +} +`, r.resourceTemplate(data), data.RandomString) +} + +func (r RoleManagementPolicyResource) resourceUpdate(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +provider "azuread" {} + +resource "azuread_group" "approver" { + display_name = "PIM Approver Test %[2]s" + mail_enabled = false + security_enabled = true +} + +resource "azurerm_role_management_policy" "test" { + scope = azurerm_storage_account.test.id + role_definition_id = data.azurerm_role_definition.contributor.id + + active_assignment_rules { + expire_after = "P15D" + } + + eligible_assignment_rules { + expiration_required = true + } + + activation_rules { + maximum_duration = "PT1H" + require_approval = true + approval_stage { + primary_approver { + object_id = azuread_group.approver.object_id + type = "Group" + } + } + } + + notification_rules { + eligible_assignments { + approver_notifications { + notification_level = "Critical" + default_recipients = false + additional_recipients = ["someone@example.com"] + } + } + eligible_activations { + assignee_notifications { + notification_level = "All" + default_recipients = true + additional_recipients = ["someone.else@example.com"] + } + } + } +} +`, r.resourceTemplate(data), data.RandomString) +} diff --git a/website/docs/d/role_management_policy.html.markdown b/website/docs/d/role_management_policy.html.markdown index 0b91502764d6..15584f6c0f59 100644 --- a/website/docs/d/role_management_policy.html.markdown +++ b/website/docs/d/role_management_policy.html.markdown @@ -51,7 +51,7 @@ data "azurerm_role_management_policy" "example" { ## Argument Reference * `role_definition_id` - (Required) The scoped Role Definition ID of the role for which this policy applies. -* `scope` - (Required) The scope to which this Role Management Policy applies. Can refer to a management group, a subscription or a resource group. +* `scope` - (Required) The scope to which this Role Management Policy applies. Can refer to a management group, a subscription, a resource group or a resource. ## Attributes Reference diff --git a/website/docs/r/role_management_policy.html.markdown b/website/docs/r/role_management_policy.html.markdown index e79d070d9a54..a1439187bf72 100644 --- a/website/docs/r/role_management_policy.html.markdown +++ b/website/docs/r/role_management_policy.html.markdown @@ -119,7 +119,7 @@ resource "azurerm_role_management_policy" "example" { * `eligible_assignment_rules` - (Optional) An `eligible_assignment_rules` block as defined below. * `notification_rules` - (Optional) A `notification_rules` block as defined below. * `role_definition_id` - (Required) The scoped Role Definition ID of the role for which this policy will apply. Changing this forces a new resource to be created. -* `scope` - (Required) The scope to which this Role Management Policy will apply. Can refer to a management group, a subscription or a resource group. Changing this forces a new resource to be created. +* `scope` - (Required) The scope to which this Role Management Policy will apply. Can refer to a management group, a subscription, a resource group or a resource. Changing this forces a new resource to be created. ---