Skip to content

fix: replace worker password with per-task JWT tokens#904

Merged
PythonFZ merged 17 commits intomainfrom
fix/worker-auth-jwt
Apr 2, 2026
Merged

fix: replace worker password with per-task JWT tokens#904
PythonFZ merged 17 commits intomainfrom
fix/worker-auth-jwt

Conversation

@PythonFZ
Copy link
Copy Markdown
Member

@PythonFZ PythonFZ commented Apr 1, 2026

Summary

  • Security fix: The internal worker user (worker@internal.user) was created with a well-known default password (zndraw-worker) that granted full superuser access on any deployment that hadn't changed it. Confirmed exploitable on zndraw.icp.uni-stuttgart.de.
  • Per-task JWT tokens: Instead of sharing a static password between the server and TaskIQ workers, the server now mints a short-lived JWT (1-hour TTL) for each dispatched task. Workers receive the token in the job payload and use ZnDraw(token=...) directly.
  • Login blocked: The internal worker email is rejected with 403 on the public login endpoint. The password is now a random UUID generated at startup — never configured, never shared.

Changes

File Change
config.py Remove worker_password, add internal_worker_email
database.py Auto-gen UUID password, add lookup_worker_user, wire WorkerTokenDep override
executor.py Accept token per-call instead of holding credentials
registry.py Add token to protocol, task fn signature, and closure
dependencies.py Add WorkerTokenDep / get_worker_token
router.py Inject WorkerTokenDep, pass token to kiq()
broker.py Simplify — executor only needs base_url
routes/auth.py Custom login route that blocks worker email (403)
docker/templates/.env Remove ZNDRAW_SERVER_WORKER_PASSWORD

Test plan

  • test_worker_login_blocked — worker email returns 403
  • test_regular_login_still_works — normal users unaffected
  • test_worker_token_dep_mints_valid_jwt — JWT authenticates as superuser worker
  • grep -r worker_password returns zero hits (excluding spec/plan docs)
  • Verify production deployment no longer needs ZNDRAW_SERVER_WORKER_PASSWORD env var

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced per-task JWT authentication for internal task execution.
  • Security

    • Replaced static shared worker password with dynamic per-task JWT tokens; internal worker access is now guarded and cannot be used via public login.
  • Configuration

    • Removed legacy worker-password env var and updated server host env var naming.
    • Internal worker identified by configurable email with automatic credential provisioning.
  • Tests & Docs

    • Added tests and design/plan docs validating token minting and login guards.

PythonFZ and others added 12 commits April 1, 2026 10:42
Replace the shared-password model for the internal worker user with
per-task JWT tokens minted at dispatch time, eliminating the well-known
default credential that grants superuser access on unpatched deployments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rely on fastapi-users' built-in "user already exists" rejection for
registration — a custom guard is YAGNI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8-task plan covering config changes, executor simplification, JWT minting
via WorkerTokenDep, login guard, broker cleanup, Docker/docs sweep, and
integration tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntials

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a custom /jwt/login route that shadows the fastapi-users login
to reject login attempts with the internal worker email (403),
while preserving normal login behavior for all other users.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes ZNDRAW_SERVER_WORKER_PASSWORD from docker/templates/.env and
the corresponding migration reference from the pydantic-settings plan.
Worker authentication now uses JWT tokens, not passwords.

Verification: grep -r 'worker_password', 'WORKER_PASSWORD', 'WORKER_EMAIL'
all return zero hits (excluding worker-auth-jwt docs).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR replaces static worker-password authentication with per-task JWT authentication: removes the worker password env var, makes the internal worker user created/updated with a random password, adds a FastAPI dependency to mint short-lived worker JWTs at dispatch time, and updates executors and dispatch paths to accept and forward per-task tokens.

Changes

