stemtrace 0.3.3__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stemtrace-0.3.3 → stemtrace-0.3.4}/.bumpversion.toml +1 -1
- {stemtrace-0.3.3 → stemtrace-0.3.4}/CHANGELOG.md +6 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/PKG-INFO +2 -2
- {stemtrace-0.3.3 → stemtrace-0.3.4}/README.md +1 -1
- {stemtrace-0.3.3 → stemtrace-0.3.4}/pyproject.toml +1 -1
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/__init__.py +1 -1
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/api/websocket.py +6 -2
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/login_routes.py +21 -4
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/package.json +1 -1
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_form_login.py +104 -6
- {stemtrace-0.3.3 → stemtrace-0.3.4}/uv.lock +1 -1
- {stemtrace-0.3.3 → stemtrace-0.3.4}/.dockerignore +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/.github/dependabot.yml +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/.github/workflows/ci.yml +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/.github/workflows/e2e.yml +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/.github/workflows/release.yml +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/.gitignore +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/.pre-commit-config.yaml +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/.python-version +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/CONTRIBUTING.md +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/Dockerfile +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/Dockerfile.e2e +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/LICENSE +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/Makefile +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/build_ui.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/docker-compose.e2e.rabbitmq.yml +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/docker-compose.e2e.yml +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/docker-compose.rabbitmq.yml +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/docker-compose.yml +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/docs/screenshots/task_details.png +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/docs/screenshots/unregistered.png +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/docs/screenshots/workflow.png +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/examples/celery_app.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/examples/fastapi_integration.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/examples/with_auth.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/examples/with_login.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/progress.md +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/scripts/wait_for_http.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/core/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/core/events.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/core/exceptions.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/core/graph.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/core/ports.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/library/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/library/bootsteps.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/library/config.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/library/scrubbing.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/library/signals.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/library/transports/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/library/transports/memory.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/library/transports/rabbitmq.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/library/transports/redis.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/py.typed +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/__main__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/api/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/api/routes.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/api/schemas.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/consumer.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/auth.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/extension.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/form_auth.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/fastapi/router.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/store.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/biome.json +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/index.html +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/package-lock.json +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/playwright.config.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/postcss.config.js +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/api/client.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/api/queries.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/DateRangePicker.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskStateBadge.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/index.css +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/main.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/__root.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/graphs.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/index.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/workers.tsx +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/utils/format.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/vite-env.d.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/graphs.spec.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/registry.spec.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/tasks.spec.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/workers.spec.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tsconfig.json +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/vite.config.ts +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/static.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/templates/login.html +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/websocket.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/conftest.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/e2e/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/e2e/conftest.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/e2e/tasks.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/e2e/test_api_e2e.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/e2e/test_workers_e2e.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/integration/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/integration/test_embedded_static.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/integration/test_fastapi_integration.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/integration/test_rabbitmq_transport.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/integration/test_redis_transport.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/integration/test_static_ui.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/integration/test_workers_refresh.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/__init__.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_auth.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_bootsteps.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_config.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_consumer.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_events.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_examples.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_exceptions.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_form_auth.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_graph.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_init.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_routes.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_scrubbing.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_signals.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_store.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_transports.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_ui_static_prefix.py +0 -0
- {stemtrace-0.3.3 → stemtrace-0.3.4}/tests/unit/test_websocket.py +0 -0
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.4] - 2026-06-25
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- WebSocket: deny unauthenticated connections during the handshake instead of accepting and then closing them. The accept-then-close path half-opened the connection, so when an unauthenticated client (e.g. a port scanner) aborted mid-handshake, uvicorn's legacy `websockets` implementation ran `close()` on a protocol whose `transfer_data_task` was never set, raising `AttributeError: 'WebSocketProtocol' object has no attribute 'transfer_data_task'`. Closing before `accept()` rejects the handshake (HTTP 403) without opening the protocol object.
|
|
14
|
+
- Form login: build the login form action and redirects as relative paths instead of absolute URLs, and derive the session cookie's `Secure` flag from `X-Forwarded-Proto`. Behind a TLS-terminating proxy that doesn't forward the scheme to the ASGI scope (e.g. uvicorn without `--forwarded-allow-ips`), absolute URLs were downgraded to `http://`, so the browser posted the login form over http, the proxy answered `301 -> https`, the POST body was dropped, and login silently failed (with a mixed-content warning). stemtrace now works behind such a proxy without requiring forwarded-headers configuration.
|
|
15
|
+
|
|
10
16
|
## [0.3.3] - 2026-03-20
|
|
11
17
|
|
|
12
18
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stemtrace
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: A lightweight Celery task flow visualizer
|
|
5
5
|
Project-URL: Homepage, https://github.com/iansokolskyi/stemtrace
|
|
6
6
|
Project-URL: Documentation, https://github.com/iansokolskyi/stemtrace#readme
|
|
@@ -56,7 +56,7 @@ Description-Content-Type: text/markdown
|
|
|
56
56
|
|
|
57
57
|
**Zero-infrastructure Celery task flow visualizer**
|
|
58
58
|
|
|
59
|
-
[](https://pypi.org/project/stemtrace)
|
|
60
60
|
[](https://pypi.org/project/stemtrace/)
|
|
61
61
|
[](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
|
|
62
62
|
[](https://codecov.io/gh/iansokolskyi/stemtrace)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Zero-infrastructure Celery task flow visualizer**
|
|
4
4
|
|
|
5
|
-
[](https://pypi.org/project/stemtrace)
|
|
6
6
|
[](https://pypi.org/project/stemtrace/)
|
|
7
7
|
[](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
|
|
8
8
|
[](https://codecov.io/gh/iansokolskyi/stemtrace)
|
|
@@ -33,8 +33,12 @@ def create_websocket_router(
|
|
|
33
33
|
secret=form_auth_config.secret,
|
|
34
34
|
expected_username=form_auth_config.username,
|
|
35
35
|
):
|
|
36
|
-
#
|
|
37
|
-
|
|
36
|
+
# Deny the handshake without completing it. Accepting first and
|
|
37
|
+
# then closing half-opens the connection; if the unauthenticated
|
|
38
|
+
# client aborts during that dance, uvicorn's legacy websockets
|
|
39
|
+
# impl runs close() on a protocol whose transfer_data_task was
|
|
40
|
+
# never set and raises AttributeError. Closing before accept lets
|
|
41
|
+
# Starlette reject the handshake (HTTP 403) without opening it.
|
|
38
42
|
await websocket.close(code=1008)
|
|
39
43
|
return
|
|
40
44
|
|
|
@@ -40,6 +40,20 @@ def _safe_next(next_path: str | None, *, default: str) -> str:
|
|
|
40
40
|
return next_path
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def _request_is_https(request: Request) -> bool:
|
|
44
|
+
"""Best-effort HTTPS detection that survives a TLS-terminating proxy.
|
|
45
|
+
|
|
46
|
+
Falls back to the ``X-Forwarded-Proto`` header when the ASGI scheme is
|
|
47
|
+
``http`` because proxy headers weren't applied to the scope (e.g. uvicorn
|
|
48
|
+
without ``--forwarded-allow-ips``). The first value of a comma-separated
|
|
49
|
+
list is the original client scheme.
|
|
50
|
+
"""
|
|
51
|
+
if request.url.scheme == "https":
|
|
52
|
+
return True
|
|
53
|
+
forwarded = request.headers.get("x-forwarded-proto", "")
|
|
54
|
+
return forwarded.split(",")[0].strip().lower() == "https"
|
|
55
|
+
|
|
56
|
+
|
|
43
57
|
def _login_page_html(*, action_path: str, error: str | None, next_path: str) -> str:
|
|
44
58
|
"""Render the login page HTML from a template."""
|
|
45
59
|
escaped_title = html.escape(_DEFAULT_TITLE, quote=True)
|
|
@@ -92,7 +106,10 @@ def create_login_router(
|
|
|
92
106
|
next_path = _safe_next(
|
|
93
107
|
request.query_params.get("next"), default=default_next_path
|
|
94
108
|
)
|
|
95
|
-
|
|
109
|
+
# Relative path (not an absolute URL): the browser submits using the
|
|
110
|
+
# page's own scheme/host, so a TLS-terminating proxy that doesn't
|
|
111
|
+
# forward the scheme can't downgrade the form action to http.
|
|
112
|
+
action_path = request.url_for("stemtrace_login_submit").path
|
|
96
113
|
rendered_html = _login_page_html(
|
|
97
114
|
action_path=action_path, error=error, next_path=next_path
|
|
98
115
|
)
|
|
@@ -125,7 +142,7 @@ def create_login_router(
|
|
|
125
142
|
qs = {"error": "Invalid username or password"}
|
|
126
143
|
if next_param:
|
|
127
144
|
qs["next"] = next_param
|
|
128
|
-
login_url =
|
|
145
|
+
login_url = request.url_for("stemtrace_login_page").path
|
|
129
146
|
return RedirectResponse(
|
|
130
147
|
url=login_url + "?" + urllib.parse.urlencode(qs),
|
|
131
148
|
status_code=303,
|
|
@@ -141,7 +158,7 @@ def create_login_router(
|
|
|
141
158
|
value=config.create_session_cookie_value(),
|
|
142
159
|
httponly=True,
|
|
143
160
|
samesite="lax",
|
|
144
|
-
secure=request
|
|
161
|
+
secure=_request_is_https(request),
|
|
145
162
|
path=config.cookie_path,
|
|
146
163
|
)
|
|
147
164
|
return response
|
|
@@ -149,7 +166,7 @@ def create_login_router(
|
|
|
149
166
|
@router.post("/logout", include_in_schema=False, name="stemtrace_logout")
|
|
150
167
|
async def logout(request: Request) -> RedirectResponse:
|
|
151
168
|
"""Clear the session cookie and redirect to login."""
|
|
152
|
-
login_url =
|
|
169
|
+
login_url = request.url_for("stemtrace_login_page").path
|
|
153
170
|
response = RedirectResponse(url=login_url, status_code=303)
|
|
154
171
|
response.delete_cookie(key=config.cookie_name, path=config.cookie_path)
|
|
155
172
|
return response
|
|
@@ -73,6 +73,107 @@ class TestFormLoginProtection:
|
|
|
73
73
|
assert ext.ws_manager.connection_count == 1
|
|
74
74
|
assert ext.ws_manager.connection_count == 0
|
|
75
75
|
|
|
76
|
+
def test_unauthenticated_websocket_denied_at_handshake(self) -> None:
|
|
77
|
+
"""An unauthenticated WebSocket must be denied DURING the handshake,
|
|
78
|
+
without calling ``accept()`` first.
|
|
79
|
+
|
|
80
|
+
Accepting the handshake and then closing it half-opens the connection;
|
|
81
|
+
when an unauthenticated client (e.g. a port scanner) aborts during that
|
|
82
|
+
accept->close dance, uvicorn's legacy ``websockets`` implementation runs
|
|
83
|
+
``close()`` on a protocol whose ``transfer_data_task`` was never set,
|
|
84
|
+
raising ``AttributeError: 'WebSocketProtocol' object has no attribute
|
|
85
|
+
'transfer_data_task'``. Denying before accept avoids opening the
|
|
86
|
+
protocol object at all. Regression test for that crash.
|
|
87
|
+
"""
|
|
88
|
+
app = FastAPI()
|
|
89
|
+
stemtrace.init_app(
|
|
90
|
+
app,
|
|
91
|
+
broker_url="memory://",
|
|
92
|
+
embedded_consumer=False,
|
|
93
|
+
serve_ui=False,
|
|
94
|
+
login_username="admin",
|
|
95
|
+
login_password="secret", # NOSONAR - test credential only
|
|
96
|
+
login_secret="test-secret",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
with TestClient(app) as client:
|
|
100
|
+
accepted = False
|
|
101
|
+
with (
|
|
102
|
+
pytest.raises(WebSocketDisconnect) as exc_info,
|
|
103
|
+
client.websocket_connect("/stemtrace/ws") as ws,
|
|
104
|
+
):
|
|
105
|
+
# Only reached if the server accepted the handshake first.
|
|
106
|
+
accepted = True
|
|
107
|
+
ws.receive_text()
|
|
108
|
+
|
|
109
|
+
assert accepted is False, (
|
|
110
|
+
"unauthenticated WebSocket must be denied before accept(); "
|
|
111
|
+
"accepting then closing crashes uvicorn's legacy websockets impl"
|
|
112
|
+
)
|
|
113
|
+
assert exc_info.value.code == 1008
|
|
114
|
+
|
|
115
|
+
def test_login_urls_relative_and_cookie_secure_behind_tls_proxy(self) -> None:
|
|
116
|
+
"""Behind a TLS-terminating proxy, login URLs must stay relative and the
|
|
117
|
+
session cookie must still be Secure.
|
|
118
|
+
|
|
119
|
+
When the proxy forwards ``X-Forwarded-Proto: https`` but the ASGI scheme
|
|
120
|
+
is left as ``http`` (e.g. uvicorn without ``--forwarded-allow-ips``),
|
|
121
|
+
building absolute URLs from the request scheme downgrades the login form
|
|
122
|
+
action / redirects to ``http://``. The browser then posts over http, the
|
|
123
|
+
proxy answers 301->https, the POST body is dropped, and login silently
|
|
124
|
+
fails. Relative URLs keep the page's https scheme; the cookie's Secure
|
|
125
|
+
flag falls back to the forwarded-proto header.
|
|
126
|
+
"""
|
|
127
|
+
app = FastAPI()
|
|
128
|
+
stemtrace.init_app(
|
|
129
|
+
app,
|
|
130
|
+
broker_url="memory://",
|
|
131
|
+
embedded_consumer=False,
|
|
132
|
+
serve_ui=False,
|
|
133
|
+
login_username="admin",
|
|
134
|
+
login_password="secret", # NOSONAR - test credential only
|
|
135
|
+
login_secret="test-secret",
|
|
136
|
+
)
|
|
137
|
+
# ALB-style header; the ASGI scheme stays http (proxy headers not applied).
|
|
138
|
+
proxied = {"x-forwarded-proto": "https"}
|
|
139
|
+
|
|
140
|
+
with TestClient(app) as client:
|
|
141
|
+
# The login form action is a relative path, never an absolute URL
|
|
142
|
+
# that a proxy could downgrade to http.
|
|
143
|
+
page = client.get("/stemtrace/login", headers=proxied)
|
|
144
|
+
assert page.status_code == 200
|
|
145
|
+
assert 'action="/stemtrace/login"' in page.text
|
|
146
|
+
assert 'action="http' not in page.text
|
|
147
|
+
|
|
148
|
+
# Successful login: relative redirect + Secure cookie (via forwarded proto).
|
|
149
|
+
resp = client.post(
|
|
150
|
+
"/stemtrace/login",
|
|
151
|
+
data={
|
|
152
|
+
"username": "admin",
|
|
153
|
+
"password": "secret",
|
|
154
|
+
"next": "/stemtrace/",
|
|
155
|
+
}, # NOSONAR - test credential only
|
|
156
|
+
headers=proxied,
|
|
157
|
+
follow_redirects=False,
|
|
158
|
+
)
|
|
159
|
+
assert resp.status_code == 303
|
|
160
|
+
assert resp.headers["location"] == "/stemtrace/"
|
|
161
|
+
assert "Secure" in resp.headers["set-cookie"]
|
|
162
|
+
|
|
163
|
+
# Failed login: relative redirect, not an absolute http:// URL.
|
|
164
|
+
bad = client.post(
|
|
165
|
+
"/stemtrace/login",
|
|
166
|
+
data={
|
|
167
|
+
"username": "admin",
|
|
168
|
+
"password": "nope",
|
|
169
|
+
"next": "/stemtrace/",
|
|
170
|
+
}, # NOSONAR - test credential only
|
|
171
|
+
headers=proxied,
|
|
172
|
+
follow_redirects=False,
|
|
173
|
+
)
|
|
174
|
+
assert bad.headers["location"].startswith("/stemtrace/login?")
|
|
175
|
+
assert not bad.headers["location"].startswith("http")
|
|
176
|
+
|
|
76
177
|
def test_invalid_credentials_redirects_to_login_with_error(self) -> None:
|
|
77
178
|
"""Invalid credentials redirect back to the login page with an error."""
|
|
78
179
|
app = FastAPI()
|
|
@@ -97,9 +198,7 @@ class TestFormLoginProtection:
|
|
|
97
198
|
follow_redirects=False,
|
|
98
199
|
)
|
|
99
200
|
assert resp.status_code == 303
|
|
100
|
-
assert resp.headers["location"].startswith(
|
|
101
|
-
"http://testserver/stemtrace/login?"
|
|
102
|
-
)
|
|
201
|
+
assert resp.headers["location"].startswith("/stemtrace/login?")
|
|
103
202
|
|
|
104
203
|
page = client.get(resp.headers["location"])
|
|
105
204
|
assert page.status_code == 200
|
|
@@ -135,9 +234,8 @@ class TestFormLoginProtection:
|
|
|
135
234
|
|
|
136
235
|
logout = client.post("/stemtrace/logout", follow_redirects=False)
|
|
137
236
|
assert logout.status_code == 303
|
|
138
|
-
assert logout.headers["location"].startswith(
|
|
139
|
-
|
|
140
|
-
)
|
|
237
|
+
assert logout.headers["location"].startswith("/stemtrace/login")
|
|
238
|
+
assert not logout.headers["location"].startswith("http")
|
|
141
239
|
|
|
142
240
|
forbidden = client.get("/stemtrace/api/health", follow_redirects=False)
|
|
143
241
|
assert forbidden.status_code == 401
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx
RENAMED
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx
RENAMED
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx
RENAMED
|
File without changes
|
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx
RENAMED
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx
RENAMED
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx
RENAMED
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx
RENAMED
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx
RENAMED
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts
RENAMED
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stemtrace-0.3.3 → stemtrace-0.3.4}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|