import { assertSafeSqlIdentifier } from './user-data-space-service.mjs'; export const PAGE_DATA_VISITOR_ROLES = new Set(['viewer', 'editor', 'owner']); export const DEFAULT_ROLE_PERMISSIONS = { viewer: { read: true, insert: false, update: false, softDelete: false }, editor: { read: true, insert: true, update: true, softDelete: true }, owner: { read: true, insert: true, update: true, softDelete: true, hardDelete: false }, }; export function normalizeRowPolicy(raw) { if (!raw || typeof raw !== 'object') return null; const scope = String(raw.scope ?? 'all_rows').trim(); if (!['all_rows', 'own_rows', 'owner_only'].includes(scope)) { throw Object.assign(new Error(`不支持的 rowPolicy.scope:${scope}`), { code: 'invalid_policy' }); } const ownerColumn = assertSafeSqlIdentifier( raw.ownerColumn ?? raw.owner_column ?? 'created_by_user_id', 'rowPolicy.ownerColumn', ); return { scope, ownerColumn }; } export function normalizeVisitorAssignments(raw) { if (!raw || typeof raw !== 'object') return {}; const visitors = {}; for (const [userId, value] of Object.entries(raw)) { const visitorId = String(userId ?? '').trim(); if (!visitorId) continue; const role = typeof value === 'string' ? value.trim() : String(value?.role ?? '').trim(); if (!PAGE_DATA_VISITOR_ROLES.has(role) || role === 'owner') { throw Object.assign(new Error(`访问者角色无效:${role}`), { code: 'invalid_policy' }); } visitors[visitorId] = role; } return visitors; } export function normalizeRoleOverrides(raw) { if (!raw || typeof raw !== 'object') return {}; const roles = {}; for (const [roleName, permissions] of Object.entries(raw)) { const role = String(roleName ?? '').trim(); if (!PAGE_DATA_VISITOR_ROLES.has(role)) { throw Object.assign(new Error(`角色定义无效:${role}`), { code: 'invalid_policy' }); } roles[role] = { read: Boolean(permissions?.read), insert: Boolean(permissions?.insert), update: Boolean(permissions?.update), softDelete: Boolean(permissions?.softDelete ?? permissions?.soft_delete), hardDelete: Boolean(permissions?.hardDelete ?? permissions?.hard_delete), }; } return roles; } export function resolveVisitorRole(policy, visitorUserId) { const visitorId = String(visitorUserId ?? '').trim(); if (!visitorId) return null; if (visitorId === policy.ownerUserId) return 'owner'; const assigned = policy.visitors?.[visitorId]; if (assigned) return assigned; const defaultRole = String(policy.defaultVisitorRole ?? 'deny').trim(); if (defaultRole === 'deny') return null; if (!PAGE_DATA_VISITOR_ROLES.has(defaultRole)) return null; return defaultRole; } export function resolveRolePermissions(policy, role) { const defaults = DEFAULT_ROLE_PERMISSIONS[role] ?? DEFAULT_ROLE_PERMISSIONS.viewer; const overrides = policy.roles?.[role]; if (!overrides) return { ...defaults }; return { read: overrides.read ?? defaults.read, insert: overrides.insert ?? defaults.insert, update: overrides.update ?? defaults.update, softDelete: overrides.softDelete ?? defaults.softDelete, hardDelete: overrides.hardDelete ?? defaults.hardDelete, }; } export function roleAllowsAction(rolePermissions, action) { if (!rolePermissions) return false; if (action === 'read') return Boolean(rolePermissions.read); if (action === 'insert') return Boolean(rolePermissions.insert); if (action === 'update') return Boolean(rolePermissions.update); if (action === 'soft_delete') return Boolean(rolePermissions.softDelete); if (action === 'hard_delete') return Boolean(rolePermissions.hardDelete); return false; } export function buildEffectiveDatasetForRole(registryDataset, policyDataset, rolePermissions) { if (!registryDataset || !policyDataset || !rolePermissions) return null; const actions = []; if (policyDataset.read && rolePermissions.read) actions.push('read'); if (policyDataset.insert && rolePermissions.insert) actions.push('insert'); if (policyDataset.update && rolePermissions.update) actions.push('update'); if (policyDataset.softDelete && rolePermissions.softDelete) actions.push('soft_delete'); if (policyDataset.hardDelete && rolePermissions.hardDelete) actions.push('hard_delete'); if (!actions.length) return null; const columns = { read: policyDataset.columns?.read ?? registryDataset.columns?.read ?? [], insert: policyDataset.columns?.insert ?? registryDataset.columns?.insert ?? [], update: policyDataset.columns?.update ?? registryDataset.columns?.update ?? [], soft_delete: policyDataset.columns?.soft_delete ?? registryDataset.columns?.soft_delete ?? ['id'], }; return { name: registryDataset.name, table: registryDataset.table, description: registryDataset.description, actions, columns, limits: policyDataset.limits ?? registryDataset.limits, rowPolicy: policyDataset.rowPolicy ?? null, }; } export function resolveRowAccessScope(rowPolicy, { visitorRole, visitorUserId, ownerUserId, tableColumns = [], } = {}) { if (!rowPolicy || rowPolicy.scope === 'all_rows') { return { bypass: true, whereClause: null, ownerColumn: null, visitorUserId }; } const isOwner = visitorRole === 'owner' || visitorUserId === ownerUserId; if (rowPolicy.scope === 'owner_only') { if (isOwner) return { bypass: true, whereClause: null, ownerColumn: null, visitorUserId }; throw Object.assign(new Error('当前数据集仅页面所有者可访问'), { code: 'row_not_allowed' }); } if (rowPolicy.scope === 'own_rows') { if (isOwner) return { bypass: true, whereClause: null, ownerColumn: rowPolicy.ownerColumn, visitorUserId }; const column = rowPolicy.ownerColumn; const hasColumn = tableColumns.some((item) => item.name === column); if (!hasColumn) { throw Object.assign(new Error(`行级权限列不存在:${column}`), { code: 'row_policy_invalid' }); } const safeVisitorId = String(visitorUserId ?? '').replace(/'/g, "''"); return { bypass: false, whereClause: `"${column}" = '${safeVisitorId}'`, ownerColumn: column, visitorUserId, }; } return { bypass: true, whereClause: null, ownerColumn: null, visitorUserId }; }