Files
curriculum-project-hub/hub/test/integration/authorization.test.ts
T

324 lines
10 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, it } from "vitest";
import { prisma, resetDb } from "./helpers.js";
import { createPermissionAuthorizer, PrismaPrincipalResolver, syncExternalPrincipalMemberships } from "../../src/permission.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: { slug: "direct-auth", name: "Direct Team" },
});
const departmentTeam = await prisma.team.create({
data: { slug: "department-auth", name: "Department Team" },
});
const chatTeam = await prisma.team.create({
data: { slug: "chat-auth", name: "Chat Team" },
});
await prisma.teamMembership.create({
data: { teamId: directTeam.id, userId: user.id },
});
await prisma.externalPrincipalMembership.create({
data: {
userId: user.id,
principalType: "FEISHU_DEPARTMENT",
principalId: "dep-physics",
source: "test",
},
});
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",
});
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("uses external Feishu principal memberships as grantable principals", async () => {
await prisma.project.create({
data: { id: "p-auth-ext", name: "External", workspaceDir: "/tmp/auth-ext" },
});
await syncExternalPrincipalMemberships(prisma, {
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, {
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, {
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" });
expect(activeB.principals).not.toContainEqual({ type: "FEISHU_DEPARTMENT", id: "dep-b" });
});
it("lets PermissionSettings constrain agent trigger without granting by itself", async () => {
await prisma.project.create({
data: { id: "p-auth-policy", 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.create({
data: {
resourceType: "PROJECT",
resourceId: "p-auth-policy",
principalType: "USER",
principalId: "ou_auth_policy",
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", 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");
});
});
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: { 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}`, name: `Project ${suffix}`, workspaceDir: `/tmp/${suffix}` },
});
return { user, team, project };
}
function defaultProjectSettings(projectId: string, overrides: { agentTrigger?: string } = {}) {
return {
resourceType: "PROJECT" as const,
resourceId: projectId,
externalShare: "ROLE",
comment: "ROLE",
copyDownload: "ROLE",
collaboratorMgmt: "ROLE",
agentTrigger: overrides.agentTrigger ?? "ROLE",
agentCancel: "ROLE",
};
}