test(auth): require existing membership and verify grant audit rollback

Refuse grants without an existing active membership. Verify audit failures roll back permission changes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-09 11:20:15 -06:00
1 parent a72aa691be
commit 338c58fb24
2 files changed
+16 -4

No files matched your search

@@ -2,13 +2,14 @@ import { randomUUID } from "node:crypto";
import express from "express";
import request from "supertest";
import { and, eq } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { activityLog, agents, authUsers, companies, companyMemberships, createDb,
principalPermissionGrants, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { agentPermissionGrantRoutes } from "../routes/agent-permission-grants.js";
import { agentCommentaryRoutes } from "../routes/agent-commentary.js";
import { errorHandler } from "../middleware/error-handler.js";
import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
import * as activityLogging from "../services/activity-log.js";
describe("Board agent permission grants", () => {
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
@@ -103,6 +104,9 @@ describe("Board agent permission grants", () => {
eq(companyMemberships.companyId, f.companyId), eq(companyMemberships.principalId, f.agentId)));
expect((await request(api).patch(path(f)).send(grant)).status).toBe(409);
expect(await grants(f)).toEqual([]);
await db.delete(companyMemberships).where(and(
eq(companyMemberships.companyId, f.companyId), eq(companyMemberships.principalId, f.agentId)));
expect((await request(api).patch(path(f)).send(grant)).status).toBe(409);
});
it("serializes concurrent additions of the same grant", async () => {
@@ -111,4 +115,13 @@ describe("Board agent permission grants", () => {
expect(responses.map(response => response.status)).toEqual([200, 200]);
expect(await grants(f)).toHaveLength(1);
});
it("rolls back the grant if its audit record cannot be saved", async () => {
const f = await seed();
const failure = vi.spyOn(activityLogging, "logActivity").mockRejectedValueOnce(new Error("Audit storage unavailable"));
try {
expect((await request(owner(f)).patch(path(f)).send(grant)).status).toBe(500);
expect(await grants(f)).toEqual([]);
} finally { failure.mockRestore(); }
});
});
+2 -3
View File
@@ -31,17 +31,16 @@ export function agentPermissionGrantRoutes(db: Db) {
}
if (enabled) {
if (agent.status === "pending_approval" || agent.status === "terminated") {
throw conflict("Approve or restore the agent before granting permissions");
throw conflict("Cannot grant permissions to an unapproved or terminated agent");
}
const membership = await tx.select().from(companyMemberships).where(and(
eq(companyMemberships.companyId, agent.companyId),
eq(companyMemberships.principalType, "agent"),
eq(companyMemberships.principalId, agent.id),
)).for("update").then(rows => rows[0]);
if (membership && membership.status !== "active") {
if (!membership || membership.status !== "active") {
throw conflict("Agent company membership is not active");
}
if (!membership) await access.ensureMembership(agent.companyId, "agent", agent.id);
await tx.insert(principalPermissionGrants).values({
companyId: agent.companyId, principalType: "agent", principalId: agent.id,
permissionKey, scope: null, grantedByUserId: req.actor.userId ?? null,