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/NotActionsmatcher, 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/*/readmatchesMicrosoft.Storage/storageAccounts/read;*/readmatches any read;*matches everything. The literal.inMicrosoft.Storagemust not behave like a regex..RoleDefinition+role_permits(role, action, is_data=False)— effective perms =union(Actions) − union(NotActions), with a separateDataActions/NotDataActionspair for the data plane. This is the mechanism behind "anOwnercannot read a blob."scope_contains(assignment_scope, resource_id)— segment-aware ARM-id containment that knows…/resourceGroups/rgcontains…/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'sifcondition tree (field/equals/notEquals/in/exists/likeunderallOf/anyOf/not) with dotted-alias field lookups, and return thethen.effecton a match (else"compliant"); roll many resources up into an effect-count.
Key concepts
| Concept | What 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 plane | Actions manage the resource; DataActions govern data inside it — different sets |
| Owner ≠ Blob Data Reader | Owner has Actions=["*"] but no DataActions → can't read a blob |
| Additive + inherited | assignments add up and flow down MG → Sub → RG → resource |
| Deny always wins | a deny assignment overrides every role assignment; evaluated first |
| Default deny | no matching allow → False; least privilege is the floor |
* is multi-segment | Microsoft.Storage/*/read and */read both rely on * crossing / |
The rg vs rg2 trap | scope containment is by segments, not raw startswith |
| Policy effects | deny blocks the write; audit/append/modify/*IfNotExists do other things |
| Condition tree | allOf/anyOf/not + leaf operators over aliases (dotted resource paths) |
Files
| File | Purpose |
|---|---|
lab.py | skeleton with # TODO markers — your implementation |
solution.py | complete reference; python solution.py runs three worked demos |
test_lab.py | the proof — run it red, make it green (43 tests) |
requirements.txt | pytest 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 butaction_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_actionsproves 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_rolereturnsFalseeven though the principal isOwnerat the subscription — the order of evaluation. - You can explain why
exists: Truemust beTruefor a present-but-Falsefield, 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 miniature | The production mechanism | Where to verify it |
|---|---|---|
action_matches | Azure'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_permits | A built-in or custom role definition: Actions, NotActions, DataActions, NotDataActions, AssignableScopes. Owner=["*"], Reader=["*/read"], Contributor=* minus Microsoft.Authorization/*. | az role definition list --name Owner |
RoleAssignment | The (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_contains | An 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-wins | Deny 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 plane | Owner 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_policy | Azure 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_summary | The 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
RoleAssignmentwith an optional condition expression (@Resource[...]predicate) and short-circuitcan_performwhen it fails — the modern way to scopeStorage 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 soevaluate_policyaccepts the real aliasMicrosoft.Storage/storageAccounts/supportsHttpsTrafficOnlyand resolves it toproperties.supportsHttpsTrafficOnly. Then add anappend/modifyeffect that returns the mutated resource, not just the effect string. - Add an initiative (
policySet): evaluate a list of policies, and define effect precedence so adenyin any member dominates anauditin another. - Wire it to real Azure: create a custom role with
az role definition create, assign it, and confirm a denied action fails withAuthorizationFailed; then write the matchingdeny-effect policy and watch the deployment get blocked at create time.
Interview / resume
- Talking points: "Why can an
Ownernot read a blob, and what do you grant instead?" / "A service principal isContributorat the subscription but a deploy fails withRequestDisallowedByPolicy— 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/NotActionswildcard matching, control-vs-data-plane permission sets, scope-hierarchy inheritance, and deny-assignment override) and an Azure Policy evaluator (nestedallOf/anyOf/notcondition trees, alias field lookups, and effect resolution with compliance roll-up).