Skip to content

[Role] Feature: Add az role deny-assignment create/delete commands#33109

Merged
isra-fel merged 18 commits into
Azure:devfrom
jruttle:jruttle/add-deny-assignment-commands
Jun 26, 2026
Merged

[Role] Feature: Add az role deny-assignment create/delete commands#33109
isra-fel merged 18 commits into
Azure:devfrom
jruttle:jruttle/add-deny-assignment-commands

Conversation

@jruttle

@jruttle jruttle commented Mar 31, 2026

Copy link
Copy Markdown
Member

Description

Adds az role deny-assignment create and az role deny-assignment delete commands for managing user-assigned deny assignments, matching the existing az role assignment pattern.

This implements the PUT/DELETE operations added in the Microsoft.Authorization/denyAssignments API (2024-07-01-preview), as specified in TypeSpec PR #41617.

Two Assignment Modes

The create command supports two modes for targeting principals:

Everyone mode (default): Denies all principals at the scope. At least one excluded principal is required via --exclude-principal-ids:

az role deny-assignment create --name "DenyAll" --scope /subscriptions/{sub} \
    --actions "Microsoft.Compute/virtualMachines/delete" \
    --exclude-principal-ids {your-object-id}

Per-principal mode: Denies a specific User or ServicePrincipal via --principal-id and --principal-type:

az role deny-assignment create --name "DenyUser" --scope /subscriptions/{sub} \
    --actions "Microsoft.Compute/virtualMachines/delete" \
    --principal-id {user-object-id} --principal-type User

Service Constraints

User-assigned deny assignments have specific restrictions enforced by the service:

  • Group principals are not permitted — only User or ServicePrincipal
  • No DataActions — only Actions/NotActions are supported
  • No Read actions — actions like */read are not permitted
  • No DoNotApplyToChildScopes — this property is not supported
  • Single principal per UADA — one principal per deny assignment (enforced by backend)

Commands

  • az role deny-assignment list — List deny assignments (existing, enhanced)
  • az role deny-assignment show — Show a deny assignment (existing, enhanced)
  • az role deny-assignment create — Create a user-assigned deny assignment
  • az role deny-assignment delete — Delete a user-assigned deny assignment

Files Changed

  • commands.py — Command registration for role deny-assignment group
  • custom.py — Business logic with dual-mode principal handling and validation
  • _params.py — Parameter definitions including --principal-id and --principal-type
  • _help.py — Help text with examples for both Everyone and per-principal modes
  • linter_exclusions.yml — Exclusions for long parameter names
  • tests/latest/test_deny_assignment.py — Unit tests (list, show, CRUD, per-principal, Group rejection, param validation)

Dependency

Note: The create and delete operations depend on azure-mgmt-authorization 5.0.0b2 (released 7 May 2026 on PyPI), which includes the create_or_update and delete methods on DenyAssignmentsOperations.

Testing

Tests are included in test_deny_assignment.py. Full end-to-end testing requires:

  1. A subscription with the Microsoft.Authorization/SubscriptionAllowedToOperateUserAssignedDenyAssignment feature flag registered
  2. The updated Python SDK with create/delete support

Related

Copilot AI review requested due to automatic review settings March 31, 2026 13:10
@azure-client-tools-bot-prd

azure-client-tools-bot-prd Bot commented Mar 31, 2026

