Skip to content

feat(proxy): audit remaining system-wide settings updates - #31754

Merged
yucheng-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_lit_3839_system_settings_audit
Jul 1, 2026
Merged

feat(proxy): audit remaining system-wide settings updates#31754
yucheng-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_lit_3839_system_settings_audit

Conversation

@yucheng-berri

@yucheng-berri yucheng-berri commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Resolves LIT-3839

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

Reproduced live against a local proxy on :4010 with store_audit_logs: true and an enterprise license, backed by a real Postgres. HEAD tested: b5f8f9f0fd (stacked on top of the merged parent PR #31753). Baseline before the sweep was LiteLLM_AuditLog count = 0; total rows written across the sweep is 13 (one per audited operation, plus four for the multi-section /config/update).

The customer scenario (PATCH /update/internal_user_settings) lives in the parent PR and is already covered there. This PR extends coverage to:

  1. PATCH /update/default_team_settings -> object_id=default_team_params
  2. PATCH /update/mcp_semantic_filter_settings -> object_id=mcp_semantic_tool_filter
  3. PATCH /update/ui_theme_settings -> object_id=ui_theme_config
  4. PATCH /update/ui_settings -> table_name=LiteLLM_UISettings, object_id=ui_settings
  5. PATCH /update/sso_settings -> table_name=LiteLLM_SSOConfig, object_id=sso_config, google_client_secret=REDACTED; plaintext sk-SUPERSECRET-PLAINTEXT verified absent via psql ILIKE
  6. POST /config/callback/delete -> object_id=litellm_settings, action=deleted, before {"success_callback":["datadog"]} after {"success_callback":[]}; audit fires before the proxy reload
  7. POST /config/update with environment_variables -> object_id=environment_variables, every value redacted wholesale (DATABASE_URL=REDACTED); plaintext AUDIT_PASSWORD, db.internal, and postgresql:// all verified absent from the audit row
  8. POST /add/allowed_ip and POST /delete/allowed_ip (use plain 127.0.0.1, not CIDR; the allowlist matcher in auth_utils._check_valid_ip is string-equality) -> object_id=general_settings, one row per action, before/after capture the list mutation
  9. POST /config/update with all four sections in one request -> four audit rows, one per section (general_settings, litellm_settings, router_settings, environment_variables), all table_name=LiteLLM_Config
  10. POST /config/field/update -> object_id=general_settings, full before/after snapshot
  11. POST /config/field/delete -> object_id=general_settings, action=deleted, full before/after snapshot

Operational config writers that are not admin settings edits (model-cost-map reload and anthropic-beta-header reload endpoints, plus internal background reloads) are intentionally out of scope.

Type

🆕 New Feature

Changes

Audit logging now fires on every admin endpoint that writes proxy-wide settings, recording the actor, the API key hash, the action, the affected config section, and a redacted before/after snapshot through the shared create_config_audit_log helper (introduced in the parent PR).

The generic config API: /config/update (general_settings, environment_variables, litellm_settings, router_settings), /config/field/update, /config/field/delete, /config/callback/delete.

The dedicated settings endpoints in proxy_setting_endpoints.py that the admin dashboard uses, which do not route through /config/update: /update/default_team_settings, /update/mcp_semantic_filter_settings (via the shared _update_litellm_setting), /add/allowed_ip and /delete/allowed_ip, /update/sso_settings, /update/ui_theme_settings, and /update/ui_settings. SSO and UI settings rows are tagged with their own table names.

New LitellmTableNames values (SSO_CONFIG_TABLE_NAME, UI_SETTINGS_TABLE_NAME) identify the SSO and UI settings rows. The parent PR added CONFIG_TABLE_NAME and the helpers.

The audit-actor parameter on _update_litellm_setting is now required rather than optional; the chokepoint covers default_team and mcp_semantic_filter for free, and a future caller that forgets the actor fails loudly instead of silently skipping the audit. The two direct-calling tests pass a dummy actor.

The /config/callback/delete and /update/sso_settings audits fire BEFORE the proxy reload and the env-var cleanup step respectively, so a failure in either still leaves the audit row intact.

object_id is the logical setting being changed, which for the dedicated endpoints is the litellm_settings sub-key (default_team_params, mcp_semantic_tool_filter, ui_theme_config) rather than the row name, so it reads naturally for an auditor looking for a specific setting.

Operational config writers that are not admin settings edits (the model-cost-map reload and anthropic-beta-header reload endpoints, plus internal background reloads) are intentionally out of scope.


Note

Medium Risk
Touches many security-sensitive admin write paths (SSO, env vars, IP allowlists, callbacks); behavior is additive audit logging with redaction, but mis-redaction or missed endpoints would be high impact.

Overview
Admin settings changes across the proxy now emit LiteLLM_AuditLog rows via the shared create_config_audit_log helper, using fire-and-forget asyncio.create_task so audit failures do not fail saves.

Bulk config paths (/config/update for general_settings, environment_variables, litellm_settings, router_settings), field update/delete, and /config/callback/delete capture deep-copied before state and log updated or deleted with the acting admin from UserAPIKeyAuth.

Dashboard settings endpoints in proxy_setting_endpoints.py are wired the same way: default team and MCP semantic filter (through _update_litellm_setting, which now requires an audit actor), allowed IP add/delete, SSO, UI theme, and UI flags. SSO audits read/decrypt the prior row for a real before snapshot and use LiteLLM_SSOConfig; UI settings use LiteLLM_UISettings. New LitellmTableNames constants name those tables.

Callback delete and SSO update schedule audits before reload or env cleanup so the row is written even when later steps fail. Tests cover per-section audits, secret/DATABASE_URL redaction, and the new endpoints.

Reviewed by Cursor Bugbot for commit e79020e. Bugbot is set up for automated code reviews on this repo. Configure here.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.67442% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...proxy/ui_crud_endpoints/proxy_setting_endpoints.py 96.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the create_config_audit_log framework (introduced in the parent PR) to cover every remaining admin endpoint that writes proxy-wide configuration: the four /config/update sections (general_settings, environment_variables, litellm_settings, router_settings), /config/field/update, /config/field/delete, /config/callback/delete, plus the dedicated dashboard endpoints for default team params, MCP semantic filter, allowed IPs, SSO, UI theme, and UI settings.

  • The SSO endpoint now reads and decrypts the existing row before the upsert so the audit captures a real before/after diff, with *_client_secret fields redacted by the shared masker. Two new LitellmTableNames constants (SSO_CONFIG_TABLE_NAME, UI_SETTINGS_TABLE_NAME) tag those rows correctly.
  • _update_litellm_setting's user_api_key_dict is promoted from Optional to required; all internal callers are updated, and the two test callers pass a dummy UserAPIKeyAuth.
  • All audit calls use asyncio.create_task (fire-and-forget) so a transient DB failure never surfaces as a 500 after the config write has committed.

Confidence Score: 5/5

Safe to merge — all audit calls are additive, fire-and-forget, and isolated from the main save path so a DB hiccup cannot break configuration writes.

The change adds audit logging to every admin settings endpoint without altering any existing write semantics. The SSO before-snapshot fix is correctly implemented: the existing row is read before the upsert, decrypted, and passed to the audit helper which redacts secret fields before persistence. All before-state captures use either copy.deepcopy (for dicts mutated in place) or Python reference-replacement semantics (for dicts swapped out at a key), so no audit log can capture a stale or mutated value. The _update_litellm_setting signature promotion is backed by complete call-site updates in both production and test code. Test coverage is mutation-protected and exercises redaction, before/after diffs, and the audit-fires-before-reload-failure ordering guarantee.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/_types.py Adds SSO_CONFIG_TABLE_NAME and UI_SETTINGS_TABLE_NAME to LitellmTableNames enum; straightforward additive change.
litellm/proxy/proxy_server.py Adds audit log calls (with before-state deep copies) to update_config (four sections), update_config_general_settings, delete_config_general_settings, and delete_callback. All local variables; no mutation-after-task-creation concerns. Audit fires before reload in delete_callback per design.
litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py Wires audit logging to add/delete allowed IP, SSO update (with before-snapshot via find_unique + _decrypt_db_variables), UI theme, UI settings, default team, and MCP semantic filter. _update_litellm_setting promoted user_api_key_dict to required; all callers updated.
tests/test_litellm/proxy/test_proxy_server.py New mutation-protected tests cover: field update/delete audit, all four config/update sections in one request, callback delete before reload failure, and environment_variables wholesale redaction. Assertions are comprehensive and not weakened.
tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py New tests cover SSO audit with redacted before-snapshot (mutation-protected), SSO audit-fires-before-env-cleanup-failure, allowed IP add/delete audit, UI theme audit, UI settings audit. Existing tests correctly updated to mock the new find_unique call added to update_sso_settings.
tests/test_litellm/proxy/management_endpoints/test_team_default_params.py Two existing tests updated to supply the now-required user_api_key_dict argument. Assertions unchanged — coverage is not weakened.
tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py One existing SCIM test updated to pass user_api_key_dict to _update_litellm_setting. Assertion unchanged.

Reviews (8): Last reviewed commit: "refactor(proxy): audit config writes via..." | Re-trigger Greptile

Comment thread litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py Outdated
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the audit-log framework to the remaining proxy-wide settings writers: /config/update (all four sections), /config/field/update, /config/field/delete, /config/callback/delete, and the dedicated admin-dashboard endpoints for SSO, UI theme, UI settings, allowed IPs, default team settings, and MCP semantic filter. Each writer now records a redacted before/after snapshot via the shared create_config_audit_log helper.

  • _update_litellm_setting's user_api_key_dict parameter is promoted from optional to required, ensuring future callers fail loudly if they forget the actor rather than silently skipping the audit; all production and test callsites are updated accordingly.
  • The /config/callback/delete and /update/sso_settings audits fire before the proxy-reload and env-var-cleanup steps respectively, so the row is persisted even when those steps fail.
  • New enum values SSO_CONFIG_TABLE_NAME and UI_SETTINGS_TABLE_NAME are added to LitellmTableNames to tag the SSO and UI settings audit rows with their own logical tables.

Confidence Score: 4/5

Safe to merge; the audit calls are non-blocking (exceptions are swallowed internally), so no existing request path is broken by this change.

The wiring is consistent across all endpoints and the redaction logic, deepcopy-before-snapshot pattern, and ordering guarantees are correct. The one gap is that update_sso_settings always passes before_value=None, so SSO audit rows will never carry a pre-change snapshot — an auditor investigating a misconfiguration can only see what the settings were changed to, not what they were before.

litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py — specifically the update_sso_settings function, which omits the before-snapshot in its audit call.

Important Files Changed

Filename Overview
litellm/proxy/_types.py Adds SSO_CONFIG_TABLE_NAME and UI_SETTINGS_TABLE_NAME enum values to LitellmTableNames for use in audit log table_name routing.
litellm/proxy/proxy_server.py Adds create_config_audit_log calls to update_config (4 sections), update_config_general_settings, delete_config_general_settings, and delete_callback; all use copy.deepcopy for before-snapshots and the shared helper handles redaction.
litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py Wires audit logging to update_sso_settings, update_ui_theme_settings, update_ui_settings, add_allowed_ip, delete_allowed_ip, and _update_litellm_setting; makes user_api_key_dict required on the shared helper; SSO endpoint always passes before_value=None.
tests/test_litellm/proxy/test_proxy_server.py Adds 6 new audit-log tests covering general_settings update/delete, all four /config/update sections, callback deletion (including reload-failure ordering), and full environment_variable redaction.
tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py Adds tests for SSO audit (including env-cleanup-failure ordering), add/delete allowed_ip audit, UI theme audit, and UI settings audit. No regression masking observed.

Reviews (2): Last reviewed commit: "feat(proxy): audit remaining system-wide..." | Re-trigger Greptile

Comment thread litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py Outdated
yucheng-berri added a commit that referenced this pull request Jun 30, 2026
Greptile review of #31754 flagged update_sso_settings as the one endpoint
where before_value is permanently None, so the LiteLLM_SSOConfig audit
trail has no pre-change state. An auditor reviewing a secret-rotation
event could see what the SSO settings were changed to but not what they
were before.

Read the existing SSO row before the upsert, decrypt it via
proxy_config._decrypt_db_variables, and pass it as before_value.
create_config_audit_log's secret-name redaction then masks the
*_client_secret fields, so neither the old nor the new plaintext secret
lands in the audit row.

Add a regression test asserting the before-snapshot reflects the
pre-change values for non-secret fields (google_client_id) and is
redacted for secret fields (google_client_secret). Mutation-checked
against reverting to before_value=None.

The pre-existing SSO tests now also mock litellm_ssoconfig.find_unique
since the endpoint reads it; the read returns None for tests that do not
care about the before-state.
@yucheng-berri
yucheng-berri force-pushed the litellm_lit_3839_system_settings_audit branch from 2bb1d97 to b796ffe Compare June 30, 2026 21:00
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re-review please. Addressed the SSO before-snapshot finding from the prior review:

update_sso_settings now reads the existing SSO row before the upsert, decrypts it via proxy_config._decrypt_db_variables, and passes it as before_value. The audit helper's secret-name redaction (_redact_secret_values_in_obj) masks the *_client_secret fields, so neither the old nor the new plaintext secret lands in the audit row.

Live-verified end-to-end on a real proxy + Postgres: a rotation from OLD-CLIENT-ID/OLD-SUPER-SECRET-AAA to NEW-CLIENT-ID/NEW-SUPER-SECRET-BBB produces an audit row with before.google_client_id=OLD-CLIENT-ID, after.google_client_id=NEW-CLIENT-ID, both *_client_secret fields as REDACTED, and neither plaintext secret anywhere in the audit table.

Added a mutation-checked regression test: reverting before_value=before_sso_data to before_value=None fails the new test. Also threaded mock_prisma.db.litellm_ssoconfig.find_unique into the 10 pre-existing SSO tests that previously only mocked upsert (the endpoint now reads the row).

Rebased onto the parent PR (#31753) which has its own fixes for the prior round of findings.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Pending zero init migration
    • Removed the committed 0_init migration so Prisma no longer treats it as a pending oldest migration on upgraded databases.

You can send follow-ups to the cloud agent here.

Comment thread litellm-proxy-extras/litellm_proxy_extras/migrations/0_init/migration.sql Outdated
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ yucheng-berri
❌ cursoragent
You have signed the CLA already but the status is still pending? Let us recheck it.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review the latest HEAD (748707c). The SSO before-snapshot finding from your prior review is addressed:

update_sso_settings now reads the existing SSO row before the upsert, decrypts it via proxy_config._decrypt_db_variables, and passes it as before_value. The audit helper's secret-name redaction masks the *_client_secret fields, so neither the old nor the new plaintext secret lands in the audit row.

Live-verified end-to-end on a real proxy + Postgres: a rotation from OLD-CLIENT-ID/OLD-SUPER-SECRET-AAA to NEW-CLIENT-ID/NEW-SUPER-SECRET-BBB produces an audit row with before.google_client_id=OLD-CLIENT-ID, after.google_client_id=NEW-CLIENT-ID, both *_client_secret fields as REDACTED, and neither plaintext anywhere in the audit table. Mutation-checked regression test included.

This PR is rebased on top of #31753 which has its own fixes; both PRs should be reviewed together.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 748707c. Configure here.

yucheng-berri added a commit that referenced this pull request Jul 1, 2026
Greptile review of #31754 flagged update_sso_settings as the one endpoint
where before_value is permanently None, so the LiteLLM_SSOConfig audit
trail has no pre-change state. An auditor reviewing a secret-rotation
event could see what the SSO settings were changed to but not what they
were before.

Read the existing SSO row before the upsert, decrypt it via
proxy_config._decrypt_db_variables, and pass it as before_value.
create_config_audit_log's secret-name redaction then masks the
*_client_secret fields, so neither the old nor the new plaintext secret
lands in the audit row.

Add a regression test asserting the before-snapshot reflects the
pre-change values for non-secret fields (google_client_id) and is
redacted for secret fields (google_client_secret). Mutation-checked
against reverting to before_value=None.

The pre-existing SSO tests now also mock litellm_ssoconfig.find_unique
since the endpoint reads it; the read returns None for tests that do not
care about the before-state.
@yucheng-berri
yucheng-berri force-pushed the litellm_lit_3839_system_settings_audit branch from 748707c to e79020e Compare July 1, 2026 00:24
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review HEAD e79020e. Two changes since your last review:

The 12 audit call sites that used blocking await create_config_audit_log(...) are now wrapped in asyncio.create_task(...), matching the fire-and-forget pattern the parent PR established for the chokepoint and matching every other audit endpoint in the repo (model_management_endpoints, key_management_endpoints, hooks/key_management_event_hooks, config_override_endpoints, team_callback_endpoints, cache_settings_endpoints). A post-save audit failure can no longer surface as a 500 to the caller at any of the 12 sites.

The PR body was corrected from "the rest of the LiteLLM_Config writers" to "the remaining system-wide settings writers", with an explicit out-of-scope note for the reload-coordination endpoints (model_cost_map_reload, anthropic_beta_headers_reload) that mutate LiteLLM_Config but are operational state, not admin settings edits.

Mutation check preserved (reverting any converted site still fails its regression test). 336 tests pass.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e79020e. Configure here.

Base automatically changed from litellm_lit_3839_default_user_settings_audit to litellm_internal_staging July 1, 2026 01:17
yucheng-berri and others added 4 commits June 30, 2026 18:19
Extends the audit logging framework introduced in the parent PR to the
rest of the LiteLLM_Config writers and the two adjacent settings tables:

  /config/update (general, environment_variables, litellm_settings,
  router_settings sections), /config/field/update, /config/field/delete,
  /config/callback/delete, /update/default_team_settings,
  /update/mcp_semantic_filter_settings, /add/allowed_ip,
  /delete/allowed_ip, /update/sso_settings, /update/ui_theme_settings,
  /update/ui_settings.

Each writer records the actor, action, the affected config section, and
a redacted before/after snapshot. SSO and UI settings rows use their own
table_name (LiteLLM_SSOConfig, LiteLLM_UISettings). The /config/callback
and /update/sso_settings audits fire BEFORE the proxy reload and the env
cleanup step respectively, so a failure in either leaves the audit row
intact.

The audit-actor parameter on _update_litellm_setting is now required
rather than optional; the chokepoint covers default_team and
mcp_semantic_filter for free, and a future caller that forgets the
actor fails loudly instead of silently skipping the audit. The two
direct-calling tests pass a dummy actor.

The environment_variables section redacts every value rather than
relying on key-name matching, because it carries credentials under
non-secret-looking uppercase keys (e.g. DATABASE_URL).
Greptile review of #31754 flagged update_sso_settings as the one endpoint
where before_value is permanently None, so the LiteLLM_SSOConfig audit
trail has no pre-change state. An auditor reviewing a secret-rotation
event could see what the SSO settings were changed to but not what they
were before.

Read the existing SSO row before the upsert, decrypt it via
proxy_config._decrypt_db_variables, and pass it as before_value.
create_config_audit_log's secret-name redaction then masks the
*_client_secret fields, so neither the old nor the new plaintext secret
lands in the audit row.

Add a regression test asserting the before-snapshot reflects the
pre-change values for non-secret fields (google_client_id) and is
redacted for secret fields (google_client_secret). Mutation-checked
against reverting to before_value=None.

The pre-existing SSO tests now also mock litellm_ssoconfig.find_unique
since the endpoint reads it; the read returns None for tests that do not
care about the before-state.
PR A's chokepoint audit call was refactored from a blocking await to
asyncio.create_task so that a post-save audit-log failure could not
surface as a 500 to the caller. The 12 other audit call sites added in
this PR were still using await, reintroducing the exact 500-after-commit
exposure at every sibling endpoint. Wrap them all in asyncio.create_task
to match the model_management_endpoints / key_management_endpoints /
hooks / config_override_endpoints / team_callback_endpoints /
cache_settings_endpoints house pattern, so the codebase tells one story.

The two direct-invocation tests (test_update_config_general_settings and
test_delete_config_general_settings, which call the handler in-process
rather than via TestClient) yield with `await asyncio.sleep(0)` after the
handler returns so the scheduled audit task runs before the assertion.
@yucheng-berri
yucheng-berri force-pushed the litellm_lit_3839_system_settings_audit branch from e79020e to b5f8f9f Compare July 1, 2026 01:21
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review HEAD b5f8f9f. Rebased onto litellm_internal_staging after #31753 was merged as a squash; PR A's commits are dropped and only PR B's residual (4 commits) remains. The 7-file diff is unchanged bytewise — same as prior reviews on this branch, just replayed on the post-merge base.

Comment on lines +853 to +865
# Read the existing SSO row first so the audit log captures a real
# before/after diff. Stored values are encrypted; decrypt them so the
# before-snapshot has the same shape as after_value, and rely on
# create_config_audit_log's secret-name redaction to mask the
# *_client_secret fields before the audit row is written.
existing_sso_record = await SSOConfigRepository(prisma_client).table.find_unique(where={"id": "sso_config"})
before_sso_data: Optional[Dict[str, Any]] = None
if existing_sso_record and existing_sso_record.sso_settings:
stored = existing_sso_record.sso_settings
if isinstance(stored, str):
stored = json.loads(stored)
if isinstance(stored, dict):
before_sso_data = proxy_config._decrypt_db_variables(stored)

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.

P1 SSO update blocked by decrypt failure in before-state capture

The find_unique + _decrypt_db_variables block runs before the upsert and has no error handling. If the stored SSO row contains data encrypted under a different key (e.g., after key rotation) or is otherwise malformed, _decrypt_db_variables will raise an unhandled exception, returning a 500 to the caller and preventing any SSO update from succeeding. Before this PR, the update path never read the existing row, so this is a net-new failure mode on a security-critical endpoint.

Wrap the before-state retrieval in a try/except and fall back to before_sso_data = None so a snapshot failure degrades gracefully to a null before-value rather than blocking the save.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review the current HEAD b5f8f9f0fd. Your existing review still points at commit 2bb1d9725884f7d043c39751d917687bf8f0305b, which predates the SSO before-snapshot fix.

The SSO before_value=None finding you flagged is addressed at commit ce63c65572 (was b796ffe28d before the post-parent-merge rebase). update_sso_settings now reads the existing SSO row via SSOConfigRepository(prisma_client).table.find_unique, decrypts it via proxy_config._decrypt_db_variables, and passes it as before_value to create_config_audit_log (which redacts *_client_secret fields before persistence). Regression test at tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py::test_update_sso_settings_audit_captures_redacted_before_snapshot proves the before-snapshot is captured with secrets redacted, and is mutation-checked.

Also relevant since your review: all 12 audit call sites are now asyncio.create_task fire-and-forget matching the codebase's audit-endpoint pattern (model_management_endpoints, key_management_endpoints, hooks/key_management_event_hooks, config_override_endpoints, team_callback_endpoints, cache_settings_endpoints); the dead-branch redact_all_values fallback that would have crashed LiteLLM_AuditLogs construction was deleted in the parent PR.

@yucheng-berri
yucheng-berri merged commit bfb8ffc into litellm_internal_staging Jul 1, 2026
124 checks passed
@yucheng-berri
yucheng-berri deleted the litellm_lit_3839_system_settings_audit branch July 1, 2026 02:32
duanhongyi pushed a commit to duanhongyi/litellm that referenced this pull request Jul 2, 2026
)

* feat(proxy): audit remaining system-wide settings updates

Extends the audit logging framework introduced in the parent PR to the
rest of the LiteLLM_Config writers and the two adjacent settings tables:

  /config/update (general, environment_variables, litellm_settings,
  router_settings sections), /config/field/update, /config/field/delete,
  /config/callback/delete, /update/default_team_settings,
  /update/mcp_semantic_filter_settings, /add/allowed_ip,
  /delete/allowed_ip, /update/sso_settings, /update/ui_theme_settings,
  /update/ui_settings.

Each writer records the actor, action, the affected config section, and
a redacted before/after snapshot. SSO and UI settings rows use their own
table_name (LiteLLM_SSOConfig, LiteLLM_UISettings). The /config/callback
and /update/sso_settings audits fire BEFORE the proxy reload and the env
cleanup step respectively, so a failure in either leaves the audit row
intact.

The audit-actor parameter on _update_litellm_setting is now required
rather than optional; the chokepoint covers default_team and
mcp_semantic_filter for free, and a future caller that forgets the
actor fails loudly instead of silently skipping the audit. The two
direct-calling tests pass a dummy actor.

The environment_variables section redacts every value rather than
relying on key-name matching, because it carries credentials under
non-secret-looking uppercase keys (e.g. DATABASE_URL).

* fix(proxy): capture redacted SSO before-snapshot in audit log

Greptile review of BerriAI#31754 flagged update_sso_settings as the one endpoint
where before_value is permanently None, so the LiteLLM_SSOConfig audit
trail has no pre-change state. An auditor reviewing a secret-rotation
event could see what the SSO settings were changed to but not what they
were before.

Read the existing SSO row before the upsert, decrypt it via
proxy_config._decrypt_db_variables, and pass it as before_value.
create_config_audit_log's secret-name redaction then masks the
*_client_secret fields, so neither the old nor the new plaintext secret
lands in the audit row.

Add a regression test asserting the before-snapshot reflects the
pre-change values for non-secret fields (google_client_id) and is
redacted for secret fields (google_client_secret). Mutation-checked
against reverting to before_value=None.

The pre-existing SSO tests now also mock litellm_ssoconfig.find_unique
since the endpoint reads it; the read returns None for tests that do not
care about the before-state.

* fix: remove committed zero init migration

* refactor(proxy): audit config writes via asyncio.create_task everywhere

PR A's chokepoint audit call was refactored from a blocking await to
asyncio.create_task so that a post-save audit-log failure could not
surface as a 500 to the caller. The 12 other audit call sites added in
this PR were still using await, reintroducing the exact 500-after-commit
exposure at every sibling endpoint. Wrap them all in asyncio.create_task
to match the model_management_endpoints / key_management_endpoints /
hooks / config_override_endpoints / team_callback_endpoints /
cache_settings_endpoints house pattern, so the codebase tells one story.

The two direct-invocation tests (test_update_config_general_settings and
test_delete_config_general_settings, which call the handler in-process
rather than via TestClient) yield with `await asyncio.sleep(0)` after the
handler returns so the scheduled audit task runs before the assertion.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
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.

4 participants