fix(audit): address /code-review findings
- scheduler _run_changed: count a run as "changed" only on a truthy NUMERIC
value — status-string no-op dicts like {"skipped":"disabled"} (Plex off, the
default) or {"skipped":"no demo account"} were flooding the trail every interval.
- audit.record: truncate target_id to the column width (128) like summary[:255],
so an over-long target (e.g. a 128+ char demo email) can't abort the mutation
the audit row is committed alongside.
- config set/reset: redact URL userinfo (user:pass@) from logged non-secret
values — a proxy setting can embed credentials that shouldn't land in the trail.
- config set: skip the audit row when a non-secret value didn't actually change
(no-op re-save shouldn't add noise); secrets are write-only so always logged.
- audit page title (pageMeta): add the missing "audit" case so the browser tab
reads "Audit log · Siftlode" instead of falling back to "Feed".
This commit is contained in:
parent
318fdc4812
commit
32d8575f79
4 changed files with 34 additions and 12 deletions
|
|
@ -1,5 +1,7 @@
|
|||
"""Admin Configuration page API: read/edit the DB-overridable operational settings defined in
|
||||
app.sysconfig. Secret values (e.g. SMTP password) are write-only — never returned."""
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
|
@ -13,6 +15,15 @@ from app.routes.admin import admin_user
|
|||
|
||||
router = APIRouter(prefix="/api/admin/config", tags=["admin"])
|
||||
|
||||
# Strip URL userinfo (user:pass@) so a non-secret setting that happens to embed credentials —
|
||||
# e.g. an authenticated egress proxy http://user:pass@host:port — never lands plaintext in the
|
||||
# audit trail. Non-secret values are shown live in the UI, but the log shouldn't retain creds.
|
||||
_URL_USERINFO = re.compile(r"(://)[^/@\s]+@")
|
||||
|
||||
|
||||
def _redact(v):
|
||||
return _URL_USERINFO.sub(r"\1***@", v) if isinstance(v, str) else v
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_config(_: User = Depends(admin_user), db: Session = Depends(get_db)) -> dict:
|
||||
|
|
@ -41,15 +52,19 @@ def set_config(
|
|||
sysconfig.set_value(db, key, payload["value"])
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc) or "Invalid value")
|
||||
# Never log a secret's value — only that it was changed, by whom.
|
||||
# Never log a secret's value — only that it was changed, by whom. Non-secret values are logged
|
||||
# but with URL credentials redacted (a proxy/URL setting can embed user:pass). Skip the row when
|
||||
# a non-secret value didn't actually change (a no-op re-save shouldn't add trail noise); secrets
|
||||
# are write-only so we can't compare — always log.
|
||||
new = None if s.secret else sysconfig.get(db, key)
|
||||
audit.record(
|
||||
db, admin.id, AuditAction.CONFIG_SET, target_type="config", target_id=key,
|
||||
summary=f"{key} = (secret updated)" if s.secret else f"{key} = {new}",
|
||||
before=None if s.secret else {"value": old},
|
||||
after=None if s.secret else {"value": new},
|
||||
)
|
||||
db.commit()
|
||||
if s.secret or new != old:
|
||||
audit.record(
|
||||
db, admin.id, AuditAction.CONFIG_SET, target_type="config", target_id=key,
|
||||
summary=f"{key} = (secret updated)" if s.secret else f"{key} = {_redact(new)}",
|
||||
before=None if s.secret else {"value": _redact(old)},
|
||||
after=None if s.secret else {"value": _redact(new)},
|
||||
)
|
||||
db.commit()
|
||||
return sysconfig.describe(db)
|
||||
|
||||
|
||||
|
|
@ -67,7 +82,7 @@ def reset_config(
|
|||
audit.record(
|
||||
db, admin.id, AuditAction.CONFIG_RESET, target_type="config", target_id=key,
|
||||
summary=f"{key} → default",
|
||||
before=None if s.secret else {"value": old},
|
||||
before=None if s.secret else {"value": _redact(old)},
|
||||
)
|
||||
db.commit()
|
||||
return sysconfig.describe(db)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue