← Back to Remy
AgentRemy

Skill · Version 1.0.0

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 helps

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

Details…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.
  • 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 (...) or account.owner_id = actor.id), apply that predicate directly in the SQL WHERE clause before COUNT(*), SUM(...), pagination, or CSV streaming.
  • 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 in approved state, 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 with SELECT ... FOR UPDATE inside the transaction before evaluating the policy).

Included files 1

  • SKILL.md