Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azurerm_role_assignment - Support scope to be /providers/Subscription #17456

Merged
merged 4 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 42 additions & 19 deletions internal/services/authorization/parse/role_assignment.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,30 @@ import (
)

type RoleAssignmentId struct {
SubscriptionID string
ResourceGroup string
ManagementGroup string
ResourceScope string
ResourceProvider string
Name string
TenantId string
SubscriptionID string
ResourceGroup string
ManagementGroup string
ResourceScope string
ResourceProvider string
Name string
TenantId string
IsSubscriptionLevel bool
}

func NewRoleAssignmentID(subscriptionId, resourceGroup, resourceProvider, resourceScope, managementGroup, name, tenantId string) (*RoleAssignmentId, error) {
if subscriptionId == "" && resourceGroup == "" && managementGroup == "" {
return nil, fmt.Errorf("one of subscriptionId, resourceGroup, or managementGroup must be provided")
func NewRoleAssignmentID(subscriptionId, resourceGroup, resourceProvider, resourceScope, managementGroup, name, tenantId string, isSubLevel bool) (*RoleAssignmentId, error) {
if subscriptionId == "" && resourceGroup == "" && managementGroup == "" && !isSubLevel {
return nil, fmt.Errorf("one of subscriptionId, resourceGroup, managementGroup or isSubscriptionLevel must be provided")
}

if managementGroup != "" {
if subscriptionId != "" || resourceGroup != "" {
return nil, fmt.Errorf("cannot provide subscriptionId or resourceGroup when managementGroup is provided")
if subscriptionId != "" || resourceGroup != "" || isSubLevel {
return nil, fmt.Errorf("cannot provide subscriptionId, resourceGroup or isSubscriptionLevel when managementGroup is provided")
}
}

if isSubLevel {
if subscriptionId != "" || resourceGroup != "" || managementGroup != "" {
return nil, fmt.Errorf("cannot provide subscriptionId, resourceGroup or managementGroup when isSubscriptionLevel is provided")
}
}

Expand All @@ -35,13 +42,14 @@ func NewRoleAssignmentID(subscriptionId, resourceGroup, resourceProvider, resour
}

return &RoleAssignmentId{
SubscriptionID: subscriptionId,
ResourceGroup: resourceGroup,
ResourceProvider: resourceProvider,
ResourceScope: resourceScope,
ManagementGroup: managementGroup,
Name: name,
TenantId: tenantId,
SubscriptionID: subscriptionId,
ResourceGroup: resourceGroup,
ResourceProvider: resourceProvider,
ResourceScope: resourceScope,
ManagementGroup: managementGroup,
Name: name,
TenantId: tenantId,
IsSubscriptionLevel: isSubLevel,
}, nil
}

Expand All @@ -63,6 +71,11 @@ func (id RoleAssignmentId) AzureResourceID() string {
return fmt.Sprintf(fmtString, id.SubscriptionID, id.ResourceGroup, id.Name)
}

if id.IsSubscriptionLevel {
fmtString := "/providers/Microsoft.Subscription/providers/Microsoft.Authorization/roleAssignments/%s"
return fmt.Sprintf(fmtString, id.Name)
}

fmtString := "/subscriptions/%s/providers/Microsoft.Authorization/roleAssignments/%s"
return fmt.Sprintf(fmtString, id.SubscriptionID, id.Name)
}
Expand Down Expand Up @@ -111,6 +124,16 @@ func RoleAssignmentID(input string) (*RoleAssignmentId, error) {
if roleAssignmentId.Name, err = id.PopSegment("roleAssignments"); err != nil {
return nil, err
}
case strings.HasPrefix(input, "/providers/Microsoft.Subscription/"):
idParts := strings.Split(input, "/providers/Microsoft.Authorization/roleAssignments/")
if len(idParts) != 2 {
return nil, fmt.Errorf("could not parse Role Assignment ID %q for subscription scope", input)
}
roleAssignmentId.IsSubscriptionLevel = true
if idParts[1] == "" {
return nil, fmt.Errorf("ID was missing a value for the roleAssignments element")
}
roleAssignmentId.Name = idParts[1]
case strings.HasPrefix(input, "/providers/Microsoft.Management/"):
idParts := strings.Split(input, "/providers/Microsoft.Authorization/roleAssignments/")
if len(idParts) != 2 {
Expand Down
35 changes: 26 additions & 9 deletions internal/services/authorization/parse/role_assignment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ var _ resourceid.Formatter = RoleAssignmentId{}

func TestRoleAssignmentIDFormatter(t *testing.T) {
testData := []struct {
SubscriptionId string
ResourceGroup string
ResourceProvider string
ResourceScope string
ManagementGroup string
Name string
TenantId string
Expected string
SubscriptionId string
ResourceGroup string
ResourceProvider string
ResourceScope string
ManagementGroup string
IsSubscriptionLevel bool
Name string
TenantId string
Expected string
}{
{
SubscriptionId: "",
Expand Down Expand Up @@ -89,14 +90,21 @@ func TestRoleAssignmentIDFormatter(t *testing.T) {
TenantId: "34567812-3456-7653-6742-345678901234",
Expected: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Storage/storageAccounts/nameStorageAccount/providers/Microsoft.Authorization/roleAssignments/23456781-2349-8764-5631-234567890121|34567812-3456-7653-6742-345678901234",
},
{
IsSubscriptionLevel: true,
Name: "23456781-2349-8764-5631-234567890121",
TenantId: "34567812-3456-7653-6742-345678901234",
Expected: "/providers/Microsoft.Subscription/providers/Microsoft.Authorization/roleAssignments/23456781-2349-8764-5631-234567890121|34567812-3456-7653-6742-345678901234",
},
}
for _, v := range testData {
t.Logf("testing %+v", v)
actual, err := NewRoleAssignmentID(v.SubscriptionId, v.ResourceGroup, v.ResourceProvider, v.ResourceScope, v.ManagementGroup, v.Name, v.TenantId)
actual, err := NewRoleAssignmentID(v.SubscriptionId, v.ResourceGroup, v.ResourceProvider, v.ResourceScope, v.ManagementGroup, v.Name, v.TenantId, v.IsSubscriptionLevel)
if err != nil {
if v.Expected == "" {
continue
}
t.Fatal(err)
}
actualId := actual.ID()
if actualId != v.Expected {
Expand Down Expand Up @@ -153,6 +161,15 @@ func TestRoleAssignmentID(t *testing.T) {
Error: true,
},

{
// valid at subscriptions scope
Input: "/providers/Microsoft.Subscription/providers/Microsoft.Authorization/roleAssignments/23456781-2349-8764-5631-234567890121",
Expected: &RoleAssignmentId{
IsSubscriptionLevel: true,
Name: "23456781-2349-8764-5631-234567890121",
},
},

{
// valid at subscription scope
Input: "/subscriptions/12345678-1234-9876-4563-123456789012/providers/Microsoft.Authorization/roleAssignments/23456781-2349-8764-5631-234567890121",
Expand Down
7 changes: 7 additions & 0 deletions internal/services/authorization/role_assignment_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ func resourceArmRoleAssignment() *pluginsdk.Resource {
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.StringInSlice([]string{
"/providers/Microsoft.Subscription",
}, false),

billingValidate.EnrollmentID,
commonids.ValidateManagementGroupID,
commonids.ValidateSubscriptionID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,24 @@ func TestAccRoleAssignment_resourceScoped(t *testing.T) {
})
}

func TestAccRoleAssignment_subscriptionScoped(t *testing.T) {
// Only user account is able to run the test, the user account needs to be elevated.
// See: https://docs.microsoft.com/en-us/answers/questions/604740/user-does-not-have-access-microsoftsubscriptionali.html
t.Skip("Skipping this test as only elevated user account is able to run the test (i.e. via CLI auth)")

data := acceptance.BuildTestData(t, "azurerm_role_assignment", "test")
r := RoleAssignmentResource{}
data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.subscriptionScoped(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep("skip_service_principal_aad_check"),
})
}

func (r RoleAssignmentResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) {
id, err := parse.RoleAssignmentID(state.ID)
if err != nil {
Expand Down Expand Up @@ -546,3 +564,27 @@ resource "azurerm_role_assignment" "test" {
}
`, groupId)
}

func (RoleAssignmentResource) subscriptionScoped(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}

data "azurerm_client_config" "test" {}

resource "azuread_application" "test" {
display_name = "acctestspa%d"
}

resource "azuread_service_principal" "test" {
application_id = azuread_application.test.application_id
}

resource "azurerm_role_assignment" "test" {
scope = "/providers/Microsoft.Subscription"
role_definition_name = "Reader"
principal_id = azuread_service_principal.test.object_id
}
`, data.RandomInteger)
}