refactor: modularize portal server and harden session recovery
This commit is contained in:
@@ -6,9 +6,15 @@ import test from 'node:test';
|
||||
import express from 'express';
|
||||
import { attachPageDataRoutes } from './page-data-routes.mjs';
|
||||
import { createPageDataService } from './page-data-service.mjs';
|
||||
import { createPageDataPublicService } from './page-data-public-service.mjs';
|
||||
import { createPageDataPublicService, isPageDataPublicPath } from './page-data-public-service.mjs';
|
||||
import { createUserDataSpaceService } from './user-data-space-service.mjs';
|
||||
import { publicationInternals } from './mindspace-publications.mjs';
|
||||
import { writePageAccessPolicy } from './page-data-policy-store.mjs';
|
||||
import {
|
||||
resolvePortalAccessEnforcementConfig,
|
||||
resolvePortalAccessEnforcementDecision,
|
||||
shouldOverridePortalLegacyGlobalAuth,
|
||||
} from './server/portal-access-policy.mjs';
|
||||
|
||||
const PAGE_ID = 'page-integration-1';
|
||||
const OWNER_ID = 'user-integration-1';
|
||||
@@ -110,6 +116,42 @@ function buildApp(workspaceRoot) {
|
||||
return app;
|
||||
}
|
||||
|
||||
function buildEnforcedPageDataApp(workspaceRoot) {
|
||||
const pageDataPublicService = createPageDataPublicService({
|
||||
getPool: () => createPool('public'),
|
||||
resolveWorkspaceRootForOwner: () => workspaceRoot,
|
||||
});
|
||||
const enforcementConfig = resolvePortalAccessEnforcementConfig({
|
||||
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
|
||||
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'page-data-public',
|
||||
});
|
||||
const api = express.Router();
|
||||
api.use(express.json());
|
||||
api.use((req, res, next) => {
|
||||
const decision = resolvePortalAccessEnforcementDecision(
|
||||
{ path: req.path, method: req.method },
|
||||
{ isPageDataPublicPath },
|
||||
enforcementConfig,
|
||||
);
|
||||
if (shouldOverridePortalLegacyGlobalAuth(decision, false)) return next();
|
||||
return res.status(401).json({ error: { code: 'unauthorized' } });
|
||||
});
|
||||
attachPageDataRoutes(api, {
|
||||
sendData: (res, _req, data, status = 200) => res.status(status).json({ data }),
|
||||
sendError: (res, _req, status, code, message) =>
|
||||
res.status(status).json({ error: { code, message } }),
|
||||
getPageDataService: () =>
|
||||
createPageDataService({
|
||||
resolveWorkspaceRoot: async () => workspaceRoot,
|
||||
}),
|
||||
getPageDataPublicService: () => pageDataPublicService,
|
||||
});
|
||||
const app = express();
|
||||
app.set('trust proxy', true);
|
||||
app.use('/api', api);
|
||||
return app;
|
||||
}
|
||||
|
||||
async function request(app, method, url, { body, headers } = {}) {
|
||||
const server = app.listen(0);
|
||||
try {
|
||||
@@ -140,6 +182,21 @@ test('integration: owner private API and public insert coexist without breaking
|
||||
assert.equal(ownerInsert.status, 201);
|
||||
assert.equal(ownerInsert.body.data.row.title, 'owner 写入');
|
||||
|
||||
const ownerInvalidInsert = await request(
|
||||
app,
|
||||
'POST',
|
||||
'/api/admin/page-data/entries/rows',
|
||||
{
|
||||
headers: { 'x-test-user': '1' },
|
||||
body: { forbidden_column: 'blocked' },
|
||||
},
|
||||
);
|
||||
assert.equal(ownerInvalidInsert.status, 403);
|
||||
assert.equal(
|
||||
ownerInvalidInsert.body.error.code,
|
||||
'columns_not_allowed',
|
||||
);
|
||||
|
||||
const ownerList = await request(app, 'GET', '/api/admin/page-data/entries?limit=10', {
|
||||
headers: { 'x-test-user': '1' },
|
||||
});
|
||||
@@ -184,6 +241,55 @@ test('integration: owner private API and public insert coexist without breaking
|
||||
assert.equal(stats.total, 2);
|
||||
});
|
||||
|
||||
test('integration: page-data-public enforcement delegates to route policy without opening adjacent APIs', async () => {
|
||||
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-enforced-'));
|
||||
await setupWorkspace(workspaceRoot);
|
||||
writePageAccessPolicy(workspaceRoot, {
|
||||
pageId: PAGE_ID,
|
||||
ownerUserId: OWNER_ID,
|
||||
accessMode: 'public',
|
||||
datasets: {
|
||||
entries: {
|
||||
read: false,
|
||||
insert: true,
|
||||
columns: {
|
||||
read: ['id', 'title', 'created_at'],
|
||||
insert: ['title'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const app = buildEnforcedPageDataApp(workspaceRoot);
|
||||
|
||||
const inserted = await request(
|
||||
app,
|
||||
'POST',
|
||||
`/api/public/pages/${PAGE_ID}/data/entries/rows`,
|
||||
{ body: { title: '灰度公开写入' } },
|
||||
);
|
||||
assert.equal(inserted.status, 201);
|
||||
assert.equal(inserted.body.data.row.title, '灰度公开写入');
|
||||
|
||||
const policyDenied = await request(
|
||||
app,
|
||||
'GET',
|
||||
`/api/public/pages/${PAGE_ID}/data/entries`,
|
||||
);
|
||||
assert.equal(policyDenied.status, 403);
|
||||
assert.equal(policyDenied.body.error.code, 'action_not_allowed');
|
||||
|
||||
for (const [method, url] of [
|
||||
['GET', '/api/admin/page-data/entries'],
|
||||
['GET', '/api/plaza/v1/categories'],
|
||||
['GET', `/api/public/pages/${PAGE_ID}/data/entries/rows`],
|
||||
['POST', `/api/public/pages/${PAGE_ID}/data/entries`],
|
||||
]) {
|
||||
const denied = await request(app, method, url);
|
||||
assert.equal(denied.status, 401, `${method} ${url}`);
|
||||
assert.equal(denied.body.error.code, 'unauthorized');
|
||||
}
|
||||
});
|
||||
|
||||
test('integration: public insert rejects SQL injection style payload keys', async () => {
|
||||
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-integration-sql-'));
|
||||
await setupWorkspace(workspaceRoot);
|
||||
|
||||
Reference in New Issue
Block a user