Skip to content

[PROTOTYPE][Do Not Merge] External Evaluation (Invoke) policy for azure-core-management#49701

Draft
weidongxu-microsoft wants to merge 10 commits into
Azure:mainfrom
weidongxu-microsoft:core-mgmt-external-evaluation-policy
Draft

[PROTOTYPE][Do Not Merge] External Evaluation (Invoke) policy for azure-core-management#49701
weidongxu-microsoft wants to merge 10 commits into
Azure:mainfrom
weidongxu-microsoft:core-mgmt-external-evaluation-policy

Conversation

@weidongxu-microsoft

@weidongxu-microsoft weidongxu-microsoft commented Jul 2, 2026

Copy link
Copy Markdown
Member

Warning

This is a PROTOTYPE / design-review draft — not intended to merge as-is.

Summary

Prototype of an External Evaluation ("Invoke") policy for azure-core-management. It lets a management client transparently satisfy Azure Policy external evaluation: when a resource operation is denied with 403 RequestDisallowedByPolicy carrying missingPolicyTokenDetails, the policy acquires a policy token (via a user-injected provider) and replays the request with the x-ms-policy-external-evaluations header.

The flow applies to all resource operations except GET (i.e. PUT/PATCH/DELETE/POST). Operations without a request body (e.g. DELETE) acquire a token with a null operation.content.

How a user opts in

Import the policy-token API (e.g. azure-resourcemanager-resources acquirePolicyToken), write a small PolicyTokenProvider, and add the policy to the client pipeline via the manager builder:

StorageManager.configure()
    .withPolicy(new ExternalEvaluationPolicy(policyTokenProvider))
    .authenticate(credential, profile);

What is included

Main (new public API — com.azure.core.management.evaluation):

  • ExternalEvaluationPolicy — detects the 403, acquires the token, replays with the header (async + sync). On acquisition failure it re-surfaces the original 403 ManagementException with the provider error chained as the cause.
  • PolicyTokenProvider, PolicyToken, PolicyTokenRequestContext, and internal MissingPolicyTokenDetails parsing.

Note on naming: the token-acquiring interface is PolicyTokenProvider (not ...Credential). The policy token is not an AAD/authentication token — it only carries external-evaluation context — so Credential (which in azure-core denotes an identity credential like TokenCredential) would be misleading.

Tests:

  • ExternalEvaluationPolicyTests — unit coverage (acquire+retry async/sync, non-GET methods such as bodyless DELETE, GET pass-through, plain deny pass-through, non-403 pass-through, change-reference guard, acquisition-failure re-surface).
  • ExternalEvaluationPolicyE2ETests — full flow against a real generated Storage client and the real acquirePolicyToken API, with ARM faked by WireMock (HTTPS). Asserts exact request URL, method, and byte-for-byte request body.

Prototype caveats (why this is a draft)

  • Reverse test dependency: the E2E test adds a test-scoped dependency on azure-resourcemanager-resources (a downstream library) purely to exercise the real acquirePolicyToken API. Needs a decision on the proper home for this test / dependency direction.
  • Copied fixtures: ~95 generated Storage sources are copied under test to act as a realistic guarded client.
  • JPMS test plumbing: pom.xml add-opens / add-reads and a trust-all Netty client are test-only scaffolding for the WireMock-over-HTTPS E2E.
  • API shape (naming, PolicyTokenProvider contract, failure semantics) is up for review.

Validation

ExternalEvaluationPolicyTests (10) + ExternalEvaluationPolicyE2ETests (1) pass; checkstyle clean.

weidongxu-microsoft and others added 7 commits July 2, 2026 10:28
… draft)

Adds ExternalEvaluationPolicy plus the PolicyTokenCredential / PolicyToken / PolicyTokenRequestContext trio to support the Azure Policy external evaluation flow: on a 403 RequestDisallowedByPolicy with missingPolicyTokenDetails, acquire a policy token and retry the operation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copied the TypeSpec-generated Storage management client into
azure-core-management test sources to serve as the guarded ARM client
for the External Evaluation policy E2E test. Suppressed a deprecation
warning on HttpResponse.getHeaderValue(String) to satisfy -Werror.

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

The External Evaluation policy set the acquired token header on an
early-captured request reference. Because the retry policy below it
replaces the context's request with a fresh copy on each attempt, the
header was lost on the replay. Set the header on context.getHttpRequest()
so the retried request carries it.

Add a WireMock-based E2E test that drives a copied Storage management
client guarded by the policy and acquires the token via the real
acquirePolicyToken API from azure-resourcemanager-resources. Opens the
storage model packages to azure-core/azure-json for reflective
(de)serialization under JPMS.

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

When ExternalEvaluationPolicy cannot acquire a policy token, the operation
remains denied by policy. Re-surface the original 403 as a ManagementException
(the exception the caller would otherwise see) with the acquisition failure
chained as its cause, in both the async and sync paths. The credential wrapper
in the E2E test now errors on a failed/missing token result.

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

The mock ARM service now asserts every aspect of the guarded requests:
- both PUTs match the exact URL path, PUT method, and a byte-for-byte identical
  request body (the External Evaluation flow requires the retried body to be
  identical to the original);
- the denied PUT has no token header while the retried PUT carries the acquired
  token;
- the acquirePolicyToken POST echoes the guarded operation verbatim (same
  httpMethod, same uri, and byte-for-byte the same content).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace matchingJsonPath/equalToJson on the acquirePolicyToken POST with a
single byte-for-byte equalTo against the exact serialized request body. The
PolicyTokenRequest serialization order is deterministic (operation -> uri,
httpMethod, content), so the whole body can be asserted as an exact UTF-8
string, matching the byte-for-byte requirement on operation.content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…2E test

Opt into the External Evaluation flow the way a user would: build the guarded
StorageManager through configure().withPolicy(new ExternalEvaluationPolicy(...))
.authenticate(credential, profile) instead of hand-assembling the pipeline and
calling authenticate(pipeline, profile). This also verifies the policy is placed
correctly (PER_CALL, above retry) within the real manager pipeline.

Because the manager pipeline adds a BearerTokenAuthenticationPolicy that requires
HTTPS, the mock service is now served over TLS and the clients use a trust-all
Netty HTTP client. Adds --add-reads for com.azure.http.netty to the module test
args.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added the Azure.Core azure-core label Jul 2, 2026
weidongxu-microsoft and others added 2 commits July 2, 2026 14:39
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The acquired policy token is not an authentication credential (it is a token
proving external policy evaluation context, not an AAD token), so 'Credential'
was misleading. Rename the interface to PolicyTokenProvider and update the
ExternalEvaluationPolicy field/parameter, tests, javadoc and CHANGELOG.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply the external evaluation flow to all resource operations except GET, and skip any request that already carries the x-ms-policy-external-evaluations header. Per the service contract such a request will not produce a 403 with missingPolicyTokenDetails, so this acts as a loop guard (relevant now that non-idempotent POST is in scope). Bodyless operations (e.g. DELETE) acquire a token with null operation.content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Azure.Core azure-core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant