Skip to content

UN-3584 [FEAT] Restrict LLM adapter creation to org admins (controlled mode)#2132

Open
Deepak-Kesavan wants to merge 4 commits into
mainfrom
UN-3584-restrict-llm-creation
Open

UN-3584 [FEAT] Restrict LLM adapter creation to org admins (controlled mode)#2132
Deepak-Kesavan wants to merge 4 commits into
mainfrom
UN-3584-restrict-llm-creation

Conversation

@Deepak-Kesavan

Copy link
Copy Markdown
Contributor

What

Add a per-organization "controlled mode" setting that restricts creation of LLM adapters (connections) to organization admins.

Why

UN-3584 (RLDatix on-prem onboarding blocker). Non-admin users must not be able to create unrestricted LLM connections, and this must be enforced in the product/API rather than by convention. Built as a general, opt-in product capability (any org can enable it), not a customer-specific hack.

How

  • account_v2 — new Organization.restrict_llm_adapter_creation BooleanField (default False) + migration 0006_organization_restrict_llm_adapter_creation.
  • tenant_account_v2 — new admin-only GET/PATCH organization/settings endpoint (reuses IsOrganizationAdmin) to read/toggle the flag.
  • adapter_processor_v2 — gate in AdapterInstanceViewSet.create: when the org flag is on and adapter_type == LLM, non-admins are rejected with 403 PermissionDenied ("contact your organization admin"). Admins bypass; other adapter types and editing existing adapters are unaffected (the gate is on create only).
  • frontend — admin-only toggle in Platform Settings, enterprise-gated (probes for an enterprise-only plugin) so it never renders in OSS, where org-admin / user-management doesn't exist.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

No.

  • The setting defaults off, so existing orgs keep open self-serve creation, unchanged.
  • Enforcement only activates when an org admin explicitly enables it, and only blocks non-admin creation of LLM adapters — other adapter types, shared-adapter usage, and editing existing adapters are all unaffected.
  • The UI toggle is gated to enterprise/cloud builds, so OSS is unaffected; the backend flag defaults off there too.

Database Migrations

Yes — backend/account_v2/migrations/0006_organization_restrict_llm_adapter_creation.py adds BooleanField(default=False). No data migration.

Env Config

None.

Relevant Docs

None.

Related Issues or PRs

Dependencies Versions

None.

Notes on Testing

Deployed to a dev namespace (image snapshot.74) and verified live:

  • Admin-only toggle renders in Platform Settings; GET /organization/settings returns 200 on load; PATCH persists the flag (200, value echoed); migration applied.
  • With the setting ON: a non-admin creating an LLM adapter receives the 403 ("contact your organization admin"); an admin can still create; existing adapters remain editable for everyone.
  • Cloud Logging for the rejected request shows a clean, single-line handled PermissionDenied (no traceback / unhandled error).

Out of scope (per ticket clarification): Bedrock inference-profile enforcement (the model_id / "Application Inference Profile ARN" field already exists in the Bedrock adapter) and an approved-model allowlist.

Screenshots

Platform Settings → "LLM Adapter Creation" toggle (admin-only, enterprise build); verified on dev.

Checklist

I have read and understood the Contribution Guidelines.

…d mode)

Add a per-org 'restrict_llm_adapter_creation' setting (default off). When an
org admin enables it, only organization admins may create LLM adapters:
non-admin create requests are rejected with 403 and a contact-admin message.
Other adapter types and the default-off behavior are unchanged.

- account_v2: new BooleanField on Organization + migration
- tenant_account_v2: admin-only GET/PATCH organization/settings endpoint
- adapter_processor_v2: enforce the gate in AdapterInstanceViewSet.create
- frontend: admin-only toggle in Platform Settings
…in OSS)

Org admin roles / user management don't exist in OSS, so the controlled-mode
toggle must not render there. Probe for an enterprise-only plugin (absent in
OSS) and show the toggle only on enterprise/cloud builds, mirroring the
plugin-gating idiom in SideNavBar. Backend is already OSS-safe: the flag
defaults off and the create gate never fires.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 377aaf4e-f5c6-4a1b-8980-024211cb630b

