mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge dee54d45f2 into 80843ae7cb
This commit is contained in:
commit
8feb378941
2 changed files with 87 additions and 8 deletions
|
|
@ -9,6 +9,7 @@ Logging Pass-Through Endpoints
|
|||
"""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import os
|
||||
from base64 import b64encode
|
||||
from typing import Final
|
||||
|
|
@ -146,6 +147,27 @@ def _build_langfuse_proxy_target(
|
|||
return str(updated_url), custom_headers
|
||||
|
||||
|
||||
def _extract_api_key_from_basic_auth(authorization_header: str) -> str:
|
||||
"""Pull the secret half out of a `Basic base64(public_key:secret_key)` header.
|
||||
|
||||
Returns "" for every shape that does not carry one, rather than raising: a
|
||||
missing header, a value that is not base64, base64 that is not utf-8, and a
|
||||
decoded value with no ":" in it. `user_api_key_auth` then rejects the empty
|
||||
key with the normal 401, instead of the route raising IndexError (or
|
||||
binascii.Error, or UnicodeDecodeError) out of an unauthenticated request and
|
||||
turning it into a 500 with a traceback in the proxy log.
|
||||
"""
|
||||
encoded: Final = authorization_header.removeprefix("Basic ").strip()
|
||||
if not encoded:
|
||||
return ""
|
||||
try:
|
||||
decoded_str = base64.b64decode(encoded).decode("utf-8")
|
||||
except (binascii.Error, UnicodeDecodeError):
|
||||
return ""
|
||||
_, separator, secret_key = decoded_str.partition(":")
|
||||
return secret_key if separator else ""
|
||||
|
||||
|
||||
@router.api_route(
|
||||
"/langfuse/{endpoint:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
||||
|
|
@ -164,14 +186,8 @@ async def langfuse_proxy_route(
|
|||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
## CHECK FOR LITELLM API KEY IN THE QUERY PARAMS - ?..key=LITELLM_API_KEY
|
||||
api_key = request.headers.get("Authorization") or ""
|
||||
|
||||
## decrypt base64 hash
|
||||
api_key = api_key.replace("Basic ", "")
|
||||
|
||||
decoded_bytes: Final = base64.b64decode(api_key)
|
||||
decoded_str: Final = decoded_bytes.decode("utf-8")
|
||||
api_key = decoded_str.split(":")[1] # assume api key is passed in as secret key
|
||||
## decrypt base64 hash; the api key is passed in as the secret key
|
||||
api_key: Final = _extract_api_key_from_basic_auth(request.headers.get("Authorization") or "")
|
||||
|
||||
user_api_key_dict: Final = await user_api_key_auth(request=request, api_key=f"Bearer {api_key}")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import socket
|
||||
from base64 import b64encode
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -6,6 +7,7 @@ from fastapi import HTTPException
|
|||
import litellm
|
||||
from litellm.proxy.vertex_ai_endpoints.langfuse_endpoints import (
|
||||
_build_langfuse_proxy_target,
|
||||
_extract_api_key_from_basic_auth,
|
||||
_get_langfuse_proxy_credentials,
|
||||
)
|
||||
|
||||
|
|
@ -100,3 +102,64 @@ def test_dynamic_langfuse_proxy_target_preserves_host_header_for_http(monkeypatc
|
|||
|
||||
assert target_url == "http://8.8.8.8/api/public/projects"
|
||||
assert headers["Host"] == "langfuse.example"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"authorization_header, expected",
|
||||
[
|
||||
# Every shape below except the two valid ones raised out of the route
|
||||
# before, turning an unauthenticated request into a 500 with a traceback.
|
||||
("", ""), # no Authorization header at all
|
||||
("Basic ", ""), # header present, credentials empty
|
||||
("Basic " + b64encode(b"pk-lf-1:sk-lf-2").decode(), "sk-lf-2"),
|
||||
(b64encode(b"pk-lf-1:sk-lf-2").decode(), "sk-lf-2"), # bare base64, no scheme
|
||||
("Basic " + b64encode(b"sk-lf-2").decode(), ""), # no ":" to split on
|
||||
("Basic YWJj=", ""), # not decodable base64
|
||||
("Basic //4=", ""), # decodes to bytes that are not utf-8
|
||||
("Bearer sk-1234", ""), # wrong scheme
|
||||
],
|
||||
)
|
||||
def test_extract_api_key_from_basic_auth_never_raises(authorization_header, expected):
|
||||
assert _extract_api_key_from_basic_auth(authorization_header) == expected
|
||||
|
||||
|
||||
def test_extract_api_key_keeps_a_secret_containing_a_colon():
|
||||
"""RFC 7617 puts everything after the first ":" in the password, so a secret
|
||||
with a colon in it must survive whole. `split(":")[1]` truncated it to the
|
||||
first segment, which then failed authentication for a non-obvious reason."""
|
||||
header = "Basic " + b64encode(b"pk-lf-1:sk:with:colons").decode()
|
||||
|
||||
assert _extract_api_key_from_basic_auth(header) == "sk:with:colons"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_langfuse_route_hands_missing_credentials_to_the_authenticator(monkeypatch):
|
||||
"""An unauthenticated request must reach user_api_key_auth and be rejected
|
||||
there, rather than raising IndexError before authentication runs."""
|
||||
from fastapi import Request
|
||||
|
||||
from litellm.proxy.vertex_ai_endpoints import langfuse_endpoints
|
||||
|
||||
seen: dict = {}
|
||||
|
||||
async def fake_user_api_key_auth(request, api_key):
|
||||
seen["api_key"] = api_key
|
||||
raise HTTPException(status_code=401, detail={"error": "Authentication Error"})
|
||||
|
||||
monkeypatch.setattr(langfuse_endpoints, "user_api_key_auth", fake_user_api_key_auth)
|
||||
|
||||
request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"method": "GET",
|
||||
"path": "/langfuse/zzz",
|
||||
"headers": [], # the reported case: no Authorization header
|
||||
"query_string": b"",
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await langfuse_endpoints.langfuse_proxy_route(endpoint="zzz", request=request, fastapi_response=None)
|
||||
|
||||
assert exc.value.status_code == 401
|
||||
assert seen["api_key"] == "Bearer "
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue