oc_chat
Loading...
Searching...
No Matches
v0_3_1_auth_events.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/migrations/v0_3_1_auth_events.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_auth_signing.py"
6"""
7Runtime migration for auth_events v0.3.1.
8
9Purpose
10-------
11Rebuild ``auth_events`` so unknown-key failures can be recorded with a NULL
12``hmac_key_id`` while successful auth rows still require a credential key.
13
14Responsibilities
15----------------
16- Preserve existing auth event rows.
17- Keep the FK to ``agent_credentials`` for non-NULL key ids.
18- Bump SQLite ``user_version`` to 31.
19
20Diagnostics
21-----------
22Domain: OC_CHAT.MAIN
23Levels:
24 L2 - migration lifecycle
25 L3 - schema version decisions
26 L4 - table definition checks
27
28Contracts
29---------
30- Callers own transaction boundaries.
31- The migration is idempotent.
32
33Dependencies
34------------
35- sqlite3 (migration connection)
36- oc_diagnostics (structured debug logging via _dbg)
37"""
38from __future__ import annotations
39
40import sqlite3
41
42from oc_diagnostics import _dbg
43
44_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
45
46AUTH_EVENTS_SCHEMA_VERSION = 31
47AUTH_EVENTS_SCHEMA_DESCRIPTION = "auth_events REGISTRY_SCHEMA_V0_3_1"
48
49AUTH_EVENTS_CHECK = "auth_ok = 0 OR hmac_key_id IS NOT NULL"
50
51
52def current_user_version(conn: sqlite3.Connection) -> int:
53 """Return SQLite's integer schema user version."""
54
55 return int(conn.execute("PRAGMA user_version").fetchone()[0])
56
57
58def apply(conn: sqlite3.Connection) -> None:
59 """Apply the auth_events v0.3.1 rebuild if needed."""
60
61 if is_applied(conn):
62 conn.execute(f"PRAGMA user_version = {AUTH_EVENTS_SCHEMA_VERSION}")
63 return
65 conn.execute(f"PRAGMA user_version = {AUTH_EVENTS_SCHEMA_VERSION}")
66
67
68def rollback(conn: sqlite3.Connection) -> None:
69 """Reset user_version for tests; table shape remains backward-compatible."""
70
71 conn.execute("PRAGMA user_version = 3")
72 conn.execute(
73 "DELETE FROM schema_migrations WHERE version = ?",
74 (AUTH_EVENTS_SCHEMA_VERSION,),
75 )
76
77
78def is_applied(conn: sqlite3.Connection) -> bool:
79 """Return True when the auth_events CHECK is present."""
80
81 row = conn.execute(
82 # OCT-LINT: disable=no_hardcoded_secrets reason="Schema object names are migration constants, not secrets"
83 "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'auth_events'"
84 ).fetchone()
85 if row is None:
86 return False
87 try:
88 sql = row["sql"]
89 except (TypeError, IndexError):
90 sql = row[0]
91 return AUTH_EVENTS_CHECK in str(sql)
92
93
94def migration_sql() -> str:
95 """Return SQL used when building a fresh schema in one executescript call."""
96
97 return _REBUILD_SQL + f"\nPRAGMA user_version = {AUTH_EVENTS_SCHEMA_VERSION};\n"
98
99
100def _rebuild_auth_events(conn: sqlite3.Connection) -> None:
101 conn.executescript(_REBUILD_SQL)
102
103
104# OCT-LINT: disable=no_hardcoded_secrets reason="Schema object names are migration constants, not secrets"
105_REBUILD_SQL = """
106ALTER TABLE auth_events RENAME TO auth_events_old;
107
108CREATE TABLE auth_events (
109 id INTEGER PRIMARY KEY AUTOINCREMENT,
110 ts TEXT NOT NULL,
111 attempted_slot TEXT,
112 hmac_key_id TEXT REFERENCES agent_credentials(hmac_key_id),
113 command TEXT NOT NULL,
114 payload_sha256 TEXT,
115 auth_ok INTEGER NOT NULL CHECK(auth_ok IN (0,1)),
116 failure_reason TEXT
117 CHECK(failure_reason IN
118 ('bad_signature','expired_ts','unknown_slot','no_credential',
119 'revoked_key','malformed_payload','clock_skew','missing_secrets_file')),
120 remote_hint TEXT CHECK(remote_hint IS NULL OR length(remote_hint) <= 255),
121 CHECK (
122 (auth_ok = 1 AND failure_reason IS NULL) OR
123 (auth_ok = 0 AND failure_reason IS NOT NULL)
124 ),
125 CHECK (auth_ok = 0 OR hmac_key_id IS NOT NULL)
126);
127
128INSERT INTO auth_events(
129 id, ts, attempted_slot, hmac_key_id, command, payload_sha256,
130 auth_ok, failure_reason, remote_hint
131)
132SELECT
133 id, ts, attempted_slot, hmac_key_id, command, payload_sha256,
134 auth_ok, failure_reason, remote_hint
135FROM auth_events_old;
136
137DROP TABLE auth_events_old;
138
139CREATE INDEX idx_auth_events_ts ON auth_events(ts);
140CREATE INDEX idx_auth_events_slot ON auth_events(attempted_slot, ts);
141CREATE INDEX idx_auth_events_slot_failure
142 ON auth_events(attempted_slot, failure_reason, ts)
143 WHERE auth_ok = 0;
144"""