Skip to content

fix(llm): enforce persona restrictions on public LLM providers#8846

Open
nmgarza5 wants to merge 1 commit intomainfrom
nikg/fix-llm-rbac
Open

fix(llm): enforce persona restrictions on public LLM providers#8846
nmgarza5 wants to merge 1 commit intomainfrom
nikg/fix-llm-rbac

Conversation

@nmgarza5
Copy link
Contributor

@nmgarza5 nmgarza5 commented Feb 27, 2026

Description

Fixes two compounding bugs in LLM provider access control:

  1. Backend (can_user_access_llm_provider): is_public=True short-circuited the entire function, bypassing persona whitelist checks. A public provider with personas=[0, 1] was accessible to all personas. Fix: enforce persona restrictions first, then apply is_public as a user/group bypass only.

  2. Frontend (useLlmManager.hasAnyProvider): Derived from the global /api/llm/provider endpoint (no persona context), so when the default provider was non-public, chat input was disabled for all personas — even those with their own assigned provider. Fix: derive hasAnyProvider from the persona-aware provider list (llmProviders) instead of allUserProviders.

How Has This Been Tested?

Additional Options

  • [Optional] Please cherry-pick this PR to the latest release version.
  • [Optional] Override Linear Check

Summary by cubic

Enforces persona restrictions on public LLM providers and fixes chat provider detection. Global lists exclude public providers that restrict personas; persona-aware lists include them only when allowed.

  • Bug Fixes
    • Backend: Enforce persona whitelist before is_public (now only bypasses groups). Global provider list uses persona=None, so persona-restricted public providers are hidden. Added tests for public-with-personas and public-without-personas.
    • Frontend: hasAnyProvider now uses the persona-aware llmProviders (falls back to global when no persona) to avoid disabling chat when the selected persona has access.

Written for commit 39db27c. Summary will update on new commits.

@nmgarza5 nmgarza5 requested a review from a team as a code owner February 27, 2026 01:34
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 27, 2026

Greptile Summary

This PR fixes two critical access control bugs that allowed unauthorized persona access to LLM providers:

Backend fix (can_user_access_llm_provider): Reordered logic to enforce persona restrictions before the is_public check. Previously, is_public=True short-circuited the function and bypassed persona whitelist checks entirely, allowing any persona to access a public provider even when specific personas were whitelisted. The fix checks persona restrictions first, then applies is_public as a user/group bypass only.

Frontend fix (useLlmManager.hasAnyProvider): Changed the derivation from allUserProviders (global list) to llmProviders (persona-aware list). Previously, when the default provider was inaccessible to a persona, the chat input would be disabled for all personas even if they had their own assigned providers.

Both fixes are well-documented with clear comments explaining the decision matrix and comprehensive regression tests covering the public provider with persona restrictions scenario.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The changes are focused, well-tested, and fix critical security bugs. The backend logic refactoring is sound with proper ordering of checks, comprehensive test coverage validates the fixes, and the frontend change is minimal and correct. No issues found.
  • No files require special attention

Important Files Changed

Filename Overview
backend/onyx/db/llm.py Refactored access control logic to enforce persona restrictions before is_public check, fixing security bug
backend/tests/integration/tests/llm_provider/test_llm_provider_access_control.py Added comprehensive regression tests for public providers with persona restrictions
web/src/lib/hooks.ts Fixed hasAnyProvider to use persona-aware provider list instead of global list

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Start([User requests LLM provider access]) --> CheckPersonas{Persona restrictions set?}
    
    CheckPersonas -->|Yes| PersonaWhitelist{Persona in whitelist?}
    CheckPersonas -->|No| CheckPublic{is_public = True?}
    
    PersonaWhitelist -->|No| Deny1[❌ Deny Access]
    PersonaWhitelist -->|Yes| CheckPublic
    
    CheckPublic -->|Yes| Allow1[✅ Allow Access]
    CheckPublic -->|No| CheckGroups{Groups set?}
    
    CheckGroups -->|Yes| UserInGroup{User in group OR admin?}
    CheckGroups -->|No| CheckPersonasOnly{Personas set?}
    
    UserInGroup -->|Yes| Allow2[✅ Allow Access]
    UserInGroup -->|No| Deny2[❌ Deny Access]
    
    CheckPersonasOnly -->|Yes| Allow3[✅ Allow Access<br/>via persona whitelist]
    CheckPersonasOnly -->|No| AdminCheck{Is admin?}
    
    AdminCheck -->|Yes| Allow4[✅ Allow Access<br/>admin-only mode]
    AdminCheck -->|No| Deny3[❌ Deny Access<br/>locked provider]
    
    style Deny1 fill:#ffcccc
    style Deny2 fill:#ffcccc
    style Deny3 fill:#ffcccc
    style Allow1 fill:#ccffcc
    style Allow2 fill:#ccffcc
    style Allow3 fill:#ccffcc
    style Allow4 fill:#ccffcc
Loading

Last reviewed commit: c0ed7fb

@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 2026

Preview Deployment

Status Preview Commit Updated
https://onyx-preview-68987h3y3-danswer.vercel.app 39db27c 2026-02-27 02:05:41 UTC

@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 2026

🖼️ Visual Regression Report

Project Changed Added Removed Unchanged Report
admin 2 0 0 107 View Report
exclusive 0 0 0 8 ✅ No changes

Public providers (`is_public=True`) with persona whitelists were
bypassing persona checks entirely — the early return on `is_public`
short-circuited before persona restrictions were evaluated. This meant
any persona could use a public provider even if it was explicitly
restricted to specific personas.

Additionally, `hasAnyProvider` in the frontend `useLlmManager` hook
used the global provider list (no persona context), causing chat input
to be disabled for all personas when the default provider was non-public
— even if the selected persona had its own provider assigned.

Backend: check persona restrictions first, then apply is_public as a
group-bypass only. Frontend: derive hasAnyProvider from the persona-aware
provider list instead of the global one.
6. is_public=False, neither set → admin-only (locked)
"""
# Public override - everyone has access
if provider.is_public:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, if it's the case that there are restriction on being public, it might be worth renaming this sometime soon. I can imagine mistakes arising down the line

if provider.is_public:
return True

has_groups = bool(provider_group_ids)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo I think this sort of function is a bit cleaner when it follows a, define all data, then perform conditionals sort of approach.

Currently finding it a bit non-trivial to follow, but I think if we did something along these lines it could be a bit nicer:

provider_group_ids = {g.id for g in (provider.groups or [])}
provider_persona_ids = {p.id for p in (provider.personas or [])}

has_groups = bool(provider_group_ids)
has_personas = bool(provider_persona_ids)

# personas always enforced
if has_personas and not (persona and persona.id in provider_persona_ids):
    return False

if provider.is_public:
    return True

if has_groups:
    return is_admin or bool(user_group_ids & provider_group_ids)

# no groups: either persona-whitelisted (already passed) or admin-only if locked
return has_personas or is_admin

Just a rough sketch out so not sure if that is correct or if it's the best way to go, but a small suggestion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants