C-4: MCP endpoint requires verified bearer token. Unauthenticated
requests rejected. _extract_principal() replaced by verified
AuthResult from middleware.
C-8: All delegation endpoints require verified bearer token.
X-Delegator-DID header removed — identity from token only.
delegator_ac_id validated to belong to authenticated principal.
Only delegators can revoke. Only delegator/delegate can view.
H-6: SQLite file permissions restricted to 0o600 (owner-only).
Umask set before creation. WAL/SHM files also restricted.
H-7: Delegation depth tracked and enforced against max_delegation_depth.
Sub-delegations increment depth. Exceeded depth → 403.
Shared TokenAuthenticator auto-detects identity driver from JWT
issuer claim (Keycloak or Entra). verify_bearer FastAPI dependency
for all protected endpoints. Health endpoint remains public.
ALL 10 critical findings CLOSED. ALL 10 high findings CLOSED.
Signed-off-by: Tyler King <tking@guildhouse.dev>
65 lines
2 KiB
Python
65 lines
2 KiB
Python
# Copyright 2026 Guildhouse Dev
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import os
|
|
import stat
|
|
|
|
from sqlmodel import SQLModel
|
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
from gsap_broker.settings import settings
|
|
|
|
engine: AsyncEngine = create_async_engine(settings.database_url, echo=False)
|
|
|
|
|
|
def _restrict_db_permissions() -> None:
|
|
"""Fix H-6: restrict SQLite file permissions to owner-only (0o600).
|
|
|
|
Also restricts WAL and SHM files if they exist.
|
|
"""
|
|
db_url = settings.database_url
|
|
if "sqlite" not in db_url:
|
|
return
|
|
|
|
# Extract path from sqlite URL
|
|
path = db_url.split("///")[-1] if "///" in db_url else ""
|
|
if not path or not os.path.exists(path):
|
|
return
|
|
|
|
mode = stat.S_IRUSR | stat.S_IWUSR # 0o600
|
|
try:
|
|
os.chmod(path, mode)
|
|
for suffix in ("-wal", "-shm"):
|
|
extra = path + suffix
|
|
if os.path.exists(extra):
|
|
os.chmod(extra, mode)
|
|
except OSError:
|
|
pass # may fail on Windows or read-only filesystem
|
|
|
|
|
|
async def init_db():
|
|
# Set restrictive umask before creating the database
|
|
old_umask = os.umask(0o077)
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(SQLModel.metadata.create_all)
|
|
# Schema migrations for existing DBs
|
|
for migration in [
|
|
"ALTER TABLE authorization_contexts ADD COLUMN session_mode BOOLEAN DEFAULT 0",
|
|
"ALTER TABLE delegations ADD COLUMN depth INTEGER DEFAULT 0",
|
|
"ALTER TABLE delegations ADD COLUMN parent_delegation_id TEXT DEFAULT NULL",
|
|
]:
|
|
try:
|
|
await conn.execute(__import__("sqlalchemy").text(migration))
|
|
except Exception:
|
|
pass # Column already exists
|
|
finally:
|
|
os.umask(old_umask)
|
|
|
|
# Fix H-6: restrict file permissions after creation
|
|
_restrict_db_permissions()
|
|
|
|
|
|
async def get_session():
|
|
async with AsyncSession(engine) as session:
|
|
yield session
|