spacedout-bits commited on
Commit
33a3bf3
·
verified ·
1 Parent(s): 50094a0

Extract dashboard auth boundary; docs

Browse files
Files changed (2) hide show
  1. finbot/dashboard.py +17 -10
  2. tests/test_dashboard.py +45 -0
finbot/dashboard.py CHANGED
@@ -257,6 +257,22 @@ again for a fresh one.
257
  """
258
 
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  def build_ui(
261
  get_services: Callable[[], Optional[Any]],
262
  gpu_probe: Optional[Callable[..., str]] = None,
@@ -268,17 +284,8 @@ def build_ui(
268
  """
269
 
270
  def _authenticate(request: gr.Request) -> Optional[int]:
271
- services = get_services()
272
- if services is None:
273
- return None
274
  token = (request.query_params or {}).get("t", "")
275
- if not token:
276
- return None
277
- try:
278
- return verify(token, services.settings.telegram_webhook_secret).user_id
279
- except TokenError as exc:
280
- log.info("Rejected dashboard token: %s", exc)
281
- return None
282
 
283
  def on_load(request: gr.Request):
284
  user_id = _authenticate(request)
 
257
  """
258
 
259
 
260
+ def authenticate(services: Optional[Any], token: str) -> Optional[int]:
261
+ """Resolve a dashboard token to a user id, or None.
262
+
263
+ The security boundary for the whole web view: everything the page renders
264
+ hangs off the id this returns, so it is deliberately a plain function that
265
+ can be tested directly rather than logic buried in a UI callback.
266
+ """
267
+ if services is None or not token:
268
+ return None
269
+ try:
270
+ return verify(token, services.settings.telegram_webhook_secret).user_id
271
+ except TokenError as exc:
272
+ log.info("Rejected dashboard token: %s", exc)
273
+ return None
274
+
275
+
276
  def build_ui(
277
  get_services: Callable[[], Optional[Any]],
278
  gpu_probe: Optional[Callable[..., str]] = None,
 
284
  """
285
 
286
  def _authenticate(request: gr.Request) -> Optional[int]:
 
 
 
287
  token = (request.query_params or {}).get("t", "")
288
+ return authenticate(get_services(), token)
 
 
 
 
 
 
289
 
290
  def on_load(request: gr.Request):
291
  user_id = _authenticate(request)
tests/test_dashboard.py CHANGED
@@ -6,6 +6,7 @@ import pytest
6
  from finbot.analytics import today_in
7
  from finbot.config import Settings
8
  from finbot.dashboard import (
 
9
  category_frame,
10
  coach_reply,
11
  load_dashboard,
@@ -223,6 +224,50 @@ class TestRendering:
223
  assert "Netflix" in render_recurring(load_dashboard(services, 1))
224
 
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  class TestCoach:
227
  def test_passes_the_question_through(self, services):
228
  add(services.store, 25000)
 
6
  from finbot.analytics import today_in
7
  from finbot.config import Settings
8
  from finbot.dashboard import (
9
+ authenticate,
10
  category_frame,
11
  coach_reply,
12
  load_dashboard,
 
224
  assert "Netflix" in render_recurring(load_dashboard(services, 1))
225
 
226
 
227
+ class TestAuthenticate:
228
+ """The gate on the whole web view. Everything else trusts its answer."""
229
+
230
+ def test_valid_token(self, services):
231
+ from finbot.auth import issue
232
+
233
+ token = issue(1, services.settings.telegram_webhook_secret)
234
+ assert authenticate(services, token) == 1
235
+
236
+ def test_no_token(self, services):
237
+ assert authenticate(services, "") is None
238
+
239
+ def test_garbage_token(self, services):
240
+ assert authenticate(services, "not-a-token") is None
241
+
242
+ def test_token_signed_with_another_secret(self, services):
243
+ from finbot.auth import issue
244
+
245
+ assert authenticate(services, issue(1, "someone-elses-secret")) is None
246
+
247
+ def test_expired_token(self, services):
248
+ import time
249
+
250
+ from finbot.auth import issue
251
+
252
+ stale = issue(
253
+ 1,
254
+ services.settings.telegram_webhook_secret,
255
+ ttl_seconds=1,
256
+ now=int(time.time()) - 60,
257
+ )
258
+ assert authenticate(services, stale) is None
259
+
260
+ def test_no_services_yet(self):
261
+ # Before the runtime exists the page must not authenticate anyone.
262
+ assert authenticate(None, "anything") is None
263
+
264
+ def test_token_identifies_the_right_user(self, services):
265
+ from finbot.auth import issue
266
+
267
+ token = issue(4242, services.settings.telegram_webhook_secret)
268
+ assert authenticate(services, token) == 4242
269
+
270
+
271
  class TestCoach:
272
  def test_passes_the_question_through(self, services):
273
  add(services.store, 25000)