import { afterAll, beforeEach, describe, expect, it } from "vitest"; import { DEFAULT_ORG_ID, prisma, resetDb, seedTestOrganization } from "./helpers.js"; import { createPermissionAuthorizer, grantTeamProjectAccess, listProjectTeamAccess, PrismaPrincipalResolver, syncExternalPrincipalMemberships, } from "../../src/permission.js"; import { lockActiveOrganization } from "../../src/org/status.js"; describe("ADR-0019 authorization", () => { beforeEach(async () => { await resetDb(); }); afterAll(async () => { await prisma.$disconnect(); }); it("resolves actor principals from user, chat, direct teams, external memberships, and external team bindings", async () => { const user = await prisma.user.create({ data: { id: "u-auth-1", feishuOpenId: "ou_auth_1", displayName: "Teacher A" }, }); const directTeam = await prisma.team.create({ data: { organizationId: DEFAULT_ORG_ID, slug: "direct-auth", name: "Direct Team" }, }); const departmentTeam = await prisma.team.create({ data: { organizationId: DEFAULT_ORG_ID, slug: "department-auth", name: "Department Team" }, }); const chatTeam = await prisma.team.create({ data: { organizationId: DEFAULT_ORG_ID, slug: "chat-auth", name: "Chat Team" }, }); await prisma.teamMembership.create({ data: { teamId: directTeam.id, userId: user.id }, }); const connection = await prisma.externalDirectoryConnection.create({ data: { organizationId: DEFAULT_ORG_ID, provider: "FEISHU", source: "test", }, }); await prisma.externalPrincipalMembership.create({ data: { userId: user.id, connectionId: connection.id, principalType: "FEISHU_DEPARTMENT", principalId: "dep-physics", }, }); await prisma.teamExternalBinding.create({ data: { teamId: departmentTeam.id, principalType: "FEISHU_DEPARTMENT", principalId: "dep-physics" }, }); await prisma.teamExternalBinding.create({ data: { teamId: chatTeam.id, principalType: "FEISHU_CHAT", principalId: "chat-auth" }, }); const resolution = await new PrismaPrincipalResolver(prisma).resolveActor({ feishuOpenId: "ou_auth_1", chatId: "chat-auth", }, { organizationId: DEFAULT_ORG_ID }); expect(resolution.userId).toBe(user.id); expect(resolution.principals).toEqual(expect.arrayContaining([ { type: "USER", id: "ou_auth_1" }, { type: "FEISHU_CHAT", id: "chat-auth" }, { type: "FEISHU_DEPARTMENT", id: "dep-physics" }, { type: "TEAM", id: directTeam.id }, { type: "TEAM", id: departmentTeam.id }, { type: "TEAM", id: chatTeam.id }, ])); }); it("allows agent trigger through a team project grant", async () => { const { team } = await seedUserTeamProject("auth-team-grant"); await prisma.permissionGrant.create({ data: { resourceType: "PROJECT", resourceId: "p-auth-team-grant", principalType: "TEAM", principalId: team.id, role: "EDIT", }, }); const decision = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: "ou_auth_team_grant" }, action: "agent.trigger", resource: { type: "PROJECT", id: "p-auth-team-grant" }, }); expect(decision.allowed).toBe(true); expect(decision.matchedGrant).toMatchObject({ principal: { type: "TEAM", id: team.id }, role: "EDIT", }); }); it.each(["SUSPENDED", "ARCHIVED"] as const)( "denies every project authorization when the organization is %s", async (status) => { await prisma.project.create({ data: { id: `p-org-${status}`, organizationId: DEFAULT_ORG_ID, name: status, workspaceDir: `/tmp/org-${status}` }, }); await prisma.user.create({ data: { id: `u-org-${status}`, feishuOpenId: `ou_org_${status}`, displayName: status, organizationMemberships: { create: { organizationId: DEFAULT_ORG_ID, role: "OWNER" } }, }, }); await prisma.permissionGrant.create({ data: { resourceType: "PROJECT", resourceId: `p-org-${status}`, principalType: "USER", principalId: `ou_org_${status}`, role: "MANAGE", }, }); await prisma.organization.update({ where: { id: DEFAULT_ORG_ID }, data: { status } }); for (const action of ["project.read", "project.edit", "collaborator.manage", "agent.trigger", "agent.cancel"] as const) { const decision = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: `ou_org_${status}` }, action, resource: { type: "PROJECT", id: `p-org-${status}` }, }); expect(decision.allowed).toBe(false); expect(decision.reason).toContain(`organization ${DEFAULT_ORG_ID} is ${status}`); } }, ); it("serializes a lifecycle transition behind an admitted customer transaction", async () => { let releaseOperation!: () => void; const operationHeld = new Promise((resolve) => { releaseOperation = resolve; }); let markLocked!: () => void; const rowLocked = new Promise((resolve) => { markLocked = resolve; }); const operation = prisma.$transaction(async (tx) => { await lockActiveOrganization(tx, DEFAULT_ORG_ID); markLocked(); await operationHeld; }); await rowLocked; let transitionCommitted = false; const transition = prisma.organization.update({ where: { id: DEFAULT_ORG_ID }, data: { status: "SUSPENDED" }, }).then(() => { transitionCommitted = true; }); await new Promise((resolve) => setTimeout(resolve, 100)); expect(transitionCommitted).toBe(false); releaseOperation(); await operation; await transition; expect(transitionCommitted).toBe(true); }); it("uses external Feishu principal memberships as grantable principals", async () => { await prisma.project.create({ data: { id: "p-auth-ext", organizationId: DEFAULT_ORG_ID, name: "External", workspaceDir: "/tmp/auth-ext" }, }); await syncExternalPrincipalMemberships(prisma, { organizationId: DEFAULT_ORG_ID, source: "feishu-test", memberships: [ { feishuOpenId: "ou_auth_ext", displayName: "Teacher Ext", principalType: "FEISHU_USER_GROUP", principalId: "ug-lesson-design", }, ], }); await prisma.permissionGrant.create({ data: { resourceType: "PROJECT", resourceId: "p-auth-ext", principalType: "FEISHU_USER_GROUP", principalId: "ug-lesson-design", role: "EDIT", }, }); const decision = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: "ou_auth_ext" }, action: "agent.trigger", resource: { type: "PROJECT", id: "p-auth-ext" }, }); expect(decision.allowed).toBe(true); expect(decision.matchedGrant?.principal).toEqual({ type: "FEISHU_USER_GROUP", id: "ug-lesson-design", }); }); it("revokes missing external memberships when replacing a sync source", async () => { await syncExternalPrincipalMemberships(prisma, { organizationId: DEFAULT_ORG_ID, source: "feishu-directory", memberships: [ { feishuOpenId: "ou_sync_a", displayName: "Teacher A", principalType: "FEISHU_DEPARTMENT", principalId: "dep-a", }, { feishuOpenId: "ou_sync_b", displayName: "Teacher B", principalType: "FEISHU_DEPARTMENT", principalId: "dep-b", }, ], }); const result = await syncExternalPrincipalMemberships(prisma, { organizationId: DEFAULT_ORG_ID, source: "feishu-directory", replaceSource: true, memberships: [ { feishuOpenId: "ou_sync_a", displayName: "Teacher A", principalType: "FEISHU_DEPARTMENT", principalId: "dep-a", }, ], }); expect(result).toEqual({ synced: 1, revoked: 1 }); const activeB = await new PrismaPrincipalResolver(prisma).resolveActor( { feishuOpenId: "ou_sync_b" }, { organizationId: DEFAULT_ORG_ID }, ); expect(activeB.principals).not.toContainEqual({ type: "FEISHU_DEPARTMENT", id: "dep-b" }); }); it("blocks directory sync and team grants for an archived organization", async () => { const team = await prisma.team.create({ data: { organizationId: DEFAULT_ORG_ID, slug: "archived-team", name: "Archived Team" }, }); await prisma.project.create({ data: { id: "p-archived-grant", organizationId: DEFAULT_ORG_ID, name: "Archived", workspaceDir: "/tmp/archived" }, }); await prisma.organization.update({ where: { id: DEFAULT_ORG_ID }, data: { status: "ARCHIVED" } }); const expected = `organization ${DEFAULT_ORG_ID} is ARCHIVED`; await expect(syncExternalPrincipalMemberships(prisma, { organizationId: DEFAULT_ORG_ID, source: "blocked-sync", memberships: [{ feishuOpenId: "ou_blocked_sync", displayName: "Blocked", principalType: "FEISHU_DEPARTMENT", principalId: "dep-blocked", }], })).rejects.toThrow(expected); await expect(grantTeamProjectAccess(prisma, { projectId: "p-archived-grant", teamId: team.id, role: "EDIT", })).rejects.toThrow(expected); await expect(prisma.externalDirectoryConnection.count()).resolves.toBe(0); await expect(prisma.permissionGrant.count({ where: { resourceId: "p-archived-grant" } })).resolves.toBe(0); }); it("lets PermissionSettings constrain agent trigger without granting by itself", async () => { await prisma.project.create({ data: { id: "p-auth-policy", organizationId: DEFAULT_ORG_ID, name: "Policy", workspaceDir: "/tmp/auth-policy" }, }); await prisma.user.create({ data: { id: "u-auth-policy", feishuOpenId: "ou_auth_policy", displayName: "Teacher Policy" }, }); await prisma.permissionGrant.create({ data: { resourceType: "PROJECT", resourceId: "p-auth-policy", principalType: "USER", principalId: "ou_auth_policy", role: "EDIT", }, }); await prisma.permissionSettings.create({ data: defaultProjectSettings("p-auth-policy", { agentTrigger: "MANAGE_ONLY" }), }); const editDecision = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: "ou_auth_policy" }, action: "agent.trigger", resource: { type: "PROJECT", id: "p-auth-policy" }, }); expect(editDecision.allowed).toBe(false); await prisma.permissionGrant.updateMany({ where: { resourceType: "PROJECT", resourceId: "p-auth-policy", principalType: "USER", principalId: "ou_auth_policy", revokedAt: null, }, data: { role: "MANAGE", }, }); const manageDecision = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: "ou_auth_policy" }, action: "agent.trigger", resource: { type: "PROJECT", id: "p-auth-policy" }, }); expect(manageDecision.allowed).toBe(true); expect(manageDecision.requiredRole).toBe("MANAGE"); }); it("requires project agent.trigger and configured role trigger grant for role.trigger", async () => { const { team } = await seedUserTeamProject("auth-role"); await prisma.permissionGrant.create({ data: { resourceType: "PROJECT", resourceId: "p-auth-role", principalType: "TEAM", principalId: team.id, role: "EDIT", }, }); await prisma.roleTriggerGrant.create({ data: { projectId: "p-auth-role", roleId: "review", principalType: "USER", principalId: "ou_someone_else", }, }); const denied = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: "ou_auth_role" }, action: "role.trigger", resource: { type: "PROJECT", id: "p-auth-role" }, roleId: "review", }); expect(denied.allowed).toBe(false); await prisma.roleTriggerGrant.create({ data: { projectId: "p-auth-role", roleId: "review", principalType: "TEAM", principalId: team.id, }, }); const allowed = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: "ou_auth_role" }, action: "role.trigger", resource: { type: "PROJECT", id: "p-auth-role" }, roleId: "review", }); expect(allowed.allowed).toBe(true); expect(allowed.matchedRoleGrant?.principal).toEqual({ type: "TEAM", id: team.id }); }); it("keeps a role configured when all role grants are revoked", async () => { await prisma.project.create({ data: { id: "p-auth-revoked-role", organizationId: DEFAULT_ORG_ID, name: "Revoked", workspaceDir: "/tmp/auth-revoked" }, }); await prisma.user.create({ data: { id: "u-auth-revoked-role", feishuOpenId: "ou_auth_revoked", displayName: "Teacher Revoked" }, }); await prisma.permissionGrant.create({ data: { resourceType: "PROJECT", resourceId: "p-auth-revoked-role", principalType: "USER", principalId: "ou_auth_revoked", role: "EDIT", }, }); await prisma.roleTriggerGrant.create({ data: { projectId: "p-auth-revoked-role", roleId: "review", principalType: "USER", principalId: "ou_auth_revoked", revokedAt: new Date(), }, }); const decision = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: "ou_auth_revoked" }, action: "role.trigger", resource: { type: "PROJECT", id: "p-auth-revoked-role" }, roleId: "review", }); expect(decision.allowed).toBe(false); expect(decision.reason).toContain("no active role review grant"); }); it("agent.cancel defaults to MANAGE and is allowed for a manage grant", async () => { await prisma.project.create({ data: { id: "p-cancel", organizationId: DEFAULT_ORG_ID, name: "Cancel", workspaceDir: "/tmp/cancel" } }); await prisma.user.create({ data: { id: "u-cancel", feishuOpenId: "ou_cancel", displayName: "Manager" } }); await prisma.permissionGrant.create({ data: { resourceType: "PROJECT", resourceId: "p-cancel", principalType: "USER", principalId: "ou_cancel", role: "MANAGE" }, }); const decision = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: "ou_cancel" }, action: "agent.cancel", resource: { type: "PROJECT", id: "p-cancel" }, }); expect(decision.allowed).toBe(true); expect(decision.requiredRole).toBe("MANAGE"); }); it("agent.cancel is denied to an edit grant (below the MANAGE default)", async () => { await prisma.project.create({ data: { id: "p-cancel-edit", organizationId: DEFAULT_ORG_ID, name: "Cancel Edit", workspaceDir: "/tmp/cancel-edit" } }); await prisma.user.create({ data: { id: "u-cancel-edit", feishuOpenId: "ou_cancel_edit", displayName: "Editor" } }); await prisma.permissionGrant.create({ data: { resourceType: "PROJECT", resourceId: "p-cancel-edit", principalType: "USER", principalId: "ou_cancel_edit", role: "EDIT" }, }); const decision = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: "ou_cancel_edit" }, action: "agent.cancel", resource: { type: "PROJECT", id: "p-cancel-edit" }, }); expect(decision.allowed).toBe(false); }); it("agentCancel DISABLED policy denies cancel even for a manage grant", async () => { await prisma.project.create({ data: { id: "p-cancel-off", organizationId: DEFAULT_ORG_ID, name: "Cancel Off", workspaceDir: "/tmp/cancel-off" } }); await prisma.user.create({ data: { id: "u-cancel-off", feishuOpenId: "ou_cancel_off", displayName: "Manager Off" } }); await prisma.permissionGrant.create({ data: { resourceType: "PROJECT", resourceId: "p-cancel-off", principalType: "USER", principalId: "ou_cancel_off", role: "MANAGE" }, }); await prisma.permissionSettings.create({ data: defaultProjectSettings("p-cancel-off", { agentCancel: "DISABLED" }), }); const decision = await createPermissionAuthorizer(prisma).can({ actor: { feishuOpenId: "ou_cancel_off" }, action: "agent.cancel", resource: { type: "PROJECT", id: "p-cancel-off" }, }); expect(decision.allowed).toBe(false); expect(decision.reason).toContain("disables this action"); }); it("scopes team slug and project team access by organization", async () => { await seedTestOrganization("org-other", "other"); await prisma.team.create({ data: { organizationId: "org-other", slug: "curriculum", name: "Other Curriculum" }, }); const team = await prisma.team.create({ data: { organizationId: DEFAULT_ORG_ID, slug: "curriculum", name: "Default Curriculum" }, }); await prisma.project.create({ data: { id: "p-team-access", organizationId: DEFAULT_ORG_ID, name: "Team Access", workspaceDir: "/tmp/team-access" }, }); const entry = await grantTeamProjectAccess(prisma, { projectId: "p-team-access", teamSlug: "curriculum", role: "EDIT", }); expect(entry).toMatchObject({ projectId: "p-team-access", organizationId: DEFAULT_ORG_ID, teamId: team.id, teamSlug: "curriculum", role: "EDIT", }); await grantTeamProjectAccess(prisma, { projectId: "p-team-access", teamSlug: "curriculum", role: "MANAGE", }); const entries = await listProjectTeamAccess(prisma, "p-team-access"); expect(entries).toHaveLength(1); expect(entries[0]).toMatchObject({ teamId: team.id, role: "MANAGE" }); }); it("refuses cross-organization team project grants", async () => { await seedTestOrganization("org-other-deny", "other-deny"); const otherTeam = await prisma.team.create({ data: { organizationId: "org-other-deny", slug: "review", name: "Other Review" }, }); await prisma.project.create({ data: { id: "p-cross-org", organizationId: DEFAULT_ORG_ID, name: "Cross Org", workspaceDir: "/tmp/cross-org" }, }); await expect(grantTeamProjectAccess(prisma, { projectId: "p-cross-org", teamId: otherTeam.id, role: "EDIT", })).rejects.toThrow(/cross-organization team grant refused/); }); }); async function seedUserTeamProject(suffix: string) { const user = await prisma.user.create({ data: { id: `u-${suffix}`, feishuOpenId: `ou_${suffix.replaceAll("-", "_")}`, displayName: "Teacher Team", }, }); const team = await prisma.team.create({ data: { organizationId: DEFAULT_ORG_ID, slug: `team-${suffix}`, name: `Team ${suffix}` }, }); await prisma.teamMembership.create({ data: { teamId: team.id, userId: user.id }, }); const project = await prisma.project.create({ data: { id: `p-${suffix}`, organizationId: DEFAULT_ORG_ID, name: `Project ${suffix}`, workspaceDir: `/tmp/${suffix}` }, }); return { user, team, project }; } function defaultProjectSettings( projectId: string, overrides: { agentTrigger?: string; agentCancel?: string } = {}, ) { return { resourceType: "PROJECT" as const, resourceId: projectId, externalShare: "ROLE", comment: "ROLE", copyDownload: "ROLE", collaboratorMgmt: "ROLE", agentTrigger: overrides.agentTrigger ?? "ROLE", agentCancel: overrides.agentCancel ?? "ROLE", }; }