Skip to content

Exclude secrets from configuration revision history#225

Merged
tarekio merged 10 commits intomainfrom
Revision-History-Secrets
Nov 22, 2025
Merged

Exclude secrets from configuration revision history#225
tarekio merged 10 commits intomainfrom
Revision-History-Secrets

Conversation

@level09
Copy link
Collaborator

@level09 level09 commented Nov 20, 2025

Problem

Secrets (passwords, API keys, cookies) were being stored in database revision history, creating a security vulnerability.

Solution

  • Database: Secrets excluded from AppConfig revision history
  • Filesystem: Full config including secrets still saved to config.json
  • Restore: Client-side only - old revisions populate the form, secrets keep current masked values
  • Saving: When user saves, backend restores actual secret values from masked ********** before validation

Key Changes

  1. AppConfig.to_dict() strips secrets from revision snapshots
  2. ConfigManager.serialize() masks secrets when displaying current config
  3. Revision restore applies directly to frontend form (no backend call)
  4. User reviews and saves normally with complete config

Secret Fields Protected

RECAPTCHA_PRIVATE_KEY, GOOGLE_CLIENT_SECRET, AWS_SECRET_ACCESS_KEY, MAIL_PASSWORD, YTDLP_COOKIES, GOOGLE_MAPS_API_KEY

Summary by CodeRabbit

  • New Features

    • User-configuration relationship tracking added.
    • Sensitive configuration fields automatically excluded from revision history.
    • API keys and credentials masked in configuration exports.
  • UI/UX Improvements

    • Simplified revision revert workflow—revisions now apply directly without confirmation.
  • Messages

    • Updated configuration save confirmation to note that secrets are excluded from revision history.

✏️ Tip: You can customize this high-level summary in your review settings.

@level09 level09 self-assigned this Nov 20, 2025
@level09 level09 marked this pull request as draft November 20, 2025 10:25
@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The changes implement a secrets masking and restoration mechanism for application configuration. Sensitive fields are now excluded from database revisions via masking, restored during validation, and handled separately in serialization and persistence workflows across models, views, and utilities.

Changes

Cohort / File(s) Summary
Configuration Model Enhancements
enferno/admin/models/AppConfig.py
Added SECRET_FIELDS class constant listing sensitive keys to exclude from revisions. Added user_id column and user relationship to AppConfig. Updated to_dict() to sanitize config by removing secret keys before returning.
Frontend UI Simplification
enferno/admin/templates/admin/system-administration.html
Removed revert changes confirmation dialog and revertChangesDialog state variable. Simplified revertVersion() to apply revisions directly to form without diff preview. Replaced diff dialog flow with direct snack notification. Removed showRevertConfigDiff() method and defaultConfig restoration logic from saveConfiguration().
Validation & Masking Logic
enferno/admin/validation/models.py
Added imports for AppConfig and ConfigManager. Introduced restore_masked_secrets model validator to replace masked secret placeholders with actual values from current configuration during validation. Removed duplicate ActivityQueryValidationModel declaration. Updated validation condition logic for Opts checks.
API Response Message
enferno/admin/views.py
Updated configuration save success message to indicate secrets are excluded from revision history.
Config Serialization & Persistence
enferno/utils/config_utils.py
Modified serialize() to mask Google Maps API key and YTDLP cookies in exported configuration. Rewrote write_config() to restore masked secrets from runtime config, create sanitized config excluding secret fields for database persistence, and write full configuration (including secrets) to filesystem.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend as Frontend UI
    participant Backend as Views/API
    participant DB as Database
    participant FSys as Filesystem
    participant Config as Config Manager

    User->>Frontend: Save Configuration (with secrets)
    Frontend->>Backend: POST configuration
    Backend->>Backend: Validate & Restore Masked Secrets
    Note over Backend: replace MASK_STRING with actual values
    Backend->>DB: Save sanitized config (secrets removed)
    Backend->>FSys: Write full config (with secrets)
    Backend->>Frontend: 200 - Configuration saved.<br/>Secrets excluded from revision.
    Frontend->>User: Snack notification

    User->>Frontend: Revert to Previous Revision
    Frontend->>Backend: GET revision config
    Backend->>Frontend: Return revision (with masked secrets)
    Frontend->>Frontend: Apply masked config to form
    Frontend->>User: Direct snack notification
Loading
sequenceDiagram
    participant Export as Serialize
    participant Runtime as Runtime Config
    
    Note over Export: Configuration Export Flow
    Export->>Runtime: Check for API keys/cookies
    alt Value exists
        Export->>Export: Replace with MASK_STRING
    else No value
        Export->>Export: Keep as-is
    end
    Export->>Export: Return masked export
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Focus areas requiring careful attention:
    • config_utils.py: The secrets restoration and sanitization logic in write_config() involves multiple conditional branches and ensures secrets persist to filesystem while being masked in database; verify the flow correctly handles all edge cases and secret field types
    • validation/models.py: The restore_masked_secrets validator must correctly iterate SECRET_FIELDS and map masked placeholders back to current config values without data loss
    • Cross-file coordination: Verify that AppConfig.SECRET_FIELDS is consistently referenced across AppConfig.to_dict(), validation logic, and config utilities
    • system-administration.html: Confirm the removal of revert diff preview doesn't break user workflows and that masked secrets are properly handled when applying revisions

Poem

🐰 Secrets tucked away in fields of mask,
While configs dance through database's task,
Restored with care, when needed most,
The filesystem gets the truth to boast! 🔐
Revisions clean, no secrets shown—
A hop, a bound, the mask has grown!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The description comprehensively covers the problem, solution approach, key changes, and protected fields, but does not follow the required template structure. Restructure the description to follow the template: add Jira issue links, organize into the required sections, and include a checklist for tests and documentation.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main objective of the PR: preventing secrets from being stored in configuration revision history.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Apply old revisions directly to the frontend form instead of backend processing.
Secret fields keep their current masked values, eliminating validation errors.

Previous flow: User clicks restore → Backend validates partial config → Error
New flow: Config populates form → User reviews → Saves with complete config
@level09 level09 marked this pull request as ready for review November 21, 2025 08:02
@tarekio
Copy link
Contributor

tarekio commented Nov 21, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 21, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
enferno/admin/models/AppConfig.py (1)

16-24: Annotate SECRET_FIELDS as ClassVar and consider making it immutable

Ruff is right about the mutable class attribute here. Also, since this set is a security‑critical allowlist, making it immutable and clearly class‑scoped helps avoid accidental mutation and improves type checking.

You can address both by annotating it as a ClassVar and switching to a frozenset:

-from typing import Any
+from typing import Any, ClassVar, FrozenSet
...
-    # Secret fields that should not be stored in revision history
-    SECRET_FIELDS = {
-        "RECAPTCHA_PRIVATE_KEY",
-        "GOOGLE_CLIENT_SECRET",
-        "AWS_SECRET_ACCESS_KEY",
-        "MAIL_PASSWORD",
-        "YTDLP_COOKIES",
-        "GOOGLE_MAPS_API_KEY",
-    }
+    # Secret fields that should not be stored in revision history
+    SECRET_FIELDS: ClassVar[FrozenSet[str]] = frozenset(
+        {
+            "RECAPTCHA_PRIVATE_KEY",
+            "GOOGLE_CLIENT_SECRET",
+            "AWS_SECRET_ACCESS_KEY",
+            "MAIL_PASSWORD",
+            "YTDLP_COOKIES",
+            "GOOGLE_MAPS_API_KEY",
+        }
+    )

Also, keep an eye on consistency: if more secret keys are introduced later, they’ll need to be added here and in ConfigManager.serialize() masking logic to stay in sync.

enferno/utils/config_utils.py (1)

306-307: Good expansion of masking; consider deriving this list from SECRET_FIELDS

Masking GOOGLE_MAPS_API_KEY and YTDLP_COOKIES in serialize() closes two clear leaks of sensitive data to the frontend/API and aligns them with how other secrets are treated.

To reduce maintenance drift between this function and AppConfig.SECRET_FIELDS, consider post‑processing conf with a single loop using the shared secret list instead of hard‑coding each key here, e.g.:

from enferno.admin.models import AppConfig

for field in AppConfig.SECRET_FIELDS:
    if field in conf:
        conf[field] = ConfigManager.MASK_STRING if getattr(cfg, field, "") else ""

This way, adding a new secret key in one place (the model) automatically updates both masking and DB sanitization.

Also applies to: 338-339

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 50caafc and 4edb560.

📒 Files selected for processing (5)
  • enferno/admin/models/AppConfig.py (2 hunks)
  • enferno/admin/templates/admin/system-administration.html (1 hunks)
  • enferno/admin/validation/models.py (6 hunks)
  • enferno/admin/views.py (1 hunks)
  • enferno/utils/config_utils.py (4 hunks)