Copy link
Copy Markdown
️✔️AzureCLI-FullTest
️✔️acr
️✔️latest
️✔️3.12
️✔️3.14
️✔️acs
️✔️latest
️✔️3.12
️✔️3.14
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.14
️✔️ams
️✔️latest
️✔️3.12
️✔️3.14
️✔️apim
️✔️latest
️✔️3.12
️✔️3.14
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.14
️✔️appservice
️✔️latest
️✔️3.12
️✔️3.14
️✔️aro
️✔️latest
️✔️3.12
️✔️3.14
️✔️backup
️✔️latest
️✔️3.12
️✔️3.14
️✔️batch
️✔️latest
️✔️3.12
️✔️3.14
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.14
️✔️billing
️✔️latest
️✔️3.12
️✔️3.14
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.14
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.14
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.14
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.14
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.14
️✔️config
️✔️latest
️✔️3.12
️✔️3.14
️✔️configure
️✔️latest
️✔️3.12
️✔️3.14
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.14
️✔️container
️✔️latest
️✔️3.12
️✔️3.14
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.14
️✔️core
️✔️latest
️✔️3.12
️✔️3.14
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.14
️✔️databoxedge
️✔️latest
️✔️3.12
️✔️3.14
️✔️dls
️✔️latest
️✔️3.12
️✔️3.14
️✔️dms
️✔️latest
️✔️3.12
️✔️3.14
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.14
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.14
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.14
️✔️find
️✔️latest
️✔️3.12
️✔️3.14
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.14
️✔️identity
️✔️latest
️✔️3.12
️✔️3.14
️✔️iot
️✔️latest
️✔️3.12
️✔️3.14
️✔️keyvault
️✔️latest
️✔️3.12
️✔️3.14
️✔️lab
️✔️latest
️✔️3.12
️✔️3.14
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.14
️✔️maps
️✔️latest
️✔️3.12
️✔️3.14
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.14
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.14
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.14
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.14
️✔️network
️✔️latest
️✔️3.12
️✔️3.14
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.14
️✔️postgresql
️✔️latest
️✔️3.12
️✔️3.14
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.14
️✔️profile
️✔️latest
️✔️3.12
️✔️3.14
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.14
️✔️redis
️✔️latest
️✔️3.12
️✔️3.14
️✔️relay
️✔️latest
️✔️3.12
️✔️3.14
️✔️resource
️✔️latest
️✔️3.12
️✔️3.14
️✔️role
️✔️latest
️✔️3.12
️✔️3.14
️✔️search
️✔️latest
️✔️3.12
️✔️3.14
️✔️security
️✔️latest
️✔️3.12
️✔️3.14
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.14
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.14
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.14
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.14
️✔️sql
️✔️latest
️✔️3.12
️✔️3.14
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.14
️✔️storage
️✔️latest
️✔️3.12
️✔️3.14
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.14
️✔️telemetry
️✔️latest
️✔️3.12
️✔️3.14
️✔️util
️✔️latest
️✔️3.12
️✔️3.14
️✔️vm
️✔️latest
️✔️3.12
️✔️3.14

@azure-client-tools-bot-prd

azure-client-tools-bot-prd Bot commented Mar 31, 2026

Copy link
Copy Markdown
⚠️AzureCLI-BreakingChangeTest
⚠️role
rule cmd_name rule_message suggest_message
⚠️ 1011 - SubgroupAdd role deny-assignment sub group role deny-assignment added

@yonzhan

yonzhan commented Mar 31, 2026

Copy link
Copy Markdown
Collaborator

Thank you for your contribution! We will review the pull request and get back to you soon.

@github-actions

Copy link
Copy Markdown

The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR.

Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions).
After that please run the following commands to enable git hooks:

pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class az role deny-assignment CRUD support (focused on PP1 user-assigned deny assignments) to align with existing role assignment workflows, including command registration, parameters, help, and tests.

Changes:

  • Register az role deny-assignment list/show/create/delete commands and add a table transformer for list output.
  • Implement deny-assignment list/show/create/delete custom handlers with PP1 validation.
  • Add help/params, linter exclusions for long options, and introduce a new deny-assignment test file.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/azure-cli/azure/cli/command_modules/role/custom.py Adds deny-assignment list/show/create/delete implementations and PP1 input validation.
src/azure-cli/azure/cli/command_modules/role/commands.py Registers new role deny-assignment commands and list table transformer.
src/azure-cli/azure/cli/command_modules/role/_params.py Defines CLI parameters for deny-assignment commands.
src/azure-cli/azure/cli/command_modules/role/_help.py Adds help text and examples for the new/updated deny-assignment commands.
src/azure-cli/azure/cli/command_modules/role/linter_exclusions.yml Excludes option-length lint rules for new long parameter names.
src/azure-cli/azure/cli/command_modules/role/tests/latest/test_deny_assignment.py Adds scenario/live tests covering list/show and PP1 create/delete validation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/azure-cli/azure/cli/command_modules/role/custom.py Outdated
Comment thread src/azure-cli/azure/cli/command_modules/role/custom.py Outdated
Comment thread src/azure-cli/azure/cli/command_modules/role/custom.py Outdated
Comment thread src/azure-cli/azure/cli/command_modules/role/_params.py
Comment thread src/azure-cli/azure/cli/command_modules/role/_params.py
Comment thread src/azure-cli/azure/cli/command_modules/role/tests/latest/test_deny_assignment.py Outdated
Comment thread src/azure-cli/azure/cli/command_modules/role/tests/latest/test_deny_assignment.py Outdated
@jruttle

jruttle commented May 7, 2026

Copy link
Copy Markdown
Member Author

Bumped azure-mgmt-authorization from 5.0.0b15.0.0b2 in src/azure-cli/setup.py (commit 20fe3a79).

