Skip to content

feat(policy): add Azure Policy control plane (definitions, set definitions, assignments, exemptions) - #281

Open
Tal-E wants to merge 1 commit into
floci-io:mainfrom
Tal-E:feat/azure-policy
Open

feat(policy): add Azure Policy control plane (definitions, set definitions, assignments, exemptions)#281
Tal-E wants to merge 1 commit into
floci-io:mainfrom
Tal-E:feat/azure-policy

Conversation

@Tal-E

@Tal-E Tal-E commented Sep 5, 2026

Copy link
Copy Markdown

Summary

Adds the Azure Policy control plane under Microsoft.Authorization: policy definitions, policy set definitions (initiatives), policy assignments and policy exemptions, at every ARM scope the real service accepts (management group, subscription, resource group, resource; tenant-rooted built-in reads answer empty). This lets az policy ..., the azurerm policy resources and the policy SDKs (armpolicy, azure-mgmt-resource, azure-resourcemanager-resources) author and deploy policies against floci-az instead of a real subscription.

Scope is deliberately the control plane only: rules are stored and validated structurally, never evaluated. Nothing is denied, audited or remediated, there is no compliance state (Microsoft.PolicyInsights is not implemented) and no built-in definitions are seeded. The docs page lists every deviation.

What is in the change:

  • services/policy/: PolicyHandler (filter-lane AzureServiceHandler, guarded provider route on Microsoft.Authorization so role assignments, locks and deny assignments keep falling through to the generic ARM handler), PolicyStore (in-memory, keyed by lower-cased resource id like the other ARM control-plane stores) and PolicyPath (scope / type / name parsing for extension resources).
  • Definitions and set definitions: CreateOrUpdate, Get, Delete, List with $filter=policyType eq '...'. Server-side policyType=Custom, mode default Indexed, version default 1.0.0, metadata.createdBy/createdOn/updatedBy/updatedOn stamps, generated policyDefinitionReferenceId and definitionVersion=1.*.* on references.
  • Assignments: Create, Get, Update (PATCH of identity, location, resourceSelectors, overrides), Delete (returns the deleted assignment), List for subscription / resource group / resource / management group with atScope(), atExactScope(), atScopeAndBelow() and policyDefinitionId eq '...'. scope, enforcementMode, definitionVersion, instanceId populated server-side; SystemAssigned identities get a stable principal, UserAssigned identities resolve principalId/clientId from the Managed Identity store when the identity exists.
  • Exemptions: CreateOrUpdate, Get, Update (PATCH), Delete (returns the body), List with atScope(), atExactScope(), excludeExpired(), policyAssignmentId eq '...'; deleted together with their assignment.
  • Referential integrity: scoped definition / set definition references must exist (PolicyDefinitionNotFound, PolicySetDefinitionNotFound), exemptions need an existing assignment (PolicyAssignmentNotFound), and referenced definitions / set definitions cannot be deleted (InvalidDeletePolicyDefinitionRequest, InvalidDeletePolicySetDefinitionRequest). Built-in ids (/providers/Microsoft.Authorization/policyDefinitions/{guid}) are accepted without lookup.
  • Routing: routeArmProviders now also considers tenant-rooted providers/Microsoft.X/... paths (matching the /providers/<ns>/ marker against the slash-prefixed path), which is how built-in listings and management-group scopes arrive. Subscription-rooted behaviour is unchanged; RoutingTableAssemblyTest gains the new golden entry and the guarded-first invariant now covers both guarded routes.
  • Config floci-az.services.policy.enabled (FLOCI_AZ_SERVICES_POLICY_ENABLED, default true), banner line, docs/services/policy.md, nav, service matrix and README rows.
  • Tests: PolicyTest (definitions, set definitions, assignments, exemptions, scope filters, management-group and tenant scopes, referential integrity, routing boundaries), PolicyDisabledTest, two new cases in AzureRoutingFilterTest; a Java compat test (PolicyCompatibilityTest, raw REST like the Managed Identity one) and an az policy BATS file for the az CLI suite.

Out of scope, left for follow-ups: rule evaluation / deny effects, PolicyInsights compliance and remediation, built-in definition catalogue, the versions sub-resource API, pagination of listings.

Type of change

  • Bug fix (fix:)
  • New feature (feat:)
  • Breaking change (feat!: or fix!:)
  • Docs / chore

