Skip to content

Commit 0b04207

Browse files
committed
test: pin the clear through an env pin and the allowed-IP recovery end to end
Claude-Session: https://claude.ai/code/session_01Utp6CZjAtpVuoYZBzB9n5i Co-authored-by: Claude <noreply@anthropic.com>
1 parent 87457d3 commit 0b04207

2 files changed

Lines changed: 141 additions & 0 deletions

File tree

‎packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
* the deployment declares `trustProxy`. A list that includes it saves.
2222
* - The recovery the refusal names: `OS_AUTH_ALLOWED_IP_RANGES` set empty
2323
* opens the gate on the next boot over a stored list.
24+
* - [#30385] The rest of that recovery: while the override is in force a
25+
* clear (`null`) goes through the settings door and clears the stored
26+
* list, so removing the override does not bring it back; any other value
27+
* is still refused `409 SETTINGS_LOCKED`.
2428
*
2529
* `@objectstack/service-settings` resolves to its `src/` here (this package's
2630
* `vitest.config.ts`), so the routes and the service are the checkout's.
@@ -229,3 +233,36 @@ describe('#30203 the recovery the refusal names', () => {
229233
expect(recovered.manager.isClientIpAllowed('::1')).toBe(true);
230234
});
231235
});
236+
237+
describe('#30385 the env recovery clears the stored list, so lifting the override does not lock the caller out again', () => {
238+
it('set the override empty, sign in, clear the stored list through the settings door, remove the override: the caller is admitted', async () => {
239+
const env: Record<string, string | undefined> = {};
240+
const svc = new SettingsService({ env });
241+
svc.registerManifest(authManifest);
242+
await svc.set('auth', 'allowed_ip_ranges', OFFICE, {});
243+
expect((await boot({ svc })).manager.isClientIpAllowed('::1')).toBe(false);
244+
245+
// Start the server with the override set empty: the gate opens.
246+
env.OS_AUTH_ALLOWED_IP_RANGES = '';
247+
const recovered = await boot({ svc });
248+
expect(recovered.manager.isClientIpAllowed('::1')).toBe(true);
249+
250+
// A value is still refused while the override is in force, the empty string included.
251+
const refused = await recovered.save({ allowed_ip_ranges: '' }, { remoteAddress: '::1' });
252+
expect(refused.status).toBe(409);
253+
expect(refused.body.error.code).toBe('SETTINGS_LOCKED');
254+
expect(refused.body.error.details).toEqual({ namespace: 'auth', key: 'allowed_ip_ranges', reason: 'locked-by-env' });
255+
256+
// The clear goes through; the answer still reads the override, locked, and the gate stays open.
257+
const cleared = await recovered.save({ allowed_ip_ranges: null }, { remoteAddress: '::1' });
258+
expect(cleared.status).toBe(200);
259+
expect(cleared.body.data.values.allowed_ip_ranges).toMatchObject({ value: '', source: 'env', locked: true });
260+
expect(recovered.manager.isClientIpAllowed('::1')).toBe(true);
261+
262+
// Remove the override and restart: no stored list comes back.
263+
delete env.OS_AUTH_ALLOWED_IP_RANGES;
264+
expect((await svc.get('auth', 'allowed_ip_ranges', {})).source).toBe('default');
265+
const restarted = await boot({ svc });
266+
expect(restarted.manager.isClientIpAllowed('::1')).toBe(true);
267+
});
268+
});

