Skip to content

chore(proxy): tighten resource ownership checks - #26951

Merged
yuneng-berri merged 32 commits into
BerriAI:litellm_internal_stagingfrom
stuxf:codex/skills-containers-tenant-guard
May 5, 2026
Merged

chore(proxy): tighten resource ownership checks#26951
yuneng-berri merged 32 commits into
BerriAI:litellm_internal_stagingfrom
stuxf:codex/skills-containers-tenant-guard

Conversation

@stuxf

@stuxf stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator

Relevant issues

Linear ticket

Pre-Submission checklist

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

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • 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

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

uv run pytest tests/test_litellm/containers/test_container_proxy_ownership.py tests/test_litellm/llms/litellm_proxy/test_skills_ownership.py tests/test_litellm/proxy/test_swagger_chat_completions.py -q
46 passed

uv run pytest tests/test_litellm/containers/test_container_utils.py tests/test_litellm/containers/test_container_api.py -q
28 passed, 4 warnings

uv run pytest tests/test_litellm/proxy/auth/test_route_checks.py -q
150 passed

uv run --no-sync python -m litellm.proxy._lazy_openapi_snapshot
wrote 31 feature fragments

uv run ruff check <touched files>
All checks passed!

uv run black --check <touched files>
touched files would be left unchanged

git diff --check

Type

🐛 Bug Fix
✅ Test

Changes

This PR tightens ownership checks for proxy-managed skills and container resources.

LiteLLM-managed skills are now created with an authenticated owner scope and read/write paths resolve only records available to the caller. The same scoped lookup is used when skills are loaded during request preprocessing.

Container operations now record proxy-created container ownership and validate the caller before forwarding container path operations. Container list responses are filtered to resources tracked for the caller, and file operations share the same container ownership check before contacting the upstream provider.

The generated lazy OpenAPI snapshot is refreshed with deterministic route operation IDs so the verify job does not depend on method set ordering.

Behavior notes:

  • Direct SDK provider container calls are unchanged.
  • Proxy-created skills and containers continue to work for the creating user, team, org, or key scope.
  • Deployments without a proxy DB track newly created containers in process memory.
  • Older untracked proxy container IDs and unowned skills require an admin, a newly tracked/owned resource path, or an explicit temporary legacy opt-out.

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

@greptile-apps

greptile-apps Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR tightens ownership enforcement for proxy-managed skills and containers: new resources are stamped with the caller's identity scope, read/write paths filter results to records the caller owns, and the decoded (provider-native) container ID is now correctly forwarded via get_container_forwarding_params across all four handler paths. Several issues raised in prior review rounds are addressed — _dump_response no longer mutates the caller's dict, has_more is recalculated when items are filtered, decoded container IDs are used in forwarding params, and ownership recording is placed outside the LLM-exception scope with a graceful fallback for unexpected errors.

  • Multiple P1s flagged in prior rounds remain open: direct prisma_client.db.* Prisma calls in hot request paths (_get_container_owner, record_container_owner, _get_allowed_container_ids, list_skills) violate the project rule requiring DB access only through get_team/get_user/get_key helpers; and backwards-incompatible ownership gates for untracked containers and skills without created_by still default to closed.
  • LiteLLM_ManagedObjectTable.file_object is not updated to include a container response type alongside the newly added \"container\" file_purpose value, so any path that constructs that Pydantic model from a container row will fail validation."

Confidence Score: 3/5

Not safe to merge: multiple P1s from prior review rounds (direct Prisma calls in hot paths, backwards-incompatible default-closed ownership gates) remain unresolved.

Several fixes from prior rounds are incorporated (decoded ID forwarding, _dump_response copy, has_more recalculation, graceful create fallback), but two project-rule violations — direct DB queries in the critical request path and backwards-incompatible ownership gates — are still present and were flagged as P1 in earlier reviews. With multiple pre-existing P1s unaddressed the ceiling is below 4.

litellm/proxy/container_endpoints/ownership.py (direct Prisma calls + backwards-incompatible gates), litellm/llms/litellm_proxy/skills/handler.py (same), litellm/proxy/_types.py (file_object union mismatch)

Important Files Changed