🧰 Additional context used
🪛 Ruff (0.14.5)
enferno/admin/models/AppConfig.py

17-24: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

enferno/admin/validation/models.py

616-616: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: semgrep/ci
🔇 Additional comments (10)
enferno/admin/models/AppConfig.py (2)

29-31: User linkage for AppConfig looks consistent

The user_id FK and user relationship/backref give you clear attribution of who saved each configuration snapshot and align with how other models track user ownership. No issues from this snippet.


33-43: The sanitization is correct and properly isolated; no changes needed

The implementation properly handles the semantics change to partial/sanitized configs. Verification confirms:

  • write_config() correctly persists only sanitized_conf (secrets filtered) to the database at line 365
  • Full config with secrets is written separately to config.json at line 377
  • The only backend flow querying AppConfig records returns to_dict() which excludes secrets (line 6341)
  • ConfigManager provides the mask-restore logic (MASK_STRING, restore_masked_secrets validator) for runtime restoration
  • No backend flows attempt to restore full configuration directly from AppConfig.config

The architecture correctly delegates runtime config to the filesystem plus mask-restore logic, leaving the database to store only sanitized history.

enferno/admin/views.py (1)

6391-6393: Updated success message matches new secrets handling

The new message correctly informs admins that secrets are excluded from revision history and is consistent with the new AppConfig/ConfigManager behavior. No further changes needed here.

enferno/utils/config_utils.py (1)

352-362: Verification confirms both follow-up concerns are unfounded—AppConfig.config is write-only for audit trail; runtime config sourced from config.json never rebuilds from DB snapshots

Verification findings:

  1. AppConfig snapshots concern: No code path rebuilds full config from AppConfig.config. Runtime configuration is sourced from config.json via ConfigManager.get_config() at application startup (referenced throughout enferno/settings.py). The sanitized AppConfig.config serves only as an audit trail; it is never read back for runtime use.

  2. Error handling concern: Already acknowledged as acceptable for this PR's scope. Both callers of write_config (setup/views.py:210, admin/views.py:6384) properly handle the return value and propagate errors appropriately.

The code meets the stated security goal of keeping secrets out of revision history without introducing runtime risks.

enferno/admin/templates/admin/system-administration.html (1)

1240-1251: LGTM! Client-side revision restore correctly implemented.

The client-side approach ensures that secrets (which are excluded from revision history) maintain their current masked values when reverting to an old configuration. The try/catch with structuredClone fallback to JSON.parse/stringify provides good browser compatibility.

enferno/admin/validation/models.py (5)

17-17: LGTM! Necessary imports for secrets restoration.

The imports of AppConfig and ConfigManager support the new restore_masked_secrets validator functionality.

Also applies to: 19-19


510-512: Good refactor: More Pythonic membership check.

Changing from if not v.lower() in [...] to if v.lower() not in [...] improves readability and follows Python style guidelines.

Also applies to: 615-617


1496-1497: LGTM! Removed unused exception variable.

The exception variable e was not being used, so removing it is a good cleanup.


1210-1211: LGTM! Improved error message clarity.

The error message update improves user experience by making the incompatibility more explicit.


1797-1806: Edge case concerns are valid design limitations, not bugs—consider if they're acceptable.

Verification confirms your concerns are valid:

  1. Empty string default handles missing attributes silently: If a field is added to AppConfig.SECRET_FIELDS but not initialized in Config, getattr() returns empty string without error. This could mask configuration oversights. Consider raising an exception or logging a warning.

  2. Intentional design limitation—secrets cannot be cleared through UI: The validator always restores masked secrets from current config, preventing accidental deletion but also preventing intentional clearing. This appears intentional and is the same pattern used in enferno/utils/config_utils.py:353–355. Verify this behavior aligns with requirements.

  3. Literal mask collision is theoretically possible: If an actual secret value equals "**********", it would be incorrectly treated as masked. Though unlikely, consider a more unique sentinel value (e.g., a UUID or null byte prefix).

No bugs found, but review whether these design choices match your security and UX requirements.

Allow ConfigManager.MASK_STRING in secret field validators so users can save config without changing secrets. Remove duplicate secret restoration from validation model.

from enferno.admin.constants import Constants
from enferno.admin.models import Activity
from enferno.admin.models import Activity, AppConfig
@tarekio tarekio merged commit 94800e7 into main Nov 22, 2025
11 checks passed
@tarekio tarekio deleted the Revision-History-Secrets branch November 22, 2025 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants