- Implemented SystemPermissionsHandler to check and repair file permissions. - Added endpoints for retrieving and repairing permissions. - Introduced utility functions for permission checks and error mapping. - Created tests for the new handler and utility functions. - Updated routes to include the new permissions endpoints. - Enhanced configuration to support new logging and plugin directories.
1019 lines
42 KiB
Markdown
1019 lines
42 KiB
Markdown
---
|
|
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,<br>
|
|
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/<br>
|
|
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/<br>
|
|
security_notification_service.go][security-notification-service-go]
|
|
- Handler:
|
|
[backend/internal/api/handlers/<br>
|
|
security_notifications.go][security-notifications-handler-go]
|
|
- UI modal:
|
|
[frontend/src/components/<br>
|
|
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 <runtime_uid>:<runtime_gid> /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/<br>
|
|
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 <runtime_uid>:<runtime_gid> /path/to/volume` using values from
|
|
diagnostics or configured `--user` / `CHARON_UID` / `CHARON_GID`
|
|
- Optionally `--user <runtime_uid>:<runtime_gid>` 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/<br>
|
|
security_notifications.go][security-notifications-handler-go]
|
|
— validation alignment.
|
|
-
|
|
[backend/internal/services/<br>
|
|
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/<br>
|
|
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
|