Skip to main content

scaffold-authz

Add CASL authorization to a feature: an isomorphic <feature>.ability.ts rule set the server and React share, server-side enforcement via attachAbility/authorize and per-record subject() checks, and <Can>/useAbility UI gating — built on the domain-free base in src/shared/authz/.

Datos de origen

Repositorio
ThibautBaissac/react-express-ai-kit
Última actividad en el origen
9 de junio de 2026 a las 08:02
Idioma detectado de SKILL.md
inglés
Estrellas
2
Forks
0

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
scaffold-authz
description
Add CASL authorization to a feature: an isomorphic <feature>.ability.ts rule set the server and React share, server-side enforcement via attachAbility/authorize and per-record subject() checks, and <Can>/useAbility UI gating — built on the domain-free base in src/shared/authz/.
when_to_use
Use WHEN gating actions by role or ownership, protecting a route or service with permission checks, hiding/disabling UI by permission, or adding per-record access rules — whether the user asks directly or an implementation agent is realizing a matching To-Do item from a `tasks/task-N.md` plan. Do NOT use for authentication (login/sessions/who-the-user-is — use /scaffold-auth), and do NOT define the API success/error shapes here (use /add-api-contract and /api-error-handling).
argument-hint
[feature]
arguments
["feature"]
model
sonnet
# Scaffold CASL authorization for a feature Authorization answers "what may this principal do?". Authentication (who they are, `req.user`) is a prerequisite owned by `/scaffold-auth`. The reusable base already exists in `src/shared/authz/` — this skill wires a feature to it. Follow `.claude/rules/authorization.md` throughout. ## Step 0 — Confirm prerequisites - `@casl/ability` and `@casl/react` are installed (the base depends on them). - A server-derived principal is available: auth middleware sets `req.user` (`{ id, roles? }`-compatible). If not, run `/scaffold-auth` first. - The base files exist: `src/shared/authz/{ability.ts,authorize.ts,ability-context.tsx}`. ## Step 1 — Write the feature's ability rules Create `src/features/$feature/$feature.ability.ts` exporting an `AbilityContributor`. It is environment-neutral (no Express/React/DB imports) so the server and the browser import the identical rules. ```ts import type { AbilityContributor } from "../../shared/authz/ability"; export const ${feature}Abilities: AbilityContributor = (can, _cannot, user) => { can("read", "${Feature}"); if (!user) return; can("create", "${Feature}"); can("update", "${Feature}", { ownerId: user.id }); can("delete", "${Feature}", { ownerId: user.id }); if (user.roles?.includes("admin")) can("manage", "all"); }; ``` Encode real rules from the spec, not a generic CRUD stub. Use a condition (`{ ownerId: user.id }`) for ownership; omit it for type-level permissions. ## Step 2 — Enforce on the server At the API composition root (`src/apiApp.ts` / `src/server.ts`), mount `attachAbility` once after auth: ```ts import { attachAbility } from "./shared/authz/authorize"; import { ${feature}Abilities } from "./features/$feature/$feature.ability"; const contributors = [${feature}Abilities /* , …other features */]; app.use(attachAbility((req) => req.user ?? null, contributors)); ``` In feature route files, guard type-level actions: ```ts import { authorize } from "../../shared/authz/authorize"; router.post("/$feature", authorize("create", "${Feature}"), create${Feature}); ``` For per-record/ownership checks, pass `req.ability` into the service as an `AppAbility`, load the record there, then check a tagged subject. This is also where the 404-vs-403 existence-leakage decision lives (`/api-error-handling`): ```ts import { subject, type AppAbility } from "../../shared/authz/ability"; export async function update${Feature}({ ability, }: { ability: AppAbility; }) { const record = await repository.findById(id); if (!ability.can("update", subject("${Feature}", record))) { throw new ForbiddenError(); // or NotFoundError to hide existence } } ``` Pass the ability into the service (e.g. as an argument) rather than `req` — keep Express objects out of services. ## Step 3 — Gate the UI (UX only) Build the same ability on the client and hide what the API would reject. This is never a security boundary. ```tsx // once, near the app root — contributors is a stable module-level constant import { AbilityProvider } from "./shared/authz/ability-context"; const { data: user } = useCurrentUser(); <AbilityProvider user={user ?? null} contributors={contributors}>…</AbilityProvider> // at a call site import { Can } from "./shared/authz/ability-context"; <Can I="update" a="${Feature}"><EditButton /></Can> ``` ## Step 4 — Test the boundary Add colocated tests (`/write-tests`). Prove BOTH directions: - Allowed: owner/admin can do the action. - Denied: anonymous and non-owner are rejected (`403`, or `404` where existence is hidden). A green deny test is what proves the rule actually bites. ```bash source "${CLAUDE_PROJECT_DIR}/.claude/lib/detect-toolchain.sh" run_typecheck run_tests src/features/$feature ``` ## Checklist - [ ] `$feature.ability.ts` exports an `AbilityContributor`; no Express/React/DB imports. - [ ] Rules read the server-derived principal, never a client-supplied id. - [ ] Server enforces via `authorize` / `req.ability.can(subject(...))`. - [ ] `<Can>`/`useAbility` only hide UI; the server still authorizes. - [ ] Per-record checks tag the record with `subject(type, record)`. - [ ] Deny-case tests exist and pass (403/404 per the existence-leakage decision).
Ver en GitHub