Files
memind/server/portal-access-policy.test.mjs
2026-07-24 19:18:14 +08:00

444 lines
16 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { isLegacyPageDataApiPath } from '../page-data-routes.mjs';
import { isPageDataPublicPath } from '../page-data-public-service.mjs';
import {
assessPortalAccessMigrationReadiness,
classifyPortalApiAccess,
comparePortalAccessPolicyShadow,
createPortalAccessShadowReporter,
findPortalStaticAccessRule,
isPortalLegacyDirectGlobalAuthBypass,
isPortalPlazaOptionalUserPath,
PORTAL_ACCESS_CLASS,
PORTAL_ACCESS_ENFORCEMENT_GROUPS,
PORTAL_STATIC_ACCESS_RULES,
resolvePortalAccessEnforcementConfig,
resolvePortalAccessEnforcementDecision,
resolvePortalAccessPolicyMode,
resolvePortalAccessShadowLogIntervalMs,
shouldOverridePortalLegacyGlobalAuth,
} from './portal-access-policy.mjs';
const pageDataPredicates = {
isPageDataPublicPath,
isLegacyPageDataApiPath,
};
const boundaryCases = [
['/status', 'GET', PORTAL_ACCESS_CLASS.PUBLIC, true],
['/runtime/status', 'POST', PORTAL_ACCESS_CLASS.PUBLIC, true],
['/config/blocked-words', 'GET', PORTAL_ACCESS_CLASS.PUBLIC, true],
['/internal/agent/jobs/job-1/claim', 'POST', PORTAL_ACCESS_CLASS.INTERNAL, true],
['/internal/deep-search/llm', 'POST', PORTAL_ACCESS_CLASS.INTERNAL, true],
['/agent/mindspace_page_patch', 'POST', PORTAL_ACCESS_CLASS.INTERNAL, true],
['/agent/mindspace_asset_delete', 'POST', PORTAL_ACCESS_CLASS.INTERNAL, true],
['/agent/mindspace_asset_download', 'POST', PORTAL_ACCESS_CLASS.INTERNAL, true],
['/agent/mindspace_image_generate', 'POST', PORTAL_ACCESS_CLASS.INTERNAL, true],
['/internal/image-make/runtime-config', 'GET', PORTAL_ACCESS_CLASS.INTERNAL, true],
['/mindspace/v1/assets/asset-1/download', 'GET', PORTAL_ACCESS_CLASS.ROUTE_AUTHORIZED, true],
['/mindspace/v1/assets/asset-1/download', 'POST', PORTAL_ACCESS_CLASS.AUTHENTICATED, false],
['/plaza/v1/categories', 'GET', PORTAL_ACCESS_CLASS.OPTIONAL_USER, false],
['/plaza/v1/feed', 'get', PORTAL_ACCESS_CLASS.OPTIONAL_USER, false],
['/plaza/v1/posts/post-1', 'GET', PORTAL_ACCESS_CLASS.OPTIONAL_USER, false],
['/plaza/v1/posts/post-1/comments', 'GET', PORTAL_ACCESS_CLASS.OPTIONAL_USER, false],
['/plaza/v1/users/alice', 'GET', PORTAL_ACCESS_CLASS.OPTIONAL_USER, false],
['/plaza/v1/users/alice/posts', 'GET', PORTAL_ACCESS_CLASS.OPTIONAL_USER, false],
['/plaza/v1/events', 'POST', PORTAL_ACCESS_CLASS.OPTIONAL_USER, false],
['/plaza/v1/posts/post-1/reports', 'POST', PORTAL_ACCESS_CLASS.AUTHENTICATED, false],
[
'/public/pages/page-1/data/signups/rows',
'POST',
PORTAL_ACCESS_CLASS.ROUTE_AUTHORIZED,
false,
],
['/public/pages/page-1/data/signups', 'GET', PORTAL_ACCESS_CLASS.ROUTE_AUTHORIZED, false],
['/page-data/signups', 'GET', PORTAL_ACCESS_CLASS.PUBLIC, false],
['/mindspace/v1/pages', 'GET', PORTAL_ACCESS_CLASS.AUTHENTICATED, false],
];
test('static access registry has stable unique rule ids', () => {
const ids = PORTAL_STATIC_ACCESS_RULES.map((rule) => rule.id);
assert.equal(new Set(ids).size, ids.length);
assert.ok(ids.length >= 10);
for (const rule of PORTAL_STATIC_ACCESS_RULES) {
assert.ok(Object.values(PORTAL_ACCESS_CLASS).includes(rule.accessClass), rule.id);
}
});
test('portal access policy classifies the existing route boundary', () => {
for (const [path, method, expected] of boundaryCases) {
assert.equal(
classifyPortalApiAccess({ path, method }, pageDataPredicates),
expected,
`${method} ${path}`,
);
}
});
test('legacy direct bypass derives from the same access registry', () => {
for (const [path, method, _expectedClass, expectedDirectBypass] of boundaryCases) {
assert.equal(
isPortalLegacyDirectGlobalAuthBypass(path, method),
expectedDirectBypass,
`${method} ${path}`,
);
}
assert.equal(findPortalStaticAccessRule({ path: '/mindspace/v1/pages', method: 'GET' }), null);
});
test('Plaza optional-user matcher excludes authenticated write routes', () => {
assert.equal(isPortalPlazaOptionalUserPath('/plaza/v1/seo/sitemap', 'GET'), true);
assert.equal(isPortalPlazaOptionalUserPath('/plaza/v1/events', 'POST'), true);
assert.equal(isPortalPlazaOptionalUserPath('/plaza/v1/posts/post-1', 'PATCH'), false);
assert.equal(isPortalPlazaOptionalUserPath('/plaza/v1/posts/post-1/reports', 'POST'), false);
assert.equal(isPortalPlazaOptionalUserPath('/mindspace/v1/pages', 'GET'), false);
});
test('portal access policy flags are safely bounded', () => {
assert.equal(resolvePortalAccessPolicyMode({}), 'off');
assert.equal(
resolvePortalAccessPolicyMode({ MEMIND_PORTAL_ACCESS_POLICY_MODE: ' SHADOW ' }),
'shadow',
);
assert.equal(resolvePortalAccessPolicyMode({ MEMIND_PORTAL_ACCESS_POLICY_MODE: 'on' }), 'off');
assert.equal(resolvePortalAccessPolicyMode({ MEMIND_PORTAL_ACCESS_POLICY_MODE: 'invalid' }), 'off');
assert.equal(resolvePortalAccessShadowLogIntervalMs({}), 60_000);
assert.equal(
resolvePortalAccessShadowLogIntervalMs({
MEMIND_PORTAL_ACCESS_SHADOW_LOG_INTERVAL_MS: '1',
}),
1_000,
);
assert.equal(
resolvePortalAccessShadowLogIntervalMs({
MEMIND_PORTAL_ACCESS_SHADOW_LOG_INTERVAL_MS: '9999999',
}),
3_600_000,
);
});
test('access enforcement requires the master switch and an exact group', () => {
const defaultConfig = resolvePortalAccessEnforcementConfig({});
assert.equal(defaultConfig.enabled, false);
assert.deepEqual(defaultConfig.activeGroups, []);
const groupOnly = resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'legacy-page-data',
});
assert.equal(groupOnly.enabled, false);
assert.deepEqual(groupOnly.requestedGroups, ['legacy-page-data']);
const enabled = resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS:
' legacy-page-data,legacy-page-data ',
});
assert.equal(enabled.enabled, true);
assert.deepEqual(enabled.activeGroups, ['legacy-page-data']);
});
test('access enforcement rejects all and unknown groups when master is enabled', () => {
assert.deepEqual(PORTAL_ACCESS_ENFORCEMENT_GROUPS, [
'legacy-page-data',
'page-data-public',
'plaza-optional-user',
]);
assert.throws(
() =>
resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'all',
}),
/Unknown Portal access enforcement group\(s\): all/,
);
assert.throws(
() =>
resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS:
'legacy-page-data,typo-group',
}),
/typo-group/,
);
});
test('kill switch overrides every requested enforcement group', () => {
const config = resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS:
PORTAL_ACCESS_ENFORCEMENT_GROUPS.join(','),
MEMIND_PORTAL_ACCESS_POLICY_KILL_SWITCH: '1',
});
assert.equal(config.masterEnabled, true);
assert.equal(config.killSwitch, true);
assert.equal(config.enabled, false);
assert.deepEqual(config.activeGroups, []);
});
test('single-group enforcement bypasses only the selected policy group', () => {
const config = resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'legacy-page-data',
});
const legacyDecision = resolvePortalAccessEnforcementDecision(
{ path: '/page-data/signups', method: 'GET' },
pageDataPredicates,
config,
);
assert.deepEqual(legacyDecision, {
selected: true,
bypassGlobalAuth: true,
reason: 'group-enabled',
proposedAccessClass: PORTAL_ACCESS_CLASS.PUBLIC,
proposedRuleId: 'legacy-page-data',
policyGroup: 'legacy-page-data',
});
const publicPageDataDecision = resolvePortalAccessEnforcementDecision(
{ path: '/public/pages/page-1/data/signups', method: 'GET' },
pageDataPredicates,
config,
);
assert.equal(publicPageDataDecision.selected, false);
assert.equal(publicPageDataDecision.bypassGlobalAuth, false);
assert.equal(publicPageDataDecision.reason, 'group-disabled');
const privateDecision = resolvePortalAccessEnforcementDecision(
{ path: '/mindspace/v1/pages', method: 'GET' },
pageDataPredicates,
config,
);
assert.equal(privateDecision.selected, false);
assert.equal(privateDecision.bypassGlobalAuth, false);
assert.equal(privateDecision.policyGroup, 'authenticated-default');
});
test('enforcement preserves legacy optional-user hydration when already bypassed', () => {
const config = resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'page-data-public',
});
const decision = resolvePortalAccessEnforcementDecision(
{ path: '/public/pages/page-1/data/signups', method: 'GET' },
pageDataPredicates,
config,
);
assert.equal(decision.selected, true);
assert.equal(decision.bypassGlobalAuth, true);
assert.equal(
shouldOverridePortalLegacyGlobalAuth(decision, false),
true,
'no-auth and legacy modes need the new bypass',
);
assert.equal(
shouldOverridePortalLegacyGlobalAuth(decision, true),
false,
'multi-user mode must continue through legacy hydration',
);
});
test('enforcement decision reports kill switch and master-off reasons', () => {
const request = { path: '/page-data/signups', method: 'GET' };
const groupOnly = resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'legacy-page-data',
});
assert.equal(
resolvePortalAccessEnforcementDecision(request, pageDataPredicates, groupOnly).reason,
'master-disabled',
);
const killed = resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'legacy-page-data',
MEMIND_PORTAL_ACCESS_POLICY_KILL_SWITCH: '1',
});
assert.equal(
resolvePortalAccessEnforcementDecision(request, pageDataPredicates, killed).reason,
'kill-switch',
);
});
test('shadow comparison matches current direct bypass and authenticated routes', () => {
const cases = [
{ path: '/status', method: 'GET', multiUserEnabled: false },
{ path: '/internal/agent/jobs/job-1/claim', method: 'POST', multiUserEnabled: false },
{
path: '/mindspace/v1/assets/asset-1/download',
method: 'GET',
multiUserEnabled: false,
},
{ path: '/plaza/v1/categories', method: 'GET', multiUserEnabled: true },
{ path: '/mindspace/v1/pages', method: 'GET', multiUserEnabled: false },
{ path: '/mindspace/v1/pages', method: 'GET', multiUserEnabled: true },
];
for (const request of cases) {
const comparison = comparePortalAccessPolicyShadow(request, {
...pageDataPredicates,
multiUserEnabled: request.multiUserEnabled,
});
assert.equal(comparison.mismatch, false, `${request.method} ${request.path}`);
}
});
test('shadow comparison exposes public-route coupling to the multi-user auth mode', () => {
const cases = [
['/plaza/v1/categories', 'GET', PORTAL_ACCESS_CLASS.OPTIONAL_USER, 'plaza-optional-user'],
[
'/public/pages/page-1/data/signups',
'GET',
PORTAL_ACCESS_CLASS.ROUTE_AUTHORIZED,
'page-data-public',
],
['/page-data/signups', 'GET', PORTAL_ACCESS_CLASS.PUBLIC, 'legacy-page-data'],
];
for (const [path, method, expectedClass, expectedGroup] of cases) {
const comparison = comparePortalAccessPolicyShadow(
{ path, method },
{
...pageDataPredicates,
multiUserEnabled: false,
},
);
assert.equal(comparison.proposedAccessClass, expectedClass);
assert.equal(comparison.policyGroup, expectedGroup);
assert.equal(comparison.currentGlobalAuthBypass, false);
assert.equal(comparison.proposedGlobalAuthBypass, true);
assert.equal(comparison.mismatch, true);
}
});
test('shadow comparison stays aligned for public routes in multi-user mode', () => {
const comparison = comparePortalAccessPolicyShadow(
{ path: '/plaza/v1/categories', method: 'GET' },
{
...pageDataPredicates,
multiUserEnabled: true,
},
);
assert.deepEqual(comparison, {
path: '/plaza/v1/categories',
method: 'GET',
proposedAccessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
proposedRuleId: 'plaza-discovery',
policyGroup: 'plaza-optional-user',
currentGlobalAuthBypass: true,
proposedGlobalAuthBypass: true,
mismatch: false,
});
});
test('migration readiness separates known differences from unexpected drift', () => {
const requests = [
{ path: '/plaza/v1/categories', method: 'GET' },
{ path: '/public/pages/page-1/data/signups', method: 'GET' },
{ path: '/page-data/signups', method: 'GET' },
{ path: '/mindspace/v1/pages', method: 'GET' },
];
const knownMismatchPolicyGroups = [
'plaza-optional-user',
'page-data-public',
'legacy-page-data',
];
const report = assessPortalAccessMigrationReadiness(requests, {
...pageDataPredicates,
multiUserEnabled: false,
knownMismatchPolicyGroups,
});
assert.equal(report.contractReady, true);
assert.equal(report.safeForBehaviorSwitch, false);
assert.equal(report.total, 4);
assert.equal(report.aligned, 1);
assert.equal(report.knownMismatches.length, 3);
assert.equal(report.unexpectedMismatches.length, 0);
const strictReport = assessPortalAccessMigrationReadiness(requests, {
...pageDataPredicates,
multiUserEnabled: false,
knownMismatchPolicyGroups: knownMismatchPolicyGroups.slice(0, 2),
});
assert.equal(strictReport.contractReady, false);
assert.equal(strictReport.unexpectedMismatches[0].policyGroup, 'legacy-page-data');
});
test('multi-user route contract is ready and aligned', () => {
const report = assessPortalAccessMigrationReadiness(
boundaryCases.map(([path, method]) => ({ path, method })),
{
...pageDataPredicates,
multiUserEnabled: true,
},
);
assert.equal(report.contractReady, true);
assert.equal(report.safeForBehaviorSwitch, true);
assert.equal(report.aligned, boundaryCases.length);
});
test('shadow reporter groups dynamic routes, suppresses repeats, and preserves counts', () => {
let currentTime = 10_000;
const emitted = [];
const reporter = createPortalAccessShadowReporter({
intervalMs: 1_000,
now: () => currentTime,
emit: (event) => emitted.push(event),
});
const mismatch = {
authMode: 'none',
method: 'GET',
proposedAccessClass: PORTAL_ACCESS_CLASS.OPTIONAL_USER,
policyGroup: 'plaza-optional-user',
currentGlobalAuthBypass: false,
proposedGlobalAuthBypass: true,
mismatch: true,
};
reporter.record({
...mismatch,
requestId: 'req-1',
path: '/plaza/v1/posts/post-1',
proposedRuleId: 'plaza-post-detail',
});
currentTime += 500;
reporter.record({
...mismatch,
requestId: 'req-2',
path: '/plaza/v1/users/alice',
proposedRuleId: 'plaza-user-profile',
});
assert.equal(emitted.length, 1);
assert.deepEqual(reporter.snapshot(), [{
key: 'none:GET:plaza-optional-user',
totalOccurrences: 2,
suppressedSinceLast: 1,
lastEmittedAt: 10_000,
}]);
currentTime += 501;
const next = reporter.record({
...mismatch,
requestId: 'req-3',
path: '/plaza/v1/feed',
proposedRuleId: 'plaza-discovery',
});
assert.equal(emitted.length, 2);
assert.equal(next.occurrences, 3);
assert.equal(next.suppressedSinceLast, 1);
assert.equal(next.requestId, 'req-3');
});
test('shadow reporter ignores aligned requests', () => {
const emitted = [];
const reporter = createPortalAccessShadowReporter({
emit: (event) => emitted.push(event),
});
assert.equal(reporter.record({ mismatch: false }), null);
assert.deepEqual(reporter.snapshot(), []);
assert.deepEqual(emitted, []);
});