Files
FabledSteward/steward/core/audit.py
T
bvandeusenandClaude Opus 5.5 8dad79f17a
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m12s
CI / integration (push) Successful in 59s
CI / publish (push) Successful in 1m3s
feat(ansible): runs stay inside their source; controller-side vars are admin-only (#5034)
#5034 "Playbook runs: path and inventory checks in trigger_run, connection
vars admin-only, confirm enforced server-side, web actions audited".
Ansible runs inside the Steward container, so an operator who could name
any playbook path, set ansible_connection=local or ProxyCommand ssh args,
or put {{ lookup('pipe', …) }} in a value could run commands on Steward.

- steward/ansible/guards.py: check_playbook (the source must list it),
  check_inventory_scope (steward:* or an inventory file the source has),
  check_vars_for_role (ansible_* beyond host/port/user/password/become,
  and any Jinja in a value, are admin-only), changed_vars (an edit is
  held to the role only for what it changes).
- trigger_run applies the path checks, so every run path — web, MCP,
  alerts, schedules, plugins — gets them.
- Variables are checked by role on the web run form, host-page runs,
  schedule create/update, inventory group/target create/update, and MCP
  run_playbook / create_schedule / update_schedule.
- A steward:confirm playbook needs the confirmation ticked on the server
  side for a real (non-check) web run, as MCP already required.
- Audited: runs, cancel, schedule create/update/enable/disable/delete/
  run-now, playbook create/update/delete, inventory group and target
  create/update/delete (variable names only, never values). One
  audit_request helper serves the web routes.
- The CSRF form guard and the route guard now each test that they fail
  on a synthetic broken input (rule: a guard must be able to fail).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:57:34 -04:00

49 lines
1.8 KiB
Python

"""steward/core/audit.py
Helpers for writing audit log entries. Each call opens its own DB
session so audit events are committed independently of the calling
route's transaction (audit is only written after the main action
succeeds, by calling this after the main `async with db.begin()` block).
"""
from __future__ import annotations
import json
import logging
from datetime import datetime, timezone
logger = logging.getLogger(__name__)
async def log_audit(
app,
user_id: str | None,
username: str,
action: str,
entity_type: str | None = None,
entity_id: str | None = None,
detail: dict | None = None,
) -> None:
"""Write one audit event. Never raises — failures are logged and swallowed."""
from steward.models.audit import AuditEvent
try:
async with app.db_sessionmaker() as db:
async with db.begin():
db.add(AuditEvent(
user_id=user_id,
username=username or "unknown",
action=action,
entity_type=entity_type,
entity_id=entity_id,
detail_json=json.dumps(detail) if detail else None,
timestamp=datetime.now(timezone.utc),
))
except Exception:
logger.exception("Failed to write audit event action=%r", action)
async def audit_request(action: str, entity_type: str | None = None,
entity_id: str | None = None, detail: dict | None = None) -> None:
"""log_audit for the logged-in user of the current web request."""
from quart import current_app, session
await log_audit(current_app, session.get("user_id"), session.get("username", ""),
action, entity_type=entity_type, entity_id=entity_id, detail=detail)