#213: warn on uploads to classified pages; instance policy can block
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m15s
CI / Build container images (pull_request) Successful in 4m27s
CI / Auth e2e pack (pull_request) Successful in 9m10s
CI / Import/export fidelity gate (pull_request) Successful in 53s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 20s
CI / Lint, typecheck, test (push) Successful in 5m47s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m26s
CI / Import/export fidelity gate (push) Successful in 1m0s
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m15s
CI / Build container images (pull_request) Successful in 4m27s
CI / Auth e2e pack (pull_request) Successful in 9m10s
CI / Import/export fidelity gate (pull_request) Successful in 53s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 20s
CI / Lint, typecheck, test (push) Successful in 5m47s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m26s
CI / Import/export fidelity gate (push) Successful in 1m0s
The attachments panel of a classified page shows a persistent notice naming the consequence (de+en): the file inherits the page's classification but its content carries no marking (#212). The new instance setting classification.uploadPolicy (default warn, documented; the VS-NfD reference configuration blocks, #227) hardens the warning into a server-side rejection (403 classified_upload_blocked) — enforced in the upload service, not only in the UI. Tests: warning visible in the local attachments pack; block enforced server-side with warn/block both ways and open pages unaffected. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e505fc74dc
commit
868b79c8bc
@ -394,6 +394,54 @@ describe.skipIf(!hasTestDb)('files (e2e, issue #27)', () => {
|
|||||||
expect(openOrphan.headers['content-disposition']).toContain('filename="lose-datei.pdf"');
|
expect(openOrphan.headers['content-disposition']).toContain('filename="lose-datei.pdf"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('blocks uploads to classified pages server-side when the policy says so (issue #213)', async () => {
|
||||||
|
const settings = app.get(InstanceSettingsService);
|
||||||
|
const page = await api()
|
||||||
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
||||||
|
.set('Cookie', ownerCookie)
|
||||||
|
.send({ title: `Blocked Uploads ${suffix}` })
|
||||||
|
.expect(201);
|
||||||
|
await prisma.page.update({
|
||||||
|
where: { id: page.body.id as string },
|
||||||
|
data: { classification: 'VS_NFD' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default policy `warn`: the upload is allowed (the UI shows the notice).
|
||||||
|
await api()
|
||||||
|
.post(`/api/v1/pages/${page.body.id}/files`)
|
||||||
|
.set('Cookie', ownerCookie)
|
||||||
|
.attach('file', Buffer.from('%PDF-1.4 warned upload'), 'warned.pdf')
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
await settings.set('classification.uploadPolicy', 'block', 'test');
|
||||||
|
try {
|
||||||
|
// Enforced server-side, not only in the UI.
|
||||||
|
const blocked = await api()
|
||||||
|
.post(`/api/v1/pages/${page.body.id}/files`)
|
||||||
|
.set('Cookie', ownerCookie)
|
||||||
|
.attach('file', Buffer.from('%PDF-1.4 blocked upload'), 'blocked.pdf')
|
||||||
|
.expect(403);
|
||||||
|
expect((blocked.body as { code: string }).code).toBe('classified_upload_blocked');
|
||||||
|
|
||||||
|
// Unclassified pages stay uploadable under `block`.
|
||||||
|
const open = await api()
|
||||||
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
||||||
|
.set('Cookie', ownerCookie)
|
||||||
|
.send({ title: `Open Uploads ${suffix}` })
|
||||||
|
.expect(201);
|
||||||
|
await api()
|
||||||
|
.post(`/api/v1/pages/${open.body.id}/files`)
|
||||||
|
.set('Cookie', ownerCookie)
|
||||||
|
.attach('file', Buffer.from('%PDF-1.4 open upload'), 'open.pdf')
|
||||||
|
.expect(201);
|
||||||
|
} finally {
|
||||||
|
await settings.set('classification.uploadPolicy', 'warn', 'test');
|
||||||
|
await prisma.instanceSetting.deleteMany({
|
||||||
|
where: { key: 'classification.uploadPolicy' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('pond file manager reports usage, orphans, and page links (#61)', async () => {
|
it('pond file manager reports usage, orphans, and page links (#61)', async () => {
|
||||||
const page = await api()
|
const page = await api()
|
||||||
.post(`/api/v1/ponds/${pondId}/pages`)
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Readable } from 'node:stream';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
Injectable,
|
Injectable,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
@ -205,6 +206,16 @@ export class FilesService {
|
|||||||
): Promise<AttachmentView> {
|
): Promise<AttachmentView> {
|
||||||
const page = await this.prisma.page.findFirst({ where: { id: pageId } });
|
const page = await this.prisma.page.findFirst({ where: { id: pageId } });
|
||||||
if (!page) throw new NotFoundException();
|
if (!page) throw new NotFoundException();
|
||||||
|
// Attaching to a classified page (issue #213, ADR 0022): the file will
|
||||||
|
// inherit a classification its content cannot carry (#212). The UI warns;
|
||||||
|
// the instance can harden the warning into a server-side block — enforced
|
||||||
|
// HERE, not only client-side.
|
||||||
|
if (page.classification === 'VS_NFD') {
|
||||||
|
const policy = await this.settings.get('classification.uploadPolicy');
|
||||||
|
if (policy === 'block') {
|
||||||
|
throw new ForbiddenException({ code: 'classified_upload_blocked' });
|
||||||
|
}
|
||||||
|
}
|
||||||
return this.upload(user, page.pondId, file, page.id);
|
return this.upload(user, page.pondId, file, page.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,11 @@ export const INSTANCE_SETTINGS = {
|
|||||||
// parent page (#205) wins over this default. The marking is not a
|
// parent page (#205) wins over this default. The marking is not a
|
||||||
// protection mechanism — permissions ignore it.
|
// protection mechanism — permissions ignore it.
|
||||||
'classification.newPageDefault': z.enum(['unclassified', 'vs_nfd']).default('unclassified'),
|
'classification.newPageDefault': z.enum(['unclassified', 'vs_nfd']).default('unclassified'),
|
||||||
|
// Attaching files to a classified page (issue #213, ADR 0022): the UI
|
||||||
|
// always warns (the file inherits a classification its content cannot
|
||||||
|
// carry, #212); `block` hardens the warning into a server-side rejection.
|
||||||
|
// Default `warn` — blocking is the reference-configuration choice (#227).
|
||||||
|
'classification.uploadPolicy': z.enum(['warn', 'block']).default('warn'),
|
||||||
// Non-image upload allowlist (ADR 0011, issue #61): lowercase extensions
|
// Non-image upload allowlist (ADR 0011, issue #61): lowercase extensions
|
||||||
// without the dot. Images are always allowed regardless; SVG is governed
|
// without the dot. Images are always allowed regardless; SVG is governed
|
||||||
// by `upload.svgPolicy`. Normalized (lowercased, dot-stripped, deduped) so
|
// by `upload.svgPolicy`. Normalized (lowercased, dot-stripped, deduped) so
|
||||||
|
|||||||
@ -109,3 +109,19 @@ test('pond file manager shows usage and flags an orphan (Pond Admin)', async ({
|
|||||||
|
|
||||||
await context.close();
|
await context.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shows the classified-upload warning on a classified page (issue #213)', async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto('/p/content-fixtures/classified-note');
|
||||||
|
await openAttachments(page);
|
||||||
|
// The persistent warning names the consequence; the wording is the fixed
|
||||||
|
// marking formula (ADR 0022), not localized.
|
||||||
|
await expect(page.locator('.attachments-panel__warning')).toBeVisible();
|
||||||
|
await expect(page.locator('.attachments-panel__warning')).toContainText(
|
||||||
|
'VS – NUR FÜR DEN DIENSTGEBRAUCH',
|
||||||
|
);
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { AttachmentListItemView } from '@dorfteich/shared';
|
import type { AttachmentListItemView, PageClassification } from '@dorfteich/shared';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import type { Editor } from '@tiptap/react';
|
import type { Editor } from '@tiptap/react';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
@ -19,11 +19,16 @@ export function AttachmentsPanel({
|
|||||||
pageId,
|
pageId,
|
||||||
editor,
|
editor,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
classification,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
editor: Editor | null;
|
editor: Editor | null;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
/** VS-NfD level of the page (issue #213): a classified page shows the
|
||||||
|
* upload warning naming the consequence — attachments inherit a level
|
||||||
|
* their content cannot carry (#212). */
|
||||||
|
classification?: PageClassification;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const { t } = useTranslation('files');
|
const { t } = useTranslation('files');
|
||||||
@ -96,6 +101,12 @@ export function AttachmentsPanel({
|
|||||||
|
|
||||||
<FormError error={error} />
|
<FormError error={error} />
|
||||||
|
|
||||||
|
{canEdit && classification === 'vs_nfd' && (
|
||||||
|
<p className="attachments-panel__warning" role="note">
|
||||||
|
{t('classifiedUploadWarning')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div className="attachments-panel__upload">
|
<div className="attachments-panel__upload">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -29,6 +29,7 @@ interface InstanceSettings {
|
|||||||
'upload.allowedExtensions': string[];
|
'upload.allowedExtensions': string[];
|
||||||
'upload.svgPolicy': 'reject' | 'sanitize';
|
'upload.svgPolicy': 'reject' | 'sanitize';
|
||||||
'classification.newPageDefault': 'unclassified' | 'vs_nfd';
|
'classification.newPageDefault': 'unclassified' | 'vs_nfd';
|
||||||
|
'classification.uploadPolicy': 'warn' | 'block';
|
||||||
'legal.imprint': string;
|
'legal.imprint': string;
|
||||||
'legal.privacyPolicy': string;
|
'legal.privacyPolicy': string;
|
||||||
'home.content': string;
|
'home.content': string;
|
||||||
@ -101,6 +102,15 @@ export function AdminSettingsPage(): React.JSX.Element {
|
|||||||
<option value="vs_nfd">{t('settings:admin.classificationVsNfd')}</option>
|
<option value="vs_nfd">{t('settings:admin.classificationVsNfd')}</option>
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field
|
||||||
|
label={t('settings:admin.uploadPolicy')}
|
||||||
|
hint={t('settings:admin.uploadPolicyHelp')}
|
||||||
|
>
|
||||||
|
<select {...form.register('classification.uploadPolicy')}>
|
||||||
|
<option value="warn">{t('settings:admin.uploadPolicyWarn')}</option>
|
||||||
|
<option value="block">{t('settings:admin.uploadPolicyBlock')}</option>
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
<button type="submit" className="button" disabled={form.formState.isSubmitting}>
|
<button type="submit" className="button" disabled={form.formState.isSubmitting}>
|
||||||
{t('settings:admin.save')}
|
{t('settings:admin.save')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { DEFAULT_FONTS, extractOutline } from '@dorfteich/shared';
|
import { DEFAULT_FONTS, extractOutline } from '@dorfteich/shared';
|
||||||
import type { PageListItemView, PageStateView, PondView } from '@dorfteich/shared';
|
import type {
|
||||||
|
PageClassification,
|
||||||
|
PageListItemView,
|
||||||
|
PageStateView,
|
||||||
|
PondView,
|
||||||
|
} from '@dorfteich/shared';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Collaboration } from '@tiptap/extension-collaboration';
|
import { Collaboration } from '@tiptap/extension-collaboration';
|
||||||
import { EditorContent, useEditor } from '@tiptap/react';
|
import { EditorContent, useEditor } from '@tiptap/react';
|
||||||
@ -126,6 +131,8 @@ interface ResolvedPage {
|
|||||||
pondId: string;
|
pondId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
/** VS-NfD level (issue #213); undefined for the offline-cache fallback. */
|
||||||
|
classification?: PageClassification;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PageEditor({
|
function PageEditor({
|
||||||
@ -305,6 +312,7 @@ function PageEditor({
|
|||||||
pageId={page.id}
|
pageId={page.id}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
|
classification={page.classification}
|
||||||
onClose={onCloseAttachments}
|
onClose={onCloseAttachments}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -421,7 +429,13 @@ export function PageEditorPage(): React.JSX.Element {
|
|||||||
const offlineCached = !navigator.onLine && !page.data ? recallPage(pondSlug, pageSlug) : null;
|
const offlineCached = !navigator.onLine && !page.data ? recallPage(pondSlug, pageSlug) : null;
|
||||||
|
|
||||||
const resolved: ResolvedPage | null = page.data
|
const resolved: ResolvedPage | null = page.data
|
||||||
? { id: page.data.id, pondId: page.data.pondId, slug: page.data.slug, title: page.data.title }
|
? {
|
||||||
|
id: page.data.id,
|
||||||
|
pondId: page.data.pondId,
|
||||||
|
slug: page.data.slug,
|
||||||
|
title: page.data.title,
|
||||||
|
classification: page.data.classification,
|
||||||
|
}
|
||||||
: offlineCached
|
: offlineCached
|
||||||
? {
|
? {
|
||||||
id: offlineCached.pageId,
|
id: offlineCached.pageId,
|
||||||
|
|||||||
@ -3071,6 +3071,17 @@ ul[data-type='task_list'] li p:last-of-type {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Upload warning on classified pages (issue #213, ADR 0022) — a persistent
|
||||||
|
note, not a dismissable toast: the consequence applies to every upload. */
|
||||||
|
.attachments-panel__warning {
|
||||||
|
margin: 0 0 var(--space-2);
|
||||||
|
padding: var(--space-2);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-left: 3px solid currentColor;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
.attachments-panel__upload {
|
.attachments-panel__upload {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -59,7 +59,7 @@ _Meilenstein: `M26 — VS-NfD: classification metadata`_
|
|||||||
- [x] Markdown-ZIP (Frontmatter + Aufdruck) · 1 AT · #210
|
- [x] Markdown-ZIP (Frontmatter + Aufdruck) · 1 AT · #210
|
||||||
- [x] Atom-Feeds, Public-API, Suchergebnisse, No-JS-Shell · 2–3 AT · #211
|
- [x] Atom-Feeds, Public-API, Suchergebnisse, No-JS-Shell · 2–3 AT · #211
|
||||||
- [x] Attachment-Download (Dateiname-Präfix + Begleitdatei) · 1–2 AT · #212
|
- [x] Attachment-Download (Dateiname-Präfix + Begleitdatei) · 1–2 AT · #212
|
||||||
- [ ] Warnung/Sperre beim Anhängen an eingestufte Seiten · 1 AT · #213
|
- [x] Warnung/Sperre beim Anhängen an eingestufte Seiten · 1 AT · #213
|
||||||
|
|
||||||
### P1-3 Verifizierter Offline-/Airgap-Pfad · 8–10 AT ⟵ neu aus Roadmap
|
### P1-3 Verifizierter Offline-/Airgap-Pfad · 8–10 AT ⟵ neu aus Roadmap
|
||||||
|
|
||||||
|
|||||||
@ -110,5 +110,6 @@
|
|||||||
"backup_set_not_found": "Das gewählte Backup-Set wurde nicht gefunden.",
|
"backup_set_not_found": "Das gewählte Backup-Set wurde nicht gefunden.",
|
||||||
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,5 +19,6 @@
|
|||||||
"svgSanitize": "Bereinigen (Skripte entfernen)",
|
"svgSanitize": "Bereinigen (Skripte entfernen)",
|
||||||
"svgReject": "Ablehnen",
|
"svgReject": "Ablehnen",
|
||||||
"save": "Upload-Einstellungen speichern"
|
"save": "Upload-Einstellungen speichern"
|
||||||
}
|
},
|
||||||
|
"classifiedUploadWarning": "Diese Seite ist als „VS – NUR FÜR DEN DIENSTGEBRAUCH\" eingestuft. Angehängte Dateien erben die Einstufung; die Datei selbst trägt im Inhalt keine Kennzeichnung (nur Dateinamens-Präfix und Begleitdatei beim Download)."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,11 @@
|
|||||||
"newPageClassification": "Einstufung neuer Seiten",
|
"newPageClassification": "Einstufung neuer Seiten",
|
||||||
"newPageClassificationHelp": "Standard-Einstufung (VS-NfD-Kennzeichnung) für neu angelegte Seiten. Die Kennzeichnung ist keine Zugriffskontrolle; die Trennung von Einstufungsniveaus leistet die Umgebung (eine Instanz je Niveau).",
|
"newPageClassificationHelp": "Standard-Einstufung (VS-NfD-Kennzeichnung) für neu angelegte Seiten. Die Kennzeichnung ist keine Zugriffskontrolle; die Trennung von Einstufungsniveaus leistet die Umgebung (eine Instanz je Niveau).",
|
||||||
"classificationUnclassified": "Offen — keine Kennzeichnung",
|
"classificationUnclassified": "Offen — keine Kennzeichnung",
|
||||||
"classificationVsNfd": "VS – NUR FÜR DEN DIENSTGEBRAUCH"
|
"classificationVsNfd": "VS – NUR FÜR DEN DIENSTGEBRAUCH",
|
||||||
|
"uploadPolicy": "Datei-Uploads auf eingestufte Seiten",
|
||||||
|
"uploadPolicyHelp": "Anhänge erben die Einstufung der Seite, tragen selbst aber keine Kennzeichnung im Inhalt. „Blockieren\" lehnt Uploads auf eingestufte Seiten serverseitig ab.",
|
||||||
|
"uploadPolicyWarn": "Warnen — Upload mit deutlichem Hinweis erlauben",
|
||||||
|
"uploadPolicyBlock": "Blockieren — Uploads auf eingestufte Seiten ablehnen"
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"title": "Startseite",
|
"title": "Startseite",
|
||||||
|
|||||||
@ -110,5 +110,6 @@
|
|||||||
"backup_set_not_found": "The selected backup set was not found.",
|
"backup_set_not_found": "The selected backup set was not found.",
|
||||||
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,5 +19,6 @@
|
|||||||
"svgSanitize": "Sanitize (strip scripts)",
|
"svgSanitize": "Sanitize (strip scripts)",
|
||||||
"svgReject": "Reject",
|
"svgReject": "Reject",
|
||||||
"save": "Save upload settings"
|
"save": "Save upload settings"
|
||||||
}
|
},
|
||||||
|
"classifiedUploadWarning": "This page is classified “VS – NUR FÜR DEN DIENSTGEBRAUCH”. Attached files inherit the classification; the file content itself carries no marking (only the filename prefix and companion file on download)."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,11 @@
|
|||||||
"newPageClassification": "Classification of new pages",
|
"newPageClassification": "Classification of new pages",
|
||||||
"newPageClassificationHelp": "Default classification (VS-NfD marking) for newly created pages. The marking is not access control; separating classification levels is the environment’s job (one instance per level).",
|
"newPageClassificationHelp": "Default classification (VS-NfD marking) for newly created pages. The marking is not access control; separating classification levels is the environment’s job (one instance per level).",
|
||||||
"classificationUnclassified": "Open — no marking",
|
"classificationUnclassified": "Open — no marking",
|
||||||
"classificationVsNfd": "VS – NUR FÜR DEN DIENSTGEBRAUCH"
|
"classificationVsNfd": "VS – NUR FÜR DEN DIENSTGEBRAUCH",
|
||||||
|
"uploadPolicy": "File uploads to classified pages",
|
||||||
|
"uploadPolicyHelp": "Attachments inherit the page’s classification but carry no marking in their content. “Block” rejects uploads to classified pages server-side.",
|
||||||
|
"uploadPolicyWarn": "Warn — allow the upload with a clear notice",
|
||||||
|
"uploadPolicyBlock": "Block — reject uploads to classified pages"
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"title": "Landing page",
|
"title": "Landing page",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user