mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 00:13:09 +00:00
fix: answer 409 for a stale policy save also when another bundle made the config manager #1066
This commit is contained in:
@@ -101,4 +101,26 @@ describe('policy save with If-Match', () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect((await stored()).features.calendarEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('answers 409 when the config manager was made by another bundle\'s copy of the module', async () => {
|
||||
// In the server the instrumentation creates the config manager, so its
|
||||
// errors are of the instrumentation's classes, not the route's.
|
||||
const KEY = Symbol.for('bulwark.admin.configManager');
|
||||
const g = globalThis as Record<symbol, unknown>;
|
||||
const mine = g[KEY];
|
||||
try {
|
||||
delete g[KEY];
|
||||
vi.resetModules();
|
||||
await (await import('@/lib/admin/config-manager')).configManager.load();
|
||||
vi.resetModules();
|
||||
const route = await import('@/app/api/admin/policy/route');
|
||||
const res = await route.GET(new NextRequest(URL));
|
||||
const etag = res.headers.get('ETag')!;
|
||||
await writeFile(path.join(dir, 'policy.json'), JSON.stringify({ features: { calendarEnabled: false } }));
|
||||
const stale = await route.PUT(new NextRequest(URL, { method: 'PUT', headers: { 'if-match': etag }, body: JSON.stringify(await res.json()) }));
|
||||
expect(stale.status).toBe(409);
|
||||
} finally {
|
||||
g[KEY] = mine;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { completePolicy, configManager, PolicyConflictError } from '@/lib/admin/config-manager';
|
||||
import { completePolicy, configManager, isPolicyConflict } from '@/lib/admin/config-manager';
|
||||
import { requireAdminAuth, getClientIP } from '@/lib/admin/session';
|
||||
import { auditLog } from '@/lib/admin/audit';
|
||||
import { logger } from '@/lib/logger';
|
||||
@@ -71,7 +71,7 @@ export async function POST(request: NextRequest) {
|
||||
await configManager.setPolicy(next, { ifVersion: version });
|
||||
break;
|
||||
} catch (error) {
|
||||
if (!(error instanceof PolicyConflictError)) throw error;
|
||||
if (!isPolicyConflict(error)) throw error;
|
||||
if (attempt >= 3) return NextResponse.json({ error: error.message }, { status: 409 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { configManager, PolicyConflictError } from '@/lib/admin/config-manager';
|
||||
import { configManager, isPolicyConflict } from '@/lib/admin/config-manager';
|
||||
import { requireAdminAuth, getClientIP } from '@/lib/admin/session';
|
||||
import { auditLog } from '@/lib/admin/audit';
|
||||
import { logger } from '@/lib/logger';
|
||||
@@ -135,7 +135,7 @@ export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
version = await configManager.setPolicy(policy, { ifVersion: ifMatchVersion(request) });
|
||||
} catch (error) {
|
||||
if (error instanceof PolicyConflictError) {
|
||||
if (isPolicyConflict(error)) {
|
||||
return NextResponse.json({ error: error.message }, { status: 409 });
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -59,6 +59,15 @@ export class PolicyConflictError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `error` is a PolicyConflictError. By name, not instanceof: the
|
||||
* config manager on globalThis may come from another bundle's copy of this
|
||||
* module (the instrumentation creates it), whose class is a different object.
|
||||
*/
|
||||
export function isPolicyConflict(error: unknown): error is PolicyConflictError {
|
||||
return error instanceof Error && error.name === 'PolicyConflictError';
|
||||
}
|
||||
|
||||
/** Policy writes in this process, one after another, so a version check and its write can't interleave. */
|
||||
const POLICY_WRITE_QUEUE = 'admin-config\0policy.json';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user