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
+15 -8
View File
@@ -114,7 +114,8 @@ function accessibleProjects(workspaceId, userId, organizationId) {
FROM projects p
LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = ? AND pm.status = 'active'
JOIN workspace_members wm ON wm.workspace_id = p.workspace_id AND wm.user_id = ? AND wm.status = 'active'
WHERE p.workspace_id = ? AND (pm.user_id IS NOT NULL OR wm.user_id IS NOT NULL)
WHERE p.workspace_id = ?
AND (wm.access_mode IS NULL OR wm.access_mode = 'all' OR (wm.access_mode = 'project-only' AND pm.user_id IS NOT NULL))
ORDER BY CASE WHEN p.status = 'archived' THEN 1 ELSE 0 END, p.updated_at DESC`,
[userId, userId, workspaceId]
);
@@ -179,7 +180,8 @@ function ensureProject(userId, organization, workspace, projectId) {
[projectId, userId]
);
const workspaceMember = dbGet("SELECT * FROM workspace_members WHERE workspace_id = ? AND user_id = ? AND status = 'active'", [workspace.id, userId]);
if (!membership && !workspaceMember && !canSeeAllWorkspaceData(organization.id, userId)) {
const workspaceCanSeeProject = workspaceMember && (workspaceMember.access_mode !== "project-only" || Boolean(membership));
if (!membership && !workspaceCanSeeProject && !canSeeAllWorkspaceData(organization.id, userId)) {
throw httpError(403, "project_forbidden", "当前用户没有该项目访问权", { projectId, userId });
}
return { project, membership };
@@ -304,7 +306,7 @@ export function orgMembers(organizationId) {
export function workspaceMembers(workspaceId) {
return dbAll(
`SELECT wm.id, wm.user_id, u.display_name, u.email, u.avatar_color, wm.role_key, r.name AS role_name, wm.status
`SELECT wm.id, wm.user_id, u.display_name, u.email, u.avatar_color, wm.role_key, r.name AS role_name, wm.access_mode, wm.status
FROM workspace_members wm
JOIN users u ON u.id = wm.user_id
LEFT JOIN roles r ON r.key = wm.role_key
@@ -486,7 +488,10 @@ export function previewInvitation(token) {
}
function invitationRoleForWorkspace(roleKey) {
return roleKey === "writer" || roleKey === "voice_editor" || roleKey === "art_director" ? roleKey : "producer";
if (["producer", "writer", "art_director", "voice_editor", "reviewer"].includes(roleKey)) return roleKey;
// Project invitations need a workspace membership for context resolution,
// but must not inherit workspace-wide production permissions.
return "project_guest";
}
function applyInvitationMembership(invitation, userId, timestamp) {
@@ -497,15 +502,17 @@ function applyInvitationMembership(invitation, userId, timestamp) {
[`om-${invitation.organization_id}-${userId}`, invitation.organization_id, userId, organizationRole, timestamp, timestamp, timestamp]
);
if (invitation.workspace_id) {
const accessMode = invitation.project_id ? "project-only" : "all";
dbRun(
"INSERT INTO workspace_members(id, workspace_id, user_id, role_key, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', ?, ?) ON CONFLICT(workspace_id, user_id) DO UPDATE SET role_key = excluded.role_key, status = 'active', updated_at = excluded.updated_at",
[`wm-${invitation.workspace_id}-${userId}`, invitation.workspace_id, userId, invitationRoleForWorkspace(invitation.role_key), timestamp, timestamp]
"INSERT INTO workspace_members(id, workspace_id, user_id, role_key, access_mode, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'active', ?, ?) ON CONFLICT(workspace_id, user_id) DO UPDATE SET role_key = excluded.role_key, access_mode = CASE WHEN workspace_members.access_mode = 'all' THEN 'all' ELSE excluded.access_mode END, status = 'active', updated_at = excluded.updated_at",
[`wm-${invitation.workspace_id}-${userId}`, invitation.workspace_id, userId, invitationRoleForWorkspace(invitation.role_key), accessMode, timestamp, timestamp]
);
}
if (invitation.project_id) {
const projectRole = ["project_editor", "project_viewer"].includes(invitation.role_key) ? invitation.role_key : "project_editor";
dbRun(
"INSERT INTO project_members(id, project_id, user_id, role_key, status, created_at, updated_at) VALUES (?, ?, ?, 'project_editor', 'active', ?, ?) ON CONFLICT(project_id, user_id) DO UPDATE SET status = 'active', updated_at = excluded.updated_at",
[`pm-${invitation.project_id}-${userId}`, invitation.project_id, userId, timestamp, timestamp]
"INSERT INTO project_members(id, project_id, user_id, role_key, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', ?, ?) ON CONFLICT(project_id, user_id) DO UPDATE SET role_key = excluded.role_key, status = 'active', updated_at = excluded.updated_at",
[`pm-${invitation.project_id}-${userId}`, invitation.project_id, userId, projectRole, timestamp, timestamp]
);
}
}