--- post_title: Discord Notification Payload Fix Plan author1: "Charon Team" post_slug: discord-notification-payload-fix-plan categories: - notifications tags: - discord - webhooks - payloads - bugfix summary: "Plan to fix Discord test notifications by aligning templates with required payload fields and updating tests." post_date: "2026-02-11" --- ## Discord Notification Payload Fix Plan Last updated: 2026-02-11 ## 1) Introduction Discord test notifications fail with the error: "discord payload requires 'content' or 'embeds' field". The fix requires aligning default notification templates with Discord (and Slack) webhook requirements across backend and frontend, while preserving custom templates and existing webhook behavior for other providers. **Objectives** - Ensure Discord test notifications succeed with default templates. - Keep template rendering consistent between backend and frontend. - Preserve validation for service-specific payload requirements. - Provide clear, testable behavior for previews and test sends. ## 2) Research Findings (Root Cause) ### 2.1 Request Flow and Failure Point 1) UI "Test" button in Notifications page sends POST to `/api/v1/notifications/providers/test`. 2) Backend handler `NotificationProviderHandler.Test()` calls `NotificationService.TestProvider()`. 3) `TestProvider()` uses `sendJSONPayload()` for JSON-template providers. 4) `sendJSONPayload()` renders built-in minimal/detailed templates when `provider.Template` is `minimal` or `detailed` and `Config` is empty. 5) Discord validation in `sendJSONPayload()` rejects payloads missing `content` or `embeds`, returning the error seen by users. ### 2.2 Where the Payload Goes Missing **Backend templates:** - Minimal template uses `message/title/time/event` keys and omits `content` or `embeds`. - Detailed template uses the same keys plus host/service data. - Validation for Discord requires `content` or `embeds`, so default templates fail. **Frontend defaults:** - The Notifications form defaults to `template: "minimal"` and uses prefilled template JSON with `message/title/time/event` only. - This reinforces the backend default template and causes test sends to fail for Discord (and Slack, which requires `text` or `blocks`). ### 2.3 Evidence (File Trace) - Default template selection and Discord/Slack validation live in [backend/internal/services/notification_service.go](backend/internal/services/notification_service.go#L170-L272) - Provider default template and `Template` field live in [backend/internal/models/notification_provider.go](backend/internal/models/notification_provider.go#L10-L47) - Test endpoint that triggers the failure is in [backend/internal/api/handlers/notification_provider_handler.go](backend/internal/api/handlers/notification_provider_handler.go#L100-L140) - Frontend template buttons and defaults are in [frontend/src/pages/Notifications.tsx](frontend/src/pages/Notifications.tsx#L24-L235) **Root cause:** built-in minimal/detailed templates do not include required Discord fields (`content` or `embeds`). The frontend defaults to those templates and the backend enforces strict validation, so tests fail even when the webhook URL is valid. ### 2.3 Security Notification Settings 404 Regression (New) **Symptom:** `GET /api/v1/notifications/settings/security` returns `404`. **Findings:** - Backend routes register security notification settings at `GET/PUT /api/v1/security/notifications/settings`. - Frontend API calls (and tests) use `GET/PUT /api/v1/notifications/settings/security`. - This path mismatch causes the 404 and is a regression relative to prior behavior where the frontend path was valid. **Root cause:** route path mismatch between frontend API client and backend route registration. The handler exists, but the frontend calls a different endpoint path. **Missing component:** route registration (alias) for `/api/v1/notifications/settings/security` or an updated frontend path to match `/api/v1/security/notifications/settings`. ### 2.4 Comprehensive Notification Path Audit (2026-02-11) **Scope:** backend notification routes in `backend/internal/api/routes/` and frontend notification API calls in `frontend/src/api/`. **Audit summary:** - **Mismatches found:** 1 - **Pattern:** All notification endpoints use `/api/v1/notifications/*` except security notification settings, which are registered under `/api/v1/security/notifications/settings` in the backend. **Path mapping table (backend vs frontend):** | Backend Route | Frontend Call | Match Status | Endpoint Purpose | | --- | --- | --- | --- | | `/api/v1/notifications` (GET) | — | ✅ Match (backend-only) | List notifications | | `/api/v1/notifications/:id/read` (POST) | — | ✅ Match (backend-only) | Mark a notification read | | `/api/v1/notifications/read-all` (POST) | — | ✅ Match (backend-only) | Mark all notifications read | | `/api/v1/notifications/providers` (GET/POST) | `/api/v1/notifications/providers` | ✅ Match | Provider list and create | | `/api/v1/notifications/providers/:id` (PUT/DELETE) | `/api/v1/notifications/providers/:id` | ✅ Match | Provider update/delete | | `/api/v1/notifications/providers/test` (POST) | `/api/v1/notifications/providers/test` | ✅ Match | Provider test send | | `/api/v1/notifications/providers/preview` (POST) | `/api/v1/notifications/providers/preview` | ✅ Match | Provider preview render | | `/api/v1/notifications/templates` (GET) | `/api/v1/notifications/templates` | ✅ Match | Built-in templates list | | `/api/v1/notifications/external-templates` (GET/POST) | `/api/v1/notifications/external-templates` | ✅ Match | External template list/create | | `/api/v1/notifications/external-templates/:id` (PUT/DELETE) | `/api/v1/notifications/external-templates/:id` | ✅ Match | External template update/delete | | `/api/v1/notifications/external-templates/preview` (POST) | `/api/v1/notifications/external-templates/preview` | ✅ Match | External template preview render | | `/api/v1/security/notifications/settings` (GET/PUT) | `/api/v1/notifications/settings/security` | ❌ Mismatch | Security notification settings | **Pattern analysis:** - The mismatch is isolated to the security notification settings endpoint. - All other notification-related routes follow a consistent `/api/v1/notifications/*` pattern. - No evidence of a broader systematic inversion beyond the security settings endpoint. **Git blame (path establishment):** - Backend path `/api/v1/security/notifications/settings` registered in [backend/internal/api/routes/routes.go](backend/internal/api/routes/routes.go#L227-L231), blamed to commit `3169b0515` (2026-02-09). - Frontend path `/api/v1/notifications/settings/security` used in [frontend/src/api/notifications.ts](frontend/src/api/notifications.ts#L188-L203), blamed to commit `3169b0515` (2026-02-09). **Fix recommendation:** - **Preferred:** Add a backend route alias for `/api/v1/notifications/settings/security` to map to the existing security settings handler. This preserves backward compatibility and keeps the broader `/api/v1/notifications/*` namespace consistent for the frontend. - **Alternative:** Update frontend calls to `/api/v1/security/notifications/settings` and adjust frontend tests accordingly. **Impact on original Discord test failure:** - The Discord test failure is tied to provider payload validation and uses `/api/v1/notifications/providers/test`; it is **not** caused by the security settings mismatch. The mismatch only explains the 404 regression for security settings reads/updates. ## 3) Technical Specifications ### 3.1 Template Catalog (Service-Specific) Introduce a service-aware template catalog for built-in templates and use it both for rendering and preview. This ensures Discord and Slack requirements are met while preserving current behavior for generic webhooks. **Template mapping (proposed):** | Provider Type | Minimal Template | Detailed Template | | --- | --- | --- | | discord | `content` from Title/Message | `embeds` with title/description/timestamp | | slack | `text` from Title/Message | `text` from Title/Message (no blocks) | | gotify | `message` + `title` | `message` + `title` + extras | | webhook | current minimal | current detailed | | generic | current minimal | current detailed | ### 3.2 Before/After Payload Structures **Before (current minimal template):** ```json { "message": "{{.Message}}", "title": "{{.Title}}", "time": "{{.Time}}", "event": "{{.EventType}}" } ``` **After (Discord minimal template):** ```json { "content": "{{.Title}} - {{.Message}}", "username": "Charon" } ``` **After (Discord detailed template):** ```json { "embeds": [ { "title": "{{.Title}}", "description": "{{.Message}}", "timestamp": "{{.Time}}", "fields": [ {"name": "Event", "value": "{{.EventType}}", "inline": true}, {"name": "Host", "value": "{{.HostName}}", "inline": true} ] } ] } ``` **After (Slack minimal template):** ```json { "text": "{{.Title}} - {{.Message}}" } ``` **After (Slack detailed template):** ```json { "text": "{{.Title}} - {{.Message}}" } ``` ### 3.3 Validation Rules - Discord: treat missing or empty `content` and `embeds` as invalid. If both are missing or empty, return the existing error message. - Slack: treat missing or empty `text` as invalid. `blocks` are not used in this plan. - Gotify: `message` must be present and non-empty. - Custom templates remain user-defined and are validated by existing rules. ### 3.4 Preview Behavior Preview (`/api/v1/notifications/providers/preview`) should use the same service-aware template selection and validation so users see failures before attempting a real send. ### 3.5 Edge Cases - Empty `Title` and `Message`: fallback to a safe string (e.g., "Charon notification") so `content` or `text` is not empty for Discord or Slack. - `Message` only or `Title` only: concatenate non-empty values with " - ". - Discord detailed: if `embeds` would be empty or missing after rendering, fallback to a `content` string derived from Title/Message. - Slack detailed: if `text` renders empty, fallback to the safe string. - No empty payloads: if a rendered payload would be empty after all fallbacks, return a validation error and do not send to any webhook. - Custom templates must remain unchanged; only built-in template selection becomes service-aware. ## 4) Implementation Plan (Phased) ### Pre-Implementation Gate (Required) 1) Update `requirements.md`, `design.md`, and `tasks.md` for this plan. 2) Add a trace map entry for preview handler/tests and include preview in the testing strategy. 3) Proceed only after these artifacts are updated and reviewed. ### Phase 1: Playwright Expectations - Update E2E tests to validate Discord provider test success with the default minimal template and confirm Slack provider test success. - Add a negative test to confirm Discord preview/test fails when a custom template omits both `content` and `embeds`. **Targets:** - `tests/settings/notifications.spec.ts` ### Phase 1a: 404 Regression Fix (Security Notification Settings) - Align the security notification settings endpoint path between frontend and backend. - Preferred minimal fix: add a route alias that maps `/api/v1/notifications/settings/security` to the existing handler for `/api/v1/security/notifications/settings` to preserve backward compatibility. - Alternative fix: update frontend API calls to use `/api/v1/security/notifications/settings` and update associated frontend tests. **Targets:** - Backend route registration in [backend/internal/api/routes/routes.go](backend/internal/api/routes/routes.go) - Frontend client in [frontend/src/api/notifications.ts](frontend/src/api/notifications.ts) - Frontend tests: [frontend/src/api/notifications.test.ts](frontend/src/api/notifications.test.ts) and [frontend/src/api/__tests__/notifications.test.ts](frontend/src/api/__tests__/notifications.test.ts) ### Phase 2: Backend Template Selection - Add a template catalog keyed by provider type and template variant. - Update `sendJSONPayload()` and `RenderTemplate()` to use service-aware templates for `minimal` and `detailed`. - Update validation to treat empty strings as missing for required fields and to enforce non-empty fallbacks. **Targets:** - `backend/internal/services/notification_service.go` - `backend/internal/services/notification_service_test.go` - `backend/internal/services/notification_service_json_test.go` ### Phase 3: Frontend Template Defaults - Update the Notifications form to set minimal/detailed templates based on provider type (Discord vs Slack vs Gotify vs Generic/Webhook). - Ensure the preview content reflects the new defaults. **Targets:** - `frontend/src/pages/Notifications.tsx` ### Phase 4: Integration and Regression Testing - Run Playwright E2E tests first (notifications suite). - Run backend unit tests with coverage after E2E passes. - Run frontend unit tests (Vitest) and type checks. ### Phase 5: Documentation - Update API or user docs only if the default template behavior is documented or exposed. Document the Discord/Slack default template expectations if needed. ## 5) Testing Strategy ### 5.1 Backend Unit Tests (Go) - Add tests for service-aware minimal/detailed templates: - Discord minimal produces `content`. - Discord detailed produces `embeds`. - Slack minimal produces `text`. - Slack detailed produces `text` (blocks are not used). - Add tests for preview path behavior: - Preview uses service-aware templates. - Preview enforces fallback rules and rejects empty payloads. - Update existing tests that assert `message/title` for minimal templates to account for provider-type differences. **Targets:** - `backend/internal/services/notification_service_test.go` - `backend/internal/services/notification_service_json_test.go` - `backend/internal/api/handlers/notification_provider_preview_handler_test.go` (new) ### 5.2 Frontend Unit Tests (Vitest) - Add or update tests to confirm template selection changes with provider type and that the generated config includes `content` for Discord. **Targets:** - `frontend/src/pages/__tests__/Notifications.test.tsx` (new or update existing if present) ### 5.3 Playwright E2E - Verify the Discord provider test succeeds with default minimal template. - Verify Slack provider test succeeds with default minimal template. - Verify custom template without required fields fails and surfaces the backend error message. - Verify provider preview uses service-aware templates and rejects empty payloads. **Targets:** - `tests/settings/notifications.spec.ts` ### 5.4 Manual Testing Steps 1) Create a Discord provider with a real webhook URL. 2) Use the default minimal template and click "Test". 3) Confirm the webhook receives a message (content or embed). 4) Switch to detailed template and click "Test". 5) Confirm the webhook receives an embed payload. 6) Repeat for Slack provider (text only). ## 6) Acceptance Criteria (EARS) - WHEN a Discord notification provider uses the default minimal template, THE SYSTEM SHALL send a payload containing `content`. - WHEN a Discord notification provider uses the default detailed template, THE SYSTEM SHALL send a payload containing `embeds`. - WHEN a Slack notification provider uses the default minimal template, THE SYSTEM SHALL send a payload containing `text`. - WHEN a custom Discord template omits both `content` and `embeds`, THE SYSTEM SHALL return a validation error and SHALL NOT send the webhook. - WHEN a preview request is made for a Discord or Slack provider, THE SYSTEM SHALL validate the rendered JSON against provider requirements. - WHEN a preview or test send renders an empty payload after fallback, THE SYSTEM SHALL return a validation error and SHALL NOT send the webhook. - WHEN a Discord provider test succeeds, THE SYSTEM SHALL return 200 OK and THE SYSTEM SHALL record the success without error. ## 7) Files and Components to Touch (Trace Map) **Backend** - [backend/internal/services/notification_service.go](backend/internal/services/notification_service.go#L170-L272) - [backend/internal/services/notification_service_test.go](backend/internal/services/notification_service_test.go) - [backend/internal/services/notification_service_json_test.go](backend/internal/services/notification_service_json_test.go) - [backend/internal/api/handlers/notification_provider_preview_handler.go](backend/internal/api/handlers/notification_provider_preview_handler.go) (new) - [backend/internal/api/handlers/notification_provider_preview_handler_test.go](backend/internal/api/handlers/notification_provider_preview_handler_test.go) (new) - [backend/internal/api/handlers/notification_provider_handler.go](backend/internal/api/handlers/notification_provider_handler.go#L100-L180) - [backend/internal/api/handlers/notification_coverage_test.go](backend/internal/api/handlers/notification_coverage_test.go#L340-L610) **Frontend** - [frontend/src/pages/Notifications.tsx](frontend/src/pages/Notifications.tsx#L24-L235) - [tests/settings/notifications.spec.ts](tests/settings/notifications.spec.ts) ## 8) Confidence Score Confidence: 86% Rationale: The failure is directly tied to a known validation rule and to default templates that omit required fields. The changes are isolated to the notification template selection path and the Notifications UI.--- post_title: Permissions Integrity Plan author1: "Charon Team" post_slug: permissions-integrity-plan-non-root microsoft_alias: "charon" featured_image: >- https://wikid82.github.io/charon/assets/images/featured/charon.png categories: - security tags: - permissions - non-root - diagnostics - settings summary: "Plan to harden non-root permissions, add diagnostics, and align saves." post_date: "2026-02-11" --- ## Permissions Integrity Plan — Non-Root Containers, Notifications, Saves,
and Dropdown State Last updated: 2026-02-11 ## 1) Introduction Running Charon as a non-root container should feel like a locked garden gate: secure, predictable, and fully functional. Today, permission mismatches on mounted volumes can silently corrode core features—notifications, settings saves, and dropdown selections—because persistence depends on writing to `/app/data`, `/config`, and related paths. This plan focuses on a full, precise remediation: map every write path, instrument permissions, tighten error handling, and make the UI reveal permission failures in plain terms. **Objectives** - Ensure all persistent paths are writable for non-root execution without weakening security. - Make permission errors visible and actionable from API to UI. - Reduce multi-request settings saves to avoid partial writes and improve reliability. - Align notification settings fields between frontend and backend. - Provide a clear path for operators to set correct volume ownership. ## Handoff Contract Use this contract to brief implementation and QA. All paths and schemas must match the plan. ```json { "endpoints": { "GET /api/v1/system/permissions": { "response_schema": { "paths": [ { "path": "/app/data", "required": "rwx", "writable": false, "owner_uid": 1000, "owner_gid": 1000, "mode": "0755", "error": "permission denied", "error_code": "permissions_write_denied" } ] } }, "POST /api/v1/system/permissions/repair": { "request_schema": { "paths": ["/app/data", "/config"], "group_mode": false }, "response_schema": { "paths": [ { "path": "/app/data", "status": "repaired", "owner_uid": 1000, "owner_gid": 1000, "mode_before": "0755", "mode_after": "0700", "message": "ownership and mode updated" }, { "path": "/config", "status": "error", "error_code": "permissions_readonly", "message": "read-only filesystem" } ] } } } } ``` ## 2) Research Findings ### 2.1 Runtime Permissions and Startup Flow - Container entrypoint: [.docker/docker-entrypoint.sh](.docker/docker-entrypoint.sh) - `is_root()` and `run_as_charon()` drop privileges using `gosu`. - Warns if `/app/data` or `/config` is not writable; does not repair unless root. - Creates `/app/data/caddy`, `/app/data/crowdsec`, `/app/data/geoip` and `chown` only when root. - If the container is started with `--user` (non-root), it cannot `chown` or repair volume permissions. - Docker runtime image: [Dockerfile](Dockerfile) - Creates `charon` user (`uid=1000`, `gid=1000`) and sets ownership of `/app`, `/config`, `/var/log/crowdsec`, `/var/log/caddy`. - Entry point starts as root, then drops privileges; this is good for dynamic socket group handling but still depends on host volume ownership. - Default environment points DB and data to `/app/data`. - Compose volumes: [.docker/compose/docker-compose.yml](.docker/compose/docker-compose.yml) - `cpm_data:/app/data` and `caddy_config:/config` are mounted without a user override. - `plugins_data:/app/plugins:ro` is read-only, so plugin operations should never require writes there. ### 2.2 Persistent Writes and Vulnerable Paths - Backend config creates directories with restrictive permissions (0700): - [backend/internal/config/config.go](backend/internal/config/config.go) - `Load()` calls `os.MkdirAll(filepath.Dir(cfg.DatabasePath), 0o700)` - `os.MkdirAll(cfg.CaddyConfigDir, 0o700)` - `os.MkdirAll(cfg.ImportDir, 0o700)` - If `/app/data` is owned by root and container runs as non-root, startup can fail or later writes can silently fail. - Database writes: [backend/internal/database/database.go](backend/internal/database/database.go) - SQLite file at `CHARON_DB_PATH` (default `/app/data/charon.db`). - Read-only DB or directory permission failures block settings, notifications, and any save flows. - Backups (writes to `/app/data/backups`): - [backend/internal/services/backup_service.go][backup-service-go] - Uses `os.MkdirAll(backupDir, 0o700)` and `os.Create()` for ZIPs. - Import workflows write under `/app/data/imports`: - [backend/internal/api/handlers/import_handler.go][import-handler-go] - Writes to `imports/uploads/` using `os.MkdirAll(..., 0o755)` and `os.WriteFile(..., 0o644)`. ### 2.3 Notifications and Settings Persistence - Notification providers and templates are stored in DB: - [backend/internal/services/notification_service.go][notification-service-go] - [backend/internal/api/handlers/
notification_handler.go][notification-handler-go] - UI: [frontend/src/pages/Notifications.tsx][notifications-page-tsx] - Security notification settings are stored in DB: - Backend: [backend/internal/services/
security_notification_service.go][security-notification-service-go] - Handler: [backend/internal/api/handlers/
security_notifications.go][security-notifications-handler-go] - UI modal: [frontend/src/components/
SecurityNotificationSettingsModal.tsx][security-notification-modal-tsx] **Field mismatch discovered:** - Frontend expects `notify_rate_limit_hits` and `email_recipients` and also offers `min_log_level = fatal`. - Backend model [backend/internal/models/notification_config.go][notification-config-go] only includes: - `Enabled`, `MinLogLevel`, `NotifyWAFBlocks`, `NotifyACLDenies`, `WebhookURL`. - Handler validation allows `debug|info|warn|error` only. This mismatch can cause failed saves or silent drops, and it is adjacent to permissions issues because a permissions error amplifies the confusion. ### 2.4 Settings and Dropdown Persistence - System settings save path: - UI: [frontend/src/pages/SystemSettings.tsx][system-settings-tsx] - API client: [frontend/src/api/settings.ts](frontend/src/api/settings.ts) - Handler: [backend/internal/api/handlers/settings_handler.go][settings-handler-go] - Current UI saves multiple settings via multiple requests; a write failure mid-way can lead to partial persistence. - SMTP settings save path: - UI: [frontend/src/pages/SMTPSettings.tsx][smtp-settings-tsx] - Backend: [backend/internal/services/mail_service.go][mail-service-go] - Dropdowns use Radix Select: - Component: [frontend/src/components/ui/Select.tsx][select-component-tsx] - If API writes fail, the UI state can appear to “stick” until a reload resets it. ### 2.5 Initial Hygiene Review - [.gitignore](.gitignore), [.dockerignore](.dockerignore), [codecov.yml](codecov.yml) currently do not require changes for permissions work. - Dockerfile may require optional enhancements to accommodate PUID/PGID or a dedicated permissions check, but no mandatory change is confirmed yet. ## 3) Technical Specifications ### 3.1 Data Paths, Ownership, and Required Access | Path | Purpose | Required Access | Notes | | --- | --- | --- | --- | | `/app/data` | Primary data root | rwx | Note A | | `/app/data/charon.db` | SQLite DB | rw | DB and parent dir must be writable | | `/app/data/backups` | Backup ZIPs | rwx | Created by backup service | | `/app/data/imports` | Import uploads | rwx | Used by import handler | | `/app/data/caddy` | Caddy state | rwx | Caddy writes certs and data | | `/app/data/crowdsec` | CrowdSec persistent config | rwx | Note B | | `/app/data/geoip` | GeoIP database | rwx | MaxMind GeoIP DB storage | | `/config` | Caddy config | rwx | Managed by Caddy | | `/var/log/caddy` | Caddy logs | rwx | Writable when file logging enabled | | `/var/log/crowdsec` | CrowdSec logs | rwx | Local bouncer and agent logs | | `/app/plugins` | Plugins | r-x | Should not be writable in production | Notes: - Note A: Must be owned by runtime user or group-writable. - Note B: Entry point chown when root. ### 3.2 Permission Readiness Diagnostics **Goal:** Provide definitive, machine-readable permission diagnostics for UI and logs. **Proposed API** - `GET /api/v1/system/permissions` - Returns a list of paths, expected access, current uid/gid ownership, mode bits, writeability, and a stable `error_code` when a check fails. - Example response schema: ```json { "paths": [ { "path": "/app/data", "required": "rwx", "writable": false, "owner_uid": 1000, "owner_gid": 1000, "mode": "0755", "error": "permission denied", "error_code": "permissions_write_denied" } ] } ``` **Writable determination (explicit, non-destructive):** - For each path, perform `os.Stat` to capture owner/mode and to confirm the path exists. - If the `required` access does not include `w` (for example `r-x`), skip any writeability probe, do not set `error_code`, and optionally set `status=expected_readonly` to clarify that non-writable is expected. - If the path is a directory, attempt a non-destructive writeability probe by creating a temp file in the directory (`os.CreateTemp`) and then immediately removing it. - If the path is a file, attempt to open it with write permissions (`os.OpenFile` with `os.O_WRONLY` or `os.O_RDWR`) without truncation and close immediately. - Do not modify file contents or truncate; no destructive writes are allowed. - If any step fails, set `writable=false` and return a stable `error_code`. **Error code coverage (explicit):** - The `error_code` field SHALL be returned by diagnostics responses for both `GET /api/v1/system/permissions` and `POST /api/v1/system/permissions/repair` whenever a per-path check fails. - For a `GET` diagnostics entry that is healthy, omit `error_code` and `error`. - Diagnostics error mapping MUST distinguish read-only vs permission denied: - `EROFS` -> `permissions_readonly` - `EACCES` -> `permissions_write_denied` - `POST /api/v1/system/permissions/repair` (optional) - Only enabled when process is root. - Attempts to `chown` and `chmod` only for known safe paths. - Returns a per-path remediation report. - **Request schema (explicit):** ```json { "paths": ["/app/data", "/config"], "group_mode": false } ``` - **Response schema (explicit):** ```json { "paths": [ { "path": "/app/data", "status": "repaired", "owner_uid": 1000, "owner_gid": 1000, "mode_before": "0755", "mode_after": "0700", "message": "ownership and mode updated" }, { "path": "/config", "status": "error", "error_code": "permissions_readonly", "message": "read-only filesystem" } ] } ``` - **Target ownership and mode rules (explicit):** - Use runtime UID/GID (effective process UID/GID at time of request). - Directory mode: `0700` by default; `0770` when `group_mode=true`. - File mode: `0600` by default; `0660` when `group_mode=true`. - `group_mode` applies to all provided paths; per-path overrides are not supported in this plan. - **Per-path behavior and responses (explicit):** - For each path in `paths`, validate and act independently. - If a path is missing, return `status=error` with `error_code=permissions_missing_path` and do not create it. - If a path resolves to a directory, apply directory mode rules and ownership updates. - If a path resolves to a file, apply file mode rules and ownership updates. - If a path resolves to neither a file nor directory, return `status=error` with `error_code=permissions_unsupported_type`. - If a path is already correct, return `status=skipped` with a `message` indicating no change. - If any mutation fails (read-only FS, permission denied), return `status=error` and include a stable `error_code`. - **Allowlist + Symlink Safety:** - **Allowlist roots (hard-coded, immutable):** - `/app/data` - `/config` - `/var/log/caddy` - `/var/log/crowdsec` - Only allow subpaths that remain within these roots after `filepath.Clean` and `filepath.EvalSymlinks` checks. - Resolve each requested path with `filepath.EvalSymlinks` and reject any that resolve outside the allowlist roots. - Use `os.Lstat` to detect and reject symlinks before any mutation. - Use no-follow semantics for any filesystem operations (reject if any path component is a symlink). - If a path is missing, return a per-path error instead of creating it. - **Path Normalization (explicit):** - Only accept absolute paths and reject relative inputs. - Normalize with `filepath.Clean` before validation. - Reject any path that resolves to `.` or contains `..` after normalization. - Reject any request where normalization would change the intended path outside the allowlist roots. **Scope:** - Diagnostics SHALL include all persistent write paths listed in section 3.1, including `/app/data/geoip`, `/var/log/caddy`, and `/var/log/crowdsec`. - Any additional persistent write paths referenced elsewhere in this plan SHALL be included in diagnostics as they are added. - Diagnostics SHALL include `/app/plugins` as a read-only check with `required: r-x`. A non-writable result for `/app/plugins` is expected and MUST NOT be treated as a failure condition; skip the write probe and do not include an `error_code`. **Backend placement:** - New handler in `backend/internal/api/handlers/system_permissions_handler.go`. - Utility in `backend/internal/util/permissions.go` for POSIX stat + access checks. ### 3.3 Access Control and Path Exposure **Goal:** Ensure diagnostics are admin-only and paths are not exposed to non- admins. - `GET /api/v1/system/permissions` and `POST /api/v1/system/permissions/repair` must be admin-only. - Non-admin requests SHALL return `403` with a stable error code `permissions_admin_only`. - Full filesystem paths SHALL only be included for admins; non-admin errors must omit or redact path details. **Redaction and authorization strategy (explicit):** - Admin enforcement happens in the handler layer using the existing admin guard middleware; handlers SHALL read the admin flag from request context and fail closed if the flag is missing. - Redaction happens in the error response builder at the handler boundary before JSON serialization. Services return a structured error with optional `path` and `detail` fields; the handler removes `path` and sensitive filesystem hints for non-admins and replaces help text with a generic remediation message. - The redaction decision SHALL not rely on client-provided hints; it must only use server-side auth context. **Non-admin response schema (redacted, brief):** - Diagnostics (non-admin, 403): ```json { "error": "admin privileges required", "error_code": "permissions_admin_only" } ``` - Repair (non-admin, 403): ```json { "error": "admin privileges required", "error_code": "permissions_admin_only" } ``` **Save endpoint access (admin-only):** - Settings and configuration save endpoints SHALL remain admin-only where applicable (e.g., system settings, SMTP settings, notification providers/templates, security notification settings, imports, and backups). - If any save endpoint is currently not admin-gated, the implementation MUST add admin-only checks or explicitly document the exception in this plan before implementation. #### 3.3.1 Admin-Gated Save Endpoints Checklist For each endpoint below, confirm the current state and enforce admin-only access unless explicitly documented as public. - System settings save - Current: Verify admin guard is enforced in handler and service. - Target: Admin-only with `403` and stable error code on failure. - Verify: API call as non-admin returns `403` without write. - SMTP settings save - Current: Verify admin guard in handler and service. - Target: Admin-only with `403` and stable error code on failure. - Verify: API call as non-admin returns `403` without write. - Notification providers save/update/delete - Current: Verify admin guard in handler and service. - Target: Admin-only with `403` and stable error code on failure. - Verify: API call as non-admin returns `403` without write. - Notification templates save/update/delete - Current: Verify admin guard in handler and service. - Target: Admin-only with `403` and stable error code on failure. - Verify: API call as non-admin returns `403` without write. - Security notification settings save - Current: Verify admin guard in handler and service. - Target: Admin-only with `403` and stable error code on failure. - Verify: API call as non-admin returns `403` without write. - Import create/upload - Current: Verify admin guard in handler and service. - Target: Admin-only with `403` and stable error code on failure. - Verify: API call as non-admin returns `403` without write. - Backup create/restore - Current: Verify admin guard in handler and service. - Target: Admin-only with `403` and stable error code on failure. - Verify: API call as non-admin returns `403` without write. ### 3.4 Permission-Aware Error Mapping **Goal:** When a save fails, the user sees “why.” - Identify key persistence actions and wrap errors with permission hints: - Settings saves: `SettingsHandler.UpdateSetting()` and `PatchConfig()`. - SMTP saves: `MailService.SaveSMTPConfig()`. - Notification providers/templates: `NotificationService.CreateProvider()`, `UpdateProvider()`, `CreateTemplate()`, `UpdateTemplate()`. - Security notification settings: `SecurityNotificationService.UpdateSettings()`. - Backup creation: `BackupService.CreateBackup()`. - Import uploads: `ImportHandler.Upload()` and `UploadMulti()`. **Error behavior:** - If error is permission-related (`os.IsPermission`, SQLite read-only), return a 500 with a standard payload: - `error`: short message - `help`: actionable guidance using runtime UID/GID (e.g., `chown -R : /path/to/volume`) - `path`: affected path (admin-only; omit or redact for non-admins) - `code`: stable error code (required for permission-related save failures; e.g., `permissions_write_failed`) **Audit logging:** - Log all diagnostics reads and repair attempts as audit events, including requestor identity, admin flag, and outcome. - Log permission-related save failures (settings, notifications, imports, backups, SMTP) as audit events with error codes and redacted path details for non-admin contexts. **SQLite read-only detection (explicit):** - Map SQLite read-only failures by driver code when available (e.g., `SQLITE_READONLY` and extended codes such as `SQLITE_READONLY_DB`, `SQLITE_READONLY_DIRECTORY`). - Also detect string-based error messages to cover driver variations (e.g., `attempt to write a readonly database`, `readonly database`, `read-only database`). - If driver codes are unavailable, fall back to message matching + `os.IsPermission` to produce the same standard payload. ### 3.4.1 Canonical Error-Code Catalog (Diagnostics + Repair + Save Failures) **Goal:** Provide a single source of truth for error codes used by diagnostics, repair, and persistence failures. All responses MUST use values from this catalog. **Scope:** - Diagnostics: `GET /api/v1/system/permissions` - Repair: `POST /api/v1/system/permissions/repair` - Save failures: settings, SMTP, notifications, security notifications, imports, backups | Error Code | Scope | Meaning | | --- | --- | --- | | `permissions_admin_only` | Diagnostics/Repair/Save | Note 1 | | `permissions_non_root` | Repair | Note 2 | | `permissions_repair_disabled` | Repair | Note 3 | | `permissions_missing_path` | Diagnostics/Repair | Path does not exist. | | `permissions_unsupported_type` | Diagnostics/Repair | Note 4 | | `permissions_outside_allowlist` | Repair | Note 5 | | `permissions_symlink_rejected` | Repair | Path or a component is a symlink. | | `permissions_invalid_path` | Diagnostics/Repair | Note 6 | | `permissions_readonly` | Diagnostics/Repair/Save | Filesystem is read-only. | | `permissions_write_denied` | Diagnostics/Save | Note 7 | | `permissions_write_failed` | Save | Note 8 | | `permissions_db_readonly` | Save | Note 9 | | `permissions_db_locked` | Save | Note 10 | | `permissions_repair_failed` | Repair | Note 11 | | `permissions_repair_skipped` | Repair | No changes required for the path. | Notes: - Note 1: Request requires admin privileges. - Note 2: Repair endpoint invoked without root privileges. - Note 3: Repair endpoint disabled because single-container mode is false. - Note 4: Path is not a file or directory. - Note 5: Path resolves outside allowlist roots. - Note 6: Path is relative, normalizes to `.`/`..`, or fails validation. - Note 7: Write probe or write operation denied. - Note 8: Write operation failed for another permission-related reason. - Note 9: SQLite database or directory is read-only. - Note 10: SQLite database locked; treat as transient write failure. - Note 11: Repair attempted but failed (non-permission errors). **Mapping rules (explicit):** - Diagnostics uses `permissions_missing_path`, `permissions_write_denied`, `permissions_readonly`, `permissions_invalid_path`, `permissions_unsupported_type` as appropriate. - Repair uses `permissions_admin_only`, `permissions_non_root`, or `permissions_repair_disabled` when blocked, and otherwise maps to the per-path codes above. - Save failures use `permissions_db_readonly` when SQLite read-only is detected; otherwise use `permissions_write_denied` or `permissions_write_failed` depending on `os.IsPermission` and error context. - Save failures SHALL always include an error code from this catalog. ### 3.5 Notification Settings Model Alignment **Goal:** Align UI fields with backend persistence. - Update [backend/internal/models/notification_config.go][notification-config-go] to include: - `NotifyRateLimitHits bool` - `EmailRecipients string` - Update handler validation in [backend/internal/api/handlers/
security_notifications.go][security-notifications-handler-go]: - Keep backend validation to `debug|info|warn|error`. - Update UI log level options to remove `fatal` and match backend validation. - Update `SecurityNotificationService.GetSettings()` default struct to include new fields. **EmailRecipients data format (explicit):** - Input accepts a comma-separated list of email addresses. - Split on `,`, trim whitespace for each entry, and drop empty values. - Validate each email using existing backend validation rules. - Store a normalized, comma-separated string joined with `, `. - If validation fails, return a single error listing invalid entries. **Validation and UX notes:** - UI helper text: "Use comma-separated emails, e.g. admin@example.com, ops@example.com". - Inline error highlights the invalid address(es) and does not save. - Empty input is treated as "no recipients" and stored as an empty string. - The UI must preserve the normalized format returned by the API. ### 3.6 Reduce Settings Write Requests **Goal:** Fewer requests, fewer partial failures. - Reuse existing `PATCH /api/v1/config` in [backend/internal/api/handlers/settings_handler.go][settings-handler-go]. - PATCH updates MUST be transactional and all-or-nothing. If any field update fails (validation, DB write, or permission), the transaction must roll back and the API must return a single failure response. - Update [frontend/src/pages/SystemSettings.tsx](frontend/src/pages/SystemSettings.tsx) to send one patch request for all fields. - Add failure-mode UI message that references permission diagnostics if present. ### 3.7 UX Guidance for Non-Root Deployments - Add a settings banner or toast when permissions fail, pointing to: - `docker run` or `docker compose` examples - `chown -R : /path/to/volume` using values from diagnostics or configured `--user` / `CHARON_UID` / `CHARON_GID` - Optionally `--user :` or PUID/PGID env if added ### 3.8 PUID/PGID and --user Behavior - If the container is started with `--user`, the entrypoint cannot `chown` mounted volumes. - When `--user` is set, `CHARON_UID`/`CHARON_GID` (and any PUID/PGID equivalents) SHALL be treated as no-ops and only used for logging. - Documentation must instruct operators to pre-create and `chown` host volumes to the runtime UID/GID when using `--user`, based on the diagnostics-reported UID/GID or the configured runtime values. **Directory permission modes (0700 vs group-writable):** - Default directory mode remains `0700` for single-user deployments. - When PUID/PGID or supplemental group access is used, directories MAY be created as `0770` (or `0750` if group write is not required). - If group-writable directories are used, ensure the runtime user is in the owning group and document the expected umask behavior. ### 3.9 Risk Register and Mitigations - **Risk:** Repair endpoint could be abused in a multi-tenant environment. - **Mitigation:** Only enabled in single-container mode; root-only; allowlist paths. - **Risk:** Adding fields to NotificationConfig might break existing migrations. - **Mitigation:** Use GORM AutoMigrate and default values. - **Risk:** UI still masks failures due to optimistic updates. - **Mitigation:** Ensure all mutations handle error states and show help text. ### 3.9.1 Single-Container Mode Detection and Enforcement **Goal:** Ensure repair operations are only enabled in single-container mode and the system can deterministically report whether this mode is active. **Detection (explicit):** - Environment flag: `CHARON_SINGLE_CONTAINER_MODE`. - Accepted values: `true|false` (case-insensitive). Any other value defaults to `false` and logs a warning. - Default: `true` in official Dockerfile and official compose examples. - Non-container installs (binary on host) default to `false` unless explicitly set. **Enforcement (explicit):** - The repair endpoint is disabled when single-container mode is `false`. - The handler MUST return `403` with `permissions_repair_disabled` when the mode check fails, and SHALL NOT attempt any filesystem mutations. - Diagnostics remain available regardless of mode. **Repair gating and precedence (explicit):** 1) Admin-only check first. If not admin, return `403` with `permissions_admin_only`. 2) Single-container mode check second. If disabled, return `403` with `permissions_repair_disabled`. 3) Root check third. If not root, return `403` with `permissions_non_root`. 4) Only after all gating checks pass, proceed to path validation and mutation. **Placement:** - Mode detection lives in `backend/internal/config` as a boolean flag on the runtime config object. - Enforcement happens in the permissions repair handler before any path validation or mutation. - Log the evaluated mode and source (explicit env vs default) once at startup. ### 3.10 Spec-Driven Workflow Artifacts (Pre-Implementation Gate) Before Phase 1 begins, update the following artifacts and confirm sign-off: - `requirements.md` with new or refined EARS statements for permissions diagnostics, admin-gated saves, and error mapping. - `design.md` with new endpoints, data flow, error payloads, and non-root permission remediation design. - `tasks.md` with the phase plan, test order, verification tasks, and the deterministic read-only DB simulation approach. ## 4) Implementation Plan (Phased, Minimal Requests) ### Pre-Implementation Gate (Required) 1) Update `requirements.md`, `design.md`, and `tasks.md` per section 3.10. 2) Confirm the deterministic read-only DB simulation approach and exact invocation are documented in `tasks.md`. 3) Proceed only after the spec artifacts are updated and reviewed. ### Phase 1 — Playwright & Diagnostic Ground Truth **Goal:** Define the expected UX and capture the failure state before changes. 1) Add E2E coverage for permissions and save failures: - Tests in `tests/settings/settings-permissions.spec.ts`: - Simulate DB read-only deterministically (compose override only). - Verify toast/error text on save failure. - Tests for dropdown persistence in System Settings and SMTP: - Ensure selections persist after reload when writes succeed. - Ensure UI reverts with visible error on write failure. - Security notification log level options: - Ensure `fatal` is not present in the dropdown options. 1a) Deterministic failure simulation setup (aligned with E2E workflow): - Use a docker-compose override to bind-mount a read-only DB file for E2E. This is the single supported approach for deterministic DB read-only simulation. - Override file (example name): `.docker/compose/docker-compose.e2e-readonly-db.override.yml`. - Read-only DB setup sequence (explicit): 1. Run the E2E rebuild skill first to ensure the base container and baseline volumes are fresh and healthy. 2. Start a one-off container (or job) with a writable volume. 3. Run migrations and seed data to create the SQLite DB file in that writable location. 4. Stop the one-off container and bind-mount the DB file into the E2E container as read-only using the override. - Exact invocation (Docker E2E mode): ```bash # Step 1: rebuild E2E container .github/skills/scripts/skill-runner.sh docker-rebuild-e2e # Step 2: start with override docker compose -f .docker/compose/docker-compose.yml \ -f .docker/compose/docker-compose.e2e-readonly-db.override.yml up -d ``` - The override SHALL mount the DB file read-only and MUST NOT require any application code changes or test-only flags. - Teardown/cleanup after the test run (explicit): 1. Stop and remove the override services/containers started for the read-only run. 2. Remove any override-specific volumes used for the read-only DB file to avoid cross-test contamination. 3. Re-run the E2E rebuild skill before the next E2E session to restore the standard writable DB state. - Add a planned VS Code task or skill-runner entry to make this workflow one-command and discoverable (example task label: "Test: E2E Readonly DB", command invoking the docker compose override sequence above). 2) Add a health check step in tests for permissions endpoint once available. **Outputs:** New E2E baseline expectations for save behavior. ### Phase 2 — Backend Permissions Diagnostics & Errors **Goal:** Make permission issues undeniable and actionable. 1) Add system permissions handler and util: - `backend/internal/api/handlers/system_permissions_handler.go` - `backend/internal/util/permissions.go` 2) Add standardized permission error mapping: - Wrap DB and filesystem errors in settings, notifications, imports, backups. 3) Extend security notifications model and defaults: - Update `NotificationConfig` fields. - Update handler validation for min log level or adjust UI. **Outputs:** A diagnostics API and consistent error payloads across persistence paths. ### Phase 3 — Frontend Save Flows and UI Messaging **Goal:** Reduce request count and surface errors clearly. 1) System Settings: - Switch to `PATCH /api/v1/config` for multi-field save. - On error, show permission hint if provided. 2) Security Notification Settings modal: - Align log level options with backend. - Ensure new fields are saved and displayed. 3) Notifications providers: - Surface permission errors on save/update/delete. **Outputs:** Fewer save calls, better error clarity, stable dropdown persistence. ### Phase 4 — Integration and Testing 1) Run Playwright E2E tests first, before any unit tests. 2) If the E2E environment changed, rebuild using the E2E Docker skill. 3) Ensure E2E tests cover permission failure UX and dropdown persistence. 4) Run unit tests only after E2E passes. 5) Enforce 100% patch coverage for all modified lines. 6) Record any coverage gaps in `tasks.md` before adding tests. ### Phase 5 — Container & Volume Hardening **Goal:** Provide a clear, secure non-root path. 1) Entrypoint improvements: - When running as root, ensure `/app/data` ownership is corrected (not only subdirs). - Log UID/GID at startup. 2) Optional PUID/PGID support: - If `CHARON_UID`/`CHARON_GID` are set and the container is not started with `--user`, re-map `charon` user or add supplemental group. - If `--user` is set, log that PUID/PGID overrides are ignored and volume ownership must be handled on the host. 3) Dockerfile/Compose review: - If PUID/PGID added, update Dockerfile and compose example. **Outputs:** Hardening changes that remove the “silent failure” path. ### Phase 6 — Integration, Documentation, and Cleanup 1) Add troubleshooting docs for non-root volumes. 2) Update any user guides referencing permissions. 3) Update API docs for new endpoints: - Add `GET /api/v1/system/permissions` and `POST /api/v1/system/permissions/repair` to [docs/api.md](docs/api.md) with schemas, auth, and error codes. 4) Update documentation to reference `CHARON_SINGLE_CONTAINER_MODE`: - Add the env var description and default behavior to the primary configuration reference (include accepted values and fallback behavior). - Add or update a Docker Compose example showing `CHARON_SINGLE_CONTAINER_MODE=true` in the environment list. 5) Ensure `requirements.md`, `design.md`, and `tasks.md` are updated. 6) Finalize tests and ensure coverage targets are met. 7) Update [docs/features.md](docs/features.md) for any user-facing permissions diagnostics or repair UX changes. ## 5) Acceptance Criteria (EARS) - WHEN the container runs as non-root and a mounted volume is not writable, THE SYSTEM SHALL expose a permissions diagnostic endpoint that reports the failing path and required access. - WHEN the permissions repair endpoint is called by a non-root process, THE SYSTEM SHALL return `403` and SHALL NOT perform any filesystem mutation. - WHEN the permissions repair endpoint is called by a non-admin user, THE SYSTEM SHALL return `403` with `permissions_admin_only` and SHALL NOT perform any filesystem mutation. - WHEN the permissions repair endpoint is called while single-container mode is disabled, THE SYSTEM SHALL return `403` with `permissions_repair_disabled` and SHALL NOT perform any filesystem mutation. - WHEN the permissions repair endpoint receives a path that is outside the allowlist, THE SYSTEM SHALL reject the request with a clear error and SHALL NOT touch the filesystem. - WHEN the permissions repair endpoint receives a symlink or a path containing a symlinked component, THE SYSTEM SHALL reject the request with a clear error and SHALL NOT follow the link. - WHEN the permissions repair endpoint receives a missing path, THE SYSTEM SHALL return a per-path error and SHALL NOT create the path. - WHEN the permissions repair endpoint receives a relative path or a path that normalizes to `.` or `..`, THE SYSTEM SHALL reject the request and SHALL NOT perform any filesystem mutation. - WHEN a user saves system, SMTP, or notification settings and the DB is read- only, THE SYSTEM SHALL return a clear error with a remediation hint. - WHEN a user updates dropdown-based settings and persistence fails, THE SYSTEM SHALL display an error and SHALL NOT silently pretend the save succeeded. - WHEN the security notification log level options are displayed, THE SYSTEM SHALL only present `debug`, `info`, `warn`, and `error`. - WHEN security notification settings are saved, THE SYSTEM SHALL persist all fields that the UI presents. - WHEN settings updates include multiple fields, THE SYSTEM SHALL apply them in a single request and a single transaction to avoid partial persistence. - WHEN a non-admin user attempts to call a save endpoint, THE SYSTEM SHALL return `403` with `permissions_admin_only` and SHALL NOT perform any write. - WHEN permissions diagnostics or repair endpoints are called, THE SYSTEM SHALL emit an audit log entry with outcome details. - WHEN a permission-related save failure occurs, THE SYSTEM SHALL emit an audit log entry with a stable error code and redacted path details for non-admin contexts. - WHEN a non-admin user receives a permission-related error, THE SYSTEM SHALL redact filesystem path details from the response payload. ## 6) Files and Components to Touch (Trace Map) **Backend** - [.docker/docker-entrypoint.sh][docker-entrypoint-sh] — permission checks and potential ownership fixes. - [backend/internal/config/config.go][config-go] — data directory creation behavior. - [backend/internal/api/handlers/settings_handler.go][settings-handler-go] — permission-aware errors, PATCH usage. - [backend/internal/api/handlers/
security_notifications.go][security-notifications-handler-go] — validation alignment. - [backend/internal/services/
security_notification_service.go][security-notification-service-go] — defaults, persistence. - [backend/internal/models/notification_config.go][notification-config-go] — new fields. - [backend/internal/services/mail_service.go][mail-service-go] — permission- aware errors. - [backend/internal/services/notification_service.go][notification-service-go] — permission-aware errors. - [backend/internal/services/backup_service.go][backup-service-go] — permission-aware errors. - [backend/internal/util/permissions.go][permissions-util-go] — permission diagnostics utility. - [backend/internal/api/handlers/import_handler.go][import-handler-go] — permission-aware errors for uploads. **Frontend** - [frontend/src/pages/SystemSettings.tsx][system-settings-tsx] — batch save via PATCH and better error UI. - [frontend/src/pages/SMTPSettings.tsx][smtp-settings-tsx] — permission error messaging. - [frontend/src/pages/Notifications.tsx][notifications-page-tsx] — save error handling. - [frontend/src/components/
SecurityNotificationSettingsModal.tsx][security-notification-modal-tsx] — align fields. - [frontend/src/components/ui/Select.tsx][select-component-tsx] — no functional change expected; verify for state persistence. **Infra** - [Dockerfile](Dockerfile) - [.docker/compose/docker-compose.yml](.docker/compose/docker-compose.yml) ## 7) Repo Hygiene Review (Requested) - **.gitignore:** No change required unless we add new diagnostics artifacts (e.g., `permissions-report.json`). If added, ignore them under root or `test- results/`. - **.dockerignore:** No change required. If we add new documentation files or test artifacts, keep them excluded from the image. - **codecov.yml:** No change required unless new diagnostics packages warrant exclusions. - **Dockerfile:** Potential update if PUID/PGID support is added; otherwise, no change required. ## 8) Unit Test Plan Backend unit tests (Go): - Permissions diagnostics utility: validate stat parsing, writable checks, and error mapping for missing paths and permission denied. - Permissions endpoints: admin-only access (403 + `permissions_admin_only`) and successful admin responses. - Permissions repair endpoint: - Rejects non-root execution with `403` and no filesystem changes. - Rejects non-admin requests with `permissions_admin_only`. - Rejects paths outside the allowlist safe roots. - Rejects relative paths, `.` and `..` after normalization, and any request where `filepath.Clean` produces an out-of-allowlist path. - Rejects symlinks and symlinked path components via `Lstat` and `EvalSymlinks` checks. - Returns per-path errors for missing paths without creating them. - Permission-aware error mapping: ensure DB read-only and `os.IsPermission` errors map to the standard payload fields and redact path details for non- admins. - Audit logging: verify diagnostics/repair calls and permission-related save failures emit audit entries with redacted path details for non-admin contexts. - Settings PATCH behavior: multi-field patch applies atomically in the handler/service and returns a single failure when any persistence step fails. Frontend unit tests (Vitest): - Diagnostics fetch handling: verify non-admin error messaging without path details. - Settings save errors: ensure error toast displays remediation text and UI state does not silently persist on failure. ## 9) Confidence Score Confidence: 80% Rationale: The permissions write paths are well mapped, and the root cause (non- root + volume ownership mismatch) is a common pattern. The only uncertainty is the exact user environment for the failure, which will be clarified once diagnostics are in place. [backup-service-go]: backend/internal/services/backup_service.go [import-handler-go]: backend/internal/api/handlers/import_handler.go [notification-service-go]: backend/internal/services/notification_service.go [notification-handler-go]: backend/internal/api/handlers/notification_handler.go [notifications-page-tsx]: frontend/src/pages/Notifications.tsx [security-notification-service-go]: backend/internal/services/security_notification_service.go [security-notifications-handler-go]: backend/internal/api/handlers/security_notifications.go [security-notification-modal-tsx]: frontend/src/components/SecurityNotificationSettingsModal.tsx [notification-config-go]: backend/internal/models/notification_config.go [system-settings-tsx]: frontend/src/pages/SystemSettings.tsx [settings-handler-go]: backend/internal/api/handlers/settings_handler.go [smtp-settings-tsx]: frontend/src/pages/SMTPSettings.tsx [mail-service-go]: backend/internal/services/mail_service.go [select-component-tsx]: frontend/src/components/ui/Select.tsx [docker-entrypoint-sh]: .docker/docker-entrypoint.sh [config-go]: backend/internal/config/config.go [permissions-util-go]: backend/internal/util/permissions.go