APS-101Recommended
Decide access in one policy module
Answer every access question through one policy function or service, the policy decision point. Every route asks it "may this user do this action on this record?" and never decides alone.
How to do it
The route is the enforcement point. It loads the record, then calls one function such as can(user, 'refund', order) and refuses on a no. Keep all rules in that module, as code or as policy files for an engine such as Open Policy Agent, Cedar or Casbin. Test each role and action pair there, once, instead of in every route. Check ownership and tenant inside the policy, so a route cannot forget them. Log every refusal with the user, action and record ID. Refuse when the policy itself fails, as Deny by default and when a check fails says. Check ownership as check ownership on every record says. Roles and permissions themselves are designed as Accounts and identity describes, and multi-tenant data as the tenant isolation rules say.
Examples
JavaScript example
const POLICY = {
'order:read': (user, order) => order.customerId === user.id || user.roles.includes('support'),
'order:refund': (user, order) => user.roles.includes('support') && order.tenantId === user.tenantId,
};
export const can = (user, action, record) => Boolean(POLICY[action]?.(user, record));
app.post('/orders/:id/refund', async (req, res) => {
const order = await orders.find(req.params.id);
if (!order || !can(req.user, 'order:refund', order)) return res.sendStatus(404);
res.json(await refunds.create(order));
});
Python example
POLICY = {
"order:read": lambda user, order: order.customer_id == user.id or "support" in user.roles,
"order:refund": lambda user, order: "support" in user.roles and order.tenant_id == user.tenant_id,
}
def can(user, action, record) -> bool:
rule = POLICY.get(action)
return bool(rule and rule(user, record))
How to check
A test lists every route the server answers. It fails on a non-public route whose handler never calls the policy function. The policy tests cover every role and action pair in the permission table. A test that removes the ownership check makes at least one of them fail.
Sources
Related rules
- APS-20 Deny by default and when a check fails in Application security
- ACC-77 Check ownership on every record in Accounts and identity
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/application-security/access-control/policy-decision-point">Agent Wranglers application security playbook</a>. In code you write, add a comment next to it with the same link.