andito HF Staff commited on
Commit
6147c55
·
verified ·
1 Parent(s): f7a6d00

Forward session bearer authorization

Browse files

Forward valid incoming bearer authorization to the fixed session allocator upstream. Missing and non-bearer credentials remain omitted. Includes regression coverage.

Files changed (3) hide show
  1. README.md +2 -1
  2. app.py +20 -7
  3. tests/test_session_proxy.py +105 -0
README.md CHANGED
@@ -30,4 +30,5 @@ Optional:
30
  - `GET /ready`: readiness; fails if the upstream URL is missing or invalid
31
  - `GET /session-url`: returns the currently configured upstream allocator URL
32
  - `GET /config`: alias for `/session-url`
33
- - `POST /session`: proxies the session allocation request to the upstream URL
 
 
30
  - `GET /ready`: readiness; fails if the upstream URL is missing or invalid
31
  - `GET /session-url`: returns the currently configured upstream allocator URL
32
  - `GET /config`: alias for `/session-url`
33
+ - `POST /session`: proxies the session allocation request to the upstream URL,
34
+ including a valid incoming bearer `Authorization` header when present
app.py CHANGED
@@ -60,6 +60,25 @@ def _no_store_headers() -> dict[str, str]:
60
  return {"Cache-Control": "no-store"}
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def _not_configured_response() -> JSONResponse:
64
  return JSONResponse(
65
  {
@@ -144,13 +163,7 @@ async def create_session(request: Request) -> Response:
144
  return _not_configured_response()
145
 
146
  body = await request.body()
147
- headers = {"X-Reachy-Mini-Realtime-URL": "1"}
148
- content_type = request.headers.get("content-type")
149
- if content_type:
150
- headers["Content-Type"] = content_type
151
- accept = request.headers.get("accept")
152
- if accept:
153
- headers["Accept"] = accept
154
 
155
  try:
156
  async with httpx.AsyncClient(timeout=config.timeout_s) as client:
 
60
  return {"Cache-Control": "no-store"}
61
 
62
 
63
+ def _bearer_authorization(value: str | None) -> str | None:
64
+ scheme, separator, token = (value or "").partition(" ")
65
+ token = token.strip()
66
+ if not separator or scheme.lower() != "bearer" or not token:
67
+ return None
68
+ return f"Bearer {token}"
69
+
70
+
71
+ def _upstream_headers(request: Request) -> dict[str, str]:
72
+ headers = {"X-Reachy-Mini-Realtime-URL": "1"}
73
+ forwarded_headers = {
74
+ "Content-Type": request.headers.get("content-type"),
75
+ "Accept": request.headers.get("accept"),
76
+ "Authorization": _bearer_authorization(request.headers.get("authorization")),
77
+ }
78
+ headers.update({name: value for name, value in forwarded_headers.items() if value})
79
+ return headers
80
+
81
+
82
  def _not_configured_response() -> JSONResponse:
83
  return JSONResponse(
84
  {
 
163
  return _not_configured_response()
164
 
165
  body = await request.body()
166
+ headers = _upstream_headers(request)
 
 
 
 
 
 
167
 
168
  try:
169
  async with httpx.AsyncClient(timeout=config.timeout_s) as client:
tests/test_session_proxy.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import unittest
3
+ from unittest.mock import patch
4
+
5
+ from fastapi import Request
6
+
7
+ import app as proxy_app
8
+
9
+
10
+ class FakeUpstreamResponse:
11
+ content = b'{"session_id":"session-1"}'
12
+ status_code = 200
13
+ headers = {"content-type": "application/json"}
14
+
15
+
16
+ class FakeAsyncClient:
17
+ posts: list[dict[str, object]] = []
18
+
19
+ def __init__(self, *, timeout: float):
20
+ self.timeout = timeout
21
+
22
+ async def __aenter__(self):
23
+ return self
24
+
25
+ async def __aexit__(self, exc_type, exc, traceback):
26
+ return False
27
+
28
+ async def post(self, url: str, *, content: bytes, headers: dict[str, str]):
29
+ self.posts.append(
30
+ {
31
+ "url": url,
32
+ "content": content,
33
+ "headers": headers,
34
+ "timeout": self.timeout,
35
+ }
36
+ )
37
+ return FakeUpstreamResponse()
38
+
39
+
40
+ def request_with_headers(headers: dict[str, str]) -> Request:
41
+ encoded_headers = [
42
+ (name.lower().encode("ascii"), value.encode("ascii"))
43
+ for name, value in headers.items()
44
+ ]
45
+ body_sent = False
46
+
47
+ async def receive():
48
+ nonlocal body_sent
49
+ if body_sent:
50
+ return {"type": "http.disconnect"}
51
+ body_sent = True
52
+ return {"type": "http.request", "body": b"{}", "more_body": False}
53
+
54
+ return Request(
55
+ {
56
+ "type": "http",
57
+ "method": "POST",
58
+ "path": "/session",
59
+ "headers": encoded_headers,
60
+ },
61
+ receive,
62
+ )
63
+
64
+
65
+ class SessionProxyTests(unittest.IsolatedAsyncioTestCase):
66
+ def setUp(self):
67
+ FakeAsyncClient.posts = []
68
+
69
+ async def proxy(self, headers: dict[str, str]) -> dict[str, object]:
70
+ request = request_with_headers(headers)
71
+ with (
72
+ patch.dict(
73
+ os.environ,
74
+ {proxy_app.UPSTREAM_ENV_NAME: "https://allocator.example/session"},
75
+ ),
76
+ patch.object(proxy_app.httpx, "AsyncClient", FakeAsyncClient),
77
+ ):
78
+ response = await proxy_app.create_session(request)
79
+ self.assertEqual(response.status_code, 200)
80
+ self.assertEqual(len(FakeAsyncClient.posts), 1)
81
+ return FakeAsyncClient.posts[0]
82
+
83
+ async def test_forwards_bearer_authorization_to_upstream(self):
84
+ post = await self.proxy(
85
+ {
86
+ "Authorization": "Bearer hf_user_token",
87
+ "Content-Type": "application/json",
88
+ "Accept": "application/json",
89
+ }
90
+ )
91
+
92
+ self.assertEqual(post["headers"]["Authorization"], "Bearer hf_user_token")
93
+ self.assertEqual(post["headers"]["Content-Type"], "application/json")
94
+ self.assertEqual(post["headers"]["Accept"], "application/json")
95
+ self.assertEqual(post["headers"]["X-Reachy-Mini-Realtime-URL"], "1")
96
+
97
+ async def test_omits_authorization_when_request_has_no_token(self):
98
+ post = await self.proxy({})
99
+
100
+ self.assertNotIn("Authorization", post["headers"])
101
+
102
+ async def test_does_not_forward_non_bearer_authorization(self):
103
+ post = await self.proxy({"Authorization": "Basic credentials"})
104
+
105
+ self.assertNotIn("Authorization", post["headers"])