fix(schema): tighten tenant boundaries on pages/refs
Three security-review findings on migration 002: - pages.space_id and refs.space_id changed to NOT NULL + ON DELETE CASCADE (was nullable + SET NULL, which allowed orphan rows surviving space deletion). - pages.parent_id wrapped in composite FK (parent_id, space_id) -> pages(id, space_id) to prevent cross-space parent linkage (same pattern as tasks.project_id in 001). - idx_refs_external promoted to UNIQUE on (space_id, source_kind, external_id); upsertByExternal now requires space_id and dedups per-space, not globally. Added 3 regression tests covering composite FK rejection, CASCADE-on-space-delete, and per-space dedup independence.
This commit is contained in:
@@ -30,13 +30,13 @@ export async function getById(id) {
|
||||
}
|
||||
|
||||
export async function upsertByExternal(input, actor) {
|
||||
const { source_kind, external_id } = input;
|
||||
if (!source_kind || !external_id) {
|
||||
throw new Error('upsertByExternal requires source_kind + external_id');
|
||||
const { space_id, source_kind, external_id } = input;
|
||||
if (!space_id || !source_kind || !external_id) {
|
||||
throw new Error('upsertByExternal requires space_id + source_kind + external_id');
|
||||
}
|
||||
const { rows: [existing] } = await pool.query(
|
||||
`SELECT * FROM refs WHERE source_kind=$1 AND external_id=$2`,
|
||||
[source_kind, external_id]
|
||||
`SELECT * FROM refs WHERE space_id=$1 AND source_kind=$2 AND external_id=$3`,
|
||||
[space_id, source_kind, external_id]
|
||||
);
|
||||
if (existing) {
|
||||
return update(existing.id, input, actor);
|
||||
|
||||
Reference in New Issue
Block a user