This picks up the new DenyAssignment management-plane operations (BeginCreateOrUpdate / BeginDelete) that shipped in azure-mgmt-authorization 5.0.0b2 on PyPI today (release request Azure/azure-sdk-for-python#46551, released by @msyyc at 10:30 UTC).

This was the last upstream blocker on this PR — the CLI commands here now have a real published SDK surface to call against. CC for review awareness: prior reviewers / CLI mgmt module owners.

@jruttle

jruttle commented May 7, 2026

Copy link
Copy Markdown
Member Author

Follow-up to commit 20fe3a79 — the setup.py bump alone wasn't enough; the CI errors

`ERROR: Cannot install azure-cli==2.85.0 and azure-mgmt-authorization==5.0.0b1 because these package versions have conflicting dependencies.`
`  The user requested azure-mgmt-authorization==5.0.0b1`
`  azure-cli 2.85.0 depends on azure-mgmt-authorization==5.0.0b2`

…showed the platform lock files (requirements.py3.Linux.txt, Darwin.txt, windows.txt) were still pinning the old beta. Now bumped in three follow-up commits:

CI should now re-run cleanly on the new HEAD.

jruttle and others added 6 commits June 18, 2026 14:33
…ract changes

The new TypeSpec-generated 5.0.0b2 SDK introduced breaking changes that
block the Full Test pipeline. Changes:

* role/custom.py _resolve_role_id: pass `filter` as a keyword argument to
  RoleDefinitionsOperations.list (its signature is now
  `list(scope, *, filter=None)`). This single line was the root cause for
  the wide blast radius across vm/iot/aro/ams/acr/resource modules and the
  role module's own RoleAssignmentScenarioTest cases - all of them resolve
  roles by name and therefore go through this function.

* role/custom.py update_role_assignment: replace
  `RoleAssignment.from_dict(d)` with `RoleAssignment(d)`. The new Model
  base class accepts a JSON mapping directly and no longer exposes
  from_dict.

* role/custom.py create_deny_assignment: switch from passing a snake_case
  dict to constructing SDK model objects (DenyAssignment,
  DenyAssignmentProperties, DenyAssignmentPermission,
  DenyAssignmentPrincipal). The TypeSpec serializer would have written the
  raw snake_case keys to the wire instead of camelCase. Also rename
  `deny_client.create(...)` to `create_or_update(...)` (the method was
  renamed in the new SDK).

* role/custom.py delete_deny_assignment: the new SDK does not expose
  delete_by_id for deny assignments, so parse the resource ID via a new
  _parse_deny_assignment_id helper (case-insensitive regex that handles
  subscription, resource-group, and management-group scopes) and call
  delete(scope, deny_assignment_id).

* role/custom.py show_deny_assignment / delete_deny_assignment /
  create_deny_assignment: move argument validation ahead of the
  _auth_client_factory(...) call so validation-only error paths don't
  require an authenticated session.

* tests/latest/test_deny_assignment.py: convert DenyAssignmentListTest to
  LiveScenarioTest (these hit the live API and have no recorded cassettes
  in the repo), and replace DenyAssignmentShowTest with a unittest-based
  DenyAssignmentShowValidationTest that calls show_deny_assignment()
  directly and asserts the CLIError - both changes remove the dependency
  on a logged-in session in playback CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract _build_deny_assignment_model helper to keep the local count under
the project's max-locals=25 threshold (was 26/25 in azdev-style after the
SDK-model refactor).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pipeline build 314889 (re-triggered 11 May) still fails because the 8 May patch
did not address every breaking change in the new TypeSpec-generated SDK. This
commit fixes the four remaining classes of issue.

* role/custom.py update_role_assignment: `RoleAssignment(some_dict)` silently
  produces a model with `properties=None` (the new model wraps domain
  attributes under a nested `properties` field, with `__flattened_items`
  exposing them via descriptors only when `properties` is set). The old code
  then read `assignment.scope` -> None and passed scope=None to the SDK URL
  serializer, which raised `ValueError('No value for given attribute')`.
  Read scope/name/principalType/etc. directly from the user-supplied flat
  camelCase dict and send a `{"properties": {...}}` JSON body to .create()
  via its JSON overload - simpler and avoids the new model's flatten/unflatten
  gymnastics. Caused test_role_assignment_create_update.

* role/custom.py list_role_assignments + list_deny_assignments +
  show_deny_assignment: `knack.util.todict` walks `__dict__` and
  therefore returns `{}` for the new MutableMapping-based models, so the
  subsequent `ra['roleDefinitionId']` / `ra['principalId']` lookups
  raised `KeyError`. Added explicit _role_assignment_to_dict and
  _deny_assignment_to_dict adapters that project the model back to the
  legacy flat camelCase shape (with enum -> str coercion) and routed all
  list/show paths through them. Caused
  test_create_for_rbac_password_with_assignment.

