Enforce server-side policies
Applies the same authorization rules to direct actions, queries, exports and background work, with denied access when context is missing.
server-side-policy-enforcement
See how it helpsWhen is it useful?
Use when hidden UI controls are insufficient to protect records and operations.
What does it help you do?
- Separates policy decisions from endpoints that enforce them.
- Scopes data before pagination or aggregation so totals cannot leak inaccessible records.
- Checks individual bulk targets and derives interface capability hints from server rules.
Try asking this
Implement quote approval and export using shared server policies, including mixed-permission bulk requests and missing workspace context. Mix allowed and forbidden quote IDs in one bulk action. Apply the documented partial-failure contract and ensure exported totals include only authorized records.
Detailed documentation
Read the skill’s original instructions, examples and checks in their authored language.
Show less
Server-Side Policy Enforcement
When to Use
Use when implementing or auditing authorization checks in API endpoints, GraphQL resolvers, RPC handlers, background jobs, data exports, and UI capability flags.
Core Principles (Saltzer & Schroeder)
1. Complete Mediation & Fail-Closed Defaults
- UI Hiding Is UX, Never Security: Hiding an "Approve" or "Delete" button in the frontend only prevents accidental clicks. Every API mutation, query, export, and webhook/worker trigger must enforce the policy on the server.
- Separate Policy Decision Point (PDP) from Policy Enforcement Point (PEP):
- Keep business policy rules in dedicated, pure policy modules/gates (
QuotePolicy.canApprove(actor, quote)), while controllers/handlers act as enforcement points (authorizeOrThrow(...)). - Derive UI capability hints (
permissions: { canEdit: true, canApprove: false, reason: 'self_approval_forbidden' }) from the exact same server-side policy function so frontend button states and backend enforcement never drift apart.
- Keep business policy rules in dedicated, pure policy modules/gates (
- Fail-Closed on Missing Context: If subject, tenant membership, or resource state is missing or ambiguous, deny access (
DENY) by default.
2. Query-Level Scoping for Lists, Search, Aggregations & Exports
Checking permissions only on GET /items/:id leaves GET /items, search endpoints, dashboard totals, and CSV/PDF exports wide open!
- Pair every record-level policy (
canView(actor, record)) with a Query Scope Builder (scopeReadable(actor, queryBuilder)):- Example: If a sales rep may only see accounts in their assigned region (
account.region_id IN (...)oraccount.owner_id = actor.id), apply that predicate directly in the SQLWHEREclause beforeCOUNT(*),SUM(...), pagination, or CSV streaming.
- Example: If a sales rep may only see accounts in their assigned region (
- Never fetch all rows into memory and filter them in application code after pagination (which breaks page sizes and leaks total counts).
3. Per-Item Enforcement on Bulk & Batch Operations
When an endpoint accepts an array of IDs (POST /quotes/bulk-archive { ids: [...] }):
- Load and evaluate authorization for every individual target resource against its current database state and tenant ownership.
- Never check only the first item or check only a coarse role (
can('quote:archive')) while allowing the ID list to include records owned by another department, locked inapprovedstate, or belonging to another tenant. - Define atomic vs. partial batch semantics explicitly (reject the entire batch inside a transaction if any item is unauthorized, reporting which item failed without leaking data from unreadable IDs).
4. Eliminate TOCTOU Between Authorization Check and State Mutation
If you check if (quote.status === 'pending_approval') in memory and then run an unscoped UPDATE quotes SET status = 'approved' WHERE id = $1, a concurrent edit can modify quote.amount between the check and the write!
- Bind the authorization preconditions into the atomic SQL
UPDATE ... WHERE id = $1 AND tenant_id = $2 AND status = 'pending_approval' AND version = $3(or lock the row withSELECT ... FOR UPDATEinside the transaction before evaluating the policy).
Included files 1
SKILL.md