📥 Commits

Reviewing files that changed from the base of the PR and between ed5e069 and 9fa26b8.

📒 Files selected for processing (1)
  • backend/adapter_processor_v2/views.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/adapter_processor_v2/views.py

Summary by CodeRabbit

  • New Features
    • Added an organization-level setting to restrict LLM adapter creation to organization admins.
    • Enterprise/cloud-only platform settings now includes an “LLM Adapter Creation” toggle for admins, persisted per organization via a new organization settings API.
  • Bug Fixes
    • Enforced the restriction during LLM adapter/connection creation, blocking non-admin users when enabled.
    • Improved the toggle experience with optimistic updates, success messaging, and automatic rollback on save failures.

Walkthrough

Adds an organization restriction flag for LLM adapter creation, exposes admin read/write access for it, enforces it during adapter creation, and adds an enterprise admin toggle in platform settings.

Changes

LLM Adapter Creation Restriction

Layer / File(s) Summary
Organization restriction field
backend/account_v2/models.py, backend/account_v2/migrations/0006_organization_restrict_llm_adapter_creation.py
Adds restrict_llm_adapter_creation to Organization with default=False and a database comment, plus the matching migration.
Organization settings endpoint
backend/tenant_account_v2/views.py, backend/tenant_account_v2/urls.py
Adds the organization_settings GET/PATCH endpoint for organization admins and wires it to organization/settings.
Adapter creation restriction
backend/adapter_processor_v2/views.py
Checks the organization flag during LLM adapter creation and binds the created adapter to the current organization.
Platform settings toggle
frontend/src/components/settings/platform/PlatformSettings.jsx
Detects enterprise builds, loads the setting for admins, and renders a switch that patches the restriction value.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: restricting LLM adapter creation to organization admins in controlled mode.
Description check ✅ Passed The description follows the template and fills the required sections with clear details, testing notes, risks, and migration info.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch UN-3584-restrict-llm-creation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a per-org "controlled mode" that gates LLM adapter creation to organization admins, addressing an on-prem onboarding requirement. The feature is entirely opt-in (flag defaults False), so no existing behavior changes unless an admin explicitly enables it.

  • Backend: adds restrict_llm_adapter_creation BooleanField to Organization, a new admin-only GET/PATCH organization/settings endpoint, and a _enforce_llm_creation_restriction static method in AdapterInstanceViewSet.create that checks the flag, bypasses service accounts, and raises PermissionDenied for non-admin human users.
  • Frontend: adds an enterprise-gated Switch in Platform Settings (hidden in OSS via top-level await import probe of an enterprise-only plugin, a pattern already established throughout the codebase), with optimistic toggle and revert-on-failure logic.
  • Migration: 0006_organization_restrict_llm_adapter_creation.py — additive, non-destructive BooleanField(default=False); no data migration needed.

Confidence Score: 5/5

Safe to merge — the flag defaults off so no existing behavior changes, the gate is narrowly scoped to human non-admin LLM adapter creation, and previously raised issues (service-account bypass, modified_by tracking) are both addressed in the current code.

The change is entirely additive: a nullable boolean column with a safe default, an admin-only endpoint that validates input strictly, and an enforcement gate that only activates when explicitly turned on. The service-account bypass and modified_by auditing are correctly implemented. The frontend guard is enterprise-only and follows the codebase's established plugin-probe idiom.

No files require special attention.

Important Files Changed

