feat: harden tenant access and production operations

This commit is contained in:
xz
2026-08-24 22:02:22 +08:00
parent ffb27d845b
commit 9811469a64
18 changed files with 709 additions and 36 deletions
+60
View File
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { dbRun, withTransaction } from "../server/db.mjs";
const base = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787";
const scope = {
@@ -33,8 +34,25 @@ async function login(email) {
const ownerHeaders = { ...(await login("producer@local.test")), ...scope };
const writerHeaders = { ...(await login("writer@local.test")), ...scope };
const inviteEmail = `invitation-smoke-${Date.now()}@local.test`;
const projectInviteEmail = `project-invitation-smoke-${Date.now()}@local.test`;
let invitationId = "";
let invitationState = "";
let projectInvitationId = "";
let projectInvitationState = "";
let projectUserId = "";
function cleanupProjectUser() {
if (!projectUserId) return;
withTransaction(() => {
dbRun("DELETE FROM invitations WHERE email = ? OR accepted_user_id = ?", [projectInviteEmail, projectUserId]);
dbRun("UPDATE audit_logs SET actor_user_id = NULL WHERE actor_user_id = ?", [projectUserId]);
dbRun("UPDATE system_settings SET updated_by = NULL WHERE updated_by = ?", [projectUserId]);
dbRun("UPDATE feature_flags SET updated_by = NULL WHERE updated_by = ?", [projectUserId]);
dbRun("UPDATE notification_channels SET created_by = NULL WHERE created_by = ?", [projectUserId]);
dbRun("UPDATE api_clients SET created_by = NULL WHERE created_by = ?", [projectUserId]);
dbRun("DELETE FROM users WHERE id = ?", [projectUserId]);
});
}
try {
const writerAttempt = await request("/api/organizations/org-studio-lab/invitations", {
@@ -99,6 +117,40 @@ try {
const afterRevoke = assertOk(await request("/api/organizations/org-studio-lab/commercial", { headers: ownerHeaders }), "读取撤销后商业席位");
assert.equal(Number(afterRevoke.seat.pending), Number(before.seat.pending), "撤销邀请后必须释放待入组席位");
const mismatchedRole = await request("/api/organizations/org-studio-lab/invitations", {
method: "POST",
headers: ownerHeaders,
body: JSON.stringify({ email: `mismatched-role-${Date.now()}@local.test`, roleKey: "project_viewer", workspaceId: "ws-local-aidrama" })
});
assert.equal(mismatchedRole.response.status, 400, "项目角色不能用于工作区级邀请");
assert.equal(mismatchedRole.payload.error, "role_invalid", "邀请层级角色不匹配需要稳定错误码");
const projectInvite = assertOk(await request("/api/organizations/org-studio-lab/invitations", {
method: "POST",
headers: ownerHeaders,
body: JSON.stringify({ email: projectInviteEmail, roleKey: "project_viewer", workspaceId: "ws-local-aidrama", projectId: "thunder-mouth" })
}), "创建项目查看者邀请");
projectInvitationId = projectInvite.invitation.id;
projectInvitationState = projectInvite.invitation.status;
assert.equal(projectInvite.invitation.role_key, "project_viewer", "项目邀请必须保留项目查看者角色");
assert.equal(projectInvite.invitation.project_id, "thunder-mouth", "项目邀请必须绑定目标项目");
const projectRegistration = assertOk(await request("/api/auth/register", {
method: "POST",
body: JSON.stringify({ inviteToken: projectInvite.invitation.inviteToken, displayName: "项目访客测试用户", password: "ProjectInvite@123456" })
}), "注册项目查看者");
projectUserId = projectRegistration.user.id;
assert.equal(projectRegistration.context.currentWorkspace.id, "ws-local-aidrama", "项目邀请用户必须进入目标工作区");
assert.equal(projectRegistration.context.currentProject.id, "thunder-mouth", "项目邀请用户必须进入目标项目");
assert.equal(projectRegistration.context.workspaceRole, "project_guest", "项目邀请用户必须使用受限工作区角色");
assert.equal(projectRegistration.context.projectRole, "project_viewer", "项目邀请用户必须保留项目查看者角色");
assert.equal(projectRegistration.context.permissions.includes("job:create"), false, "项目查看者不能继承生成权限");
projectInvitationState = "accepted";
const projectUserProjects = assertOk(await request("/api/projects", {
headers: { authorization: `Bearer ${projectRegistration.session.token}`, "x-organization-id": "org-studio-lab", "x-workspace-id": "ws-local-aidrama" }
}), "读取项目查看者可访问项目");
assert.deepEqual(projectUserProjects.projects.map((item) => item.id), ["thunder-mouth"], "项目查看者不能继承工作区其他项目");
const listed = assertOk(await request("/api/organizations/org-studio-lab", { headers: ownerHeaders }), "读取组织邀请列表");
assert.ok(!listed.invitations.some((item) => item.id === invitationId), "撤销邀请不能继续出现在待处理列表");
} finally {
@@ -109,6 +161,14 @@ try {
body: "{}"
});
}
if (projectInvitationId && projectInvitationState === "pending") {
await request(`/api/organizations/org-studio-lab/invitations/${encodeURIComponent(projectInvitationId)}/revoke`, {
method: "POST",
headers: ownerHeaders,
body: "{}"
});
}
cleanupProjectUser();
}
console.log(`invitations smoke passed: ${base}`);