"""Durable active-session registry used to revoke signed operator sessions.""" import hashlib import secrets import sqlite3 from dataclasses import dataclass from pathlib import Path from typing import Callable from src.private_state import connect_private_sqlite class SessionStoreError(RuntimeError): """Raised when session state cannot be read or changed safely.""" class SessionStatus(str): """String-compatible status carrying the server-confirmed idle deadline.""" idle_expires_at: int | None def __new__(cls, value: str, idle_expires_at: int | None = None): instance = super().__new__(cls, value) instance.idle_expires_at = idle_expires_at return instance @dataclass(frozen=True) class ActiveDevice: management_id: str device_label: str created_at: int expires_at: int current: bool class SessionStore: def __init__( self, path: str | Path, *, clock: Callable[[], float], lock_timeout_seconds: float = 0.1, ) -> None: self.path = Path(path) self.clock = clock self.lock_timeout_seconds = lock_timeout_seconds @staticmethod def _digest(session_id: str) -> str: return hashlib.sha256(session_id.encode()).hexdigest() def _connect(self, *, initialize: bool = False) -> sqlite3.Connection: try: if initialize: connection = connect_private_sqlite( self.path, timeout=self.lock_timeout_seconds ) else: connection = connect_private_sqlite( self.path, timeout=self.lock_timeout_seconds, existing_only=True, ) if initialize: connection.execute( """ CREATE TABLE IF NOT EXISTS active_sessions ( session_hash TEXT PRIMARY KEY, expires_at INTEGER NOT NULL, management_id TEXT, device_label TEXT, created_at INTEGER, last_active_at INTEGER ) """ ) connection.execute( """ CREATE TABLE IF NOT EXISTS step_up_grants ( grant_hash TEXT PRIMARY KEY, session_hash TEXT NOT NULL, action TEXT NOT NULL, target TEXT NOT NULL, expires_at INTEGER NOT NULL ) """ ) connection.execute( "CREATE INDEX IF NOT EXISTS step_up_grants_session_hash " "ON step_up_grants(session_hash)" ) columns = { row[1] for row in connection.execute("PRAGMA table_info(active_sessions)") } additions = { "management_id": "TEXT", "device_label": "TEXT", "created_at": "INTEGER", "last_active_at": "INTEGER", } for name, column_type in additions.items(): if name not in columns: connection.execute( f"ALTER TABLE active_sessions ADD COLUMN {name} {column_type}" ) connection.execute( "UPDATE active_sessions SET management_id = lower(hex(randomblob(16))) " "WHERE management_id IS NULL" ) connection.execute( "UPDATE active_sessions SET device_label = 'Existing device' " "WHERE device_label IS NULL" ) connection.execute( "UPDATE active_sessions SET created_at = ? WHERE created_at IS NULL", (int(self.clock()),), ) connection.execute( "UPDATE active_sessions SET last_active_at = ? WHERE last_active_at IS NULL", (int(self.clock()),), ) connection.execute( "CREATE UNIQUE INDEX IF NOT EXISTS active_sessions_management_id " "ON active_sessions(management_id)" ) return connection except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc def activate( self, session_id: str, expires_at: int, *, device_label: str = "This device", management_id: str | None = None, ) -> None: label = " ".join(str(device_label).split())[:64] or "This device" now = int(self.clock()) try: with self._connect(initialize=True) as connection: connection.execute( "DELETE FROM active_sessions WHERE expires_at <= ?", (now,) ) connection.execute( "INSERT INTO active_sessions(" "session_hash, expires_at, management_id, device_label, created_at, last_active_at" ") VALUES (?, ?, ?, ?, ?, ?)", ( self._digest(session_id), expires_at, management_id or secrets.token_urlsafe(18), label, now, now, ), ) except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc def is_active(self, session_id: str, expires_at: int) -> bool: now = int(self.clock()) try: with self._connect() as connection: row = connection.execute( "SELECT expires_at FROM active_sessions WHERE session_hash = ?", (self._digest(session_id),), ).fetchone() except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc return row is not None and row[0] == expires_at and expires_at > now def status( self, session_id: str, expires_at: int, *, idle_timeout_seconds: int ) -> str: now = int(self.clock()) query = ( "SELECT expires_at, last_active_at FROM active_sessions " "WHERE session_hash = ?" ) parameters = (self._digest(session_id),) try: try: with self._connect() as connection: row = connection.execute(query, parameters).fetchone() except sqlite3.OperationalError as exc: if "no such column: last_active_at" not in str(exc): raise with self._connect(initialize=True) as connection: row = connection.execute(query, parameters).fetchone() except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc if row is None or row[0] != expires_at or expires_at <= now: return SessionStatus("revoked") idle_expires_at = min(row[0], row[1] + max(1, idle_timeout_seconds)) if idle_expires_at <= now: return SessionStatus("idle", idle_expires_at) return SessionStatus("active", idle_expires_at) def touch( self, session_id: str, expires_at: int, *, idle_timeout_seconds: int ) -> bool: now = int(self.clock()) try: with self._connect() as connection: cursor = connection.execute( "UPDATE active_sessions SET last_active_at = ? " "WHERE session_hash = ? AND expires_at = ? AND expires_at > ? " "AND last_active_at + ? > ?", ( now, self._digest(session_id), expires_at, now, max(1, idle_timeout_seconds), now, ), ) return cursor.rowcount == 1 except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc def revoke(self, session_id: str) -> None: try: with self._connect(initialize=True) as connection: connection.execute( "DELETE FROM step_up_grants WHERE session_hash = ?", (self._digest(session_id),), ) connection.execute( "DELETE FROM active_sessions WHERE session_hash = ?", (self._digest(session_id),), ) except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc def list_active(self, current_session_id: str) -> list[ActiveDevice]: now = int(self.clock()) current_hash = self._digest(current_session_id) try: with self._connect() as connection: rows = connection.execute( "SELECT management_id, device_label, created_at, expires_at, session_hash " "FROM active_sessions WHERE expires_at > ? " "ORDER BY expires_at DESC, created_at DESC", (now,), ).fetchall() except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc return [ ActiveDevice( management_id=row[0], device_label=row[1], created_at=row[2], expires_at=row[3], current=secrets.compare_digest(row[4], current_hash), ) for row in rows ] def management_id(self, session_id: str) -> str: try: with self._connect() as connection: row = connection.execute( "SELECT management_id FROM active_sessions WHERE session_hash = ?", (self._digest(session_id),), ).fetchone() except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc if row is None: raise SessionStoreError("Session is no longer active") return row[0] def managed_status(self, management_id: str, *, idle_timeout_seconds: int) -> str: """Return the authorization state for a durable device identifier.""" return self.managed_statuses( [management_id], idle_timeout_seconds=idle_timeout_seconds )[management_id] def managed_statuses( self, management_ids, *, idle_timeout_seconds: int ) -> dict[str, str]: """Return authorization states for durable device identifiers in one read.""" requested = set(management_ids) if not requested: return {} now = int(self.clock()) try: with self._connect() as connection: placeholders = ",".join("?" for _ in requested) rows = connection.execute( "SELECT management_id, expires_at, last_active_at " f"FROM active_sessions WHERE management_id IN ({placeholders})", tuple(requested), ).fetchall() except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc known = {row[0]: row[1:] for row in rows if row[0] in requested} idle_timeout = max(1, idle_timeout_seconds) statuses = {} for management_id in requested: row = known.get(management_id) if row is None or row[0] <= now: statuses[management_id] = "revoked" elif row[1] + idle_timeout <= now: statuses[management_id] = "idle" else: statuses[management_id] = "active" return statuses def revoke_managed(self, management_id: str) -> bool: try: with self._connect() as connection: row = connection.execute( "SELECT session_hash FROM active_sessions WHERE management_id = ?", (management_id,), ).fetchone() if row is None: return False connection.execute( "DELETE FROM step_up_grants WHERE session_hash = ?", (row[0],) ) cursor = connection.execute( "DELETE FROM active_sessions WHERE management_id = ?", (management_id,) ) return cursor.rowcount == 1 except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc def revoke_all(self) -> None: try: with self._connect(initialize=True) as connection: connection.execute("DELETE FROM step_up_grants") connection.execute("DELETE FROM active_sessions") except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc def mint_step_up( self, session_id: str, *, action: str, target: str, ttl_seconds: int, ) -> str: now = int(self.clock()) grant = secrets.token_urlsafe(32) session_hash = self._digest(session_id) try: with self._connect(initialize=True) as connection: active = connection.execute( "SELECT 1 FROM active_sessions " "WHERE session_hash = ? AND expires_at > ?", (session_hash, now), ).fetchone() if active is None: raise SessionStoreError("Session is no longer active") connection.execute( "DELETE FROM step_up_grants WHERE expires_at <= ?", (now,) ) connection.execute( "INSERT INTO step_up_grants(" "grant_hash, session_hash, action, target, expires_at" ") VALUES (?, ?, ?, ?, ?)", ( self._digest(grant), session_hash, action, target, now + max(1, ttl_seconds), ), ) except SessionStoreError: raise except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc return grant def consume_step_up( self, grant: str, session_id: str, *, action: str, target: str, ) -> bool: now = int(self.clock()) try: with self._connect() as connection: connection.execute( "DELETE FROM step_up_grants WHERE expires_at <= ?", (now,) ) cursor = connection.execute( "DELETE FROM step_up_grants WHERE grant_hash = ? " "AND session_hash = ? AND action = ? AND target = ? " "AND expires_at > ? AND EXISTS (" "SELECT 1 FROM active_sessions " "WHERE active_sessions.session_hash = step_up_grants.session_hash " "AND active_sessions.expires_at > ?)", ( self._digest(grant), self._digest(session_id), action, target, now, now, ), ) return cursor.rowcount == 1 except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc