#246: mode enforced — reject profile-violating configuration writes
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m18s
CI / Build container images (pull_request) Successful in 4m3s
CI / Auth e2e pack (pull_request) Successful in 8m43s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
CD / Build and push images (push) Successful in 23s
CD / Smoke tests against Test (push) Successful in 1m22s
CD / Deploy to Test (push) Successful in 12s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m27s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m24s
CI / Import/export fidelity gate (push) Successful in 58s
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m18s
CI / Build container images (pull_request) Successful in 4m3s
CI / Auth e2e pack (pull_request) Successful in 8m43s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
CD / Build and push images (push) Successful in 23s
CD / Smoke tests against Test (push) Successful in 1m22s
CD / Deploy to Test (push) Successful in 12s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m27s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m24s
CI / Import/export fidelity gate (push) Successful in 58s
In enforced mode the ONE settings write path every caller uses rejects catalog-violating values with the stable code vs_nfd_profile_violation (403 — the request is well-formed, the policy says no). Existing violating values are reported at startup (log line, database-less boots must not fail) and on the admin card, never auto-changed. The UI renders as in hidden (#245 already keys on hidden|enforced). The hardening guide now names enforced as the recommended mode for VS-NfD reference operation. Tests: violating write rejected with the stable code and nothing stored; compliant writes pass; the same violating write passes in marked and hidden (own app boots); pre-existing violation reported and untouched. Full api suite 100 files / 555 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
This commit is contained in:
parent
0d95e1304e
commit
4d9f913845
@ -1,10 +1,11 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
|
||||||
import { DEFAULT_ATTACHMENT_EXTENSIONS } from '@dorfteich/shared';
|
import { DEFAULT_ATTACHMENT_EXTENSIONS, VS_NFD_PROFILE, isVsNfdCompliant } from '@dorfteich/shared';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { PinoLogger } from 'nestjs-pino';
|
import { PinoLogger } from 'nestjs-pino';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AuditService } from '../audit/audit.service';
|
import { AuditService } from '../audit/audit.service';
|
||||||
|
import { AppConfig } from '../config/app-config.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -201,6 +202,7 @@ export class InstanceSettingsService {
|
|||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly audit: AuditService,
|
private readonly audit: AuditService,
|
||||||
private readonly logger: PinoLogger,
|
private readonly logger: PinoLogger,
|
||||||
|
private readonly config: AppConfig,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(InstanceSettingsService.name);
|
this.logger.setContext(InstanceSettingsService.name);
|
||||||
}
|
}
|
||||||
@ -237,6 +239,22 @@ export class InstanceSettingsService {
|
|||||||
details: { [key]: parsed.error.issues.map((i) => i.message) },
|
details: { [key]: parsed.error.issues.map((i) => i.message) },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Mode `enforced` (#246, ADR 0027): a write that would set a
|
||||||
|
// catalog-violating value is rejected at the ONE write path every
|
||||||
|
// caller uses — hiding alone (#245) is UI cosmetics a scripted client
|
||||||
|
// bypasses. Existing violating values are reported (startup log,
|
||||||
|
// admin card), never auto-changed: the operator resolves them
|
||||||
|
// consciously, and writes that DECREASE compliance are what this
|
||||||
|
// blocks. 403, not 400: the request is well-formed, the policy says no.
|
||||||
|
if (this.config.env.VS_NFD_MODE === 'enforced') {
|
||||||
|
const entry = VS_NFD_PROFILE.find((e) => e.scope === 'instance' && e.key === key);
|
||||||
|
if (entry && !isVsNfdCompliant(entry, parsed.data)) {
|
||||||
|
throw new ForbiddenException({
|
||||||
|
code: 'vs_nfd_profile_violation',
|
||||||
|
details: { [key]: ['vs_nfd_profile_violation'] },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
// Nullable settings (setup.completedAt) store JSON null explicitly —
|
// Nullable settings (setup.completedAt) store JSON null explicitly —
|
||||||
// Prisma requires the sentinel for that.
|
// Prisma requires the sentinel for that.
|
||||||
const stored = parsed.data === null ? Prisma.JsonNull : parsed.data;
|
const stored = parsed.data === null ? Prisma.JsonNull : parsed.data;
|
||||||
|
|||||||
143
apps/api/src/settings/vs-nfd-enforced.e2e.db.test.ts
Normal file
143
apps/api/src/settings/vs-nfd-enforced.e2e.db.test.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import { type VsNfdProfileView } from '@dorfteich/shared';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { createTestApp, sessionCookieOf } from '../testing/test-app';
|
||||||
|
import { createTestPrisma, hasTestDb, uniqueSuffix } from '../testing/test-db';
|
||||||
|
import { UsersService } from '../users/users.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mode `enforced` (issue #246, ADR 0027): the api rejects settings writes
|
||||||
|
* that would set a catalog-violating value — with a stable error code —
|
||||||
|
* while existing violations are reported and never auto-changed. The mode
|
||||||
|
* is env-fixed per boot, so each mode gets its own app (sequential
|
||||||
|
* describes; pattern vs-nfd-profile.e2e.db.test.ts).
|
||||||
|
*/
|
||||||
|
async function makeAdmin(
|
||||||
|
app: INestApplication,
|
||||||
|
prisma: PrismaClient,
|
||||||
|
suffix: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const users = app.get(UsersService);
|
||||||
|
const username = `enf-admin-${suffix}`;
|
||||||
|
const user = await users.createUser({
|
||||||
|
username,
|
||||||
|
email: `${username}@example.org`,
|
||||||
|
displayName: 'Enf Admin',
|
||||||
|
password: 'erzwungen ist erzwungen 1',
|
||||||
|
locale: 'en',
|
||||||
|
});
|
||||||
|
await users.markEmailVerified(user.id);
|
||||||
|
await prisma.user.update({ where: { id: user.id }, data: { isSiteAdmin: true } });
|
||||||
|
return sessionCookieOf(
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post('/api/v1/auth/login')
|
||||||
|
.send({ usernameOrEmail: username, password: 'erzwungen ist erzwungen 1' })
|
||||||
|
.expect(200),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.skipIf(!hasTestDb)('VS-NfD mode enforced (e2e, issue #246)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
let cookie: string;
|
||||||
|
const suffix = uniqueSuffix();
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
process.env.VS_NFD_MODE = 'enforced';
|
||||||
|
prisma = createTestPrisma();
|
||||||
|
await prisma.rateLimit.deleteMany({});
|
||||||
|
// A violation that exists BEFORE the enforced boot: reported, never
|
||||||
|
// auto-changed (feeds.enabled default is true = violating anyway, but
|
||||||
|
// pin it as an explicit stored row).
|
||||||
|
await prisma.instanceSetting.upsert({
|
||||||
|
where: { key: 'feeds.enabled' },
|
||||||
|
create: { key: 'feeds.enabled', value: true },
|
||||||
|
update: { value: true },
|
||||||
|
});
|
||||||
|
app = await createTestApp();
|
||||||
|
cookie = await makeAdmin(app, prisma, suffix);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
delete process.env.VS_NFD_MODE;
|
||||||
|
await prisma.instanceSetting.deleteMany({
|
||||||
|
where: { key: { in: ['feeds.enabled', 'auth.registrationMode', 'upload.svgPolicy'] } },
|
||||||
|
});
|
||||||
|
await prisma.user.deleteMany({ where: { username: { contains: suffix } } });
|
||||||
|
await prisma.$disconnect();
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a violating write with the stable error code', async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.patch('/api/v1/admin/settings')
|
||||||
|
.set('Cookie', cookie)
|
||||||
|
.send({ 'upload.svgPolicy': 'sanitize' })
|
||||||
|
.expect(403);
|
||||||
|
expect(res.body.code).toBe('vs_nfd_profile_violation');
|
||||||
|
// Nothing was stored — the read still returns the schema default.
|
||||||
|
const settings = await request(app.getHttpServer())
|
||||||
|
.get('/api/v1/admin/settings')
|
||||||
|
.set('Cookie', cookie)
|
||||||
|
.expect(200);
|
||||||
|
expect(settings.body['upload.svgPolicy']).toBe('sanitize');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts compliant writes', async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.patch('/api/v1/admin/settings')
|
||||||
|
.set('Cookie', cookie)
|
||||||
|
.send({ 'auth.registrationMode': 'closed', 'upload.svgPolicy': 'reject' })
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports the pre-existing violation and never auto-changes it', async () => {
|
||||||
|
const profile = (
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get('/api/v1/admin/system/vs-nfd-profile')
|
||||||
|
.set('Cookie', cookie)
|
||||||
|
.expect(200)
|
||||||
|
).body as VsNfdProfileView;
|
||||||
|
expect(profile.mode).toBe('enforced');
|
||||||
|
expect(profile.entries.find((e) => e.key === 'feeds.enabled')!.compliant).toBe(false);
|
||||||
|
const settings = await request(app.getHttpServer())
|
||||||
|
.get('/api/v1/admin/settings')
|
||||||
|
.set('Cookie', cookie)
|
||||||
|
.expect(200);
|
||||||
|
expect(settings.body['feeds.enabled']).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.skipIf(!hasTestDb)('the same write passes outside enforced (issue #246)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
let cookie: string;
|
||||||
|
const suffix = uniqueSuffix();
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
process.env.VS_NFD_MODE = 'hidden';
|
||||||
|
prisma = createTestPrisma();
|
||||||
|
await prisma.rateLimit.deleteMany({});
|
||||||
|
app = await createTestApp();
|
||||||
|
cookie = await makeAdmin(app, prisma, suffix);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
delete process.env.VS_NFD_MODE;
|
||||||
|
await prisma.instanceSetting.deleteMany({ where: { key: 'upload.svgPolicy' } });
|
||||||
|
await prisma.user.deleteMany({ where: { username: { contains: suffix } } });
|
||||||
|
await prisma.$disconnect();
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mode hidden: the violating write is NOT rejected (UI-level only)', async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.patch('/api/v1/admin/settings')
|
||||||
|
.set('Cookie', cookie)
|
||||||
|
.send({ 'upload.svgPolicy': 'sanitize' })
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -93,6 +93,14 @@ describe.skipIf(!hasTestDb)('VS-NfD profile endpoint (e2e, issue #243)', () => {
|
|||||||
expect(view.violations).toBeGreaterThan(0);
|
expect(view.violations).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('mode marked: a violating write is NOT rejected (issue #246 contrast)', async () => {
|
||||||
|
await api()
|
||||||
|
.patch('/api/v1/admin/settings')
|
||||||
|
.set('Cookie', cookies.admin!)
|
||||||
|
.send({ 'upload.svgPolicy': 'sanitize' })
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
it('verdict follows a settings change', async () => {
|
it('verdict follows a settings change', async () => {
|
||||||
const settings = app.get(InstanceSettingsService);
|
const settings = app.get(InstanceSettingsService);
|
||||||
const before = (
|
const before = (
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, type OnModuleInit } from '@nestjs/common';
|
||||||
|
import { PinoLogger } from 'nestjs-pino';
|
||||||
import {
|
import {
|
||||||
VS_NFD_PROFILE,
|
VS_NFD_PROFILE,
|
||||||
describeCompliance,
|
describeCompliance,
|
||||||
@ -20,11 +21,33 @@ import { InstanceSettingsService, type InstanceSettings } from './instance-setti
|
|||||||
* (#244–#246) build on this evaluation; here it is exposure only.
|
* (#244–#246) build on this evaluation; here it is exposure only.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VsNfdProfileService {
|
export class VsNfdProfileService implements OnModuleInit {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly settings: InstanceSettingsService,
|
private readonly settings: InstanceSettingsService,
|
||||||
private readonly config: AppConfig,
|
private readonly config: AppConfig,
|
||||||
) {}
|
private readonly logger: PinoLogger,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(VsNfdProfileService.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Startup report (#246): existing violations are stated once, never
|
||||||
|
* auto-changed — the operator resolves them consciously. Must not throw
|
||||||
|
* on a database-less boot (healthz suites boot without a db). */
|
||||||
|
async onModuleInit(): Promise<void> {
|
||||||
|
if (this.config.env.VS_NFD_MODE === 'off') return;
|
||||||
|
try {
|
||||||
|
const view = await this.evaluate();
|
||||||
|
this.logger.info(
|
||||||
|
{
|
||||||
|
mode: view.mode,
|
||||||
|
violations: view.entries.filter((e) => !e.compliant).map((e) => e.key),
|
||||||
|
},
|
||||||
|
'VS-NfD profile verdict at startup',
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Stated at the next successful evaluation instead.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private valueOf(entry: VsNfdProfileEntry, settings: InstanceSettings): unknown {
|
private valueOf(entry: VsNfdProfileEntry, settings: InstanceSettings): unknown {
|
||||||
return entry.scope === 'instance'
|
return entry.scope === 'instance'
|
||||||
|
|||||||
@ -48,7 +48,7 @@ Settings-Cache ist in-process (operations.md).
|
|||||||
### 1.2 Deploy-Konfiguration (`.env` / Compose — nur Plattformzugriff, bewusst nicht per Admin-UI)
|
### 1.2 Deploy-Konfiguration (`.env` / Compose — nur Plattformzugriff, bewusst nicht per Admin-UI)
|
||||||
|
|
||||||
| Variable | Referenzwert | Warum |
|
| Variable | Referenzwert | Warum |
|
||||||
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `BACKUP_ALLOWED_TARGETS` | leer lassen **oder** exakt der eine freigegebene Spiegel-Host | leere Allowlist schaltet ALLE Fernziele hart ab (ADR 0026, #192) — „Backup nur lokal" ist damit deploy-seitig erzwungen und vom Site-Admin nicht aufweichbar (Rollentrennung, Betriebshandbuch §6). |
|
| `BACKUP_ALLOWED_TARGETS` | leer lassen **oder** exakt der eine freigegebene Spiegel-Host | leere Allowlist schaltet ALLE Fernziele hart ab (ADR 0026, #192) — „Backup nur lokal" ist damit deploy-seitig erzwungen und vom Site-Admin nicht aufweichbar (Rollentrennung, Betriebshandbuch §6). |
|
||||||
| `SESSION_ABSOLUTE_HOURS` | `12` (Default 168) | eine Sitzung überdauert keinen Arbeitstag; Neuanmeldung am nächsten Tag ist der Preis. |
|
| `SESSION_ABSOLUTE_HOURS` | `12` (Default 168) | eine Sitzung überdauert keinen Arbeitstag; Neuanmeldung am nächsten Tag ist der Preis. |
|
||||||
| `SESSION_IDLE_HOURS` | `2` (Default 72) | unbeaufsichtigte, noch angemeldete Arbeitsplätze fallen schnell zurück auf die Anmeldemaske. |
|
| `SESSION_IDLE_HOURS` | `2` (Default 72) | unbeaufsichtigte, noch angemeldete Arbeitsplätze fallen schnell zurück auf die Anmeldemaske. |
|
||||||
@ -58,7 +58,7 @@ Settings-Cache ist in-process (operations.md).
|
|||||||
| `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_SCOPES`, `OIDC_PROVIDER_LABEL` | IdP der Behörde konfigurieren | Fremdauthentisierung (#214, ADR 0021): Authorization Code + PKCE gegen den IdP der Umgebung; deploy-seitig, weil die Authentisierungshoheit Plattformsache ist. Erst-Login legt Konten just-in-time an; bestehende lokale Konten werden NIE stillschweigend per E-Mail übernommen (expliziter Link-Flow). Konfigurationsdetails: security.md §External authentication. |
|
| `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_SCOPES`, `OIDC_PROVIDER_LABEL` | IdP der Behörde konfigurieren | Fremdauthentisierung (#214, ADR 0021): Authorization Code + PKCE gegen den IdP der Umgebung; deploy-seitig, weil die Authentisierungshoheit Plattformsache ist. Erst-Login legt Konten just-in-time an; bestehende lokale Konten werden NIE stillschweigend per E-Mail übernommen (expliziter Link-Flow). Konfigurationsdetails: security.md §External authentication. |
|
||||||
| `AUTH_PROXY_HEADER`, `AUTH_PROXY_TRUSTED_PEERS`, `AUTH_PROXY_MAP`, `AUTH_PROXY_MODE`, `AUTH_PROXY_DN_ATTRIBUTE` | nur bei Perimeter-Authentisierung setzen | Alternativpfad (#215): Identität aus dem Proxy-Header, gültig NUR vom TCP-Peer der Allowlist; fremder Peer mit Header wird abgewiesen und auditiert (`auth.proxy_rejected`). Der Proxy MUSS den Header aus eingehendem Verkehr strippen. mTLS-Variante über weitergereichten Zertifikats-DN (`mtls-dn`). Ohne Perimeter-Auth: unkonfiguriert lassen (Header wirkungslos). |
|
| `AUTH_PROXY_HEADER`, `AUTH_PROXY_TRUSTED_PEERS`, `AUTH_PROXY_MAP`, `AUTH_PROXY_MODE`, `AUTH_PROXY_DN_ATTRIBUTE` | nur bei Perimeter-Authentisierung setzen | Alternativpfad (#215): Identität aus dem Proxy-Header, gültig NUR vom TCP-Peer der Allowlist; fremder Peer mit Header wird abgewiesen und auditiert (`auth.proxy_rejected`). Der Proxy MUSS den Header aus eingehendem Verkehr strippen. mTLS-Variante über weitergereichten Zertifikats-DN (`mtls-dn`). Ohne Perimeter-Auth: unkonfiguriert lassen (Header wirkungslos). |
|
||||||
| `AUTH_LOCAL_ENABLED` | `false` (Default `true`) | **der** harte Schalter aus ADR 0021 (#216, deploy-seitige Realisierung von `auth.local.enabled`): `false` schaltet JEDEN lokalen Credential-Flow auf 404 (Login, Signup, Verifikation, Passwort vergessen/zuruecksetzen/aendern) — Anmeldung ausschliesslich ueber OIDC (#214) bzw. Perimeter-Auth (#215). Deploy-seitig, damit ein kompromittierter Site-Admin ihn nicht zurueckdrehen kann. Reihenfolge: erst Setup/Bootstrap (SETUP_ADMIN_*), dann auf `false`. PAT-/Feed-Token-Ausgabe bleibt fuer IdP-authentisierte Nutzer verfuegbar (eigene Schalter `api.enabled`/`feeds.enabled`; bewusste Entscheidung #216). |
|
| `AUTH_LOCAL_ENABLED` | `false` (Default `true`) | **der** harte Schalter aus ADR 0021 (#216, deploy-seitige Realisierung von `auth.local.enabled`): `false` schaltet JEDEN lokalen Credential-Flow auf 404 (Login, Signup, Verifikation, Passwort vergessen/zuruecksetzen/aendern) — Anmeldung ausschliesslich ueber OIDC (#214) bzw. Perimeter-Auth (#215). Deploy-seitig, damit ein kompromittierter Site-Admin ihn nicht zurueckdrehen kann. Reihenfolge: erst Setup/Bootstrap (SETUP_ADMIN_*), dann auf `false`. PAT-/Feed-Token-Ausgabe bleibt fuer IdP-authentisierte Nutzer verfuegbar (eigene Schalter `api.enabled`/`feeds.enabled`; bewusste Entscheidung #216). |
|
||||||
| `VS_NFD_MODE` | `marked` (Default `off`; Ziel `enforced`) | Härtungsprofil-Modus (#243, ADR 0027): die Anwendung kennt diese Referenzkonfiguration als maschinenlesbaren Katalog und bewertet die laufende Konfiguration dagegen (Site-Admin → Einstellungen). `marked` markiert Abweichungen, `hidden` blendet abweichende Optionen aus, `enforced` weist abweichende Schreibzugriffe serverseitig ab — die drei Behandlungsstufen rollen mit #244–#246 aus; bis dahin ist `marked` der empfohlene Wert (reine Anzeige). Deploy-seitig wie `BACKUP_ALLOWED_TARGETS`: ein kompromittierter Site-Admin kann den Modus nicht aufweichen. Außerhalb von VS-Kontexten bleibt der Default `off` — keinerlei Markierung. |
|
| `VS_NFD_MODE` | `enforced` (Default `off`) | Härtungsprofil-Modus (#243, ADR 0027): die Anwendung kennt diese Referenzkonfiguration als maschinenlesbaren Katalog und bewertet die laufende Konfiguration dagegen (Site-Admin → Einstellungen). `marked` markiert Abweichungen, `hidden` blendet abweichende Optionen aus, `enforced` weist abweichende Schreibzugriffe serverseitig ab — alle drei Behandlungsstufen sind umgesetzt (#244–#246); für den VS-NfD-Referenzbetrieb ist `enforced` der empfohlene Modus — nur er hält auch skriptgesteuerte Clients an das Profil. Bestehende Abweichungen werden beim Start und in der Admin-Ansicht gemeldet, nie automatisch geändert. Deploy-seitig wie `BACKUP_ALLOWED_TARGETS`: ein kompromittierter Site-Admin kann den Modus nicht aufweichen. Außerhalb von VS-Kontexten bleibt der Default `off` — keinerlei Markierung. |
|
||||||
|
|
||||||
### 1.3 Noch nicht verfügbar (Regel: landet hier im selben PR)
|
### 1.3 Noch nicht verfügbar (Regel: landet hier im selben PR)
|
||||||
|
|
||||||
|
|||||||
@ -111,5 +111,6 @@
|
|||||||
"scope_required": "Dieses API-Token hat nicht den erforderlichen Scope.",
|
"scope_required": "Dieses API-Token hat nicht den erforderlichen Scope.",
|
||||||
"pond_not_found": "Der Teich existiert nicht.",
|
"pond_not_found": "Der Teich existiert nicht.",
|
||||||
"classification_lower_forbidden": "Zum Herabstufen der Einstufung fehlt die Berechtigung (Teich-Admin erforderlich).",
|
"classification_lower_forbidden": "Zum Herabstufen der Einstufung fehlt die Berechtigung (Teich-Admin erforderlich).",
|
||||||
"classified_upload_blocked": "Uploads auf eingestufte Seiten sind auf dieser Instanz blockiert."
|
"classified_upload_blocked": "Uploads auf eingestufte Seiten sind auf dieser Instanz blockiert.",
|
||||||
|
"vs_nfd_profile_violation": "Diese Einstellung würde vom VS-NfD-Referenzprofil abweichen — das Deployment erzwingt das Profil (VS_NFD_MODE=enforced)."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,5 +111,6 @@
|
|||||||
"scope_required": "This API token does not have the required scope.",
|
"scope_required": "This API token does not have the required scope.",
|
||||||
"pond_not_found": "The pond does not exist.",
|
"pond_not_found": "The pond does not exist.",
|
||||||
"classification_lower_forbidden": "You lack the permission to lower the classification (Pond Admin required).",
|
"classification_lower_forbidden": "You lack the permission to lower the classification (Pond Admin required).",
|
||||||
"classified_upload_blocked": "Uploads to classified pages are blocked on this instance."
|
"classified_upload_blocked": "Uploads to classified pages are blocked on this instance.",
|
||||||
|
"vs_nfd_profile_violation": "This setting would deviate from the VS-NfD reference profile — the deployment enforces the profile (VS_NFD_MODE=enforced)."
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user