Filename Overview
litellm/proxy/container_endpoints/ownership.py New file implementing container ownership tracking and access control — contains direct Prisma DB queries in hot request paths and backwards-incompatible ownership gates (covered in prior review rounds); several issues from prior threads now addressed (decoded ID forwarding, _dump_response mutation, has_more recalculation)
litellm/proxy/common_utils/resource_ownership.py New shared ownership scope helpers; correctly avoids shared sentinel for identity-less callers; clean and FastAPI-free
litellm/proxy/container_endpoints/endpoints.py Wires ownership record/filter/assert into create/list/retrieve/delete flows; decoded container ID now correctly forwarded via get_container_forwarding_params; record_container_owner placed outside LLM exception scope with graceful non-HTTPException fallback
litellm/proxy/container_endpoints/handler_factory.py Ownership checks added to _process_binary_request, _process_multipart_upload_request, and _process_request; decoded container ID is now forwarded via get_container_forwarding_params in all three paths
litellm/llms/litellm_proxy/skills/handler.py Adds per-skill cache and owner-scoped filtering; raises ValueError (not HTTPException) to stay FastAPI-free; backwards-incompatible gate on skills without created_by covered in prior threads
litellm/proxy/_types.py Adds 'container' to LiteLLM_ManagedObjectTable.file_purpose Literal but file_object Union type is not updated to accept container response types, creating a type mismatch
litellm/proxy/auth/auth_utils.py Security improvement: _request_blocked_callback_params is now included in _build_banned_observability_params, closing a gap in the request-body deny list
litellm/skills/main.py Plumbs user_api_key_dict from request kwargs through to skills handler for create/list/get/delete operations; helpers correctly extract auth from metadata
litellm/proxy/hooks/litellm_skills/main.py Passes user_api_key_dict to fetch_skill_from_db in the skills injection hook; minimal change, correct

Reviews (23): Last reviewed commit: "chore(container): use delete_cache, json..." | Re-trigger Greptile

Comment thread litellm/proxy/container_endpoints/ownership.py
Comment thread litellm/proxy/container_endpoints/handler_factory.py Outdated
Comment thread litellm/proxy/container_endpoints/handler_factory.py Outdated
@codecov

codecov Bot commented May 1, 2026

Copy link
Copy Markdown

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

Comment thread litellm/proxy/container_endpoints/ownership.py Outdated
@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

Re-requesting on the latest head. The previous encoded-container concern is covered by preserving the managed ID through proxy forwarding so the container layer can decode before upstream calls while retaining routing metadata, with tests added. The untracked-container concern now has no-DB in-memory ownership for new containers plus a secure-default legacy opt-out.

Comment thread litellm/proxy/container_endpoints/ownership.py
Comment thread litellm/llms/litellm_proxy/skills/handler.py
Comment thread litellm/proxy/container_endpoints/ownership.py Outdated
@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

Re-requesting on latest head after adding direct proxy retrieve/delete forwarding coverage for the managed-container-id path.

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

Re-requesting on latest head. Added secure-default legacy opt-out coverage for unowned skills as well.

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

Re-requesting on latest head after resetting has_more for empty filtered container-list pages and adding coverage for object and dict responses.

Comment thread litellm/proxy/container_endpoints/endpoints.py Outdated
Comment thread litellm/proxy/container_endpoints/ownership.py Outdated
@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

Re-requesting on latest head. Proxy container paths now forward decoded provider container IDs and carry decoded model metadata for routing, filtered pagination is conservative when any items are removed, and ownership persistence failure falls back to in-process tracking.

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review the latest head. The follow-up addresses the lifecycle/error-handling and compatibility concerns raised in the last summary, with regression coverage. Note that this repo's DB convention is Prisma model methods and no raw SQL; the changed code follows that convention.

@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review the latest head. Added endpoint coverage for the post-processing branches in the follow-up test commit.

Comment thread litellm/proxy/container_endpoints/ownership.py
@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review the latest head. The dict response mutation finding is now fixed with regression coverage.

Comment thread litellm/proxy/container_endpoints/ownership.py Outdated
Comment thread litellm/proxy/container_endpoints/ownership.py Outdated
@stuxf

stuxf commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review the latest head. The DB-recovery/in-memory fallback path and duplicate OpenAPI operation IDs are now addressed with regression coverage.

@stuxf

