mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
7492816ab2
* feat: governance — RBAC, tool policies, prompt templates, usage tracking, audit logging Add comprehensive governance layer for the admin console: - RBAC with 15 granular permissions, 3 builtin roles (admin, operator, viewer), custom role CRUD, user-role assignment with privilege escalation prevention - Tool policies with glob pattern matching, priority-ordered evaluation (allow/deny/ask), enforced before auto-approve in WebUI.approve_tools() - Prompt templates with variable substitution, categories, default flag - Usage tracking: per-LLM-request token/tool metrics, aggregated queries (group by day/model/user), automatic 90-day pruning via scheduler - Audit logging: append-only event trail for all admin mutations, filterable/paginated queries, automatic 365-day pruning, X-Forwarded-For aware IP extraction - require_permission() enforced on all 35+ admin endpoints (users, tokens, channels, schedules, watches, roles, orgs, policies, templates, usage, audit) - Field allowlists on storage update methods prevent mass-assignment bugs - Self-deletion guard on admin_delete_user, delete_user cascades user_roles - _row_to_dict helper eliminates ~400 lines of fragile positional row mapping - _audit_context helper deduplicates 18 instances of audit boilerplate - Migration 008: 7 new tables, 3 builtin roles, org_id on users - Console admin panel: 5 new tabs (Roles, Policies, Templates, Usage, Audit) with permission-gated visibility, 7 modal dialogs, full keyboard accessibility - Python + TypeScript SDK methods for all governance endpoints - 120+ new tests (1554 total) * fix: address PR #39 review feedback - Rebuild serialized items after policy evaluation so denied/allowed verdicts are reflected in tool_info/approve_request SSE payloads - Make `since` query param optional in usage OpenAPI spec (handler already defaults to last 7 days) - Add response_model=StatusResponse to DELETE role/policy/template and POST/DELETE role assignment endpoints in OpenAPI spec - Add missing org_id/created/updated fields to UserRoleInfo schema - Add missing created field to AuditEventInfo schema - Show "no permissions" empty state instead of loading inaccessible tab when all admin tabs are permission-gated - Fix "13 permissions" → "15 permissions" in architecture.md and security.md - Fix import sorting in test_audit.py and test_tool_policy.py * fix: address PR #39 round 2 review feedback - Clear stale permissions from sessionStorage on config-token login (auth.js _storePermissions) - Only trust X-Forwarded-For when behind a proxy that sets X-Forwarded-Proto (conditional on is_secure_request trust model) - Thread user_id from auth into WebUI.on_status for usage events - Add created field to TS AuditEventInfo type - Return typed Pydantic models from all SDK governance methods instead of dict[str, Any] — both async and sync clients - Validate group_by param against allowed enum in admin_usage handler - Add deterministic secondary sort (event_id DESC) to list_audit_events in both SQLite and PostgreSQL backends
59 lines
1.6 KiB
Python
59 lines
1.6 KiB
Python
"""Tests for turnstone.core.audit."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from turnstone.core.audit import record_audit
|
|
from turnstone.core.storage._sqlite import SQLiteBackend
|
|
|
|
|
|
@pytest.fixture
|
|
def storage(tmp_path):
|
|
path = str(tmp_path / "test.db")
|
|
backend = SQLiteBackend(path)
|
|
yield backend
|
|
backend.close()
|
|
|
|
|
|
def test_record_audit_basic(storage):
|
|
record_audit(
|
|
storage, "user-1", "user.create", "user", "u123", {"username": "alice"}, "127.0.0.1"
|
|
)
|
|
events = storage.list_audit_events()
|
|
assert len(events) == 1
|
|
ev = events[0]
|
|
assert ev["user_id"] == "user-1"
|
|
assert ev["action"] == "user.create"
|
|
assert ev["resource_type"] == "user"
|
|
assert ev["resource_id"] == "u123"
|
|
assert ev["ip_address"] == "127.0.0.1"
|
|
detail = json.loads(ev["detail"])
|
|
assert detail["username"] == "alice"
|
|
|
|
|
|
def test_record_audit_no_detail(storage):
|
|
record_audit(storage, "user-1", "token.revoke", "token", "t456")
|
|
events = storage.list_audit_events()
|
|
assert len(events) == 1
|
|
assert events[0]["detail"] == "{}"
|
|
|
|
|
|
def test_record_audit_silent_on_failure():
|
|
"""record_audit should not raise even if storage is broken."""
|
|
|
|
class BrokenStorage:
|
|
def record_audit_event(self, **kw):
|
|
raise RuntimeError("boom")
|
|
|
|
# Should not raise
|
|
record_audit(BrokenStorage(), "u1", "test.action")
|
|
|
|
|
|
def test_record_audit_generates_unique_ids(storage):
|
|
record_audit(storage, "u1", "a.one")
|
|
record_audit(storage, "u1", "a.two")
|
|
events = storage.list_audit_events()
|
|
assert len(events) == 2
|
|
assert events[0]["event_id"] != events[1]["event_id"]
|