Filename Overview
backend/account_v2/migrations/0006_organization_restrict_llm_adapter_creation.py Clean additive migration: adds BooleanField(default=False) with a descriptive db_comment; no data migration required.
backend/account_v2/models.py Adds restrict_llm_adapter_creation BooleanField with matching db_comment to Organization; consistent with existing model style.
backend/adapter_processor_v2/views.py Adds _enforce_llm_creation_restriction with correct service-account bypass, runs after serializer.is_valid, and binds org on save to prevent payload injection; logic is well-scoped to the create action only.
backend/tenant_account_v2/views.py New organization_settings endpoint is correctly gated with IsAuthenticated + IsOrganizationAdmin; PATCH validates boolean type, sets modified_by and modified_at (included in update_fields so auto_now fires), and returns the current value.
backend/tenant_account_v2/urls.py Adds organization/settings path without trailing slash, consistent with the existing organization path style; routing is unambiguous.
frontend/src/components/settings/platform/PlatformSettings.jsx Enterprise detection via top-level await import probe follows the existing codebase idiom; optimistic toggle with revert-on-failure and admin+enterprise guards before API call are all correct.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Admin as Org Admin
    participant NonAdmin as Non-Admin User
    participant FE as Frontend (PlatformSettings)
    participant OrgSettingsAPI as GET/PATCH /organization/settings
    participant AdapterAPI as POST /adapter-instances
    participant DB as Organization (DB)

    Note over FE: Enterprise build probe (top-level await import)
    Admin->>FE: Loads Platform Settings
    FE->>OrgSettingsAPI: GET /organization/settings (IsOrganizationAdmin)
    OrgSettingsAPI->>DB: UserContext.get_organization()
    DB-->>OrgSettingsAPI: "restrict_llm_adapter_creation = false"
    OrgSettingsAPI-->>FE: "{restrict_llm_adapter_creation: false}"
    Admin->>FE: Toggle ON
    FE->>OrgSettingsAPI: "PATCH {restrict_llm_adapter_creation: true}"
    OrgSettingsAPI->>DB: "org.save(update_fields=[...])"
    OrgSettingsAPI-->>FE: "{restrict_llm_adapter_creation: true}"

    Note over NonAdmin,AdapterAPI: Restriction now active
    NonAdmin->>AdapterAPI: POST create LLM adapter
    AdapterAPI->>AdapterAPI: _enforce_llm_creation_restriction()
    AdapterAPI->>DB: get_organization().restrict_llm_adapter_creation
    DB-->>AdapterAPI: true
    AdapterAPI->>AdapterAPI: is_user_organization_admin() → false
    AdapterAPI-->>NonAdmin: 403 PermissionDenied

    Admin->>AdapterAPI: POST create LLM adapter
    AdapterAPI->>AdapterAPI: _enforce_llm_creation_restriction()
    AdapterAPI->>AdapterAPI: is_user_organization_admin() → true
    AdapterAPI->>DB: "serializer.save(organization=...)"
    AdapterAPI-->>Admin: 201 Created
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Admin as Org Admin
    participant NonAdmin as Non-Admin User
    participant FE as Frontend (PlatformSettings)
    participant OrgSettingsAPI as GET/PATCH /organization/settings
    participant AdapterAPI as POST /adapter-instances
    participant DB as Organization (DB)

    Note over FE: Enterprise build probe (top-level await import)
    Admin->>FE: Loads Platform Settings
    FE->>OrgSettingsAPI: GET /organization/settings (IsOrganizationAdmin)
    OrgSettingsAPI->>DB: UserContext.get_organization()
    DB-->>OrgSettingsAPI: "restrict_llm_adapter_creation = false"
    OrgSettingsAPI-->>FE: "{restrict_llm_adapter_creation: false}"
    Admin->>FE: Toggle ON
    FE->>OrgSettingsAPI: "PATCH {restrict_llm_adapter_creation: true}"
    OrgSettingsAPI->>DB: "org.save(update_fields=[...])"
    OrgSettingsAPI-->>FE: "{restrict_llm_adapter_creation: true}"

    Note over NonAdmin,AdapterAPI: Restriction now active
    NonAdmin->>AdapterAPI: POST create LLM adapter
    AdapterAPI->>AdapterAPI: _enforce_llm_creation_restriction()
    AdapterAPI->>DB: get_organization().restrict_llm_adapter_creation
    DB-->>AdapterAPI: true
    AdapterAPI->>AdapterAPI: is_user_organization_admin() → false
    AdapterAPI-->>NonAdmin: 403 PermissionDenied

    Admin->>AdapterAPI: POST create LLM adapter
    AdapterAPI->>AdapterAPI: _enforce_llm_creation_restriction()
    AdapterAPI->>AdapterAPI: is_user_organization_admin() → true
    AdapterAPI->>DB: "serializer.save(organization=...)"
    AdapterAPI-->>Admin: 201 Created
