96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
"""Durable active-session registry used to revoke signed operator sessions."""
|
|
|
|
import hashlib
|
|
import sqlite3
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
|
|
class SessionStoreError(RuntimeError):
|
|
"""Raised when session state cannot be read or changed safely."""
|
|
|
|
|
|
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:
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
connection = sqlite3.connect(
|
|
self.path, timeout=self.lock_timeout_seconds
|
|
)
|
|
else:
|
|
connection = sqlite3.connect(
|
|
f"{self.path.resolve().as_uri()}?mode=rw",
|
|
timeout=self.lock_timeout_seconds,
|
|
uri=True,
|
|
)
|
|
if initialize:
|
|
connection.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS active_sessions (
|
|
session_hash TEXT PRIMARY KEY,
|
|
expires_at INTEGER NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
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) -> None:
|
|
try:
|
|
with self._connect(initialize=True) as connection:
|
|
connection.execute(
|
|
"DELETE FROM active_sessions WHERE expires_at <= ?", (int(self.clock()),)
|
|
)
|
|
connection.execute(
|
|
"INSERT INTO active_sessions(session_hash, expires_at) VALUES (?, ?)",
|
|
(self._digest(session_id), expires_at),
|
|
)
|
|
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 revoke(self, session_id: str) -> None:
|
|
try:
|
|
with self._connect() as connection:
|
|
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 revoke_all(self) -> None:
|
|
try:
|
|
with self._connect() as connection:
|
|
connection.execute("DELETE FROM active_sessions")
|
|
except (OSError, sqlite3.Error) as exc:
|
|
raise SessionStoreError("Session registry is temporarily unavailable") from exc
|