stuxf commented May 2, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai re-review please. Addressing the three P1s from your last round:

P1 #2 (direct Prisma in hot path) — addressed in 6194028. ContainerOwnershipStore.get_owner and LiteLLMSkillsStore.find_skill are now wrapped in a TTL'd in-memory cache (_CONTAINER_OWNER_CACHE / _SKILL_CACHE) that mirrors the _byok_cred_cache pattern already used in mcp_server/server.py: per-key (value, monotonic_timestamp), 60s TTL, 10k-entry cap with full-clear on overflow, invalidation on every write. Negatives (None) are cached so untracked-resource checks also skip the DB. Net: one DB query per (key, TTL window) instead of one per request. The get_team/get_user/get_key precedent uses UserApiKeyCache (DualCache over Redis) — I picked the simpler module-level dict pattern because (a) ownership changes are local-write-then-read so cross-worker invalidation isn't load-bearing here, (b) the BYOK cache uses the same shape and was accepted, (c) no new infra dependency. The CLAUDE.md guidance on proxy DB access prescribes prisma_client.db.<model>.<method>() directly — there's no project rule mandating cached helpers for new tables (get_team/etc are auth hot-path optimizations specific to those tables); the rule we do apply here is the perf one, which is now satisfied.

P1 #3 (orphan container) — addressed in 6c37e9d. record_container_owner runs inside the create_container handler with split exception handling: HTTPException (auth conflict) propagates verbatim so callers see real status codes; unexpected Exception is logged and the response is returned to the caller (so they aren't billed for a resource they can't address — an operator reconciles the untracked DB row from logs). Best-effort upstream-delete-on-conflict was considered and rejected: the failure modes that surface HTTPException are themselves degenerate (provider returned a duplicate ID, or the DB row's file_purpose was reused for a different object type) — that's a reconcile-by-hand situation, not something a synchronous delete-by-id call should paper over.

P1 #1 (default-deny gates) — held intentionally. This PR is the fix for VERIA-20 (cross-tenant data leakage in skills/containers). Pre-existing untracked rows are exactly the data that's leaking — flipping the gates to default-permissive would re-introduce the vulnerability for anyone who upgrades without explicitly opting in to safety. The LITELLM_ALLOW_UNTRACKED_CONTAINER_ACCESS and LITELLM_ALLOW_UNOWNED_SKILL_ACCESS env vars are the upgrade-path opt-out for operators who need to keep legacy access while they migrate. Same pattern was accepted on VERIA-37 / PR #26867. The "default permissive + opt-in to strict" rule cited applies to feature flags, not to security fixes whose entire point is closing a leak.

CI is green; cache adds 11 new cache tests, 56 total in the suite.

@stuxf

stuxf commented May 2, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the current Greptile P1 in handler_factory.py: all dynamic container handlers now use the decoded provider-native container id and resolved provider returned by assert_user_can_access_container, preserving model_id from managed ids for routing.

Validation:

  • uv run --extra proxy --group proxy-dev pytest tests/test_litellm/containers/test_container_proxy_ownership.py::test_should_validate_owner_and_forward_decoded_id_for_proxy_forwarding tests/test_litellm/containers/test_container_proxy_ownership.py::test_should_validate_owner_and_forward_decoded_id_for_multipart_upload tests/test_litellm/containers/test_azure_container_transformation.py::TestAzureContainerKnownFailureRegressions::test_proxy_process_request_forwards_decoded_container_id tests/test_litellm/containers/test_azure_container_transformation.py::TestAzureContainerKnownFailureRegressions::test_regression_binary_file_request_routes_through_proxy_processor tests/test_litellm/containers/test_azure_container_transformation.py::TestAzureContainerKnownFailureRegressions::test_regression_multipart_upload_request_uses_provider_from_managed_id -q
  • uv run ruff check litellm/proxy/container_endpoints/handler_factory.py tests/test_litellm/containers/test_container_proxy_ownership.py tests/test_litellm/containers/test_azure_container_transformation.py
  • uv run black --check litellm/proxy/container_endpoints/handler_factory.py tests/test_litellm/containers/test_container_proxy_ownership.py tests/test_litellm/containers/test_azure_container_transformation.py

@greptileai

stuxf added 3 commits May 4, 2026 22:43
… path

