Skip to content

feat(adr): make unlink permission checks advisory - #928

Open
Hangyi (HangyiWang) wants to merge 14 commits into
Azure:release/1.1.0-previewfrom
HangyiWang:feat/adr-unlink-advisory
Open

Hangyi (HangyiWang) wants to merge 14 commits into
Azure:release/1.1.0-previewfrom
HangyiWang:feat/adr-unlink-advisory

Conversation

@HangyiWang

@HangyiWang Hangyi (HangyiWang) commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Make namespace link removal use read-only, advisory permission checks after an expanded warning. LinkedResourceDeleted and Failed skip the check; Succeeded checks read-grant evidence for the configured outbound identity at the linked resource's resource group, falling back to its subscription when the group is confirmed deleted.

Missing access or malformed/unavailable checks warn and continue without changing role assignments; --skip-rbac-check bypasses only this advisory step, and ADR remains authoritative. ETag protection, single-attempt conditional PUTs, and asynchronous polling with retryable reads remain intact.

Hangyi (HangyiWang) and others added 8 commits October 12, 2026 03:52
Capture the namespace GET response ETag and require If-Match on the replacement PUT. Preserve the existing no-retry and no-wait contracts; reject missing ETags before RBAC or writes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the shared offline CLI fixture migration together with ETag-aware unlink mocks. Record the unlink fix in the 1.1.0b1 release notes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject weak or malformed entity tags before RBAC, normalize HTTP optional whitespace, and preserve the original 412 while logging rerun guidance. Cover wire headers, failure side effects, and CLI diagnostics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wait for ARM operation status through the shared bounded ADR poller and propagate asynchronous unlink errors. Add an explicit no-wait option and update command, transport, CLI and integration coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Combine Azure#925 and Azure#927 for dependent advisory development, preserving both sets of regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Gate read-only outbound identity checks by endpoint state, fall back to subscription only for confirmed RG deletion, and keep ADR authoritative. Add an independent skip option and preserve ETag and async error protections.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Include the new live case in the explicit xdist allocation and offline collection contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve conflicts with merged ETag and polling fixes; preserve read-only advisory checks, skip controls, retries, and updated readiness fixtures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@HangyiWang
Hangyi (HangyiWang) marked this pull request as ready for review October 15, 2026 23:21
Copilot AI balanced review requested due to automatic review settings October 15, 2026 23:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Non-string target IDs can escape the advisory error handler and block unlink.

1 open finding
What changed in this PR

Makes ADR namespace unlink permission checks advisory and read-only while preserving concurrency protection and service error reporting.

Changes:

  • Adds state-aware read-grant checks with resource-group deletion fallback.
  • Introduces --skip-permission-check and clearer warnings.
  • Updates documentation and regression coverage.
File Description
README.md Summarizes advisory unlink behavior.
HISTORY.rst Records changes and removes obsolete grant guidance.
azext_iot/​tests/​test_workflow_results_unit.py Updates integration-test discovery expectations.
azext_iot/​tests/​adr/​test_adr_registration_unit.py Checks confirmation and flag registration.
azext_iot/​tests/​adr/​test_adr_link_rbac_unit.py Tests read-grant evidence and scope fallback.
azext_iot/​tests/​adr/​test_adr_link_delete_unit.py Tests advisory failures, state gating, and safeguards.
azext_iot/​tests/​adr/​test_adr_link_delete_int.py Adds live advisory-check coverage.
azext_iot/​tests/​adr/​conftest.py Registers the new integration scenario.
azext_iot/​adr/​rbac.py Replaces grant creation with read-only checks.
azext_iot/​adr/​providers/​link.py Applies state-aware advisory preflight.
azext_iot/​adr/​params.py Registers the skip flag.
azext_iot/​adr/​commands_link.py Forwards the skip flag.
azext_iot/​adr/​command_map.py Expands unlink confirmation.
azext_iot/​adr/​_help.py Documents checks and independent flags.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread azext_iot/adr/providers/link.py Outdated
Validate resourceId before advisory parsing so malformed service values warn and continue. Rename the skip option to --skip-rbac-check without a legacy alias and update all command, help, and test surfaces.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 15, 2026 23:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Malformed identity metadata can still abort the advisory preflight and prevent unlink submission.

1 open finding
1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Handle malformed identity metadata in advisory checks

azext_iot/​adr/​providers/​link.py:275

For a Succeeded endpoint, malformed identity metadata can still block this advisory check. A non-string outboundIdentity.userAssignedIdentity raises AttributeError in _normalized_id(), and a non-dict attached UAMI projection can raise it when reading principalId. Neither reaches the warning handler, so no namespace PUT is submitted. Convert shape errors from the identity resolver into a handled advisory error without changing add/update behavior, and add regression tests for both cases.

🧠 Review effort: Balanced

Comment thread azext_iot/adr/params.py
@zzcodework

Copy link
Copy Markdown
Contributor

Azure SRE Agent - automated review

Reviewing full diff (head 2122cc0)