‎packages/services/service-settings/src/settings-service.test.ts‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2745,3 +2745,107 @@ describe('auth lockout window is enforced again (#7169)', () => {
27452745
}
27462746
});
27472747
});
2748+
2749+
describe('SettingsService — a clear goes through an env pin, every other write does not (#30385)', () => {
2750+
// Two keys a pin can sit on: one below the global rung, one on it.
2751+
const PINNED = {
2752+
namespace: 'pinned',
2753+
version: 1,
2754+
label: 'Pinned',
2755+
scope: 'tenant',
2756+
readPermission: 'setup.access',
2757+
writePermission: 'setup.write',
2758+
specifiers: [
2759+
{ type: 'text', key: 'motto', label: 'Motto', scope: 'tenant', default: 'none' },
2760+
{ type: 'text', key: 'banner', label: 'Banner', scope: 'global', default: 'none' },
2761+
],
2762+
} as any;
2763+
const orgAdmin = { enforced: true, permissions: ['setup.access', 'setup.write'], userId: 'usr_org_admin' };
2764+
const platformAdmin = {
2765+
enforced: true,
2766+
permissions: ['setup.access', 'setup.write', 'manage_platform_settings'],
2767+
userId: 'usr_platform_admin',
2768+
};
2769+
const refusalOf = (p: Promise<unknown>) => p.then(() => undefined, (e: any) => e);
2770+
2771+
/** A stored value under a pin that is set after it: the operator's recovery. */
2772+
async function pinnedOverStored() {
2773+
const env: Record<string, string | undefined> = {};
2774+
const svc = new SettingsService({ env });
2775+
svc.registerManifest(PINNED);
2776+
await svc.set('pinned', 'motto', 'Stored');
2777+
env.OS_PINNED_MOTTO = 'Pinned';
2778+
return { env, svc };
2779+
}
2780+
2781+
it('a clear clears the stored value beneath the pin and changes nothing in force: the answer still reads env, locked', async () => {
2782+
const { env, svc } = await pinnedOverStored();
2783+
const changes: unknown[] = [];
2784+
svc.subscribe('pinned', (e) => { changes.push(e); });
2785+
2786+
const answer = await svc.set('pinned', 'motto', null);
2787+
expect(answer).toMatchObject({ value: 'Pinned', source: 'env', locked: true });
2788+
expect(answer.lockedReason).toContain('OS_PINNED_MOTTO');
2789+
expect(await svc.get('pinned', 'motto')).toMatchObject({ value: 'Pinned', source: 'env', locked: true });
2790+
expect(changes).toEqual([expect.objectContaining({ key: 'motto', action: 'reset' })]);
2791+
2792+
// Lift the pin: the key falls back to its default, not to the old value.
2793+
delete env.OS_PINNED_MOTTO;
2794+
expect(await svc.get('pinned', 'motto')).toMatchObject({ value: 'none', source: 'default', locked: false });
2795+
});
2796+
2797+
it('`undefined` is a clear too, alongside an ordinary write in the same batch', async () => {
2798+
const { env, svc } = await pinnedOverStored();
2799+
await svc.setMany('pinned', { motto: undefined, banner: 'Saved' });
2800+
delete env.OS_PINNED_MOTTO;
2801+
expect((await svc.get('pinned', 'motto')).source).toBe('default');
2802+
expect((await svc.get('pinned', 'banner')).value).toBe('Saved');
2803+
});
2804+
2805+
it('every other write is refused SETTINGS_LOCKED as before, the empty string included, and the whole batch stores nothing', async () => {
2806+
const { env, svc } = await pinnedOverStored();
2807+
for (const value of ['Other', '']) {
2808+
const refusal = await refusalOf(svc.set('pinned', 'motto', value));
2809+
expect(refusal).toBeInstanceOf(SettingsLockedError);
2810+
expect(refusal).toMatchObject({ code: 'SETTINGS_LOCKED', reason: 'locked-by-env', key: 'motto' });
2811+
}
2812+
const batch = await refusalOf(svc.setMany('pinned', { banner: 'Saved', motto: 'Other' }));
2813+
expect(batch).toMatchObject({ code: 'SETTINGS_LOCKED', reason: 'locked-by-env', key: 'motto' });
2814+
2815+
delete env.OS_PINNED_MOTTO;
2816+
expect(await svc.get('pinned', 'motto')).toMatchObject({ value: 'Stored', source: 'tenant' });
2817+
expect((await svc.get('pinned', 'banner')).source).toBe('default');
2818+
});
2819+
2820+
it('a locked row at an upper rung still refuses a clear below it', async () => {
2821+
const { env, svc } = await pinnedOverStored();
2822+
await (svc as any).upsertRow({
2823+
namespace: 'pinned', key: 'motto', scope: 'global', user_id: null, value: 'Platform motto',
2824+
value_enc: null, encrypted: false, locked: true, locked_reason: 'Set by the platform.',
2825+
});
2826+
2827+
const refusal = await refusalOf(svc.set('pinned', 'motto', null));
2828+
expect(refusal).toBeInstanceOf(SettingsLockedError);
2829+
expect(refusal).toMatchObject({ code: 'SETTINGS_LOCKED', reason: 'locked-by-global', key: 'motto' });
2830+
2831+
delete env.OS_PINNED_MOTTO;
2832+
expect(await svc.get('pinned', 'motto')).toMatchObject({ value: 'Platform motto', source: 'global', locked: true });
2833+
});
2834+
2835+
it('a locked row at the write\'s own rung refuses a non-platform clear, and the platform writes through it', async () => {
2836+
const env: Record<string, string | undefined> = { OS_PINNED_BANNER: 'Pinned banner' };
2837+
const svc = new SettingsService({ env });
2838+
svc.registerManifest(PINNED);
2839+
await (svc as any).upsertRow({
2840+
namespace: 'pinned', key: 'banner', scope: 'global', user_id: null, value: 'Platform banner',
2841+
value_enc: null, encrypted: false, locked: true, locked_reason: 'Set by the platform.',
2842+
});
2843+
2844+
const refusal = await refusalOf(svc.setMany('pinned', { banner: null }, orgAdmin));
2845+
expect(refusal).toBeInstanceOf(SettingsLockedError);
2846+
expect(refusal).toMatchObject({ code: 'SETTINGS_LOCKED', reason: 'locked-by-global', key: 'banner' });
2847+
2848+
const answer = await svc.setMany('pinned', { banner: null }, platformAdmin);
2849+
expect(answer.banner).toMatchObject({ value: 'Pinned banner', source: 'env', locked: true });
2850+
});
2851+
});

0 commit comments

Comments
 (0)