Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8018005
feat: added call-imports metadata
MSami625 Aug 7, 2026
6d46894
feat: added import-calls user metadata
MSami625 Aug 7, 2026
3115b13
feat: implement actor stamping for call import evaluations and user i…
MSami625 Aug 7, 2026
681fe37
feat: enhance user insights with improved call import evaluations
MSami625 Aug 7, 2026
8786a43
chore: add config.docker.yml to .gitignore
MSami625 Aug 7, 2026
55b3bfd
feat: Cache evaluation PDF reports by content fingerprint and reuse S…
MSami625 Aug 7, 2026
7579e28
refactor: update tests for new principal handling
MSami625 Aug 10, 2026
b3f5609
test: add is_enabled method to fake S3 for PDF report branding logo test
MSami625 Aug 10, 2026
ebad2cb
feat: add actor stamping to call import evaluation deletion process
MSami625 Aug 10, 2026
e1281b4
feat: enhance call import evaluation components with inline metadata …
MSami625 Aug 10, 2026
8cf5ec7
fix: update token revocation logic to use correct Redis set parameters
MSami625 Aug 10, 2026
78a664c
feat: implement build_eval_chain_import_apply_async function for impr…
MSami625 Aug 10, 2026
e10fec4
chore: placeholder
MSami625 Aug 11, 2026
0ce2d6a
fix: downloading call import evaluation PDF reports and UI improvments
MSami625 Aug 11, 2026
7e09155
feat: integrate LLM usage tracking and reporting across various compo…
MSami625 Aug 11, 2026
03a7070
Merge branch 'main' into token-usage
MSami625 Aug 12, 2026
f0ebac9
feat: enhance LLM/STT usage tracking with additional context and metr…
MSami625 Aug 12, 2026
16ab94a
Merge branch 'token-usage' of https://github.com/EfficientAI-tech/eff…
MSami625 Aug 12, 2026
09b9793
fix: committed claims management in db
MSami625 Aug 13, 2026
5c51608
refactor: enhanced Redis handling and database transaction integrity
MSami625 Aug 13, 2026
4dd9fdc
refactor: usage commited claims
MSami625 Aug 13, 2026
cbe1c47
feat: usage pricing and integrate usage context across various routes
MSami625 Aug 14, 2026
7d59a5b
feat: add usage flush configuration and enterprise entitlement checks…
MSami625 Aug 14, 2026
cf9547d
refactor: remove Celery beat service and update usage flush configura…
MSami625 Aug 15, 2026
96b0038
Merge remote-tracking branch 'origin/main' into token-usage
MSami625 Aug 15, 2026
468f246
fix: correct indentation in call_imports.py and improve comments in p…
MSami625 Aug 15, 2026
5694c1f
fix(usage): aggregate eval usage per evaluation instead of per record…
MSami625 Aug 15, 2026
a48dc64
feat: add enabled models support for AI providers and new endpoint fo…
MSami625 Aug 15, 2026
cae3bf8
fix(tests): update monkeypatch paths to reflect correct structure
MSami625 Aug 15, 2026
5e4ea48
feat: introduce Celery Beat for platform tasks and update Docker conf…
MSami625 Aug 17, 2026
02e8c0a
fix(migrations): widen source column in model_pricing_rates to VARCHA…
MSami625 Aug 17, 2026
7576d5f
fix(pricing): add clear_rates_table_cache function and update migrati…
MSami625 Aug 17, 2026
83fc098
fix(usage): update recompute flag to false in pricing overrides and u…
MSami625 Aug 17, 2026
49871cb
fix(usage): implement _cost_fields_for_pending_deltas function to str…
MSami625 Aug 17, 2026
a33972d
feat(docs): add usage tracking feature documentation and update relat…
MSami625 Aug 18, 2026
5f5810f
fix(config): update security settings and operational access configur…
MSami625 Aug 20, 2026
cc6292a
fix(tests): enhance test_switch_org_revokes_previous_session_tokens
MSami625 Aug 20, 2026
4d73dc4
Merge origin/main into s-compliance
MSami625 Aug 24, 2026
d6852ab
feat(auth): enhance logout functionality to support access tokens and…
MSami625 Aug 24, 2026
9cf4915
feat(auth): implement revocable session management for user and platf…
MSami625 Aug 24, 2026
d144bb5
fix(auth): improve error handling in session revocation
MSami625 Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions app/api/v1/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,18 @@ def _extract_bearer(authorization: Optional[str]) -> Optional[str]:
return token.strip()


def _revoke_local_password_access_token(bearer: str) -> None:
try:
claims = decode_access_token(bearer)
jti = claims.get("jti")
exp = claims.get("exp")
if jti and exp:
ttl = max(int(exp) - int(datetime.now(timezone.utc).timestamp()), 1)
revoke_access_jti(jti, ttl)
except JWTError:
pass


