All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
## Summary - Add Coolify deployment configuration (docker-compose, healthchecks, network setup) - Replace deploy-hetzner CI job with Coolify webhook deploy - Externalize postgres, qdrant, S3 for Coolify environment ## All changes since branch creation - Coolify docker-compose with Traefik labels and healthchecks - CI pipeline: deploy-hetzner → deploy-coolify (simple webhook curl) - SQLAlchemy 2.x text() compatibility fixes - Alpine-compatible Dockerfile fixes Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com> Reviewed-on: #1
136 lines
4.3 KiB
Python
136 lines
4.3 KiB
Python
"""
|
|
FastAPI routes for Compliance Scope persistence.
|
|
|
|
Stores the tenant's scope decision (frameworks, regulations, industry context)
|
|
in sdk_states.state->compliance_scope as JSONB.
|
|
|
|
Endpoints:
|
|
- GET /v1/compliance-scope?tenant_id=... → returns scope or 404
|
|
- POST /v1/compliance-scope → UPSERT scope (idempotent)
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any, Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Header
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import text
|
|
|
|
from database import SessionLocal
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/v1/compliance-scope", tags=["compliance-scope"])
|
|
|
|
|
|
# =============================================================================
|
|
# REQUEST / RESPONSE MODELS
|
|
# =============================================================================
|
|
|
|
class ComplianceScopeRequest(BaseModel):
|
|
"""Scope selection submitted by the frontend wizard."""
|
|
scope: dict[str, Any]
|
|
tenant_id: Optional[str] = None
|
|
|
|
|
|
class ComplianceScopeResponse(BaseModel):
|
|
"""Persisted scope object returned to the frontend."""
|
|
tenant_id: str
|
|
scope: dict[str, Any]
|
|
updated_at: str
|
|
created_at: str
|
|
|
|
|
|
# =============================================================================
|
|
# HELPERS
|
|
# =============================================================================
|
|
|
|
def _get_tid(
|
|
x_tenant_id: Optional[str],
|
|
query_tenant_id: str,
|
|
) -> str:
|
|
return x_tenant_id or query_tenant_id or "default"
|
|
|
|
|
|
def _row_to_response(row) -> ComplianceScopeResponse:
|
|
"""Convert a DB row (tenant_id, scope, created_at, updated_at) to response."""
|
|
return ComplianceScopeResponse(
|
|
tenant_id=row[0],
|
|
scope=row[1] if isinstance(row[1], dict) else {},
|
|
created_at=str(row[2]),
|
|
updated_at=str(row[3]),
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# ROUTES
|
|
# =============================================================================
|
|
|
|
@router.get("", response_model=ComplianceScopeResponse)
|
|
async def get_compliance_scope(
|
|
tenant_id: str = "default",
|
|
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
|
):
|
|
"""Return the persisted compliance scope for a tenant, or 404 if not set."""
|
|
tid = _get_tid(x_tenant_id, tenant_id)
|
|
db = SessionLocal()
|
|
try:
|
|
row = db.execute(
|
|
text("""SELECT tenant_id,
|
|
state->'compliance_scope' AS scope,
|
|
created_at,
|
|
updated_at
|
|
FROM sdk_states
|
|
WHERE tenant_id = :tid
|
|
AND state ? 'compliance_scope'"""),
|
|
{"tid": tid},
|
|
).fetchone()
|
|
|
|
if not row or row[1] is None:
|
|
raise HTTPException(status_code=404, detail="Compliance scope not found")
|
|
|
|
return _row_to_response(row)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("", response_model=ComplianceScopeResponse)
|
|
async def upsert_compliance_scope(
|
|
body: ComplianceScopeRequest,
|
|
tenant_id: str = "default",
|
|
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
|
):
|
|
"""Create or update the compliance scope for a tenant (UPSERT)."""
|
|
tid = _get_tid(x_tenant_id, body.tenant_id or tenant_id)
|
|
scope_json = json.dumps(body.scope)
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
db.execute(
|
|
text("""INSERT INTO sdk_states (tenant_id, state)
|
|
VALUES (:tid, jsonb_build_object('compliance_scope', :scope::jsonb))
|
|
ON CONFLICT (tenant_id) DO UPDATE
|
|
SET state = sdk_states.state || jsonb_build_object('compliance_scope', :scope::jsonb),
|
|
updated_at = NOW()"""),
|
|
{"tid": tid, "scope": scope_json},
|
|
)
|
|
db.commit()
|
|
|
|
row = db.execute(
|
|
text("""SELECT tenant_id,
|
|
state->'compliance_scope' AS scope,
|
|
created_at,
|
|
updated_at
|
|
FROM sdk_states
|
|
WHERE tenant_id = :tid"""),
|
|
{"tid": tid},
|
|
).fetchone()
|
|
|
|
return _row_to_response(row)
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to upsert compliance scope: {e}")
|
|
raise HTTPException(status_code=500, detail="Failed to save compliance scope")
|
|
finally:
|
|
db.close()
|