198 lines
15 KiB
JavaScript
198 lines
15 KiB
JavaScript
const base = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787";
|
|
const scopeHeaders = {
|
|
"x-organization-id": "org-studio-lab",
|
|
"x-workspace-id": "ws-local-aidrama",
|
|
"x-project-id": "thunder-mouth"
|
|
};
|
|
|
|
async function request(path, headers = ownerHeaders, init = {}) {
|
|
const response = await fetch(`${base}${path}`, { ...init, headers: { ...headers, ...(init.headers || {}) } });
|
|
const payload = await response.json();
|
|
return { response, payload };
|
|
}
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
async function login(email) {
|
|
const result = await request("/api/auth/login", {}, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ email, password: "Demo@123456" })
|
|
});
|
|
assert(result.response.ok, `${email} 登录失败`);
|
|
assert(result.payload.user?.id && result.payload.user.id !== result.payload.session.id, `${email} 登录响应不能把 session id 当作 user id`);
|
|
assert(result.payload.user.id === result.payload.context.currentUser.id, `${email} 登录响应 user 与 context.currentUser 不一致`);
|
|
return { authorization: `Bearer ${result.payload.session.token}` };
|
|
}
|
|
|
|
const ownerAuth = await login("producer@local.test");
|
|
const writerAuth = await login("writer@local.test");
|
|
const ownerHeaders = { ...ownerAuth, ...scopeHeaders };
|
|
|
|
const sessionTestAuth = await login("producer2@local.test");
|
|
const sessionTestHeaders = { ...sessionTestAuth };
|
|
const ownerSessions = await request("/api/auth/sessions", sessionTestHeaders);
|
|
assert(ownerSessions.response.ok && ownerSessions.payload.sessions.some((session) => session.current), "当前用户必须能看到当前登录会话");
|
|
const revokedOthers = await request("/api/auth/sessions/revoke-others", sessionTestHeaders, { method: "POST", body: "{}", headers: { "content-type": "application/json" } });
|
|
assert(revokedOthers.response.ok && revokedOthers.payload.sessions.some((session) => session.current), "撤销其他会话后当前会话必须保持有效");
|
|
|
|
const context = await request("/api/context", {});
|
|
assert(context.response.status === 401, "未登录请求不应访问 context");
|
|
|
|
const authenticatedContext = await request("/api/context", ownerHeaders);
|
|
assert(authenticatedContext.response.ok, "登录后的 context 不可访问");
|
|
assert(authenticatedContext.payload.platform.organizations.length >= 2, "没有多组织数据");
|
|
assert(authenticatedContext.payload.platform.workspaces.length >= 2, "没有多工作区数据");
|
|
assert(authenticatedContext.payload.context.permissions.includes("job:create"), "所有者缺少生成任务权限");
|
|
assert(authenticatedContext.payload.context.systemAdmin === true, "系统管理员身份未生效");
|
|
assert(authenticatedContext.payload.rolePermissions.some((role) => role.name === "组织所有者" && role.permissions.includes("organization:manage")), "管理员必须能读取服务端角色权限矩阵");
|
|
|
|
const ownerRolePolicies = await request("/api/organizations/org-studio-lab/role-policies", ownerHeaders);
|
|
assert(ownerRolePolicies.response.ok, "组织管理员无法读取角色权限策略");
|
|
assert(ownerRolePolicies.payload.roles.some((role) => role.key === "writer"), "角色策略目录缺少编剧角色");
|
|
assert(ownerRolePolicies.payload.permissions.some((permission) => permission.key === "voice:approve"), "角色策略目录缺少声音审批权限");
|
|
const writerRolePolicy = ownerRolePolicies.payload.roles.find((role) => role.key === "writer");
|
|
const policyPermissionKey = "voice:approve";
|
|
const originalWriterPermission = writerRolePolicy.permissions.includes(policyPermissionKey);
|
|
const writerRolePolicies = await request("/api/organizations/org-studio-lab/role-policies", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerRolePolicies.response.status === 403, "普通用户不应读取组织角色权限策略");
|
|
try {
|
|
const grantedWriterPolicy = await request("/api/organizations/org-studio-lab/role-policies/writer", ownerHeaders, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ permissionKey: policyPermissionKey, enabled: true }),
|
|
headers: { "content-type": "application/json" }
|
|
});
|
|
assert(grantedWriterPolicy.response.ok && grantedWriterPolicy.payload.roles.find((role) => role.key === "writer")?.permissions.includes(policyPermissionKey), "管理员授予组织角色权限失败");
|
|
const writerAfterGrant = await request("/api/context", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerAfterGrant.response.ok && writerAfterGrant.payload.context.permissions.includes(policyPermissionKey), "组织角色授权没有进入普通用户有效权限");
|
|
|
|
const revokedWriterPolicy = await request("/api/organizations/org-studio-lab/role-policies/writer", ownerHeaders, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ permissionKey: policyPermissionKey, enabled: false }),
|
|
headers: { "content-type": "application/json" }
|
|
});
|
|
assert(revokedWriterPolicy.response.ok && !revokedWriterPolicy.payload.roles.find((role) => role.key === "writer")?.permissions.includes(policyPermissionKey), "管理员撤销组织角色权限失败");
|
|
const writerAfterRevoke = await request("/api/context", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerAfterRevoke.response.ok && !writerAfterRevoke.payload.context.permissions.includes(policyPermissionKey), "组织角色撤销没有从普通用户有效权限移除");
|
|
} finally {
|
|
await request("/api/organizations/org-studio-lab/role-policies/writer", ownerHeaders, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ permissionKey: policyPermissionKey, enabled: originalWriterPermission }),
|
|
headers: { "content-type": "application/json" }
|
|
});
|
|
}
|
|
const lockedOwnerPolicy = await request("/api/organizations/org-studio-lab/role-policies/org_owner", ownerHeaders, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ permissionKey: policyPermissionKey, enabled: false }),
|
|
headers: { "content-type": "application/json" }
|
|
});
|
|
assert(lockedOwnerPolicy.response.status === 400 && lockedOwnerPolicy.payload.error === "owner_policy_locked", "组织所有者策略必须保持锁定");
|
|
|
|
const systemConfig = await request("/api/system/config", ownerHeaders);
|
|
assert(systemConfig.response.ok, "系统管理员无法访问系统配置");
|
|
|
|
const ownerModels = await request("/api/platform/models", ownerHeaders);
|
|
assert(ownerModels.response.ok, "系统管理员无法访问模型中台");
|
|
const ownerCosts = await request("/api/platform/costs", ownerHeaders);
|
|
assert(ownerCosts.response.ok, "系统管理员无法访问成本中心");
|
|
const ownerCompliance = await request("/api/platform/compliance", ownerHeaders);
|
|
assert(ownerCompliance.response.ok, "系统管理员无法访问合规中心");
|
|
const ownerOrganizationPath = await request("/api/organizations/org-northstar", ownerHeaders);
|
|
assert(ownerOrganizationPath.response.ok && ownerOrganizationPath.payload.organization.id === "org-northstar", "组织路径没有优先于旧上下文头");
|
|
|
|
const writerContext = await request("/api/context", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerContext.response.ok, "普通用户 context 不可访问");
|
|
assert(!writerContext.payload.context.permissions.includes("system:settings:view"), "普通用户不应拥有系统配置权限");
|
|
assert(writerContext.payload.platform.members.length === 0, "普通用户不应收到组织成员明细");
|
|
assert(writerContext.payload.platform.workspaceMembers.length === 0, "普通用户不应收到工作区成员明细");
|
|
assert(writerContext.payload.platform.projectMembers.length === 0, "普通用户不应收到项目成员明细");
|
|
assert(writerContext.payload.platform.invitations.length === 0, "普通用户不应收到组织邀请明细");
|
|
assert(writerContext.payload.platform.billing === null, "普通用户不应收到计费账户");
|
|
assert(writerContext.payload.platform.usage === null, "普通用户不应收到用量明细");
|
|
assert(writerContext.payload.platform.auditLog.length === 0, "普通用户不应收到审计日志");
|
|
assert(writerContext.payload.platform.modelRegistry.length === 0, "普通用户不应收到模型连接器明细");
|
|
assert(writerContext.payload.platform.adapterCatalog.some((model) => model.id === "owned-i2v"), "有生成权限的普通用户必须收到可用适配器目录");
|
|
assert(writerContext.payload.platform.adapterCatalog.find((model) => model.id === "newapi-audio-production")?.approvalRequired === true, "混合音频连接器必须携带审批标记");
|
|
assert(writerContext.payload.platform.adapterCatalog.every((model) => !Object.prototype.hasOwnProperty.call(model, "endpoint")), "普通用户的适配器目录不应暴露连接器 Endpoint");
|
|
assert(writerContext.payload.platform.runnerHealth?.length === 0, "普通用户不应收到 Runner 健康明细");
|
|
const writerSystem = await request("/api/system/config", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerSystem.response.status === 403, "普通用户不应访问系统配置");
|
|
const writerProjectPayload = await request("/api/project", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerProjectPayload.response.ok, "普通用户应能读取当前项目生产数据");
|
|
assert((writerProjectPayload.payload.adapters || []).every((adapter) => !Object.prototype.hasOwnProperty.call(adapter, "endpoint") && !Object.prototype.hasOwnProperty.call(adapter, "baseUrl")), "项目接口不应向普通用户泄露连接器地址");
|
|
const writerModels = await request("/api/platform/models", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerModels.response.status === 403, "普通用户不应访问模型中台");
|
|
const writerCosts = await request("/api/platform/costs", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerCosts.response.status === 403, "普通用户不应访问成本中心");
|
|
const writerCompliance = await request("/api/platform/compliance", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerCompliance.response.status === 403, "普通用户不应访问合规中心");
|
|
const writerMembers = await request("/api/organizations/org-studio-lab/members", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerMembers.response.status === 403, "普通用户不应访问组织成员接口");
|
|
const writerQa = await request("/api/qa", { ...writerAuth, ...scopeHeaders });
|
|
assert(writerQa.response.status === 403, "普通用户不应执行审片接口");
|
|
const writerCreateOrganization = await request("/api/organizations", { ...writerAuth, ...scopeHeaders }, { method: "POST", body: JSON.stringify({ name: "blocked-org" }), headers: { "content-type": "application/json" } });
|
|
assert(writerCreateOrganization.response.status === 403, "普通用户不应创建组织");
|
|
|
|
const apiClientCreated = await request("/api/system/api-clients", ownerHeaders, {
|
|
method: "POST",
|
|
body: JSON.stringify({ name: `smoke-runner-${Date.now()}`, scopes: ["jobs:read", "jobs:write"] }),
|
|
headers: { "content-type": "application/json" }
|
|
});
|
|
assert(apiClientCreated.response.status === 201 && apiClientCreated.payload.clientKey, "系统 API 客户端必须返回一次性 client key");
|
|
const apiClientHeaders = { authorization: `Bearer ${apiClientCreated.payload.clientKey}`, ...scopeHeaders };
|
|
const apiClientContext = await request("/api/context", apiClientHeaders);
|
|
assert(apiClientContext.response.ok && apiClientContext.payload.context.systemAdmin === false, "API 客户端不应继承系统管理员身份");
|
|
assert(apiClientContext.payload.context.permissions.includes("job:create"), "API 客户端 jobs scope 未映射为生成权限");
|
|
const apiClientSystem = await request("/api/system/config", apiClientHeaders);
|
|
assert(apiClientSystem.response.status === 403, "API 客户端不应访问系统配置");
|
|
const revokeApiClient = await request(`/api/system/api-clients/${apiClientCreated.payload.client.id}`, ownerHeaders, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ status: "revoked" }),
|
|
headers: { "content-type": "application/json" }
|
|
});
|
|
assert(revokeApiClient.response.ok, "API 客户端撤销失败");
|
|
const revokedApiClient = await request("/api/context", apiClientHeaders);
|
|
assert(revokedApiClient.response.status === 401, "撤销 API 客户端后必须返回 401");
|
|
const externalJob = await request("/api/jobs", { ...writerAuth, ...scopeHeaders }, { method: "POST", body: JSON.stringify({ adapter: "newapi-audio-production", kind: "smoke external gate", shotId: "shot-01", output: "qa/smoke/external-gate.json" }), headers: { "content-type": "application/json" } });
|
|
assert(externalJob.response.status === 403 && externalJob.payload.error === "external_connector_requires_approval", "混合连接器未批准时必须由后端阻断");
|
|
|
|
const orgAdminAuth = await login("producer2@local.test");
|
|
const orgAdminHeaders = { ...orgAdminAuth, "x-organization-id": "org-northstar", "x-workspace-id": "ws-northstar-main", "x-project-id": "northstar-pilot" };
|
|
const orgAdminContext = await request("/api/context", orgAdminHeaders);
|
|
assert(orgAdminContext.response.ok, "组织管理员 context 不可访问");
|
|
assert(orgAdminContext.payload.context.systemAdmin === false, "组织管理员不应被识别为系统管理员");
|
|
assert(orgAdminContext.payload.context.permissions.includes("model:manage"), "组织管理员缺少模型管理权限");
|
|
const orgAdminModels = await request("/api/platform/models", orgAdminHeaders);
|
|
assert(orgAdminModels.response.ok, "组织管理员无法访问本组织模型中台");
|
|
const orgAdminQueue = await request("/api/admin/queue", orgAdminHeaders);
|
|
assert(orgAdminQueue.response.ok, "组织管理员无法访问本组织任务队列");
|
|
const orgAdminSystem = await request("/api/system/config", orgAdminHeaders);
|
|
assert(orgAdminSystem.response.status === 403, "组织管理员不应访问系统配置");
|
|
|
|
const northstar = await request("/api/context", {
|
|
...ownerAuth,
|
|
"x-organization-id": "org-northstar",
|
|
"x-workspace-id": "ws-northstar-main",
|
|
"x-project-id": "northstar-pilot"
|
|
});
|
|
assert(northstar.response.ok, "组织切换失败");
|
|
assert(northstar.payload.context.currentOrganization.id === "org-northstar", "组织 scope 没有切换");
|
|
assert(northstar.payload.platform.modelRegistry.every((model) => model.id === "northstar-image"), "模型没有按组织隔离");
|
|
|
|
const writerInvite = await request("/api/organizations/org-studio-lab/invitations", {
|
|
...writerAuth,
|
|
...scopeHeaders
|
|
}, { method: "POST", body: JSON.stringify({ email: "blocked-smoke@local.test", roleKey: "writer" }), headers: { "content-type": "application/json" } });
|
|
assert(writerInvite.response.status === 403 && writerInvite.payload.error === "permission_denied", "编剧越权邀请未被拒绝");
|
|
|
|
const crossOrg = await request("/api/context", {
|
|
...writerAuth,
|
|
"x-organization-id": "org-northstar",
|
|
"x-workspace-id": "ws-northstar-main"
|
|
});
|
|
assert(crossOrg.response.status === 403 && crossOrg.payload.error === "organization_forbidden", "跨组织访问未被拒绝");
|
|
|
|
console.log(`tenant smoke passed: ${base}`);
|