Skip to content

[upstream-queue] feat: optional/anonymous auth mode (AUTH_MODE) (PR-2/8)#5

Open
damienriehl wants to merge 4 commits into
devfrom
upstream-queue/optional-auth
Open

[upstream-queue] feat: optional/anonymous auth mode (AUTH_MODE) (PR-2/8)#5
damienriehl wants to merge 4 commits into
devfrom
upstream-queue/optional-auth

Conversation

@damienriehl

@damienriehl damienriehl commented Jul 7, 2026

Copy link
Copy Markdown

PR-2 of 8 — LLM node-expansion drain: "optional/anonymous auth mode" (api)

Queue policy (TODAY-2026-07-07): staged INSIDE the alea fork, base dev, UNMERGED (dev/main stay pure upstream mirrors). Retarget to CatholicOS decided post-today. Tracked in ontokit-web/docs/UPSTREAM-TRACKING.md (A2.2). Paired with web PR-2 (alea-institute/ontokit-web#5) — the frontend AUTH_MODE gating.

What this is

The backend half of AUTH_MODE=required|optional|disabled — lets the API serve anonymous browse without a Zitadel token. Cherry-picked from the llm-helper lineage (phase 08-optional-auth, commits 0bd679b, f98f3e0) onto fresh origin/dev.

  • ontokit/core/config.py — adds auth_mode: str = "required" to Settings (default = unchanged behavior).
  • ontokit/core/auth.py — adds ANONYMOUS_USER constant (id="anonymous", roles=["viewer"] — least privilege) and an early-return guard for disabled mode in all three auth dependencies: get_current_user, get_current_user_optional, get_current_user_with_token.
  • tests/unit/test_auth_disabled.py (new, 11 tests) — covers ANONYMOUS_USER properties + all three modes across all three dependency functions.

Mode semantics:

  • required (default): unchanged — token enforced, 401 without it.
  • optional: unchanged — OptionalUser returns None for no credentials (browse works), RequiredUser still 401s (write protection). No code path change needed.
  • disabled: the three dependencies return ANONYMOUS_USER (viewer-only) instead of enforcing a token.

Purely additive (+150 lines, 0 deletions); backward compatible.

Verification (all green)

  • pytest tests/unit/test_auth_disabled.py11 passed
  • pytest full auth suite (test_auth_core, test_auth, test_auth_disabled) — 42 passed (confirms no regression in required/optional paths my change touches)

Note for review

auth_mode is a free str (not an Enum/Literal). An unrecognized value (e.g. a typo) falls through to enforcing auth (fail-closed / safe default), but a Literal["required","optional","disabled"] would surface config typos at load time — flagged for reviewer preference before upstream merge.

Merge order: 0 → 1 → 2 → 3 → 4 → 5 → 6 → (7). Pairs with web PR-2. Backend endpoints for PR-3+ (/llm/*) are not part of this slice.

damienriehl and others added 3 commits July 7, 2026 08:18
…odes

- Add auth_mode field (required/optional/disabled) to Settings class in config.py
- Add ANONYMOUS_USER constant to auth.py (id=anonymous, roles=[viewer])
- Add early returns in get_current_user, get_current_user_optional, get_current_user_with_token for disabled mode
- Optional mode: RequiredUser still 401s (write protection), OptionalUser returns None (browse works)
…disabled)

- Test ANONYMOUS_USER properties (id, roles, type, not superadmin)
- Test disabled mode: all three auth functions return ANONYMOUS_USER
- Test required mode: get_current_user raises 401, get_current_user_optional returns None
- Test optional mode: RequiredUser raises 401 (write protection), OptionalUser returns None (browse works)
/ce:review (PR-2) follow-ups:
- config.py: auth_mode str -> Literal[required|optional|disabled] so
  pydantic-settings rejects config typos at startup (fail-fast) instead of
  silently falling through to required behavior.
- test_auth_disabled.py: add test_disabled_ignores_valid_credentials proving
  disabled mode ignores even a valid Bearer token (everyone anonymous viewer).

WS-auth parity (authenticate_ws does not consult auth_mode) is intentionally
NOT changed here — it is new work beyond the extracted phase-08 slice; noted
on the PR for a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
@damienriehl

Copy link
Copy Markdown
Author

/ce:review outcome (kieran-python-reviewer) — findings + resolutions

Reviewer confirmed the disabled-mode early-return placement is correct, consistent, and complete across all three auth dependencies, with no privilege escalation (ANONYMOUS_USER is viewer-only, not in superadmin_ids) and no change to required/optional behavior. No BLOCKER/HIGH.

Sev Finding Resolution
MED auth_mode was a bare str — a typo silently falls through to required behavior Fixed (c4a4a36) — now Literal["required","optional","disabled"]; pydantic-settings rejects bad values at startup
MED WebSocket auth (ws_auth.authenticate_ws) never consults auth_mode — in disabled mode REST goes anonymous but WS collab still hard-requires a token Deferred + noted — this is new work beyond the extracted phase-08 slice (the upstream feature didn't touch WS auth); real-time collab requiring identity is defensible. Flagging for a follow-up PR rather than silently widening this slice. Direction is the more restrictive one (no security hole).
MED TestAuthModeOptional/TestAuthModeRequired don't actually branch on the mode value (no "optional" vs "required" conditional exists) → false coverage impression Kept as intent-documentation; the genuinely new behavior is covered by the disabled tests + the new credential-ignoring test below
LOW No test for "disabled ignores a valid token" Fixed (c4a4a36) — test_disabled_ignores_valid_credentials
NIT "anonymous" magic-string token sentinel Kept — access_token is currently dead downstream (# noqa: ARG002); noted for when/if it's wired up

Auth suite 43 passed (was 42); ruff + mypy clean.

authenticate_ws hard-required a token regardless of settings.auth_mode,
so a disabled/optional deployment admitted anonymous callers on its HTTP
API but rejected them at the WebSocket handshake. Resolve caller identity
per auth_mode mirroring core.auth:

- disabled  -> ANONYMOUS_USER, no token required
- optional  -> absent/invalid token downgrades to anonymous (get_current_user_optional)
- required  -> valid token mandatory (unchanged)

Project-access check still gates private projects for anonymous callers.
+7 parity tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eQ2CoAwpGv9Q8vBhuGGtH
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.

1 participant