mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 16:51:28 +02:00
fix(server): support large retained reader sets
Use PostgreSQL array constructors and cover private project grant changes. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
be0d39b8d6
commit
7e5c5448a8
2 files changed
+39
-3
No files matched your search
@@ -6,7 +6,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { agents, authUsers, companies, companyMemberships, createDb, executionWorkspaces, executionWorkspaceRepositoryAccess, heartbeatRuns, issueAccessGrants,
|
||||
issues, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
|
||||
issues, projects, projectAccessMembers, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
|
||||
import { authorizationService, executionWorkspaceReadSqlCondition, type AuthorizationActor } from "../services/authorization.js";
|
||||
import { executionWorkspaceRepositoryService, assertExecutionWorkspaceRootReadAccess } from "../services/execution-workspace-repositories.js";
|
||||
import { assertTaskWorkspaceAccess } from "../services/task-workspace-source-access.js";
|
||||
@@ -200,6 +200,42 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
} finally { vi.stubEnv("PAPERCLIP_ISSUE_PRIVACY_MODE", "enforce"); }
|
||||
});
|
||||
|
||||
it("matches private project decisions as each principal gains and loses access", async () => {
|
||||
const [project] = await db.insert(projects).values({ companyId, name: "Private batch source", visibility: "private" }).returning();
|
||||
const resource = { type: "project" as const, companyId, projectId: project.id };
|
||||
const service = authorizationService(db);
|
||||
const candidates: AuthorizationActor[] = [actor, { ...actor, onBehalfOfUserId: undefined }, { type: "board", source: "session", userId }];
|
||||
async function check(expected: boolean[]) {
|
||||
const batched = await service.decideReadsForActors(candidates, [resource]);
|
||||
for (let i = 0; i < candidates.length; i++) {
|
||||
expect(batched[i][0]).toEqual(await service.decide({ actor: { ...candidates[i] }, action: "project:read", resource }));
|
||||
expect(batched[i][0].allowed).toBe(expected[i]);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await check([false, false, false]);
|
||||
await db.insert(projectAccessMembers).values({ companyId, projectId: project.id, subjectType: "agent", subjectId: agentId });
|
||||
await check([false, true, false]);
|
||||
const [grant] = await db.insert(projectAccessMembers).values({ companyId, projectId: project.id, subjectType: "user", subjectId: userId }).returning();
|
||||
await check([true, true, true]);
|
||||
await db.delete(projectAccessMembers).where(eq(projectAccessMembers.id, grant.id));
|
||||
await check([false, true, false]);
|
||||
} finally { await db.delete(projects).where(eq(projects.id, project.id)); }
|
||||
});
|
||||
|
||||
it("supports more than 100 distinct reader principals for issue and project batches", async () => {
|
||||
const [project] = await db.insert(projects).values({ companyId, name: "Many readers" }).returning();
|
||||
const candidates: AuthorizationActor[] = Array.from({ length: 101 }, () => ({ type: "board", source: "session", userId: randomUUID() }));
|
||||
const service = authorizationService(db);
|
||||
try {
|
||||
for (const resource of [{ type: "issue" as const, companyId, issueId: sourceId }, { type: "project" as const, companyId, projectId: project.id }]) {
|
||||
const decisions = await service.decideReadsForActors(candidates, [resource]);
|
||||
expect(decisions).toHaveLength(101);
|
||||
expect(decisions.every(batch => batch.length === 1 && !batch[0].allowed)).toBe(true);
|
||||
}
|
||||
} finally { await db.delete(projects).where(eq(projects.id, project.id)); }
|
||||
});
|
||||
|
||||
it("reloads run trust and responsible-user membership between read batches", async () => {
|
||||
const service = authorizationService(db);
|
||||
const resources = [{ type: "company" as const, companyId }];
|
||||
|
||||
@@ -775,7 +775,7 @@ function createAuthorizationService(db: Db | DbTransaction, readSnapshot?: ReadA
|
||||
status: issues.status, executionPolicy: issues.executionPolicy, originKind: issues.originKind, originId: issues.originId,
|
||||
visibility: issues.visibility, privacyRootIssueId: issues.privacyRootIssueId,
|
||||
responsibleUserId: issues.responsibleUserId, createdByUserId: issues.createdByUserId,
|
||||
readable: sql<boolean[]>`jsonb_build_array(${sql.join(privacyActors.map(actor => issuePrivacyCondition(actor)), sql`, `)})`,
|
||||
readable: sql<boolean[]>`ARRAY[${sql.join(privacyActors.map(actor => issuePrivacyCondition(actor)), sql`, `)}]`,
|
||||
}).from(issues).where(inArray(issues.id, issueIds.slice(offset, offset + 500)));
|
||||
for (const row of rows) {
|
||||
for (let index = 0; index < snapshots.length; index++) snapshots[index].issues.set(row.id, { ...row, readable: row.readable[privacyIndexes[index]] });
|
||||
@@ -786,7 +786,7 @@ function createAuthorizationService(db: Db | DbTransaction, readSnapshot?: ReadA
|
||||
for (let offset = 0; offset < ids.length; offset += 500) {
|
||||
const rows = await db.select({ id: projects.id, companyId: projects.companyId,
|
||||
executionWorkspacePolicy: projects.executionWorkspacePolicy, visibility: projects.visibility,
|
||||
readable: sql<boolean[]>`jsonb_build_array(${sql.join(privacyActors.map(actor => projectPrivacyCondition(actor)), sql`, `)})`,
|
||||
readable: sql<boolean[]>`ARRAY[${sql.join(privacyActors.map(actor => projectPrivacyCondition(actor)), sql`, `)}]`,
|
||||
}).from(projects).where(inArray(projects.id, ids.slice(offset, offset + 500)));
|
||||
for (const row of rows) for (let index = 0; index < snapshots.length; index++) {
|
||||
snapshots[index].projects.set(row.id, { ...row, readable: row.readable[privacyIndexes[index]] });
|
||||
|
||||
Reference in new issue
Block a user