Cohort / File(s) Summary
Docker & Env
docker/templates/.env, Dockerfile
Removed ZNDRAW_SERVER_WORKER_PASSWORD entry from Docker templates; renamed ZNDRAW_HOSTZNDRAW_SERVER_HOST in Dockerfile.
Config
src/zndraw/config.py
Replaced worker_password setting with internal_worker_email configuration field.
Database & Init
src/zndraw/database.py
Removed module-level WORKER_EMAIL; ensure_internal_worker() now keyed by email and refreshes hashed password with a random UUID; DB init uses settings.internal_worker_email.
Broker / Executor
src/zndraw/broker.py, src/zndraw/executor.py
Executor no longer stores worker credentials; InternalExtensionExecutor now only has base_url and requires per-call token: str; broker initializes executor without credentials.
Joblib / API
src/zndraw_joblib/dependencies.py, src/zndraw_joblib/registry.py, src/zndraw_joblib/router.py
Added get_worker_token dependency and WorkerTokenDep; updated InternalExecutor protocol and dynamic task callsites to accept/forward token; submit_task route injects and passes worker_token.
Tests & Fixtures
tests/zndraw/test_worker_auth.py, tests/zndraw/conftest.py, tests/zndraw_joblib/conftest.py
Added tests for worker-token behavior and regular login; test fixtures now ensure internal worker exists and stub get_worker_token where needed; added settings fixture.
Docs & Plans
docs/superpowers/...
Removed prior Docker worker-password rename note; added design and implementation plan/spec detailing JWT minting, internal-worker init, executor/interface changes, and cleanup steps.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Router as Task Router
    participant TokenDep as WorkerTokenDep
    participant DB as Database
    participant Queue as TaskIQ Queue
    participant Executor as Internal Executor

    Client->>Router: POST /submit_task (job)
    Router->>TokenDep: get_worker_token()
    TokenDep->>DB: lookup_worker_user(internal_worker_email)
    DB-->>TokenDep: user
    TokenDep->>TokenDep: mint short-lived JWT
    TokenDep-->>Router: token
    Router->>Queue: kiq(task_payload, token=token)
    Queue-->>Executor: invoke task with token
    Executor->>Executor: ZnDraw(base_url, token=token)
    Executor-->>Client: execute extension (authenticated)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hop to mint a token bright,

No more passwords in the night,
Per-task JWTs, short and clean,
Worker auth now nimble, lean,
Hooray — the queue hums light and right!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary security-focused change: replacing a static worker password with per-task JWT tokens, which aligns with all major code changes across configuration, authentication, execution layers, and dependencies.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/worker-auth-jwt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/zndraw/database.py (1)

377-386: Consider adding error handling for the session acquisition.