Azure Compatibility

  • Request/response shapes follow azure-rest-api-specs policy stable/2025-03-01/openapi.json and exemptions preview/2022-07-01-preview/policyExemptions.json; any api-version is accepted.
  • Response conventions were cross-checked against the Azure CLI's recorded live traffic in Azure/azure-cli (test_resource_policy_default.yaml, test_resource_policyset_default.yaml, test_resource_policyexemption_default.yaml, api-version 2025-11-01): 201 on definition and assignment PUT, 200 on set definition / exemption update, metadata.createdBy/createdOn stamps, generated numeric policyDefinitionReferenceId, definitionVersion 1.*.*, assignment instanceId, DELETE of assignments and exemptions returning the resource body, and the exact PolicyDefinitionNotFound / PolicySetDefinitionNotFound messages.
  • InvalidDeletePolicyDefinitionRequest (definition referenced by a set definition) reproduces the live message reported in AzureRM custom policy deletion fails when included in policy set definition hashicorp/terraform-provider-azurerm#15615.
  • Not verified against a live tenant: the InvalidCreatePolicyDefinitionRequest / InvalidCreatePolicyAssignmentRequest / InvalidCreatePolicyExemptionRequest codes returned for missing required properties (modelled on the recorded InvalidCreatePolicySetDefinitionRequest), and the assignment-referenced variant of InvalidDeletePolicyDefinitionRequest. Happy to adjust if a maintainer has live samples.
  • Verification SDKs: the Java compat test drives the REST wire protocol directly (java.net.http, as ManagedIdentityCompatibilityTest does); the BATS file uses az from mcr.microsoft.com/azure-cli:latest.

Checklist

  • ./mvnw test passes locally
  • New or updated integration test added
  • Commit messages follow Conventional Commits

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🎉 Thanks for your first pull request to floci-az!

Your CI checks need a maintainer to approve them before they run. That is GitHub's standard gate on first-time contributors, not a problem with your PR — so if the checks look like they are doing nothing, that is why. Once a maintainer approves, CI and the compatibility suite start automatically. Nothing is needed from you in the meantime.

While you wait, a couple of things that make review faster:

  • Link the issue this fixes with Closes #N in the description
  • Commits follow Conventional Commits (feat(blob): ..., fix(keyvault): ...)
  • Behaviour changes come with a test — see CONTRIBUTING.md

Come join us in Slack — it is the fastest way to reach maintainers if you get stuck, or want feedback on an approach before investing more time in it.

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces an in-memory Azure Policy control plane and wires it into ARM provider routing, configuration, documentation, and compatibility coverage.

  • Implements definitions, set definitions, assignments, exemptions, filtering, identities, and reference checks.
  • Adds tenant-rooted and management-group routing support for Microsoft.Authorization policy paths.
  • Adds Java REST and Azure CLI compatibility coverage plus service documentation.
  • The current implementation mishandles combined identities and some valid resource scopes, and its multi-resource integrity operations are not atomic under concurrent requests.

Confidence Score: 2/5

This PR is not yet safe to merge because valid assignment payloads and resource scopes can be handled incorrectly, and concurrent operations can leave dangling policy resources.

Combined managed identities are converted to None, subscription-level resource scopes are rejected, and non-atomic cross-map operations can violate the advertised referential-integrity and cascade guarantees.

Files Needing Attention: src/main/java/io/floci/az/services/policy/PolicyHandler.java, src/main/java/io/floci/az/services/policy/PolicyPath.java, src/main/java/io/floci/az/services/policy/PolicyStore.java

Important Files Changed

Filename Overview
src/main/java/io/floci/az/services/policy/PolicyHandler.java Implements policy CRUD, filtering, identities, and integrity checks, but mishandles combined identities and accepts malformed unscoped references.
src/main/java/io/floci/az/services/policy/PolicyPath.java Parses policy extension-resource paths but rejects valid subscription-level resource scopes.
src/main/java/io/floci/az/services/policy/PolicyStore.java Stores policy resources in concurrent maps without atomic cross-resource integrity operations.
src/main/java/io/floci/az/core/AzureRoutingFilter.java Extends provider routing to tenant-rooted paths and preserves guarded policy dispatch.
src/test/java/io/floci/az/services/policy/PolicyTest.java Provides broad lifecycle and routing coverage but omits combined identities, subscription-level resource scopes, and concurrency races.
docs/services/policy.md Documents the new control-plane surface and intentional deviations comprehensively.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Client[Azure CLI / SDK / Terraform] --> Router[AzureRoutingFilter]
    Router -->|guarded Microsoft.Authorization policy path| Handler[PolicyHandler]
    Router -->|other ARM path| Generic[Generic ARM handler]
    Handler --> Path[PolicyPath scope and type parsing]
    Handler --> Definitions[Definitions store]
    Handler --> Sets[Set definitions store]
    Handler --> Assignments[Assignments store]
    Handler --> Exemptions[Exemptions store]
    Assignments -->|definition reference| Definitions
    Assignments -->|initiative reference| Sets
    Exemptions -->|assignment reference| Assignments
Loading

Reviews (1): Last reviewed commit: "feat(policy): add Azure Policy control p..." | Re-trigger Greptile

