Lab 01 — RBAC & Azure Policy Evaluation Engine

Phase: 04 — RBAC & Azure Policy Evaluation Difficulty: ⭐⭐⭐☆☆ (the matcher is fiddly; the evaluation order is the principal bit) Time: 4–5 hours

Every Azure write passes through two authorization gates before a resource provider ever sees it: RBAC decides may this identity do this action on this scope? and Azure Policy decides is the resulting resource allowed to exist? Both are deny-biased, both evaluate against a scope hierarchy, and the most expensive bugs in a regulated Azure estate live in the evaluation order of each. This lab makes you build both machines — the wildcard Actions/NotActions matcher, scope containment, the deny-assignment override, and the Policy condition-tree evaluator — so "why can't this service principal read the blob?" stops being a portal mystery and becomes an algorithm you can run in your head.

What you build

  • action_matches(pattern, action) — the heart of RBAC: case-insensitive matching of an action against a wildcard pattern where * spans one-or-more path segments (and within a segment). Microsoft.Storage/*/read matches Microsoft.Storage/storageAccounts/read; */read matches any read; * matches everything. The literal . in Microsoft.Storage must not behave like a regex ..
  • RoleDefinition + role_permits(role, action, is_data=False) — effective perms = union(Actions) − union(NotActions), with a separate DataActions/NotDataActions pair for the data plane. This is the mechanism behind "an Owner cannot read a blob."
  • scope_contains(assignment_scope, resource_id) — segment-aware ARM-id containment that knows …/resourceGroups/rg contains …/rg/providers/… but not …/rg2.
  • RoleAssignment, DenyAssignment, can_perform(...) — the full decision: deny assignments win, then default-deny unless some in-scope role assignment permits the action on the requested plane.
  • evaluate_policy(resource, policy) + compliance_summary(...) — walk a policy's if condition tree (field/equals/notEquals/in/exists/like under allOf/anyOf/not) with dotted-alias field lookups, and return the then.effect on a match (else "compliant"); roll many resources up into an effect-count.

Key concepts

ConceptWhat to understand
Role assignment = (principal, role, scope)the atomic RBAC fact; all three must line up
union(Actions) − union(NotActions)NotActions subtract from this role; they are not a deny
Control plane vs data planeActions manage the resource; DataActions govern data inside it — different sets
Owner ≠ Blob Data ReaderOwner has Actions=["*"] but no DataActions → can't read a blob
Additive + inheritedassignments add up and flow down MG → Sub → RG → resource
Deny always winsa deny assignment overrides every role assignment; evaluated first
Default denyno matching allow → False; least privilege is the floor
* is multi-segmentMicrosoft.Storage/*/read and */read both rely on * crossing /
The rg vs rg2 trapscope containment is by segments, not raw startswith
Policy effectsdeny blocks the write; audit/append/modify/*IfNotExists do other things
Condition treeallOf/anyOf/not + leaf operators over aliases (dotted resource paths)

Files

FilePurpose
lab.pyskeleton with # TODO markers — your implementation
solution.pycomplete reference; python solution.py runs three worked demos
test_lab.pythe proof — run it red, make it green (43 tests)
requirements.txtpytest only (pure stdlib otherwise)

Run

pip install -r requirements.txt
pytest test_lab.py -v                       # against your lab.py
LAB_MODULE=solution pytest test_lab.py -v   # against the reference
python solution.py                          # the three worked demos

Success criteria

  • All 43 tests pass against your implementation (and LAB_MODULE=solution).
  • You can explain why action_matches("Microsoft.Storage/*/read", "Microsoft.Compute/…") is False but action_matches("*/read", …) is True — i.e. what the * is and isn't allowed to swallow.
  • You can explain why test_owner_actions_star_but_no_data_actions proves the control-plane/data-plane split, and what role you add to actually read the blob.
  • You can explain why scope_contains("/subscriptions/s/resourceGroups/rg", ".../rg2/…") must be False even though "…/rg2/…".startswith("…/rg") is True.
  • You can explain why test_deny_assignment_wins_over_role returns False even though the principal is Owner at the subscription — the order of evaluation.
  • You can explain why exists: True must be True for a present-but-False field, and how the missing-key sentinel makes that work.
  • Given a resource JSON and a policy rule, you can hand-evaluate the effect in 60 seconds.

