{"record":{"id":"3f8208510fc0bd56","repo":"BerriAI/litellm","slug":"service-account-keys-cannot-query-user-analytics","errorCode":null,"errorMessage":"Service-account keys cannot query user analytics. Use a user-bound key, or call as a proxy admin.","messagePattern":"Service-account keys cannot query user analytics\\. Use a user-bound key, or call as a proxy admin\\.","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"litellm/proxy/management_endpoints/common_utils.py","lineNumber":97,"sourceCode":"\n\ndef require_caller_user_id_for_non_admin(\n    user_api_key_dict: UserAPIKeyAuth,\n) -> str:\n    \"\"\"Return the caller's user_id, or raise 403 if missing.\n\n    Non-admin analytics endpoints scope queries by the caller's own user_id.\n    Service-account keys are deliberately created with user_id=None\n    (key_management_endpoints.py forces ``data.user_id = None`` at key\n    creation). Without this guard, that None value flows through to the\n    daily-activity builder, which treats ``entity_id is None`` as \"no filter\"\n    and returns every tenant's data.\n\n    Callers must check is_admin first; this helper is only valid on the\n    non-admin scoping branch.\n    \"\"\"\n    if user_api_key_dict.user_id is None:\n        raise HTTPException(\n            status_code=status.HTTP_403_FORBIDDEN,\n            detail={\n                \"error\": (\n                    \"Service-account keys cannot query user analytics. Use a user-bound key, or call as a proxy admin.\"\n                )\n            },\n        )\n    return user_api_key_dict.user_id\n\n\ndef _check_passthrough_routes_caller_permission(\n    data: BaseModel,\n    user_api_key_dict: UserAPIKeyAuth,\n    *,\n    entity: str = \"key\",\n) -> None:\n    \"\"\"\n    Only proxy admins may set `allowed_passthrough_routes` (top-level or under","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/common_utils.py#L79-L115","documentation":"require_caller_user_id_for_non_admin backs the non-admin branch of the user analytics endpoints: those endpoints scope queries to the caller's own user_id, but service-account keys are deliberately created with user_id=None (key_management_endpoints.py forces data.user_id=None). A None user_id would flow into the daily-activity builder, where entity_id=None means 'no filter' — returning every tenant's data. The guard closes that hole by raising HTTP 403 before the query whenever the non-admin caller has no user_id.","triggerScenarios":"Calling user analytics endpoints (e.g. GET /user/info spend/activity routes) with a service-account virtual key (created without a user, or explicitly with NO user binding) while not being a proxy admin.","commonSituations":"Machine-to-machine integrations using service keys that try to read 'their own' analytics; health checks wired to analytics endpoints with a service key; multi-tenant deployments where the 403 is the correct privacy guarantee.","solutions":["Use a key bound to a real user (create the key with a valid user_id) when self-scoped analytics are needed","Or perform the call with an admin/master key, which takes the admin branch and is not subject to this guard","If you own the service account, treat analytics as admin-only and query them from an admin context"],"exampleFix":"# before: service-account key (user_id=None)\ncurl http://localhost:4000/user/info -H \"Authorization: Bearer sk-service\"  # 403 Service-account keys cannot query user analytics\n\n# after: user-bound key or master key\ncurl http://localhost:4000/user/info -H \"Authorization: Bearer sk-user-bound\"\ncurl http://localhost:4000/user/info -H \"Authorization: Bearer $LITELLM_MASTER_KEY\"","handlingStrategy":"validation","validationCode":"import httpx\n\nr = httpx.get(f\"{PROXY_URL}/key/info\", params={\"key\": KEY}, headers=admin_hdrs)\nr.raise_for_status()\ninfo = r.json().get(\"key_info\", {})\nif info.get(\"user_id\") is None:\n    raise PermissionError(\n        \"this is a service-account key; query analytics with a user-bound or admin key\"\n    )","typeGuard":"from typing import TypeGuard\n\ndef is_user_bound_key(info: dict) -> TypeGuard[dict]:\n    \"\"\"True when the key's info binds to a real user (analytics-safe).\"\"\"\n    return isinstance(info.get(\"user_id\"), str) and info[\"user_id\"] != \"\"","tryCatchPattern":"import httpx\n\ntry:\n    r = httpx.get(f\"{PROXY_URL}/user/info\", params=analytics_params, headers=hdrs)\n    r.raise_for_status()\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 403 and \"Service-account keys\" in e.response.text:\n        # correct behavior in multi-tenant setups: switch keys, do not retry\n        raise PermissionError(\"use a user-bound key or call as proxy admin\") from e\n    raise","preventionTips":["Never wire service-account keys to self-scoped analytics endpoints","For machine analytics, use an admin key and explicit user_id filters","Treat this 403 as tenant isolation working, not as a bug to bypass"],"tags":["litellm-proxy","authorization","service-account","multi-tenant","spend-analytics"],"backgroundTag":"insufficient-permissions","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}