Comment on lines +453 to +479
if ("SystemAssigned".equalsIgnoreCase(type)) {
Map<?, ?> previous = existing != null && existing.get("identity") instanceof Map<?, ?> map ? map : Map.of();
String principalId = "SystemAssigned".equalsIgnoreCase(String.valueOf(previous.get("type")))
&& previous.get("principalId") instanceof String kept
? kept : UUID.randomUUID().toString();
identity.put("principalId", principalId);
identity.put("tenantId", config.services().entra().defaultTenantId());
identity.put("type", "SystemAssigned");
return identity;
}
if ("UserAssigned".equalsIgnoreCase(type)) {
identity.put("type", "UserAssigned");
Map<String, Object> userAssigned = new LinkedHashMap<>();
if (requested.get("userAssignedIdentities") instanceof Map<?, ?> ids) {
for (Object key : ids.keySet()) {
String resourceId = String.valueOf(key);
userAssigned.put(resourceId, identities.findByResourceId(resourceId)
.map(PolicyHandler::identityIds)
.orElseGet(() -> Map.<String, Object>of(
"principalId", TokenIssuer.deterministicGuid("policy-uai-principal:" + resourceId.toLowerCase(Locale.ROOT)),
"clientId", TokenIssuer.deterministicGuid("policy-uai-client:" + resourceId.toLowerCase(Locale.ROOT)))));
}
}
identity.put("userAssignedIdentities", userAssigned);
return identity;
}
identity.put("type", "None");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Combined identities become None

Policy assignments can use the combined SystemAssigned,UserAssigned identity type. This code only recognizes each type separately, so a combined payload falls through to None. The stored assignment then loses both identities, breaking clients that manage assignments with mixed identities.

Knowledge Base Used: Compatibility test suite

private static final String MARKER = "/providers/microsoft.authorization/";
private static final Pattern SUBSCRIPTION = Pattern.compile("subscriptions/[^/]+");
private static final Pattern RESOURCE_GROUP = Pattern.compile("subscriptions/[^/]+/resourcegroups/[^/]+");
private static final Pattern RESOURCE = Pattern.compile("subscriptions/[^/]+/resourcegroups/[^/]+/providers/.+");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Valid resource scopes rejected

The RESOURCE pattern requires a resourceGroups segment. A policy assignment or exemption targeting a subscription-level resource such as /subscriptions/{sub}/providers/{namespace}/{type}/{name} is therefore classified as UNKNOWN and rejected with a 404, even though this feature promises support at arbitrary resource scope.

Comment on lines +25 to +28
private final Map<String, Map<String, Object>> definitions = new ConcurrentHashMap<>();
private final Map<String, Map<String, Object>> setDefinitions = new ConcurrentHashMap<>();
private final Map<String, Map<String, Object>> assignments = new ConcurrentHashMap<>();
private final Map<String, Map<String, Object>> exemptions = new ConcurrentHashMap<>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Integrity operations are non-atomic

Referential-integrity checks and mutations span separate concurrent maps without one atomic operation. For example, exemption creation can confirm that an assignment exists, race with assignment deletion after exemption cleanup has run, and then insert an orphan exemption. An assignment can similarly validate a definition just before another request deletes it. Concurrent requests can therefore violate the promised reference and cascade guarantees.

Knowledge Base Used: Core routing and authentication

Comment on lines +626 to +629
private static boolean isScopedId(String id) {
String lower = id.toLowerCase(Locale.ROOT);
return lower.contains("/subscriptions/") || lower.contains("/managementgroups/");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Malformed references bypass validation

Every reference ID without /subscriptions/ or /managementGroups/ is treated as a built-in and bypasses lookup. Relative IDs such as policyDefinitions/missing and unrelated provider IDs such as /providers/Other.Provider/policyDefinitions/missing can therefore create permanently dangling assignments or initiatives. Restrict this bypass to canonical tenant-rooted Microsoft.Authorization IDs.

@hectorvent hectorvent added feature arm Azure Resource Manager (ARM) labels Sep 7, 2026
@hectorvent

Copy link
Copy Markdown
Contributor

Thank you, this is a well-scoped control plane and the description is a model: CLI cassettes named per claim, unverified codes listed, deviations on the docs page. Handler-owned routing, Resettable, constructor injection and the Managed Identity store pattern are all followed.

(blocking) Your own az policy exemption create case fails in CI: the CLI exits with status 3 (its not-found class) while the other seventeen policy cases pass. The suite image is mcr.microsoft.com/azure-cli:latest, whose generated exemption command PUTs /{scope}/providers/Microsoft.Authorization/policyExemptions/{name} at api-version 2026-01-01-preview with the scope built from -g, so an older local CLI may not show it. Reproducing with that image and capturing stderr should point at the assignment id lookup or the newer body shape.

On Greptile: the 2025-03-01 assignment identity enum is SystemAssigned, UserAssigned, None with no combined type (openapi.json#L4275), so None is correct; the documented resource scope includes resourceGroups; the non-atomic maps are the pattern every ARM store here uses.

(follow-up, separate PR) Narrow the built-in bypass to /providers/Microsoft.Authorization/ ids, and add a Java PolicyClient test beside the raw-REST one.

That is the only blocker.

@hectorvent

Copy link
Copy Markdown
Contributor

Two things since my review. A rebase is now owed, and the cause is my merge batch: #268 landed and both branches add tests at the end of AzureRoutingFilterTest, so main conflicts there. And the exemption case above still stands. Rebasing onto main first also gives you the current suite to reproduce it against.

@hectorvent hectorvent added the policy Azure Policy label Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arm Azure Resource Manager (ARM) feature policy Azure Policy waiting-contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants