Compare commits

...

32 Commits

Author SHA1 Message Date
Justin Tahara
41f2bd2f19 chore(edge): Skip edge tag (#10019) 2026-04-09 00:56:51 +00:00
Jamison Lahman
bfa2f672f9 fix: /api/admin/llm/built-in/options/custom 404 (#10009) 2026-04-08 17:47:13 -07:00
Justin Tahara
a823c3ead1 chore(ods): Bump from v0.7.2 -> v0.7.3 (#10018) 2026-04-09 00:30:22 +00:00
Justin Tahara
bd7d378a9a chore(python sandbox): Bump to v0.3.3 (#10016) 2026-04-09 00:10:19 +00:00
Justin Tahara
dcec0c8ef3 feat(ods): Ad Hoc Deploys (#10014) 2026-04-08 23:54:57 +00:00
Raunak Bhagat
6456b51dcf feat: @opal/logos (#10002) 2026-04-08 16:48:11 -07:00
Bo-Onyx
7cfe27e31e feat(metrics): add pruning-specific Prometheus metrics (#9983) 2026-04-08 22:18:32 +00:00
Jamison Lahman
3c5f77f5a4 fix: fetch Custom Models provider names (#10004) 2026-04-08 14:22:42 -07:00
Jamison Lahman
ab4d1dce01 fix: Custom LLM Provider requires a Provider Name (#10003) 2026-04-08 20:33:43 +00:00
Raunak Bhagat
80c928eb58 fix: enable force-delete for last LLM provider (#9998) 2026-04-08 20:09:38 +00:00
Raunak Bhagat
77528876b1 chore: delete unused files (#10001) 2026-04-08 19:53:47 +00:00
Raunak Bhagat
3bf53495f3 refactor: foldable model list in ModelSelectionField (#9996) 2026-04-08 18:32:58 +00:00
Wenxi
e4cfcda0bf fix: initialize tracing in Slack bot service (#9993)
Co-authored-by: Adam Serafin <aserafin@match-trade.com>
2026-04-08 17:46:56 +00:00
Raunak Bhagat
475e8f6cdc refactor: remove auto-refresh from LLM provider model selection (#9995) 2026-04-08 17:45:19 +00:00
Raunak Bhagat
945272c1d2 fix: LM Studio API key field mismatch (#9991) 2026-04-08 09:52:15 -07:00
Raunak Bhagat
185b057483 fix: onboarding LLM Provider configuration fixes (#9972) 2026-04-08 08:35:36 -07:00
SubashMohan
ac89b42b38 fix(auth): migrate limited-role checks to account-type based access control (#9930) 2026-04-08 16:27:18 +05:30
Justin Tahara
e19198f1f2 chore(mt): reduce cleanup-idle-sandboxes beat cadence (#9984) 2026-04-08 02:29:21 +00:00
Bo-Onyx
45a4c5c28f feat(pruning): Add Wire Prometheus metrics into the Heavy Celery worker (#9982) 2026-04-08 00:37:30 +00:00
Nikolas Garza
7a3e7fad7a feat(chat): wire multi-model streaming into chat controller and UI (#9929) 2026-04-07 21:27:24 +00:00
Wenxi
3a8ba15c8d refactor(ollama): manual fetch and fix ollama cloud base url (#9973) 2026-04-07 20:22:02 +00:00
Jessica Singh
67b7d115db fix(fe): use Modal.Footer for token rate limit modal button (#9978) 2026-04-07 20:18:01 +00:00
Jamison Lahman
0e6759135f chore(docker): docker bake cache-from :edge images (#9976) 2026-04-07 19:51:38 +00:00
acaprau
a95e2fd99a fix(indexing, powerpoint files): Patch markitdown _convert_chart_to_markdown to no-op (#9970) 2026-04-07 19:51:06 +00:00
Justin Tahara
10ad7f92da chore(mt): Update cloud tasks (#9967) 2026-04-07 19:48:30 +00:00
Justin Tahara
f9f8f56ec1 fix(groups): Global Curator Permissions (#9974) 2026-04-07 19:44:07 +00:00
Jamison Lahman
91ed204f7a feat: generic OpenAI Compatible LLM Provider setup (#9968) 2026-04-07 19:17:57 +00:00
Nikolas Garza
e519490c85 docs(celery): add Prometheus metrics integration guide (#9969) 2026-04-07 19:15:13 +00:00
Nikolas Garza
93251cf558 feat(chat): add multi-model response panels (#9855) 2026-04-07 16:08:58 +00:00
Jamison Lahman
c31338e9b7 fix: stop falsely rejecting owner-password-only PDFs as protected (#9953)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 04:11:46 +00:00
Raunak Bhagat
1c32a83dc2 fix: replace React context hover tracking with pure CSS (#9961) 2026-04-06 20:57:36 -07:00
Raunak Bhagat
4a2ff7e0ef fix: a proper revamp of "Custom LLM Configuration Models" (#9958) 2026-04-07 03:27:41 +00:00
245 changed files with 8078 additions and 4968 deletions

View File

@@ -13,7 +13,7 @@ permissions:
id-token: write # zizmor: ignore[excessive-permissions]
env:
EDGE_TAG: ${{ startsWith(github.ref_name, 'nightly-latest') }}
EDGE_TAG: ${{ startsWith(github.ref_name, 'nightly-latest') || github.ref_name == 'edge' }}
jobs:
# Determine which components to build based on the tag
@@ -156,7 +156,7 @@ jobs:
check-version-tag:
runs-on: ubuntu-slim
timeout-minutes: 10
if: ${{ !startsWith(github.ref_name, 'nightly-latest') && github.event_name != 'workflow_dispatch' }}
if: ${{ !startsWith(github.ref_name, 'nightly-latest') && github.ref_name != 'edge' && github.event_name != 'workflow_dispatch' }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6

View File

@@ -10,9 +10,10 @@ from fastapi import status
from ee.onyx.configs.app_configs import SUPER_CLOUD_API_KEY
from ee.onyx.configs.app_configs import SUPER_USERS
from ee.onyx.server.seeding import get_seed_config
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import AUTH_TYPE
from onyx.configs.app_configs import USER_AUTH_SECRET
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.utils.logger import setup_logger
@@ -39,7 +40,7 @@ def get_default_admin_user_emails_() -> list[str]:
async def current_cloud_superuser(
request: Request,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> User:
api_key = request.headers.get("Authorization", "").replace("Bearer ", "")
if api_key != SUPER_CLOUD_API_KEY:

View File

@@ -5,6 +5,7 @@ from celery import Task
from celery.exceptions import SoftTimeLimitExceeded
from redis.lock import Lock as RedisLock
from ee.onyx.server.tenants.product_gating import get_gated_tenants
from onyx.background.celery.apps.app_base import task_logger
from onyx.background.celery.tasks.beat_schedule import BEAT_EXPIRES_DEFAULT
from onyx.configs.constants import CELERY_GENERIC_BEAT_LOCK_TIMEOUT
@@ -30,6 +31,7 @@ def cloud_beat_task_generator(
queue: str = OnyxCeleryTask.DEFAULT,
priority: int = OnyxCeleryPriority.MEDIUM,
expires: int = BEAT_EXPIRES_DEFAULT,
skip_gated: bool = True,
) -> bool | None:
"""a lightweight task used to kick off individual beat tasks per tenant."""
time_start = time.monotonic()
@@ -48,20 +50,22 @@ def cloud_beat_task_generator(
last_lock_time = time.monotonic()
tenant_ids: list[str] = []
num_processed_tenants = 0
num_skipped_gated = 0
try:
tenant_ids = get_all_tenant_ids()
# NOTE: for now, we are running tasks for gated tenants, since we want to allow
# connector deletion to run successfully. The new plan is to continously prune
# the gated tenants set, so we won't have a build up of old, unused gated tenants.
# Keeping this around in case we want to revert to the previous behavior.
# gated_tenants = get_gated_tenants()
# Per-task control over whether gated tenants are included. Most periodic tasks
# do no useful work on gated tenants and just waste DB connections fanning out
# to ~10k+ inactive tenants. A small number of cleanup tasks (connector deletion,
# checkpoint/index attempt cleanup) need to run on gated tenants and pass
# `skip_gated=False` from the beat schedule.
gated_tenants: set[str] = get_gated_tenants() if skip_gated else set()
for tenant_id in tenant_ids:
# Same comment here as the above NOTE
# if tenant_id in gated_tenants:
# continue
if tenant_id in gated_tenants:
num_skipped_gated += 1
continue
current_time = time.monotonic()
if current_time - last_lock_time >= (CELERY_GENERIC_BEAT_LOCK_TIMEOUT / 4):
@@ -104,6 +108,7 @@ def cloud_beat_task_generator(
f"cloud_beat_task_generator finished: "
f"task={task_name} "
f"num_processed_tenants={num_processed_tenants} "
f"num_skipped_gated={num_skipped_gated} "
f"num_tenants={len(tenant_ids)} "
f"elapsed={time_elapsed:.2f}"
)

View File

@@ -39,6 +39,7 @@ from onyx.db.models import User__UserGroup
from onyx.db.models import UserGroup
from onyx.db.models import UserGroup__ConnectorCredentialPair
from onyx.db.models import UserRole
from onyx.db.permissions import recompute_permissions_for_group__no_commit
from onyx.db.permissions import recompute_user_permissions__no_commit
from onyx.db.users import fetch_user_by_id
from onyx.utils.logger import setup_logger
@@ -952,3 +953,46 @@ def delete_user_group_cc_pair_relationship__no_commit(
UserGroup__ConnectorCredentialPair.cc_pair_id == cc_pair_id,
)
db_session.execute(delete_stmt)
def set_group_permission__no_commit(
group_id: int,
permission: Permission,
enabled: bool,
granted_by: UUID,
db_session: Session,
) -> None:
"""Grant or revoke a single permission for a group using soft-delete.
Does NOT commit — caller must commit the session.
"""
existing = db_session.execute(
select(PermissionGrant)
.where(
PermissionGrant.group_id == group_id,
PermissionGrant.permission == permission,
)
.with_for_update()
).scalar_one_or_none()
if enabled:
if existing is not None:
if existing.is_deleted:
existing.is_deleted = False
existing.granted_by = granted_by
existing.granted_at = func.now()
else:
db_session.add(
PermissionGrant(
group_id=group_id,
permission=permission,
grant_source=GrantSource.USER,
granted_by=granted_by,
)
)
else:
if existing is not None and not existing.is_deleted:
existing.is_deleted = True
db_session.flush()
recompute_permissions_for_group__no_commit(group_id, db_session)

View File

@@ -155,7 +155,7 @@ def get_application() -> FastAPI:
include_router_with_global_prefix_prepended(application, license_router)
# Unified billing API - always registered in EE.
# Each endpoint is protected by the `current_admin_user` dependency (admin auth).
# Each endpoint is protected by admin permission checks.
include_router_with_global_prefix_prepended(application, billing_router)
if MULTI_TENANT:

View File

@@ -17,10 +17,10 @@ from ee.onyx.db.analytics import fetch_persona_message_analytics
from ee.onyx.db.analytics import fetch_persona_unique_users
from ee.onyx.db.analytics import fetch_query_analytics
from ee.onyx.db.analytics import user_can_view_assistant_stats
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
router = APIRouter(prefix="/analytics", tags=PUBLIC_API_TAGS)
@@ -40,7 +40,7 @@ class QueryAnalyticsResponse(BaseModel):
def get_query_analytics(
start: datetime.datetime | None = None,
end: datetime.datetime | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[QueryAnalyticsResponse]:
daily_query_usage_info = fetch_query_analytics(
@@ -71,7 +71,7 @@ class UserAnalyticsResponse(BaseModel):
def get_user_analytics(
start: datetime.datetime | None = None,
end: datetime.datetime | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[UserAnalyticsResponse]:
daily_query_usage_info_per_user = fetch_per_user_query_analytics(
@@ -105,7 +105,7 @@ class OnyxbotAnalyticsResponse(BaseModel):
def get_onyxbot_analytics(
start: datetime.datetime | None = None,
end: datetime.datetime | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[OnyxbotAnalyticsResponse]:
daily_onyxbot_info = fetch_onyxbot_analytics(
@@ -141,7 +141,7 @@ def get_persona_messages(
persona_id: int,
start: datetime.datetime | None = None,
end: datetime.datetime | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[PersonaMessageAnalyticsResponse]:
"""Fetch daily message counts for a single persona within the given time range."""
@@ -179,7 +179,7 @@ def get_persona_unique_users(
persona_id: int,
start: datetime.datetime,
end: datetime.datetime,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[PersonaUniqueUsersResponse]:
"""Get unique users per day for a single persona."""
@@ -218,7 +218,7 @@ def get_assistant_stats(
assistant_id: int,
start: datetime.datetime | None = None,
end: datetime.datetime | None = None,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> AssistantStatsResponse:
"""

View File

@@ -29,7 +29,6 @@ from fastapi import Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ee.onyx.auth.users import current_admin_user
from ee.onyx.db.license import get_license
from ee.onyx.db.license import get_used_seats
from ee.onyx.server.billing.models import BillingInformationResponse
@@ -51,11 +50,13 @@ from ee.onyx.server.billing.service import (
get_billing_information as get_billing_service,
)
from ee.onyx.server.billing.service import update_seat_count as update_seat_service
from onyx.auth.permissions import require_permission
from onyx.auth.users import User
from onyx.configs.app_configs import STRIPE_PUBLISHABLE_KEY_OVERRIDE
from onyx.configs.app_configs import STRIPE_PUBLISHABLE_KEY_URL
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.redis.redis_pool import get_shared_redis_client
@@ -147,7 +148,7 @@ def _get_tenant_id() -> str | None:
@router.post("/create-checkout-session")
async def create_checkout_session(
request: CreateCheckoutSessionRequest | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> CreateCheckoutSessionResponse:
"""Create a Stripe checkout session for new subscription or renewal.
@@ -191,7 +192,7 @@ async def create_checkout_session(
@router.post("/create-customer-portal-session")
async def create_customer_portal_session(
request: CreateCustomerPortalSessionRequest | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> CreateCustomerPortalSessionResponse:
"""Create a Stripe customer portal session for managing subscription.
@@ -216,7 +217,7 @@ async def create_customer_portal_session(
@router.get("/billing-information")
async def get_billing_information(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> BillingInformationResponse | SubscriptionStatusResponse:
"""Get billing information for the current subscription.
@@ -258,7 +259,7 @@ async def get_billing_information(
@router.post("/seats/update")
async def update_seats(
request: SeatUpdateRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> SeatUpdateResponse:
"""Update the seat count for the current subscription.
@@ -364,7 +365,7 @@ class ResetConnectionResponse(BaseModel):
@router.post("/reset-connection")
async def reset_stripe_connection(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> ResetConnectionResponse:
"""Reset the Stripe connection circuit breaker.

View File

@@ -27,11 +27,12 @@ from ee.onyx.server.scim.auth import generate_scim_token
from ee.onyx.server.scim.models import ScimTokenCreate
from ee.onyx.server.scim.models import ScimTokenCreatedResponse
from ee.onyx.server.scim.models import ScimTokenResponse
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_user_with_expired_token
from onyx.auth.users import get_user_manager
from onyx.auth.users import UserManager
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.file_store.file_store import get_default_file_store
from onyx.server.utils import BasicAuthenticationError
@@ -120,7 +121,8 @@ async def refresh_access_token(
@admin_router.put("")
def admin_ee_put_settings(
settings: EnterpriseSettings, _: User = Depends(current_admin_user)
settings: EnterpriseSettings,
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
store_settings(settings)
@@ -139,7 +141,7 @@ def ee_fetch_settings() -> EnterpriseSettings:
def put_logo(
file: UploadFile,
is_logotype: bool = False,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
upload_logo(file=file, is_logotype=is_logotype)
@@ -196,7 +198,8 @@ def fetch_logo(
@admin_router.put("/custom-analytics-script")
def upload_custom_analytics_script(
script_upload: AnalyticsScriptUpload, _: User = Depends(current_admin_user)
script_upload: AnalyticsScriptUpload,
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
try:
store_analytics_script(script_upload)
@@ -220,7 +223,7 @@ def _get_scim_dal(db_session: Session = Depends(get_session)) -> ScimDAL:
@admin_router.get("/scim/token")
def get_active_scim_token(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
dal: ScimDAL = Depends(_get_scim_dal),
) -> ScimTokenResponse:
"""Return the currently active SCIM token's metadata, or 404 if none."""
@@ -250,7 +253,7 @@ def get_active_scim_token(
@admin_router.post("/scim/token", status_code=201)
def create_scim_token(
body: ScimTokenCreate,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
dal: ScimDAL = Depends(_get_scim_dal),
) -> ScimTokenCreatedResponse:
"""Create a new SCIM bearer token.

View File

@@ -4,12 +4,13 @@ from fastapi import Depends
from fastapi import Query
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import User
from onyx.db.constants import UNSET
from onyx.db.constants import UnsetType
from onyx.db.engine.sql_engine import get_session
from onyx.db.engine.sql_engine import get_session_with_current_tenant
from onyx.db.enums import Permission
from onyx.db.hook import create_hook__no_commit
from onyx.db.hook import delete_hook__no_commit
from onyx.db.hook import get_hook_by_id
@@ -178,7 +179,7 @@ router = APIRouter(prefix="/admin/hooks")
@router.get("/specs")
def get_hook_point_specs(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
_hook_enabled: None = Depends(require_hook_enabled),
) -> list[HookPointMetaResponse]:
return [
@@ -199,7 +200,7 @@ def get_hook_point_specs(
@router.get("")
def list_hooks(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
_hook_enabled: None = Depends(require_hook_enabled),
db_session: Session = Depends(get_session),
) -> list[HookResponse]:
@@ -210,7 +211,7 @@ def list_hooks(
@router.post("")
def create_hook(
req: HookCreateRequest,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
_hook_enabled: None = Depends(require_hook_enabled),
db_session: Session = Depends(get_session),
) -> HookResponse:
@@ -246,7 +247,7 @@ def create_hook(
@router.get("/{hook_id}")
def get_hook(
hook_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
_hook_enabled: None = Depends(require_hook_enabled),
db_session: Session = Depends(get_session),
) -> HookResponse:
@@ -258,7 +259,7 @@ def get_hook(
def update_hook(
hook_id: int,
req: HookUpdateRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
_hook_enabled: None = Depends(require_hook_enabled),
db_session: Session = Depends(get_session),
) -> HookResponse:
@@ -328,7 +329,7 @@ def update_hook(
@router.delete("/{hook_id}")
def delete_hook(
hook_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
_hook_enabled: None = Depends(require_hook_enabled),
db_session: Session = Depends(get_session),
) -> None:
@@ -339,7 +340,7 @@ def delete_hook(
@router.post("/{hook_id}/activate")
def activate_hook(
hook_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
_hook_enabled: None = Depends(require_hook_enabled),
db_session: Session = Depends(get_session),
) -> HookResponse:
@@ -381,7 +382,7 @@ def activate_hook(
@router.post("/{hook_id}/validate")
def validate_hook(
hook_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
_hook_enabled: None = Depends(require_hook_enabled),
db_session: Session = Depends(get_session),
) -> HookValidateResponse:
@@ -409,7 +410,7 @@ def validate_hook(
@router.post("/{hook_id}/deactivate")
def deactivate_hook(
hook_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
_hook_enabled: None = Depends(require_hook_enabled),
db_session: Session = Depends(get_session),
) -> HookResponse:
@@ -432,7 +433,7 @@ def deactivate_hook(
def list_hook_execution_logs(
hook_id: int,
limit: int = Query(default=10, ge=1, le=100),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
_hook_enabled: None = Depends(require_hook_enabled),
db_session: Session = Depends(get_session),
) -> list[HookExecutionRecord]:

View File

@@ -17,7 +17,6 @@ from fastapi import File
from fastapi import UploadFile
from sqlalchemy.orm import Session
from ee.onyx.auth.users import current_admin_user
from ee.onyx.configs.app_configs import CLOUD_DATA_PLANE_URL
from ee.onyx.db.license import delete_license as db_delete_license
from ee.onyx.db.license import get_license
@@ -32,8 +31,10 @@ from ee.onyx.server.license.models import LicenseStatusResponse
from ee.onyx.server.license.models import LicenseUploadResponse
from ee.onyx.server.license.models import SeatUsageResponse
from ee.onyx.utils.license import verify_license_signature
from onyx.auth.permissions import require_permission
from onyx.auth.users import User
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
@@ -60,7 +61,7 @@ def _strip_pem_delimiters(content: str) -> str:
@router.get("")
async def get_license_status(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> LicenseStatusResponse:
"""Get current license status and seat usage."""
@@ -84,7 +85,7 @@ async def get_license_status(
@router.get("/seats")
async def get_seat_usage(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> SeatUsageResponse:
"""Get detailed seat usage information."""
@@ -107,7 +108,7 @@ async def get_seat_usage(
@router.post("/claim")
async def claim_license(
session_id: str | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> LicenseResponse:
"""
@@ -215,7 +216,7 @@ async def claim_license(
@router.post("/upload")
async def upload_license(
license_file: UploadFile = File(...),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> LicenseUploadResponse:
"""
@@ -263,7 +264,7 @@ async def upload_license(
@router.post("/refresh")
async def refresh_license_cache_endpoint(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> LicenseStatusResponse:
"""
@@ -292,7 +293,7 @@ async def refresh_license_cache_endpoint(
@router.delete("")
async def delete_license(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, bool]:
"""

View File

@@ -12,8 +12,9 @@ from ee.onyx.db.standard_answer import insert_standard_answer_category
from ee.onyx.db.standard_answer import remove_standard_answer
from ee.onyx.db.standard_answer import update_standard_answer
from ee.onyx.db.standard_answer import update_standard_answer_category
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.server.manage.models import StandardAnswer
from onyx.server.manage.models import StandardAnswerCategory
@@ -27,7 +28,7 @@ router = APIRouter(prefix="/manage")
def create_standard_answer(
standard_answer_creation_request: StandardAnswerCreationRequest,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> StandardAnswer:
standard_answer_model = insert_standard_answer(
keyword=standard_answer_creation_request.keyword,
@@ -43,7 +44,7 @@ def create_standard_answer(
@router.get("/admin/standard-answer")
def list_standard_answers(
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> list[StandardAnswer]:
standard_answer_models = fetch_standard_answers(db_session=db_session)
return [
@@ -57,7 +58,7 @@ def patch_standard_answer(
standard_answer_id: int,
standard_answer_creation_request: StandardAnswerCreationRequest,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> StandardAnswer:
existing_standard_answer = fetch_standard_answer(
standard_answer_id=standard_answer_id,
@@ -83,7 +84,7 @@ def patch_standard_answer(
def delete_standard_answer(
standard_answer_id: int,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
return remove_standard_answer(
standard_answer_id=standard_answer_id,
@@ -95,7 +96,7 @@ def delete_standard_answer(
def create_standard_answer_category(
standard_answer_category_creation_request: StandardAnswerCategoryCreationRequest,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> StandardAnswerCategory:
standard_answer_category_model = insert_standard_answer_category(
category_name=standard_answer_category_creation_request.name,
@@ -107,7 +108,7 @@ def create_standard_answer_category(
@router.get("/admin/standard-answer/category")
def list_standard_answer_categories(
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> list[StandardAnswerCategory]:
standard_answer_category_models = fetch_standard_answer_categories(
db_session=db_session
@@ -123,7 +124,7 @@ def patch_standard_answer_category(
standard_answer_category_id: int,
standard_answer_category_creation_request: StandardAnswerCategoryCreationRequest,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> StandardAnswerCategory:
existing_standard_answer_category = fetch_standard_answer_category(
standard_answer_category_id=standard_answer_category_id,

View File

@@ -9,9 +9,10 @@ from ee.onyx.server.oauth.api_router import router
from ee.onyx.server.oauth.confluence_cloud import ConfluenceCloudOAuth
from ee.onyx.server.oauth.google_drive import GoogleDriveOAuth
from ee.onyx.server.oauth.slack import SlackOAuth
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import DEV_MODE
from onyx.configs.constants import DocumentSource
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.redis.redis_pool import get_redis_client
from onyx.utils.logger import setup_logger
@@ -24,7 +25,7 @@ logger = setup_logger()
def prepare_authorization_request(
connector: DocumentSource,
redirect_on_success: str | None,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
tenant_id: str | None = Depends(get_current_tenant_id),
) -> JSONResponse:
"""Used by the frontend to generate the url for the user's browser during auth request.

View File

@@ -15,7 +15,7 @@ from pydantic import ValidationError
from sqlalchemy.orm import Session
from ee.onyx.server.oauth.api_router import router
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import DEV_MODE
from onyx.configs.app_configs import OAUTH_CONFLUENCE_CLOUD_CLIENT_ID
from onyx.configs.app_configs import OAUTH_CONFLUENCE_CLOUD_CLIENT_SECRET
@@ -26,6 +26,7 @@ from onyx.db.credentials import create_credential
from onyx.db.credentials import fetch_credential_by_id_for_user
from onyx.db.credentials import update_credential_json
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.redis.redis_pool import get_redis_client
from onyx.server.documents.models import CredentialBase
@@ -146,7 +147,7 @@ class ConfluenceCloudOAuth:
def confluence_oauth_callback(
code: str,
state: str,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
tenant_id: str | None = Depends(get_current_tenant_id),
) -> JSONResponse:
@@ -258,7 +259,7 @@ def confluence_oauth_callback(
@router.get("/connector/confluence/accessible-resources")
def confluence_oauth_accessible_resources(
credential_id: int,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
tenant_id: str | None = Depends(get_current_tenant_id), # noqa: ARG001
) -> JSONResponse:
@@ -325,7 +326,7 @@ def confluence_oauth_finalize(
cloud_id: str,
cloud_name: str,
cloud_url: str,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
tenant_id: str | None = Depends(get_current_tenant_id), # noqa: ARG001
) -> JSONResponse:

View File

@@ -12,7 +12,7 @@ from pydantic import BaseModel
from sqlalchemy.orm import Session
from ee.onyx.server.oauth.api_router import router
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import DEV_MODE
from onyx.configs.app_configs import OAUTH_GOOGLE_DRIVE_CLIENT_ID
from onyx.configs.app_configs import OAUTH_GOOGLE_DRIVE_CLIENT_SECRET
@@ -34,6 +34,7 @@ from onyx.connectors.google_utils.shared_constants import (
)
from onyx.db.credentials import create_credential
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.redis.redis_pool import get_redis_client
from onyx.server.documents.models import CredentialBase
@@ -114,7 +115,7 @@ class GoogleDriveOAuth:
def handle_google_drive_oauth_callback(
code: str,
state: str,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
tenant_id: str | None = Depends(get_current_tenant_id),
) -> JSONResponse:

View File

@@ -10,7 +10,7 @@ from pydantic import BaseModel
from sqlalchemy.orm import Session
from ee.onyx.server.oauth.api_router import router
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import DEV_MODE
from onyx.configs.app_configs import OAUTH_SLACK_CLIENT_ID
from onyx.configs.app_configs import OAUTH_SLACK_CLIENT_SECRET
@@ -18,6 +18,7 @@ from onyx.configs.app_configs import WEB_DOMAIN
from onyx.configs.constants import DocumentSource
from onyx.db.credentials import create_credential
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.redis.redis_pool import get_redis_client
from onyx.server.documents.models import CredentialBase
@@ -98,7 +99,7 @@ class SlackOAuth:
def handle_slack_oauth_callback(
code: str,
state: str,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
tenant_id: str | None = Depends(get_current_tenant_id),
) -> JSONResponse:

View File

@@ -8,8 +8,9 @@ from ee.onyx.onyxbot.slack.handlers.handle_standard_answers import (
)
from ee.onyx.server.query_and_chat.models import StandardAnswerRequest
from ee.onyx.server.query_and_chat.models import StandardAnswerResponse
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.utils.logger import setup_logger
@@ -22,7 +23,7 @@ basic_router = APIRouter(prefix="/query")
def get_standard_answer(
request: StandardAnswerRequest,
db_session: Session = Depends(get_session),
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> StandardAnswerResponse:
try:
standard_answers = oneoff_standard_answers(

View File

@@ -19,10 +19,11 @@ from ee.onyx.server.query_and_chat.models import SearchHistoryResponse
from ee.onyx.server.query_and_chat.models import SearchQueryResponse
from ee.onyx.server.query_and_chat.models import SendSearchQueryRequest
from ee.onyx.server.query_and_chat.streaming_models import SearchErrorPacket
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import ONYX_SEARCH_UI_USES_OPENSEARCH_KEYWORD_SEARCH
from onyx.db.engine.sql_engine import get_session
from onyx.db.engine.sql_engine import get_session_with_current_tenant
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.llm.factory import get_default_llm
from onyx.server.usage_limits import check_llm_cost_limit_for_provider
@@ -39,7 +40,7 @@ router = APIRouter(prefix="/search")
@router.post("/search-flow-classification")
def search_flow_classification(
request: SearchFlowClassificationRequest,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> SearchFlowClassificationResponse:
query = request.user_query
@@ -79,7 +80,7 @@ def search_flow_classification(
)
def handle_send_search_message(
request: SendSearchQueryRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> StreamingResponse | SearchFullResponse:
"""
@@ -129,7 +130,7 @@ def handle_send_search_message(
def get_search_history(
limit: int = 100,
filter_days: int | None = None,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> SearchHistoryResponse:
"""

View File

@@ -20,7 +20,7 @@ from ee.onyx.server.query_history.models import ChatSessionMinimal
from ee.onyx.server.query_history.models import ChatSessionSnapshot
from ee.onyx.server.query_history.models import MessageSnapshot
from ee.onyx.server.query_history.models import QueryHistoryExport
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import get_display_email
from onyx.background.celery.versioned_apps.client import app as client_app
from onyx.background.task_utils import construct_query_history_report_name
@@ -39,6 +39,7 @@ from onyx.configs.constants import SessionType
from onyx.db.chat import get_chat_session_by_id
from onyx.db.chat import get_chat_sessions_by_user
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.enums import TaskStatus
from onyx.db.file_record import get_query_history_export_files
from onyx.db.models import ChatSession
@@ -153,7 +154,7 @@ def snapshot_from_chat_session(
@router.get("/admin/chat-sessions")
def admin_get_chat_sessions(
user_id: UUID,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> ChatSessionsResponse:
# we specifically don't allow this endpoint if "anonymized" since
@@ -196,7 +197,7 @@ def get_chat_session_history(
feedback_type: QAFeedbackType | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> PaginatedReturn[ChatSessionMinimal]:
ensure_query_history_is_enabled(disallowed=[QueryHistoryType.DISABLED])
@@ -234,7 +235,7 @@ def get_chat_session_history(
@router.get("/admin/chat-session-history/{chat_session_id}")
def get_chat_session_admin(
chat_session_id: UUID,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> ChatSessionSnapshot:
ensure_query_history_is_enabled(disallowed=[QueryHistoryType.DISABLED])
@@ -269,7 +270,7 @@ def get_chat_session_admin(
@router.get("/admin/query-history/list")
def list_all_query_history_exports(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[QueryHistoryExport]:
ensure_query_history_is_enabled(disallowed=[QueryHistoryType.DISABLED])
@@ -297,7 +298,7 @@ def list_all_query_history_exports(
@router.post("/admin/query-history/start-export", tags=PUBLIC_API_TAGS)
def start_query_history_export(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
start: datetime | None = None,
end: datetime | None = None,
@@ -344,7 +345,7 @@ def start_query_history_export(
@router.get("/admin/query-history/export-status", tags=PUBLIC_API_TAGS)
def get_query_history_export_status(
request_id: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, str]:
ensure_query_history_is_enabled(disallowed=[QueryHistoryType.DISABLED])
@@ -378,7 +379,7 @@ def get_query_history_export_status(
@router.get("/admin/query-history/download", tags=PUBLIC_API_TAGS)
def download_query_history_csv(
request_id: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> StreamingResponse:
ensure_query_history_is_enabled(disallowed=[QueryHistoryType.DISABLED])

View File

@@ -12,10 +12,11 @@ from sqlalchemy.orm import Session
from ee.onyx.db.usage_export import get_all_usage_reports
from ee.onyx.db.usage_export import get_usage_report_data
from ee.onyx.db.usage_export import UsageReportMetadata
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.background.celery.versioned_apps.client import app as client_app
from onyx.configs.constants import OnyxCeleryTask
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.file_store.constants import STANDARD_CHUNK_SIZE
from shared_configs.contextvars import get_current_tenant_id
@@ -31,7 +32,7 @@ class GenerateUsageReportParams(BaseModel):
@router.post("/admin/usage-report", status_code=204)
def generate_report(
params: GenerateUsageReportParams,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
# Validate period parameters
if params.period_from and params.period_to:
@@ -58,7 +59,7 @@ def generate_report(
@router.get("/admin/usage-report/{report_name}")
def read_usage_report(
report_name: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session), # noqa: ARG001
) -> Response:
try:
@@ -82,7 +83,7 @@ def read_usage_report(
@router.get("/admin/usage-report")
def fetch_usage_reports(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[UsageReportMetadata]:
try:

View File

@@ -12,12 +12,13 @@ from ee.onyx.server.tenants.anonymous_user_path import (
from ee.onyx.server.tenants.anonymous_user_path import modify_anonymous_user_path
from ee.onyx.server.tenants.anonymous_user_path import validate_anonymous_user_path
from ee.onyx.server.tenants.models import AnonymousUserPath
from onyx.auth.permissions import require_permission
from onyx.auth.users import anonymous_user_enabled
from onyx.auth.users import current_admin_user
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.db.enums import Permission
from onyx.utils.logger import setup_logger
from shared_configs.contextvars import get_current_tenant_id
@@ -28,7 +29,7 @@ router = APIRouter(prefix="/tenants")
@router.get("/anonymous-user-path")
async def get_anonymous_user_path_api(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> AnonymousUserPath:
tenant_id = get_current_tenant_id()
@@ -44,7 +45,7 @@ async def get_anonymous_user_path_api(
@router.post("/anonymous-user-path")
async def set_anonymous_user_path_api(
anonymous_user_path: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
tenant_id = get_current_tenant_id()
try:

View File

@@ -22,7 +22,6 @@ import httpx
from fastapi import APIRouter
from fastapi import Depends
from ee.onyx.auth.users import current_admin_user
from ee.onyx.server.tenants.access import control_plane_dep
from ee.onyx.server.tenants.billing import fetch_billing_information
from ee.onyx.server.tenants.billing import fetch_customer_portal_session
@@ -38,10 +37,12 @@ from ee.onyx.server.tenants.models import SubscriptionSessionResponse
from ee.onyx.server.tenants.models import SubscriptionStatusResponse
from ee.onyx.server.tenants.product_gating import overwrite_full_gated_set
from ee.onyx.server.tenants.product_gating import store_product_gating
from onyx.auth.permissions import require_permission
from onyx.auth.users import User
from onyx.configs.app_configs import STRIPE_PUBLISHABLE_KEY_OVERRIDE
from onyx.configs.app_configs import STRIPE_PUBLISHABLE_KEY_URL
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.db.enums import Permission
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.utils.logger import setup_logger
@@ -99,7 +100,7 @@ def gate_product_full_sync(
@router.get("/billing-information")
async def billing_information(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> BillingInformation | SubscriptionStatusResponse:
logger.info("Fetching billing information")
tenant_id = get_current_tenant_id()
@@ -108,7 +109,7 @@ async def billing_information(
@router.post("/create-customer-portal-session")
async def create_customer_portal_session(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> dict:
"""Create a Stripe customer portal session via the control plane."""
tenant_id = get_current_tenant_id()
@@ -130,7 +131,7 @@ async def create_customer_portal_session(
@router.post("/create-checkout-session")
async def create_checkout_session(
request: CreateCheckoutSessionRequest | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> dict:
"""Create a Stripe checkout session via the control plane."""
tenant_id = get_current_tenant_id()
@@ -153,7 +154,7 @@ async def create_checkout_session(
@router.post("/create-subscription-session")
async def create_subscription_session(
request: CreateSubscriptionSessionRequest | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> SubscriptionSessionResponse:
try:
tenant_id = CURRENT_TENANT_ID_CONTEXTVAR.get()

View File

@@ -6,10 +6,11 @@ from sqlalchemy.orm import Session
from ee.onyx.server.tenants.provisioning import delete_user_from_control_plane
from ee.onyx.server.tenants.user_mapping import remove_all_users_from_tenant
from ee.onyx.server.tenants.user_mapping import remove_users_from_tenant
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import User
from onyx.db.auth import get_user_count
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.users import delete_user_from_db
from onyx.db.users import get_user_by_email
from onyx.server.manage.models import UserByEmail
@@ -24,7 +25,9 @@ router = APIRouter(prefix="/tenants")
@router.post("/leave-team")
async def leave_organization(
user_email: UserByEmail,
current_user: User = Depends(current_admin_user),
current_user: User = Depends(
require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)
),
db_session: Session = Depends(get_session),
) -> None:
tenant_id = get_current_tenant_id()

View File

@@ -3,8 +3,9 @@ from fastapi import Depends
from ee.onyx.server.tenants.models import TenantByDomainResponse
from ee.onyx.server.tenants.provisioning import get_tenant_by_domain_from_control_plane
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import User
from onyx.db.enums import Permission
from onyx.utils.logger import setup_logger
from shared_configs.contextvars import get_current_tenant_id
@@ -26,7 +27,7 @@ FORBIDDEN_COMMON_EMAIL_SUBSTRINGS = [
@router.get("/existing-team-by-domain")
def get_existing_tenant_by_domain(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> TenantByDomainResponse | None:
domain = user.email.split("@")[1]
if any(substring in domain for substring in FORBIDDEN_COMMON_EMAIL_SUBSTRINGS):

View File

@@ -10,9 +10,9 @@ from ee.onyx.server.tenants.user_mapping import approve_user_invite
from ee.onyx.server.tenants.user_mapping import deny_user_invite
from ee.onyx.server.tenants.user_mapping import invite_self_to_tenant
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.permissions import require_permission
from onyx.auth.users import User
from onyx.db.enums import Permission
from onyx.utils.logger import setup_logger
from shared_configs.contextvars import get_current_tenant_id
@@ -24,7 +24,7 @@ router = APIRouter(prefix="/tenants")
@router.post("/users/invite/request")
async def request_invite(
invite_request: RequestInviteRequest,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
try:
invite_self_to_tenant(user.email, invite_request.tenant_id)
@@ -37,7 +37,7 @@ async def request_invite(
@router.get("/users/pending")
def list_pending_users(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> list[PendingUserSnapshot]:
pending_emails = get_pending_users()
return [PendingUserSnapshot(email=email) for email in pending_emails]
@@ -46,7 +46,7 @@ def list_pending_users(
@router.post("/users/invite/approve")
async def approve_user(
approve_user_request: ApproveUserRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
tenant_id = get_current_tenant_id()
approve_user_invite(approve_user_request.email, tenant_id)
@@ -55,7 +55,7 @@ async def approve_user(
@router.post("/users/invite/accept")
async def accept_invite(
invite_request: RequestInviteRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> None:
"""
Accept an invitation to join a tenant.
@@ -70,7 +70,7 @@ async def accept_invite(
@router.post("/users/invite/deny")
async def deny_invite(
invite_request: RequestInviteRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> None:
"""
Deny an invitation to join a tenant.

View File

@@ -7,10 +7,11 @@ from sqlalchemy.orm import Session
from ee.onyx.db.token_limit import fetch_all_user_group_token_rate_limits_by_group
from ee.onyx.db.token_limit import fetch_user_group_token_rate_limits_for_user
from ee.onyx.db.token_limit import insert_user_group_token_rate_limit
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_curator_or_admin_user
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.token_limit import fetch_all_user_token_rate_limits
from onyx.db.token_limit import insert_user_token_rate_limit
@@ -28,7 +29,7 @@ Group Token Limit Settings
@router.get("/user-groups")
def get_all_group_token_limit_settings(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, list[TokenRateLimitDisplay]]:
user_groups_to_token_rate_limits = fetch_all_user_group_token_rate_limits_by_group(
@@ -64,7 +65,7 @@ def get_group_token_limit_settings(
def create_group_token_limit_settings(
group_id: int,
token_limit_settings: TokenRateLimitArgs,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> TokenRateLimitDisplay:
rate_limit_display = TokenRateLimitDisplay.from_db(
@@ -86,7 +87,7 @@ User Token Limit Settings
@router.get("/users")
def get_user_token_limit_settings(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[TokenRateLimitDisplay]:
return [
@@ -98,7 +99,7 @@ def get_user_token_limit_settings(
@router.post("/users")
def create_user_token_limit_settings(
token_limit_settings: TokenRateLimitArgs,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> TokenRateLimitDisplay:
rate_limit_display = TokenRateLimitDisplay.from_db(

View File

@@ -13,22 +13,26 @@ from ee.onyx.db.user_group import fetch_user_groups_for_user
from ee.onyx.db.user_group import insert_user_group
from ee.onyx.db.user_group import prepare_user_group_for_deletion
from ee.onyx.db.user_group import rename_user_group
from ee.onyx.db.user_group import set_group_permission__no_commit
from ee.onyx.db.user_group import update_user_curator_relationship
from ee.onyx.db.user_group import update_user_group
from ee.onyx.server.user_group.models import AddUsersToUserGroupRequest
from ee.onyx.server.user_group.models import MinimalUserGroupSnapshot
from ee.onyx.server.user_group.models import SetCuratorRequest
from ee.onyx.server.user_group.models import SetPermissionRequest
from ee.onyx.server.user_group.models import SetPermissionResponse
from ee.onyx.server.user_group.models import UpdateGroupAgentsRequest
from ee.onyx.server.user_group.models import UserGroup
from ee.onyx.server.user_group.models import UserGroupCreate
from ee.onyx.server.user_group.models import UserGroupRename
from ee.onyx.server.user_group.models import UserGroupUpdate
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import NON_TOGGLEABLE_PERMISSIONS
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.configs.app_configs import DISABLE_VECTOR_DB
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.models import UserRole
from onyx.db.persona import get_persona_by_id
@@ -68,7 +72,7 @@ def list_user_groups(
@router.get("/user-groups/minimal")
def list_minimal_user_groups(
include_default: bool = False,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[MinimalUserGroupSnapshot]:
if user.role == UserRole.ADMIN:
@@ -91,23 +95,50 @@ def list_minimal_user_groups(
@router.get("/admin/user-group/{user_group_id}/permissions")
def get_user_group_permissions(
user_group_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[str]:
) -> list[Permission]:
group = fetch_user_group(db_session, user_group_id)
if group is None:
raise OnyxError(OnyxErrorCode.NOT_FOUND, "User group not found")
return [
grant.permission.value
for grant in group.permission_grants
if not grant.is_deleted
grant.permission for grant in group.permission_grants if not grant.is_deleted
]
@router.put("/admin/user-group/{user_group_id}/permissions")
def set_user_group_permission(
user_group_id: int,
request: SetPermissionRequest,
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> SetPermissionResponse:
group = fetch_user_group(db_session, user_group_id)
if group is None:
raise OnyxError(OnyxErrorCode.NOT_FOUND, "User group not found")
if request.permission in NON_TOGGLEABLE_PERMISSIONS:
raise OnyxError(
OnyxErrorCode.INVALID_INPUT,
f"Permission '{request.permission}' cannot be toggled via this endpoint",
)
set_group_permission__no_commit(
group_id=user_group_id,
permission=request.permission,
enabled=request.enabled,
granted_by=user.id,
db_session=db_session,
)
db_session.commit()
return SetPermissionResponse(permission=request.permission, enabled=request.enabled)
@router.post("/admin/user-group")
def create_user_group(
user_group: UserGroupCreate,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> UserGroup:
try:
@@ -124,7 +155,7 @@ def create_user_group(
@router.patch("/admin/user-group/rename")
def rename_user_group_endpoint(
rename_request: UserGroupRename,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> UserGroup:
group = fetch_user_group(db_session, rename_request.id)
@@ -212,7 +243,7 @@ def set_user_curator(
@router.delete("/admin/user-group/{user_group_id}")
def delete_user_group(
user_group_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
group = fetch_user_group(db_session, user_group_id)
@@ -233,7 +264,7 @@ def delete_user_group(
def update_group_agents(
user_group_id: int,
request: UpdateGroupAgentsRequest,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
for agent_id in request.added_agent_ids:

View File

@@ -2,6 +2,7 @@ from uuid import UUID
from pydantic import BaseModel
from onyx.auth.permissions import Permission
from onyx.db.models import UserGroup as UserGroupModel
from onyx.server.documents.models import ConnectorCredentialPairDescriptor
from onyx.server.documents.models import ConnectorSnapshot
@@ -121,3 +122,13 @@ class SetCuratorRequest(BaseModel):
class UpdateGroupAgentsRequest(BaseModel):
added_agent_ids: list[int]
removed_agent_ids: list[int]
class SetPermissionRequest(BaseModel):
permission: Permission
enabled: bool
class SetPermissionResponse(BaseModel):
permission: Permission
enabled: bool

View File

@@ -47,6 +47,20 @@ IMPLIED_PERMISSIONS: dict[str, set[str]] = {
},
}
# Permissions that cannot be toggled via the group-permission API.
# BASIC_ACCESS is always granted, FULL_ADMIN_PANEL_ACCESS is too broad,
# and READ_* permissions are implied (never stored directly).
NON_TOGGLEABLE_PERMISSIONS: frozenset[Permission] = frozenset(
{
Permission.BASIC_ACCESS,
Permission.FULL_ADMIN_PANEL_ACCESS,
Permission.READ_CONNECTORS,
Permission.READ_DOCUMENT_SETS,
Permission.READ_AGENTS,
Permission.READ_USERS,
}
)
def resolve_effective_permissions(granted: set[str]) -> set[str]:
"""Expand granted permissions with their implied permissions.
@@ -107,4 +121,5 @@ def require_permission(
return user
dependency._is_require_permission = True # type: ignore[attr-defined] # sentinel for auth_check detection
return dependency

View File

@@ -127,6 +127,7 @@ from onyx.db.models import User
from onyx.db.pat import fetch_user_for_pat
from onyx.db.users import assign_user_to_default_groups__no_commit
from onyx.db.users import get_user_by_email
from onyx.db.users import is_limited_user
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import log_onyx_error
from onyx.error_handling.exceptions import onyx_error_to_json_response
@@ -1681,9 +1682,9 @@ async def current_user(
) -> User:
user = await double_check_user(user)
if user.role == UserRole.LIMITED:
if is_limited_user(user):
raise BasicAuthenticationError(
detail="Access denied. User role is LIMITED. BASIC or higher permissions are required.",
detail="Access denied. User has limited permissions.",
)
return user
@@ -1700,15 +1701,6 @@ async def current_curator_or_admin_user(
return user
async def current_admin_user(user: User = Depends(current_user)) -> User:
if user.role != UserRole.ADMIN:
raise BasicAuthenticationError(
detail="Access denied. User must be an admin to perform this action.",
)
return user
async def _get_user_from_token_data(token_data: dict) -> User | None:
"""Shared logic: token data dict → User object.
@@ -1817,11 +1809,11 @@ async def current_user_from_websocket(
# Apply same checks as HTTP auth (verification, OIDC expiry, role)
user = await double_check_user(user)
# Block LIMITED users (same as current_user)
if user.role == UserRole.LIMITED:
logger.warning(f"WS auth: user {user.email} has LIMITED role")
# Block limited users (same as current_user)
if is_limited_user(user):
logger.warning(f"WS auth: user {user.email} is limited")
raise BasicAuthenticationError(
detail="Access denied. User role is LIMITED. BASIC or higher permissions are required.",
detail="Access denied. User has limited permissions.",
)
logger.debug(f"WS auth: authenticated {user.email}")

View File

@@ -1,6 +1,7 @@
# Overview of Onyx Background Jobs
The background jobs take care of:
1. Pulling/Indexing documents (from connectors)
2. Updating document metadata (from connectors)
3. Cleaning up checkpoints and logic around indexing work (indexing indexing checkpoints and index attempt metadata)
@@ -9,37 +10,41 @@ The background jobs take care of:
## Worker → Queue Mapping
| Worker | File | Queues |
|--------|------|--------|
| Primary | `apps/primary.py` | `celery` |
| Light | `apps/light.py` | `vespa_metadata_sync`, `connector_deletion`, `doc_permissions_upsert`, `checkpoint_cleanup`, `index_attempt_cleanup` |
| Heavy | `apps/heavy.py` | `connector_pruning`, `connector_doc_permissions_sync`, `connector_external_group_sync`, `csv_generation`, `sandbox` |
| Docprocessing | `apps/docprocessing.py` | `docprocessing` |
| Docfetching | `apps/docfetching.py` | `connector_doc_fetching` |
| User File Processing | `apps/user_file_processing.py` | `user_file_processing`, `user_file_project_sync`, `user_file_delete` |
| Monitoring | `apps/monitoring.py` | `monitoring` |
| Background (consolidated) | `apps/background.py` | All queues above except `celery` |
| Worker | File | Queues |
| ------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| Primary | `apps/primary.py` | `celery` |
| Light | `apps/light.py` | `vespa_metadata_sync`, `connector_deletion`, `doc_permissions_upsert`, `checkpoint_cleanup`, `index_attempt_cleanup` |
| Heavy | `apps/heavy.py` | `connector_pruning`, `connector_doc_permissions_sync`, `connector_external_group_sync`, `csv_generation`, `sandbox` |
| Docprocessing | `apps/docprocessing.py` | `docprocessing` |
| Docfetching | `apps/docfetching.py` | `connector_doc_fetching` |
| User File Processing | `apps/user_file_processing.py` | `user_file_processing`, `user_file_project_sync`, `user_file_delete` |
| Monitoring | `apps/monitoring.py` | `monitoring` |
| Background (consolidated) | `apps/background.py` | All queues above except `celery` |
## Non-Worker Apps
| App | File | Purpose |
|-----|------|---------|
| **Beat** | `beat.py` | Celery beat scheduler with `DynamicTenantScheduler` that generates per-tenant periodic task schedules |
| **Client** | `client.py` | Minimal app for task submission from non-worker processes (e.g., API server) |
| App | File | Purpose |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------- |
| **Beat** | `beat.py` | Celery beat scheduler with `DynamicTenantScheduler` that generates per-tenant periodic task schedules |
| **Client** | `client.py` | Minimal app for task submission from non-worker processes (e.g., API server) |
### Shared Module
`app_base.py` provides:
- `TenantAwareTask` - Base task class that sets tenant context
- Signal handlers for logging, cleanup, and lifecycle events
- Readiness probes and health checks
## Worker Details
### Primary (Coordinator and task dispatcher)
It is the single worker which handles tasks from the default celery queue. It is a singleton worker ensured by the `PRIMARY_WORKER` Redis lock
which it touches every `CELERY_PRIMARY_WORKER_LOCK_TIMEOUT / 8` seconds (using Celery Bootsteps)
On startup:
- waits for redis, postgres, document index to all be healthy
- acquires the singleton lock
- cleans all the redis states associated with background jobs
@@ -47,33 +52,34 @@ On startup:
Then it cycles through its tasks as scheduled by Celery Beat:
| Task | Frequency | Description |
|------|-----------|-------------|
| `check_for_indexing` | 15s | Scans for connectors needing indexing → dispatches to `DOCFETCHING` queue |
| `check_for_vespa_sync_task` | 20s | Finds stale documents/document sets → dispatches sync tasks to `VESPA_METADATA_SYNC` queue |
| `check_for_pruning` | 20s | Finds connectors due for pruning → dispatches to `CONNECTOR_PRUNING` queue |
| `check_for_connector_deletion` | 20s | Processes deletion requests → dispatches to `CONNECTOR_DELETION` queue |
| `check_for_user_file_processing` | 20s | Checks for user uploads → dispatches to `USER_FILE_PROCESSING` queue |
| `check_for_checkpoint_cleanup` | 1h | Cleans up old indexing checkpoints |
| `check_for_index_attempt_cleanup` | 30m | Cleans up old index attempts |
| `celery_beat_heartbeat` | 1m | Heartbeat for Beat watchdog |
| Task | Frequency | Description |
| --------------------------------- | --------- | ------------------------------------------------------------------------------------------ |
| `check_for_indexing` | 15s | Scans for connectors needing indexing → dispatches to `DOCFETCHING` queue |
| `check_for_vespa_sync_task` | 20s | Finds stale documents/document sets → dispatches sync tasks to `VESPA_METADATA_SYNC` queue |
| `check_for_pruning` | 20s | Finds connectors due for pruning → dispatches to `CONNECTOR_PRUNING` queue |
| `check_for_connector_deletion` | 20s | Processes deletion requests → dispatches to `CONNECTOR_DELETION` queue |
| `check_for_user_file_processing` | 20s | Checks for user uploads → dispatches to `USER_FILE_PROCESSING` queue |
| `check_for_checkpoint_cleanup` | 1h | Cleans up old indexing checkpoints |
| `check_for_index_attempt_cleanup` | 30m | Cleans up old index attempts |
| `celery_beat_heartbeat` | 1m | Heartbeat for Beat watchdog |
Watchdog is a separate Python process managed by supervisord which runs alongside celery workers. It checks the ONYX_CELERY_BEAT_HEARTBEAT_KEY in
Redis to ensure Celery Beat is not dead. Beat schedules the celery_beat_heartbeat for Primary to touch the key and share that it's still alive.
See supervisord.conf for watchdog config.
### Light
Fast and short living tasks that are not resource intensive. High concurrency:
Can have 24 concurrent workers, each with a prefetch of 8 for a total of 192 tasks in flight at once.
Tasks it handles:
- Syncs access/permissions, document sets, boosts, hidden state
- Deletes documents that are marked for deletion in Postgres
- Cleanup of checkpoints and index attempts
### Heavy
Long running, resource intensive tasks, handles pruning and sandbox operations. Low concurrency - max concurrency of 4 with 1 prefetch.
Does not interact with the Document Index, it handles the syncs with external systems. Large volume API calls to handle pruning and fetching permissions, etc.
@@ -82,16 +88,24 @@ Generates CSV exports which may take a long time with significant data in Postgr
Sandbox (new feature) for running Next.js, Python virtual env, OpenCode AI Agent, and access to knowledge files
### Docprocessing, Docfetching, User File Processing
Docprocessing and Docfetching are for indexing documents:
- Docfetching runs connectors to pull documents from external APIs (Google Drive, Confluence, etc.), stores batches to file storage, and dispatches docprocessing tasks
- Docprocessing retrieves batches, runs the indexing pipeline (chunking, embedding), and indexes into the Document Index
User Files come from uploads directly via the input bar
Docprocessing and Docfetching are for indexing documents:
- Docfetching runs connectors to pull documents from external APIs (Google Drive, Confluence, etc.), stores batches to file storage, and dispatches docprocessing tasks
- Docprocessing retrieves batches, runs the indexing pipeline (chunking, embedding), and indexes into the Document Index
- User Files come from uploads directly via the input bar
### Monitoring
Observability and metrics collections:
- Queue lengths, connector success/failure, lconnector latencies
- Queue lengths, connector success/failure, connector latencies
- Memory of supervisor managed processes (workers, beat, slack)
- Cloud and multitenant specific monitorings
## Prometheus Metrics
Workers can expose Prometheus metrics via a standalone HTTP server. Currently docfetching and docprocessing have push-based task lifecycle metrics; the monitoring worker runs pull-based collectors for queue depth and connector health.
For the full metric reference, integration guide, and PromQL examples, see [`docs/METRICS.md`](../../../docs/METRICS.md#celery-worker-metrics).

View File

@@ -13,6 +13,12 @@ from celery.signals import worker_shutdown
import onyx.background.celery.apps.app_base as app_base
from onyx.configs.constants import POSTGRES_CELERY_WORKER_HEAVY_APP_NAME
from onyx.db.engine.sql_engine import SqlEngine
from onyx.server.metrics.celery_task_metrics import on_celery_task_postrun
from onyx.server.metrics.celery_task_metrics import on_celery_task_prerun
from onyx.server.metrics.celery_task_metrics import on_celery_task_rejected
from onyx.server.metrics.celery_task_metrics import on_celery_task_retry
from onyx.server.metrics.celery_task_metrics import on_celery_task_revoked
from onyx.server.metrics.metrics_server import start_metrics_server
from onyx.utils.logger import setup_logger
from shared_configs.configs import MULTI_TENANT
@@ -34,6 +40,7 @@ def on_task_prerun(
**kwds: Any,
) -> None:
app_base.on_task_prerun(sender, task_id, task, args, kwargs, **kwds)
on_celery_task_prerun(task_id, task)
@signals.task_postrun.connect
@@ -48,6 +55,31 @@ def on_task_postrun(
**kwds: Any,
) -> None:
app_base.on_task_postrun(sender, task_id, task, args, kwargs, retval, state, **kwds)
on_celery_task_postrun(task_id, task, state)
@signals.task_retry.connect
def on_task_retry(sender: Any | None = None, **kwargs: Any) -> None: # noqa: ARG001
task_id = getattr(getattr(sender, "request", None), "id", None)
on_celery_task_retry(task_id, sender)
@signals.task_revoked.connect
def on_task_revoked(sender: Any | None = None, **kwargs: Any) -> None:
task_name = getattr(sender, "name", None) or str(sender)
on_celery_task_revoked(kwargs.get("task_id"), task_name)
@signals.task_rejected.connect
def on_task_rejected(sender: Any | None = None, **kwargs: Any) -> None: # noqa: ARG001
message = kwargs.get("message")
task_name: str | None = None
if message is not None:
headers = getattr(message, "headers", None) or {}
task_name = headers.get("task")
if task_name is None:
task_name = "unknown"
on_celery_task_rejected(None, task_name)
@celeryd_init.connect
@@ -76,6 +108,7 @@ def on_worker_init(sender: Worker, **kwargs: Any) -> None:
@worker_ready.connect
def on_worker_ready(sender: Any, **kwargs: Any) -> None:
start_metrics_server("heavy")
app_base.on_worker_ready(sender, **kwargs)

View File

@@ -1,3 +1,4 @@
import time
from collections.abc import Generator
from collections.abc import Iterator
from collections.abc import Sequence
@@ -30,6 +31,8 @@ from onyx.connectors.models import HierarchyNode
from onyx.connectors.models import SlimDocument
from onyx.httpx.httpx_pool import HttpxPool
from onyx.indexing.indexing_heartbeat import IndexingHeartbeatInterface
from onyx.server.metrics.pruning_metrics import inc_pruning_rate_limit_error
from onyx.server.metrics.pruning_metrics import observe_pruning_enumeration_duration
from onyx.utils.logger import setup_logger
@@ -130,6 +133,7 @@ def _extract_from_batch(
def extract_ids_from_runnable_connector(
runnable_connector: BaseConnector,
callback: IndexingHeartbeatInterface | None = None,
connector_type: str = "unknown",
) -> SlimConnectorExtractionResult:
"""
Extract document IDs and hierarchy nodes from a runnable connector.
@@ -179,21 +183,38 @@ def extract_ids_from_runnable_connector(
)
# process raw batches to extract both IDs and hierarchy nodes
for doc_list in raw_batch_generator:
if callback and callback.should_stop():
raise RuntimeError(
"extract_ids_from_runnable_connector: Stop signal detected"
)
enumeration_start = time.monotonic()
try:
for doc_list in raw_batch_generator:
if callback and callback.should_stop():
raise RuntimeError(
"extract_ids_from_runnable_connector: Stop signal detected"
)
batch_result = _extract_from_batch(doc_list)
batch_ids = batch_result.raw_id_to_parent
batch_nodes = batch_result.hierarchy_nodes
doc_batch_processing_func(batch_ids)
all_raw_id_to_parent.update(batch_ids)
all_hierarchy_nodes.extend(batch_nodes)
batch_result = _extract_from_batch(doc_list)
batch_ids = batch_result.raw_id_to_parent
batch_nodes = batch_result.hierarchy_nodes
doc_batch_processing_func(batch_ids)
all_raw_id_to_parent.update(batch_ids)
all_hierarchy_nodes.extend(batch_nodes)
if callback:
callback.progress("extract_ids_from_runnable_connector", len(batch_ids))
if callback:
callback.progress("extract_ids_from_runnable_connector", len(batch_ids))
except Exception as e:
# Best-effort rate limit detection via string matching.
# Connectors surface rate limits inconsistently — some raise HTTP 429,
# some use SDK-specific exceptions (e.g. google.api_core.exceptions.ResourceExhausted)
# that may or may not include "rate limit" or "429" in the message.
# TODO(Bo): replace with a standard ConnectorRateLimitError exception that all
# connectors raise when rate limited, making this check precise.
error_str = str(e)
if "rate limit" in error_str.lower() or "429" in error_str:
inc_pruning_rate_limit_error(connector_type)
raise
finally:
observe_pruning_enumeration_duration(
time.monotonic() - enumeration_start, connector_type
)
return SlimConnectorExtractionResult(
raw_id_to_parent=all_raw_id_to_parent,

View File

@@ -75,6 +75,8 @@ beat_task_templates: list[dict] = [
"options": {
"priority": OnyxCeleryPriority.LOW,
"expires": BEAT_EXPIRES_DEFAULT,
# Run on gated tenants too — they may still have stale checkpoints to clean.
"skip_gated": False,
},
},
{
@@ -84,6 +86,8 @@ beat_task_templates: list[dict] = [
"options": {
"priority": OnyxCeleryPriority.MEDIUM,
"expires": BEAT_EXPIRES_DEFAULT,
# Run on gated tenants too — they may still have stale index attempts.
"skip_gated": False,
},
},
{
@@ -93,6 +97,8 @@ beat_task_templates: list[dict] = [
"options": {
"priority": OnyxCeleryPriority.MEDIUM,
"expires": BEAT_EXPIRES_DEFAULT,
# Gated tenants may still have connectors awaiting deletion.
"skip_gated": False,
},
},
{
@@ -136,7 +142,14 @@ beat_task_templates: list[dict] = [
{
"name": "cleanup-idle-sandboxes",
"task": OnyxCeleryTask.CLEANUP_IDLE_SANDBOXES,
"schedule": timedelta(minutes=1),
# SANDBOX_IDLE_TIMEOUT_SECONDS defaults to 1 hour, so there is no
# functional reason to scan more often than every ~15 minutes. In the
# cloud this is multiplied by CLOUD_BEAT_MULTIPLIER_DEFAULT (=8) so
# the effective cadence becomes ~2 hours, which still meets the
# idle-detection SLA. The previous 1-minute base schedule produced
# an 8-minute per-tenant fan-out and was the dominant source of
# background DB load on the cloud cluster.
"schedule": timedelta(minutes=15),
"options": {
"priority": OnyxCeleryPriority.LOW,
"expires": BEAT_EXPIRES_DEFAULT,
@@ -266,7 +279,7 @@ def make_cloud_generator_task(task: dict[str, Any]) -> dict[str, Any]:
cloud_task["kwargs"] = {}
cloud_task["kwargs"]["task_name"] = task["task"]
optional_fields = ["queue", "priority", "expires"]
optional_fields = ["queue", "priority", "expires", "skip_gated"]
for field in optional_fields:
if field in task["options"]:
cloud_task["kwargs"][field] = task["options"][field]
@@ -359,7 +372,13 @@ if not MULTI_TENANT:
]
)
tasks_to_schedule.extend(beat_task_templates)
# `skip_gated` is a cloud-only hint consumed by `cloud_beat_task_generator`. Strip
# it before extending the self-hosted schedule so it doesn't leak into apply_async
# as an unrecognised option on every fired task message.
for _template in beat_task_templates:
_self_hosted_template = copy.deepcopy(_template)
_self_hosted_template["options"].pop("skip_gated", None)
tasks_to_schedule.append(_self_hosted_template)
def generate_cloud_tasks(

View File

@@ -72,6 +72,7 @@ from onyx.redis.redis_hierarchy import get_source_node_id_from_cache
from onyx.redis.redis_hierarchy import HierarchyNodeCacheEntry
from onyx.redis.redis_pool import get_redis_client
from onyx.redis.redis_pool import get_redis_replica_client
from onyx.server.metrics.pruning_metrics import observe_pruning_diff_duration
from onyx.server.runtime.onyx_runtime import OnyxRuntime
from onyx.server.utils import make_short_id
from onyx.utils.logger import format_error_for_logging
@@ -217,7 +218,7 @@ def check_for_pruning(self: Task, *, tenant_id: str) -> bool | None:
try:
# the entire task needs to run frequently in order to finalize pruning
# but pruning only kicks off once per hour
# but pruning only kicks off once per min
if not r.exists(OnyxRedisSignals.BLOCK_PRUNING):
task_logger.info("Checking for pruning due")
@@ -570,8 +571,9 @@ def connector_pruning_generator_task(
)
# Extract docs and hierarchy nodes from the source
connector_type = cc_pair.connector.source.value
extraction_result = extract_ids_from_runnable_connector(
runnable_connector, callback
runnable_connector, callback, connector_type=connector_type
)
all_connector_doc_ids = extraction_result.raw_id_to_parent
@@ -636,40 +638,46 @@ def connector_pruning_generator_task(
commit=True,
)
# a list of docs in our local index
all_indexed_document_ids = {
doc.id
for doc in get_documents_for_connector_credential_pair(
db_session=db_session,
connector_id=connector_id,
credential_id=credential_id,
diff_start = time.monotonic()
try:
# a list of docs in our local index
all_indexed_document_ids = {
doc.id
for doc in get_documents_for_connector_credential_pair(
db_session=db_session,
connector_id=connector_id,
credential_id=credential_id,
)
}
# generate list of docs to remove (no longer in the source)
doc_ids_to_remove = list(
all_indexed_document_ids - all_connector_doc_ids.keys()
)
}
# generate list of docs to remove (no longer in the source)
doc_ids_to_remove = list(
all_indexed_document_ids - all_connector_doc_ids.keys()
)
task_logger.info(
"Pruning set collected: "
f"cc_pair={cc_pair_id} "
f"connector_source={cc_pair.connector.source} "
f"docs_to_remove={len(doc_ids_to_remove)}"
)
task_logger.info(
"Pruning set collected: "
f"cc_pair={cc_pair_id} "
f"connector_source={cc_pair.connector.source} "
f"docs_to_remove={len(doc_ids_to_remove)}"
)
task_logger.info(
f"RedisConnector.prune.generate_tasks starting. cc_pair={cc_pair_id}"
)
tasks_generated = redis_connector.prune.generate_tasks(
set(doc_ids_to_remove), self.app, db_session, None
)
if tasks_generated is None:
return None
task_logger.info(
f"RedisConnector.prune.generate_tasks starting. cc_pair={cc_pair_id}"
)
tasks_generated = redis_connector.prune.generate_tasks(
set(doc_ids_to_remove), self.app, db_session, None
)
if tasks_generated is None:
return None
task_logger.info(
f"RedisConnector.prune.generate_tasks finished. cc_pair={cc_pair_id} tasks_generated={tasks_generated}"
)
task_logger.info(
f"RedisConnector.prune.generate_tasks finished. cc_pair={cc_pair_id} tasks_generated={tasks_generated}"
)
finally:
observe_pruning_diff_duration(
time.monotonic() - diff_start, connector_type
)
redis_connector.prune.generator_complete = tasks_generated

View File

@@ -996,6 +996,7 @@ def _run_models(
def _run_model(model_idx: int) -> None:
"""Run one LLM loop inside a worker thread, writing packets to ``merged_queue``."""
model_emitter = Emitter(
model_idx=model_idx,
merged_queue=merged_queue,
@@ -1102,33 +1103,33 @@ def _run_models(
finally:
merged_queue.put((model_idx, _MODEL_DONE))
def _delete_orphaned_message(model_idx: int, context: str) -> None:
"""Delete a reserved ChatMessage that was never populated due to a model error."""
def _save_errored_message(model_idx: int, context: str) -> None:
"""Save an error message to a reserved ChatMessage that failed during execution."""
try:
orphaned = db_session.get(
ChatMessage, setup.reserved_messages[model_idx].id
)
if orphaned is not None:
db_session.delete(orphaned)
msg = db_session.get(ChatMessage, setup.reserved_messages[model_idx].id)
if msg is not None:
error_text = f"Error from {setup.model_display_names[model_idx]}: model encountered an error during generation."
msg.message = error_text
msg.error = error_text
db_session.commit()
except Exception:
logger.exception(
"%s orphan cleanup failed for model %d (%s)",
"%s error save failed for model %d (%s)",
context,
model_idx,
setup.model_display_names[model_idx],
)
# Copy contextvars before submitting futures — ThreadPoolExecutor does NOT
# auto-propagate contextvars in Python 3.11; threads would inherit a blank context.
worker_context = contextvars.copy_context()
# Each worker thread needs its own Context copy — a single Context object
# cannot be entered concurrently by multiple threads (RuntimeError).
executor = ThreadPoolExecutor(
max_workers=n_models, thread_name_prefix="multi-model"
)
completion_persisted: bool = False
try:
for i in range(n_models):
executor.submit(worker_context.run, _run_model, i)
ctx = contextvars.copy_context()
executor.submit(ctx.run, _run_model, i)
# ── Main thread: merge and yield packets ────────────────────────────
models_remaining = n_models
@@ -1145,7 +1146,7 @@ def _run_models(
# save "stopped by user" for a model that actually threw an exception.
for i in range(n_models):
if model_errored[i]:
_delete_orphaned_message(i, "stop-button")
_save_errored_message(i, "stop-button")
continue
try:
succeeded = model_succeeded[i]
@@ -1211,7 +1212,7 @@ def _run_models(
for i in range(n_models):
if not model_succeeded[i]:
# Model errored — delete its orphaned reserved message.
_delete_orphaned_message(i, "normal")
_save_errored_message(i, "normal")
continue
try:
llm_loop_completion_handle(
@@ -1264,7 +1265,7 @@ def _run_models(
setup.model_display_names[i],
)
elif model_errored[i]:
_delete_orphaned_message(i, "disconnect")
_save_errored_message(i, "disconnect")
# 4. Drain buffered packets from memory — no consumer is running.
while not merged_queue.empty():
try:

View File

@@ -110,8 +110,8 @@ def insert_api_key(
# Assign the API key virtual user to the appropriate default group
# before commit so everything is atomic.
# LIMITED role service accounts should have no group membership.
if api_key_args.role != UserRole.LIMITED:
# Only ADMIN and BASIC roles get default group membership.
if api_key_args.role in (UserRole.ADMIN, UserRole.BASIC):
assign_user_to_default_groups__no_commit(
db_session,
api_key_user_row,
@@ -161,8 +161,8 @@ def update_api_key(
)
db_session.execute(delete_stmt)
# Re-assign to the correct default group (skip for LIMITED).
if api_key_args.role != UserRole.LIMITED:
# Re-assign to the correct default group (only for ADMIN/BASIC).
if api_key_args.role in (UserRole.ADMIN, UserRole.BASIC):
assign_user_to_default_groups__no_commit(
db_session,
api_key_user,

View File

@@ -236,14 +236,15 @@ def upsert_llm_provider(
db_session.add(existing_llm_provider)
# Filter out empty strings and None values from custom_config to allow
# providers like Bedrock to fall back to IAM roles when credentials are not provided
# providers like Bedrock to fall back to IAM roles when credentials are not provided.
# NOTE: An empty dict ({}) is preserved as-is — it signals that the provider was
# created via the custom modal and must be reopened with CustomModal, not a
# provider-specific modal. Only None means "no custom config at all".
custom_config = llm_provider_upsert_request.custom_config
if custom_config:
custom_config = {
k: v for k, v in custom_config.items() if v is not None and v.strip() != ""
}
# Set to None if the dict is empty after filtering
custom_config = custom_config or None
api_base = llm_provider_upsert_request.api_base or None
existing_llm_provider.provider = llm_provider_upsert_request.provider
@@ -303,16 +304,7 @@ def upsert_llm_provider(
).delete(synchronize_session="fetch")
db_session.flush()
# Import here to avoid circular imports
from onyx.llm.utils import get_max_input_tokens
for model_config in llm_provider_upsert_request.model_configurations:
max_input_tokens = model_config.max_input_tokens
if max_input_tokens is None:
max_input_tokens = get_max_input_tokens(
model_name=model_config.name,
model_provider=llm_provider_upsert_request.provider,
)
supported_flows = [LLMModelFlowType.CHAT]
if model_config.supports_image_input:
@@ -325,7 +317,7 @@ def upsert_llm_provider(
model_configuration_id=existing.id,
supported_flows=supported_flows,
is_visible=model_config.is_visible,
max_input_tokens=max_input_tokens,
max_input_tokens=model_config.max_input_tokens,
display_name=model_config.display_name,
)
else:
@@ -335,7 +327,7 @@ def upsert_llm_provider(
model_name=model_config.name,
supported_flows=supported_flows,
is_visible=model_config.is_visible,
max_input_tokens=max_input_tokens,
max_input_tokens=model_config.max_input_tokens,
display_name=model_config.display_name,
)

View File

@@ -20,6 +20,7 @@ from onyx.db.models import User__UserGroup
from onyx.db.models import UserGroup
from onyx.db.permissions import recompute_user_permissions__no_commit
from onyx.db.users import assign_user_to_default_groups__no_commit
from onyx.db.users import is_limited_user
from onyx.server.manage.models import MemoryItem
from onyx.server.manage.models import UserSpecificAssistantPreference
from onyx.utils.logger import setup_logger
@@ -65,8 +66,11 @@ def update_user_role(
)
)
# Re-assign to the correct default group (skip for LIMITED).
if new_role != UserRole.LIMITED:
# Re-assign to the correct default group.
# assign_user_to_default_groups__no_commit internally skips
# ANONYMOUS, BOT, and EXT_PERM_USER account types.
# Also skip limited users (no group assignment).
if not is_limited_user(user):
assign_user_to_default_groups__no_commit(
db_session,
user,
@@ -98,7 +102,10 @@ def activate_user(
created while inactive or deactivated before the backfill migration.
"""
user.is_active = True
if user.role != UserRole.LIMITED:
# assign_user_to_default_groups__no_commit internally skips
# ANONYMOUS, BOT, and EXT_PERM_USER account types.
# Also skip limited users (no group assignment).
if not is_limited_user(user):
assign_user_to_default_groups__no_commit(
db_session, user, is_admin=(user.role == UserRole.ADMIN)
)

View File

@@ -34,9 +34,27 @@ from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
logger = setup_logger()
def is_limited_user(user: User) -> bool:
"""Check if a user is effectively limited — i.e. should be denied
access by ``current_user`` and should not receive default-group
membership.
A user is limited when they are:
* an anonymous user, or
* a service account with no effective permissions (no group membership).
"""
if user.account_type == AccountType.ANONYMOUS:
return True
if (
user.account_type == AccountType.SERVICE_ACCOUNT
and not user.effective_permissions
):
return True
return False
def validate_user_role_update(
requested_role: UserRole,
current_role: UserRole,
current_account_type: AccountType,
explicit_override: bool = False,
) -> None:
@@ -50,7 +68,7 @@ def validate_user_role_update(
- requested role is a limited user
- current account type is BOT (slack user)
- current account type is EXT_PERM_USER
- current role is a limited user
- current account type is ANONYMOUS or SERVICE_ACCOUNT
"""
if current_account_type == AccountType.BOT:
@@ -65,10 +83,10 @@ def validate_user_role_update(
detail="To change an External Permissioned User's role, they must first login to Onyx via the web app.",
)
if current_role == UserRole.LIMITED:
if current_account_type in (AccountType.ANONYMOUS, AccountType.SERVICE_ACCOUNT):
raise HTTPException(
status_code=400,
detail="To change a Limited User's role, they must first login to Onyx via the web app.",
detail="Cannot change the role of an anonymous or service account user.",
)
if explicit_override:

View File

@@ -52,9 +52,21 @@ KNOWN_OPENPYXL_BUGS = [
def get_markitdown_converter() -> "MarkItDown":
global _MARKITDOWN_CONVERTER
from markitdown import MarkItDown
if _MARKITDOWN_CONVERTER is None:
from markitdown import MarkItDown
# Patch this function to effectively no-op because we were seeing this
# module take an inordinate amount of time to convert charts to markdown,
# making some powerpoint files with many or complicated charts nearly
# unindexable.
from markitdown.converters._pptx_converter import PptxConverter
setattr(
PptxConverter,
"_convert_chart_to_markdown",
lambda self, chart: "\n\n[chart omitted]\n\n", # noqa: ARG005
)
_MARKITDOWN_CONVERTER = MarkItDown(enable_plugins=False)
return _MARKITDOWN_CONVERTER
@@ -205,18 +217,26 @@ def read_pdf_file(
try:
pdf_reader = PdfReader(file)
if pdf_reader.is_encrypted and pdf_pass is not None:
if pdf_reader.is_encrypted:
# Try the explicit password first, then fall back to an empty
# string. Owner-password-only PDFs (permission restrictions but
# no open password) decrypt successfully with "".
# See https://github.com/onyx-dot-app/onyx/issues/9754
passwords = [p for p in [pdf_pass, ""] if p is not None]
decrypt_success = False
try:
decrypt_success = pdf_reader.decrypt(pdf_pass) != 0
except Exception:
logger.error("Unable to decrypt pdf")
for pw in passwords:
try:
if pdf_reader.decrypt(pw) != 0:
decrypt_success = True
break
except Exception:
pass
if not decrypt_success:
logger.error(
"Encrypted PDF could not be decrypted, returning empty text."
)
return "", metadata, []
elif pdf_reader.is_encrypted:
logger.warning("No Password for an encrypted PDF, returning empty text.")
return "", metadata, []
# Basic PDF metadata
if pdf_reader.metadata is not None:

View File

@@ -33,8 +33,20 @@ def is_pdf_protected(file: IO[Any]) -> bool:
with preserve_position(file):
reader = PdfReader(file)
if not reader.is_encrypted:
return False
return bool(reader.is_encrypted)
# PDFs with only an owner password (permission restrictions like
# print/copy disabled) use an empty user password — any viewer can open
# them without prompting. decrypt("") returns 0 only when a real user
# password is required. See https://github.com/onyx-dot-app/onyx/issues/9754
try:
return reader.decrypt("") == 0
except Exception:
logger.exception(
"Failed to evaluate PDF encryption; treating as password protected"
)
return True
def is_docx_protected(file: IO[Any]) -> bool:

View File

@@ -26,6 +26,7 @@ class LlmProviderNames(str, Enum):
MISTRAL = "mistral"
LITELLM_PROXY = "litellm_proxy"
BIFROST = "bifrost"
OPENAI_COMPATIBLE = "openai_compatible"
def __str__(self) -> str:
"""Needed so things like:
@@ -46,6 +47,7 @@ WELL_KNOWN_PROVIDER_NAMES = [
LlmProviderNames.LM_STUDIO,
LlmProviderNames.LITELLM_PROXY,
LlmProviderNames.BIFROST,
LlmProviderNames.OPENAI_COMPATIBLE,
]
@@ -64,6 +66,7 @@ PROVIDER_DISPLAY_NAMES: dict[str, str] = {
LlmProviderNames.LM_STUDIO: "LM Studio",
LlmProviderNames.LITELLM_PROXY: "LiteLLM Proxy",
LlmProviderNames.BIFROST: "Bifrost",
LlmProviderNames.OPENAI_COMPATIBLE: "OpenAI Compatible",
"groq": "Groq",
"anyscale": "Anyscale",
"deepseek": "DeepSeek",
@@ -84,6 +87,44 @@ PROVIDER_DISPLAY_NAMES: dict[str, str] = {
"gemini": "Gemini",
"stability": "Stability",
"writer": "Writer",
# Custom provider display names (used in the custom provider picker)
"aiml": "AI/ML",
"assemblyai": "AssemblyAI",
"aws_polly": "AWS Polly",
"azure_ai": "Azure AI",
"chatgpt": "ChatGPT",
"cohere_chat": "Cohere Chat",
"datarobot": "DataRobot",
"deepgram": "Deepgram",
"deepinfra": "DeepInfra",
"elevenlabs": "ElevenLabs",
"fal_ai": "fal.ai",
"featherless_ai": "Featherless AI",
"fireworks_ai": "Fireworks AI",
"friendliai": "FriendliAI",
"gigachat": "GigaChat",
"github_copilot": "GitHub Copilot",
"gradient_ai": "Gradient AI",
"huggingface": "HuggingFace",
"jina_ai": "Jina AI",
"lambda_ai": "Lambda AI",
"llamagate": "LlamaGate",
"meta_llama": "Meta Llama",
"minimax": "MiniMax",
"nlp_cloud": "NLP Cloud",
"nvidia_nim": "NVIDIA NIM",
"oci": "OCI",
"ovhcloud": "OVHcloud",
"palm": "PaLM",
"publicai": "PublicAI",
"runwayml": "RunwayML",
"sambanova": "SambaNova",
"together_ai": "Together AI",
"vercel_ai_gateway": "Vercel AI Gateway",
"volcengine": "Volcengine",
"wandb": "W&B",
"watsonx": "IBM watsonx",
"zai": "ZAI",
}
# Map vendors to their brand names (used for provider_display_name generation)
@@ -116,6 +157,7 @@ AGGREGATOR_PROVIDERS: set[str] = {
LlmProviderNames.AZURE,
LlmProviderNames.LITELLM_PROXY,
LlmProviderNames.BIFROST,
LlmProviderNames.OPENAI_COMPATIBLE,
}
# Model family name mappings for display name generation

View File

@@ -327,12 +327,19 @@ class LitellmLLM(LLM):
):
model_kwargs[VERTEX_LOCATION_KWARG] = "global"
# Bifrost: OpenAI-compatible proxy that expects model names in
# provider/model format (e.g. "anthropic/claude-sonnet-4-6").
# We route through LiteLLM's openai provider with the Bifrost base URL,
# and ensure /v1 is appended.
if model_provider == LlmProviderNames.BIFROST:
# Bifrost and OpenAI-compatible: OpenAI-compatible proxies that send
# model names directly to the endpoint. We route through LiteLLM's
# openai provider with the server's base URL, and ensure /v1 is appended.
if model_provider in (
LlmProviderNames.BIFROST,
LlmProviderNames.OPENAI_COMPATIBLE,
):
self._custom_llm_provider = "openai"
# LiteLLM's OpenAI client requires an api_key to be set.
# Many OpenAI-compatible servers don't need auth, so supply a
# placeholder to prevent LiteLLM from raising AuthenticationError.
if not self._api_key:
model_kwargs.setdefault("api_key", "not-needed")
if self._api_base is not None:
base = self._api_base.rstrip("/")
self._api_base = base if base.endswith("/v1") else f"{base}/v1"
@@ -449,17 +456,20 @@ class LitellmLLM(LLM):
optional_kwargs: dict[str, Any] = {}
# Model name
is_bifrost = self._model_provider == LlmProviderNames.BIFROST
is_openai_compatible_proxy = self._model_provider in (
LlmProviderNames.BIFROST,
LlmProviderNames.OPENAI_COMPATIBLE,
)
model_provider = (
f"{self.config.model_provider}/responses"
if is_openai_model # Uses litellm's completions -> responses bridge
else self.config.model_provider
)
if is_bifrost:
# Bifrost expects model names in provider/model format
# (e.g. "anthropic/claude-sonnet-4-6") sent directly to its
# OpenAI-compatible endpoint. We use custom_llm_provider="openai"
# so LiteLLM doesn't try to route based on the provider prefix.
if is_openai_compatible_proxy:
# OpenAI-compatible proxies (Bifrost, generic OpenAI-compatible
# servers) expect model names sent directly to their endpoint.
# We use custom_llm_provider="openai" so LiteLLM doesn't try
# to route based on the provider prefix.
model = self.config.deployment_name or self.config.model_name
else:
model = f"{model_provider}/{self.config.deployment_name or self.config.model_name}"
@@ -550,7 +560,10 @@ class LitellmLLM(LLM):
if structured_response_format:
optional_kwargs["response_format"] = structured_response_format
if not (is_claude_model or is_ollama or is_mistral) or is_bifrost:
if (
not (is_claude_model or is_ollama or is_mistral)
or is_openai_compatible_proxy
):
# Litellm bug: tool_choice is dropped silently if not specified here for OpenAI
# However, this param breaks Anthropic and Mistral models,
# so it must be conditionally included unless the request is

View File

@@ -15,6 +15,8 @@ LITELLM_PROXY_PROVIDER_NAME = "litellm_proxy"
BIFROST_PROVIDER_NAME = "bifrost"
OPENAI_COMPATIBLE_PROVIDER_NAME = "openai_compatible"
# Providers that use optional Bearer auth from custom_config
PROVIDERS_WITH_SPECIAL_API_KEY_HANDLING: dict[str, str] = {
LlmProviderNames.OLLAMA_CHAT: OLLAMA_API_KEY_CONFIG_KEY,

View File

@@ -19,6 +19,7 @@ from onyx.llm.well_known_providers.constants import BIFROST_PROVIDER_NAME
from onyx.llm.well_known_providers.constants import LITELLM_PROXY_PROVIDER_NAME
from onyx.llm.well_known_providers.constants import LM_STUDIO_PROVIDER_NAME
from onyx.llm.well_known_providers.constants import OLLAMA_PROVIDER_NAME
from onyx.llm.well_known_providers.constants import OPENAI_COMPATIBLE_PROVIDER_NAME
from onyx.llm.well_known_providers.constants import OPENAI_PROVIDER_NAME
from onyx.llm.well_known_providers.constants import OPENROUTER_PROVIDER_NAME
from onyx.llm.well_known_providers.constants import VERTEXAI_PROVIDER_NAME
@@ -51,6 +52,7 @@ def _get_provider_to_models_map() -> dict[str, list[str]]:
OPENROUTER_PROVIDER_NAME: [], # Dynamic - fetched from OpenRouter API
LITELLM_PROXY_PROVIDER_NAME: [], # Dynamic - fetched from LiteLLM proxy API
BIFROST_PROVIDER_NAME: [], # Dynamic - fetched from Bifrost API
OPENAI_COMPATIBLE_PROVIDER_NAME: [], # Dynamic - fetched from OpenAI-compatible API
}
@@ -336,6 +338,7 @@ def get_provider_display_name(provider_name: str) -> str:
VERTEXAI_PROVIDER_NAME: "Google Vertex AI",
OPENROUTER_PROVIDER_NAME: "OpenRouter",
LITELLM_PROXY_PROVIDER_NAME: "LiteLLM Proxy",
OPENAI_COMPATIBLE_PROVIDER_NAME: "OpenAI Compatible",
}
if provider_name in _ONYX_PROVIDER_DISPLAY_NAMES:

View File

@@ -90,6 +90,7 @@ from onyx.onyxbot.slack.utils import respond_in_thread_or_channel
from onyx.onyxbot.slack.utils import TenantSocketModeClient
from onyx.redis.redis_pool import get_redis_client
from onyx.server.manage.models import SlackBotTokens
from onyx.tracing.setup import setup_tracing
from onyx.utils.logger import setup_logger
from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
from onyx.utils.variable_functionality import set_is_ee_based_on_env_variable
@@ -1206,6 +1207,7 @@ if __name__ == "__main__":
tenant_handler = SlackbotHandler()
set_is_ee_based_on_env_variable()
setup_tracing()
try:
# Keep the main thread alive

View File

@@ -2,7 +2,7 @@ from fastapi import APIRouter
from fastapi import Depends
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.db.api_key import ApiKeyDescriptor
from onyx.db.api_key import fetch_api_keys
from onyx.db.api_key import insert_api_key
@@ -10,6 +10,7 @@ from onyx.db.api_key import regenerate_api_key
from onyx.db.api_key import remove_api_key
from onyx.db.api_key import update_api_key
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.server.api_key.models import APIKeyArgs
@@ -19,7 +20,7 @@ router = APIRouter(prefix="/admin/api-key")
@router.get("")
def list_api_keys(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[ApiKeyDescriptor]:
return fetch_api_keys(db_session)
@@ -28,7 +29,7 @@ def list_api_keys(
@router.post("")
def create_api_key(
api_key_args: APIKeyArgs,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> ApiKeyDescriptor:
return insert_api_key(db_session, api_key_args, user.id)
@@ -37,7 +38,7 @@ def create_api_key(
@router.post("/{api_key_id}/regenerate")
def regenerate_existing_api_key(
api_key_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> ApiKeyDescriptor:
return regenerate_api_key(db_session, api_key_id)
@@ -47,7 +48,7 @@ def regenerate_existing_api_key(
def update_existing_api_key(
api_key_id: int,
api_key_args: APIKeyArgs,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> ApiKeyDescriptor:
return update_api_key(db_session, api_key_id, api_key_args)
@@ -56,7 +57,7 @@ def update_existing_api_key(
@router.delete("/{api_key_id}")
def delete_api_key(
api_key_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
remove_api_key(db_session, api_key_id)

View File

@@ -4,7 +4,6 @@ from fastapi import FastAPI
from fastapi.dependencies.models import Dependant
from starlette.routing import BaseRoute
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_chat_accessible_user
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_limited_user
@@ -91,6 +90,11 @@ def is_route_in_spec_list(
return False
def _is_require_permission_dependency(fn: object) -> bool:
"""Detect closures generated by ``require_permission()``."""
return bool(getattr(fn, "_is_require_permission", False))
def check_router_auth(
application: FastAPI,
public_endpoint_specs: list[tuple[str, set[str]]] = PUBLIC_ENDPOINT_SPECS,
@@ -126,7 +130,6 @@ def check_router_auth(
if (
depends_fn == current_limited_user
or depends_fn == current_user
or depends_fn == current_admin_user
or depends_fn == current_curator_or_admin_user
or depends_fn == current_user_with_expired_token
or depends_fn == current_chat_accessible_user
@@ -134,6 +137,7 @@ def check_router_auth(
or depends_fn == control_plane_dep
or depends_fn == current_cloud_superuser
or depends_fn == verify_scim_token
or _is_require_permission_dependency(depends_fn)
):
found_auth = True
break

View File

@@ -10,8 +10,8 @@ from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.background.celery.tasks.pruning.tasks import (
try_creating_prune_generator_task,
)
@@ -38,6 +38,7 @@ from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import AccessType
from onyx.db.enums import ConnectorCredentialPairStatus
from onyx.db.enums import IndexingStatus
from onyx.db.enums import Permission
from onyx.db.enums import PermissionSyncStatus
from onyx.db.index_attempt import count_index_attempt_errors_for_cc_pair
from onyx.db.index_attempt import count_index_attempts_for_cc_pair
@@ -622,7 +623,7 @@ def associate_credential_to_connector(
def dissociate_credential_from_connector(
connector_id: int,
credential_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse[int]:
return remove_credential_from_connector(

View File

@@ -22,10 +22,9 @@ from pydantic import BaseModel
from sqlalchemy.orm import Session
from onyx.auth.email_utils import send_email
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_chat_accessible_user
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.background.celery.tasks.pruning.tasks import (
try_creating_prune_generator_task,
)
@@ -105,6 +104,7 @@ from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import AccessType
from onyx.db.enums import ConnectorCredentialPairStatus
from onyx.db.enums import IndexingMode
from onyx.db.enums import Permission
from onyx.db.enums import ProcessingMode
from onyx.db.federated import fetch_all_federated_connectors_parallel
from onyx.db.index_attempt import get_index_attempts_for_cc_pair
@@ -189,7 +189,8 @@ def check_google_app_gmail_credentials_exist(
@router.put("/admin/connector/gmail/app-credential")
def upsert_google_app_gmail_credentials(
app_credentials: GoogleAppCredentials, _: User = Depends(current_admin_user)
app_credentials: GoogleAppCredentials,
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> StatusResponse:
try:
upsert_google_app_cred(app_credentials, DocumentSource.GMAIL)
@@ -203,7 +204,7 @@ def upsert_google_app_gmail_credentials(
@router.delete("/admin/connector/gmail/app-credential")
def delete_google_app_gmail_credentials(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse:
try:
@@ -231,7 +232,8 @@ def check_google_app_credentials_exist(
@router.put("/admin/connector/google-drive/app-credential")
def upsert_google_app_credentials(
app_credentials: GoogleAppCredentials, _: User = Depends(current_admin_user)
app_credentials: GoogleAppCredentials,
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> StatusResponse:
try:
upsert_google_app_cred(app_credentials, DocumentSource.GOOGLE_DRIVE)
@@ -245,7 +247,7 @@ def upsert_google_app_credentials(
@router.delete("/admin/connector/google-drive/app-credential")
def delete_google_app_credentials(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse:
try:
@@ -277,7 +279,8 @@ def check_google_service_gmail_account_key_exist(
@router.put("/admin/connector/gmail/service-account-key")
def upsert_google_service_gmail_account_key(
service_account_key: GoogleServiceAccountKey, _: User = Depends(current_admin_user)
service_account_key: GoogleServiceAccountKey,
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> StatusResponse:
try:
upsert_service_account_key(service_account_key, DocumentSource.GMAIL)
@@ -291,7 +294,7 @@ def upsert_google_service_gmail_account_key(
@router.delete("/admin/connector/gmail/service-account-key")
def delete_google_service_gmail_account_key(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse:
try:
@@ -323,7 +326,8 @@ def check_google_service_account_key_exist(
@router.put("/admin/connector/google-drive/service-account-key")
def upsert_google_service_account_key(
service_account_key: GoogleServiceAccountKey, _: User = Depends(current_admin_user)
service_account_key: GoogleServiceAccountKey,
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> StatusResponse:
try:
upsert_service_account_key(service_account_key, DocumentSource.GOOGLE_DRIVE)
@@ -337,7 +341,7 @@ def upsert_google_service_account_key(
@router.delete("/admin/connector/google-drive/service-account-key")
def delete_google_service_account_key(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse:
try:
@@ -407,7 +411,7 @@ def upsert_gmail_service_account_credential(
@router.get("/admin/connector/google-drive/check-auth/{credential_id}")
def check_drive_tokens(
credential_id: int,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> AuthStatus:
db_credentials = fetch_credential_by_id_for_user(credential_id, user, db_session)
@@ -1794,7 +1798,9 @@ def connector_run_once(
@router.get("/connector/gmail/authorize/{credential_id}")
def gmail_auth(
response: Response, credential_id: str, _: User = Depends(current_user)
response: Response,
credential_id: str,
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> AuthUrl:
# set a cookie that we can read in the callback (used for `verify_csrf`)
response.set_cookie(
@@ -1808,7 +1814,9 @@ def gmail_auth(
@router.get("/connector/google-drive/authorize/{credential_id}")
def google_drive_auth(
response: Response, credential_id: str, _: User = Depends(current_user)
response: Response,
credential_id: str,
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> AuthUrl:
# set a cookie that we can read in the callback (used for `verify_csrf`)
response.set_cookie(
@@ -1826,7 +1834,7 @@ def google_drive_auth(
def gmail_callback(
request: Request,
callback: GmailCallback = Depends(),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse:
credential_id_cookie = request.cookies.get(_GMAIL_CREDENTIAL_ID_COOKIE_NAME)
@@ -1856,7 +1864,7 @@ def gmail_callback(
def google_drive_callback(
request: Request,
callback: GDriveCallback = Depends(),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse:
credential_id_cookie = request.cookies.get(_GOOGLE_DRIVE_CREDENTIAL_ID_COOKIE_NAME)
@@ -1885,7 +1893,7 @@ def google_drive_callback(
@router.get("/connector", tags=PUBLIC_API_TAGS)
def get_connectors(
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[ConnectorSnapshot]:
connectors = fetch_connectors(db_session)
@@ -1900,7 +1908,7 @@ def get_connectors(
@router.get("/indexed-sources", tags=PUBLIC_API_TAGS)
def get_indexed_sources(
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> IndexedSourcesResponse:
sources = sorted(
@@ -1912,7 +1920,7 @@ def get_indexed_sources(
@router.get("/connector/{connector_id}", tags=PUBLIC_API_TAGS)
def get_connector_by_id(
connector_id: int,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> ConnectorSnapshot | StatusResponse[int]:
connector = fetch_connector_by_id(connector_id, db_session)
@@ -1941,7 +1949,7 @@ def get_connector_by_id(
@router.post("/connector-request")
def submit_connector_request(
request_data: ConnectorRequestSubmission,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> StatusResponse:
"""
Submit a connector request for Cloud deployments.

View File

@@ -9,9 +9,8 @@ from fastapi import Query
from fastapi import UploadFile
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.connectors.factory import validate_ccpair_for_user
from onyx.db.credentials import alter_credential
@@ -26,6 +25,7 @@ from onyx.db.credentials import fetch_credentials_for_user
from onyx.db.credentials import swap_credentials_connector
from onyx.db.credentials import update_credential
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import DocumentSource
from onyx.db.models import User
from onyx.server.documents.models import CredentialBase
@@ -95,7 +95,7 @@ def get_cc_source_full_info(
@router.delete("/admin/credential/{credential_id}")
def delete_credential_by_id_admin(
credential_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse:
"""Same as the user endpoint, but can delete any credential (not just the user's own)"""
@@ -108,7 +108,7 @@ def delete_credential_by_id_admin(
@router.put("/admin/credential/swap")
def swap_credentials_for_connector(
credential_swap_req: CredentialSwapRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse:
validate_ccpair_for_user(
@@ -228,7 +228,7 @@ def create_credential_with_private_key(
@router.get("/credential")
def list_credentials(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[CredentialSnapshot]:
credentials = fetch_credentials_for_user(db_session=db_session, user=user)
@@ -241,7 +241,7 @@ def list_credentials(
@router.get("/credential/{credential_id}")
def get_credential_by_id(
credential_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> CredentialSnapshot | StatusResponse[int]:
credential = fetch_credential_by_id_for_user(
@@ -263,7 +263,7 @@ def get_credential_by_id(
def update_credential_data(
credential_id: int,
credential_update: CredentialDataUpdateRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> CredentialBase:
credential = alter_credential(
@@ -291,7 +291,7 @@ def update_credential_private_key(
uploaded_file: UploadFile = File(...),
field_key: str = Form(...),
type_definition_key: str = Form(...),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> CredentialBase:
try:
@@ -334,7 +334,7 @@ def update_credential_private_key(
def update_credential_from_model(
credential_id: int,
credential_data: CredentialBase,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> CredentialSnapshot | StatusResponse[int]:
updated_credential = update_credential(
@@ -369,7 +369,7 @@ def update_credential_from_model(
@router.delete("/credential/{credential_id}")
def delete_credential_by_id(
credential_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse:
delete_credential_for_user(
@@ -386,7 +386,7 @@ def delete_credential_by_id(
@router.delete("/credential/force/{credential_id}")
def force_delete_credential_by_id(
credential_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> StatusResponse:
delete_credential_for_user(credential_id, user, db_session, True)

View File

@@ -4,12 +4,13 @@ from fastapi import HTTPException
from fastapi import Query
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.context.search.models import IndexFilters
from onyx.context.search.preprocessing.access_filters import (
build_access_filters_for_user,
)
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.search_settings import get_current_search_settings
from onyx.document_index.factory import get_default_document_index
@@ -29,7 +30,7 @@ router = APIRouter(prefix="/document")
@router.get("/document-size-info", dependencies=[Depends(require_vector_db)])
def get_document_info(
document_id: str = Query(...),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> DocumentInfo:
search_settings = get_current_search_settings(db_session)
@@ -74,7 +75,7 @@ def get_document_info(
def get_chunk_info(
document_id: str = Query(...),
chunk_id: int = Query(...),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> ChunkInfo:
search_settings = get_current_search_settings(db_session)

View File

@@ -12,12 +12,13 @@ from pydantic import BaseModel
from pydantic import ValidationError
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.configs.constants import DocumentSource
from onyx.connectors.interfaces import OAuthConnector
from onyx.db.credentials import create_credential
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.redis.redis_pool import get_redis_client
from onyx.server.documents.models import CredentialBase
@@ -89,7 +90,7 @@ def oauth_authorize(
request: Request,
source: DocumentSource,
desired_return_url: Annotated[str | None, Query()] = None,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> AuthorizeResponse:
"""Initiates the OAuth flow by redirecting to the provider's auth page"""
@@ -141,7 +142,7 @@ def oauth_callback(
code: Annotated[str, Query()],
state: Annotated[str, Query()],
db_session: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> CallbackResponse:
"""Handles the OAuth callback and exchanges the code for tokens"""
oauth_connectors = _discover_oauth_connectors()
@@ -201,7 +202,7 @@ class OAuthDetails(BaseModel):
@router.get("/details/{source}")
def oauth_details(
source: DocumentSource,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> OAuthDetails:
oauth_connectors = _discover_oauth_connectors()

View File

@@ -13,13 +13,14 @@ from fastapi.responses import RedirectResponse
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import optional_user
from onyx.configs.constants import DocumentSource
from onyx.db.connector_credential_pair import get_connector_credential_pairs_for_user
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import ConnectorCredentialPairStatus
from onyx.db.enums import IndexingStatus
from onyx.db.enums import Permission
from onyx.db.enums import ProcessingMode
from onyx.db.enums import SharingScope
from onyx.db.index_attempt import get_latest_index_attempt_for_cc_pair_id
@@ -45,7 +46,9 @@ _TEMPLATES_DIR = Path(__file__).parent / "templates"
_WEBAPP_HMR_FIXER_TEMPLATE = (_TEMPLATES_DIR / "webapp_hmr_fixer.js").read_text()
def require_onyx_craft_enabled(user: User = Depends(current_user)) -> User:
def require_onyx_craft_enabled(
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> User:
"""
Dependency that checks if Onyx Craft is enabled for the user.
Raises HTTP 403 if Onyx Craft is disabled via feature flag.
@@ -73,7 +76,7 @@ router.include_router(user_library_router, tags=["build"])
@router.get("/limit", response_model=RateLimitResponse)
def get_rate_limit(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> RateLimitResponse:
"""Get rate limit information for the current user."""
@@ -87,7 +90,7 @@ def get_rate_limit(
@router.get("/connectors", response_model=BuildConnectorListResponse)
def get_build_connectors(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> BuildConnectorListResponse:
"""Get all connectors for the build admin panel.
@@ -518,7 +521,7 @@ def get_webapp(
@router.post("/sandbox/reset", response_model=None)
def reset_sandbox(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
"""Reset the user's sandbox by terminating it and cleaning up all sessions.

View File

@@ -9,10 +9,11 @@ from fastapi import HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.db.engine.sql_engine import get_session
from onyx.db.engine.sql_engine import get_session_with_current_tenant
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.server.features.build.api.models import MessageListResponse
from onyx.server.features.build.api.models import MessageRequest
@@ -30,7 +31,7 @@ router = APIRouter()
def check_build_rate_limits(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
"""
@@ -53,7 +54,7 @@ def check_build_rate_limits(
@router.get("/sessions/{session_id}/messages", tags=PUBLIC_API_TAGS)
def list_messages(
session_id: UUID,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> MessageListResponse:
"""Get all messages for a build session."""
@@ -73,7 +74,7 @@ def list_messages(
def send_message(
session_id: UUID,
request: MessageRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
_rate_limit_check: None = Depends(check_build_rate_limits),
) -> StreamingResponse:
"""

View File

@@ -11,9 +11,10 @@ from fastapi import UploadFile
from sqlalchemy import exists
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import BuildSessionStatus
from onyx.db.enums import Permission
from onyx.db.enums import SandboxStatus
from onyx.db.models import BuildMessage
from onyx.db.models import User
@@ -65,7 +66,7 @@ router = APIRouter(prefix="/sessions")
@router.get("", response_model=SessionListResponse)
def list_sessions(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> SessionListResponse:
"""List all build sessions for the current user."""
@@ -88,7 +89,7 @@ SESSION_CREATE_LOCK_TIMEOUT_SECONDS = 300
@router.post("", response_model=DetailedSessionResponse)
def create_session(
request: SessionCreateRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> DetailedSessionResponse:
"""
@@ -157,7 +158,7 @@ def create_session(
@router.get("/{session_id}", response_model=DetailedSessionResponse)
def get_session_details(
session_id: UUID,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> DetailedSessionResponse:
"""
@@ -195,7 +196,7 @@ def get_session_details(
)
def check_pre_provisioned_session(
session_id: UUID,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> PreProvisionedCheckResponse:
"""
@@ -228,7 +229,7 @@ def check_pre_provisioned_session(
@router.post("/{session_id}/generate-name", response_model=SessionNameGenerateResponse)
def generate_session_name(
session_id: UUID,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> SessionNameGenerateResponse:
"""Generate a session name using LLM based on the first user message."""
@@ -248,7 +249,7 @@ def generate_session_name(
def generate_suggestions(
session_id: UUID,
request: GenerateSuggestionsRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> GenerateSuggestionsResponse:
"""Generate follow-up suggestions based on the first exchange in a session."""
@@ -281,7 +282,7 @@ def generate_suggestions(
def update_session_name(
session_id: UUID,
request: SessionUpdateRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> SessionResponse:
"""Update the name of a build session."""
@@ -301,7 +302,7 @@ def update_session_name(
def set_session_public(
session_id: UUID,
request: SetSessionSharingRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> SetSessionSharingResponse:
"""Set the sharing scope of a build session's webapp."""
@@ -319,7 +320,7 @@ def set_session_public(
@router.delete("/{session_id}", response_model=None)
def delete_session(
session_id: UUID,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
"""Delete a build session and all associated data.
@@ -356,7 +357,7 @@ RESTORE_LOCK_TIMEOUT_SECONDS = 300
@router.post("/{session_id}/restore", response_model=DetailedSessionResponse)
def restore_session(
session_id: UUID,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> DetailedSessionResponse:
"""Restore sandbox and load session snapshot. Blocks until complete.
@@ -536,7 +537,7 @@ def restore_session(
)
def list_artifacts(
session_id: UUID,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[dict]:
"""List artifacts generated in the session."""
@@ -554,7 +555,7 @@ def list_artifacts(
def list_directory(
session_id: UUID,
path: str = "",
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> DirectoryListing:
"""
@@ -592,7 +593,7 @@ def list_directory(
def download_artifact(
session_id: UUID,
path: str,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
"""Download a specific artifact file."""
@@ -643,7 +644,7 @@ def download_artifact(
def export_docx(
session_id: UUID,
path: str,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
"""Export a markdown file as DOCX."""
@@ -685,7 +686,7 @@ def export_docx(
def get_pptx_preview(
session_id: UUID,
path: str,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> PptxPreviewResponse:
"""Generate slide image previews for a PPTX file."""
@@ -711,7 +712,7 @@ def get_pptx_preview(
@router.get("/{session_id}/webapp-info", response_model=WebappInfo)
def get_webapp_info(
session_id: UUID,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> WebappInfo:
"""
@@ -733,7 +734,7 @@ def get_webapp_info(
@router.get("/{session_id}/webapp-download")
def download_webapp(
session_id: UUID,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
"""
@@ -764,7 +765,7 @@ def download_webapp(
def download_directory(
session_id: UUID,
path: str,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
"""
@@ -801,7 +802,7 @@ def download_directory(
def upload_file_endpoint(
session_id: UUID,
file: UploadFile = File(...),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UploadResponse:
"""Upload a file to the session's sandbox.
@@ -852,7 +853,7 @@ def upload_file_endpoint(
def delete_file_endpoint(
session_id: UUID,
path: str,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
"""Delete a file from the session's sandbox.

View File

@@ -38,7 +38,7 @@ from fastapi import UploadFile
from pydantic import BaseModel
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.background.celery.versioned_apps.client import app as celery_app
from onyx.configs.constants import DocumentSource
from onyx.configs.constants import OnyxCeleryQueues
@@ -48,6 +48,7 @@ from onyx.db.document import upsert_document_by_connector_credential_pair
from onyx.db.document import upsert_documents
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import ConnectorCredentialPairStatus
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.document_index.interfaces import DocumentMetadata
from onyx.server.features.build.configs import USER_LIBRARY_MAX_FILE_SIZE_BYTES
@@ -281,7 +282,7 @@ def _store_and_track_file(
@router.get("/tree")
def get_library_tree(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[LibraryEntryResponse]:
"""Get user's uploaded files as a tree structure.
@@ -322,7 +323,7 @@ def get_library_tree(
async def upload_files(
files: list[UploadFile] = File(...),
path: str = Form("/"),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UploadResponse:
"""Upload files directly to S3 and track in PostgreSQL.
@@ -439,7 +440,7 @@ async def upload_files(
async def upload_zip(
file: UploadFile = File(...),
path: str = Form("/"),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UploadResponse:
"""Upload and extract a zip file, storing each extracted file to S3.
@@ -608,7 +609,7 @@ async def upload_zip(
@router.post("/directories")
def create_directory(
request: CreateDirectoryRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> LibraryEntryResponse:
"""Create a virtual directory.
@@ -658,7 +659,7 @@ def create_directory(
def toggle_file_sync(
document_id: str,
enabled: bool = Query(...),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> ToggleSyncResponse:
"""Enable/disable syncing a file to sandboxes.
@@ -710,7 +711,7 @@ def toggle_file_sync(
@router.delete("/files/{document_id}")
def delete_file(
document_id: str,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> DeleteFileResponse:
"""Delete a file from both S3 and the document table."""

View File

@@ -58,7 +58,7 @@ docker buildx build --platform linux/amd64,linux/arm64 \
1. **Build and push** the new image (see above)
2. **Update the ConfigMap** in `cloud-deployment-yamls/danswer/configmap/env-configmap.yaml`:
2. **Update the ConfigMap** in in the internal repo
```yaml
SANDBOX_CONTAINER_IMAGE: "onyxdotapp/sandbox:v0.1.x"
```

View File

@@ -5,8 +5,9 @@ from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.persona import get_default_assistant
from onyx.db.persona import update_default_assistant_configuration
@@ -22,7 +23,7 @@ router = APIRouter(prefix="/admin/default-assistant")
@router.get("/configuration")
def get_default_assistant_configuration(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> DefaultAssistantConfiguration:
"""Get the current default assistant configuration.
@@ -47,7 +48,7 @@ def get_default_assistant_configuration(
@router.patch("")
def update_default_assistant(
update_request: DefaultAssistantUpdateRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> DefaultAssistantConfiguration:
"""Update the default assistant configuration.

View File

@@ -4,8 +4,8 @@ from fastapi import HTTPException
from fastapi import Query
from sqlalchemy.orm import Session
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.background.celery.versioned_apps.client import app as client_app
from onyx.configs.app_configs import DISABLE_VECTOR_DB
from onyx.configs.constants import OnyxCeleryPriority
@@ -18,6 +18,7 @@ from onyx.db.document_set import insert_document_set
from onyx.db.document_set import mark_document_set_as_to_be_deleted
from onyx.db.document_set import update_document_set
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.server.features.document_set.models import CheckDocSetPublicRequest
from onyx.server.features.document_set.models import CheckDocSetPublicResponse
@@ -159,7 +160,7 @@ def delete_document_set(
@router.get("/document-set")
def list_document_sets_for_user(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
get_editable: bool = Query(
False, description="If true, return editable document sets"
@@ -174,7 +175,7 @@ def list_document_sets_for_user(
@router.get("/document-set-public")
def document_set_public(
check_public_request: CheckDocSetPublicRequest,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> CheckDocSetPublicResponse:
is_public = check_document_sets_are_public(

View File

@@ -4,11 +4,12 @@ from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.access.hierarchy_access import get_user_external_group_ids
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import ENABLE_OPENSEARCH_INDEXING_FOR_ONYX
from onyx.configs.constants import DocumentSource
from onyx.db.document import get_accessible_documents_for_hierarchy_node_paginated
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.hierarchy import get_accessible_hierarchy_nodes_for_source
from onyx.db.models import User
from onyx.db.opensearch_migration import get_opensearch_retrieval_state
@@ -58,7 +59,7 @@ def _get_user_access_info(user: User, db_session: Session) -> tuple[str, list[st
@router.get(HIERARCHY_NODES_LIST_PATH)
def list_accessible_hierarchy_nodes(
source: DocumentSource,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> HierarchyNodesResponse:
_require_opensearch(db_session)
@@ -85,7 +86,7 @@ def list_accessible_hierarchy_nodes(
@router.post(HIERARCHY_NODE_DOCUMENTS_PATH)
def list_accessible_hierarchy_node_documents(
documents_request: HierarchyNodeDocumentsRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> HierarchyNodeDocumentsResponse:
_require_opensearch(db_session)

View File

@@ -3,9 +3,9 @@ from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.input_prompt import disable_input_prompt_for_user
from onyx.db.input_prompt import fetch_input_prompt_by_id
from onyx.db.input_prompt import fetch_input_prompts_by_user
@@ -28,7 +28,7 @@ admin_router = APIRouter(prefix="/admin/input_prompt")
@basic_router.get("")
def list_input_prompts(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
include_public: bool = True,
db_session: Session = Depends(get_session),
) -> list[InputPromptSnapshot]:
@@ -43,7 +43,7 @@ def list_input_prompts(
@basic_router.get("/{input_prompt_id}")
def get_input_prompt(
input_prompt_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> InputPromptSnapshot:
input_prompt = fetch_input_prompt_by_id(
@@ -58,7 +58,7 @@ def get_input_prompt(
@basic_router.post("")
def create_input_prompt(
create_input_prompt_request: CreateInputPromptRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> InputPromptSnapshot:
input_prompt = insert_input_prompt(
@@ -82,7 +82,7 @@ def create_input_prompt(
def patch_input_prompt(
input_prompt_id: int,
update_input_prompt_request: UpdateInputPromptRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> InputPromptSnapshot:
try:
@@ -105,7 +105,7 @@ def patch_input_prompt(
@basic_router.delete("/{input_prompt_id}")
def delete_input_prompt(
input_prompt_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
delete_public: bool = False,
) -> None:
@@ -123,7 +123,7 @@ def delete_input_prompt(
@admin_router.delete("/{input_prompt_id}")
def delete_public_input_prompt(
input_prompt_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
try:
@@ -138,7 +138,7 @@ def delete_public_input_prompt(
@basic_router.post("/{input_prompt_id}/hide")
def hide_input_prompt_for_user(
input_prompt_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
"""

View File

@@ -25,9 +25,9 @@ from pydantic import AnyUrl
from pydantic import BaseModel
from sqlalchemy.orm import Session
from onyx.auth.permissions import require_permission
from onyx.auth.schemas import UserRole
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.db.engine.sql_engine import get_session
from onyx.db.engine.sql_engine import get_session_with_current_tenant
@@ -35,6 +35,7 @@ from onyx.db.enums import MCPAuthenticationPerformer
from onyx.db.enums import MCPAuthenticationType
from onyx.db.enums import MCPServerStatus
from onyx.db.enums import MCPTransport
from onyx.db.enums import Permission
from onyx.db.mcp import create_connection_config
from onyx.db.mcp import create_mcp_server__no_commit
from onyx.db.mcp import delete_all_user_connection_configs_for_server_no_commit
@@ -360,7 +361,7 @@ async def connect_admin_oauth(
async def connect_user_oauth(
request: MCPUserOAuthConnectRequest,
db: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> MCPUserOAuthConnectResponse:
return await _connect_oauth(request, db, is_admin=False, user=user)
@@ -572,7 +573,7 @@ async def _connect_oauth(
async def process_oauth_callback(
request: Request,
db_session: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> MCPOAuthCallbackResponse:
"""Complete OAuth flow by exchanging code for tokens and storing them.
@@ -655,7 +656,7 @@ async def process_oauth_callback(
def save_user_credentials(
request: MCPUserCredentialsRequest,
db_session: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> MCPApiKeyResponse:
"""Save user credentials for template-based MCP server authentication"""
@@ -983,7 +984,7 @@ def _db_mcp_server_to_api_mcp_server(
def get_mcp_servers_for_assistant(
assistant_id: str,
db: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> MCPServersResponse:
"""Get MCP servers for an assistant"""
@@ -1011,7 +1012,7 @@ def get_mcp_servers_for_assistant(
@router.get("/servers", response_model=MCPServersResponse)
def get_mcp_servers_for_user(
db: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> MCPServersResponse:
"""List all MCP servers for use in agent configuration and chat UI.
@@ -1140,7 +1141,7 @@ def get_mcp_server_tools_snapshots(
def user_list_mcp_tools_by_id(
server_id: int,
db: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> MCPToolListResponse:
return _list_mcp_tools_by_id(server_id, db, False, user)

View File

@@ -3,8 +3,9 @@ from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.notification import dismiss_notification
from onyx.db.notification import get_notification_by_id
@@ -22,7 +23,7 @@ router = APIRouter(prefix="/notifications")
@router.get("")
def get_notifications_api(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[NotificationModel]:
"""
@@ -58,7 +59,7 @@ def get_notifications_api(
@router.post("/{notification_id}/dismiss")
def dismiss_notification_endpoint(
notification_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
try:

View File

@@ -6,10 +6,11 @@ from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.oauth_token_manager import OAuthTokenManager
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import OAuthConfig
from onyx.db.models import User
from onyx.db.oauth_config import create_oauth_config
@@ -155,7 +156,7 @@ def delete_oauth_config_endpoint(
def initiate_oauth_flow(
request: OAuthInitiateRequest,
db_session: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> OAuthInitiateResponse:
"""
Initiate OAuth flow for the current user.
@@ -192,7 +193,7 @@ def handle_oauth_callback(
code: str,
state: str,
db_session: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> OAuthCallbackResponse:
"""
Handle OAuth callback after user authorizes the application.
@@ -253,7 +254,7 @@ def handle_oauth_callback(
def revoke_oauth_token(
oauth_config_id: int,
db_session: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> dict[str, str]:
"""
Revoke (delete) the current user's OAuth token for a specific OAuth config.

View File

@@ -4,12 +4,12 @@ from fastapi import HTTPException
from fastapi_users.exceptions import InvalidPasswordException
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import get_user_manager
from onyx.auth.users import User
from onyx.auth.users import UserManager
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.users import get_user_by_email
from onyx.server.features.password.models import ChangePasswordRequest
from onyx.server.features.password.models import UserResetRequest
@@ -22,7 +22,7 @@ router = APIRouter(prefix="/password")
async def change_my_password(
form_data: ChangePasswordRequest,
user_manager: UserManager = Depends(get_user_manager),
current_user: User = Depends(current_user),
current_user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> None:
"""
Change the password for the current user.
@@ -46,7 +46,7 @@ async def admin_reset_user_password(
user_reset_request: UserResetRequest,
user_manager: UserManager = Depends(get_user_manager),
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> UserResetResponse:
"""
Reset the password for a user (admin only).

View File

@@ -9,16 +9,16 @@ from pydantic import BaseModel
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_chat_accessible_user
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_limited_user
from onyx.auth.users import current_user
from onyx.configs.app_configs import DISABLE_VECTOR_DB
from onyx.configs.constants import FileOrigin
from onyx.configs.constants import MilestoneRecordType
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.persona import create_assistant_label
from onyx.db.persona import create_update_persona
@@ -150,7 +150,7 @@ def patch_persona_visibility(
def patch_user_persona_public_status(
persona_id: int,
is_public_request: IsPublicRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
try:
@@ -187,7 +187,7 @@ def patch_persona_featured_status(
@admin_agents_router.patch("/display-priorities")
def patch_agents_display_priorities(
display_priority_request: DisplayPriorityRequest,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
try:
@@ -265,7 +265,7 @@ def get_agents_admin_paginated(
@admin_router.patch("/{persona_id}/undelete", tags=PUBLIC_API_TAGS)
def undelete_persona(
persona_id: int,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
mark_persona_as_not_deleted(
@@ -279,7 +279,7 @@ def undelete_persona(
@admin_router.post("/upload-image")
def upload_file(
file: UploadFile,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> dict[str, str]:
file_store = get_default_file_store()
file_type = ChatFileType.IMAGE
@@ -298,7 +298,7 @@ def upload_file(
@basic_router.post("", tags=PUBLIC_API_TAGS)
def create_persona(
persona_upsert_request: PersonaUpsertRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> PersonaSnapshot:
tenant_id = get_current_tenant_id()
@@ -328,7 +328,7 @@ def create_persona(
def update_persona(
persona_id: int,
persona_upsert_request: PersonaUpsertRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> PersonaSnapshot:
_validate_user_knowledge_enabled(persona_upsert_request, "update")
@@ -350,7 +350,7 @@ class PersonaLabelPatchRequest(BaseModel):
@basic_router.get("/labels")
def get_labels(
db: Session = Depends(get_session),
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> list[PersonaLabelResponse]:
return [
PersonaLabelResponse.from_model(label)
@@ -362,7 +362,7 @@ def get_labels(
def create_label(
label: PersonaLabelCreate,
db: Session = Depends(get_session),
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> PersonaLabelResponse:
"""Create a new assistant label"""
try:
@@ -379,7 +379,7 @@ def create_label(
def patch_persona_label(
label_id: int,
persona_label_patch_request: PersonaLabelPatchRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_persona_label(
@@ -392,7 +392,7 @@ def patch_persona_label(
@admin_router.delete("/label/{label_id}")
def delete_label(
label_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
delete_persona_label(label_id=label_id, db_session=db_session)
@@ -410,7 +410,7 @@ class PersonaShareRequest(BaseModel):
def share_persona(
persona_id: int,
persona_share_request: PersonaShareRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
try:
@@ -434,7 +434,7 @@ def share_persona(
@basic_router.delete("/{persona_id}", tags=PUBLIC_API_TAGS)
def delete_persona(
persona_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
mark_persona_as_deleted(

View File

@@ -12,7 +12,7 @@ from fastapi import UploadFile
from pydantic import BaseModel
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import DISABLE_VECTOR_DB
from onyx.configs.constants import OnyxCeleryPriority
from onyx.configs.constants import OnyxCeleryQueues
@@ -20,6 +20,7 @@ from onyx.configs.constants import OnyxCeleryTask
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.configs.constants import USER_FILE_PROJECT_SYNC_MAX_QUEUE_DEPTH
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.enums import UserFileStatus
from onyx.db.models import ChatSession
from onyx.db.models import Project__UserFile
@@ -98,7 +99,7 @@ def _trigger_user_file_project_sync(
@router.get("", tags=PUBLIC_API_TAGS)
def get_projects(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[UserProjectSnapshot]:
user_id = user.id
@@ -111,7 +112,7 @@ def get_projects(
@router.post("/create", tags=PUBLIC_API_TAGS)
def create_project(
name: str,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UserProjectSnapshot:
if name == "":
@@ -129,7 +130,7 @@ def upload_user_files(
files: list[UploadFile] = File(...),
project_id: int | None = Form(None),
temp_id_map: str | None = Form(None), # JSON string mapping hashed key -> temp_id
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> CategorizedFilesSnapshot:
try:
@@ -168,7 +169,7 @@ def upload_user_files(
@router.get("/{project_id}", tags=PUBLIC_API_TAGS)
def get_project(
project_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UserProjectSnapshot:
user_id = user.id
@@ -185,7 +186,7 @@ def get_project(
@router.get("/files/{project_id}", tags=PUBLIC_API_TAGS)
def get_files_in_project(
project_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[UserFileSnapshot]:
user_id = user.id
@@ -208,7 +209,7 @@ def unlink_user_file_from_project(
project_id: int,
file_id: UUID,
bg_tasks: BackgroundTasks,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
"""Unlink an existing user file from a specific project for the current user.
@@ -253,7 +254,7 @@ def link_user_file_to_project(
project_id: int,
file_id: UUID,
bg_tasks: BackgroundTasks,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UserFileSnapshot:
"""Link an existing user file to a specific project for the current user.
@@ -300,7 +301,7 @@ class ProjectInstructionsResponse(BaseModel):
)
def get_project_instructions(
project_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> ProjectInstructionsResponse:
user_id = user.id
@@ -328,7 +329,7 @@ class UpsertProjectInstructionsRequest(BaseModel):
def upsert_project_instructions(
project_id: int,
body: UpsertProjectInstructionsRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> ProjectInstructionsResponse:
"""Create or update this project's instructions stored on the project itself."""
@@ -359,7 +360,7 @@ class ProjectPayload(BaseModel):
)
def get_project_details(
project_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> ProjectPayload:
project = get_project(project_id, user, db_session)
@@ -389,7 +390,7 @@ class UpdateProjectRequest(BaseModel):
def update_project(
project_id: int,
body: UpdateProjectRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UserProjectSnapshot:
user_id = user.id
@@ -414,7 +415,7 @@ def update_project(
@router.delete("/{project_id}", tags=PUBLIC_API_TAGS)
def delete_project(
project_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
user_id = user.id
@@ -443,7 +444,7 @@ def delete_project(
def delete_user_file(
file_id: UUID,
bg_tasks: BackgroundTasks,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UserFileDeleteResult:
"""Delete a user file belonging to the current user.
@@ -501,7 +502,7 @@ def delete_user_file(
@router.get("/file/{file_id}", response_model=UserFileSnapshot, tags=PUBLIC_API_TAGS)
def get_user_file(
file_id: UUID,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UserFileSnapshot:
"""Fetch a single user file by ID for the current user.
@@ -529,7 +530,7 @@ class UserFileIdsRequest(BaseModel):
)
def get_user_file_statuses(
body: UserFileIdsRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[UserFileSnapshot]:
"""Fetch statuses for a set of user file IDs owned by the current user.
@@ -555,7 +556,7 @@ def get_user_file_statuses(
def move_chat_session(
project_id: int,
body: ChatSessionRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
user_id = user.id
@@ -574,7 +575,7 @@ def move_chat_session(
@router.post("/remove_chat_session")
def remove_chat_session(
body: ChatSessionRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
user_id = user.id
@@ -593,7 +594,7 @@ def remove_chat_session(
@router.get("/session/{chat_session_id}/token-count", response_model=TokenCountResponse)
def get_chat_session_project_token_count(
chat_session_id: str,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> TokenCountResponse:
"""Return sum of token_count for all user files in the project linked to the given chat session.
@@ -621,7 +622,7 @@ def get_chat_session_project_token_count(
@router.get("/session/{chat_session_id}/files", tags=PUBLIC_API_TAGS)
def get_chat_session_project_files(
chat_session_id: str,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[UserFileSnapshot]:
"""Return user files for the project linked to the given chat session.
@@ -659,7 +660,7 @@ def get_chat_session_project_files(
@router.get("/{project_id}/token-count", response_model=TokenCountResponse)
def get_project_total_token_count(
project_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> TokenCountResponse:
"""Return sum of token_count for all user files in the given project for the current user."""

View File

@@ -6,11 +6,12 @@ from fastapi import HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
from onyx.auth.permissions import require_permission
from onyx.auth.schemas import UserRole
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import Tool
from onyx.db.models import User
from onyx.db.tools import create_tool__no_commit
@@ -219,7 +220,7 @@ def validate_tool(
@router.get("/openapi", tags=PUBLIC_API_TAGS)
def list_openapi_tools(
db_session: Session = Depends(get_session),
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> list[ToolSnapshot]:
tools = get_tools(db_session, only_openapi=True)
@@ -237,7 +238,7 @@ def list_openapi_tools(
def get_custom_tool(
tool_id: int,
db_session: Session = Depends(get_session),
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> ToolSnapshot:
try:
tool = get_tool_by_id(tool_id, db_session)
@@ -249,7 +250,7 @@ def get_custom_tool(
@router.get("", tags=PUBLIC_API_TAGS)
def list_tools(
db_session: Session = Depends(get_session),
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> list[ToolSnapshot]:
tools = get_tools(db_session, only_enabled=True, only_connected_mcp=True)

View File

@@ -6,8 +6,9 @@ from pydantic import BaseModel
from sqlalchemy.orm import Session
from onyx.auth.oauth_token_manager import OAuthTokenManager
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.oauth_config import get_all_user_oauth_tokens
@@ -23,7 +24,7 @@ class OAuthTokenStatus(BaseModel):
@router.get("/status")
def get_user_oauth_token_status(
db_session: Session = Depends(get_session),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> list[OAuthTokenStatus]:
"""
Get the OAuth token status for the current user across all OAuth configs.

View File

@@ -2,9 +2,10 @@ from fastapi import APIRouter
from fastapi import Depends
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.web_search import fetch_active_web_content_provider
from onyx.db.web_search import fetch_active_web_search_provider
@@ -225,7 +226,7 @@ def _open_urls(
@router.post("/search", response_model=WebSearchWithContentResponse)
def execute_web_search(
request: WebSearchToolRequest,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> WebSearchWithContentResponse:
"""
@@ -268,7 +269,7 @@ def execute_web_search(
@router.post("/search-lite", response_model=WebSearchToolResponse)
def execute_web_search_lite(
request: WebSearchToolRequest,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> WebSearchToolResponse:
"""
@@ -284,7 +285,7 @@ def execute_web_search_lite(
@router.post("/open-urls", response_model=OpenUrlsToolResponse)
def execute_open_urls(
request: OpenUrlsToolRequest,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> OpenUrlsToolResponse:
"""

View File

@@ -9,10 +9,11 @@ from fastapi import Request
from fastapi import Response
from sqlalchemy.orm import Session
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.configs.constants import FederatedConnectorSource
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.federated import (
create_federated_connector as db_create_federated_connector,
)
@@ -319,7 +320,7 @@ def validate_entities(
@router.get("/{id}/authorize")
def get_authorize_url(
id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> AuthorizeUrlResponse:
"""Get URL to send the user for OAuth"""
@@ -368,7 +369,7 @@ def get_authorize_url(
@router.post("/callback")
def handle_oauth_callback_generic(
request: Request,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> OAuthCallbackResult:
"""Handle callback for any federated connector using state parameter"""
@@ -445,7 +446,7 @@ def handle_oauth_callback_generic(
@router.get("")
def get_federated_connectors(
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[FederatedConnectorStatus]:
"""Get all federated connectors for display in the status table"""
@@ -465,7 +466,7 @@ def get_federated_connectors(
@router.get("/oauth-status")
def get_user_oauth_status(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[UserOAuthStatus]:
"""Get OAuth status for all federated connectors for the current user"""
@@ -610,7 +611,7 @@ def delete_federated_connector_endpoint(
@router.delete("/{id}/oauth")
def disconnect_oauth_token(
id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> bool:
"""Disconnect OAuth token for the current user from a federated connector"""

View File

@@ -2,13 +2,14 @@ from fastapi import APIRouter
from fastapi import Depends
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.configs.constants import TMP_DRALPHA_PERSONA_NAME
from onyx.configs.kg_configs import KG_BETA_ASSISTANT_DESCRIPTION
from onyx.db.engine.sql_engine import get_session
from onyx.db.entities import get_entity_stats_by_grounded_source_name
from onyx.db.entity_type import get_configured_entity_types
from onyx.db.entity_type import update_entity_types_and_related_connectors__commit
from onyx.db.enums import Permission
from onyx.db.kg_config import disable_kg
from onyx.db.kg_config import enable_kg
from onyx.db.kg_config import get_kg_config_settings
@@ -47,7 +48,9 @@ admin_router = APIRouter(prefix="/admin/kg")
@admin_router.get("/exposed")
def get_kg_exposed(_: User = Depends(current_admin_user)) -> bool:
def get_kg_exposed(
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> bool:
kg_config_settings = get_kg_config_settings()
return kg_config_settings.KG_EXPOSED
@@ -57,7 +60,7 @@ def get_kg_exposed(_: User = Depends(current_admin_user)) -> bool:
@admin_router.put("/reset")
def reset_kg(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> SourceAndEntityTypeView:
reset_full_kg_index__commit(db_session)
@@ -69,7 +72,9 @@ def reset_kg(
@admin_router.get("/config")
def get_kg_config(_: User = Depends(current_admin_user)) -> KGConfig:
def get_kg_config(
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> KGConfig:
config = get_kg_config_settings()
return KGConfigAPIModel.from_kg_config_settings(config)
@@ -77,7 +82,7 @@ def get_kg_config(_: User = Depends(current_admin_user)) -> KGConfig:
@admin_router.put("/config")
def enable_or_disable_kg(
req: EnableKGConfigRequest | DisableKGConfigRequest,
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
if isinstance(req, DisableKGConfigRequest):
@@ -163,7 +168,7 @@ def enable_or_disable_kg(
@admin_router.get("/entity-types")
def get_kg_entity_types(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> SourceAndEntityTypeView:
# when using for the first time, populate with default entity types
@@ -194,7 +199,7 @@ def get_kg_entity_types(
@admin_router.put("/entity-types")
def update_kg_entity_types(
updates: list[EntityType],
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_entity_types_and_related_connectors__commit(

View File

@@ -8,7 +8,7 @@ from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_curator_or_admin_user
from onyx.background.celery.versioned_apps.client import app as client_app
from onyx.configs.app_configs import GENERATIVE_MODEL_ACCESS_CHECK_FREQ
@@ -23,6 +23,7 @@ from onyx.db.connector_credential_pair import (
)
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import ConnectorCredentialPairStatus
from onyx.db.enums import Permission
from onyx.db.feedback import fetch_docs_ranked_by_boost_for_user
from onyx.db.feedback import update_document_boost_for_user
from onyx.db.feedback import update_document_hidden_for_user
@@ -105,7 +106,7 @@ def document_hidden_update(
@router.get("/admin/genai-api-key/validate")
def validate_existing_genai_api_key(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
# Only validate every so often
kv_store = get_kv_store()

View File

@@ -2,10 +2,11 @@ from fastapi import APIRouter
from fastapi import Depends
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.db.code_interpreter import fetch_code_interpreter_server
from onyx.db.code_interpreter import update_code_interpreter_server_enabled
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.server.manage.code_interpreter.models import CodeInterpreterServer
from onyx.server.manage.code_interpreter.models import CodeInterpreterServerHealth
@@ -18,7 +19,7 @@ admin_router = APIRouter(prefix="/admin/code-interpreter")
@admin_router.get("/health")
def get_code_interpreter_health(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> CodeInterpreterServerHealth:
try:
client = CodeInterpreterClient()
@@ -29,7 +30,8 @@ def get_code_interpreter_health(
@admin_router.get("")
def get_code_interpreter(
_: User = Depends(current_admin_user), db_session: Session = Depends(get_session)
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> CodeInterpreterServer:
ci_server = fetch_code_interpreter_server(db_session)
return CodeInterpreterServer(enabled=ci_server.server_enabled)
@@ -38,7 +40,7 @@ def get_code_interpreter(
@admin_router.put("")
def update_code_interpreter(
update: CodeInterpreterServer,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_code_interpreter_server_enabled(

View File

@@ -6,7 +6,7 @@ from fastapi import HTTPException
from fastapi import status
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import AUTH_TYPE
from onyx.configs.app_configs import DISCORD_BOT_TOKEN
from onyx.configs.constants import AuthType
@@ -23,6 +23,7 @@ from onyx.db.discord_bot import get_guild_configs
from onyx.db.discord_bot import update_discord_channel_config
from onyx.db.discord_bot import update_guild_config
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.server.manage.discord_bot.models import DiscordBotConfigCreateRequest
from onyx.server.manage.discord_bot.models import DiscordBotConfigResponse
@@ -64,7 +65,7 @@ def _check_bot_config_api_access() -> None:
@router.get("/config", response_model=DiscordBotConfigResponse)
def get_bot_config(
_: None = Depends(_check_bot_config_api_access),
__: User = Depends(current_admin_user),
__: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> DiscordBotConfigResponse:
"""Get Discord bot config. Returns 403 on Cloud or if env vars set."""
@@ -82,7 +83,7 @@ def get_bot_config(
def create_bot_request(
request: DiscordBotConfigCreateRequest,
_: None = Depends(_check_bot_config_api_access),
__: User = Depends(current_admin_user),
__: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> DiscordBotConfigResponse:
"""Create Discord bot config. Returns 403 on Cloud or if env vars set."""
@@ -108,7 +109,7 @@ def create_bot_request(
@router.delete("/config")
def delete_bot_config_endpoint(
_: None = Depends(_check_bot_config_api_access),
__: User = Depends(current_admin_user),
__: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict:
"""Delete Discord bot config.
@@ -131,7 +132,7 @@ def delete_bot_config_endpoint(
@router.delete("/service-api-key")
def delete_service_api_key_endpoint(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict:
"""Delete the Discord service API key.
@@ -154,7 +155,7 @@ def delete_service_api_key_endpoint(
@router.get("/guilds", response_model=list[DiscordGuildConfigResponse])
def list_guild_configs(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[DiscordGuildConfigResponse]:
"""List all guild configs (pending and registered)."""
@@ -164,7 +165,7 @@ def list_guild_configs(
@router.post("/guilds", response_model=DiscordGuildConfigCreateResponse)
def create_guild_request(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> DiscordGuildConfigCreateResponse:
"""Create new guild config with registration key. Key shown once."""
@@ -183,7 +184,7 @@ def create_guild_request(
@router.get("/guilds/{config_id}", response_model=DiscordGuildConfigResponse)
def get_guild_config(
config_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> DiscordGuildConfigResponse:
"""Get specific guild config."""
@@ -197,7 +198,7 @@ def get_guild_config(
def update_guild_request(
config_id: int,
request: DiscordGuildConfigUpdateRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> DiscordGuildConfigResponse:
"""Update guild config."""
@@ -219,7 +220,7 @@ def update_guild_request(
@router.delete("/guilds/{config_id}")
def delete_guild_request(
config_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict:
"""Delete guild config (invalidates registration key).
@@ -248,7 +249,7 @@ def delete_guild_request(
)
def list_channel_configs(
config_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[DiscordChannelConfigResponse]:
"""List whitelisted channels for a guild."""
@@ -270,7 +271,7 @@ def update_channel_request(
guild_config_id: int,
channel_config_id: int,
request: DiscordChannelConfigUpdateRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> DiscordChannelConfigResponse:
"""Update channel config."""

View File

@@ -2,8 +2,9 @@ from fastapi import APIRouter
from fastapi import Depends
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.llm import fetch_existing_embedding_providers
from onyx.db.llm import remove_embedding_provider
from onyx.db.llm import upsert_cloud_embedding_provider
@@ -34,7 +35,7 @@ basic_router = APIRouter(prefix="/embedding")
@admin_router.post("/test-embedding")
def test_embedding_configuration(
test_llm_request: TestEmbeddingRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
try:
test_model = EmbeddingModel(
@@ -65,7 +66,7 @@ def test_embedding_configuration(
@admin_router.get("", response_model=list[EmbeddingModelDetail])
def list_embedding_models(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[EmbeddingModelDetail]:
search_settings = get_all_search_settings(db_session)
@@ -74,7 +75,7 @@ def list_embedding_models(
@admin_router.get("/embedding-provider")
def list_embedding_providers(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[CloudEmbeddingProvider]:
return [
@@ -86,7 +87,7 @@ def list_embedding_providers(
@admin_router.delete("/embedding-provider/{provider_type}")
def delete_embedding_provider(
provider_type: EmbeddingProvider,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
embedding_provider = get_current_db_embedding_provider(db_session=db_session)
@@ -105,7 +106,7 @@ def delete_embedding_provider(
@admin_router.put("/embedding-provider")
def put_cloud_embedding_provider(
provider: CloudEmbeddingProviderCreationRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> CloudEmbeddingProvider:
return upsert_cloud_embedding_provider(db_session, provider)

View File

@@ -3,8 +3,9 @@ from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.image_generation import create_image_generation_config__no_commit
from onyx.db.image_generation import delete_image_generation_config__no_commit
from onyx.db.image_generation import get_all_image_generation_configs
@@ -194,7 +195,7 @@ def _create_image_gen_llm_provider__no_commit(
@admin_router.post("/test")
def test_image_generation(
test_request: TestImageGenerationRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
"""Test if an API key is valid for image generation.
@@ -291,7 +292,7 @@ def test_image_generation(
@admin_router.post("/config")
def create_config(
config_create: ImageGenerationConfigCreate,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> ImageGenerationConfigView:
"""Create a new image generation configuration.
@@ -353,7 +354,7 @@ def create_config(
@admin_router.get("/config")
def get_all_configs(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[ImageGenerationConfigView]:
"""Get all image generation configurations."""
@@ -364,7 +365,7 @@ def get_all_configs(
@admin_router.get("/config/{image_provider_id}/credentials")
def get_config_credentials(
image_provider_id: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> ImageGenerationCredentials:
"""Get the credentials for an image generation config (for edit mode).
@@ -385,7 +386,7 @@ def get_config_credentials(
def update_config(
image_provider_id: str,
config_update: ImageGenerationConfigUpdate,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> ImageGenerationConfigView:
"""Update an image generation configuration.
@@ -481,7 +482,7 @@ def update_config(
@admin_router.delete("/config/{image_provider_id}")
def delete_config(
image_provider_id: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
"""Delete an image generation configuration and its associated LLM provider."""
@@ -512,7 +513,7 @@ def delete_config(
@admin_router.post("/config/{image_provider_id}/default")
def set_config_as_default(
image_provider_id: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
"""Set a configuration as the default for image generation."""
@@ -525,7 +526,7 @@ def set_config_as_default(
@admin_router.delete("/config/{image_provider_id}/default")
def unset_config_as_default(
image_provider_id: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
"""Unset a configuration as the default for image generation."""

View File

@@ -15,11 +15,12 @@ from fastapi import Query
from pydantic import ValidationError
from sqlalchemy.orm import Session
from onyx.auth.permissions import require_permission
from onyx.auth.schemas import UserRole
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_chat_accessible_user
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import LLMModelFlowType
from onyx.db.enums import Permission
from onyx.db.llm import can_user_access_llm_provider
from onyx.db.llm import fetch_default_llm_model
from onyx.db.llm import fetch_default_vision_model
@@ -39,6 +40,8 @@ from onyx.db.models import User
from onyx.db.persona import user_can_access_persona
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.llm.constants import PROVIDER_DISPLAY_NAMES
from onyx.llm.constants import WELL_KNOWN_PROVIDER_NAMES
from onyx.llm.factory import get_default_llm
from onyx.llm.factory import get_llm
from onyx.llm.factory import get_max_input_tokens_from_llm_provider
@@ -59,6 +62,7 @@ from onyx.server.manage.llm.models import BedrockFinalModelResponse
from onyx.server.manage.llm.models import BedrockModelsRequest
from onyx.server.manage.llm.models import BifrostFinalModelResponse
from onyx.server.manage.llm.models import BifrostModelsRequest
from onyx.server.manage.llm.models import CustomProviderOption
from onyx.server.manage.llm.models import DefaultModel
from onyx.server.manage.llm.models import LitellmFinalModelResponse
from onyx.server.manage.llm.models import LitellmModelDetails
@@ -74,6 +78,8 @@ from onyx.server.manage.llm.models import ModelConfigurationUpsertRequest
from onyx.server.manage.llm.models import OllamaFinalModelResponse
from onyx.server.manage.llm.models import OllamaModelDetails
from onyx.server.manage.llm.models import OllamaModelsRequest
from onyx.server.manage.llm.models import OpenAICompatibleFinalModelResponse
from onyx.server.manage.llm.models import OpenAICompatibleModelsRequest
from onyx.server.manage.llm.models import OpenRouterFinalModelResponse
from onyx.server.manage.llm.models import OpenRouterModelDetails
from onyx.server.manage.llm.models import OpenRouterModelsRequest
@@ -247,9 +253,32 @@ def _validate_llm_provider_change(
)
@admin_router.get("/custom-provider-names")
def fetch_custom_provider_names(
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> list[CustomProviderOption]:
"""Returns the sorted list of LiteLLM provider names that can be used
with the custom provider modal (i.e. everything that is not already
covered by a well-known provider modal)."""
import litellm
well_known = {p.value for p in WELL_KNOWN_PROVIDER_NAMES}
return sorted(
(
CustomProviderOption(
value=name,
label=PROVIDER_DISPLAY_NAMES.get(name, name.replace("_", " ").title()),
)
for name in litellm.models_by_provider.keys()
if name not in well_known
),
key=lambda o: o.label.lower(),
)
@admin_router.get("/built-in/options")
def fetch_llm_options(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> list[WellKnownLLMProviderDescriptor]:
return fetch_available_well_known_llms()
@@ -257,7 +286,7 @@ def fetch_llm_options(
@admin_router.get("/built-in/options/{provider_name}")
def fetch_llm_provider_options(
provider_name: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> WellKnownLLMProviderDescriptor:
well_known_llms = fetch_available_well_known_llms()
for well_known_llm in well_known_llms:
@@ -269,7 +298,7 @@ def fetch_llm_provider_options(
@admin_router.post("/test")
def test_llm_configuration(
test_llm_request: TestLLMRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
"""Test LLM configuration settings"""
@@ -327,7 +356,7 @@ def test_llm_configuration(
@admin_router.post("/test/default")
def test_default_provider(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
try:
llm = get_default_llm()
@@ -343,7 +372,7 @@ def test_default_provider(
@admin_router.get("/provider")
def list_llm_providers(
include_image_gen: bool = Query(False),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> LLMProviderResponse[LLMProviderView]:
start_time = datetime.now(timezone.utc)
@@ -388,7 +417,7 @@ def put_llm_provider(
False,
description="True if creating a new one, False if updating an existing provider",
),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> LLMProviderView:
# validate request (e.g. if we're intending to create but the name already exists we should throw an error)
@@ -526,7 +555,7 @@ def put_llm_provider(
def delete_llm_provider(
provider_id: int,
force: bool = Query(False),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
if not force:
@@ -547,7 +576,7 @@ def delete_llm_provider(
@admin_router.post("/default")
def set_provider_as_default(
default_model_request: DefaultModel,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_default_provider(
@@ -560,7 +589,7 @@ def set_provider_as_default(
@admin_router.post("/default-vision")
def set_provider_as_default_vision(
default_model: DefaultModel,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_default_vision_provider(
@@ -572,7 +601,7 @@ def set_provider_as_default_vision(
@admin_router.get("/auto-config")
def get_auto_config(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> dict:
"""Get the current Auto mode configuration from GitHub.
@@ -590,7 +619,7 @@ def get_auto_config(
@admin_router.get("/vision-providers")
def get_vision_capable_providers(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> LLMProviderResponse[VisionProviderResponse]:
"""Return a list of LLM providers and their models that support image input"""
@@ -821,7 +850,7 @@ def list_llm_providers_for_persona(
@admin_router.get("/provider-contextual-cost")
def get_provider_contextual_cost(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[LLMCost]:
"""
@@ -870,7 +899,7 @@ def get_provider_contextual_cost(
@admin_router.post("/bedrock/available-models")
def get_bedrock_available_models(
request: BedrockModelsRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[BedrockFinalModelResponse]:
"""Fetch available Bedrock models for a specific region and credentials.
@@ -1045,7 +1074,7 @@ def _get_ollama_available_model_names(api_base: str) -> set[str]:
@admin_router.post("/ollama/available-models")
def get_ollama_available_models(
request: OllamaModelsRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[OllamaFinalModelResponse]:
"""Fetch the list of available models from an Ollama server."""
@@ -1169,7 +1198,7 @@ def _get_openrouter_models_response(api_base: str, api_key: str) -> dict:
@admin_router.post("/openrouter/available-models")
def get_openrouter_available_models(
request: OpenRouterModelsRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[OpenRouterFinalModelResponse]:
"""Fetch available models from OpenRouter `/models` endpoint.
@@ -1250,7 +1279,7 @@ def get_openrouter_available_models(
@admin_router.post("/lm-studio/available-models")
def get_lm_studio_available_models(
request: LMStudioModelsRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[LMStudioFinalModelResponse]:
"""Fetch available models from an LM Studio server.
@@ -1357,7 +1386,7 @@ def get_lm_studio_available_models(
@admin_router.post("/litellm/available-models")
def get_litellm_available_models(
request: LitellmModelsRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[LitellmFinalModelResponse]:
"""Fetch available models from Litellm proxy /v1/models endpoint."""
@@ -1490,7 +1519,7 @@ def _get_openai_compatible_models_response(
@admin_router.post("/bifrost/available-models")
def get_bifrost_available_models(
request: BifrostModelsRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[BifrostFinalModelResponse]:
"""Fetch available models from Bifrost gateway /v1/models endpoint."""
@@ -1575,3 +1604,95 @@ def _get_bifrost_models_response(api_base: str, api_key: str | None = None) -> d
source_name="Bifrost",
api_key=api_key,
)
@admin_router.post("/openai-compatible/available-models")
def get_openai_compatible_server_available_models(
request: OpenAICompatibleModelsRequest,
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[OpenAICompatibleFinalModelResponse]:
"""Fetch available models from a generic OpenAI-compatible /v1/models endpoint."""
response_json = _get_openai_compatible_server_response(
api_base=request.api_base, api_key=request.api_key
)
models = response_json.get("data", [])
if not isinstance(models, list) or len(models) == 0:
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"No models found from your OpenAI-compatible endpoint",
)
results: list[OpenAICompatibleFinalModelResponse] = []
for model in models:
try:
model_id = model.get("id", "")
model_name = model.get("name", model_id)
if not model_id:
continue
# Skip embedding models
if is_embedding_model(model_id):
continue
results.append(
OpenAICompatibleFinalModelResponse(
name=model_id,
display_name=model_name,
max_input_tokens=model.get("context_length"),
supports_image_input=infer_vision_support(model_id),
supports_reasoning=is_reasoning_model(model_id, model_name),
)
)
except Exception as e:
logger.warning(
"Failed to parse OpenAI-compatible model entry",
extra={"error": str(e), "item": str(model)[:1000]},
)
if not results:
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"No compatible models found from OpenAI-compatible endpoint",
)
sorted_results = sorted(results, key=lambda m: m.name.lower())
# Sync new models to DB if provider_name is specified
if request.provider_name:
_sync_fetched_models(
db_session=db_session,
provider_name=request.provider_name,
models=[
SyncModelEntry(
name=r.name,
display_name=r.display_name,
max_input_tokens=r.max_input_tokens,
supports_image_input=r.supports_image_input,
)
for r in sorted_results
],
source_label="OpenAI Compatible",
)
return sorted_results
def _get_openai_compatible_server_response(
api_base: str, api_key: str | None = None
) -> dict:
"""Perform GET to an OpenAI-compatible /v1/models and return parsed JSON."""
cleaned_api_base = api_base.strip().rstrip("/")
# Ensure we hit /v1/models
if cleaned_api_base.endswith("/v1"):
url = f"{cleaned_api_base}/models"
else:
url = f"{cleaned_api_base}/v1/models"
return _get_openai_compatible_models_response(
url=url,
source_name="OpenAI Compatible",
api_key=api_key,
)

View File

@@ -28,6 +28,13 @@ if TYPE_CHECKING:
T = TypeVar("T", "LLMProviderDescriptor", "LLMProviderView", "VisionProviderResponse")
class CustomProviderOption(BaseModel):
"""A provider slug + human-friendly label for the custom-provider picker."""
value: str
label: str
class TestLLMRequest(BaseModel):
# provider level
id: int | None = None
@@ -79,7 +86,9 @@ class LLMProviderDescriptor(BaseModel):
provider=provider,
provider_display_name=get_provider_display_name(provider),
model_configurations=filter_model_configurations(
llm_provider_model.model_configurations, provider
llm_provider_model.model_configurations,
provider,
use_stored_display_name=llm_provider_model.custom_config is not None,
),
)
@@ -156,7 +165,9 @@ class LLMProviderView(LLMProvider):
personas=personas,
deployment_name=llm_provider_model.deployment_name,
model_configurations=filter_model_configurations(
llm_provider_model.model_configurations, provider
llm_provider_model.model_configurations,
provider,
use_stored_display_name=llm_provider_model.custom_config is not None,
),
)
@@ -198,13 +209,13 @@ class ModelConfigurationView(BaseModel):
cls,
model_configuration_model: "ModelConfigurationModel",
provider_name: str,
use_stored_display_name: bool = False,
) -> "ModelConfigurationView":
# For dynamic providers (OpenRouter, Bedrock, Ollama), use the display_name
# stored in DB from the source API. Skip LiteLLM parsing entirely.
# For dynamic providers (OpenRouter, Bedrock, Ollama) and custom-config
# providers, use the display_name stored in DB. Skip LiteLLM parsing.
if (
provider_name in DYNAMIC_LLM_PROVIDERS
and model_configuration_model.display_name
):
provider_name in DYNAMIC_LLM_PROVIDERS or use_stored_display_name
) and model_configuration_model.display_name:
# Extract vendor from model name for grouping (e.g., "Anthropic", "OpenAI")
vendor = extract_vendor_from_model_name(
model_configuration_model.name, provider_name
@@ -464,3 +475,18 @@ class BifrostFinalModelResponse(BaseModel):
max_input_tokens: int | None
supports_image_input: bool
supports_reasoning: bool
# OpenAI Compatible dynamic models fetch
class OpenAICompatibleModelsRequest(BaseModel):
api_base: str
api_key: str | None = None
provider_name: str | None = None # Optional: to save models to existing provider
class OpenAICompatibleFinalModelResponse(BaseModel):
name: str # Model ID (e.g. "meta-llama/Llama-3-8B-Instruct")
display_name: str # Human-readable name from API
max_input_tokens: int | None
supports_image_input: bool
supports_reasoning: bool

View File

@@ -26,6 +26,7 @@ DYNAMIC_LLM_PROVIDERS = frozenset(
LlmProviderNames.OLLAMA_CHAT,
LlmProviderNames.LM_STUDIO,
LlmProviderNames.BIFROST,
LlmProviderNames.OPENAI_COMPATIBLE,
}
)
@@ -308,12 +309,15 @@ def should_filter_as_dated_duplicate(
def filter_model_configurations(
model_configurations: list,
provider: str,
use_stored_display_name: bool = False,
) -> list:
"""Filter out obsolete and dated duplicate models from configurations.
Args:
model_configurations: List of ModelConfiguration DB models
provider: The provider name (e.g., "openai", "anthropic")
use_stored_display_name: If True, prefer the display_name stored in the
DB over LiteLLM enrichments. Set for custom-config providers.
Returns:
List of ModelConfigurationView objects with obsolete/duplicate models removed
@@ -333,7 +337,9 @@ def filter_model_configurations(
if should_filter_as_dated_duplicate(model_configuration.name, all_model_names):
continue
filtered_configs.append(
ModelConfigurationView.from_model(model_configuration, provider)
ModelConfigurationView.from_model(
model_configuration, provider, use_stored_display_name
)
)
return filtered_configs

View File

@@ -2,8 +2,9 @@ from fastapi import APIRouter
from fastapi import Depends
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.opensearch_migration import get_opensearch_migration_state
from onyx.db.opensearch_migration import get_opensearch_retrieval_state
@@ -23,7 +24,7 @@ admin_router = APIRouter(prefix="/admin/opensearch-migration")
@admin_router.get("/status")
def get_opensearch_migration_status(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> OpenSearchMigrationStatusResponse:
(
@@ -42,7 +43,7 @@ def get_opensearch_migration_status(
@admin_router.get("/retrieval")
def get_opensearch_retrieval_status(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> OpenSearchRetrievalStatusResponse:
enable_opensearch_retrieval = get_opensearch_retrieval_state(db_session)
@@ -54,7 +55,7 @@ def get_opensearch_retrieval_status(
@admin_router.put("/retrieval")
def set_opensearch_retrieval_status(
request: OpenSearchRetrievalStatusRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> OpenSearchRetrievalStatusResponse:
set_enable_opensearch_retrieval_with_commit(

View File

@@ -4,14 +4,14 @@ from fastapi import HTTPException
from fastapi import status
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.configs.app_configs import DISABLE_INDEX_UPDATE_ON_SWAP
from onyx.context.search.models import SavedSearchSettings
from onyx.context.search.models import SearchSettingsCreationRequest
from onyx.db.connector_credential_pair import get_connector_credential_pairs
from onyx.db.connector_credential_pair import resync_cc_pair
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.index_attempt import expire_index_attempts
from onyx.db.llm import fetch_existing_llm_provider
from onyx.db.llm import update_default_contextual_model
@@ -46,7 +46,7 @@ logger = setup_logger()
@router.post("/set-new-search-settings", dependencies=[Depends(require_vector_db)])
def set_new_search_settings(
search_settings_new: SearchSettingsCreationRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> IdReturn:
"""
@@ -146,7 +146,7 @@ def set_new_search_settings(
@router.post("/cancel-new-embedding", dependencies=[Depends(require_vector_db)])
def cancel_new_embedding(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
secondary_search_settings = get_secondary_search_settings(db_session)
@@ -179,7 +179,7 @@ def cancel_new_embedding(
@router.delete("/delete-search-settings")
def delete_search_settings_endpoint(
deletion_request: SearchSettingsDeleteRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
try:
@@ -193,7 +193,7 @@ def delete_search_settings_endpoint(
@router.get("/get-current-search-settings")
def get_current_search_settings_endpoint(
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> SavedSearchSettings:
current_search_settings = get_current_search_settings(db_session)
@@ -202,7 +202,7 @@ def get_current_search_settings_endpoint(
@router.get("/get-secondary-search-settings")
def get_secondary_search_settings_endpoint(
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> SavedSearchSettings | None:
secondary_search_settings = get_secondary_search_settings(db_session)
@@ -214,7 +214,7 @@ def get_secondary_search_settings_endpoint(
@router.get("/get-all-search-settings")
def get_all_search_settings(
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> FullModelVersionResponse:
current_search_settings = get_current_search_settings(db_session)
@@ -233,7 +233,7 @@ def get_all_search_settings(
@router.post("/update-inference-settings")
def update_saved_search_settings(
search_settings: SavedSearchSettings,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
# Disallow contextual RAG for cloud deployments
@@ -263,7 +263,7 @@ def update_saved_search_settings(
@router.get("/unstructured-api-key-set")
def unstructured_api_key_set(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> bool:
api_key = get_unstructured_api_key()
return api_key is not None
@@ -272,14 +272,14 @@ def unstructured_api_key_set(
@router.put("/upsert-unstructured-api-key")
def upsert_unstructured_api_key(
unstructured_api_key: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
update_unstructured_api_key(unstructured_api_key)
@router.delete("/delete-unstructured-api-key")
def delete_unstructured_api_key_endpoint(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
delete_unstructured_api_key()

View File

@@ -3,10 +3,11 @@ from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.configs.constants import MilestoneRecordType
from onyx.db.constants import SLACK_BOT_PERSONA_PREFIX
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import ChannelConfig
from onyx.db.models import User
from onyx.db.persona import get_persona_by_id
@@ -113,7 +114,7 @@ def _form_channel_config(
def create_slack_channel_config(
slack_channel_config_creation_request: SlackChannelConfigCreationRequest,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> SlackChannelConfig:
channel_config = _form_channel_config(
db_session=db_session,
@@ -154,7 +155,7 @@ def patch_slack_channel_config(
slack_channel_config_id: int,
slack_channel_config_creation_request: SlackChannelConfigCreationRequest,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> SlackChannelConfig:
channel_config = _form_channel_config(
db_session=db_session,
@@ -215,7 +216,7 @@ def patch_slack_channel_config(
def delete_slack_channel_config(
slack_channel_config_id: int,
db_session: Session = Depends(get_session),
user: User = Depends(current_admin_user),
user: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
remove_slack_channel_config(
db_session=db_session,
@@ -227,7 +228,7 @@ def delete_slack_channel_config(
@router.get("/admin/slack-app/channel")
def list_slack_channel_configs(
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> list[SlackChannelConfig]:
slack_channel_config_models = fetch_slack_channel_configs(db_session=db_session)
return [
@@ -240,7 +241,7 @@ def list_slack_channel_configs(
def create_bot(
slack_bot_creation_request: SlackBotCreationRequest,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> SlackBot:
tenant_id = get_current_tenant_id()
@@ -286,7 +287,7 @@ def patch_bot(
slack_bot_id: int,
slack_bot_creation_request: SlackBotCreationRequest,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> SlackBot:
validate_bot_token(slack_bot_creation_request.bot_token)
validate_app_token(slack_bot_creation_request.app_token)
@@ -307,7 +308,7 @@ def patch_bot(
def delete_bot(
slack_bot_id: int,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
remove_slack_bot(
db_session=db_session,
@@ -319,7 +320,7 @@ def delete_bot(
def get_bot_by_id(
slack_bot_id: int,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> SlackBot:
slack_bot_model = fetch_slack_bot(
db_session=db_session,
@@ -331,7 +332,7 @@ def get_bot_by_id(
@router.get("/admin/slack-app/bots")
def list_bots(
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> list[SlackBot]:
slack_bot_models = fetch_slack_bots(db_session=db_session)
return [
@@ -343,7 +344,7 @@ def list_bots(
def list_bot_configs(
bot_id: int,
db_session: Session = Depends(get_session),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> list[SlackChannelConfig]:
slack_bot_config_models = fetch_slack_channel_configs(
db_session=db_session, slack_bot_id=bot_id

View File

@@ -28,11 +28,10 @@ from onyx.auth.invited_users import get_invited_users
from onyx.auth.invited_users import remove_user_from_invited_users
from onyx.auth.invited_users import write_invited_users
from onyx.auth.permissions import get_effective_permissions
from onyx.auth.permissions import require_permission
from onyx.auth.schemas import UserRole
from onyx.auth.users import anonymous_user_enabled
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.auth.users import enforce_seat_limit
from onyx.auth.users import optional_user
from onyx.configs.app_configs import AUTH_BACKEND
@@ -52,6 +51,7 @@ from onyx.db.api_key import is_api_key_email_address
from onyx.db.auth import get_live_users_count
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import AccountType
from onyx.db.enums import Permission
from onyx.db.enums import UserFileStatus
from onyx.db.models import User
from onyx.db.models import UserFile
@@ -126,7 +126,9 @@ USERS_PAGE_SIZE = 10
@router.patch("/manage/set-user-role", tags=PUBLIC_API_TAGS)
def set_user_role(
user_role_update_request: UserRoleUpdateRequest,
current_user: User = Depends(current_admin_user),
current_user: User = Depends(
require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)
),
db_session: Session = Depends(get_session),
) -> None:
user_to_update = get_user_by_email(
@@ -143,7 +145,6 @@ def set_user_role(
# This will raise an exception if the role update is invalid
validate_user_role_update(
requested_role=requested_role,
current_role=current_role,
current_account_type=user_to_update.account_type,
explicit_override=user_role_update_request.explicit_override,
)
@@ -171,7 +172,7 @@ class TestUpsertRequest(BaseModel):
@router.post("/manage/users/test-upsert-user")
async def test_upsert_user(
request: TestUpsertRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None | FullUserSnapshot:
"""Test endpoint for upsert_saml_user. Only used for integration testing."""
user = await fetch_ee_implementation_or_noop(
@@ -187,7 +188,7 @@ def list_accepted_users(
page_size: int = Query(10, ge=1, le=1000),
roles: list[UserRole] = Query(default=[]),
is_active: bool | None = Query(default=None),
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> PaginatedReturn[FullUserSnapshot]:
filtered_accepted_users = get_page_of_filtered_users(
@@ -249,7 +250,7 @@ def list_accepted_users(
@router.get("/manage/users/accepted/all", tags=PUBLIC_API_TAGS)
def list_all_accepted_users(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[FullUserSnapshot]:
"""Returns all accepted users without pagination.
@@ -292,7 +293,7 @@ def list_all_accepted_users(
@router.get("/manage/users/counts")
def get_user_counts(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, dict[str, int]]:
return get_user_counts_by_role_and_status(db_session)
@@ -300,7 +301,7 @@ def get_user_counts(
@router.get("/manage/users/invited", tags=PUBLIC_API_TAGS)
def list_invited_users(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[InvitedUserSnapshot]:
invited_emails = get_invited_users()
@@ -388,7 +389,7 @@ def list_all_users(
@router.get("/manage/users/download")
def download_users_csv(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> StreamingResponse:
"""Download all users as a CSV file."""
@@ -426,7 +427,9 @@ def download_users_csv(
@router.put("/manage/admin/users", tags=PUBLIC_API_TAGS)
def bulk_invite_users(
emails: list[str] = Body(..., embed=True),
current_user: User = Depends(current_admin_user),
current_user: User = Depends(
require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)
),
db_session: Session = Depends(get_session),
) -> BulkInviteResponse:
"""emails are string validated. If any email fails validation, no emails are
@@ -526,7 +529,7 @@ def bulk_invite_users(
@router.patch("/manage/admin/remove-invited-user", tags=PUBLIC_API_TAGS)
def remove_invited_user(
user_email: UserByEmail,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> int:
tenant_id = get_current_tenant_id()
@@ -554,7 +557,9 @@ def remove_invited_user(
@router.patch("/manage/admin/deactivate-user", tags=PUBLIC_API_TAGS)
def deactivate_user_api(
user_email: UserByEmail,
current_user: User = Depends(current_admin_user),
current_user: User = Depends(
require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)
),
db_session: Session = Depends(get_session),
) -> None:
if current_user.email == user_email.user_email:
@@ -583,7 +588,7 @@ def deactivate_user_api(
@router.delete("/manage/admin/delete-user", tags=PUBLIC_API_TAGS)
async def delete_user(
user_email: UserByEmail,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
user_to_delete = get_user_by_email(
@@ -627,7 +632,7 @@ async def delete_user(
@router.patch("/manage/admin/activate-user", tags=PUBLIC_API_TAGS)
def activate_user_api(
user_email: UserByEmail,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
user_to_activate = get_user_by_email(
@@ -656,7 +661,7 @@ def activate_user_api(
@router.get("/manage/admin/valid-domains")
def get_valid_domains(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> list[str]:
return VALID_EMAIL_DOMAINS
@@ -667,7 +672,7 @@ def get_valid_domains(
@router.get("/users", tags=PUBLIC_API_TAGS)
def list_all_users_basic_info(
include_api_keys: bool = False,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[MinimalUserSnapshot]:
users = get_all_users(db_session)
@@ -680,7 +685,9 @@ def list_all_users_basic_info(
@router.get("/get-user-role", tags=PUBLIC_API_TAGS)
async def get_user_role(user: User = Depends(current_user)) -> UserRoleResponse:
async def get_user_role(
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> UserRoleResponse:
return UserRoleResponse(role=user.role)
@@ -779,7 +786,7 @@ def _get_token_created_at(
@router.get("/me/permissions", tags=PUBLIC_API_TAGS)
def get_current_user_permissions(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> list[str]:
return sorted(p.value for p in get_effective_permissions(user))
@@ -861,7 +868,7 @@ def verify_user_logged_in(
@router.patch("/temperature-override-enabled")
def update_user_temperature_override_enabled_api(
temperature_override_enabled: bool,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_user_temperature_override_enabled(
@@ -876,7 +883,7 @@ class ChosenDefaultModelRequest(BaseModel):
@router.patch("/shortcut-enabled")
def update_user_shortcut_enabled_api(
shortcut_enabled: bool,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_user_shortcut_enabled(user.id, shortcut_enabled, db_session)
@@ -885,7 +892,7 @@ def update_user_shortcut_enabled_api(
@router.patch("/auto-scroll")
def update_user_auto_scroll_api(
request: AutoScrollRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_user_auto_scroll(user.id, request.auto_scroll, db_session)
@@ -894,7 +901,7 @@ def update_user_auto_scroll_api(
@router.patch("/user/theme-preference")
def update_user_theme_preference_api(
request: ThemePreferenceRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_user_theme_preference(user.id, request.theme_preference, db_session)
@@ -903,7 +910,7 @@ def update_user_theme_preference_api(
@router.patch("/user/chat-background")
def update_user_chat_background_api(
request: ChatBackgroundRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_user_chat_background(user.id, request.chat_background, db_session)
@@ -912,7 +919,7 @@ def update_user_chat_background_api(
@router.patch("/user/default-app-mode")
def update_user_default_app_mode_api(
request: DefaultAppModeRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_user_default_app_mode(user.id, request.default_app_mode, db_session)
@@ -921,7 +928,7 @@ def update_user_default_app_mode_api(
@router.patch("/user/default-model")
def update_user_default_model_api(
request: ChosenDefaultModelRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_user_default_model(user.id, request.default_model, db_session)
@@ -930,7 +937,7 @@ def update_user_default_model_api(
@router.patch("/user/personalization")
def update_user_personalization_api(
request: PersonalizationUpdateRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
new_name = request.name if request.name is not None else user.personal_name
@@ -978,7 +985,7 @@ class ReorderPinnedAssistantsRequest(BaseModel):
@router.patch("/user/pinned-assistants")
def update_user_pinned_assistants_api(
request: ReorderPinnedAssistantsRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
ordered_assistant_ids = request.ordered_assistant_ids
@@ -1015,7 +1022,7 @@ def update_assistant_visibility(
def update_user_assistant_visibility_api(
assistant_id: int,
show: bool,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
user_preferences = UserInfo.from_model(user).preferences
@@ -1035,7 +1042,7 @@ def update_user_assistant_visibility_api(
@router.get("/user/assistant/preferences")
def get_user_assistant_preferences(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UserSpecificAssistantPreferences | None:
"""Fetch all assistant preferences for the user."""
@@ -1054,7 +1061,7 @@ def get_user_assistant_preferences(
def update_assistant_preferences_for_user_api(
assistant_id: int,
new_assistant_preference: UserSpecificAssistantPreference,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
update_assistant_preferences(
@@ -1065,7 +1072,7 @@ def update_assistant_preferences_for_user_api(
@router.get("/user/files/recent")
def get_recent_files(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[UserFileSnapshot]:
user_id = user.id

View File

@@ -3,8 +3,9 @@ from fastapi import Depends
from fastapi import Response
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import LLMProvider as LLMProviderModel
from onyx.db.models import User
from onyx.db.models import VoiceProvider
@@ -73,7 +74,7 @@ def _provider_to_view(provider: VoiceProvider) -> VoiceProviderView:
@admin_router.get("/providers")
def list_voice_providers(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[VoiceProviderView]:
"""List all configured voice providers."""
@@ -84,7 +85,7 @@ def list_voice_providers(
@admin_router.post("/providers")
async def upsert_voice_provider_endpoint(
request: VoiceProviderUpsertRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> VoiceProviderView:
"""Create or update a voice provider."""
@@ -153,7 +154,7 @@ async def upsert_voice_provider_endpoint(
)
def delete_voice_provider_endpoint(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
"""Delete a voice provider."""
@@ -165,7 +166,7 @@ def delete_voice_provider_endpoint(
@admin_router.post("/providers/{provider_id}/activate-stt")
def activate_stt_provider_endpoint(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> VoiceProviderView:
"""Set a voice provider as the default STT provider."""
@@ -177,7 +178,7 @@ def activate_stt_provider_endpoint(
@admin_router.post("/providers/{provider_id}/deactivate-stt")
def deactivate_stt_provider_endpoint(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> VoiceProviderUpdateSuccess:
"""Remove the default STT status from a voice provider."""
@@ -190,7 +191,7 @@ def deactivate_stt_provider_endpoint(
def activate_tts_provider_endpoint(
provider_id: int,
tts_model: str | None = None,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> VoiceProviderView:
"""Set a voice provider as the default TTS provider."""
@@ -204,7 +205,7 @@ def activate_tts_provider_endpoint(
@admin_router.post("/providers/{provider_id}/deactivate-tts")
def deactivate_tts_provider_endpoint(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> VoiceProviderUpdateSuccess:
"""Remove the default TTS status from a voice provider."""
@@ -216,7 +217,7 @@ def deactivate_tts_provider_endpoint(
@admin_router.post("/providers/test")
async def test_voice_provider(
request: VoiceProviderTestRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> VoiceProviderUpdateSuccess:
"""Test a voice provider connection by making a real API call."""
@@ -277,7 +278,7 @@ async def test_voice_provider(
@admin_router.get("/providers/{provider_id}/voices")
def get_provider_voices(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[VoiceOption]:
"""Get available voices for a provider."""
@@ -301,7 +302,7 @@ def get_provider_voices(
@admin_router.get("/voices")
def get_voices_by_type(
provider_type: str,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> list[VoiceOption]:
"""Get available voices for a provider type.

View File

@@ -10,9 +10,10 @@ from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.engine.sql_engine import get_session_with_current_tenant
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.voice import fetch_default_stt_provider
from onyx.db.voice import fetch_default_tts_provider
@@ -42,7 +43,7 @@ class VoiceStatusResponse(BaseModel):
@router.get("/status")
def get_voice_status(
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> VoiceStatusResponse:
"""Check whether STT and TTS providers are configured and ready."""
@@ -57,7 +58,7 @@ def get_voice_status(
@router.post("/transcribe")
async def transcribe_audio(
audio: UploadFile = File(...),
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, str]:
"""Transcribe audio to text using the default STT provider."""
@@ -121,7 +122,7 @@ async def synthesize_speech(
speed: float | None = Query(
default=None, description="Playback speed (0.5-2.0)", ge=0.5, le=2.0
),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> StreamingResponse:
"""
Synthesize text to speech using the default TTS provider.
@@ -208,7 +209,7 @@ async def synthesize_speech(
@router.patch("/settings")
def update_voice_settings(
request: VoiceSettingsUpdateRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, str]:
"""Update user's voice settings."""
@@ -229,7 +230,7 @@ class WSTokenResponse(BaseModel):
@router.post("/ws-token")
async def get_ws_token(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
) -> WSTokenResponse:
"""
Generate a short-lived token for WebSocket authentication.

View File

@@ -7,8 +7,9 @@ from fastapi import Response
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import InternetContentProvider
from onyx.db.models import InternetSearchProvider
from onyx.db.models import User
@@ -57,7 +58,7 @@ admin_router = APIRouter(prefix="/admin/web-search")
@admin_router.get("/search-providers", response_model=list[WebSearchProviderView])
def list_search_providers(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[WebSearchProviderView]:
providers = fetch_web_search_providers(db_session)
@@ -77,7 +78,7 @@ def list_search_providers(
@admin_router.post("/search-providers", response_model=WebSearchProviderView)
def upsert_search_provider_endpoint(
request: WebSearchProviderUpsertRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> WebSearchProviderView:
existing_by_name = fetch_web_search_provider_by_name(request.name, db_session)
@@ -140,7 +141,7 @@ def upsert_search_provider_endpoint(
)
def delete_search_provider(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
delete_web_search_provider(provider_id, db_session)
@@ -150,7 +151,7 @@ def delete_search_provider(
@admin_router.post("/search-providers/{provider_id}/activate")
def activate_search_provider(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> WebSearchProviderView:
provider = set_active_web_search_provider(
@@ -170,7 +171,7 @@ def activate_search_provider(
@admin_router.post("/search-providers/{provider_id}/deactivate")
def deactivate_search_provider(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, str]:
deactivate_web_search_provider(provider_id=provider_id, db_session=db_session)
@@ -181,7 +182,7 @@ def deactivate_search_provider(
@admin_router.post("/search-providers/test")
def test_search_provider(
request: WebSearchProviderTestRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, str]:
requires_key = provider_requires_api_key(request.provider_type)
@@ -230,7 +231,7 @@ def test_search_provider(
@admin_router.get("/content-providers", response_model=list[WebContentProviderView])
def list_content_providers(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[WebContentProviderView]:
providers = fetch_web_content_providers(db_session)
@@ -250,7 +251,7 @@ def list_content_providers(
@admin_router.post("/content-providers", response_model=WebContentProviderView)
def upsert_content_provider_endpoint(
request: WebContentProviderUpsertRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> WebContentProviderView:
existing_by_name = fetch_web_content_provider_by_name(request.name, db_session)
@@ -313,7 +314,7 @@ def upsert_content_provider_endpoint(
)
def delete_content_provider(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
delete_web_content_provider(provider_id, db_session)
@@ -323,7 +324,7 @@ def delete_content_provider(
@admin_router.post("/content-providers/{provider_id}/activate")
def activate_content_provider(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> WebContentProviderView:
provider = set_active_web_content_provider(
@@ -342,7 +343,7 @@ def activate_content_provider(
@admin_router.post("/content-providers/reset-default")
def reset_content_provider_default(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, str]:
providers = fetch_web_content_providers(db_session)
@@ -358,7 +359,7 @@ def reset_content_provider_default(
@admin_router.post("/content-providers/{provider_id}/deactivate")
def deactivate_content_provider(
provider_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, str]:
deactivate_web_content_provider(provider_id=provider_id, db_session=db_session)
@@ -369,7 +370,7 @@ def deactivate_content_provider(
@admin_router.post("/content-providers/test")
def test_content_provider(
request: WebContentProviderTestRequest,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, str]:
# Determine which API key to use

View File

@@ -26,6 +26,7 @@ _DEFAULT_PORTS: dict[str, int] = {
"monitoring": 9096,
"docfetching": 9092,
"docprocessing": 9093,
"heavy": 9094,
}
_server_started = False

View File

@@ -0,0 +1,72 @@
"""Pruning-specific Prometheus metrics.
Tracks three pruning pipeline phases for connector_pruning_generator_task:
1. Document ID enumeration duration (extract_ids_from_runnable_connector)
2. Diff + dispatch duration (DB lookup, set diff, generate_tasks)
3. Rate limit errors during enumeration
All metrics are labeled by connector_type to identify which connector sources
are the most expensive to prune. cc_pair_id is intentionally excluded to avoid
unbounded cardinality.
Usage:
from onyx.server.metrics.pruning_metrics import (
observe_pruning_enumeration_duration,
observe_pruning_diff_duration,
inc_pruning_rate_limit_error,
)
"""
from prometheus_client import Counter
from prometheus_client import Histogram
from onyx.utils.logger import setup_logger
logger = setup_logger()
PRUNING_ENUMERATION_DURATION = Histogram(
"onyx_pruning_enumeration_duration_seconds",
"Duration of document ID enumeration from the source connector during pruning",
["connector_type"],
buckets=[1, 5, 15, 30, 60, 120, 300, 600, 1800, 3600],
)
PRUNING_DIFF_DURATION = Histogram(
"onyx_pruning_diff_duration_seconds",
"Duration of diff computation and subtask dispatch during pruning",
["connector_type"],
buckets=[1, 5, 15, 30, 60, 120, 300, 600, 1800, 3600],
)
PRUNING_RATE_LIMIT_ERRORS = Counter(
"onyx_pruning_rate_limit_errors_total",
"Total rate limit errors encountered during pruning document ID enumeration",
["connector_type"],
)
def observe_pruning_enumeration_duration(
duration_seconds: float, connector_type: str
) -> None:
try:
PRUNING_ENUMERATION_DURATION.labels(connector_type=connector_type).observe(
duration_seconds
)
except Exception:
logger.debug("Failed to record pruning enumeration duration", exc_info=True)
def observe_pruning_diff_duration(duration_seconds: float, connector_type: str) -> None:
try:
PRUNING_DIFF_DURATION.labels(connector_type=connector_type).observe(
duration_seconds
)
except Exception:
logger.debug("Failed to record pruning diff duration", exc_info=True)
def inc_pruning_rate_limit_error(connector_type: str) -> None:
try:
PRUNING_RATE_LIMIT_ERRORS.labels(connector_type=connector_type).inc()
except Exception:
logger.debug("Failed to record pruning rate limit error", exc_info=True)

View File

@@ -5,8 +5,9 @@ from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.pat import create_pat
from onyx.db.pat import list_user_pats
@@ -24,7 +25,7 @@ router = APIRouter(prefix="/user/pats")
@router.get("")
def list_tokens(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[TokenResponse]:
"""List all active tokens for current user."""
@@ -45,7 +46,7 @@ def list_tokens(
@router.post("")
def create_token(
request: CreateTokenRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> CreatedTokenResponse:
"""Create new personal access token for current user."""
@@ -75,7 +76,7 @@ def create_token(
@router.delete("/{token_id}")
def delete_token(
token_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> dict[str, str]:
"""Delete (revoke) personal access token. Only owner can revoke their own tokens."""

View File

@@ -16,8 +16,8 @@ from sqlalchemy.orm import Session
from onyx.auth.api_key import get_hashed_api_key_from_request
from onyx.auth.pat import get_hashed_pat_from_request
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_chat_accessible_user
from onyx.auth.users import current_user
from onyx.cache.factory import get_cache_backend
from onyx.chat.chat_processing_checker import is_chat_session_processing
from onyx.chat.chat_state import ChatStateContainer
@@ -53,6 +53,7 @@ from onyx.db.chat import update_chat_session
from onyx.db.chat_search import search_chat_sessions
from onyx.db.engine.sql_engine import get_session
from onyx.db.engine.sql_engine import get_session_with_current_tenant
from onyx.db.enums import Permission
from onyx.db.feedback import create_chat_message_feedback
from onyx.db.feedback import remove_chat_message_feedback
from onyx.db.models import ChatSessionSharedStatus
@@ -149,7 +150,7 @@ def _get_available_tokens_for_persona(
@router.get("/get-user-chat-sessions", tags=PUBLIC_API_TAGS)
def get_user_chat_sessions(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
project_id: int | None = None,
only_non_project_chats: bool = True,
@@ -206,7 +207,7 @@ def get_user_chat_sessions(
@router.put("/update-chat-session-temperature")
def update_chat_session_temperature(
update_thread_req: UpdateChatSessionTemperatureRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
chat_session = get_chat_session_by_id(
@@ -246,7 +247,7 @@ def update_chat_session_temperature(
@router.put("/update-chat-session-model")
def update_chat_session_model(
update_thread_req: UpdateChatSessionThreadRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
chat_session = get_chat_session_by_id(
@@ -401,7 +402,7 @@ def create_new_chat_session(
def rename_chat_session(
rename_req: ChatRenameRequest,
request: Request,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> RenameChatSessionResponse:
# 3000 tokens is more than enough for a pair of messages which is enough to provide the required context for generating a
@@ -470,7 +471,7 @@ def rename_chat_session(
def patch_chat_session(
session_id: UUID,
chat_session_update_req: ChatSessionUpdateRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
user_id = user.id
@@ -485,7 +486,7 @@ def patch_chat_session(
@router.delete("/delete-all-chat-sessions", tags=PUBLIC_API_TAGS)
def delete_all_chat_sessions(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
try:
@@ -498,7 +499,7 @@ def delete_all_chat_sessions(
def delete_chat_session_by_id(
session_id: UUID,
hard_delete: bool | None = None,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
user_id = user.id
@@ -687,7 +688,7 @@ def handle_send_chat_message(
@router.put("/set-message-as-latest")
def set_message_as_latest(
message_identifier: ChatMessageIdentifier,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
user_id = user.id
@@ -708,7 +709,7 @@ def set_message_as_latest(
@router.put("/set-preferred-response")
def set_preferred_response_endpoint(
request_body: SetPreferredResponseRequest,
user: User | None = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
"""Set the preferred assistant response for a multi-model turn."""
@@ -717,7 +718,7 @@ def set_preferred_response_endpoint(
# doesn't belong to this user, preventing cross-user mutation.
get_chat_message(
chat_message_id=request_body.user_message_id,
user_id=user.id if user else None,
user_id=user.id,
db_session=db_session,
)
set_preferred_response(
@@ -769,7 +770,7 @@ class MaxSelectedDocumentTokens(BaseModel):
@router.get("/max-selected-document-tokens")
def get_max_document_tokens(
persona_id: int,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> MaxSelectedDocumentTokens:
try:
@@ -840,7 +841,7 @@ class SeedChatFromSlackResponse(BaseModel):
@router.post("/seed-chat-session-from-slack")
def seed_chat_from_slack(
chat_seed_request: SeedChatFromSlackRequest,
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> SeedChatFromSlackResponse:
slack_chat_session_id = chat_seed_request.chat_session_id
@@ -865,7 +866,7 @@ def seed_chat_from_slack(
def fetch_chat_file(
file_id: str,
request: Request,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> Response:
@@ -902,7 +903,7 @@ async def search_chats(
query: str | None = Query(None),
page: int = Query(1),
page_size: int = Query(10),
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> ChatSearchResponse:
"""
@@ -980,7 +981,7 @@ async def search_chats(
@router.post("/stop-chat-session/{chat_session_id}", tags=PUBLIC_API_TAGS)
def stop_chat_session(
chat_session_id: UUID,
user: User = Depends(current_user), # noqa: ARG001
user: User = Depends(require_permission(Permission.BASIC_ACCESS)), # noqa: ARG001
) -> dict[str, str]:
"""
Stop a chat session by setting a stop signal.

View File

@@ -2,8 +2,8 @@ from fastapi import APIRouter
from fastapi import Depends
from sqlalchemy.orm import Session
from onyx.auth.permissions import require_permission
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.configs.constants import DocumentSource
from onyx.context.search.models import IndexFilters
from onyx.context.search.models import SearchDoc
@@ -12,6 +12,7 @@ from onyx.context.search.preprocessing.access_filters import (
)
from onyx.context.search.utils import get_query_embedding
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.search_settings import get_current_search_settings
from onyx.db.tag import find_tags
@@ -81,7 +82,7 @@ def get_tags(
sources: list[DocumentSource] | None = None,
allow_prefix: bool = True, # This is currently the only option
limit: int = 50,
_: User = Depends(current_user),
_: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> TagResponse:
if not allow_prefix:

View File

@@ -6,8 +6,7 @@ from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from onyx import __version__ as onyx_version
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_user
from onyx.auth.permissions import require_permission
from onyx.auth.users import is_user_admin
from onyx.configs.app_configs import DEFAULT_USER_FILE_MAX_UPLOAD_SIZE_MB
from onyx.configs.app_configs import DISABLE_VECTOR_DB
@@ -15,6 +14,7 @@ from onyx.configs.app_configs import MAX_ALLOWED_UPLOAD_SIZE_MB
from onyx.configs.constants import KV_REINDEX_KEY
from onyx.configs.constants import NotificationType
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.notification import dismiss_all_notifications
from onyx.db.notification import get_notifications
@@ -47,7 +47,8 @@ basic_router = APIRouter(prefix="/settings")
@admin_router.put("")
def admin_put_settings(
settings: Settings, _: User = Depends(current_admin_user)
settings: Settings,
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
) -> None:
if (
settings.user_file_max_upload_size_mb is not None
@@ -68,7 +69,7 @@ def apply_license_status_to_settings(settings: Settings) -> Settings:
@basic_router.get("")
def fetch_settings(
user: User = Depends(current_user),
user: User = Depends(require_permission(Permission.BASIC_ACCESS)),
db_session: Session = Depends(get_session),
) -> UserSettings:
"""Settings and notifications are stuffed into this single endpoint to reduce number of

View File

@@ -2,9 +2,10 @@ from fastapi import APIRouter
from fastapi import Depends
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
from onyx.auth.permissions import require_permission
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import User
from onyx.db.token_limit import delete_token_rate_limit
from onyx.db.token_limit import fetch_all_global_token_rate_limits
@@ -24,7 +25,7 @@ Global Token Limit Settings
@router.get("/global")
def get_global_token_limit_settings(
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> list[TokenRateLimitDisplay]:
return [
@@ -36,7 +37,7 @@ def get_global_token_limit_settings(
@router.post("/global")
def create_global_token_limit_settings(
token_limit_settings: TokenRateLimitArgs,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> TokenRateLimitDisplay:
rate_limit_display = TokenRateLimitDisplay.from_db(
@@ -56,7 +57,7 @@ General Token Limit Settings
def update_token_limit_settings(
token_rate_limit_id: int,
token_limit_settings: TokenRateLimitArgs,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> TokenRateLimitDisplay:
return TokenRateLimitDisplay.from_db(
@@ -71,7 +72,7 @@ def update_token_limit_settings(
@router.delete("/rate-limit/{token_rate_limit_id}")
def delete_token_limit_settings(
token_rate_limit_id: int,
_: User = Depends(current_admin_user),
_: User = Depends(require_permission(Permission.FULL_ADMIN_PANEL_ACCESS)),
db_session: Session = Depends(get_session),
) -> None:
return delete_token_rate_limit(

View File

@@ -263,7 +263,7 @@ oauthlib==3.2.2
# via
# kubernetes
# requests-oauthlib
onyx-devtools==0.7.2
onyx-devtools==0.7.3
# via onyx
openai==2.14.0
# via

View File

@@ -16,8 +16,9 @@ from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.testclient import TestClient
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_user
from onyx.db.engine.sql_engine import get_session
from onyx.db.enums import Permission
from onyx.db.models import UserRole
from onyx.main import get_application
from onyx.utils.logger import setup_logger
@@ -40,10 +41,11 @@ def mock_get_session() -> Generator[MagicMock, None, None]:
yield MagicMock()
def mock_current_admin_user() -> MagicMock:
"""Mock admin user for endpoints protected by current_admin_user."""
def mock_current_user() -> MagicMock:
"""Mock admin user for endpoints protected by require_permission."""
mock_admin = MagicMock()
mock_admin.role = UserRole.ADMIN
mock_admin.effective_permissions = [Permission.FULL_ADMIN_PANEL_ACCESS.value]
return mock_admin
@@ -59,7 +61,7 @@ def client() -> Generator[TestClient, None, None]:
# Override the database session dependency with a mock
# (these tests don't actually need DB access)
app.dependency_overrides[get_session] = mock_get_session
app.dependency_overrides[current_admin_user] = mock_current_admin_user
app.dependency_overrides[current_user] = mock_current_user
# Use TestClient as a context manager to properly trigger lifespan
with TestClient(app) as client:

View File

@@ -116,6 +116,20 @@ class UserGroupManager:
response.raise_for_status()
return response.json()
@staticmethod
def set_permission(
user_group: DATestUserGroup,
permission: str,
enabled: bool,
user_performing_action: DATestUser,
) -> requests.Response:
response = requests.put(
f"{API_SERVER_URL}/manage/admin/user-group/{user_group.id}/permissions",
json={"permission": permission, "enabled": enabled},
headers=user_performing_action.headers,
)
return response
@staticmethod
def get_all(
user_performing_action: DATestUser,

View File

@@ -154,3 +154,68 @@ def test_api_key_admin_service_account(reset: None) -> None: # noqa: ARG001
assert (
user_id_str not in basic_ids
), "ADMIN API key should NOT be in Basic default group"
def test_limited_key_blocked_by_current_user(reset: None) -> None: # noqa: ARG001
"""A LIMITED API key (service account, no permissions) should be rejected
by endpoints behind current_user but allowed through current_limited_user."""
admin_user: DATestUser = UserManager.create(name="admin_user")
limited_key: DATestAPIKey = APIKeyManager.create(
api_key_role=UserRole.LIMITED,
user_performing_action=admin_user,
)
# current_limited_user endpoint → should succeed
resp = requests.get(
f"{API_SERVER_URL}/persona/0",
headers=limited_key.headers,
)
assert (
resp.status_code == 200
), f"Limited key should access /persona/0, got {resp.status_code}: {resp.text}"
# current_user endpoint → should be blocked
resp = requests.get(
f"{API_SERVER_URL}/query/valid-tags",
headers=limited_key.headers,
)
assert (
resp.status_code == 403
), f"Limited key should be blocked from /query/valid-tags, got {resp.status_code}: {resp.text}"
def test_basic_key_passes_current_user(reset: None) -> None: # noqa: ARG001
"""A BASIC API key should pass the current_user dependency."""
admin_user: DATestUser = UserManager.create(name="admin_user")
basic_key: DATestAPIKey = APIKeyManager.create(
api_key_role=UserRole.BASIC,
user_performing_action=admin_user,
)
resp = requests.get(
f"{API_SERVER_URL}/query/valid-tags",
headers=basic_key.headers,
)
assert (
resp.status_code == 200
), f"Basic key should access /query/valid-tags, got {resp.status_code}: {resp.text}"
def test_admin_key_passes_current_user(reset: None) -> None: # noqa: ARG001
"""An ADMIN API key should pass the current_user dependency."""
admin_user: DATestUser = UserManager.create(name="admin_user")
admin_key: DATestAPIKey = APIKeyManager.create(
api_key_role=UserRole.ADMIN,
user_performing_action=admin_user,
)
resp = requests.get(
f"{API_SERVER_URL}/query/valid-tags",
headers=admin_key.headers,
)
assert (
resp.status_code == 200
), f"Admin key should access /query/valid-tags, got {resp.status_code}: {resp.text}"

View File

@@ -0,0 +1,101 @@
"""Shared fixtures for permission integration tests.
Creates six user types that cover the full access spectrum:
- admin_user: STANDARD account, ADMIN role
- basic_user: STANDARD account, BASIC role
- limited_service_account: SERVICE_ACCOUNT, LIMITED role (no group)
- bot_user: BOT account, SLACK_USER role
- ext_perm_user: EXT_PERM_USER account, EXT_PERM_USER role
- anonymous (no fixture): unauthenticated request (empty headers)
All fixtures are module-scoped because permission tests are read-only
(GET requests checking status codes) and don't mutate state between tests.
This avoids a costly full reset per test.
"""
import pytest
from onyx.auth.schemas import UserRole
from onyx.db.engine.sql_engine import get_session_with_current_tenant
from onyx.db.pat import create_pat
from onyx.db.users import add_slack_user_if_not_exists
from onyx.db.users import batch_add_ext_perm_user_if_not_exists
from tests.integration.common_utils.managers.api_key import APIKeyManager
from tests.integration.common_utils.managers.user import UserManager
from tests.integration.common_utils.reset import reset_all
from tests.integration.common_utils.test_models import DATestAPIKey
from tests.integration.common_utils.test_models import DATestUser
@pytest.fixture(scope="module")
def module_reset() -> None:
"""Reset once per test module instead of per test."""
reset_all()
@pytest.fixture(scope="module")
def permission_admin_user(module_reset: None) -> DATestUser: # noqa: ARG001
"""First registered user — automatically gets ADMIN role."""
return UserManager.create(name="perm_admin")
@pytest.fixture(scope="module")
def permission_basic_user(
permission_admin_user: DATestUser, # noqa: ARG001
) -> DATestUser:
"""Second registered user — gets BASIC role."""
return UserManager.create(name="perm_basic")
@pytest.fixture(scope="module")
def limited_service_account(
permission_admin_user: DATestUser,
) -> DATestAPIKey:
"""API key with LIMITED role — creates a SERVICE_ACCOUNT with no group membership."""
return APIKeyManager.create(
api_key_role=UserRole.LIMITED,
user_performing_action=permission_admin_user,
name="limited_svc_key",
)
@pytest.fixture(scope="module")
def bot_user_headers(
permission_admin_user: DATestUser, # noqa: ARG001
) -> dict[str, str]:
"""BOT account (SLACK_USER role) authenticated via PAT.
BOT users can't log in via web — we create one in the DB directly
and issue a PAT for authentication.
"""
with get_session_with_current_tenant() as db_session:
user = add_slack_user_if_not_exists(db_session, email="bot_test@example.com")
_, raw_token = create_pat(
db_session=db_session,
user_id=user.id,
name="bot_test_pat",
expiration_days=None,
)
return {"Authorization": f"Bearer {raw_token}"}
@pytest.fixture(scope="module")
def ext_perm_user_headers(
permission_admin_user: DATestUser, # noqa: ARG001
) -> dict[str, str]:
"""EXT_PERM_USER account authenticated via PAT.
EXT_PERM_USER users can't log in via web — we create one in the DB
directly and issue a PAT for authentication.
"""
with get_session_with_current_tenant() as db_session:
users = batch_add_ext_perm_user_if_not_exists(
db_session, emails=["ext_perm_test@example.com"]
)
_, raw_token = create_pat(
db_session=db_session,
user_id=users[0].id,
name="ext_perm_test_pat",
expiration_days=None,
)
return {"Authorization": f"Bearer {raw_token}"}

Some files were not shown because too many files have changed in this diff Show More