Policy as code
A policy that lives in a prompt is a suggestion. Policy as code puts the decision in a versioned, tested function that the model cannot edit and the logs can explain.
Decision point, enforcement point
A policy has two halves. The decision point answers the question: may this user, in this session, run this tool with these arguments? The enforcement point is the code that refuses to proceed when the answer is no. If the same function both decides and acts, a bug in either becomes a bypass.
The model is neither. It proposes a call; the decision point evaluates it against rules the model cannot read or edit; the enforcement point runs the tool only on an allow. That is the same shape as an operating system’s permission check, and for the same reason.
The decision point should be pure: same inputs, same answer, no network and no model. That is what makes it testable, and it is why the approval service is an input to the decision rather than a call inside it. A decision that depends on a live service is a decision you cannot replay.
Declarative rules
Write the policy as data, not as a chain of ifs scattered through the handler. Roles, tool names, argument limits, approval requirements and the default belong in one file that is versioned, reviewed and tested. A declarative policy can be diffed in a pull request, which is the point: a permission change should look like a permission change.
Keep the rules small and composable. A rule that says support may refund up to 100 is testable; a rule that says be careful with refunds is not. Every rule needs a reason code, because the log has to say which one fired, and an operator has to be able to find it.
Version the policy with the code, not in a wiki. A policy file in the repository is reviewed with the change that needs it, and the deploy is the moment the new rule takes effect. A wiki page has no deploy, so nobody knows which version is live.
A policy engine you do not have to write
import casbin
enforcer = casbin.Enforcer("model.conf", "policy.csv")
print(enforcer.enforce("support", "notes", "read"))
print(enforcer.enforce("support", "refunds", "write"))
print(enforcer.enforce("admin", "refunds", "write"))A decision table with reason codes
Run the cases and find the two that need approval rather than an outright no. Then add a tenant check so a user may only read notes in their own tenant.
Fail closed and explain
The default is deny. An unknown tool, an unknown role, a missing policy file or an unreachable approval service all resolve to no. Failing open turns an outage into an incident, and the availability argument is never as expensive as the incident. Read-only tools can be the exception, and even that should be a named rule rather than a default.
Every decision returns a reason code and the policy version. The code is for the log and the operator, not for the end user. When a decision is disputed, the record says which rule fired on which version, and the fix is a policy change with a test rather than a prompt edit.
Log the arguments that mattered, not the whole request. A decision record that contains a password or a card number has recreated the problem the policy was meant to prevent, and it has done so with a longer retention period.
The model must not be able to call the policy engine to change it. Read access to a decision is not write access to a rule.
Testing policy
A policy is code, so test it like code: a table of user, tool, arguments and expected decision, with the reason code asserted too. Add a property test for the invariant that no role outside admin can ever reach an amount above the cap, whatever the arguments contain.
Roll out changes in shadow mode first: evaluate the new policy on live traffic, log what it would have decided, and compare. A policy that would have blocked a third of legitimate requests is a product change, and the shadow run is how you find out before the deploy.
A table test is the minimum; a property test is the one that catches the case nobody thought of. The property does not need a theorem, just a sentence such as no support user may exceed the cap, checked over generated inputs and a range of amounts.
Diff two policy versions
Compare the decisions under v1 and v2. Then add a test that asserts support can never exceed 50 under v2, and one that asserts an unknown user is denied.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What is the difference between the decision point and the enforcement point?
