Compare commits

...

1 Commits

Author SHA1 Message Date
Nik
21d9175913 refactor(be): replace HTTPException with OnyxError in tenant files 2026-03-09 15:48:54 -07:00
7 changed files with 77 additions and 66 deletions

View File

@@ -2,12 +2,13 @@ from datetime import datetime
from datetime import timedelta
import jwt
from fastapi import HTTPException
from fastapi import Request
from onyx.configs.app_configs import DATA_PLANE_SECRET
from onyx.configs.app_configs import EXPECTED_API_KEY
from onyx.configs.app_configs import JWT_ALGORITHM
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
logger = setup_logger()
@@ -32,22 +33,24 @@ async def control_plane_dep(request: Request) -> None:
api_key = request.headers.get("X-API-KEY")
if api_key != EXPECTED_API_KEY:
logger.warning("Invalid API key")
raise HTTPException(status_code=401, detail="Invalid API key")
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "Invalid API key")
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
logger.warning("Invalid authorization header")
raise HTTPException(status_code=401, detail="Invalid authorization header")
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "Invalid authorization header")
token = auth_header.split(" ")[1]
try:
payload = jwt.decode(token, DATA_PLANE_SECRET, algorithms=[JWT_ALGORITHM])
if payload.get("scope") != "tenant:create":
logger.warning("Insufficient permissions")
raise HTTPException(status_code=403, detail="Insufficient permissions")
raise OnyxError(
OnyxErrorCode.INSUFFICIENT_PERMISSIONS, "Insufficient permissions"
)
except jwt.ExpiredSignatureError:
logger.warning("Token has expired")
raise HTTPException(status_code=401, detail="Token has expired")
raise OnyxError(OnyxErrorCode.TOKEN_EXPIRED, "Token has expired")
except jwt.InvalidTokenError:
logger.warning("Invalid token")
raise HTTPException(status_code=401, detail="Invalid token")
raise OnyxError(OnyxErrorCode.INVALID_TOKEN, "Invalid token")

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Response
from fastapi_users import exceptions
@@ -12,6 +11,8 @@ from onyx.auth.users import get_redis_strategy
from onyx.auth.users import User
from onyx.db.engine.sql_engine import get_session_with_tenant
from onyx.db.users import get_user_by_email
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
logger = setup_logger()
@@ -30,7 +31,7 @@ async def impersonate_user(
except exceptions.UserNotExists:
detail = f"User has no tenant mapping: {impersonate_request.email=}"
logger.warning(detail)
raise HTTPException(status_code=422, detail=detail)
raise OnyxError(OnyxErrorCode.USER_NOT_FOUND, detail)
with get_session_with_tenant(tenant_id=tenant_id) as tenant_session:
user_to_impersonate = get_user_by_email(
@@ -41,7 +42,7 @@ async def impersonate_user(
f"User not found in tenant: {impersonate_request.email=} {tenant_id=}"
)
logger.warning(detail)
raise HTTPException(status_code=422, detail=detail)
raise OnyxError(OnyxErrorCode.USER_NOT_FOUND, detail)
token = await get_redis_strategy().write_token(user_to_impersonate)

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Response
from sqlalchemy.exc import IntegrityError
@@ -18,6 +17,8 @@ from onyx.auth.users import User
from onyx.configs.constants import ANONYMOUS_USER_COOKIE_NAME
from onyx.configs.constants import FASTAPI_USERS_AUTH_COOKIE_NAME
from onyx.db.engine.sql_engine import get_session_with_shared_schema
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
from shared_configs.contextvars import get_current_tenant_id
@@ -33,7 +34,7 @@ async def get_anonymous_user_path_api(
tenant_id = get_current_tenant_id()
if tenant_id is None:
raise HTTPException(status_code=404, detail="Tenant not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Tenant not found")
with get_session_with_shared_schema() as db_session:
current_path = get_anonymous_user_path(tenant_id, db_session)
@@ -50,21 +51,21 @@ async def set_anonymous_user_path_api(
try:
validate_anonymous_user_path(anonymous_user_path)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
with get_session_with_shared_schema() as db_session:
try:
modify_anonymous_user_path(tenant_id, anonymous_user_path, db_session)
except IntegrityError:
raise HTTPException(
status_code=409,
detail="The anonymous user path is already in use. Please choose a different path.",
raise OnyxError(
OnyxErrorCode.CONFLICT,
"The anonymous user path is already in use. Please choose a different path.",
)
except Exception as e:
logger.exception(f"Failed to modify anonymous user path: {str(e)}")
raise HTTPException(
status_code=500,
detail="An unexpected error occurred while modifying the anonymous user path",
raise OnyxError(
OnyxErrorCode.INTERNAL_ERROR,
"An unexpected error occurred while modifying the anonymous user path",
)
@@ -77,10 +78,10 @@ async def login_as_anonymous_user(
anonymous_user_path, db_session
)
if not tenant_id:
raise HTTPException(status_code=404, detail="Tenant not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Tenant not found")
if not anonymous_user_enabled(tenant_id=tenant_id):
raise HTTPException(status_code=403, detail="Anonymous user is not enabled")
raise OnyxError(OnyxErrorCode.UNAUTHORIZED, "Anonymous user is not enabled")
token = generate_anonymous_user_jwt_token(tenant_id)

View File

@@ -4,7 +4,6 @@ import uuid
import aiohttp # Async HTTP client
import httpx
import requests
from fastapi import HTTPException
from fastapi import Request
from sqlalchemy import select
from sqlalchemy.orm import Session
@@ -41,6 +40,8 @@ from onyx.db.models import AvailableTenant
from onyx.db.models import IndexModelStatus
from onyx.db.models import SearchSettings
from onyx.db.models import UserTenantMapping
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.llm.well_known_providers.auto_update_models import LLMRecommendations
from onyx.llm.well_known_providers.constants import ANTHROPIC_PROVIDER_NAME
from onyx.llm.well_known_providers.constants import OPENAI_PROVIDER_NAME
@@ -116,9 +117,9 @@ async def get_or_provision_tenant(
# If we've encountered an error, log and raise an exception
error_msg = "Failed to provision tenant"
logger.error(error_msg, exc_info=e)
raise HTTPException(
status_code=500,
detail="Failed to provision tenant. Please try again later.",
raise OnyxError(
OnyxErrorCode.INTERNAL_ERROR,
"Failed to provision tenant. Please try again later.",
)
@@ -144,18 +145,18 @@ async def create_tenant(
await rollback_tenant_provisioning(tenant_id)
except Exception:
logger.exception(f"Failed to rollback tenant provisioning for {tenant_id}")
raise HTTPException(status_code=500, detail="Failed to provision tenant.")
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, "Failed to provision tenant.")
return tenant_id
async def provision_tenant(tenant_id: str, email: str) -> None:
if not MULTI_TENANT:
raise HTTPException(status_code=403, detail="Multi-tenancy is not enabled")
raise OnyxError(OnyxErrorCode.UNAUTHORIZED, "Multi-tenancy is not enabled")
if user_owns_a_tenant(email):
raise HTTPException(
status_code=409, detail="User already belongs to an organization"
raise OnyxError(
OnyxErrorCode.CONFLICT, "User already belongs to an organization"
)
logger.debug(f"Provisioning tenant {tenant_id} for user {email}")
@@ -175,8 +176,8 @@ async def provision_tenant(tenant_id: str, email: str) -> None:
except Exception as e:
logger.exception(f"Failed to create tenant {tenant_id}")
raise HTTPException(
status_code=500, detail=f"Failed to create tenant: {str(e)}"
raise OnyxError(
OnyxErrorCode.INTERNAL_ERROR, f"Failed to create tenant: {str(e)}"
)

View File

@@ -25,7 +25,6 @@ import httpx
from fastapi import APIRouter
from fastapi import Depends
from fastapi import Header
from fastapi import HTTPException
from pydantic import BaseModel
from ee.onyx.configs.app_configs import LICENSE_ENFORCEMENT_ENABLED
@@ -36,6 +35,8 @@ from ee.onyx.server.tenants.access import generate_data_plane_token
from ee.onyx.utils.license import is_license_valid
from ee.onyx.utils.license import verify_license_signature
from onyx.configs.app_configs import CONTROL_PLANE_API_BASE_URL
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
logger = setup_logger()
@@ -46,9 +47,9 @@ router = APIRouter(prefix="/proxy")
def _check_license_enforcement_enabled() -> None:
"""Ensure LICENSE_ENFORCEMENT_ENABLED is true (proxy endpoints only work on cloud DP)."""
if not LICENSE_ENFORCEMENT_ENABLED:
raise HTTPException(
status_code=501,
detail="Proxy endpoints are only available on cloud data plane",
raise OnyxError(
OnyxErrorCode.NOT_IMPLEMENTED,
"Proxy endpoints are only available on cloud data plane",
)
@@ -81,8 +82,9 @@ def _extract_license_from_header(
"""
if not authorization or not authorization.startswith("Bearer "):
if required:
raise HTTPException(
status_code=401, detail="Missing or invalid authorization header"
raise OnyxError(
OnyxErrorCode.UNAUTHENTICATED,
"Missing or invalid authorization header",
)
return None
@@ -110,10 +112,10 @@ def verify_license_auth(
try:
payload = verify_license_signature(license_data)
except ValueError as e:
raise HTTPException(status_code=401, detail=f"Invalid license: {e}")
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, f"Invalid license: {e}")
if not allow_expired and not is_license_valid(payload):
raise HTTPException(status_code=401, detail="License has expired")
raise OnyxError(OnyxErrorCode.TOKEN_EXPIRED, "License has expired")
return payload
@@ -197,12 +199,12 @@ async def forward_to_control_plane(
except Exception:
pass
logger.error(f"Control plane returned {status_code}: {detail}")
raise HTTPException(status_code=status_code, detail=detail)
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY, detail, status_code_override=status_code
)
except httpx.RequestError:
logger.exception("Failed to connect to control plane")
raise HTTPException(
status_code=502, detail="Failed to connect to control plane"
)
raise OnyxError(OnyxErrorCode.BAD_GATEWAY, "Failed to connect to control plane")
# -----------------------------------------------------------------------------
@@ -294,9 +296,9 @@ async def proxy_claim_license(
if not tenant_id or not license_data:
logger.error(f"Control plane returned incomplete claim response: {result}")
raise HTTPException(
status_code=502,
detail="Control plane returned incomplete license data",
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY,
"Control plane returned incomplete license data",
)
return ClaimLicenseResponse(
@@ -326,7 +328,7 @@ async def proxy_create_customer_portal_session(
# tenant_id is a required field in LicensePayload (Pydantic validates this),
# but we check explicitly for defense in depth
if not license_payload.tenant_id:
raise HTTPException(status_code=401, detail="License missing tenant_id")
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "License missing tenant_id")
tenant_id = license_payload.tenant_id
@@ -367,7 +369,7 @@ async def proxy_billing_information(
# tenant_id is a required field in LicensePayload (Pydantic validates this),
# but we check explicitly for defense in depth
if not license_payload.tenant_id:
raise HTTPException(status_code=401, detail="License missing tenant_id")
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "License missing tenant_id")
tenant_id = license_payload.tenant_id
@@ -398,12 +400,12 @@ async def proxy_license_fetch(
# tenant_id is a required field in LicensePayload (Pydantic validates this),
# but we check explicitly for defense in depth
if not license_payload.tenant_id:
raise HTTPException(status_code=401, detail="License missing tenant_id")
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "License missing tenant_id")
if tenant_id != license_payload.tenant_id:
raise HTTPException(
status_code=403,
detail="Cannot fetch license for a different tenant",
raise OnyxError(
OnyxErrorCode.UNAUTHORIZED,
"Cannot fetch license for a different tenant",
)
result = await forward_to_control_plane("GET", f"/license/{tenant_id}")
@@ -411,9 +413,9 @@ async def proxy_license_fetch(
license_data = result.get("license")
if not license_data:
logger.error(f"Control plane returned incomplete license response: {result}")
raise HTTPException(
status_code=502,
detail="Control plane returned incomplete license data",
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY,
"Control plane returned incomplete license data",
)
# Return license to caller - self-hosted instance stores it via /api/license/claim
@@ -432,7 +434,7 @@ async def proxy_seat_update(
Returns the regenerated license in the response for the caller to store.
"""
if not license_payload.tenant_id:
raise HTTPException(status_code=401, detail="License missing tenant_id")
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "License missing tenant_id")
tenant_id = license_payload.tenant_id

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from ee.onyx.server.tenants.provisioning import delete_user_from_control_plane
@@ -12,6 +11,8 @@ from onyx.db.auth import get_user_count
from onyx.db.engine.sql_engine import get_session
from onyx.db.users import delete_user_from_db
from onyx.db.users import get_user_by_email
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.server.manage.models import UserByEmail
from onyx.utils.logger import setup_logger
from shared_configs.contextvars import get_current_tenant_id
@@ -30,13 +31,14 @@ async def leave_organization(
tenant_id = get_current_tenant_id()
if current_user.email != user_email.user_email:
raise HTTPException(
status_code=403, detail="You can only leave the organization as yourself"
raise OnyxError(
OnyxErrorCode.INSUFFICIENT_PERMISSIONS,
"You can only leave the organization as yourself",
)
user_to_delete = get_user_by_email(user_email.user_email, db_session)
if user_to_delete is None:
raise HTTPException(status_code=404, detail="User not found")
raise OnyxError(OnyxErrorCode.USER_NOT_FOUND, "User not found")
num_admin_users = await get_user_count(only_admin_users=True)
@@ -53,9 +55,9 @@ async def leave_organization(
logger.exception(
f"Failed to delete user from control plane for tenant {tenant_id}: {e}"
)
raise HTTPException(
status_code=500,
detail=f"Failed to remove user from control plane: {str(e)}",
raise OnyxError(
OnyxErrorCode.INTERNAL_ERROR,
f"Failed to remove user from control plane: {str(e)}",
)
db_session.expunge(user_to_delete)

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from ee.onyx.server.tenants.models import ApproveUserRequest
from ee.onyx.server.tenants.models import PendingUserSnapshot
@@ -13,6 +12,8 @@ from onyx.auth.invited_users import get_pending_users
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_user
from onyx.auth.users import User
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
from shared_configs.contextvars import get_current_tenant_id
@@ -32,7 +33,7 @@ async def request_invite(
logger.exception(
f"Failed to invite self to tenant {invite_request.tenant_id}: {e}"
)
raise HTTPException(status_code=500, detail=str(e))
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
@router.get("/users/pending")
@@ -64,7 +65,7 @@ async def accept_invite(
accept_user_invite(user.email, invite_request.tenant_id)
except Exception as e:
logger.exception(f"Failed to accept invite: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to accept invitation")
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, "Failed to accept invitation")
@router.post("/users/invite/deny")
@@ -79,4 +80,4 @@ async def deny_invite(
deny_user_invite(user.email, invite_request.tenant_id)
except Exception as e:
logger.exception(f"Failed to deny invite: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to deny invitation")
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, "Failed to deny invitation")