Skip to content
1Claw Academy
Curriculum/Security Foundations3 minBeginner · Lesson 12 of 19

Lab: build a policy engine and break it

Lab

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.

Tip

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. 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.

    bash
    cat > /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. 2

    Read the middle line carefully. The deny rule matched, and lost.

    text
    app/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. 3

    Now raise the deny above the allow and run it again.

    bash
    cat >> /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. 4

    Same rules, same paths, different outcome. The script re-runs both cases, so you get the before and after together.

    text
    app/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. 5

    Clean up.

    bash
    rm /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.
Watch out

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.

Tip

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 questions
1

Why was other/thing denied when no rule mentioned it?

2

Why did the deny on app/billing/** have no effect in the first run?

3

What should you do before writing a deny rule in an unfamiliar system?