add spine-validation owner runbook; lock privilege-escalation RLS test
docs/SPINE_VALIDATION.md: a precise owner runbook + checklist that provisions the shipped-but-inert spine (Resend, CRON_SECRET + both crontabs, per-space Stripe test mode + Billing Portal, the Supabase change-email template) and proves each path end to end (dues cycle, notifications, prefs/inbox, /me, door). Grounded in the actual config and route fail-closed codes; linked from the README docs table. integration/permission-escalation.test.ts: lock the additive permission model's core guard, previously untested -- only admin/board may write space_role_permissions, so a plain member cannot grant themselves a capability, and the WITH CHECK pins the space. No app code or migration; suite 626 unit / 54 integration green.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// The additive permission model's integrity rests on one RLS gate: only an
|
||||
// admin/board may WRITE space_role_permissions. If a plain member could insert
|
||||
// a grant, they could hand themselves any capability (settings.manage, etc.).
|
||||
// This locks that gate against real Postgres -- previously untested. Same
|
||||
// role-simulation harness as the other integration tests.
|
||||
import { describe, it, expect, afterAll } from 'vitest'
|
||||
import { dbReady, execAsUser, rowsAsUser, seedSpace, seedMember, dropSpace } from './db'
|
||||
|
||||
const d = describe.skipIf(!dbReady)
|
||||
|
||||
d('space_role_permissions write RLS (privilege-escalation guard)', () => {
|
||||
const spaces: string[] = []
|
||||
function space(): string {
|
||||
const { spaceId } = seedSpace()
|
||||
spaces.push(spaceId)
|
||||
return spaceId
|
||||
}
|
||||
afterAll(() => spaces.forEach(dropSpace))
|
||||
|
||||
it('a plain member cannot grant themselves a permission', () => {
|
||||
const sid = space()
|
||||
const member = seedMember(sid, { role: 'member' })
|
||||
const r = execAsUser(
|
||||
member.userId,
|
||||
`insert into space_role_permissions (space_id, subject, permission) values ('${sid}','member','settings.manage');`,
|
||||
)
|
||||
expect(r.ok).toBe(false)
|
||||
})
|
||||
|
||||
it('an admin can grant a permission (positive control)', () => {
|
||||
const sid = space()
|
||||
const admin = seedMember(sid, { role: 'admin' })
|
||||
const r = execAsUser(
|
||||
admin.userId,
|
||||
`insert into space_role_permissions (space_id, subject, permission) values ('${sid}','member','ops.kb.write');`,
|
||||
)
|
||||
expect(r.ok).toBe(true)
|
||||
})
|
||||
|
||||
it("a member's delete of an existing grant is filtered out (the grant survives)", () => {
|
||||
const sid = space()
|
||||
const admin = seedMember(sid, { role: 'admin' })
|
||||
const member = seedMember(sid, { role: 'member' })
|
||||
// The space seed already grants 'member' -> 'ops.kb.read'. A member's DELETE
|
||||
// is USING-gated to admin/board, so it matches 0 rows (no error, no effect).
|
||||
execAsUser(member.userId, `delete from space_role_permissions where space_id='${sid}' and subject='member' and permission='ops.kb.read';`)
|
||||
const still = rowsAsUser(
|
||||
admin.userId,
|
||||
`select 1 from space_role_permissions where space_id='${sid}' and subject='member' and permission='ops.kb.read'`,
|
||||
)
|
||||
expect(still.length).toBe(1)
|
||||
})
|
||||
|
||||
it('an admin cannot write a grant into another space (WITH CHECK pins space)', () => {
|
||||
const sidA = space()
|
||||
const adminA = seedMember(sidA, { role: 'admin' })
|
||||
const sidB = space()
|
||||
const r = execAsUser(
|
||||
adminA.userId,
|
||||
`insert into space_role_permissions (space_id, subject, permission) values ('${sidB}','member','settings.manage');`,
|
||||
)
|
||||
expect(r.ok).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user