mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-01 04:22:20 +02:00
security: bind bearer sessions to endpoint provenance
This commit is contained in:
@@ -187,6 +187,13 @@ class Session(TimestampMixin, Base):
|
||||
endpoint_url = Column(String, nullable=False)
|
||||
model = Column(String, nullable=False)
|
||||
owner = Column(String, nullable=True, index=True) # username; null = legacy/shared
|
||||
|
||||
# Bearer-chat sessions must retain the exact server-owned endpoint they
|
||||
# were created from. Keep this reference non-cascading so endpoint
|
||||
# disable/delete/owner changes remain observable as an orphan and fail
|
||||
# closed at the next bearer LLM boundary.
|
||||
model_endpoint_id = Column(String, nullable=True, index=True)
|
||||
endpoint_provenance = Column(String, nullable=True)
|
||||
|
||||
# Configuration flags
|
||||
rag = Column(Boolean, default=False)
|
||||
@@ -999,6 +1006,40 @@ def _migrate_add_owner_column():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _migrate_add_session_endpoint_provenance_columns():
|
||||
"""Add the durable endpoint identity used by bearer session validation."""
|
||||
import sqlite3
|
||||
|
||||
db_path = DATABASE_URL.replace("sqlite:///", "")
|
||||
if not os.path.exists(db_path):
|
||||
return
|
||||
conn = None
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
columns = {row[1] for row in conn.execute("PRAGMA table_info(sessions)")}
|
||||
if "model_endpoint_id" not in columns:
|
||||
conn.execute("ALTER TABLE sessions ADD COLUMN model_endpoint_id TEXT")
|
||||
if "endpoint_provenance" not in columns:
|
||||
conn.execute("ALTER TABLE sessions ADD COLUMN endpoint_provenance TEXT")
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_sessions_model_endpoint_id "
|
||||
"ON sessions(model_endpoint_id)"
|
||||
)
|
||||
conn.commit()
|
||||
logging.getLogger(__name__).info(
|
||||
"Migrated: added session endpoint identity/provenance columns"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).warning(
|
||||
"Session endpoint provenance migration failed: %s", e
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _migrate_model_endpoints():
|
||||
"""Recreate model_endpoints table if schema changed (url->base_url)."""
|
||||
import sqlite3
|
||||
@@ -2152,6 +2193,7 @@ def init_db():
|
||||
_migrate_add_supports_tools_column()
|
||||
_migrate_add_task_run_model_column()
|
||||
_migrate_add_owner_column()
|
||||
_migrate_add_session_endpoint_provenance_columns()
|
||||
_migrate_add_document_archived_column()
|
||||
_migrate_add_last_message_at_column()
|
||||
_migrate_add_folder_column()
|
||||
|
||||
@@ -90,6 +90,8 @@ class Session:
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
history: List[ChatMessage] = None
|
||||
owner: Optional[str] = None
|
||||
model_endpoint_id: Optional[str] = None
|
||||
endpoint_provenance: Optional[str] = None
|
||||
is_important: bool = False
|
||||
message_count: int = 0
|
||||
|
||||
|
||||
@@ -165,6 +165,8 @@ class SessionManager:
|
||||
headers=headers,
|
||||
history=[],
|
||||
owner=getattr(db_session, "owner", None),
|
||||
model_endpoint_id=getattr(db_session, "model_endpoint_id", None),
|
||||
endpoint_provenance=getattr(db_session, "endpoint_provenance", None),
|
||||
is_important=getattr(db_session, "is_important", False) or False,
|
||||
)
|
||||
session.message_count = getattr(db_session, "message_count", 0) or 0
|
||||
@@ -221,6 +223,8 @@ class SessionManager:
|
||||
headers=headers,
|
||||
history=history,
|
||||
owner=getattr(db_session, 'owner', None),
|
||||
model_endpoint_id=getattr(db_session, 'model_endpoint_id', None),
|
||||
endpoint_provenance=getattr(db_session, 'endpoint_provenance', None),
|
||||
is_important=getattr(db_session, 'is_important', False) or False,
|
||||
)
|
||||
|
||||
@@ -502,6 +506,8 @@ class SessionManager:
|
||||
session.rag = db_session.rag
|
||||
session.archived = db_session.archived
|
||||
session.owner = getattr(db_session, "owner", None)
|
||||
session.model_endpoint_id = getattr(db_session, "model_endpoint_id", None)
|
||||
session.endpoint_provenance = getattr(db_session, "endpoint_provenance", None)
|
||||
session.is_important = getattr(db_session, "is_important", False) or False
|
||||
session.message_count = (
|
||||
db.query(DbChatMessage)
|
||||
@@ -602,6 +608,50 @@ class SessionManager:
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def set_session_endpoint_provenance(
|
||||
self,
|
||||
session_id: str,
|
||||
*,
|
||||
model_endpoint_id: Optional[str],
|
||||
endpoint_provenance: str,
|
||||
) -> bool:
|
||||
"""Persist the server-owned endpoint provenance for a session.
|
||||
|
||||
``registered`` rows carry an exact ModelEndpoint id. ``direct`` rows
|
||||
deliberately carry no endpoint id and retain direct API-key
|
||||
compatibility. The values are assigned only after the durable write
|
||||
succeeds so an in-memory session cannot claim provenance the database
|
||||
did not accept.
|
||||
"""
|
||||
provenance = str(endpoint_provenance or "").strip().lower()
|
||||
endpoint_id = str(model_endpoint_id or "").strip() or None
|
||||
if provenance == "registered" and not endpoint_id:
|
||||
raise ValueError("registered session provenance requires an endpoint id")
|
||||
if provenance == "direct":
|
||||
endpoint_id = None
|
||||
if provenance not in {"registered", "direct"}:
|
||||
raise ValueError("unsupported session endpoint provenance")
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
db_session = db.query(DbSession).filter(DbSession.id == session_id).first()
|
||||
if db_session is None:
|
||||
raise KeyError(f"Session {session_id} not found")
|
||||
db_session.model_endpoint_id = endpoint_id
|
||||
db_session.endpoint_provenance = provenance
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
session = self.sessions.get(session_id)
|
||||
if session is not None:
|
||||
session.model_endpoint_id = endpoint_id
|
||||
session.endpoint_provenance = provenance
|
||||
return True
|
||||
|
||||
def delete_session(self, session_id: str) -> bool:
|
||||
"""Permanently delete a session and all its messages."""
|
||||
db = SessionLocal()
|
||||
|
||||
Reference in New Issue
Block a user