Lab: build a policy engine and break it
Implement deny-by-default with priorities in fifteen lines, then watch a deny rule silently do nothing.
The previous lessons said that deny-by-default fails closed, and that priority rather than effect resolves overlapping rules. Both are easy to nod along to and easy to get wrong under deadline, so build the engine and let it bite you.
Each step appends to the same script, so every run reprints the sections before it. The expected output shown under each step is the new part; scroll to the bottom of your terminal to find it.
- 1
Write the engine. It is a matcher, a conflict rule, and a default, which is genuinely all a policy engine is at its core.
bashcat > /tmp/policy.py <<'EOF' import fnmatch POLICIES = [ {"name": "broad-read", "path": "app/**", "effect": "allow", "priority": 10}, {"name": "no-billing", "path": "app/billing/**", "effect": "deny", "priority": 5}, ] def decide(path, policies): matches = [p for p in policies if fnmatch.fnmatch(path, p["path"].replace("**", "*"))] if not matches: return "DENY", "no policy matched (deny-by-default)" winner = max(matches, key=lambda p: p["priority"]) return winner["effect"].upper(), f'{winner["name"]} (priority {winner["priority"]})' for path in ["app/search/key", "app/billing/stripe", "other/thing"]: verdict, why = decide(path, POLICIES) print(f"{path:24} {verdict:6} <- {why}") EOF python3 /tmp/policy.py - 2
Read the middle line carefully. The deny rule matched, and lost.
textapp/search/key ALLOW <- broad-read (priority 10) app/billing/stripe ALLOW <- broad-read (priority 10) other/thing DENY <- no policy matched (deny-by-default) - 3
Now raise the deny above the allow and run it again.
bashcat >> /tmp/policy.py <<'EOF' print("\n--- raise the deny above the allow ---") FIXED = [dict(p, priority=100) if p["name"] == "no-billing" else p for p in POLICIES] for path in ["app/search/key", "app/billing/stripe"]: verdict, why = decide(path, FIXED) print(f"{path:24} {verdict:6} <- {why}") EOF python3 /tmp/policy.py - 4
Same rules, same paths, different outcome. The script re-runs both cases, so you get the before and after together.
textapp/search/key ALLOW <- broad-read (priority 10) app/billing/stripe ALLOW <- broad-read (priority 10) other/thing DENY <- no policy matched (deny-by-default) --- raise the deny above the allow --- app/search/key ALLOW <- broad-read (priority 10) app/billing/stripe DENY <- no-billing (priority 100) - 5
Clean up.
bashrm /tmp/policy.py
Two things happened, and the second is the one that ships to production.
- The third path was denied by nothing at all. No rule mentioned other/thing, and the absence of a rule refused it. That is deny-by-default, and it is why an oversight becomes a support ticket rather than a breach.
- The billing secret was allowed despite an explicit deny. The rule matched, it was correct, and it lost the comparison. Nothing errored, nothing warned, and a reviewer skimming the policy list sees a deny and assumes billing is protected.
This is the single most transferable bug in this course. Some engines resolve conflicts by deny-always-wins and some by priority, and a deny rule written against the wrong assumption is not a weak control, it is no control at all while looking like one.
Before you write a deny rule in any system, find out which model it uses. Then test the rule against a path it should refuse, because a silently inert policy looks identical to a working one in every list, diff and review.
The 1Claw policy engine resolves by priority, which is exactly the case this lab demonstrates. A deny needs a higher number than the allow it is carving into.
Check your understanding
3 questionsWhy was other/thing denied when no rule mentioned it?
Why did the deny on app/billing/** have no effect in the first run?
What should you do before writing a deny rule in an unfamiliar system?