Phase 04 — RBAC & Azure Policy Evaluation

Difficulty: ⭐⭐⭐☆☆ (the matchers) → ⭐⭐⭐⭐⭐ (the judgment about least privilege, deny, and effect choice) Estimated Time: 1 week (12–18 hours) Prerequisites: Phase 00 (control plane vs data plane, the resource-ID shape and its scope levels), Phase 03 (Entra ID — the token that arrives carries the principal whose access we evaluate here). No live Azure subscription required for the lab.


Why This Phase Exists

Phase 03 ended with a validated JWT: ARM now knows who is calling (the oid/sub claim) and that the token is genuine. This phase answers the very next question — the one the entire security posture of an Azure estate turns on: is this caller allowed to do this, to this resource?

There are two gates, in series, and almost nobody can describe both precisely:

  1. Azure RBACauthorization. Given the principal, the action, and the resource's scope, gather every role assignment that applies, compute the effective permission set, and let any deny assignment win. This is the gate behind AuthorizationFailed.
  2. Azure Policygovernance. Even if RBAC says yes, Policy can deny the write (or append/modify/audit it) based on the shape of the resource — its SKU, region, encryption setting, tags. This is the gate behind RequestDisallowedByPolicy.

Here is the thing that separates a senior from a principal in an interview: a senior can assign the Owner role; a principal can explain why that Owner still can't read a blob (control plane vs data plane), why a Contributor can't grant access to anyone (NotActions on Microsoft.Authorization/*), why a deny assignment from a Deployment Stack overrides a subscription-Owner grant, and why a policy with effect: deny blocks a deployment that RBAC happily authorized. These are not trivia — they are the four shapes of the support ticket "I have permission but it still fails."

So this phase makes you build both evaluators — the wildcard Actions/NotActions matcher, scope containment, the deny-wins decision, and the Policy condition-tree evaluator. Once you've written the algorithm, "effective permissions" stops being a portal blade you squint at and becomes a function you can run in your head during an incident. This is the keystone for Phase 05 (landing zones are RBAC + Policy inherited down a management-group tree at scale) and underpins every later phase that touches identity.

What "Principal-Level" Means Here

A senior assigns roles and writes policies. A principal understands the evaluation machinery well enough to:

  • Predict an authorization decision from the assignments without the portal — by computing union(Actions) − union(NotActions), checking scope containment, and applying deny-wins, in that order.
  • Debug the four "I have permission but it fails" tickets: (a) control-plane grant but data-plane action (Owner reading a blob), (b) a NotActions subtraction (Contributor granting access), (c) a deny assignment overriding the grant, (d) RBAC allows but Policy denies the resource shape.
  • Design least privilege: pick the narrowest built-in role that covers the action, reach for a custom role only when no built-in fits, and prefer scope narrowing (assign at the RG, not the subscription) over broader roles.
  • Choose the right Policy effect and know when it runs: deny/append/modify at the request, audit/auditIfNotExists/deployIfNotExists on the compliance scan — and why deployIfNotExists needs a managed identity.
  • Reason about blast radius of an assignment: a role at a management group reaches every subscription, RG, and resource beneath it; a deny at an RG stops there.

Concepts

  • The RBAC model — a role assignment is the triple (principal, role definition, scope). RBAC is allow-based (there is no "deny by role"), additive (assignments union together), and inherited down the hierarchy management group → subscription → resource group → resource. You grant by adding an assignment and revoke by removing one (or by a deny assignment).
  • Role-definition internalsActions (allowed control-plane operations), NotActions (subtracted from this role's own Actions), DataActions / NotDataActions (the same pair for the data plane), and AssignableScopes (where the role may be assigned). Effective perms = union(Actions) − union(NotActions). Wildcards: * matches any run of characters including / (so Microsoft.Storage/*/read spans segments).
  • Control plane vs data planeActions manage the resource (create/configure a storage account); DataActions govern the data inside it (read a blob). They are separate sets, which is why Owner (Actions=["*"]) cannot read a blob — it has no DataActions; you add Storage Blob Data Reader. The same split exists for Key Vault, Service Bus, Cosmos DB, and more.
  • Deny assignments — an explicit block on actions at a scope that overrides role assignments. Deny always wins, and it is evaluated first. You cannot create deny assignments with a role; they come from Blueprints, Deployment Stacks (denySettings), and managed applications — which is exactly why they're the system's last word for protecting platform-managed resources.
  • Scope hierarchy & inheritance — a scope is an ARM resource ID (or a prefix). An assignment at a higher scope applies to all descendants. A resource is "in scope of" an assignment iff its ID is the scope or sits below it by whole path segments — the reason …/resourceGroups/rg contains …/rg/providers/… but not …/rg2.
  • Azure Policy — a policyRule of if (a condition tree) and then.effect. Leaf conditions test a field/alias (equals/notEquals/in/exists/like); logical nodes combine them (allOf/anyOf/not). Effects: disabled, audit, auditIfNotExists, append, modify, deployIfNotExists, deny — and deny dominates. deny/append/modify run at create/update; audit* and deployIfNotExists run on the compliance scan.
  • Aliases — Policy can't read raw JSON paths; it reads aliases that map a friendly name to the resource property at a given API version (e.g. Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly). The lab models the dotted lookup; the extension wires real aliases.

