420 lines
12 KiB
JavaScript
420 lines
12 KiB
JavaScript
export const PORTAL_ACCESS_CLASS = Object.freeze({
|
|
PUBLIC: 'public',
|
|
OPTIONAL_USER: 'optional-user',
|
|
AUTHENTICATED: 'authenticated',
|
|
INTERNAL: 'internal',
|
|
ROUTE_AUTHORIZED: 'route-authorized',
|
|
});
|
|
|
|
export const PORTAL_ACCESS_POLICY_MODE = Object.freeze({
|
|
OFF: 'off',
|
|
SHADOW: 'shadow',
|
|
});
|
|
|
|
export const PORTAL_ACCESS_ENFORCEMENT_GROUPS = Object.freeze([
|
|
'legacy-page-data',
|
|
'page-data-public',
|
|
'plaza-optional-user',
|
|
]);
|
|
|
|
const DEFAULT_SHADOW_LOG_INTERVAL_MS = 60_000;
|
|
const MIN_SHADOW_LOG_INTERVAL_MS = 1_000;
|
|
const MAX_SHADOW_LOG_INTERVAL_MS = 60 * 60_000;
|
|
|
|
function freezeRule(rule) {
|
|
return Object.freeze({
|
|
...rule,
|
|
paths: rule.paths ? Object.freeze([...rule.paths]) : undefined,
|
|
methods: rule.methods ? Object.freeze([...rule.methods]) : undefined,
|
|
});
|
|
}
|
|
|
|
export const PORTAL_STATIC_ACCESS_RULES = Object.freeze([
|
|
freezeRule({
|
|
id: 'runtime-status',
|
|
accessClass: PORTAL_ACCESS_CLASS.PUBLIC,
|
|
legacyDirectBypass: true,
|
|
paths: ['/status', '/runtime/status'],
|
|
}),
|
|
freezeRule({
|
|
id: 'blocked-words',
|
|
accessClass: PORTAL_ACCESS_CLASS.PUBLIC,
|
|
legacyDirectBypass: true,
|
|
paths: ['/config/blocked-words'],
|
|
}),
|
|
freezeRule({
|
|
id: 'internal-agent',
|
|
accessClass: PORTAL_ACCESS_CLASS.INTERNAL,
|
|
legacyDirectBypass: true,
|
|
pathPrefix: '/internal/agent/',
|
|
}),
|
|
freezeRule({
|
|
id: 'internal-deep-search-llm',
|
|
accessClass: PORTAL_ACCESS_CLASS.INTERNAL,
|
|
legacyDirectBypass: true,
|
|
paths: ['/internal/deep-search/llm'],
|
|
}),
|
|
freezeRule({
|
|
id: 'mindspace-agent-callback',
|
|
accessClass: PORTAL_ACCESS_CLASS.INTERNAL,
|
|
legacyDirectBypass: true,
|
|
paths: [
|
|
'/agent/mindspace_page_patch',
|
|
'/agent/mindspace_asset_delete',
|
|
'/agent/mindspace_asset_download',
|
|
'/agent/mindspace_image_generate',
|
|
],
|
|
}),
|
|
freezeRule({
|
|
id: 'image-make-runtime-config',
|
|
accessClass: PORTAL_ACCESS_CLASS.INTERNAL,
|
|
legacyDirectBypass: true,
|
|
paths: ['/internal/image-make/runtime-config'],
|
|
}),
|
|
freezeRule({
|
|
id: 'mindspace-asset-download',
|
|
accessClass: PORTAL_ACCESS_CLASS.ROUTE_AUTHORIZED,
|
|
legacyDirectBypass: true,
|
|
methods: ['GET'],
|
|
pathPattern: /^\/mindspace\/v1\/assets\/[^/]+\/download$/,
|
|
}),
|
|
freezeRule({
|
|
id: 'plaza-events',
|
|
policyGroup: 'plaza-optional-user',
|
|
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
|
|
methods: ['POST'],
|
|
paths: ['/plaza/v1/events'],
|
|
}),
|
|
freezeRule({
|
|
id: 'plaza-discovery',
|
|
policyGroup: 'plaza-optional-user',
|
|
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
|
|
methods: ['GET'],
|
|
paths: ['/plaza/v1/feed', '/plaza/v1/categories', '/plaza/v1/seo/sitemap'],
|
|
}),
|
|
freezeRule({
|
|
id: 'plaza-post-detail',
|
|
policyGroup: 'plaza-optional-user',
|
|
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
|
|
methods: ['GET'],
|
|
pathPattern: /^\/plaza\/v1\/posts\/[^/]+$/,
|
|
}),
|
|
freezeRule({
|
|
id: 'plaza-post-comments',
|
|
policyGroup: 'plaza-optional-user',
|
|
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
|
|
methods: ['GET'],
|
|
pathPattern: /^\/plaza\/v1\/posts\/[^/]+\/comments$/,
|
|
}),
|
|
freezeRule({
|
|
id: 'plaza-user-profile',
|
|
policyGroup: 'plaza-optional-user',
|
|
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
|
|
methods: ['GET'],
|
|
pathPattern: /^\/plaza\/v1\/users\/[^/]+$/,
|
|
}),
|
|
freezeRule({
|
|
id: 'plaza-user-posts',
|
|
policyGroup: 'plaza-optional-user',
|
|
accessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
|
|
methods: ['GET'],
|
|
pathPattern: /^\/plaza\/v1\/users\/[^/]+\/posts$/,
|
|
}),
|
|
]);
|
|
|
|
function normalizeRequest({ path, method } = {}) {
|
|
return {
|
|
path: String(path ?? ''),
|
|
method: String(method ?? 'GET').trim().toUpperCase() || 'GET',
|
|
};
|
|
}
|
|
|
|
function ruleMatches(rule, request) {
|
|
if (rule.methods && !rule.methods.includes(request.method)) return false;
|
|
if (rule.paths?.includes(request.path)) return true;
|
|
if (rule.pathPrefix && request.path.startsWith(rule.pathPrefix)) return true;
|
|
return Boolean(rule.pathPattern?.test(request.path));
|
|
}
|
|
|
|
export function findPortalStaticAccessRule(requestInput) {
|
|
const request = normalizeRequest(requestInput);
|
|
return PORTAL_STATIC_ACCESS_RULES.find((rule) => ruleMatches(rule, request)) ?? null;
|
|
}
|
|
|
|
export function resolvePortalAccessPolicyMode(env = process.env) {
|
|
const raw = String(env.MEMIND_PORTAL_ACCESS_POLICY_MODE ?? '').trim().toLowerCase();
|
|
return raw === PORTAL_ACCESS_POLICY_MODE.SHADOW
|
|
? PORTAL_ACCESS_POLICY_MODE.SHADOW
|
|
: PORTAL_ACCESS_POLICY_MODE.OFF;
|
|
}
|
|
|
|
export function resolvePortalAccessShadowLogIntervalMs(env = process.env) {
|
|
const parsed = Number(env.MEMIND_PORTAL_ACCESS_SHADOW_LOG_INTERVAL_MS);
|
|
if (!Number.isFinite(parsed)) return DEFAULT_SHADOW_LOG_INTERVAL_MS;
|
|
return Math.min(
|
|
MAX_SHADOW_LOG_INTERVAL_MS,
|
|
Math.max(MIN_SHADOW_LOG_INTERVAL_MS, Math.trunc(parsed)),
|
|
);
|
|
}
|
|
|
|
function isExplicitlyEnabled(value) {
|
|
return String(value ?? '').trim() === '1';
|
|
}
|
|
|
|
export function resolvePortalAccessEnforcementConfig(env = process.env) {
|
|
const masterEnabled = isExplicitlyEnabled(
|
|
env.MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED,
|
|
);
|
|
const killSwitch = isExplicitlyEnabled(
|
|
env.MEMIND_PORTAL_ACCESS_POLICY_KILL_SWITCH,
|
|
);
|
|
const requestedGroups = [
|
|
...new Set(
|
|
String(env.MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS ?? '')
|
|
.split(',')
|
|
.map((group) => group.trim())
|
|
.filter(Boolean),
|
|
),
|
|
];
|
|
const unknownGroups = requestedGroups.filter(
|
|
(group) => !PORTAL_ACCESS_ENFORCEMENT_GROUPS.includes(group),
|
|
);
|
|
|
|
if (masterEnabled && unknownGroups.length > 0) {
|
|
throw new Error(
|
|
`Unknown Portal access enforcement group(s): ${unknownGroups.join(', ')}`,
|
|
);
|
|
}
|
|
|
|
const activeGroups =
|
|
masterEnabled && !killSwitch
|
|
? requestedGroups.filter((group) =>
|
|
PORTAL_ACCESS_ENFORCEMENT_GROUPS.includes(group))
|
|
: [];
|
|
|
|
return Object.freeze({
|
|
masterEnabled,
|
|
killSwitch,
|
|
enabled: activeGroups.length > 0,
|
|
requestedGroups: Object.freeze(requestedGroups),
|
|
activeGroups: Object.freeze(activeGroups),
|
|
unknownGroups: Object.freeze(unknownGroups),
|
|
});
|
|
}
|
|
|
|
export function isPortalLegacyDirectGlobalAuthBypass(path, method) {
|
|
return Boolean(
|
|
findPortalStaticAccessRule({ path, method })?.legacyDirectBypass,
|
|
);
|
|
}
|
|
|
|
export function isPortalPlazaOptionalUserPath(path, method) {
|
|
return (
|
|
findPortalStaticAccessRule({ path, method })?.policyGroup ===
|
|
'plaza-optional-user'
|
|
);
|
|
}
|
|
|
|
export function resolvePortalApiAccess(
|
|
requestInput,
|
|
{
|
|
isPageDataPublicPath = () => false,
|
|
isLegacyPageDataApiPath = () => false,
|
|
} = {},
|
|
) {
|
|
const request = normalizeRequest(requestInput);
|
|
const staticRule = findPortalStaticAccessRule(request);
|
|
if (staticRule) {
|
|
return {
|
|
accessClass: staticRule.accessClass,
|
|
ruleId: staticRule.id,
|
|
policyGroup: staticRule.policyGroup ?? staticRule.id,
|
|
};
|
|
}
|
|
|
|
if (isPageDataPublicPath(request.path, request.method)) {
|
|
return {
|
|
accessClass: PORTAL_ACCESS_CLASS.ROUTE_AUTHORIZED,
|
|
ruleId: 'page-data-public',
|
|
policyGroup: 'page-data-public',
|
|
};
|
|
}
|
|
|
|
if (isLegacyPageDataApiPath(request.path)) {
|
|
return {
|
|
accessClass: PORTAL_ACCESS_CLASS.PUBLIC,
|
|
ruleId: 'legacy-page-data',
|
|
policyGroup: 'legacy-page-data',
|
|
};
|
|
}
|
|
|
|
return {
|
|
accessClass: PORTAL_ACCESS_CLASS.AUTHENTICATED,
|
|
ruleId: 'authenticated-default',
|
|
policyGroup: 'authenticated-default',
|
|
};
|
|
}
|
|
|
|
export function classifyPortalApiAccess(requestInput, predicates = {}) {
|
|
return resolvePortalApiAccess(requestInput, predicates).accessClass;
|
|
}
|
|
|
|
export function portalAccessBypassesGlobalAuth(accessClass) {
|
|
return accessClass !== PORTAL_ACCESS_CLASS.AUTHENTICATED;
|
|
}
|
|
|
|
export function resolvePortalAccessEnforcementDecision(
|
|
requestInput,
|
|
predicates,
|
|
config,
|
|
) {
|
|
const proposed = resolvePortalApiAccess(requestInput, predicates);
|
|
const activeGroups = config?.activeGroups ?? [];
|
|
const selected =
|
|
Boolean(config?.enabled) &&
|
|
activeGroups.includes(proposed.policyGroup);
|
|
let reason = 'group-disabled';
|
|
if (config?.killSwitch) reason = 'kill-switch';
|
|
else if (!config?.masterEnabled) reason = 'master-disabled';
|
|
else if (selected) reason = 'group-enabled';
|
|
|
|
return {
|
|
selected,
|
|
bypassGlobalAuth:
|
|
selected && portalAccessBypassesGlobalAuth(proposed.accessClass),
|
|
reason,
|
|
proposedAccessClass: proposed.accessClass,
|
|
proposedRuleId: proposed.ruleId,
|
|
policyGroup: proposed.policyGroup,
|
|
};
|
|
}
|
|
|
|
export function shouldOverridePortalLegacyGlobalAuth(
|
|
decision,
|
|
currentGlobalAuthBypass,
|
|
) {
|
|
return (
|
|
Boolean(decision?.selected) &&
|
|
Boolean(decision?.bypassGlobalAuth) &&
|
|
!Boolean(currentGlobalAuthBypass)
|
|
);
|
|
}
|
|
|
|
export function comparePortalAccessPolicyShadow(
|
|
requestInput,
|
|
{
|
|
multiUserEnabled = false,
|
|
isPageDataPublicPath = () => false,
|
|
isLegacyPageDataApiPath = () => false,
|
|
} = {},
|
|
) {
|
|
const request = normalizeRequest(requestInput);
|
|
const plazaOptionalUser = isPortalPlazaOptionalUserPath(request.path, request.method);
|
|
const pageDataRouteAuthorized = isPageDataPublicPath(request.path, request.method);
|
|
const legacyPageDataPublic = isLegacyPageDataApiPath(request.path);
|
|
const currentGlobalAuthBypass =
|
|
isPortalLegacyDirectGlobalAuthBypass(request.path, request.method) ||
|
|
(
|
|
Boolean(multiUserEnabled) &&
|
|
(plazaOptionalUser || pageDataRouteAuthorized || legacyPageDataPublic)
|
|
);
|
|
const proposed = resolvePortalApiAccess(request, {
|
|
isPageDataPublicPath,
|
|
isLegacyPageDataApiPath,
|
|
});
|
|
const proposedGlobalAuthBypass = portalAccessBypassesGlobalAuth(proposed.accessClass);
|
|
|
|
return {
|
|
path: request.path,
|
|
method: request.method,
|
|
proposedAccessClass: proposed.accessClass,
|
|
proposedRuleId: proposed.ruleId,
|
|
policyGroup: proposed.policyGroup,
|
|
currentGlobalAuthBypass,
|
|
proposedGlobalAuthBypass,
|
|
mismatch: currentGlobalAuthBypass !== proposedGlobalAuthBypass,
|
|
};
|
|
}
|
|
|
|
export function assessPortalAccessMigrationReadiness(
|
|
requests,
|
|
{
|
|
knownMismatchPolicyGroups = [],
|
|
...comparisonOptions
|
|
} = {},
|
|
) {
|
|
const knownGroups = new Set(knownMismatchPolicyGroups);
|
|
const comparisons = requests.map((request) =>
|
|
comparePortalAccessPolicyShadow(request, comparisonOptions));
|
|
const mismatches = comparisons.filter((comparison) => comparison.mismatch);
|
|
const knownMismatches = mismatches.filter((comparison) =>
|
|
knownGroups.has(comparison.policyGroup));
|
|
const unexpectedMismatches = mismatches.filter((comparison) =>
|
|
!knownGroups.has(comparison.policyGroup));
|
|
|
|
return {
|
|
contractReady: unexpectedMismatches.length === 0,
|
|
safeForBehaviorSwitch: mismatches.length === 0,
|
|
total: comparisons.length,
|
|
aligned: comparisons.length - mismatches.length,
|
|
knownMismatches,
|
|
unexpectedMismatches,
|
|
};
|
|
}
|
|
|
|
export function createPortalAccessShadowReporter({
|
|
intervalMs = DEFAULT_SHADOW_LOG_INTERVAL_MS,
|
|
now = Date.now,
|
|
emit = () => {},
|
|
} = {}) {
|
|
const effectiveIntervalMs = Math.min(
|
|
MAX_SHADOW_LOG_INTERVAL_MS,
|
|
Math.max(MIN_SHADOW_LOG_INTERVAL_MS, Math.trunc(Number(intervalMs) || 0)),
|
|
);
|
|
const counters = new Map();
|
|
|
|
function record(event) {
|
|
if (!event?.mismatch) return null;
|
|
const key = [
|
|
event.authMode ?? 'unknown',
|
|
event.method ?? 'GET',
|
|
event.policyGroup ?? event.proposedRuleId ?? 'unknown',
|
|
].join(':');
|
|
const timestamp = Number(now());
|
|
const counter = counters.get(key) ?? {
|
|
key,
|
|
totalOccurrences: 0,
|
|
suppressedSinceLast: 0,
|
|
lastEmittedAt: null,
|
|
};
|
|
counter.totalOccurrences += 1;
|
|
|
|
const shouldEmit =
|
|
counter.lastEmittedAt === null ||
|
|
timestamp - counter.lastEmittedAt >= effectiveIntervalMs;
|
|
if (!shouldEmit) {
|
|
counter.suppressedSinceLast += 1;
|
|
counters.set(key, counter);
|
|
return null;
|
|
}
|
|
|
|
const payload = {
|
|
...event,
|
|
occurrences: counter.totalOccurrences,
|
|
suppressedSinceLast: counter.suppressedSinceLast,
|
|
shadowLogIntervalMs: effectiveIntervalMs,
|
|
};
|
|
counter.lastEmittedAt = timestamp;
|
|
counter.suppressedSinceLast = 0;
|
|
counters.set(key, counter);
|
|
emit(payload);
|
|
return payload;
|
|
}
|
|
|
|
function snapshot() {
|
|
return [...counters.values()].map((counter) => ({ ...counter }));
|
|
}
|
|
|
|
return Object.freeze({ record, snapshot });
|
|
}
|