filter_container_list_response runs after the upstream call has
already succeeded; treating an ownership-lookup failure as an LLM-API
error fires post_call_failure_hook for a successful upstream call and
returns a misleading provider-shaped error to the client. Run the
filter outside the try/except so genuine LLM errors stay scoped to
the upstream call.
…rpose Literal

Two cleanups from the /simplify pass:

* ``_CONTAINER_OWNER_CACHE`` and ``_SKILL_CACHE`` now LRU-evict via
  ``OrderedDict.popitem(last=False)`` instead of full ``clear()`` at
  capacity. Full clears converted a steady-state cached workload into a
  periodic full-DB-load oscillation as the cache repopulated from zero
  and cleared again. Reads now ``move_to_end`` so the just-touched
  entry survives the next eviction. Mirrors the pre-existing LRU
  pattern in ``_remember_container_owner``.

* ``LiteLLM_ManagedObjectTable.file_purpose`` Literal now includes
  ``"container"`` so Pydantic validation accepts rows written by the
  ownership store.
LITELLM_ALLOW_UNTRACKED_CONTAINER_ACCESS and
LITELLM_ALLOW_UNOWNED_SKILL_ACCESS were operator-toggleable opt-outs
for the cross-tenant access primitive this PR closes — flipping either
on re-enabled exactly the VERIA-20 read path. Default-secure with no
escape hatch matches sibling fixes (vector-store cred isolation, semantic
cache key isolation, user_config strip): all rejected the
opt-out-of-security pattern.

Untracked containers and unowned skills (rows that pre-date this
enforcement) are admin-only. Non-admin owners need to either re-create
via the now-tracked flow or have an admin assign ``created_by`` on the
existing row. Update tests to assert the strict-only behaviour.
@stuxf

stuxf commented May 4, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai review

For the prior P1 about "direct Prisma queries in critical request path" — re-reading CLAUDE.md, the rule is specifically against raw SQL (`execute_raw` / `query_raw`):

Do not write raw SQL for proxy DB operations. Use Prisma model methods instead of `execute_raw` / `query_raw`.

Use the generated client: `prisma_client.db.` (e.g. `litellm_tooltable`, `litellm_usertable`) with `.upsert()`, `.find_many()`, `.find_unique()`, `.update()`, `.update_many()` as appropriate.

`ContainerOwnershipStore` and `LiteLLMSkillsStore` use only Prisma model methods (`find_unique`, `find_first`, `find_many`, `create`, `update`, `delete`) — exactly what CLAUDE.md prescribes. The hot-path concern is mitigated by the per-key TTL caches (`_CONTAINER_OWNER_CACHE`, `_SKILL_CACHE`) which absorb the read load before it hits Prisma. The list-path query uses a single batched `find_many({"created_by": {"in": ...}})` — no N+1.

Other addressed items in this revision:

  • `LITELLM_ALLOW_UNTRACKED_CONTAINER_ACCESS` and `LITELLM_ALLOW_UNOWNED_SKILL_ACCESS` opt-out env vars dropped (de682c8) — default-secure, no escape hatch.
  • Cache eviction switched from `clear()` to LRU `popitem(last=False)` (ec9b84d).
  • `filter_container_list_response` moved out of the LLM-exception scope (4fa5778).
  • `file_purpose` Literal widened to include `"container"` (ec9b84d).

Comment thread litellm/proxy/common_utils/resource_ownership.py Outdated
stuxf added 2 commits May 4, 2026 23:40
…tinel scope

UNSCOPED_RESOURCE_OWNER_SCOPE collapsed every caller without an
identity field (no user_id / team_id / org_id / api_key / token) into
a single shared owner — a cross-tenant access primitive: any two such
callers could see and delete each other's containers and skills.

Drop the sentinel. ``get_primary_resource_owner_scope`` returns
``None`` and ``get_resource_owner_scopes`` returns ``[]`` for
identity-less callers. ``record_container_owner`` and
``LiteLLMSkillsHandler.create_skill`` now reject creates from
identity-less callers with a 403 instead of stamping the placeholder.
Read paths already deny ``owner is None`` correctly so legacy rows
(if any) are admin-only.
@stuxf

