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.
65 lines
2.6 KiB
TypeScript
65 lines
2.6 KiB
TypeScript
// 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)
|
|
})
|
|
})
|