360 lines
10 KiB
JavaScript
360 lines
10 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 { createPortalApiAuthMiddleware } from './portal-api-auth-middleware.mjs';
|
|
import {
|
|
PORTAL_ACCESS_POLICY_MODE,
|
|
resolvePortalAccessEnforcementConfig,
|
|
} from './portal-access-policy.mjs';
|
|
|
|
function createResponseRecorder() {
|
|
return {
|
|
statusCode: 200,
|
|
body: null,
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(body) {
|
|
this.body = body;
|
|
return this;
|
|
},
|
|
};
|
|
}
|
|
|
|
async function invoke(middleware, overrides = {}) {
|
|
const req = {
|
|
path: '/mindspace/v1/pages',
|
|
method: 'GET',
|
|
requestId: 'req-test',
|
|
userToken: 'user-token',
|
|
get: () => '',
|
|
...overrides,
|
|
};
|
|
const res = createResponseRecorder();
|
|
let nextCalls = 0;
|
|
await middleware(req, res, () => {
|
|
nextCalls += 1;
|
|
});
|
|
return { req, res, nextCalls };
|
|
}
|
|
|
|
function createLogger() {
|
|
const entries = [];
|
|
return {
|
|
entries,
|
|
info(...args) {
|
|
entries.push(['info', ...args]);
|
|
},
|
|
error(...args) {
|
|
entries.push(['error', ...args]);
|
|
},
|
|
};
|
|
}
|
|
|
|
function createMiddleware(overrides = {}) {
|
|
return createPortalApiAuthMiddleware({
|
|
isPageDataPublicPath,
|
|
isLegacyPageDataApiPath,
|
|
isProductAnalyticsPublicPath: (path, method) =>
|
|
method === 'GET' && path === '/analytics/context',
|
|
logger: createLogger(),
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
test('direct global-auth bypass routes preserve unauthenticated access', async () => {
|
|
const middleware = createMiddleware();
|
|
for (const [path, method] of [
|
|
['/status', 'GET'],
|
|
['/runtime/status', 'GET'],
|
|
['/config/blocked-words', 'GET'],
|
|
['/internal/agent/jobs/job-1/claim', 'POST'],
|
|
['/internal/deep-search/llm', 'POST'],
|
|
['/mindspace/v1/assets/asset-1/download', 'GET'],
|
|
]) {
|
|
const result = await invoke(middleware, { path, method });
|
|
assert.equal(result.nextCalls, 1, `${method} ${path}`);
|
|
assert.equal(result.res.body, null);
|
|
}
|
|
});
|
|
|
|
test('no-auth mode keeps private and adjacent routes protected', async () => {
|
|
const middleware = createMiddleware();
|
|
for (const [path, method] of [
|
|
['/mindspace/v1/pages', 'GET'],
|
|
['/public/pages/page-1/data/signups/rows', 'GET'],
|
|
['/public/pages/page-1/data/signups', 'POST'],
|
|
['/page-dataevil/signups', 'GET'],
|
|
['/plaza/v1/posts/post-1/reports', 'POST'],
|
|
]) {
|
|
const result = await invoke(middleware, { path, method });
|
|
assert.equal(result.nextCalls, 0, `${method} ${path}`);
|
|
assert.equal(result.res.statusCode, 401);
|
|
}
|
|
});
|
|
|
|
test('legacy mode accepts only a verified legacy session', async () => {
|
|
const verifiedTokens = [];
|
|
const legacyAuth = {
|
|
verify(token) {
|
|
verifiedTokens.push(token);
|
|
return token === 'legacy-ok';
|
|
},
|
|
};
|
|
const middleware = createMiddleware({
|
|
getLegacyAuth: () => legacyAuth,
|
|
getLegacySessionToken: (req) => req.legacyToken,
|
|
});
|
|
|
|
const denied = await invoke(middleware, { legacyToken: 'legacy-bad' });
|
|
assert.equal(denied.res.statusCode, 401);
|
|
const allowed = await invoke(middleware, { legacyToken: 'legacy-ok' });
|
|
assert.equal(allowed.nextCalls, 1);
|
|
assert.deepEqual(verifiedTokens, ['legacy-bad', 'legacy-ok']);
|
|
});
|
|
|
|
test('multi-user public routes preserve optional user hydration', async () => {
|
|
let getMeCalls = 0;
|
|
const user = { id: 'user-1' };
|
|
const middleware = createMiddleware({
|
|
getUserAuth: () => ({
|
|
async getMe(token) {
|
|
getMeCalls += 1;
|
|
assert.equal(token, 'user-token');
|
|
return user;
|
|
},
|
|
}),
|
|
getTkmindProxy: () => ({}),
|
|
});
|
|
|
|
const anonymous = await invoke(middleware, {
|
|
path: '/public/pages/page-1/data/signups',
|
|
userSession: null,
|
|
});
|
|
assert.equal(anonymous.nextCalls, 1);
|
|
assert.equal(anonymous.req.currentUser, undefined);
|
|
|
|
const authenticated = await invoke(middleware, {
|
|
path: '/public/pages/page-1/data/signups',
|
|
userSession: { id: 'session-1' },
|
|
});
|
|
assert.equal(authenticated.nextCalls, 1);
|
|
assert.equal(authenticated.req.currentUser, user);
|
|
assert.equal(getMeCalls, 1);
|
|
});
|
|
|
|
test('product analytics context preserves optional user hydration', async () => {
|
|
let getMeCalls = 0;
|
|
const user = { id: 'analytics-user' };
|
|
const middleware = createMiddleware({
|
|
getUserAuth: () => ({
|
|
async getMe(token) {
|
|
getMeCalls += 1;
|
|
assert.equal(token, 'user-token');
|
|
return user;
|
|
},
|
|
}),
|
|
getTkmindProxy: () => ({}),
|
|
});
|
|
|
|
const anonymous = await invoke(middleware, {
|
|
path: '/analytics/context',
|
|
userSession: null,
|
|
});
|
|
assert.equal(anonymous.nextCalls, 1);
|
|
assert.equal(anonymous.req.currentUser, undefined);
|
|
|
|
const authenticated = await invoke(middleware, {
|
|
path: '/analytics/context',
|
|
userSession: { id: 'session-analytics' },
|
|
});
|
|
assert.equal(authenticated.nextCalls, 1);
|
|
assert.equal(authenticated.req.currentUser, user);
|
|
assert.equal(getMeCalls, 1);
|
|
});
|
|
|
|
test('multi-user private routes preserve login, expiry, and double-check behavior', async () => {
|
|
let currentUser = { id: 'user-1' };
|
|
let getMeCalls = 0;
|
|
const middleware = createMiddleware({
|
|
getUserAuth: () => ({
|
|
async getMe() {
|
|
getMeCalls += 1;
|
|
return currentUser;
|
|
},
|
|
}),
|
|
getTkmindProxy: () => ({}),
|
|
});
|
|
|
|
const anonymous = await invoke(middleware, { userSession: null });
|
|
assert.equal(anonymous.res.statusCode, 401);
|
|
assert.equal(anonymous.res.body.message, '未授权,请重新登录');
|
|
|
|
const authenticated = await invoke(middleware, {
|
|
userSession: { id: 'session-1' },
|
|
});
|
|
assert.equal(authenticated.nextCalls, 1);
|
|
assert.equal(authenticated.req.currentUser, currentUser);
|
|
assert.equal(getMeCalls, 2);
|
|
|
|
currentUser = null;
|
|
getMeCalls = 0;
|
|
const expired = await invoke(middleware, {
|
|
userSession: { id: 'session-expired' },
|
|
});
|
|
assert.equal(expired.res.statusCode, 401);
|
|
assert.equal(expired.res.body.message, '登录已过期');
|
|
assert.equal(getMeCalls, 2);
|
|
});
|
|
|
|
test('multi-user session service errors fail closed only for private routes', async () => {
|
|
const middleware = createMiddleware({
|
|
getUserAuth: () => ({ getMe: async () => ({ id: 'user-1' }) }),
|
|
getTkmindProxy: () => ({}),
|
|
});
|
|
|
|
const privateResult = await invoke(middleware, {
|
|
userSessionError: new Error('session unavailable'),
|
|
});
|
|
assert.equal(privateResult.res.statusCode, 503);
|
|
|
|
const publicResult = await invoke(middleware, {
|
|
path: '/public/pages/page-1/data/signups',
|
|
userSessionError: new Error('session unavailable'),
|
|
});
|
|
assert.equal(publicResult.nextCalls, 1);
|
|
});
|
|
|
|
test('multi-user getMe failures preserve the 503 response and error log', async () => {
|
|
const logger = createLogger();
|
|
const middleware = createMiddleware({
|
|
getUserAuth: () => ({
|
|
async getMe() {
|
|
throw new Error('database timeout');
|
|
},
|
|
}),
|
|
getTkmindProxy: () => ({}),
|
|
logger,
|
|
});
|
|
const result = await invoke(middleware, {
|
|
userSession: { id: 'session-1' },
|
|
});
|
|
assert.equal(result.res.statusCode, 503);
|
|
assert.equal(result.res.body.message, '用户认证服务不可用,请稍后重试');
|
|
assert.deepEqual(logger.entries[0], [
|
|
'error',
|
|
'[Auth] API auth failed:',
|
|
'database timeout',
|
|
]);
|
|
});
|
|
|
|
test('shadow mode records differences without changing the response', async () => {
|
|
const events = [];
|
|
const middleware = createMiddleware({
|
|
accessPolicyMode: PORTAL_ACCESS_POLICY_MODE.SHADOW,
|
|
accessShadowReporter: {
|
|
record(event) {
|
|
events.push(event);
|
|
},
|
|
},
|
|
});
|
|
const result = await invoke(middleware, {
|
|
path: '/plaza/v1/categories',
|
|
});
|
|
assert.equal(result.res.statusCode, 401);
|
|
assert.equal(events.length, 1);
|
|
assert.equal(events[0].policyGroup, 'plaza-optional-user');
|
|
assert.equal(events[0].mismatch, true);
|
|
});
|
|
|
|
test('single-group enforcement overrides only the selected no-auth boundary', async () => {
|
|
const logger = createLogger();
|
|
const middleware = createMiddleware({
|
|
accessEnforcementConfig: resolvePortalAccessEnforcementConfig({
|
|
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
|
|
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'page-data-public',
|
|
}),
|
|
logger,
|
|
});
|
|
|
|
const selected = await invoke(middleware, {
|
|
path: '/public/pages/page-1/data/signups',
|
|
});
|
|
assert.equal(selected.nextCalls, 1);
|
|
assert.equal(logger.entries.length, 1);
|
|
assert.match(logger.entries[0][2], /"behaviorChanged":true/);
|
|
|
|
const legacy = await invoke(middleware, {
|
|
path: '/page-data/signups',
|
|
});
|
|
assert.equal(legacy.res.statusCode, 401);
|
|
const plaza = await invoke(middleware, {
|
|
path: '/plaza/v1/categories',
|
|
});
|
|
assert.equal(plaza.res.statusCode, 401);
|
|
});
|
|
|
|
test('enforcement does not skip multi-user optional identity hydration', async () => {
|
|
let getMeCalls = 0;
|
|
const middleware = createMiddleware({
|
|
getUserAuth: () => ({
|
|
async getMe() {
|
|
getMeCalls += 1;
|
|
return { id: 'user-1' };
|
|
},
|
|
}),
|
|
getTkmindProxy: () => ({}),
|
|
accessEnforcementConfig: resolvePortalAccessEnforcementConfig({
|
|
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
|
|
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'page-data-public',
|
|
}),
|
|
});
|
|
const result = await invoke(middleware, {
|
|
path: '/public/pages/page-1/data/signups',
|
|
userSession: { id: 'session-1' },
|
|
});
|
|
assert.equal(result.nextCalls, 1);
|
|
assert.equal(result.req.currentUser.id, 'user-1');
|
|
assert.equal(getMeCalls, 1);
|
|
});
|
|
|
|
test('kill switch restores the legacy global-auth result', async () => {
|
|
const middleware = createMiddleware({
|
|
accessEnforcementConfig: resolvePortalAccessEnforcementConfig({
|
|
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
|
|
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'page-data-public',
|
|
MEMIND_PORTAL_ACCESS_POLICY_KILL_SWITCH: '1',
|
|
}),
|
|
});
|
|
const result = await invoke(middleware, {
|
|
path: '/public/pages/page-1/data/signups',
|
|
});
|
|
assert.equal(result.res.statusCode, 401);
|
|
assert.equal(result.nextCalls, 0);
|
|
});
|
|
|
|
test('middleware waits for auth bootstrap before reading service getters', async () => {
|
|
const calls = [];
|
|
const middleware = createMiddleware({
|
|
waitForUserAuthReady: async () => {
|
|
calls.push('ready');
|
|
},
|
|
getUserAuth: () => {
|
|
calls.push('user-auth');
|
|
return null;
|
|
},
|
|
getTkmindProxy: () => {
|
|
calls.push('proxy');
|
|
return null;
|
|
},
|
|
getLegacyAuth: () => {
|
|
calls.push('legacy');
|
|
return null;
|
|
},
|
|
});
|
|
await invoke(middleware);
|
|
assert.deepEqual(calls, ['ready', 'user-auth', 'proxy', 'legacy']);
|
|
});
|