mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
86513fda0d
On a managed install the operator runs place search and routing, the plugin runtime, the integrations and the documentation, so the admin and the users no longer see switches and guides for them: - The place search and routing card, the transit provider and the search log are gone from the admin settings (desktop and mobile). places_provider is a managed-locked setting, and the transit provider, search log and wanderer routes refuse writes there. Share place details stays the admin's, as a card of its own, hidden when TREK_PLACES_CONTRIBUTE switches it off. - File types and trip reminders move to the right column, so the columns stay balanced. - Plugins disappear from the admin and the user settings, wanderer from Tours. - The Help & Docs page and its links are not offered. The help center stays and leaves out guides about what the operator runs (keys, OIDC, backups, storage, releases, plugins, document sync) and about integrations that are off. - Mapbox GL is offered only with the operator's token, Amap tiles only where the operator set Amap up. - The mobile admin no longer shows OIDC and the WebAuthn relying party fields, matching the desktop tab. For every install: - Notification matrices list only events the install can raise: no plugin row without the plugin runtime, no Vacay, collection, photo, Synology, wanderer or packing rows while those are off. On a managed install the release and replica alerts are left out. - The journey entry editor offers External photos only where a photo provider is switched on, as the phone already did. - app-config is asked again after a failed answer. A page opened while the server restarted kept every default for the session, which on a managed install brought back the operator's settings.
131 lines
5.1 KiB
TypeScript
131 lines
5.1 KiB
TypeScript
/**
|
||
* Who owns which setting, pinned.
|
||
*
|
||
* The lists grow and the mode does not: that is how this kind of switch drifts.
|
||
* A key added to the admin form (ADMIN_FORM_SETTING_KEYS) or DEFAULTABLE_USER_SETTING_KEYS belongs to
|
||
* neither set until somebody decides, and MANAGED-KEYS-001 is what makes that
|
||
* decision unavoidable rather than optional.
|
||
*
|
||
* Deliberately an assignment test, not an equality test. Pinning the locked list
|
||
* alone would pass forever while new keys quietly defaulted to "the admin may
|
||
* set it", which is the failure this is here to prevent.
|
||
*/
|
||
import { ADMIN_FORM_SETTING_KEYS } from '../../../src/nest/common/app-settings.registry';
|
||
import {
|
||
MANAGED_LOCKED_SETTING_KEYS,
|
||
MANAGED_LOCKED_PROFILE_KEYS,
|
||
MANAGED_CUSTOMER_KEYS,
|
||
isManagedLockedKey,
|
||
splitManagedKeys,
|
||
} from '../../../src/nest/common/managed';
|
||
import { DEFAULTABLE_USER_SETTING_KEYS } from '../../../src/nest/settings/settings.service';
|
||
|
||
import { describe, it, expect } from 'vitest';
|
||
|
||
const SOURCE_KEYS: string[] = [...ADMIN_FORM_SETTING_KEYS, ...DEFAULTABLE_USER_SETTING_KEYS];
|
||
const locked = new Set<string>(MANAGED_LOCKED_SETTING_KEYS);
|
||
const customer = new Set<string>(MANAGED_CUSTOMER_KEYS);
|
||
|
||
describe('managed key assignment', () => {
|
||
it('MANAGED-KEYS-001: every key from both source lists is assigned to exactly one set', () => {
|
||
const unassigned = SOURCE_KEYS.filter((k) => !locked.has(k) && !customer.has(k));
|
||
const both = SOURCE_KEYS.filter((k) => locked.has(k) && customer.has(k));
|
||
|
||
// Both in one assertion so the failure message names the offending key.
|
||
expect({ unassigned, both }).toEqual({ unassigned: [], both: [] });
|
||
});
|
||
|
||
it('MANAGED-KEYS-002: no customer entry outlives the key it describes', () => {
|
||
expect(MANAGED_CUSTOMER_KEYS.filter((k) => !SOURCE_KEYS.includes(k))).toEqual([]);
|
||
});
|
||
|
||
it('MANAGED-KEYS-003: the only locked names outside both source lists are the users columns and the service key', () => {
|
||
// service_key is written by the server alone, never through a settings
|
||
// form; it is locked so a managed install keeps the operator's key.
|
||
expect(
|
||
MANAGED_LOCKED_SETTING_KEYS.filter((k) => !SOURCE_KEYS.includes(k))
|
||
.slice()
|
||
.sort(),
|
||
).toEqual([...MANAGED_LOCKED_PROFILE_KEYS, 'service_key'].sort());
|
||
});
|
||
|
||
it('MANAGED-KEYS-004: neither set repeats itself', () => {
|
||
expect(locked.size).toBe(MANAGED_LOCKED_SETTING_KEYS.length);
|
||
expect(customer.size).toBe(MANAGED_CUSTOMER_KEYS.length);
|
||
});
|
||
|
||
it('MANAGED-KEYS-005: the locked list is pinned verbatim', () => {
|
||
expect([...MANAGED_LOCKED_SETTING_KEYS]).toEqual([
|
||
'amap_api_key',
|
||
'carto_api_key',
|
||
'llm_api_key',
|
||
'llm_base_url',
|
||
'llm_model',
|
||
'llm_multimodal',
|
||
'llm_provider',
|
||
'mapbox_access_token',
|
||
'maps_api_key',
|
||
'oidc_login',
|
||
'oidc_registration',
|
||
'openweather_api_key',
|
||
'places_provider',
|
||
'routing_base_url',
|
||
'routing_valhalla_provider',
|
||
'service_key',
|
||
'smtp_from',
|
||
'smtp_host',
|
||
'smtp_pass',
|
||
'smtp_port',
|
||
'smtp_skip_tls_verify',
|
||
'smtp_user',
|
||
'unsplash_api_key',
|
||
'valhalla_base_url',
|
||
'webauthn_origins',
|
||
'webauthn_rp_id',
|
||
]);
|
||
});
|
||
|
||
it("MANAGED-KEYS-006: mapbox_access_token is the operator's, and reaches the browser by design", () => {
|
||
// The one locked key whose value is meant to be public: a managed instance
|
||
// ships the operator's pk.* token, injected when settings are read. Locked so
|
||
// a per-user save cannot land on top of it and break that user's map.
|
||
expect(isManagedLockedKey('mapbox_access_token')).toBe(true);
|
||
expect(customer.has('mapbox_access_token')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('splitManagedKeys', () => {
|
||
it('MANAGED-KEYS-007: hands the body back untouched on a self-hosted install', () => {
|
||
const body = { smtp_host: 'mail.example.test', allow_registration: 'true' };
|
||
|
||
expect(splitManagedKeys(body, false)).toEqual({ allowed: body, blocked: [] });
|
||
});
|
||
|
||
it('MANAGED-KEYS-008: keeps the customer keys and reports the locked ones', () => {
|
||
// The admin settings tab saves SMTP and the registration toggles in one
|
||
// request. Refusing the request would take the toggles down with the SMTP.
|
||
const { allowed, blocked } = splitManagedKeys(
|
||
{ smtp_host: 'mail.example.test', smtp_pass: 'hunter2', allow_registration: 'true' },
|
||
true,
|
||
);
|
||
|
||
expect(allowed).toEqual({ allow_registration: 'true' });
|
||
expect(blocked).toEqual(['smtp_host', 'smtp_pass']);
|
||
});
|
||
|
||
it('MANAGED-KEYS-009: reports blocked names sorted, so a response body is stable', () => {
|
||
const { blocked } = splitManagedKeys({ webauthn_rp_id: 'x', llm_api_key: 'y', smtp_from: 'z' }, true);
|
||
|
||
expect(blocked).toEqual(['llm_api_key', 'smtp_from', 'webauthn_rp_id']);
|
||
});
|
||
|
||
it('MANAGED-KEYS-010: an unknown key is not the filter’s business', () => {
|
||
// Whatever rejects unknown keys today keeps rejecting them; this only ever
|
||
// removes names it was told to remove.
|
||
const { allowed, blocked } = splitManagedKeys({ not_a_setting: '1' }, true);
|
||
|
||
expect(allowed).toEqual({ not_a_setting: '1' });
|
||
expect(blocked).toEqual([]);
|
||
});
|
||
});
|