stuxf commented May 4, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Two new commits:

  • 758b488 drops UNSCOPED_RESOURCE_OWNER_SCOPE sentinel (the cross-tenant primitive you flagged). Identity-less callers now get an empty scope set; create paths reject with 403 instead of stamping a placeholder.
  • 777862a is the merge from internal_staging (previous CI failure was a stale-branch issue, the test that broke was added by an upstream PR).

Re the prior P1 about ContainerOwnershipStore and LiteLLMSkillsStore direct Prisma calls — CLAUDE.md prohibits raw SQL (execute_raw / query_raw), not Prisma model methods:

Do not write raw SQL for proxy DB operations. Use Prisma model methods instead of execute_raw / query_raw.
Use the generated client: prisma_client.db.<model> (e.g. litellm_tooltable, litellm_usertable) with .upsert(), .find_many(), .find_unique(), .update(), .update_many() as appropriate.

Both stores use only .find_unique(), .find_first(), .find_many(), .create(), .update(), .delete(). Hot-path concern is mitigated by the per-key TTL caches (_CONTAINER_OWNER_CACHE, _SKILL_CACHE) which absorb reads. The list path uses a single batched find_many({"created_by": {"in": ...}}) — no N+1.

Comment thread litellm/llms/litellm_proxy/skills/handler.py Outdated
…dy bouncer

Two cleanups:

* ``LiteLLMSkillsHandler.create_skill`` raised ``HTTPException`` for
  identity-less callers, importing FastAPI from a ``litellm/llms/``
  module — that violates the project rule that FastAPI lives only
  under ``proxy/``. Switch to ``ValueError`` (the same shape the rest
  of the handler uses for not-found/forbidden) and update the test.

* The proxy-auth body bouncer derived its observability ban list from
  ``_supported_callback_params`` only, missing
  ``_request_blocked_callback_params`` (where ``gcs_bucket_name`` and
  ``gcs_path_service_account`` live). Two recently-merged sibling PRs
  (#27019 added the deny list, #27081 added the test asserting these
  are rejected at the request body root) crossed without folding them
  together. Union the GCS deny list into the bouncer's derivation so
  the single source of truth covers both code paths.
@stuxf

stuxf commented May 4, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai review

12fe945 addresses the FastAPI-import P1 — create_skill now raises ValueError (matching the rest of the handler) instead of HTTPException, keeping the litellm/llms/litellm_proxy/ module FastAPI-free.

Also fixed an upstream-integration bug surfaced by the merge: _build_banned_observability_params() derived from _supported_callback_params only, but gcs_bucket_name/gcs_path_service_account live in the parallel _request_blocked_callback_params set added by #27019. PR #27081's TestObservabilityCallbackBans asserts those are rejected at request body root. The two sibling PRs landed without folding the lists together — folded now.

Re the persistent "direct Prisma calls in hot path" P1: I posted CLAUDE.md's exact wording on the prior comment — the rule is against raw SQL (execute_raw / query_raw), and the canonical Prisma method calls used here (find_unique, find_first, find_many, create, update, delete) are explicitly what the rule prescribes. The 60s TTL caches absorb hot-path reads. The list path uses a single batched find_many({"created_by": {"in": ...}}) — no N+1.

Re the "backwards-incompatible gates" P1 — yes, by design. Default-secure with no opt-out flag matches sibling fixes (vector store credential leak, semantic cache key isolation, user_config strip, callback strip — all merged into litellm_internal_staging). Operators with pre-existing untracked containers/skills migrate via admin-managed re-creation or direct UPDATE created_by on the row.

Comment thread litellm/proxy/container_endpoints/ownership_store.py Outdated
Comment thread litellm/proxy/container_endpoints/ownership.py
@stuxf

stuxf commented May 5, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai review

The two remaining P1s in your latest review look like they're reading stale context. Concretely:

1. "The env-var escape hatches exist but default to disabled" — they don't exist. LITELLM_ALLOW_UNTRACKED_CONTAINER_ACCESS and LITELLM_ALLOW_UNOWNED_SKILL_ACCESS were removed in commit de682c810e (visible in the file list). Verifying on HEAD:

$ grep -rn 'ALLOW_UNTRACKED\|ALLOW_UNOWNED\|allow_untracked\|allow_unowned' .
(no matches)