How this maps to real Azure

The miniatureThe production mechanismWhere to verify it
action_matchesAzure's Actions/NotActions are exactly these slash-delimited operation strings with * wildcards; the matcher ARM runs is the same shape.az provider operation show --namespace Microsoft.Storage
RoleDefinition / role_permitsA built-in or custom role definition: Actions, NotActions, DataActions, NotDataActions, AssignableScopes. Owner=["*"], Reader=["*/read"], Contributor=* minus Microsoft.Authorization/*.az role definition list --name Owner
RoleAssignmentThe (principalId, roleDefinitionId, scope) triple created by az role assignment create; additive and inherited down the hierarchy.az role assignment list --scope <id> --include-inherited
scope_containsAn assignment at MG/Sub/RG applies to all descendants; the scope string is an ARM resource ID prefix.az role assignment list --scope <rgId> shows inherited rows
DenyAssignment / deny-winsDeny assignments (created by Blueprints, Deployment Stacks denySettings, managed apps) override role assignments unconditionally — you cannot author them with a role.az role assignment list --include-inherited (deny shown by REST Microsoft.Authorization/denyAssignments)
Control vs data planeOwner manages the storage account but needs Storage Blob Data Reader/…Owner to touch blob data; same split for Key Vault data actions, Service Bus, etc.az role definition list --name "Storage Blob Data Reader" — note DataActions
evaluate_policyAzure Policy's policyRule (if/then), condition operators, aliases (Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly), and effects (deny/audit/append/modify/auditIfNotExists/deployIfNotExists/disabled).az policy definition show -n <builtin>
compliance_summaryThe Policy compliance state roll-up: counts of compliant/non-compliant resources per assignment from the periodic scan.az policy state summarize

Limits of the miniature (say these in the interview): real RBAC keys principals by object ID, has ABAC conditions on a role assignment (e.g. tag/path constraints on blob actions) we don't model, supports principal types (user/group/SP/MI) and group nesting, and resolves role definition IDs not names. Real Policy uses aliases (a curated map from a property to its API path, with [*] array aliases) rather than raw dotted JSON paths, evaluates count/value/field in templates, has initiatives (policy sets) and effect parameters, and runs deny/modify/append at request time but audit*/deployIfNotExists on the async compliance scan (with a managed identity for remediation). The evaluation order and the deny-bias, though, are faithful.

Extensions (build these on your own subscription)

  • Add ABAC conditions: extend RoleAssignment with an optional condition expression (@Resource[...] predicate) and short-circuit can_perform when it fails — the modern way to scope Storage Blob Data * to a path prefix.
  • Add group membership: let a principal belong to groups and let assignments target a group; resolve transitively (mind nesting and the "deny still wins" rule).
  • Add policy aliases: a {alias: json_path} map so evaluate_policy accepts the real alias Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly and resolves it to properties.supportsHttpsTrafficOnly. Then add an append/modify effect that returns the mutated resource, not just the effect string.
  • Add an initiative (policySet): evaluate a list of policies, and define effect precedence so a deny in any member dominates an audit in another.
  • Wire it to real Azure: create a custom role with az role definition create, assign it, and confirm a denied action fails with AuthorizationFailed; then write the matching deny-effect policy and watch the deployment get blocked at create time.

Interview / resume

  • Talking points: "Why can an Owner not read a blob, and what do you grant instead?" / "A service principal is Contributor at the subscription but a deploy fails with RequestDisallowedByPolicy — walk me through both authorization gates." / "Explain why a deny assignment can't be created by a role, and when you'd use one." / "Order the Policy effects and tell me which fire at request time vs the compliance scan." / "How does an RBAC assignment at a management group reach a single blob?"
  • Resume bullet: Built an offline Azure authorization engine modeling RBAC (Actions/NotActions wildcard matching, control-vs-data-plane permission sets, scope-hierarchy inheritance, and deny-assignment override) and an Azure Policy evaluator (nested allOf/anyOf/not condition trees, alias field lookups, and effect resolution with compliance roll-up).