Loading

Reviews (3): Last reviewed commit: "UN-3584 [FIX] Bind adapter creation to r..." | Re-trigger Greptile

Comment thread backend/adapter_processor_v2/views.py Outdated
Comment thread backend/tenant_account_v2/views.py Outdated
…audit, extract gate

- Bypass the LLM-creation gate for service accounts (platform API-key
  sessions), consistent with how the rest of the permission layer treats
  them (Greptile P1).
- Set Organization.modified_by on the settings PATCH so the audit field
  isn't left stale (Greptile P2).
- Extract the controlled-mode check into _enforce_llm_creation_restriction
  to bring AdapterInstanceViewSet.create back under the cognitive-complexity
  limit (SonarQube).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/adapter_processor_v2/views.py`:
- Around line 189-193: The adapter creation flow in
AdapterInstanceSerializer/AdapterInstanceView should not trust the payload’s
organization field. Update the save path in the view so the organization used
for creation is always the request-scoped organization from
UserContext.get_organization() (or otherwise force it server-side), and ensure
any serializer fields for organization cannot override it. Keep the existing
restrict_llm_adapter_creation/admin check tied to the same organization that
will actually be assigned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61151cf5-6d80-4831-999e-99a2b6e73456

📥 Commits

Reviewing files that changed from the base of the PR and between 93a4799 and ed5e069.

📒 Files selected for processing (2)
  • backend/adapter_processor_v2/views.py
  • backend/tenant_account_v2/views.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/tenant_account_v2/views.py

Comment thread backend/adapter_processor_v2/views.py
…rg override)

AdapterInstanceSerializer exposes organization via fields=__all__, and
DefaultOrganizationMixin.save only fills it when None — so a payload-supplied
organization would persist. Pass organization=UserContext.get_organization()
to serializer.save() so the row is bound to the same request-scoped org the
controlled-mode check evaluates, closing a per-org restriction bypass
(CodeRabbit security finding).
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

All checks passed! No linting or formatting issues found.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
unit-connectors unit 64 12 0 3 17.0
unit-core unit 0 0 4 0 1.2
unit-platform-service unit 9 0 1 0 1.4
unit-rig unit 53 0 0 0 3.4
unit-runner unit 11 0 0 0 3.2
unit-sdk1 unit 381 0 0 0 18.8
unit-tool-registry unit 0 0 1 0 1.3
unit-workers unit 0 0 0 0 18.0
TOTAL 518 12 6 3 64.2

Critical paths

⚠️ Critical paths not yet covered

  • auth-login — User can log in and obtain a session cookie. (entry: POST /api/v1/auth/login; declared coverage: no groups declared)
  • adapter-register-llm — Register and validate an LLM adapter. (entry: POST /api/v1/adapter/; declared coverage: no groups declared)
  • workflow-create-execute — Create a workflow, configure source+destination, execute, poll, fetch result. (entry: POST /api/v1/workflow/{id}/execute/; declared coverage: e2e-workflow)
  • api-deployment-run — Deploy a workflow as an API, POST a document, receive structured JSON. (entry: POST /deployment/api/{org}/{name}/; declared coverage: e2e-api-deployment)
  • prompt-studio-fetch-response — Prompt Studio: create project, add prompt, run single-pass, get response. (entry: POST /api/v1/prompt-studio/prompt-studio-tool/{id}/fetch_response/; declared coverage: e2e-prompt-studio)
  • pipeline-etl-execute — Run an ETL pipeline from source connector to destination. (entry: POST /api/v1/pipeline/{id}/execute/; declared coverage: no groups declared)
  • usage-token-tracking — Per-execution token usage is recorded and retrievable. (entry: GET /api/v1/usage/get_token_usage/; declared coverage: no groups declared)
  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (entry: internal: backend → rabbitmq → workers/file_processing; declared coverage: no groups declared)
  • callback-result-delivery — Async results are posted back via the callback worker. (entry: internal: workers/callback → backend /internal endpoints; declared coverage: no groups declared)
✅ Covered critical paths
  • tool-sandbox-exec — covered by unit-runner

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.

1 participant