Supporting the "Invoke" / External Evaluation Policy Flow in the Azure SDKs
- Dominant language
- PowerShell
- Stars
- 597
- Forks
- 374
- Avg merge
- 5d 21h
- Merged PRs (30d)
- 42
Description
## Background: what is "Invoke"?
Azure Policy is adding **External Evaluation** (colloquially "Invoke"): an enforcement policy that factors in the result of an external call when deciding to allow or deny an ARM operation. Today the only endpoint is an **Azure Resource Graph (ARG)** query; more are planned.
A normal policy can only inspect the request body. An External Evaluation policy can also query **other state in the subscription** via ARG, so it can enforce rules a plain policy cannot. Rule semantics: **"Deny unless the request carries a token proving the external check ran and returned an acceptable result."**
## Typical use cases — what ARG actually checks
The ARG query runs at request time, scoped to the **single subscription** of the operation, and must return **0 or 1 rows** projecting **one claim** the rule compares against. Patterns:
- **Reference / over-sharing checks** — canonical example: *deny attaching an NSG to a subnet if that NSG is already attached to a different subnet*. ARG counts the other subnets referencing the NSG; the rule denies if count > 0.
- **Existence checks** — deny (or require) based on whether a related resource exists, e.g. deny creating a resource unless a prerequisite resource is already present.
- **Reference-by-others checks** — deny deleting/changing a resource that other resources still depend on.
- **Cross-resource invariants** — e.g. deny a tag change on an NSG if it would violate an immutable-tag rule, decided by querying the resource's current state.
Not a fit: cross-subscription / cross-tenant / management-group checks (query is pinned to one subscription), or anything needing more than a single projected value.
## How enforcement works (why ARM rejects the request)
1. An External Evaluation policy is **assigned** to a scope. Its definition holds an ARG query + the claim it projects, plus `missingTokenAction` (typically `deny`).
2. A user issues a normal ARM op (e.g. `PUT` a subnet referencing an NSG).
3. The op carries **no policy token**, so ARM rejects it with **`403 RequestDisallowedByPolicy`** — meaning "this op is guarded; prove the external check passes first." The 403 body carries machine-readable instructions.
4. The client calls the subscription-level **`acquirePolicyToken`** API, which runs the ARG queries of the applicable policies and signs the results into one token.
5. The client **retries** the original op with the token in a header.
6. ARM validates the token and proceeds (or the policy denies if the result is rejected).
The flow is **reactive** (triggered by the 403) and needs **client-side orchestration**, not just API exposure.
## The Ask to the SDKs
Stage 1 (exposing `acquirePolicyToken` in the SDKs) is **done**. The ask is **Stage 2**: the **end-to-end orchestration** — *detect the 403 → acquire token → retry* — transparently, ideally hidden from the user, matching clients that already do this:
- **Azure Portal** and **ARM Template / Deployments** — auto intercept, acquire, retry.
- **Azure CLI** — global `--acquire-policy-token` flag since v2.85.0.
- **PowerShell** — similar flag-based support.
> CLI/PS add the behavior behind an **optional switch initially**, with a plan to **remove the switch and make it default** once the flow has been in use for a while.
Deliverables: a **cross-language standardized** design, plus **samples and docs**, aligned with each language owner / the architecture board.
## REST contract (key wire details)
**API versions**
| Operation | API version |
| --- | --- |
| Policy definition / assignment CRUD | `2025-03-01` |
| `acquirePolicyToken` | `2025-11-01` |
**1. Initial 403** — `403 RequestDisallowedByPolicy`; actionable payload under `error.additionalInfo[*].info.evaluationDetails.missingPolicyTokenDetails` (`shouldDeny`, `endpointKind`, `endpointDetails`, `endpointResultLifespan`, `isChangeReferenceRequired`). Clients consume this directly to drive acquire + retry.
**2. `acquirePolicyToken` request** — subscription-level call describing the operation (URI, method, **request body**). The token is bound to the body via a `contentHash`.
**3. `acquirePolicyToken` response**
- `result` — `Succeeded` (per-policy outcomes under `results[*]`) vs. `Failed` (acquisition itself failed — **don't retry**).
- `token` — present when minted; **always `"PoP "`-prefixed** (pass verbatim).
- `results[*].policyAction` — verdict: all `Allow`/`Audit` → token issued, retry; any `Deny` → **no token, don't retry**; `Error` → see `policyEvaluationDetails.reason`.
**4. Retry**
- Header **`x-ms-policy-external-evaluations`** (lowercase, dashes; a typo = no header).
- Value = `token` **verbatim, incl. `PoP ` prefix**.
- Body must match the acquire request body **byte-for-byte** (`contentHash` binding).
## Current gap in the SDKs
- ✅ **Stage 1** — API exposed.
- ❌ **Stage 2** — E2E orchestration not implemented or documented; **no clear owner**.
Key challenges:
1. **Cross-library dependency** — `acquirePolicyToken` lives in the policy/authorization library, but guarded operations live across **many** resource libraries; the orchestration must work without coupling each service library.
2. **Byte-for-byte payload binding** — the body sent to `acquirePolicyToken` (`operation.content`) and the retried body must match `contentHash` exactly. This is **handled naturally by the pipeline policy**, which already has the final serialized bytes (re-serializing the model would risk drift).
---
## Current direction & considerations <-- **This is one proposal**
- **Where**: an **HTTP pipeline policy** that sees the request body + response, performs acquire + retry, shared across libraries. (Agreed as the primary direction.)
- **Detection**: only signal is the `403 RequestDisallowedByPolicy` + `missingPolicyTokenDetails`; no proactive detection.
- **Errors**: surface *which policy* blocked and *what's required*, not a raw 403; on `Failed`/`Deny`, **don't retry** — fail clearly.
- **Enablement**: **default-enabled in the SDK with an optional override** (opt-in has no long-term value — it just lets policies break apps). CLI/PS start behind a switch and plan to make it default.
## Open questions & current proposals
Each item lists the question and the **current proposed direction** (✅), or flags it as an **open gap** (❓) where no concrete answer exists yet.
- **Enablement model** — auto-enable vs. opt-in/opt-out. ✅ **Default-enabled in the SDK, with an optional override.** An opt-in approach has no long-term value — not opting in effectively asks for policies to break your app. (CLI/PS add the behavior behind a switch initially and plan to remove the switch once the flow has been in use for a while; SDKs should aim for default-on.)
One consideration *for* `opt-in` (instead of enable by default) is the [Diagnosable guideline](https://azure.github.io/azure-sdk/general_introduction.html#diagnosable), and it is easier for customer to "add" a pipeline policy, than to "remove" one -- and this may depend on how many subscription enables this External Evaluation Policy.
- **Error surfacing** — original 403 vs. enriched error. ❓ In the case that `acquirePolicyToken` itself fails (4xx), **Keep the original 403 and surface its enriched policy details** rather than replacing it with a new error type. In the case that `acquirePolicyToken` completes (200), but the Evalutation didn't pass, the 403 error need to wrap this information. We need more discussion on error/exception structure, as they are vital for customer to figure out that went wrong on 403 and no token, and the details of implementation can be language dependent.
- **Retry semantics** — how much custom retry logic is needed. ✅ **Drive acquire + retry from the pipeline policy and reuse standard ARM retry behavior**; avoid a bespoke retry model. In ambiguous internal-failure cases it may be best to just return the first response. ❓ `200 + result=Failed + retryAfter` and any multi-step retry chains beyond *403 → acquire → retry* are not yet worked out.
- **Cross-library integration** — `acquirePolicyToken` ships in one library while the guarded resource ops live in others. ✅ **Solve it in a shared HTTP pipeline policy** so the logic isn't coupled to individual service libraries. ❓ The concrete packaging (shared dependency vs. interface contract) is still open.
- **Payload access / rebinding** — the token request needs the full JSON payload, and the retry must resend it byte-for-byte to match `contentHash`. ✅ **Largely solved by the pipeline policy**: it sits at the bottom of the pipeline and already holds the **final serialized request body**, so it can feed those exact bytes to `acquirePolicyToken` as `operation.content` and replay the identical bytes on retry — no re-serialization, no drift. (The user-facing models never need to expose the raw JSON.)
- **Endpoint / version construction** — which endpoint and API-version the pipeline uses for `acquirePolicyToken`. ✅ **Reuse the client's own endpoint** (the same ARM host as the guarded PUT), so Public/Gov/other clouds are handled automatically. Use a **default API-version with the ability for the user to override** it.
- **RBAC** — what the caller needs to acquire a token. ✅ **Subscription-level read on the same subscription as the original request, plus permission to perform the guarded operation; no additional restriction.** Surface permission failures clearly.
- **Cross-language alignment** — architecture-board sign-off so behavior is consistent across languages. ❓ Pending.
## Status
| Item | Status |
| --- | --- |
| Stage 1 — API exposure | ✅ Done |
| Stage 2 — E2E SDK support | ❌ Not started / no owner |
| Design | 🟡 Active discussion, open questions |
| Cross-language alignment | 🔄 Needed (architecture board) |
## Next steps
1. Define a **reference design** for the pipeline policy (detect 403 → acquire → retry with token + original body).
2. Settle **payload rebinding** and **cross-library integration**.
3. Align with **language architects / architecture board**.
4. Produce **E2E scenarios, samples, and docs**.
## References
- External Evaluation knowledge docs (Azure/azure-policy):
- [01-overview.md](https://github.com/Azure/azure-policy/blob/master/ExternalEvaluationPolicies/agent-skills/azure-arg-external-evaluation-policy-author/knowledge/01-overview.md)
- [02-policy-artifacts-shape.md](https://github.com/Azure/azure-policy/blob/master/ExternalEvaluationPolicies/agent-skills/azure-arg-external-evaluation-policy-author/knowledge/02-policy-artifacts-shape.md)
- [03-rest-flow.md](https://github.com/Azure/azure-policy/blob/master/ExternalEvaluationPolicies/agent-skills/azure-arg-external-evaluation-policy-author/knowledge/03-rest-flow.md)
- [`acquirePolicyToken` REST API reference](https://learn.microsoft.com/en-us/rest/api/policy-authorization/policy-tokens/acquire?view=rest-policy-authorization-2025-11-01)
- Companion REST walkthrough: `invoke-policy-rest-flow.md`.
Contributor guide
Research direction
Start with invoke-policy-rest-flow.md and the acquirePolicyToken REST API reference, then examine the shared HTTP pipeline policy entry point described in the issue. Done means a cross-language reference design for detecting the 403, acquiring the token, retrying with the original serialized body, and defining the remaining error and packaging behavior, with E2E scenarios, samples, and docs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- azure
- Domain
- api, cloud
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100