If lookup_worker_user raises RuntimeError (db not initialized), the error will propagate to the request handler. This is reasonable behavior, but you may want to ensure the error message is user-friendly in the HTTP response.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zndraw/database.py` around lines 377 - 386, The _mint_worker_token
coroutine should catch runtime errors from session acquisition or
lookup_worker_user and convert them into a user-friendly HTTP error; wrap the
session/lookup call in a try/except for RuntimeError (and optionally Exception),
log the original exception, and raise a fastapi.HTTPException with a clear
message (e.g., "internal worker not available" or "database not initialized") so
callers get a friendly response; keep the rest of the flow (JWTStrategy and
strategy.write_token) unchanged and reference _mint_worker_token,
lookup_worker_user, app.state.session_maker, and strategy.write_token when
implementing the try/except and rethrowing the HTTPException.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/plans/2026-04-01-worker-auth-jwt.md`:
- Around line 664-666: Add explicit language identifiers to the unlabeled fenced
code blocks containing the environment variable and the inline diff: change the
block with "ZNDRAW_SERVER_WORKER_PASSWORD=zndraw-worker" to use ```env and
change the block showing the `ZNDRAW_WORKER_PASSWORD` →
`ZNDRAW_SERVER_WORKER_PASSWORD` line to use ```text (or ```diff if you prefer
highlighting), ensuring the fenced backticks around those snippets include the
language identifier so markdownlint MD040 warnings are resolved; locate the
blocks by searching for the strings
"ZNDRAW_SERVER_WORKER_PASSWORD=zndraw-worker" and "ZNDRAW_WORKER_PASSWORD" to
update them.

In `@docs/superpowers/specs/2026-04-01-worker-auth-jwt-design.md`:
- Around line 43-44: The spec contains contradictory statements about
registration (one place says “no custom guard needed” in the Register section
while other places state “register should return 403 / register guards added”);
pick one canonical behavior (either rely on fastapi-users to reject duplicates
with no custom guard, or explicitly require/register custom guard that returns
403) and make all references consistent: update the Register section text, the
later statements that mention “register should return 403 / register guards
added”, any examples, and any expected HTTP status lists to reflect the chosen
behavior; ensure any references to fastapi-users behavior (e.g., duplicate-user
handling) are accurate and that the spec clearly documents the expected HTTP
status codes and guard presence/absence for functions/methods named in the spec
(Register, register guard entries, and any register-related examples).

In `@tests/zndraw/test_worker_auth.py`:
- Around line 83-101: The test mutates app.dependency_overrides by setting
app.dependency_overrides[get_worker_token] = _mint_worker_token and never
restores it, leaking a closure that captures session; wrap the override in a
try/finally (or use a context manager) to save the original override =
app.dependency_overrides.get(get_worker_token) before you set it, run the test
logic (including calling override() and the /v1/auth/users/me request), and in
finally restore app.dependency_overrides[get_worker_token] = original (or delete
the key if original is None) so _mint_worker_token/session aren’t kept across
tests.

---

Nitpick comments:
In `@src/zndraw/database.py`:
- Around line 377-386: The _mint_worker_token coroutine should catch runtime
errors from session acquisition or lookup_worker_user and convert them into a
user-friendly HTTP error; wrap the session/lookup call in a try/except for
RuntimeError (and optionally Exception), log the original exception, and raise a
fastapi.HTTPException with a clear message (e.g., "internal worker not
available" or "database not initialized") so callers get a friendly response;
keep the rest of the flow (JWTStrategy and strategy.write_token) unchanged and
reference _mint_worker_token, lookup_worker_user, app.state.session_maker, and
strategy.write_token when implementing the try/except and rethrowing the
HTTPException.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5cc38110-943f-4139-92fd-5f6e2d7efe52

📥 Commits

Reviewing files that changed from the base of the PR and between 42b7c36 and a2b7304.

📒 Files selected for processing (13)
  • docker/templates/.env
  • docs/superpowers/plans/2026-03-25-pydantic-settings-phase1.md
  • docs/superpowers/plans/2026-04-01-worker-auth-jwt.md
  • docs/superpowers/specs/2026-04-01-worker-auth-jwt-design.md
  • src/zndraw/broker.py
  • src/zndraw/config.py
  • src/zndraw/database.py
  • src/zndraw/executor.py
  • src/zndraw/routes/auth.py
  • src/zndraw_joblib/dependencies.py
  • src/zndraw_joblib/registry.py
  • src/zndraw_joblib/router.py
  • tests/zndraw/test_worker_auth.py
💤 Files with no reviewable changes (2)
  • docker/templates/.env
  • docs/superpowers/plans/2026-03-25-pydantic-settings-phase1.md

Comment on lines +664 to +666
```
ZNDRAW_SERVER_WORKER_PASSWORD=zndraw-worker
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks (MD040).

Line 664 and Line 672 use unlabeled fenced blocks, which triggers markdownlint warnings.

Suggested fix
-```
+```env
 ZNDRAW_SERVER_WORKER_PASSWORD=zndraw-worker

@@
- +text

  • ZNDRAW_WORKER_PASSWORDZNDRAW_SERVER_WORKER_PASSWORD

Also applies to: 672-674

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 664-664: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-01-worker-auth-jwt.md` around lines 664 - 666,
Add explicit language identifiers to the unlabeled fenced code blocks containing
the environment variable and the inline diff: change the block with
"ZNDRAW_SERVER_WORKER_PASSWORD=zndraw-worker" to use ```env and change the block
showing the `ZNDRAW_WORKER_PASSWORD` → `ZNDRAW_SERVER_WORKER_PASSWORD` line to
use ```text (or ```diff if you prefer highlighting), ensuring the fenced
backticks around those snippets include the language identifier so markdownlint
MD040 warnings are resolved; locate the blocks by searching for the strings
"ZNDRAW_SERVER_WORKER_PASSWORD=zndraw-worker" and "ZNDRAW_WORKER_PASSWORD" to
update them.