* vm/_validators.py, ams/operations/sp.py, containerapp/_utils.py,
  acs/_roleassignments.py: `RoleDefinitionsOperations.list` is now
  `list(scope, *, filter=None)`, so each remaining caller that passed
  filter positionally raised
  `TypeError: list() takes 2 positional arguments but 3 were given`.
  This is the same bug fixed in 2d2c80a for role/custom.py::_resolve_role_id;
  these four call sites were missed. Caused test_vm_msi, test_vm_explicit_msi,
  test_vmss_msi, test_vmss_explicit_msi, test_ams_sp_create_reset, and the
  ACS / Container Apps role-resolution paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
While running the previously-failing tests locally to verify 005aa58,
test_identity_hub continued to fail because azure-cli-core itself has its
own resolve_role_id helper at src/azure-cli-core/azure/cli/core/commands/arm.py
that also passed filter positionally to RoleDefinitionsOperations.list.
This is the shared utility many modules call via core.commands.arm.create_role_assignment.

Same one-line fix as the four module-level callers patched in 005aa58:
pass filter as a keyword argument so it works with the new TypeSpec-generated
SDK signature `list(scope, *, filter=None)`.

Local verification (azure-mgmt-authorization 5.0.0b2 installed):
  * test_role_assignment_create_update PASSED (was: ValueError)
  * test_role_assignment_scenario PASSED
  * test_role_assignment_create_using_principal_type PASSED
  * test_role_assignment_handle_conflicted_assignments PASSED
  * test_create_for_rbac_password_with_assignment PASSED (was: KeyError)
  * test_vm_msi PASSED (was: TypeError)
  * test_vm_explicit_msi PASSED
  * test_vmss_msi PASSED
  * test_vmss_explicit_msi PASSED
  * test_identity_hub PASSED (was: TypeError - this commit's fix)
  * Full role test sweep: 17 passed, 2 LiveOnly skipped
  * Role unit tests: 14 passed

test_ams_sp_create_reset still fails with VCR cassette mismatch (the cassette
was recorded against azure-mgmt-authorization 4.x but dev branch bumped to
5.0.0b1 in Azure#31859 without re-recording AMS cassettes; multiple identical
requests now exhaust the single recorded response). This is pre-existing on
dev and outside the scope of this PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The azdev-style GitHub Actions check (run id 25725694563) failed flake8 on
the new `_coerce` function added in 005aa58 because it followed the
section comment block with only one blank line. Adding the second blank line
resolves the two reported E302 errors at line 46.

Verified locally with `azdev style --pep8` -> Flake8: PASSED.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jruttle jruttle force-pushed the jruttle/add-deny-assignment-commands branch from 9064242 to 07b2efa Compare June 18, 2026 13:34
@jruttle jruttle requested a review from MaddyMicrosoft as a code owner June 18, 2026 13:34
@notyashhh

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@isra-fel isra-fel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi please see the inline comment about aligning the design of assignment and deny-assignment. Also please make sure CI is green.

Comment thread src/azure-cli/azure/cli/command_modules/role/_params.py Outdated
Jonathan Ruttle and others added 2 commits June 19, 2026 10:07
Aligns with the --assignee-object-id / --assignee-principal-type pattern
from 'az role assignment create', per review feedback from isra-fel.

Uses --principal-object-id rather than --assignee-object-id because the
target of a deny assignment is semantically a 'principal being denied',
not an 'assignee being granted'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@teresaritorto

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

- arm.py: Coerce AAZSimpleValue to str before passing to authorization SDK
  (fixes TypeError 'AAZSimpleValue is not JSON serializable' in policy
  assignment create with managed identity)
- resource: Re-record test_resource_policy_identity and
  test_resource_policy_identity_systemassigned cassettes against 5.0.0b2
- aro: Fix test_aro_public_cluster cassette URI mismatches (double-slash
  from empty parentResourcePath + casing alignment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@teresaritorto

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

The TypeSpec-generated authorization SDK (5.x) nests RoleAssignment domain
attributes under a 'properties' envelope, and knack.util.todict does not walk
these MutableMapping models. ams list_role_assignments assumed the legacy flat
shape and raised KeyError: 'roleDefinitionId', causing sp reset-credentials to
exhaust its role-assignment retries and fail test_ams_sp_create_reset with a
VCR cassette-exhaustion error. Project the model to the flat camelCase dict via
its snake_case attributes, mirroring the role module's adapter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@teresaritorto

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

…nment-commands

# Conflicts:
#	src/azure-cli/azure/cli/command_modules/resource/tests/latest/recordings/test_resource_policy_identity.yaml
#	src/azure-cli/azure/cli/command_modules/resource/tests/latest/recordings/test_resource_policy_identity_systemassigned.yaml
@teresaritorto

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@isra-fel

Copy link
Copy Markdown
Member

This PR is one of those who updates test cases from multiple modules because of sdk update. As we agreed on instead of pinging several codeonwers I will force merge it.

@isra-fel isra-fel merged commit 92f2c3d into Azure:dev Jun 26, 2026
50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants