fix(auth): classify migrated role-default grants

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
cryppadotta 2026-09-10 20:37:31 +00:00
parent 3fff8a959f
commit 2161240692
2 changed files with 138 additions and 1 deletions

View File

@ -1,2 +1,48 @@
ALTER TABLE "principal_permission_grants" ADD COLUMN "grant_origin" text DEFAULT 'explicit' NOT NULL;--> statement-breakpoint
ALTER TABLE "principal_permission_grants" ADD CONSTRAINT "principal_permission_grants_origin_check" CHECK ("principal_permission_grants"."grant_origin" in ('explicit', 'role_default'));
UPDATE "principal_permission_grants" grants
SET "grant_origin" = 'role_default'
FROM "company_memberships" memberships
WHERE grants."company_id" = memberships."company_id"
AND grants."principal_type" = 'user'
AND grants."principal_id" = memberships."principal_id"
AND memberships."principal_type" = 'user'
AND memberships."status" = 'active'
AND grants."scope" IS NULL
AND grants."granted_by_user_id" IS NULL
AND (
(memberships."membership_role" = 'owner' AND grants."permission_key" IN (
'agents:create',
'agents:configure',
'skills:create',
'environments:manage',
'users:invite',
'users:manage_permissions',
'tasks:assign',
'tasks:manage_active_checkouts',
'joins:approve',
'pipelines:write',
'audit:view_agent_actions',
'tools:manage_connections',
'tools:manage_runtime',
'tools:use',
'tools:admin'
))
OR (memberships."membership_role" = 'admin' AND grants."permission_key" IN (
'agents:create',
'agents:configure',
'skills:create',
'environments:manage',
'users:invite',
'tasks:assign',
'tasks:manage_active_checkouts',
'joins:approve',
'pipelines:write',
'audit:view_agent_actions',
'tools:manage_connections',
'tools:manage_runtime',
'tools:use',
'tools:admin'
))
OR (memberships."membership_role" IN ('member', 'operator') AND grants."permission_key" = 'tasks:assign')
);--> statement-breakpoint
ALTER TABLE "principal_permission_grants" ADD CONSTRAINT "principal_permission_grants_origin_check" CHECK ("principal_permission_grants"."grant_origin" in ('explicit', 'role_default'));

View File

@ -0,0 +1,91 @@
import fs from "node:fs";
import { afterEach, describe, expect, it } from "vitest";
import postgres from "postgres";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./test-embedded-postgres.js";
const MIGRATION_FILE = "0273_round_domino.sql";
const cleanups: Array<() => Promise<void>> = [];
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
afterEach(async () => {
while (cleanups.length > 0) await cleanups.pop()?.();
});
async function migrationStatements() {
const content = await fs.promises.readFile(
new URL(`./migrations/${MIGRATION_FILE}`, import.meta.url),
"utf8",
);
return content
.split("--> statement-breakpoint")
.map((statement) => statement.trim())
.filter(Boolean);
}
describeEmbeddedPostgres("principal permission grant origin migration", () => {
it("marks historical role defaults without consuming explicit grants", async () => {
const database = await startEmbeddedPostgresTestDatabase("paperclip-grant-origin-");
cleanups.push(database.cleanup);
const sql = postgres(database.connectionString, { max: 1, onnotice: () => {} });
try {
await sql.unsafe(`
DROP TABLE principal_permission_grants CASCADE;
DROP TABLE company_memberships CASCADE;
CREATE TABLE company_memberships (
company_id uuid NOT NULL,
principal_type text NOT NULL,
principal_id text NOT NULL,
status text NOT NULL,
membership_role text
);
CREATE TABLE principal_permission_grants (
id uuid PRIMARY KEY,
company_id uuid NOT NULL,
principal_type text NOT NULL,
principal_id text NOT NULL,
permission_key text NOT NULL,
scope jsonb,
granted_by_user_id text
);
INSERT INTO company_memberships VALUES
('00000000-0000-4000-8000-000000000001', 'user', 'owner', 'active', 'owner'),
('00000000-0000-4000-8000-000000000001', 'user', 'admin', 'active', 'admin'),
('00000000-0000-4000-8000-000000000001', 'user', 'operator', 'active', 'member');
INSERT INTO principal_permission_grants VALUES
('00000000-0000-4000-8000-000000000010', '00000000-0000-4000-8000-000000000001', 'user', 'owner', 'users:manage_permissions', NULL, NULL),
('00000000-0000-4000-8000-000000000011', '00000000-0000-4000-8000-000000000001', 'user', 'admin', 'tools:use', NULL, NULL),
('00000000-0000-4000-8000-000000000012', '00000000-0000-4000-8000-000000000001', 'user', 'operator', 'tasks:assign', NULL, NULL),
('00000000-0000-4000-8000-000000000013', '00000000-0000-4000-8000-000000000001', 'user', 'owner', 'tools:use', '{"projectId":"project-1"}', NULL),
('00000000-0000-4000-8000-000000000014', '00000000-0000-4000-8000-000000000001', 'user', 'owner', 'tools:use', NULL, 'grant-author'),
('00000000-0000-4000-8000-000000000015', '00000000-0000-4000-8000-000000000001', 'user', 'admin', 'users:manage_permissions', NULL, NULL),
('00000000-0000-4000-8000-000000000016', '00000000-0000-4000-8000-000000000001', 'agent', 'owner', 'tools:use', NULL, NULL);
`);
for (const statement of await migrationStatements()) {
await sql.unsafe(statement);
}
const rows = await sql.unsafe<Array<{ id: string; grant_origin: string }>>(`
SELECT id, grant_origin
FROM principal_permission_grants
ORDER BY id
`);
expect(rows).toEqual([
{ id: "00000000-0000-4000-8000-000000000010", grant_origin: "role_default" },
{ id: "00000000-0000-4000-8000-000000000011", grant_origin: "role_default" },
{ id: "00000000-0000-4000-8000-000000000012", grant_origin: "role_default" },
{ id: "00000000-0000-4000-8000-000000000013", grant_origin: "explicit" },
{ id: "00000000-0000-4000-8000-000000000014", grant_origin: "explicit" },
{ id: "00000000-0000-4000-8000-000000000015", grant_origin: "explicit" },
{ id: "00000000-0000-4000-8000-000000000016", grant_origin: "explicit" },
]);
} finally {
await sql.end();
}
}, 20_000);
});