_mint_worker_token now accepts SessionDep so FastAPI reuses the
request-scoped session instead of opening a second one (which
re-entered the non-reentrant SQLite asyncio.Lock and deadlocked
every @internal task submission). JWTStrategy moved to closure scope.

Test conftest now wires get_worker_token override (mirrors lifespan),
and the test verifies the conftest-wired override — not a hand-rolled
one. Also fixes FAST002 lint (Annotated), removes dead TYPE_CHECKING
block, corrects stale error message, and updates spec wording.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/zndraw/routes/auth.py (1)

78-81: Note: Redundant fastapi-users login route is still included.

The get_auth_router(auth_backend) creates a /login endpoint that will never be reached since the custom /jwt/login route shadows it. Consider whether to:

  1. Keep it as-is (harmless, provides other auth endpoints like /logout)
  2. Document that the login route is intentionally shadowed

This is a minor observation - the current approach works correctly and preserves other auth router endpoints.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zndraw/routes/auth.py` around lines 78 - 81, The included fastapi-users
auth router via
router.include_router(fastapi_users.get_auth_router(auth_backend),
prefix="/jwt") exposes a /jwt/login endpoint that duplicates and is shadowed by
your custom /jwt/login, so either remove or document the redundancy; to fix,
either remove the get_auth_router(...) include if you only want your custom
login and re-add only needed endpoints from fastapi-users (e.g., mount specific
routers or use get_register_router/get_reset_password_router), or keep the
include and add a code comment in auth.py explaining that
get_auth_router(auth_backend) is intentionally included to preserve other
endpoints (like /jwt/logout) while the custom /jwt/login shadows the built-in
login.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/specs/2026-04-01-worker-auth-jwt-design.md`:
- Around line 43-45: The register behavior in auth.py currently mounts
fastapi_users.get_register_router() without the spec-required guard; implement a
custom POST /v1/auth/register handler that checks the internal worker email
constant (e.g., WORKER_EMAIL or the existing internal worker email value used
elsewhere) and returns HTTP 403 when the incoming registration email equals that
value, otherwise delegate to the fastapi-users registration flow (or call the
underlying create user logic); replace or shadow the existing
fastapi_users.get_register_router() registration route so the 403 check runs
first and ensure the new handler uses the same input schema and response codes
as fastapi-users.

---

Nitpick comments:
In `@src/zndraw/routes/auth.py`:
- Around line 78-81: The included fastapi-users auth router via
router.include_router(fastapi_users.get_auth_router(auth_backend),
prefix="/jwt") exposes a /jwt/login endpoint that duplicates and is shadowed by
your custom /jwt/login, so either remove or document the redundancy; to fix,
either remove the get_auth_router(...) include if you only want your custom
login and re-add only needed endpoints from fastapi-users (e.g., mount specific
routers or use get_register_router/get_reset_password_router), or keep the
include and add a code comment in auth.py explaining that
get_auth_router(auth_backend) is intentionally included to preserve other
endpoints (like /jwt/logout) while the custom /jwt/login shadows the built-in
login.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b60ba8e7-3ca2-4729-9e56-b6ffafb7bdd8

📥 Commits

Reviewing files that changed from the base of the PR and between a2b7304 and 8411da7.

📒 Files selected for processing (7)
  • Dockerfile
  • docs/superpowers/specs/2026-04-01-worker-auth-jwt-design.md
  • src/zndraw/database.py
  • src/zndraw/routes/auth.py
  • src/zndraw_joblib/dependencies.py
  • tests/zndraw/conftest.py
  • tests/zndraw/test_worker_auth.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/zndraw_joblib/dependencies.py
  • src/zndraw/database.py
  • tests/zndraw/test_worker_auth.py