def _issue_session_tokens(
db: Session,
*,
Expand Down Expand Up @@ -611,15 +623,7 @@ def logout(
"""Revoke the current session's refresh token and blacklist the access token."""
bearer = _extract_bearer(authorization)
if bearer and principal.auth_method == AuthMethod.LOCAL_PASSWORD:
try:
claims = decode_access_token(bearer)
jti = claims.get("jti")
exp = claims.get("exp")
if jti and exp:
ttl = max(int(exp) - int(datetime.now(timezone.utc).timestamp()), 1)
revoke_access_jti(jti, ttl)
except JWTError:
pass
_revoke_local_password_access_token(bearer)

if payload and payload.refresh_token:
revoke_refresh_token(db, payload.refresh_token)
Expand Down Expand Up @@ -697,12 +701,14 @@ def refresh_session(payload: RefreshRequest, db: Session = Depends(get_db)) -> T

class SwitchOrgRequest(BaseModel):
organization_id: str
refresh_token: Optional[str] = None


@router.post("/switch-org", response_model=TokenResponse)
def switch_organization(
payload: SwitchOrgRequest,
principal: Principal = Depends(get_principal),
authorization: Optional[str] = Header(None, alias="Authorization"),
db: Session = Depends(get_db),
) -> TokenResponse:
"""
Expand Down Expand Up @@ -766,6 +772,13 @@ def switch_organization(
detail="User is no longer active.",
)

if principal.auth_method == AuthMethod.LOCAL_PASSWORD:
bearer = _extract_bearer(authorization)
if bearer:
_revoke_local_password_access_token(bearer)
if payload.refresh_token:
revoke_refresh_token(db, payload.refresh_token)

user.last_login_at = datetime.now(timezone.utc)
db.commit()
db.refresh(user)
Expand Down
24 changes: 23 additions & 1 deletion app/api/v1/routes/platform_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from typing import List, Optional
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, Header, HTTPException, Query, status
from jose import JWTError
from pydantic import BaseModel, EmailStr, Field
from sqlalchemy import func
from sqlalchemy.orm import Session
Expand All @@ -16,6 +17,7 @@
create_platform_access_token,
get_platform_admin,
platform_admin_feature_enabled,
revoke_platform_access_token,
)
from app.core.auth.refresh_tokens import revoke_all_user_refresh_tokens
from app.core.password import hash_password, validate_password_strength, verify_password
Expand Down Expand Up @@ -184,6 +186,26 @@ def platform_me(
return PlatformAdminSummary(id=str(principal.platform_admin_id), email=principal.email)


def _extract_bearer(authorization: Optional[str]) -> Optional[str]:
if not authorization:
return None
scheme, _, token = authorization.partition(" ")
if scheme.lower() != "bearer" or not token.strip():
return None
return token.strip()


@router.post("/auth/logout")
def platform_logout(
authorization: Optional[str] = Header(None, alias="Authorization"),
principal: PlatformAdminPrincipal = Depends(get_platform_admin),
) -> dict:
bearer = _extract_bearer(authorization)
if bearer:
revoke_platform_access_token(bearer)
return {"success": True, "admin_id": str(principal.platform_admin_id)}


@router.get("/organizations", response_model=OrganizationListResponse)
def list_organizations(
offset: int = Query(0, ge=0),
Expand Down
13 changes: 11 additions & 2 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ class Settings(BaseSettings):
FRONTEND_DIR: str = "./frontend/dist"
FRONTEND_BASE_URL: str = ""

# Content Security Policy (Report-Only by default; set CSP_REPORT_ONLY=false to enforce)
# Content Security Policy (enforcing by default; set CSP_REPORT_ONLY=true for local report-only mode)
CSP_ENABLED: bool = True
CSP_REPORT_ONLY: bool = True
CSP_REPORT_ONLY: bool = False
CSP_POLICY: str = (
"default-src 'self'; "
"script-src 'self'; "
Expand Down Expand Up @@ -859,6 +859,15 @@ def _apply_llm_gateway_settings(gateway_cfg: dict, *, gateway_type: str) -> None
if "trusted_ips" in operational_config:
settings.OPERATIONAL_TRUSTED_IPS = operational_config["trusted_ips"]

if "security" in config_data:
security_config = config_data["security"]
if "csp_enabled" in security_config:
settings.CSP_ENABLED = bool(security_config["csp_enabled"])
if "csp_report_only" in security_config:
settings.CSP_REPORT_ONLY = bool(security_config["csp_report_only"])
if security_config.get("csp_policy"):
settings.CSP_POLICY = security_config["csp_policy"]

if "flexprice" in config_data:
flexprice_config = config_data["flexprice"]
if "enabled" in flexprice_config:
Expand Down
20 changes: 20 additions & 0 deletions app/core/auth/platform_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from sqlalchemy.orm import Session

from app.config import settings
from app.core.auth.token_revocation import is_access_jti_revoked, revoke_access_jti
from app.database import get_db
from app.models.database import PlatformAdmin

Expand Down Expand Up @@ -58,6 +59,18 @@ def decode_platform_access_token(token: str) -> Dict[str, Any]:
)


def revoke_platform_access_token(token: str) -> None:
try:
claims = decode_platform_access_token(token)
jti = claims.get("jti")
exp = claims.get("exp")
if jti and exp:
ttl = max(int(exp) - int(datetime.now(timezone.utc).timestamp()), 1)
revoke_access_jti(jti, ttl)
except JWTError:
pass


def _extract_bearer(authorization: Optional[str]) -> Optional[str]:
if not authorization:
return None
Expand Down Expand Up @@ -107,6 +120,13 @@ def get_platform_admin(
detail="Invalid platform admin token scope.",
)

jti = claims.get("jti")
if jti and is_access_jti_revoked(jti):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has been revoked.",
)

try:
admin_id = UUID(claims["sub"])
except (KeyError, ValueError) as exc:
Expand Down
28 changes: 5 additions & 23 deletions app/core/operational_access_middleware.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
"""Restrict /metrics from the public internet (/health stays open for load balancers)."""
"""Restrict /metrics from the public internet (/health stays open for load balancers).

Not Spring Boot Actuator: this FastAPI app exposes /health (LB probes) and /metrics
(Prometheus scrape). /metrics is gated by trusted IPs or OPERATIONAL_PUBLIC only.
"""

from __future__ import annotations

import ipaddress
import logging
from typing import Iterable

from fastapi import HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse, Response

from app.config import settings
from app.core.auth.dependency import _resolve
from app.database import SessionLocal

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -75,22 +76,6 @@ def _resolved_trusted_ip(request: Request) -> str | None:
return hops[-1]


def _has_authenticated_caller(request: Request) -> bool:
db = SessionLocal()
try:
principal = _resolve(
request.headers.get("authorization"),
request.headers.get("x-api-key"),
request.headers.get("x-efficientai-api-key"),
db,
)
return principal is not None
except HTTPException:
return False
finally:
db.close()


def is_operational_access_allowed(request: Request) -> bool:
"""Return True when the caller may access a protected operational endpoint."""
if settings.OPERATIONAL_PUBLIC:
Expand All @@ -100,9 +85,6 @@ def is_operational_access_allowed(request: Request) -> bool:
if resolved_ip and _ip_in_trusted(resolved_ip, settings.OPERATIONAL_TRUSTED_IPS):
return True

if _has_authenticated_caller(request):
return True

return False


Expand Down
9 changes: 7 additions & 2 deletions config.docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
app:
name: "EfficientAI Voice AI Evaluation Platform"
version: "0.1.0"
debug: true
debug: false
secret_key: "your-secret-key-here-change-in-production"
frontend_base_url: "http://localhost:8000"

Expand All @@ -15,12 +15,17 @@ server:
port: 8000

# Operational endpoints (/metrics). /health is always open for load balancers.
# Not Spring Boot Actuator — /metrics is IP-gated via trusted_ips below.
# Add VPC CIDRs here if Prometheus scrapes /metrics from inside the VPC.
operational:
public: false
trusted_ips:
- "10.0.0.0/8"

security:
csp_enabled: true
csp_report_only: false

# Database Configuration (Docker service name)
database:
url: "postgresql://efficientai:password@db:5432/efficientai"
Expand Down Expand Up @@ -99,7 +104,7 @@ auth:
local_password:
# Lifetime of the Bearer tokens minted at POST /auth/login (in minutes).
# Keep this short; clients re-authenticate silently.
token_ttl_minutes: 720 # 12 hours
token_ttl_minutes: 15
# Turn this off in Cloud SaaS to block self-serve signup.
allow_signup: true

Expand Down
8 changes: 8 additions & 0 deletions config.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@ server:
port: 8000

# Operational endpoints (/metrics). /health is always open for ALB/kube probes.
# This is FastAPI, not Spring Boot Actuator — scanners may flag /health or /metrics
# under an "Actuator" template. /metrics is IP-gated; /health returns minimal status only.
# Include VPC CIDRs in trusted_ips if Prometheus scrapes /metrics from inside the VPC.
operational:
public: false
trusted_ips:
- "10.0.0.0/8"

# HTTP security headers (CSP, X-Frame-Options, etc.)
security:
csp_enabled: true
csp_report_only: false # false = enforcing Content-Security-Policy header (required for compliance scans)
# csp_policy: "default-src 'self'; ..." # optional override

# Database Configuration
database:
# Legacy single-DB mode: use one database (e.g. efficientai). Sharding off (default).
Expand Down
4 changes: 4 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ CORS_ORIGINS=["http://localhost:3000", "http://localhost:8000"]
API_KEY_HEADER=X-API-Key
RATE_LIMIT_PER_MINUTE=60

# HTTP security headers (see config.yml.example security section)
CSP_ENABLED=true
CSP_REPORT_ONLY=false

# -----------------------------------------------------------------------------
# Authentication (pluggable auth providers)
# -----------------------------------------------------------------------------
Expand Down
Loading
Loading