feat(api): capability enforcement on writes
Add lib/api/cap.js: requireWrite(entity_type) maps HTTP method to action, runs canAct, and tags req.capTier as allow|suggest|deny→403. Mutating routes (pages, projects, tasks, refs, resources, source_docs) now check req.capTier and either run the repo (allow) or divert to pending_changes returning 202 (suggest). Owner and worker actors stay on the allow path. requireOwner helper added for Task 11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
31
lib/api/cap.js
Normal file
31
lib/api/cap.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { canAct } from '../auth/capability.js';
|
||||
import * as pendingChanges from '../db/repos/pending_changes.js';
|
||||
import { ForbiddenError } from './errors.js';
|
||||
|
||||
const METHOD_TO_ACTION = { POST: 'create', PATCH: 'update', PUT: 'update', DELETE: 'delete' };
|
||||
|
||||
export function requireWrite(entity_type) {
|
||||
return (req, _res, next) => {
|
||||
const action = METHOD_TO_ACTION[req.method] || 'update';
|
||||
const tier = canAct(req.actor, action, entity_type);
|
||||
if (tier === 'allow') { req.capTier = 'allow'; return next(); }
|
||||
if (tier === 'suggest') { req.capTier = 'suggest'; return next(); }
|
||||
return next(new ForbiddenError(`agent not permitted to ${action} ${entity_type}`));
|
||||
};
|
||||
}
|
||||
|
||||
export function requireOwner(req, _res, next) {
|
||||
if (req.actor?.kind !== 'user') {
|
||||
return next(new ForbiddenError('owner-only endpoint'));
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
export async function divertToPending(req, res, { entity_type, entity_id = null, action, payload, reason = null }) {
|
||||
const change = await pendingChanges.create({
|
||||
agent_id: req.actor.id,
|
||||
entity_type, entity_id, action, payload,
|
||||
reason: reason ?? req.headers['x-reason'] ?? null
|
||||
});
|
||||
res.status(202).json({ pending: true, change_id: change.id });
|
||||
}
|
||||
Reference in New Issue
Block a user