Comment on lines +43 to +45
- **Register** (`POST /v1/auth/register`): reject with 403 if email matches.
Registration would already fail with "user exists", but an explicit check
avoids leaking that the email is taken and makes the intent clear.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for any register guard implementation in the auth routes
rg -n "register" src/zndraw/routes/auth.py

# Check if there's a custom /register route that shadows the fastapi-users one
ast-grep --pattern 'router.post("/register")'

# Search for any 403 handling related to registration
rg -n -C3 "register.*403|403.*register" src/

Repository: zincware/ZnDraw

Length of output: 122


Spec describes register guard, but implementation does not include it.

Lines 43–45 state the register endpoint should "reject with 403 if email matches" the internal worker email, and lines 152, 162, and 177 reference this behavior. However, auth.py line 83 includes fastapi_users.get_register_router() without a custom shadowing route that implements the 403 check.

The current implementation allows registration attempts with the internal worker email to fail with "user exists" rather than returning a clean 403, leaking that the email is taken.

Either implement the register guard in auth.py or update the spec to reflect that this validation is not included.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-01-worker-auth-jwt-design.md` around lines 43
- 45, The register behavior in auth.py currently mounts
fastapi_users.get_register_router() without the spec-required guard; implement a
custom POST /v1/auth/register handler that checks the internal worker email
constant (e.g., WORKER_EMAIL or the existing internal worker email value used
elsewhere) and returns HTTP 403 when the incoming registration email equals that
value, otherwise delegate to the fastapi-users registration flow (or call the
underlying create user logic); replace or shadow the existing
fastapi_users.get_register_router() registration route so the 403 check runs
first and ensure the new handler uses the same input schema and response codes
as fastapi-users.

PythonFZ and others added 2 commits April 1, 2026 12:39
submit_task always resolves WorkerTokenDep, even for @global jobs.
The joblib test conftest was missing the override, causing 18 test
failures with NotImplementedError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make get_worker_token a proper FastAPI dependency that reads from
request.app.state instead of a stub requiring override in lifespan
and test conftest. Remove the YAGNI custom /jwt/login route — the
random UUID password is sufficient protection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/specs/2026-04-01-worker-auth-jwt-design.md`:
- Around line 25-28: The spec example and implementation diverge: the spec uses
SecretStr for the generated password (password = SecretStr(str(uuid.uuid4())))
but database.py's ensure_internal_worker implementation calls
password_helper.hash(str(uuid.uuid4())) directly; update the spec example to
match the implementation by removing SecretStr (or alternatively update the
implementation to wrap the generated UUID in SecretStr before hashing if you
prefer that API), and reference the ensure_internal_worker routine and the
password_helper.hash call so the example and actual behavior are consistent.

In `@src/zndraw/database.py`:
- Around line 137-162: The helper lookup_worker_user currently duplicates the
select logic used in get_worker_token; refactor get_worker_token to call
lookup_worker_user(session, settings.internal_worker_email) instead of
re-running the select, removing the duplicated query block; ensure
get_worker_token imports lookup_worker_user, handle/translate the RuntimeError
raised by lookup_worker_user into the same behavior get_worker_token previously
had (e.g., raise HTTPException or return None as appropriate), and delete
lookup_worker_user only if you confirm it's not part of the public API or used
elsewhere.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 35c48c00-6abb-41fc-995f-e32bf538e65f

📥 Commits

Reviewing files that changed from the base of the PR and between 319bb13 and f5d7e89.

📒 Files selected for processing (5)
  • docs/superpowers/specs/2026-04-01-worker-auth-jwt-design.md
  • src/zndraw/database.py
  • src/zndraw_joblib/dependencies.py
  • tests/zndraw/conftest.py
  • tests/zndraw/test_worker_auth.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/zndraw/test_worker_auth.py
  • src/zndraw_joblib/dependencies.py
  • tests/zndraw/conftest.py