The default IS strict, with no escape hatch — by design, matching every other security PR merged into litellm_internal_staging recently (vector store credential leak #27082, semantic cache key isolation #26990, user_config strip #27084, callback strip #27081). Operators with pre-existing untracked rows migrate via direct UPDATE created_by or admin-managed re-creation.

2. "Direct Prisma calls violate the project rule" — the project rule (CLAUDE.md, in this repo's root) says:

Do not write raw SQL for proxy DB operations. Use Prisma model methods instead of execute_raw / query_raw.
Use the generated client: prisma_client.db.<model> (e.g. litellm_tooltable, litellm_usertable) with .upsert(), .find_many(), .find_unique(), .update(), .update_many() as appropriate.

ContainerOwnershipStore and LiteLLMSkillsStore use only those methods — exactly what the rule prescribes. get_team/get_user/get_key are user/team/key-specific helpers that wrap the same Prisma calls plus internal_usage_cache; they don't apply to arbitrary entity types. The same pattern is used by:

  • vector_store_endpoints/management_endpoints.py (10+ direct prisma_client.db.litellm_managedvectorstorestable.* calls)
  • agent_endpoints/endpoints.py (direct prisma_client.db.litellm_agentstable.* calls)
  • mcp_server/db.py (direct prisma_client.db.litellm_mcpservertable.* calls)

Hot-path concern is mitigated by the _CONTAINER_OWNER_CACHE/_SKILL_CACHE 60s TTL caches (which are the same shape as the working _byok_cred_cache in mcp_server/server.py). The list path is a single batched find_many({"created_by": {"in": ...}}) — no N+1.

If you have specific evidence either of those two interpretations is correct, please point at the line — I'd rather fix a real issue than ship past a misunderstanding.

…back, hand-rolled cache

Substantial reduction (~765 LOC) without changing the security
boundary:

* Drop ContainerOwnershipStore and LiteLLMSkillsStore — both were
  one-method-per-Prisma-call wrappers. Inline the calls instead,
  matching the established pattern in vector_store_endpoints,
  agent_endpoints, and mcp_server/db.py.

* Drop the prisma_client is None in-memory fallback. Production
  deploys always have Prisma; running ownership-critical paths on a
  process-local dict is a security footgun in the dev-mode case it
  was meant to support, and complicates every code path with a
  branch. Fail-secure: skip recording if Prisma is unavailable, and
  treat reads as "not found" (admin-only).

* Drop the hand-rolled module-level cache. Replace with the existing
  litellm.caching.in_memory_cache.InMemoryCache, which already has
  TTL + max-size + eviction tested in its own module. Sentinel string
  for negative caching since InMemoryCache can't disambiguate "miss"
  from "cached as None".

* Tests: drop coverage for removed code paths (in-memory fallback,
  hand-rolled cache internals). Keep tests for actual behavior (cache
  hit-rate, negative caching, owner check, list filtering,
  identity-less reject, admin bypass).
@stuxf

stuxf commented May 5, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Significant simplification (commit 6ce84effe1): -945/+180 lines.

  • Dropped ContainerOwnershipStore and LiteLLMSkillsStore thin Prisma wrappers — inlined the calls (matching the established pattern in vector_store_endpoints, agent_endpoints, mcp_server/db.py).
  • Dropped the prisma_client is None in-memory fallback path. Production always has Prisma; running ownership-critical reads from a process-local dict was a footgun. Fail-secure: skip recording if Prisma is unavailable, treat reads as not-found.
  • Replaced the hand-rolled module-level cache (OrderedDict[str, Tuple[Any, float]] + _read/_write/_invalidate helpers) with the existing litellm.caching.in_memory_cache.InMemoryCache. Same LRU/TTL semantics, much less code, and it's the canonical cache primitive in this codebase.
  • Tests reduced accordingly — kept coverage for owner check, list filtering, identity-less reject, cache hit-rate, negative caching, admin bypass.

Address Greptile P2 follow-ups from the prior round:

* Cache ``_get_allowed_container_ids`` (60s LRU/TTL keyed by sorted
  owner-scope tuple) so ``GET /v1/containers`` doesn't issue a fresh
  ``find_many`` against ``litellm_managedobjecttable`` on every list
  call. Invalidate the caller's own cache entry when they record a
  new owner so the just-created container shows up on their next list.

* Tighten the admin early-return in ``record_container_owner`` to skip
  ONLY when there's literally no container ID to stamp. An admin with
  identity (the master-key path populates ``user_id`` + ``api_key``)
  flows through the normal record path so admin-created containers are
  tracked like any other caller's. The truly-identity-less admin case
  still falls through to the 403 below — correct fail-secure default.

Skill-cache invalidation gap (also flagged by Greptile) is moot: there
is no skill update endpoint exposed; ownership-affecting mutations are
only delete (already invalidates) and create (new ID, no cache entry
to update).
@stuxf

stuxf commented May 5, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai review

2adfa96db2 addresses two of the three remaining P2s from your last round:

  1. _get_allowed_container_ids cache — added a per-scope-tuple InMemoryCache (60s TTL, 2048 entries) so GET /v1/containers filtering doesn't issue a fresh find_many on every list call. Invalidates the caller's own entry on record_container_owner so a just-created container shows up on the next list.

  2. Admin early-return in record_container_owner — narrowed to skip only when there's no container ID to stamp. Admins with identity (master-key path populates user_id + api_key) now flow through the normal record path, so admin-created containers aren't permanently untracked.

  3. Skill cache stale on update — moot: no update_skill endpoint exists. Ownership-affecting mutations are delete (already invalidates) and create (new ID, no entry to invalidate).

Backwards-compat default-strict gates remain by design — every other security PR merged into litellm_internal_staging (vector store cred leak, semantic cache key isolation, user_config strip, callback strip, GHSA fixes) follows the same default-strict-no-opt-out pattern. Migration is via direct UPDATE created_by on the row or admin-managed re-creation, documented in the PR body.

/simplify follow-ups:

* Replace the two-``pop`` reach into ``cache_dict``/``ttl_dict`` with
  the existing public ``InMemoryCache.delete_cache(key)`` — the same
  idiom used elsewhere in the proxy. Bonus: ``delete_cache`` calls
  ``_remove_key`` which also handles ``expiration_heap`` consistency
  the direct pops were silently leaking.

* JSON-encode the sorted scope list for the cache key instead of
  ``"|".join``. ``user_id`` / ``team_id`` / ``org_id`` / ``api_key``
  are free-form strings and could contain a literal ``|`` — JSON
  quoting escapes any in-string separator unambiguously.

* Extract ``_allowed_container_ids_cache_key()`` so the read and
  invalidation sites compute the key the same way.

* Fix a placeholder-then-overwrite test construction: the
  ``__module__.split(".")[0] and "proxy_admin"`` line evaluated to a
  literal string that was immediately overwritten with the real enum
  value. Hoist the import and construct directly.
user_api_key_dict: Optional[UserAPIKeyAuth],
) -> bool:
if user_api_key_dict is None:
return True

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.

Medium: Ownership bypass for None auth context

Returning True when user_api_key_dict is None means any caller that omits the auth parameter gets unrestricted access to all resources. Since get_skill, delete_skill, list_skills, and fetch_skill_from_db all default user_api_key_dict=None, a code path that forgets to thread the auth object through (e.g. a new endpoint or internal helper) silently skips the ownership check.

The stated rationale (anonymous callers should pass) conflicts with the PR's goal of tenant isolation. Consider defaulting to False (deny) when user_api_key_dict is None, and requiring callers that genuinely need open access to pass an explicit admin-like sentinel.

@veria-ai

veria-ai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Medium: Auth bypass when user_api_key_dict is None in resource ownership check

This PR adds resource ownership checks for skills and containers. The core ownership utility user_can_access_resource_owner returns True when user_api_key_dict is None, and the skills handler methods (get_skill, delete_skill, list_skills, fetch_skill_from_db) all default user_api_key_dict=None. Any internal caller that omits the parameter bypasses tenant isolation entirely.


Status: 1 open
Risk: 4/10

… codex/skills-containers-tenant-guard

# Conflicts:
#	litellm/proxy/auth/auth_utils.py
@yuneng-berri
yuneng-berri enabled auto-merge May 5, 2026 01:45
@yuneng-berri
yuneng-berri merged commit 555a813 into BerriAI:litellm_internal_staging May 5, 2026
42 checks passed
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…enant-guard

chore(proxy): tighten resource ownership checks
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