Files
memind/page-access-policy.mjs

237 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { assertSafeSqlIdentifier, normalizeDatasetConfig } from './user-data-space-service.mjs';
import {
normalizeRowPolicy,
normalizeRoleOverrides,
normalizeVisitorAssignments,
} from './page-access-visitor.mjs';
export const PAGE_DATA_ACCESS_MODES = new Set(['public', 'password', 'login_required']);
export function normalizePolicyDataset(name, raw) {
const datasetName = assertSafeSqlIdentifier(name, 'dataset 名称');
if (!raw || typeof raw !== 'object') {
throw Object.assign(new Error(`dataset 策略无效:${datasetName}`), { code: 'invalid_policy' });
}
const columns = raw.columns && typeof raw.columns === 'object' ? raw.columns : {};
const normalizedColumns = {};
for (const [action, fields] of Object.entries(columns)) {
if (!Array.isArray(fields)) continue;
normalizedColumns[action] = fields.map((field) => assertSafeSqlIdentifier(field, `${action} 字段`));
}
return {
name: datasetName,
read: Boolean(raw.read),
insert: Boolean(raw.insert),
update: Boolean(raw.update),
softDelete: Boolean(raw.softDelete ?? raw.soft_delete),
// `delete` is the page-facing spelling for an intentional permanent PG
// delete; retain hardDelete for existing policies.
hardDelete: Boolean(raw.hardDelete ?? raw.hard_delete ?? raw.delete),
columns: normalizedColumns,
limits:
raw.limits && typeof raw.limits === 'object'
? {
maxRowsPerRead: Number(raw.limits.maxRowsPerRead ?? 100),
maxInsertBytes: Number(raw.limits.maxInsertBytes ?? 8192),
}
: undefined,
rowPolicy: normalizeRowPolicy(raw.rowPolicy ?? raw.row_policy),
closed: Boolean(raw.closed),
closedAt: raw.closedAt ?? raw.closed_at ?? null,
};
}
export function normalizePageAccessPolicy(raw, { fallbackPageId = null, fallbackOwnerUserId = null } = {}) {
if (!raw || typeof raw !== 'object') {
throw Object.assign(new Error('Page Access Policy 无效'), { code: 'invalid_policy' });
}
const pageId = String(raw.pageId ?? fallbackPageId ?? '').trim();
const ownerUserId = String(raw.ownerUserId ?? fallbackOwnerUserId ?? '').trim();
if (!pageId) throw Object.assign(new Error('缺少 pageId'), { code: 'invalid_policy' });
if (!ownerUserId) throw Object.assign(new Error('缺少 ownerUserId'), { code: 'invalid_policy' });
const accessMode = String(raw.accessMode ?? raw.access_mode ?? 'public').trim();
if (!PAGE_DATA_ACCESS_MODES.has(accessMode)) {
throw Object.assign(new Error(`不支持的 accessMode${accessMode}`), { code: 'invalid_policy' });
}
const datasetsInput = raw.datasets && typeof raw.datasets === 'object' ? raw.datasets : {};
const datasets = {};
for (const [name, config] of Object.entries(datasetsInput)) {
datasets[assertSafeSqlIdentifier(name, 'dataset 名称')] = normalizePolicyDataset(name, config);
}
const defaultVisitorRole = String(
raw.defaultVisitorRole ??
raw.default_visitor_role ??
(accessMode === 'login_required' ? 'viewer' : 'deny'),
).trim();
if (!['deny', 'viewer', 'editor'].includes(defaultVisitorRole)) {
throw Object.assign(new Error(`不支持的 defaultVisitorRole${defaultVisitorRole}`), {
code: 'invalid_policy',
});
}
return {
pageId,
ownerUserId,
workspaceRef: String(raw.workspaceRef ?? raw.workspace_ref ?? '').trim() || null,
accessMode,
defaultVisitorRole,
visitors: normalizeVisitorAssignments(raw.visitors),
roles: normalizeRoleOverrides(raw.roles),
datasets,
updatedAt: raw.updatedAt ?? new Date().toISOString(),
};
}
export function buildEffectiveDataset(registryDataset, policyDataset) {
if (!registryDataset || !policyDataset) return null;
const actions = [];
if (policyDataset.read) actions.push('read');
if (policyDataset.insert) actions.push('insert');
if (policyDataset.update) actions.push('update');
if (policyDataset.softDelete) actions.push('soft_delete');
if (policyDataset.hardDelete) actions.push('hard_delete');
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 normalizeDatasetConfig({
name: registryDataset.name,
table: registryDataset.table,
description: registryDataset.description,
actions,
columns,
limits: policyDataset.limits ?? registryDataset.limits,
});
}
export function defaultDatasetsForAccessMode(accessMode, datasets = {}) {
const normalized = {};
for (const [name, config] of Object.entries(datasets)) {
const base = normalizePolicyDataset(name, config);
if (accessMode === 'public') {
normalized[name] = {
...base,
read: false,
update: false,
softDelete: false,
hardDelete: false,
};
} else if (accessMode === 'password') {
normalized[name] = {
...base,
hardDelete: false,
};
} else {
normalized[name] = {
...base,
hardDelete: false,
};
}
}
return normalized;
}
export function policyAllowsAction(policy, datasetName, action) {
const dataset = policy?.datasets?.[datasetName];
if (!dataset) return false;
if (dataset.closed) return false;
if (action === 'read') return dataset.read;
if (action === 'insert') return dataset.insert;
if (action === 'update') return dataset.update;
if (action === 'soft_delete') return dataset.softDelete;
if (action === 'hard_delete') return dataset.hardDelete;
return false;
}
export function closePolicyDataset(policy, datasetName) {
const dataset = policy?.datasets?.[datasetName];
if (!dataset) {
throw Object.assign(new Error('dataset 未授权'), { code: 'dataset_not_found' });
}
return {
...policy,
datasets: {
...policy.datasets,
[datasetName]: {
...dataset,
read: false,
insert: false,
update: false,
softDelete: false,
hardDelete: false,
closed: true,
closedAt: new Date().toISOString(),
},
},
updatedAt: new Date().toISOString(),
};
}
export function mapPublicationAccessModeToPageData(accessMode) {
const mode = String(accessMode ?? '').trim();
if (PAGE_DATA_ACCESS_MODES.has(mode)) return mode;
return null;
}
export function buildDatasetPolicyFromRegistry(
registryDataset,
{ read = false, insert = false, update = false, softDelete = false, accessMode = 'public' } = {},
) {
const rowPolicy =
accessMode === 'login_required' && (update || softDelete || read)
? { scope: 'own_rows', ownerColumn: 'created_by_user_id' }
: null;
return normalizePolicyDataset(registryDataset?.name ?? 'dataset', {
read: Boolean(read),
insert: Boolean(insert),
update: Boolean(update),
softDelete: Boolean(softDelete),
columns: {
read: registryDataset?.columns?.read ?? [],
insert: registryDataset?.columns?.insert ?? [],
update: registryDataset?.columns?.update ?? [],
soft_delete: registryDataset?.columns?.soft_delete ?? ['id'],
},
rowPolicy,
});
}
export function buildPageDataPolicyFromPublishInput({
pageId,
ownerUserId,
accessMode,
datasetName,
registryDataset,
capabilities = {},
}) {
const mappedAccessMode = mapPublicationAccessModeToPageData(accessMode);
if (!mappedAccessMode) {
throw Object.assign(new Error('当前发布访问模式不支持 Page Data'), { code: 'invalid_policy' });
}
const defaults =
mappedAccessMode === 'public'
? { read: false, insert: true, update: false, softDelete: false }
: mappedAccessMode === 'password'
? { read: true, insert: true, update: false, softDelete: false }
: { read: true, insert: true, update: true, softDelete: false };
const datasetPolicy = buildDatasetPolicyFromRegistry(registryDataset, {
read: capabilities.read ?? defaults.read,
insert: capabilities.insert ?? defaults.insert,
update: capabilities.update ?? defaults.update,
softDelete: capabilities.softDelete ?? defaults.softDelete,
accessMode: mappedAccessMode,
});
return normalizePageAccessPolicy(
{
pageId,
ownerUserId,
accessMode: mappedAccessMode,
datasets: {
[datasetName]: datasetPolicy,
},
},
{ fallbackPageId: pageId, fallbackOwnerUserId: ownerUserId },
);
}