Comment on lines +25 to +28
```python
# database.py — ensure_internal_worker
password = SecretStr(str(uuid.uuid4()))
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor spec/code mismatch in the code example.

The spec shows password = SecretStr(str(uuid.uuid4())), but the actual implementation in database.py directly hashes the UUID string without wrapping in SecretStr:

# Actual code (database.py line 118)
hashed = password_helper.hash(str(uuid.uuid4()))

Consider updating the example to match the implementation:

 # database.py — ensure_internal_worker
-password = SecretStr(str(uuid.uuid4()))
+hashed = password_helper.hash(str(uuid.uuid4()))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-01-worker-auth-jwt-design.md` around lines 25
- 28, The spec example and implementation diverge: the spec uses SecretStr for
the generated password (password = SecretStr(str(uuid.uuid4()))) but
database.py's ensure_internal_worker implementation calls
password_helper.hash(str(uuid.uuid4())) directly; update the spec example to
match the implementation by removing SecretStr (or alternatively update the
implementation to wrap the generated UUID in SecretStr before hashing if you
prefer that API), and reference the ensure_internal_worker routine and the
password_helper.hash call so the example and actual behavior are consistent.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 92.72727% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.78%. Comparing base (42b7c36) to head (f5df7e6).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/zndraw/database.py 57.14% 3 Missing ⚠️
src/zndraw_joblib/dependencies.py 92.85% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #904      +/-   ##
==========================================
+ Coverage   91.71%   91.78%   +0.07%     
==========================================
  Files         245      246       +1     
  Lines       22839    22881      +42     
==========================================
+ Hits        20946    21001      +55     
+ Misses       1893     1880      -13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Query logic now lives solely in get_worker_token (zndraw_joblib).
Calling back into zndraw from zndraw_joblib would create a circular
import, so the helper had no callers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/zndraw/database.py`:
- Around line 120-135: When updating an existing internal worker (variable
existing of class User) ensure you set all the same flags as in the create path:
set existing.hashed_password = hashed, existing.is_active = True,
existing.is_verified = True, and existing.is_superuser = True before calling
await session.commit(); update the log message calls (log.debug/log.info) remain
the same but ensure the update branch mirrors the create branch's flags so a
previously disabled or unverified user is repaired on startup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d314e32e-9edd-4edd-9110-e9db9033da12

📥 Commits

Reviewing files that changed from the base of the PR and between f5d7e89 and ba362e1.

📒 Files selected for processing (1)
  • src/zndraw/database.py

Comment on lines +97 to 125
email: str,
) -> None:
"""Create or update the internal worker superuser.

Idempotent — safe to call on every startup.
Idempotent — safe to call on every startup. The password is a random
UUID generated each time — it is never used for login.

Parameters
----------
session
Async database session.
password
Worker password from ``Settings.worker_password``.
email
Internal worker email from ``Settings.internal_worker_email``.
"""
password_helper = PasswordHelper()

result = await session.execute(
select(User).where(User.email == WORKER_EMAIL) # type: ignore[arg-type]
select(User).where(User.email == email) # type: ignore[arg-type]
)
existing = result.scalar_one_or_none()

hashed = password_helper.hash(password.get_secret_value())
hashed = password_helper.hash(str(uuid.uuid4()))

if existing is None:
worker = User(
email=WORKER_EMAIL,
email=email,
hashed_password=hashed,
is_active=True,
is_superuser=True,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't identify the internal worker by arbitrary email match.

ensure_internal_worker() now trusts settings.internal_worker_email as the worker's identity. If that setting changes during an upgrade, the legacy worker@internal.user row is never rotated or disabled, which undermines the shipped-credential remediation. If it collides with a real user, startup will reset that user's password and force is_superuser=True. This needs a dedicated internal-worker marker or a fixed non-overridable identity, not an email-only rewrite.

Also applies to: 132-135, 176-176

Set is_active and is_verified in the update branch so a previously
disabled or unverified worker user is repaired on startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@PythonFZ PythonFZ merged commit 06cab0d into main Apr 2, 2026
6 checks passed
@PythonFZ PythonFZ deleted the fix/worker-auth-jwt branch April 2, 2026 11:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants