Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions infra/tests/test_minimal_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,14 @@ def setUp(self):

def tearDown(self):
os.environ.pop("SIMPLEAUDIT_AUTO_LOGIN_TOKEN", None)
os.environ.pop("SIMPLEAUDIT_AUTO_LOGIN_NEXT", None)

def _login(self, token=None):
def _login(self, token=None, follow=True):
url = "/auto-login/"
if token is not None:
url += f"?token={token}"
# follow=True: the view 302s to the dashboard after signing in.
return self.client.get(url, follow=True)
return self.client.get(url, follow=follow)

@override_settings(MINIMAL_CONFIG=True)
def test_valid_token_logs_in_and_consumes(self):
Expand All @@ -210,6 +211,29 @@ def test_valid_token_logs_in_and_consumes(self):
# The token is single-use: it must be gone after the first login.
self.assertNotIn("SIMPLEAUDIT_AUTO_LOGIN_TOKEN", os.environ)

@override_settings(MINIMAL_CONFIG=True)
def test_redirects_to_visualizer_when_next_set(self):
# visualize-only mode sets SIMPLEAUDIT_AUTO_LOGIN_NEXT so the
# one-time link lands on the visualizer, not the dashboard.
env = {"SIMPLEAUDIT_AUTO_LOGIN_TOKEN": self.TOKEN, "SIMPLEAUDIT_AUTO_LOGIN_NEXT": "/visualizer/"}
with patch.dict(os.environ, env):
response = self._login(self.TOKEN, follow=False)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], "/visualizer/")

@override_settings(MINIMAL_CONFIG=True)
def test_next_must_be_a_relative_path(self):
# An absolute (http://) or protocol-relative (//evil) value must be
# ignored so a tampered env var can't turn this into an open redirect.
for bad in ("https://evil.example.com", "//evil.example.com", ""):
with patch.dict(
os.environ,
{"SIMPLEAUDIT_AUTO_LOGIN_TOKEN": self.TOKEN, "SIMPLEAUDIT_AUTO_LOGIN_NEXT": bad},
):
response = self._login(self.TOKEN, follow=False)
self.assertEqual(response.status_code, 302)
self.assertNotIn("evil.example.com", response.headers["Location"])

@override_settings(MINIMAL_CONFIG=True)
def test_token_cannot_be_replayed(self):
with patch.dict(os.environ, {"SIMPLEAUDIT_AUTO_LOGIN_TOKEN": self.TOKEN}):
Expand Down
6 changes: 6 additions & 0 deletions infra/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ def auto_login_view(request):
if user is None:
raise Http404
login(request, user)
# visualize-only mode sets this so the signed-in browser lands on the
# visualizer instead of the (unpopulated) dashboard. Trust the server
# env var, not the request: the browser only gets the token URL.
next_path = os.environ.get("SIMPLEAUDIT_AUTO_LOGIN_NEXT", "").strip()
if next_path.startswith("/") and not next_path.startswith("//"):
return redirect(next_path)
return redirect("dashboard")


Expand Down
3 changes: 3 additions & 0 deletions simpleaudit_studio/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,9 @@ def _run_visualize_only(args) -> None:

auto_login_token = secrets.token_urlsafe(32)
os.environ["SIMPLEAUDIT_AUTO_LOGIN_TOKEN"] = auto_login_token
# After the one-time sign-in, land on the visualizer instead of the
# dashboard (which has no useful content in visualize-only mode).
os.environ["SIMPLEAUDIT_AUTO_LOGIN_NEXT"] = "/visualizer/"
auto_login_url = f"http://localhost:{port}/auto-login/?token={auto_login_token}"

print("┌─────────────────────────────────────────────────────────┐")
Expand Down
Loading