diff --git a/cli/azd/extensions/azure.ai.agents/cspell.yaml b/cli/azd/extensions/azure.ai.agents/cspell.yaml index 4ca5869261d..26736b20074 100644 --- a/cli/azd/extensions/azure.ai.agents/cspell.yaml +++ b/cli/azd/extensions/azure.ai.agents/cspell.yaml @@ -22,6 +22,7 @@ words: - uaenorth - uksouth # Project terms + - ABAC - ADLS - agentserver - aiservices diff --git a/cli/azd/extensions/azure.ai.agents/go.mod b/cli/azd/extensions/azure.ai.agents/go.mod index 59a76c0ccec..71f94343c91 100644 --- a/cli/azd/extensions/azure.ai.agents/go.mod +++ b/cli/azd/extensions/azure.ai.agents/go.mod @@ -7,7 +7,9 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0-beta.3 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2 v2.0.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0 + // armcontainerregistry v1.3.0-beta.3 is required for RoleAssignmentMode (ABAC detection). + // Upgrade to GA once the property is available in a stable release. + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.3.0-beta.3 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 github.com/azure/azure-dev/cli/azd v1.23.14 diff --git a/cli/azd/extensions/azure.ai.agents/go.sum b/cli/azd/extensions/azure.ai.agents/go.sum index 6592c1bb487..098ea504f0a 100644 --- a/cli/azd/extensions/azure.ai.agents/go.sum +++ b/cli/azd/extensions/azure.ai.agents/go.sum @@ -21,6 +21,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcogni github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2 v2.0.0/go.mod h1:twcwRey+l1znKBL5TEzYiZMtiVkWfM7Pq8a9vY04xYc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0 h1:DWlwvVV5r/Wy1561nZ3wrpI1/vDIBRY/Wd1HWaRBZWA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0/go.mod h1:E7ltexgRDmeJ0fJWv0D/HLwY2xbDdN+uv+X2uZtOx3w= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.3.0-beta.3 h1:4qfc7os3wRQcl+ImfeH9z0abWJzuV9IGcN1B9olmPTU= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.3.0-beta.3/go.mod h1:NlNAngH4e++mzPTN0+1EEvyUmwFmR91u/MQUVV230Z4= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= diff --git a/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go b/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go index 2ce773a2a4d..0fe0d309bdd 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go +++ b/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go @@ -119,9 +119,10 @@ const ( // Error codes for developer RBAC pre-flight checks. const ( - CodeDeveloperMissingAIUserRole = "developer_missing_ai_user_role" - CodeDeveloperMissingACRRole = "developer_missing_acr_role" - CodeACRResolutionFailed = "acr_resolution_failed" + CodeDeveloperMissingAIUserRole = "developer_missing_ai_user_role" + CodeDeveloperMissingRoleAssignWriteRole = "developer_missing_role_assign_write_role" + CodeDeveloperMissingACRRole = "developer_missing_acr_role" + CodeACRResolutionFailed = "acr_resolution_failed" ) // Error codes commonly used for internal errors. diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/agent_identity_rbac.go b/cli/azd/extensions/azure.ai.agents/internal/project/agent_identity_rbac.go index 3660776b1aa..4cba7371997 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/agent_identity_rbac.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/agent_identity_rbac.go @@ -332,6 +332,7 @@ func ensureSingleAgentRBAC( created, err := assignRoleToIdentity( ctx, cred, principalID, roleAzureAIUser, "Azure AI User → Foundry Project", info.ProjectScope, + armauthorization.PrincipalTypeServicePrincipal, ) if err != nil { return fmt.Errorf("failed to assign Azure AI User role: %w", err) @@ -368,7 +369,7 @@ func discoverAgentIdentity( return resp.Value, nil } -// assignRoleToIdentity assigns a single RBAC role to a service principal at the given scope. +// assignRoleToIdentity assigns a single RBAC role to a principal at the given scope. // It is idempotent: existing assignments are detected and skipped. // Returns true if a new assignment was created, false if it already existed. func assignRoleToIdentity( @@ -378,6 +379,7 @@ func assignRoleToIdentity( roleID string, roleName string, scope string, + principalType armauthorization.PrincipalType, ) (bool, error) { subscriptionID := extractSubscriptionID(scope) if subscriptionID == "" { @@ -421,11 +423,10 @@ func assignRoleToIdentity( return false, nil } - // Create assignment with explicit ServicePrincipal type + // Create assignment fullRoleDefinitionID := fmt.Sprintf( "%s/providers/Microsoft.Authorization/roleDefinitions/%s", scope, roleID) roleAssignmentName := uuid.New().String() - principalType := armauthorization.PrincipalTypeServicePrincipal parameters := armauthorization.RoleAssignmentCreateParameters{ Properties: &armauthorization.RoleAssignmentProperties{ RoleDefinitionID: new(fullRoleDefinitionID), diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/developer_rbac_check.go b/cli/azd/extensions/azure.ai.agents/internal/project/developer_rbac_check.go index c156a68a8e8..926e4c8a8a6 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/developer_rbac_check.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/developer_rbac_check.go @@ -5,11 +5,14 @@ package project import ( "context" + "errors" "fmt" + "net/http" "strings" "azureaiagent/internal/exterrors" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry" @@ -22,17 +25,27 @@ const ( roleOwner = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" roleContributor = "b24988ac-6180-42a0-ab88-20f7382dd24c" - // ACR-specific roles that grant push/build access. + // Roles that grant Microsoft.Authorization/roleAssignments/write. + // Note: roleContributor is intentionally excluded — its notActions explicitly + // block Microsoft.Authorization/*/Write. + roleUserAccessAdministrator = "18d7d88d-d35e-4fb5-a5c3-7773c20a72d9" + roleRBACAdministrator = "f58310d9-a9f6-439a-9e8d-f62e7b41a168" + + // Classic ACR roles that grant push/build access. roleAcrPush = "8311e382-0749-4cb8-b61a-304f252e45ec" roleContainerRegistryTasksContributor = "fb382eab-e894-4461-af04-94435c366c3f" roleContainerRegistryRepositoryContributor = "2efddaa5-3f1f-4df3-97df-af3f13818f4c" + // ABAC repository-scoped role that grants write access. + // Required for ABAC-mode registries (roleAssignmentMode == AbacRepositoryPermissions). + roleAcrRepositoryWriter = "2a1e307c-b015-4ebd-883e-5b7698a07328" + // AI-specific roles that grant agent management access. roleAzureAIDeveloper = "64702f94-c441-49e6-a78b-ef80e0188fee" ) // sufficientACRRoles lists every role that grants enough ACR access to build -// and push container images. Order: broadest first for early exit. +// and push container images on classic-mode registries. Order: broadest first for early exit. var sufficientACRRoles = []string{ roleOwner, roleContributor, @@ -41,6 +54,15 @@ var sufficientACRRoles = []string{ roleContainerRegistryRepositoryContributor, } +// sufficientACRAbacRoles lists roles sufficient for ABAC-mode registries +// (roleAssignmentMode == AbacRepositoryPermissions). Classic roles such as AcrPush and +// Contributor do not grant repository-scoped dataActions on ABAC registries. +var sufficientACRAbacRoles = []string{ + roleOwner, + roleAcrRepositoryWriter, + roleContainerRegistryRepositoryContributor, // superset of RepositoryWriter +} + // sufficientAIUserRoles lists every role that grants enough Foundry Project // access to create and run agents. var sufficientAIUserRoles = []string{ @@ -50,6 +72,17 @@ var sufficientAIUserRoles = []string{ roleAzureAIDeveloper, } +// sufficientRoleAssignWriteRoles lists every role that grants +// Microsoft.Authorization/roleAssignments/write on Azure Resource Manager. +// Required for the postdeploy hook to assign Azure AI User to agent service principals. +// Note: roleContributor is intentionally excluded — Contributor's notActions explicitly +// block Microsoft.Authorization/*/Write. +var sufficientRoleAssignWriteRoles = []string{ + roleOwner, + roleUserAccessAdministrator, + roleRBACAdministrator, +} + // CheckDeveloperRBAC verifies that the currently authenticated developer has the required // RBAC roles for deploying hosted agents: // - Azure AI User on the Foundry Project (to create and run agents) @@ -134,41 +167,101 @@ func CheckDeveloperRBAC(ctx context.Context, azdClient *azdext.AzdClient) error if err != nil { fmt.Printf(" ⚠ Could not check AI User role: %s\n", err) } else if !hasAIAccess { + // Attempt to auto-assign Azure AI User to the developer. This succeeds when the + // developer has Owner, User Access Administrator, or RBAC Administrator. + fmt.Println(" Azure AI User role not found — attempting to auto-assign...") + if _, assignErr := assignRoleToIdentity( + ctx, cred, principalID, roleAzureAIUser, + "Azure AI User → Foundry Project", info.ProjectScope, + armauthorization.PrincipalTypeUser, + ); assignErr != nil { + // Only treat 403 as a hard RBAC failure — transient errors (throttling, network) are non-blocking. + if respErr, ok := errors.AsType[*azcore.ResponseError](assignErr); ok && + respErr.StatusCode == http.StatusForbidden { + return exterrors.Auth( + exterrors.CodeDeveloperMissingAIUserRole, + fmt.Sprintf( + "your identity (%s) does not have the 'Azure AI User' role on the Foundry Project %s/%s "+ + "and auto-assign was denied: %s", + userProfile.DisplayName, info.AccountName, info.ProjectName, assignErr, + ), + fmt.Sprintf( + "ask a subscription Owner or User Access Administrator to assign the 'Azure AI User' role "+ + "to your identity on the Foundry Project scope:\n"+ + " az role assignment create --assignee %s --role \"Azure AI User\" --scope %q", + principalID, info.ProjectScope, + ), + ) + } + fmt.Printf(" ⚠ Azure AI User auto-assign failed (non-auth error): %s — continuing\n", assignErr) + } else { + fmt.Println(" ✓ Azure AI User auto-assigned to developer identity") + } + } else { + fmt.Println(" ✓ Azure AI User on Foundry Project") + } + + // Check 2: roleAssignments/write capability on Foundry Project scope. + // Required for the postdeploy hook to assign Azure AI User to agent service principals. + // Note: Contributor cannot write role assignments (it is excluded from sufficientRoleAssignWriteRoles). + hasRoleWrite, err := hasAnyRoleAssignment(ctx, cred, principalID, sufficientRoleAssignWriteRoles, info.ProjectScope) + if err != nil { + fmt.Printf(" ⚠ Could not check role-assignment-write capability: %s\n", err) + } else if !hasRoleWrite { return exterrors.Auth( - exterrors.CodeDeveloperMissingAIUserRole, + exterrors.CodeDeveloperMissingRoleAssignWriteRole, fmt.Sprintf( - "your identity (%s) does not have the 'Azure AI User' role on the Foundry Project %s/%s", + "your identity (%s) does not have the permission to write role assignments on the "+ + "Foundry Project %s/%s — this is required for the postdeploy step to assign "+ + "'Azure AI User' to agent service principals", userProfile.DisplayName, info.AccountName, info.ProjectName, ), fmt.Sprintf( - "ask a subscription Owner or User Access Administrator to assign the 'Azure AI User' role "+ + "ask a subscription Owner or User Access Administrator to assign one of these roles "+ "to your identity on the Foundry Project scope:\n"+ - " az role assignment create --assignee %s --role \"Azure AI User\" --scope %q", + " • Owner\n"+ + " • User Access Administrator\n"+ + " • Role Based Access Control Administrator\n\n"+ + " az role assignment create --assignee %s "+ + "--role \"Role Based Access Control Administrator\" --scope %q\n\n"+ + "Alternatively, if role assignments are managed externally:\n"+ + " AZD_AGENT_SKIP_ROLE_ASSIGNMENTS=true", principalID, info.ProjectScope, ), ) } else { - fmt.Println(" ✓ Azure AI User on Foundry Project") + fmt.Println(" ✓ Role assignment write on Foundry Project") } - // Check 2: ACR role — any role that grants push/build access. + // Check 3: ACR role — must branch on registry mode (ABAC vs classic). acrEndpoint := azdEnv["AZURE_CONTAINER_REGISTRY_ENDPOINT"] if acrEndpoint == "" { fmt.Println(" ⚠ AZURE_CONTAINER_REGISTRY_ENDPOINT not set — skipping ACR role check") return nil } - // Prefer the persisted ARM resource ID (set during init); fall back to listing registries. - acrResourceID := azdEnv["AZURE_CONTAINER_REGISTRY_RESOURCE_ID"] - if acrResourceID == "" { - acrResourceID, err = resolveACRResourceID(ctx, cred, info.SubscriptionID, acrEndpoint) - if err != nil { - fmt.Printf(" ⚠ Could not resolve ACR resource ID: %s — skipping ACR role check\n", err) - return nil - } + // When AZURE_CONTAINER_REGISTRY_RESOURCE_ID is set (populated during init), use a targeted + // RegistriesClient.Get() call — O(1) instead of paging all registries in the subscription. + var ( + acrResourceID string + isAbac bool + ) + if rid := azdEnv["AZURE_CONTAINER_REGISTRY_RESOURCE_ID"]; rid != "" { + acrResourceID, isAbac, err = resolveACRInfoByResourceID(ctx, cred, rid) + } else { + acrResourceID, isAbac, err = resolveACRInfo(ctx, cred, info.SubscriptionID, acrEndpoint) + } + if err != nil { + fmt.Printf(" ⚠ Could not resolve ACR resource info: %s — skipping ACR role check\n", err) + return nil + } + + acrRolesToCheck := sufficientACRRoles + if isAbac { + acrRolesToCheck = sufficientACRAbacRoles } - hasACRAccess, err := hasAnyRoleAssignment(ctx, cred, principalID, sufficientACRRoles, acrResourceID) + hasACRAccess, err := hasAnyRoleAssignment(ctx, cred, principalID, acrRolesToCheck, acrResourceID) if err != nil { fmt.Printf(" ⚠ Could not check ACR role: %s\n", err) return nil @@ -176,6 +269,26 @@ func CheckDeveloperRBAC(ctx context.Context, azdClient *azdext.AzdClient) error if !hasACRAccess { acrName := strings.TrimSuffix(normalizeLoginServer(acrEndpoint), ".azurecr.io") + if isAbac { + return exterrors.Auth( + exterrors.CodeDeveloperMissingACRRole, + fmt.Sprintf( + "your identity (%s) does not have the required role on the ABAC-mode Container Registry '%s' "+ + "to push container images", + userProfile.DisplayName, acrName, + ), + fmt.Sprintf( + "ask a subscription Owner or User Access Administrator to assign one of these roles "+ + "to your identity on the Container Registry scope:\n"+ + " • Owner (broad access)\n"+ + " • Container Registry Repository Writer (ABAC push)\n"+ + " • Container Registry Repository Contributor (superset of Writer)\n\n"+ + " az role assignment create --assignee %s "+ + "--role \"Container Registry Repository Writer\" --scope %q", + principalID, acrResourceID, + ), + ) + } return exterrors.Auth( exterrors.CodeDeveloperMissingACRRole, fmt.Sprintf( @@ -196,7 +309,11 @@ func CheckDeveloperRBAC(ctx context.Context, azdClient *azdext.AzdClient) error ) } - fmt.Println(" ✓ Container Registry role on ACR") + if isAbac { + fmt.Println(" ✓ Container Registry role on ACR (ABAC mode)") + } else { + fmt.Println(" ✓ Container Registry role on ACR") + } fmt.Println() return nil } @@ -266,37 +383,93 @@ func normalizeLoginServer(loginServer string) string { return strings.ToLower(strings.TrimSuffix(s, "/")) } -// resolveACRResourceID finds the ARM resource ID for an Azure Container Registry +// resolveACRInfoByResourceID fetches ABAC mode and validates the resource ID for a registry +// using a single targeted ARM Get() call — O(1) vs paging all registries. +// resourceID must be a full ARM resource ID: +// /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ContainerRegistry/registries/{name} +func resolveACRInfoByResourceID( + ctx context.Context, + cred *azidentity.AzureDeveloperCLICredential, + resourceID string, +) (string, bool, error) { + // Parse resource group and registry name from the ARM resource ID. + parts := strings.Split(resourceID, "/") + var resourceGroup, registryName, subscriptionID string + for i, p := range parts { + switch strings.ToLower(p) { + case "subscriptions": + if i+1 < len(parts) { + subscriptionID = parts[i+1] + } + case "resourcegroups": + if i+1 < len(parts) { + resourceGroup = parts[i+1] + } + case "registries": + if i+1 < len(parts) { + registryName = parts[i+1] + } + } + } + if subscriptionID == "" || resourceGroup == "" || registryName == "" { + return "", false, fmt.Errorf("could not parse subscription, resource group, or registry name from resource ID: %s", resourceID) + } + + client, err := armcontainerregistry.NewRegistriesClient(subscriptionID, cred, nil) + if err != nil { + return "", false, fmt.Errorf("failed to create ACR client: %w", err) + } + + resp, err := client.Get(ctx, resourceGroup, registryName, nil) + if err != nil { + return "", false, fmt.Errorf("failed to get container registry: %w", err) + } + + if resp.ID == nil { + return "", false, fmt.Errorf("registry response missing ID for resource: %s", resourceID) + } + + abac := resp.Properties != nil && + resp.Properties.RoleAssignmentMode != nil && + *resp.Properties.RoleAssignmentMode == armcontainerregistry.RoleAssignmentModeAbacRepositoryPermissions + + return *resp.ID, abac, nil +} + +// resolveACRInfo finds the ARM resource ID and ABAC mode for an Azure Container Registry // given its login server endpoint (e.g., "myregistry.azurecr.io"). -func resolveACRResourceID( +// isAbac is true when the registry uses roleAssignmentMode == AbacRepositoryPermissions. +func resolveACRInfo( ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, subscriptionID string, loginServer string, -) (string, error) { +) (resourceID string, isAbac bool, err error) { loginServer = normalizeLoginServer(loginServer) client, err := armcontainerregistry.NewRegistriesClient(subscriptionID, cred, nil) if err != nil { - return "", fmt.Errorf("failed to create ACR client: %w", err) + return "", false, fmt.Errorf("failed to create ACR client: %w", err) } pager := client.NewListPager(nil) for pager.More() { page, err := pager.NextPage(ctx) if err != nil { - return "", fmt.Errorf("failed to list container registries: %w", err) + return "", false, fmt.Errorf("failed to list container registries: %w", err) } for _, registry := range page.Value { if registry.Properties != nil && registry.Properties.LoginServer != nil && strings.EqualFold(*registry.Properties.LoginServer, loginServer) { if registry.ID != nil { - return *registry.ID, nil + abac := registry.Properties.RoleAssignmentMode != nil && + *registry.Properties.RoleAssignmentMode == armcontainerregistry.RoleAssignmentModeAbacRepositoryPermissions + return *registry.ID, abac, nil } } } } - return "", fmt.Errorf("container registry with login server '%s' not found in subscription %s", loginServer, subscriptionID) + return "", false, fmt.Errorf("container registry with login server '%s' not found in subscription %s", loginServer, subscriptionID) } diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/developer_rbac_check_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/developer_rbac_check_test.go index 233f314e279..9e0ca560e5c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/developer_rbac_check_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/developer_rbac_check_test.go @@ -37,11 +37,16 @@ func TestDeveloperRBACRoleConstants(t *testing.T) { assert.Equal(t, "fb382eab-e894-4461-af04-94435c366c3f", roleContainerRegistryTasksContributor) assert.Equal(t, "2efddaa5-3f1f-4df3-97df-af3f13818f4c", roleContainerRegistryRepositoryContributor) assert.Equal(t, "8311e382-0749-4cb8-b61a-304f252e45ec", roleAcrPush) + assert.Equal(t, "2a1e307c-b015-4ebd-883e-5b7698a07328", roleAcrRepositoryWriter) // Superset roles assert.Equal(t, "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", roleOwner) assert.Equal(t, "b24988ac-6180-42a0-ab88-20f7382dd24c", roleContributor) + // Role-assignment write roles + assert.Equal(t, "18d7d88d-d35e-4fb5-a5c3-7773c20a72d9", roleUserAccessAdministrator) + assert.Equal(t, "f58310d9-a9f6-439a-9e8d-f62e7b41a168", roleRBACAdministrator) + // AI roles assert.Equal(t, "64702f94-c441-49e6-a78b-ef80e0188fee", roleAzureAIDeveloper) } @@ -58,4 +63,16 @@ func TestSufficientRoleLists(t *testing.T) { assert.Contains(t, sufficientAIUserRoles, roleContributor) assert.Contains(t, sufficientAIUserRoles, roleAzureAIUser) assert.Contains(t, sufficientAIUserRoles, roleAzureAIDeveloper) + + // Role-assignment write: Owner, UAA, RBAC Admin; Contributor must NOT be included. + assert.Contains(t, sufficientRoleAssignWriteRoles, roleOwner) + assert.Contains(t, sufficientRoleAssignWriteRoles, roleUserAccessAdministrator) + assert.Contains(t, sufficientRoleAssignWriteRoles, roleRBACAdministrator) + assert.NotContains(t, sufficientRoleAssignWriteRoles, roleContributor) + + // ABAC ACR roles: Owner, RepositoryWriter, RepositoryContributor; AcrPush must NOT be included. + assert.Contains(t, sufficientACRAbacRoles, roleOwner) + assert.Contains(t, sufficientACRAbacRoles, roleAcrRepositoryWriter) + assert.Contains(t, sufficientACRAbacRoles, roleContainerRegistryRepositoryContributor) + assert.NotContains(t, sufficientACRAbacRoles, roleAcrPush) }