mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-26 01:52:45 +00:00
Compare commits
5 Commits
v3.0.4
...
nikg/std-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07a5eb3e69 | ||
|
|
e04936b83e | ||
|
|
76d430242a | ||
|
|
f2b5ff095a | ||
|
|
77a3243b80 |
@@ -23,11 +23,9 @@ from email_validator import EmailUndeliverableError
|
||||
from email_validator import validate_email
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Query
|
||||
from fastapi import Request
|
||||
from fastapi import Response
|
||||
from fastapi import status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi_users import BaseUserManager
|
||||
@@ -119,6 +117,8 @@ from onyx.db.models import Persona
|
||||
from onyx.db.models import User
|
||||
from onyx.db.pat import fetch_user_for_pat
|
||||
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.redis.redis_pool import get_async_redis_connection
|
||||
from onyx.server.settings.store import load_settings
|
||||
from onyx.server.utils import BasicAuthenticationError
|
||||
@@ -137,8 +137,6 @@ from shared_configs.contextvars import get_current_tenant_id
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
REGISTER_INVITE_ONLY_CODE = "REGISTER_INVITE_ONLY"
|
||||
|
||||
|
||||
def is_user_admin(user: User) -> bool:
|
||||
return user.role == UserRole.ADMIN
|
||||
@@ -227,17 +225,17 @@ def verify_email_is_invited(email: str) -> None:
|
||||
whitelist = get_invited_users()
|
||||
|
||||
if not email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={"reason": "Email must be specified"},
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"Email must be specified",
|
||||
)
|
||||
|
||||
try:
|
||||
email_info = validate_email(email, check_deliverability=False)
|
||||
except EmailUndeliverableError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={"reason": "Email is not valid"},
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"Email is not valid",
|
||||
)
|
||||
|
||||
for email_whitelist in whitelist:
|
||||
@@ -255,12 +253,9 @@ def verify_email_is_invited(email: str) -> None:
|
||||
if email_info.normalized.lower() == email_info_whitelist.normalized.lower():
|
||||
return
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
"code": REGISTER_INVITE_ONLY_CODE,
|
||||
"reason": "This workspace is invite-only. Please ask your admin to invite you.",
|
||||
},
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.UNAUTHORIZED,
|
||||
"This workspace is invite-only. Please ask your admin to invite you.",
|
||||
)
|
||||
|
||||
|
||||
@@ -272,9 +267,9 @@ def verify_email_in_whitelist(email: str, tenant_id: str) -> None:
|
||||
|
||||
def verify_email_domain(email: str) -> None:
|
||||
if email.count("@") != 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email is not valid",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"Email is not valid",
|
||||
)
|
||||
|
||||
local_part, domain = email.split("@")
|
||||
@@ -283,39 +278,35 @@ def verify_email_domain(email: str) -> None:
|
||||
if AUTH_TYPE == AuthType.CLOUD:
|
||||
# Normalize googlemail.com to gmail.com (they deliver to the same inbox)
|
||||
if domain == "googlemail.com":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={"reason": "Please use @gmail.com instead of @googlemail.com."},
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"Please use @gmail.com instead of @googlemail.com.",
|
||||
)
|
||||
|
||||
if "+" in local_part and domain != "onyx.app":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"reason": "Email addresses with '+' are not allowed. Please use your base email address."
|
||||
},
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"Email addresses with '+' are not allowed. Please use your base email address.",
|
||||
)
|
||||
|
||||
# Check if email uses a disposable/temporary domain
|
||||
if is_disposable_email(email):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"reason": "Disposable email addresses are not allowed. Please use a permanent email address."
|
||||
},
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"Disposable email addresses are not allowed. Please use a permanent email address.",
|
||||
)
|
||||
|
||||
# Check domain whitelist if configured
|
||||
if VALID_EMAIL_DOMAINS:
|
||||
if domain not in VALID_EMAIL_DOMAINS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email domain is not valid",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"Email domain is not valid",
|
||||
)
|
||||
|
||||
|
||||
def enforce_seat_limit(db_session: Session, seats_needed: int = 1) -> None:
|
||||
"""Raise HTTPException(402) if adding users would exceed the seat limit.
|
||||
"""Raise OnyxError if adding users would exceed the seat limit.
|
||||
|
||||
No-op for multi-tenant or CE deployments.
|
||||
"""
|
||||
@@ -327,7 +318,7 @@ def enforce_seat_limit(db_session: Session, seats_needed: int = 1) -> None:
|
||||
)(db_session, seats_needed=seats_needed)
|
||||
|
||||
if result is not None and not result.available:
|
||||
raise HTTPException(status_code=402, detail=result.error_message)
|
||||
raise OnyxError(OnyxErrorCode.SEAT_LIMIT_EXCEEDED, result.error_message)
|
||||
|
||||
|
||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
@@ -380,9 +371,9 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
captcha_token or "", expected_action="signup"
|
||||
)
|
||||
except CaptchaVerificationError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={"reason": str(e)},
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
str(e),
|
||||
)
|
||||
|
||||
# We verify the password here to make sure it's valid before we proceed
|
||||
@@ -394,11 +385,11 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
# This prevents creating tenants for throwaway email addresses
|
||||
try:
|
||||
verify_email_domain(user_create.email)
|
||||
except HTTPException as e:
|
||||
except OnyxError as e:
|
||||
# Log blocked disposable email attempts
|
||||
if (
|
||||
e.status_code == status.HTTP_400_BAD_REQUEST
|
||||
and "Disposable email" in str(e.detail)
|
||||
e.error_code == OnyxErrorCode.VALIDATION_ERROR
|
||||
and "Disposable email" in str(e.message)
|
||||
):
|
||||
domain = (
|
||||
user_create.email.split("@")[-1]
|
||||
@@ -623,7 +614,7 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
)
|
||||
|
||||
if not tenant_id:
|
||||
raise HTTPException(status_code=401, detail="User not found")
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "User not found")
|
||||
|
||||
# Proceed with the tenant context
|
||||
token = None
|
||||
@@ -863,8 +854,8 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
logger.error(
|
||||
"Email is not configured. Please configure email in the admin panel"
|
||||
)
|
||||
raise HTTPException(
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INTERNAL_ERROR,
|
||||
"Your admin has not enabled this feature.",
|
||||
)
|
||||
tenant_id = await fetch_ee_implementation_or_noop(
|
||||
@@ -964,12 +955,9 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
old_password, user.hashed_password
|
||||
)
|
||||
if not verified:
|
||||
# Raise some HTTPException (or your custom exception) if old password is invalid:
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid current password",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.CREDENTIAL_INVALID,
|
||||
"Invalid current password",
|
||||
)
|
||||
|
||||
# If the hash was upgraded behind the scenes, we can keep it before setting the new password:
|
||||
@@ -1239,11 +1227,7 @@ class FastAPIUserWithLogoutRouter(FastAPIUsers[models.UP, models.ID]):
|
||||
)
|
||||
|
||||
logout_responses: OpenAPIResponseType = {
|
||||
**{
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Missing token or inactive user."
|
||||
}
|
||||
},
|
||||
**{401: {"description": "Missing token or inactive user."}},
|
||||
**backend.transport.get_openapi_logout_responses_success(),
|
||||
}
|
||||
|
||||
@@ -1277,11 +1261,7 @@ class FastAPIUserWithLogoutRouter(FastAPIUsers[models.UP, models.ID]):
|
||||
)
|
||||
|
||||
refresh_responses: OpenAPIResponseType = {
|
||||
**{
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Missing token or inactive user."
|
||||
}
|
||||
},
|
||||
**{401: {"description": "Missing token or inactive user."}},
|
||||
**backend.transport.get_openapi_login_responses_success(),
|
||||
}
|
||||
|
||||
@@ -1334,9 +1314,9 @@ class FastAPIUserWithLogoutRouter(FastAPIUsers[models.UP, models.ID]):
|
||||
return await backend.login(strategy, user)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in refresh endpoint: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Token refresh failed: {str(e)}",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INVALID_TOKEN,
|
||||
f"Token refresh failed: {str(e)}",
|
||||
)
|
||||
|
||||
return router
|
||||
@@ -1759,7 +1739,7 @@ def get_oauth_router(
|
||||
name=callback_route_name,
|
||||
description="The response varies based on the authentication backend used.",
|
||||
responses={
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
400: {
|
||||
"model": ErrorModel,
|
||||
"content": {
|
||||
"application/json": {
|
||||
@@ -1792,24 +1772,24 @@ def get_oauth_router(
|
||||
)
|
||||
|
||||
if account_email is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.OAUTH_NOT_AVAILABLE_EMAIL,
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
ErrorCode.OAUTH_NOT_AVAILABLE_EMAIL,
|
||||
)
|
||||
|
||||
try:
|
||||
state_data = decode_jwt(state, state_secret, [STATE_TOKEN_AUDIENCE])
|
||||
except jwt.DecodeError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=getattr(
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INVALID_TOKEN,
|
||||
getattr(
|
||||
ErrorCode, "ACCESS_TOKEN_DECODE_ERROR", "ACCESS_TOKEN_DECODE_ERROR"
|
||||
),
|
||||
)
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=getattr(
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.TOKEN_EXPIRED,
|
||||
getattr(
|
||||
ErrorCode,
|
||||
"ACCESS_TOKEN_ALREADY_EXPIRED",
|
||||
"ACCESS_TOKEN_ALREADY_EXPIRED",
|
||||
@@ -1823,9 +1803,9 @@ def get_oauth_router(
|
||||
or not state_csrf_token
|
||||
or not secrets.compare_digest(cookie_csrf_token, state_csrf_token)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=getattr(ErrorCode, "OAUTH_INVALID_STATE", "OAUTH_INVALID_STATE"),
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.CSRF_FAILURE,
|
||||
getattr(ErrorCode, "OAUTH_INVALID_STATE", "OAUTH_INVALID_STATE"),
|
||||
)
|
||||
|
||||
next_url = state_data.get("next_url", "/")
|
||||
@@ -1853,15 +1833,15 @@ def get_oauth_router(
|
||||
is_verified_by_default=is_verified_by_default,
|
||||
)
|
||||
except UserAlreadyExists:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.OAUTH_USER_ALREADY_EXISTS,
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.CONFLICT,
|
||||
ErrorCode.OAUTH_USER_ALREADY_EXISTS,
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.LOGIN_BAD_CREDENTIALS,
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.UNAUTHENTICATED,
|
||||
ErrorCode.LOGIN_BAD_CREDENTIALS,
|
||||
)
|
||||
|
||||
# Login user
|
||||
|
||||
@@ -90,12 +90,23 @@ class OnyxErrorCode(Enum):
|
||||
def detail(self, message: str | None = None) -> dict[str, str]:
|
||||
"""Build a structured error detail dict.
|
||||
|
||||
Returns a dict like:
|
||||
{"error_code": "UNAUTHENTICATED", "message": "Token expired"}
|
||||
Returns a dict like::
|
||||
|
||||
{
|
||||
"error_code": "UNAUTHENTICATED",
|
||||
"message": "Token expired",
|
||||
"detail": "Token expired", # backward-compat alias
|
||||
}
|
||||
|
||||
The ``detail`` key mirrors ``message`` for backward compatibility with
|
||||
clients that read the ``detail`` field from FastAPI's default
|
||||
``HTTPException`` response shape.
|
||||
|
||||
If no message is supplied, the error code itself is used as the message.
|
||||
"""
|
||||
msg = message or self.code
|
||||
return {
|
||||
"error_code": self.code,
|
||||
"message": message or self.code,
|
||||
"message": msg,
|
||||
"detail": msg,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
Raise ``OnyxError`` instead of ``HTTPException`` in business code. A global
|
||||
FastAPI exception handler (registered via ``register_onyx_exception_handlers``)
|
||||
converts it into a JSON response with the standard
|
||||
``{"error_code": "...", "message": "..."}`` shape.
|
||||
``{"error_code": "...", "message": "...", "detail": "..."}`` shape. The
|
||||
``detail`` key mirrors ``message`` for backward compatibility with clients
|
||||
that expect FastAPI's default ``HTTPException`` response format.
|
||||
|
||||
Usage::
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi import Response
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -23,6 +22,8 @@ from onyx.db.federated import update_federated_connector
|
||||
from onyx.db.federated import update_federated_connector_oauth_token
|
||||
from onyx.db.federated import validate_federated_connector_credentials
|
||||
from onyx.db.models import User
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.federated_connectors.factory import get_federated_connector
|
||||
from onyx.federated_connectors.factory import get_federated_connector_cls
|
||||
from onyx.federated_connectors.interfaces import FederatedConnector
|
||||
@@ -58,7 +59,7 @@ def _get_federated_connector_instance(
|
||||
try:
|
||||
return get_federated_connector(source, credentials)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
|
||||
|
||||
|
||||
@router.post("")
|
||||
@@ -96,11 +97,11 @@ def create_federated_connector(
|
||||
except ValueError as e:
|
||||
logger.warning(f"Validation error creating federated connector: {e}")
|
||||
db_session.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating federated connector: {e}")
|
||||
db_session.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
|
||||
|
||||
|
||||
@router.get("/{id}/entities")
|
||||
@@ -113,10 +114,10 @@ def get_entities(
|
||||
try:
|
||||
federated_connector = fetch_federated_connector_by_id(id, db_session)
|
||||
if not federated_connector:
|
||||
raise HTTPException(status_code=404, detail="Federated connector not found")
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
|
||||
if federated_connector.credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Federated connector has no credentials"
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR, "Federated connector has no credentials"
|
||||
)
|
||||
|
||||
connector_instance = _get_federated_connector_instance(
|
||||
@@ -138,11 +139,11 @@ def get_entities(
|
||||
|
||||
return EntitySpecResponse(entities=entities_dict)
|
||||
|
||||
except HTTPException:
|
||||
except OnyxError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching entities for federated connector {id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
|
||||
|
||||
|
||||
@router.get("/{id}/credentials/schema")
|
||||
@@ -155,10 +156,10 @@ def get_credentials_schema(
|
||||
try:
|
||||
federated_connector = fetch_federated_connector_by_id(id, db_session)
|
||||
if not federated_connector:
|
||||
raise HTTPException(status_code=404, detail="Federated connector not found")
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
|
||||
if federated_connector.credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Federated connector has no credentials"
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR, "Federated connector has no credentials"
|
||||
)
|
||||
|
||||
connector_instance = _get_federated_connector_instance(
|
||||
@@ -181,13 +182,13 @@ def get_credentials_schema(
|
||||
|
||||
return CredentialSchemaResponse(credentials=credentials_dict)
|
||||
|
||||
except HTTPException:
|
||||
except OnyxError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error fetching credentials schema for federated connector {id}: {e}"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
|
||||
|
||||
|
||||
@router.get("/sources/{source}/configuration/schema")
|
||||
@@ -215,7 +216,7 @@ def get_configuration_schema_by_source(
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching configuration schema for source {source}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
|
||||
|
||||
|
||||
@router.get("/sources/{source}/credentials/schema")
|
||||
@@ -242,11 +243,11 @@ def get_credentials_schema_by_source(
|
||||
|
||||
return CredentialSchemaResponse(credentials=credentials_dict)
|
||||
|
||||
except HTTPException:
|
||||
except OnyxError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching credentials schema for source {source}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
|
||||
|
||||
|
||||
@router.post("/sources/{source}/credentials/validate")
|
||||
@@ -262,15 +263,15 @@ def validate_credentials(
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=400, detail="Credentials are invalid")
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Credentials are invalid")
|
||||
|
||||
return is_valid
|
||||
|
||||
except HTTPException:
|
||||
except OnyxError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating credentials for source {source}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
|
||||
|
||||
|
||||
@router.head("/{id}/entities/validate")
|
||||
@@ -284,7 +285,7 @@ def validate_entities(
|
||||
try:
|
||||
federated_connector = fetch_federated_connector_by_id(id, db_session)
|
||||
if not federated_connector:
|
||||
raise HTTPException(status_code=404, detail="Federated connector not found")
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
|
||||
if federated_connector.credentials is None:
|
||||
return Response(status_code=400)
|
||||
|
||||
@@ -310,7 +311,7 @@ def validate_entities(
|
||||
else:
|
||||
return Response(status_code=400)
|
||||
|
||||
except HTTPException:
|
||||
except OnyxError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating entities for federated connector {id}: {e}")
|
||||
@@ -326,14 +327,16 @@ def get_authorize_url(
|
||||
"""Get URL to send the user for OAuth"""
|
||||
# Validate that the ID is not None or invalid
|
||||
if id is None or id <= 0:
|
||||
raise HTTPException(status_code=400, detail="Invalid federated connector ID")
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR, "Invalid federated connector ID"
|
||||
)
|
||||
|
||||
federated_connector = fetch_federated_connector_by_id(id, db_session)
|
||||
if not federated_connector:
|
||||
raise HTTPException(status_code=404, detail="Federated connector not found")
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
|
||||
if federated_connector.credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Federated connector has no credentials"
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR, "Federated connector has no credentials"
|
||||
)
|
||||
|
||||
# Update credentials to include the correct redirect URI with the connector ID
|
||||
@@ -379,19 +382,19 @@ def handle_oauth_callback_generic(
|
||||
# Verify state parameter and get session info
|
||||
state = callback_data.get("state")
|
||||
if not state:
|
||||
raise HTTPException(status_code=400, detail="Missing state parameter")
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Missing state parameter")
|
||||
|
||||
try:
|
||||
oauth_session = verify_oauth_state(state)
|
||||
except ValueError:
|
||||
logger.exception("Error verifying OAuth state")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid or expired state parameter"
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR, "Invalid or expired state parameter"
|
||||
)
|
||||
|
||||
if not oauth_session:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid or expired state parameter"
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR, "Invalid or expired state parameter"
|
||||
)
|
||||
|
||||
# Get federated connector ID from the state
|
||||
@@ -400,19 +403,19 @@ def handle_oauth_callback_generic(
|
||||
# Validate federated_connector_id is not None
|
||||
if federated_connector_id is None:
|
||||
logger.error("OAuth session has null federated_connector_id")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid OAuth session: missing federated connector ID",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"Invalid OAuth session: missing federated connector ID",
|
||||
)
|
||||
|
||||
federated_connector = fetch_federated_connector_by_id(
|
||||
federated_connector_id, db_session
|
||||
)
|
||||
if not federated_connector:
|
||||
raise HTTPException(status_code=404, detail="Federated connector not found")
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
|
||||
if federated_connector.credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Federated connector has no credentials"
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR, "Federated connector has no credentials"
|
||||
)
|
||||
|
||||
connector_instance = _get_federated_connector_instance(
|
||||
@@ -519,10 +522,10 @@ def get_federated_connector_detail(
|
||||
"""Get detailed information about a specific federated connector"""
|
||||
federated_connector = fetch_federated_connector_by_id(id, db_session)
|
||||
if not federated_connector:
|
||||
raise HTTPException(status_code=404, detail="Federated connector not found")
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
|
||||
if federated_connector.credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Federated connector has no credentials"
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR, "Federated connector has no credentials"
|
||||
)
|
||||
|
||||
# Get OAuth token information for the current user
|
||||
@@ -581,14 +584,14 @@ def update_federated_connector_endpoint(
|
||||
)
|
||||
|
||||
if not updated_connector:
|
||||
raise HTTPException(status_code=404, detail="Federated connector not found")
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
|
||||
|
||||
# Return updated connector details
|
||||
return get_federated_connector_detail(id, user, db_session)
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Validation error updating federated connector {id}: {e}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
@@ -604,7 +607,7 @@ def delete_federated_connector_endpoint(
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Federated connector not found")
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
|
||||
|
||||
return True
|
||||
|
||||
@@ -619,7 +622,7 @@ def disconnect_oauth_token(
|
||||
# Check if the federated connector exists
|
||||
federated_connector = fetch_federated_connector_by_id(id, db_session)
|
||||
if not federated_connector:
|
||||
raise HTTPException(status_code=404, detail="Federated connector not found")
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Federated connector not found")
|
||||
|
||||
# Find and delete the user's OAuth token
|
||||
oauth_token = None
|
||||
@@ -633,6 +636,4 @@ def disconnect_oauth_token(
|
||||
db_session.commit()
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=404, detail="No OAuth token found for this user"
|
||||
)
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "No OAuth token found for this user")
|
||||
|
||||
@@ -6,7 +6,6 @@ from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Query
|
||||
from fastapi import Request
|
||||
from fastapi import Response
|
||||
@@ -60,6 +59,8 @@ from onyx.db.persona import get_persona_by_id
|
||||
from onyx.db.usage import increment_usage
|
||||
from onyx.db.usage import UsageType
|
||||
from onyx.db.user_file import get_file_id_by_user_file_id
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.file_store.file_store import get_default_file_store
|
||||
from onyx.llm.constants import LlmProviderNames
|
||||
from onyx.llm.factory import get_default_llm
|
||||
@@ -159,7 +160,9 @@ def get_user_chat_sessions(
|
||||
datetime.datetime.fromisoformat(before) if before is not None else None
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=422, detail="Invalid 'before' timestamp format")
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR, "Invalid 'before' timestamp format"
|
||||
)
|
||||
|
||||
try:
|
||||
# Fetch one extra to determine if there are more results
|
||||
@@ -216,8 +219,8 @@ def update_chat_session_temperature(
|
||||
update_thread_req.temperature_override < 0
|
||||
or update_thread_req.temperature_override > 2
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Temperature must be between 0 and 2"
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR, "Temperature must be between 0 and 2"
|
||||
)
|
||||
|
||||
# Additional check for Anthropic models
|
||||
@@ -227,9 +230,9 @@ def update_chat_session_temperature(
|
||||
in chat_session.current_alternate_model.lower()
|
||||
):
|
||||
if update_thread_req.temperature_override > 1:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Temperature for Anthropic models must be between 0 and 1",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"Temperature for Anthropic models must be between 0 and 1",
|
||||
)
|
||||
|
||||
chat_session.temperature_override = update_thread_req.temperature_override
|
||||
@@ -285,23 +288,25 @@ def get_chat_session(
|
||||
include_deleted=True,
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="Chat session not found")
|
||||
raise OnyxError(OnyxErrorCode.SESSION_NOT_FOUND, "Chat session not found")
|
||||
|
||||
if not include_deleted and existing_chat_session.deleted:
|
||||
raise HTTPException(status_code=404, detail="Chat session has been deleted")
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.SESSION_NOT_FOUND, "Chat session has been deleted"
|
||||
)
|
||||
|
||||
if is_shared:
|
||||
if existing_chat_session.shared_status != ChatSessionSharedStatus.PUBLIC:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Chat session is not shared"
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.UNAUTHORIZED, "Chat session is not shared"
|
||||
)
|
||||
elif user_id is not None and existing_chat_session.user_id not in (
|
||||
user_id,
|
||||
None,
|
||||
):
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHORIZED, "Access denied")
|
||||
|
||||
raise HTTPException(status_code=404, detail="Chat session not found")
|
||||
raise OnyxError(OnyxErrorCode.SESSION_NOT_FOUND, "Chat session not found")
|
||||
|
||||
# for chat-seeding: if the session is unassigned, assign it now. This is done here
|
||||
# to avoid another back and forth between FE -> BE before starting the first
|
||||
@@ -384,10 +389,10 @@ def create_new_chat_session(
|
||||
)
|
||||
except ValueError as e:
|
||||
# Project access denied
|
||||
raise HTTPException(status_code=403, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHORIZED, str(e))
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
raise HTTPException(status_code=400, detail="Invalid Persona provided.")
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Invalid Persona provided.")
|
||||
|
||||
return CreateChatSessionID(chat_session_id=new_chat_session.id)
|
||||
|
||||
@@ -486,7 +491,7 @@ def delete_all_chat_sessions(
|
||||
try:
|
||||
delete_all_chat_sessions_for_user(user=user, db_session=db_session)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
|
||||
|
||||
|
||||
@router.delete("/delete-chat-session/{session_id}", tags=PUBLIC_API_TAGS)
|
||||
@@ -506,7 +511,7 @@ def delete_chat_session_by_id(
|
||||
user_id, session_id, db_session, hard_delete=actual_hard_delete
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
|
||||
|
||||
|
||||
# NOTE: This endpoint is extremely central to the application, any changes to it should be reviewed and approved by an experienced
|
||||
@@ -711,7 +716,7 @@ def get_max_document_tokens(
|
||||
is_for_edit=False,
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="Persona not found")
|
||||
raise OnyxError(OnyxErrorCode.PERSONA_NOT_FOUND, "Persona not found")
|
||||
|
||||
return MaxSelectedDocumentTokens(
|
||||
max_tokens=_get_available_tokens_for_persona(
|
||||
@@ -743,10 +748,10 @@ def get_available_context_tokens_for_session(
|
||||
include_deleted=False,
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="Chat session not found")
|
||||
raise OnyxError(OnyxErrorCode.SESSION_NOT_FOUND, "Chat session not found")
|
||||
|
||||
if not chat_session.persona:
|
||||
raise HTTPException(status_code=400, detail="Chat session has no persona")
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "Chat session has no persona")
|
||||
|
||||
available = _get_available_tokens_for_persona(
|
||||
persona=chat_session.persona,
|
||||
@@ -808,7 +813,7 @@ def fetch_chat_file(
|
||||
file_store = get_default_file_store()
|
||||
file_record = file_store.read_file_record(file_id)
|
||||
if not file_record:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "File not found")
|
||||
|
||||
media_type = file_record.file_type
|
||||
file_io = file_store.read_file(file_id, mode="b")
|
||||
|
||||
@@ -6,7 +6,6 @@ from functools import lru_cache
|
||||
|
||||
from dateutil import tz
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -18,6 +17,8 @@ from onyx.db.models import ChatSession
|
||||
from onyx.db.models import TokenRateLimit
|
||||
from onyx.db.models import User
|
||||
from onyx.db.token_limit import fetch_all_global_token_rate_limits
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.utils.logger import setup_logger
|
||||
from onyx.utils.variable_functionality import fetch_versioned_implementation
|
||||
|
||||
@@ -62,9 +63,9 @@ def _user_is_rate_limited_by_global() -> None:
|
||||
global_usage = _fetch_global_usage(global_cutoff_time, db_session)
|
||||
|
||||
if _is_rate_limited(global_rate_limits, global_usage):
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail="Token budget exceeded for organization. Try again later.",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.RATE_LIMITED,
|
||||
"Token budget exceeded for organization. Try again later.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,10 +7,8 @@ from urllib.parse import urlparse
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi import Response
|
||||
from fastapi import status
|
||||
from fastapi_users import exceptions
|
||||
from fastapi_users.authentication import Strategy
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth # type: ignore
|
||||
@@ -29,6 +27,8 @@ from onyx.db.auth import get_user_count
|
||||
from onyx.db.auth import get_user_db
|
||||
from onyx.db.engine.async_sql_engine import get_async_session_context_manager
|
||||
from onyx.db.models import User
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.utils.logger import setup_logger
|
||||
|
||||
|
||||
@@ -233,17 +233,17 @@ async def _process_saml_callback(
|
||||
"Error when processing SAML Response: %s %s"
|
||||
% (", ".join(errors), auth.get_last_error_reason())
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied. Failed to parse SAML Response.",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.UNAUTHENTICATED,
|
||||
"Access denied. Failed to parse SAML Response.",
|
||||
)
|
||||
|
||||
if not auth.is_authenticated():
|
||||
detail = "Access denied. User was not authenticated"
|
||||
logger.error(detail)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=detail,
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.UNAUTHENTICATED,
|
||||
detail,
|
||||
)
|
||||
|
||||
user_email: str | None = None
|
||||
@@ -273,9 +273,9 @@ async def _process_saml_callback(
|
||||
"Received SAML attributes without email: %s",
|
||||
list(attributes.keys()),
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=detail,
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.UNAUTHENTICATED,
|
||||
detail,
|
||||
)
|
||||
|
||||
user = await upsert_saml_user(email=user_email)
|
||||
|
||||
@@ -10,7 +10,9 @@ export const forgotPassword = async (email: string): Promise<void> => {
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
const errorMessage =
|
||||
error?.detail || "An error occurred during password reset.";
|
||||
error?.detail ||
|
||||
error?.message ||
|
||||
"An error occurred during password reset.";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
@@ -33,7 +35,9 @@ export const resetPassword = async (
|
||||
throw new Error(error.detail.reason || "Invalid password");
|
||||
}
|
||||
const errorMessage =
|
||||
error?.detail || "An error occurred during password reset.";
|
||||
error?.detail ||
|
||||
error?.message ||
|
||||
"An error occurred during password reset.";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,13 +107,18 @@ export default function EmailPasswordForm({
|
||||
if (!response.ok) {
|
||||
setIsWorking(false);
|
||||
|
||||
const errorDetail: any = (await response.json()).detail;
|
||||
const errorData: any = await response.json();
|
||||
const errorDetail: any = errorData.detail;
|
||||
let errorMsg: string = "Unknown error";
|
||||
if (typeof errorDetail === "object" && errorDetail.reason) {
|
||||
errorMsg = errorDetail.reason;
|
||||
} else if (errorDetail === "REGISTER_USER_ALREADY_EXISTS") {
|
||||
errorMsg =
|
||||
"An account already exists with the specified email.";
|
||||
} else if (typeof errorDetail === "string") {
|
||||
errorMsg = errorDetail;
|
||||
} else if (errorData.message) {
|
||||
errorMsg = errorData.message;
|
||||
}
|
||||
if (response.status === 429) {
|
||||
errorMsg = "Too many requests. Please try again later.";
|
||||
|
||||
@@ -46,7 +46,8 @@ export default function Verify({ user }: VerifyProps) {
|
||||
} else {
|
||||
let errorDetail = "unknown error";
|
||||
try {
|
||||
errorDetail = (await response.json()).detail;
|
||||
const errorData = await response.json();
|
||||
errorDetail = errorData.detail || errorData.message || "unknown error";
|
||||
} catch (e) {
|
||||
console.error("Failed to parse verification error response:", e);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ export function RequestNewVerificationEmail({
|
||||
if (response.ok) {
|
||||
toast.success("A new verification email has been sent!");
|
||||
} else {
|
||||
const errorDetail = (await response.json()).detail;
|
||||
const errorData = await response.json();
|
||||
const errorDetail =
|
||||
errorData.detail || errorData.message || "Unknown error";
|
||||
toast.error(
|
||||
`Failed to send a new verification email - ${errorDetail}`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user