feat: add organization tenant model

This commit is contained in:
2026-07-09 23:37:20 +08:00
parent 5d315fff21
commit 2b7aa3294f
20 changed files with 900 additions and 116 deletions
+91 -18
View File
@@ -1,6 +1,12 @@
import { afterAll, beforeEach, describe, expect, it } from "vitest";
import { prisma, resetDb } from "./helpers.js";
import { createPermissionAuthorizer, PrismaPrincipalResolver, syncExternalPrincipalMemberships } from "../../src/permission.js";
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 () => {
@@ -16,23 +22,30 @@ describe("ADR-0019 authorization", () => {
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" },
data: { organizationId: DEFAULT_ORG_ID, slug: "direct-auth", name: "Direct Team" },
});
const departmentTeam = await prisma.team.create({
data: { slug: "department-auth", name: "Department Team" },
data: { organizationId: DEFAULT_ORG_ID, slug: "department-auth", name: "Department Team" },
});
const chatTeam = await prisma.team.create({
data: { slug: "chat-auth", name: "Chat Team" },
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",
source: "test",
},
});
await prisma.teamExternalBinding.create({
@@ -45,7 +58,7 @@ describe("ADR-0019 authorization", () => {
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([
@@ -85,9 +98,10 @@ describe("ADR-0019 authorization", () => {
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" },
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: [
{
@@ -123,6 +137,7 @@ describe("ADR-0019 authorization", () => {
it("revokes missing external memberships when replacing a sync source", async () => {
await syncExternalPrincipalMemberships(prisma, {
organizationId: DEFAULT_ORG_ID,
source: "feishu-directory",
memberships: [
{
@@ -141,6 +156,7 @@ describe("ADR-0019 authorization", () => {
});
const result = await syncExternalPrincipalMemberships(prisma, {
organizationId: DEFAULT_ORG_ID,
source: "feishu-directory",
replaceSource: true,
memberships: [
@@ -154,13 +170,16 @@ describe("ADR-0019 authorization", () => {
});
expect(result).toEqual({ synced: 1, revoked: 1 });
const activeB = await new PrismaPrincipalResolver(prisma).resolveActor({ feishuOpenId: "ou_sync_b" });
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", name: "Policy", workspaceDir: "/tmp/auth-policy" },
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" },
@@ -185,12 +204,15 @@ describe("ADR-0019 authorization", () => {
});
expect(editDecision.allowed).toBe(false);
await prisma.permissionGrant.create({
data: {
await prisma.permissionGrant.updateMany({
where: {
resourceType: "PROJECT",
resourceId: "p-auth-policy",
principalType: "USER",
principalId: "ou_auth_policy",
revokedAt: null,
},
data: {
role: "MANAGE",
},
});
@@ -253,7 +275,7 @@ describe("ADR-0019 authorization", () => {
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" },
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" },
@@ -288,7 +310,7 @@ describe("ADR-0019 authorization", () => {
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", name: "Cancel", workspaceDir: "/tmp/cancel" } });
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" },
@@ -304,7 +326,7 @@ describe("ADR-0019 authorization", () => {
});
it("agent.cancel is denied to an edit grant (below the MANAGE default)", async () => {
await prisma.project.create({ data: { id: "p-cancel-edit", name: "Cancel Edit", workspaceDir: "/tmp/cancel-edit" } });
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" },
@@ -319,7 +341,7 @@ describe("ADR-0019 authorization", () => {
});
it("agentCancel DISABLED policy denies cancel even for a manage grant", async () => {
await prisma.project.create({ data: { id: "p-cancel-off", name: "Cancel Off", workspaceDir: "/tmp/cancel-off" } });
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" },
@@ -336,6 +358,57 @@ describe("ADR-0019 authorization", () => {
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) {
@@ -347,13 +420,13 @@ async function seedUserTeamProject(suffix: string) {
},
});
const team = await prisma.team.create({
data: { slug: `team-${suffix}`, name: `Team ${suffix}` },
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}`, name: `Project ${suffix}`, workspaceDir: `/tmp/${suffix}` },
data: { id: `p-${suffix}`, organizationId: DEFAULT_ORG_ID, name: `Project ${suffix}`, workspaceDir: `/tmp/${suffix}` },
});
return { user, team, project };
}