Labs

Lab 01 — RBAC & Azure Policy Evaluation Engine (flagship, implemented)

FieldValue
GoalBuild both authorization machines: a wildcard action_matches, a RoleDefinition with role_permits (union(Actions) − union(NotActions) and the data-plane pair), segment-aware scope_contains, the full can_perform decision (deny wins, then default-deny over in-scope role assignments), and an evaluate_policy that walks a nested allOf/anyOf/not condition tree with dotted-alias field lookups and returns the effect (plus a compliance_summary roll-up)
Concepts(principal, role, scope) triple; NotActions as subtraction; control-vs-data-plane permission sets; scope-hierarchy inheritance and the rg-vs-rg2 trap; deny-assignment override and evaluation order; Policy condition trees, field operators, effects, and the exists absent-vs-falsey distinction
Steps1. action_matches (*.*, escape literals, case-insensitive, anchored); 2. RoleDefinition/role_permits with the data-plane pair; 3. scope_contains (segment-aware prefix); 4. RoleAssignment/DenyAssignment/can_perform (deny-wins → default-deny); 5. evaluate_policy recursive condition tree + dotted lookup; 6. compliance_summary
How to Testpytest test_lab.py -v — 43 tests covering wildcard boundaries (*/read, mid-pattern *, exact, literal-dot, case), NotActions subtraction, the Owner-can't-read-a-blob data-plane split, scope containment incl. the rg/rg2 trap, deny-wins / out-of-scope deny / wrong-principal deny, default-deny + unknown-role, and Policy deny/compliant/nested-allOf-anyOf-not/exists/in/like/dotted-alias + the bad-shape guards
Talking Points"Why can't an Owner read a blob?" / "Walk me through both authorization gates for one PUT." / "Why does a deny assignment beat a subscription-Owner grant?" / "Order the Policy effects and tell me which fire at request time." / "How does a role at a management group reach a single blob?"
Resume bulletBuilt an offline Azure authorization engine modeling RBAC (wildcard Actions/NotActions matching, control-vs-data-plane permission sets, scope-hierarchy inheritance, deny-assignment override) and an Azure Policy evaluator (nested condition trees, alias lookups, effect resolution, compliance roll-up)

→ Lab folder: lab-01-rbac-policy-engine/

Integrated-Scenario Suggestions (carried through the whole track)

These compound into Phase 05 (landing zones) and the Phase 15 capstone. Keep them in mind as you build — each plugs the evaluator into a larger governance story:

  1. Least-privilege CI/CD identity — a deployment service principal that is Contributor on one RG (not subscription Owner), plus the narrowest data-plane roles for the secrets/blobs it touches. The exercise is removing every action it never uses and proving the deploy still works. (→ P08, P12)
  2. Break-glass and deny protection — a Deployment-Stack denySettings deny assignment that blocks delete on production resources even for Owner, with a documented break-glass account that is not in the deny scope. (→ P05)
  3. Policy-as-code guardrails — an initiative (policy set) that denies non-HTTPS storage, denies public network access, appends required tags, and audits resources missing a cost-center tag — assigned at a management group so every subscription inherits it. (→ P05)
  4. Data-plane vs control-plane separation of duties — platform admins get Owner to manage a Key Vault but not read its secrets; app identities get Key Vault Secrets User (a data role) but cannot change the vault's firewall. (→ P12)
  5. Effective-access audit — given a principal and a resource, compute the effective permissions across all inherited assignments and deny assignments — the offline version of the portal's "Check access" / az role assignment list --include-inherited. (→ P13)

Guides in This Phase

Key Takeaways

  • Authorization is two gates in series. RBAC (may this identity?) then Policy (is this resource shape allowed?). The four "I have permission but it fails" tickets each map to one specific mechanism — name it and you've solved it.
  • A role assignment is (principal, role, scope) — additive, inherited down the hierarchy, and allow-based. You revoke by removing or by a deny assignment, never by a "deny role" (there is none).
  • Effective perms = union(Actions) − union(NotActions), and the data plane is a separate set (DataActions). OwnerStorage Blob Data Reader.
  • Deny always wins and is evaluated first. A deny assignment from a stack/blueprint overrides even subscription Owner — that's its whole purpose.
  • Pick the narrowest role at the tightest scope. Least privilege is the default; a custom role is a last resort, and broad-scope assignments are a blast-radius decision.
  • Choose the Policy effect deliberatelydeny/append/modify shape the request; audit*/deployIfNotExists report and remediate on the scan; deny dominates them all.

Deliverables Checklist

  • Lab 01 implemented; all 43 tests pass against solution.py and your lab.py
  • You can state the RBAC evaluation order (deny first, then allow, default deny) from memory and explain why deny is evaluated first
  • You can explain why Owner can't read a blob and which role fixes it
  • You can name the four built-in roles (Owner/Contributor/Reader/User Access Administrator) and the one operation Contributor lacks
  • You can list the seven Policy effects, order them by precedence, and say which run at request time vs the compliance scan
  • Given assignments + a resource ID, you can compute the decision on paper