This replaces LinkRbacManager.ensure_unlink_reader (which auto-created a Reader role assignment and waited 30s for propagation before unlink) with check_unlink_read_access, a read-only advisory check: LinkedResourceDeleted/Failed endpoints skip it entirely, Succeeded endpoints look for an existing Reader/Contributor/Owner grant (including inherited) at the linked resource's resource group — falling back to the subscription only if the group is confirmed ResourceGroupNotFound — and any failure to confirm access just warns and continues rather than blocking the unlink. --skip-rbac-check bypasses only this advisory step (not confirmation, not ETag validation).

This is a real behavior change worth flagging explicitly for anyone relying on the old auto-grant: the CLI no longer creates the Reader assignment needed for ADR's own backend to verify the linked resource is gone, so a removal that previously "just worked" by self-granting now depends on the permission already existing, or on ADR's server-side check tolerating its absence. That tradeoff is called out in HISTORY.rst/README.md/_help.py and is exercised end-to-end by the new _assert_live_target_removal_rejected int-test helper, so I'm treating it as intentional rather than a defect.

Both open Copilot findings on this PR look incorrect — verified against the code:

  1. "Non-string resourceId reaches the shared parser's .strip() and raises an uncaught AttributeError" — not reachable. _check_unlink_permissions (azext_iot/adr/providers/link.py) does if not isinstance(resource_id, str): raise AzureResponseError(...) before calling _parse_hub_resource_id/_parse_dps_resource_id/_parse_su_resource_id, and that AzureResponseError is itself caught by the surrounding except (CLIError, HttpResponseError, ServiceRequestError, ServiceResponseError) (confirmed AzureResponseError → UserFault → AzCLIError → knack.util.CLIError in azure-cli-core/azure/cli/core/azclierror.py), so it warns and continues exactly like any other advisory-check failure. The PR's own new test, test_delete_real_sdk_non_string_resource_id_does_not_block_unlink, parametrizes resourceId over int/list/dict/bool and asserts the PUT still happens — i.e. this exact scenario is covered and passes.
  2. "PR description promises --skip-permission-check, but only --skip-rbac-check is registered" — the PR description, HISTORY.rst, README.md, _help.py and params.py all consistently use --skip-rbac-check only; I could not find --skip-permission-check promised anywhere in this PR. The new parametrize case Copilot is likely reacting to — adding "--skip-permission-check" to test_delete_rejects_composite_and_wait_options's option list — asserts the opposite of what the comment claims: it's a regression test confirming that --skip-permission-check is correctly rejected (SystemExit code 2) as an unregistered option, guarding against that plausible-but-wrong spelling ever being accepted by mistake.

Checked and clean

  • resource["identity"] = sanitize_arm_identity(namespace["identity"]) is now correctly guarded by if "identity" in namespace: — needed because the state-gated permission check can skip resolving the outbound identity, so a namespace without an identity key no longer raises KeyError on the PUT body.
  • UNLINK_READ_ROLE_IDS's Owner id 8e3af657-a8ff-443c-a75c-2fe8c4bcb635 matches the well-known Azure built-in Owner role definition id.
  • _assignment_scope_applies(..., inherited_at_scope=True) (pre-existing, unchanged) correctly treats both a parent subscription scope and a management-group ancestor scope as covering the target resource group, which is what the new inherited-grant test parametrization exercises.
  • The conflict-retry path (test_delete_real_sdk_conflict_retries_with_updated_resource equivalent) still calls check_unlink_read_access once per attempt and doesn't regress the existing If-Match/412 handling.

No blocking issues found.


Automated review; may be incomplete.

Copilot AI balanced review requested due to automatic review settings October 16, 2026 00:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Malformed identity data and SDK deserialization failures can still escape the advisory handler and block unlink.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Comment thread azext_iot/adr/providers/link.py Outdated
Validate outbound UAMI shapes only during unlink checks and explicitly handle SDK DeserializationError. Cover real SDK malformed responses, bypass states and preserved namespace backend failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 18:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Malformed identity containers can still abort unlink before submission despite the advisory continuation warning.

0 open findings

1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Handle non-iterable user-assigned identities without TypeError

azext_iot/​adr/​providers/​link.py:224

If identity.userAssignedIdentities is a non-iterable value such as 123, the advisory check warns that unlink will continue, but this call then raises TypeError: sanitize_arm_identity iterates that value in azext_iot/common/arm.py:67-70. No namespace PUT is sent, and --skip-rbac-check hits the same failure. Handle malformed identity containers when building the writable payload without silently dropping attached identities, and add regression cases for both normal and skipped preflight.

🧠 Review effort: Balanced

@zzcodework

Copy link
Copy Markdown
Contributor

Azure SRE Agent - automated review

No blockers. 0 new suggestions on b9a813a9.

This commit resolves Copilot's inline finding (4289561224, not restating) that malformed outboundIdentity/userAssignedIdentities data could raise an uncaught AttributeError via _normalized_id, and that a DeserializationError from the resource-group lookup in check_unlink_read_access fell outside the advisory catch.

Verified: link.py's new pre-validation guards every malformed shape the tests cover (non-dict/list/int/bool at each level), short-circuits before .items() so a non-dict userAssignedIdentities can't crash, and the DeserializationError catch is scoped to _check_unlink_permissions only — _remove_endpoint's PUT/412 handling is untouched and still propagates.

Automated review, unverified. 0 clean checks recorded in state.

Comment thread azext_iot/adr/providers/link.py Outdated
Reject malformed identity containers without losing attached identities, even when advisory checks are skipped. Keep unscoped linked resource IDs advisory and expand real-SDK and live identity-preservation coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 17, 2026 07:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Falsy malformed outbound-identity metadata can select the system identity and produce misleading permission-check results.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Preserve falsy malformed outboundIdentity values for validation

azext_iot/​adr/​providers/​link.py:284

For a Succeeded endpoint, falsy non-object outboundIdentity values ([], false, 0, or "") become {} before validation. If the namespace has a system-assigned identity, the resolver then selects that identity and the check can report read-grant evidence instead of warning about malformed metadata. Default only missing/null values to {} so other malformed values reach the existing advisory warning path. Add these falsy values to the malformed-outbound-identity tests.

Review effort: Balanced

@zzcodework

Copy link
Copy Markdown
Contributor

Azure SRE Agent - automated review

No blockers. 0 new suggestions on 05b4c34f (changes since b9a813a9, 1 commit, 4 files).

Not restating — this is the direct fix for cheatsheet1999's inline comment (confirmed by HangyiWang): sanitize_arm_identity() assumed identity/identity.userAssignedIdentities are mappings. The commit now raises AzureResponseError before preflight when identity isn't a dict, or userAssignedIdentities isn't a dict with string keys — matching both flagged cases (identity: 123, userAssignedIdentities: 123). Verified via test_delete_real_sdk_rejects_malformed_identity_containers_before_preflight and test_delete_rejects_non_string_attached_identity_keys: GET-only, no PUT, source untouched, preflight/check_unlink_read_access never called.

Also added: resourceId must resolve to a subscription+RG+name-scoped ARM id via parse_resource_id (still advisory, not a hard block) — consistent with existing unscoped-id test coverage.

Automated review, unverified. 1 clean check recorded in state.

Preserve ADR unlink and DPS certificate/enrollment release notes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 17, 2026 20:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

No blocking issues were identified; focused regression coverage supports advisory behavior while preserving existing write safeguards.

No open issues 🎉

Review effort: Balanced

Comment thread azext_iot/adr/rbac.py
UNLINK_READER_PROPAGATION_DELAY_SECONDS = 30
UNLINK_WARNING = (
"This removes only the namespace endpoint, not the linked resource. "
"Delete the linked resource first. ADR may reject removal if the namespace outbound identity "

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.

This warning makes deleting the linked resource a prerequisite for every removal, which is broader than what ADR requires and still incomplete. ADR removes a Failed endpoint without the linked resource being deleted, so a user cleaning up a failed link is told to delete a resource that may still be in use. For a Hub endpoint, including a Failed one, ADR also returns 400 EndpointUsedByNestedDevices before starting the update while any Registry Device still has an IoT Hub capability that references the endpoint, and deleting the IoT Hub does not clear those references. The warning and the hub remove help should limit the deletion step to Succeeded links and add the device reference prerequisite for Hubs, for example deleting the referencing devices with az iot adr ns device delete.

Comment thread azext_iot/adr/rbac.py
"The advisory check recognizes unconditional Reader, Contributor and Owner assignments, "
"including inherited grants; custom roles, group membership and deny assignments are not fully evaluated. "
"Continuing without changing role assignments; ADR may reject removal. "
"Verify the identity's read access or use --skip-rbac-check to skip this advisory check.",

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.

The roles link add grants the namespace identity are scoped to the target itself, so once the target is deleted ADR can reject removing a Succeeded endpoint for missing read access. The old path created that Reader grant, and this warning now describes the gap without a runnable fix. Could it print the Reader command for the checked scope through _manual_commands, the way _ensure_assignments already does, when no read grant is found? The remove help and README could also say that ADR confirms a deletion by reading the target, which needs read access on a scope that outlives it, such as the resource group, or the subscription when the group is deleted.

Comment thread azext_iot/adr/params.py
for kind in ("hub", "dps", "su"):
with self.argument_context(f"iot adr ns link {kind} remove") as context:
context.argument(
"skip_rbac_check", options_list=["--skip-rbac-check"], action="store_true",

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.

Could we add an opt-in next to this flag, such as --grant-reader, for users who want the removal to set up the read access itself? When the check finds no read grant, it would create Reader for the namespace outbound identity on the linked resource's resource group through _ensure_assignments, the same path link add uses, which already confirms the caller can assign roles and otherwise prints the exact commands. The default stays read only, and a grant left behind after a 412 is then one the user asked for. It would apply only to Succeeded endpoints, reject combination with --skip-rbac-check, and print the subscription command rather than granting it when the resource group is deleted, because a subscription grant reaches every resource in it. Its help and the message after a grant should also say that a new assignment can take several minutes to take effect, so if ADR rejects the removal right after the grant, rerunning the command later can succeed. The link update help already notes that assignment visibility does not guarantee the service honors access.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants