forked from bai/curriculum-project-hub
449 lines
16 KiB
TypeScript
449 lines
16 KiB
TypeScript
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";
|
|
|
|
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("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("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",
|
|
};
|
|
}
|