fix(schema): tighten tenant boundaries on resources/deps/creds/source_docs

Apply same composite-FK pattern as 001/002 for migration 003:
- resources: add UNIQUE (id, space_id) as FK target.
- resource_dependencies: denormalize space_id, composite FKs on both endpoints
  (enforces both ends of a dep live in the same space at the DB layer).
- resource_credentials: denormalize space_id, composite FK to resources.
- source_docs.resource_id: NOT NULL (tenancy anchor); resource_id was already
  absent from the update FIELDS list so docs cannot move resources.

Repos derive space_id from the resource on addDependency/addCredential so callers
can't fake cross-tenant assignment. 3 regression tests added.
This commit is contained in:
root
2026-05-31 10:33:17 +10:00
parent 9dd944226d
commit 6086cf9a7a
3 changed files with 76 additions and 13 deletions

View File

@@ -15,4 +15,43 @@ describe('migration 003 — resources', () => {
}
});
});
it('resource_dependencies rejects cross-space endpoints', async () => {
await withClient(async (c) => {
const { rows: [s1] } = await c.query(`INSERT INTO spaces(slug,name) VALUES('a','A') RETURNING id;`);
const { rows: [s2] } = await c.query(`INSERT INTO spaces(slug,name) VALUES('b','B') RETURNING id;`);
const { rows: [a] } = await c.query(
`INSERT INTO resources(space_id, slug, name, runtime_type) VALUES($1,'a','A','lxc') RETURNING id;`,
[s1.id]);
const { rows: [b] } = await c.query(
`INSERT INTO resources(space_id, slug, name, runtime_type) VALUES($1,'b','B','lxc') RETURNING id;`,
[s2.id]);
await expect(c.query(
`INSERT INTO resource_dependencies(resource_id, depends_on, space_id) VALUES($1,$2,$3);`,
[a.id, b.id, s1.id]
)).rejects.toThrow(/foreign key/i);
});
});
it('resource_credentials rejects assignment to a resource in another space', async () => {
await withClient(async (c) => {
const { rows: [s1] } = await c.query(`INSERT INTO spaces(slug,name) VALUES('a','A') RETURNING id;`);
const { rows: [s2] } = await c.query(`INSERT INTO spaces(slug,name) VALUES('b','B') RETURNING id;`);
const { rows: [r] } = await c.query(
`INSERT INTO resources(space_id, slug, name, runtime_type) VALUES($1,'r','R','lxc') RETURNING id;`,
[s1.id]);
await expect(c.query(
`INSERT INTO resource_credentials(resource_id, space_id, label, vault_path) VALUES($1,$2,'l','env:X');`,
[r.id, s2.id]
)).rejects.toThrow(/foreign key/i);
});
});
it('source_docs.resource_id is NOT NULL', async () => {
await withClient(async (c) => {
await expect(c.query(
`INSERT INTO source_docs(name, upstream_url) VALUES('n','https://x');`
)).rejects.toThrow(/not.null|null value/i);
});
});
});