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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user