Skip to content

feat(authenticator): restore the __override view-as login (#1941) - #1944

Merged
cyberantonz merged 3 commits into
constructorfabric:mainfrom
cyberantonz:feat/override-login-1941
Jul 27, 2026
Merged

feat(authenticator): restore the __override view-as login (#1941)#1944
cyberantonz merged 3 commits into
constructorfabric:mainfrom
cyberantonz:feat/override-login-1941

Conversation

@cyberantonz

@cyberantonz cyberantonz commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Restores the operator "view the dashboard as another user" facility (?__override=<email>) that the gateway authz hardening (#1769) removed as collateral — re-implemented at the only seam compatible with the gateway-authored identity model: inside the authenticator, at session mint.

  • GET /auth/login?__override=<email> stores the target with the transient login state — only when the new override_enabled config flag is on (default false at every layer; Helm: authenticator.overrideEnabled). Flag off → the parameter is inert and logged.
  • At /auth/callback, after full IdP authentication and resolution of the real person, the target email is resolved through the same Identity lookup and the session + linked gateway JWT are minted for the target person_id/email.
  • The session record keeps the real principal (impersonator_person_id/email) and the real idp_sub/idp_sid/refresh token — audit attribution, back-channel logout, and the background refresher keep targeting the real IdP grant.
  • View-as sessions are also indexed under the impersonator, so admin revoke-by-person and self "log out everywhere" reach them.
  • /auth/me exposes impersonator_email for the SPA "viewing as" banner (FE follow-up in insight-front).
  • Unknown target → 403, audited to the durable sink (no silent fallback to the caller's identity). The __override value is sanitized (control chars stripped, length capped) before the one log path reachable in flag-off environments.

No #1769 regression: no client-supplied header/param is ever trusted as identity; every decision input is server-side (flag, login-state value, person store), and the caller still fully authenticates at the IdP.

Accepted edge (documented in DD-AUTH-09): Identity's internal lookup is email-only (no tenant memberships until #1687), so a target from another tenant resolves and is paired with the caller's tenant claim — acceptable while the flag marks whole single-tenant dev/demo environments.

Config

authenticator:
  overrideEnabled: true   # dev/demo ONLY; default false

Testing

  • 49 unit tests, clippy clean, pre-commit green.
  • Full run-e2e.sh suite passes, including new e2e_override: identity swap (JWT sub + /auth/me), unknown-target 403, flag-off inertia (second authenticator instance at default config), impersonator revoke-all reaching the view-as session.
  • Chart rendering verified: flag unset → no env var emitted; set → APP__gears__authenticator__config__override_enabled: "true".
  • Specs: PRD §5.16 (cpt-insightspec-fr-auth-override) + DESIGN DD-AUTH-09; cfs validate + check-language pass on both artifacts.

Closes #1941

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an optional view-as login override using the __override=<email> parameter.
    • When enabled, sessions can represent another person while retaining the authenticated principal as the impersonator.
    • Added impersonator details to /auth/me responses.
    • Allowed impersonators to revoke view-as sessions.
  • Security

    • Override behavior is disabled by default and intended only for development or demonstrations.
    • Unknown override targets are denied without creating a session.
  • Tests

    • Added end-to-end coverage for enabled, disabled, invalid, switching, and session-revocation scenarios.

@cyberantonz
cyberantonz requested a review from a team as a code owner July 27, 2026 10:09
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The authenticator now supports a disabled-by-default __override=<email> login flow that resolves a target person after IdP authentication, mints sessions for that target, records the authenticated principal as impersonator, and validates session revocation and disabled behavior end to end.

Changes

View-As Override

Layer / File(s) Summary
Configuration and documented contracts
charts/insight/..., docs/components/backend/authenticator/..., src/backend/services/authenticator/src/config.rs
Adds the disabled-by-default override configuration, Helm wiring, session schemas, API behavior, acceptance criteria, and design documentation.
Login and session persistence
src/backend/services/authenticator/src/session.rs
Persists override login state and impersonator session metadata, including secondary session indexing and revocation cleanup.
Callback identity and session behavior
src/backend/services/authenticator/src/api/handlers.rs
Sanitizes and stores override input, resolves target identities after callback authentication, mints effective-identity sessions and JWTs, exposes impersonator details, and expands revocation ownership checks.
End-to-end validation and test orchestration
src/backend/services/authenticator/tests/e2e_override.rs, src/backend/services/authenticator/tests/identity-stub.py, src/backend/services/authenticator/tests/run-e2e.sh
Adds coverage for target sessions, relogin and fixation handling, unknown targets, impersonator revocation, disabled behavior, and execution against two authenticator instances.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant AuthLogin
  participant AuthCallback
  participant IdentityService
  participant SessionStore
  Browser->>AuthLogin: Request login with __override=email
  AuthLogin->>SessionStore: Store override_email in login state
  Browser->>AuthCallback: Complete authenticated OIDC callback
  AuthCallback->>IdentityService: Resolve caller and override target
  AuthCallback->>SessionStore: Store target session with impersonator metadata
  Browser->>AuthCallback: Request /auth/me
  AuthCallback-->>Browser: Return target identity and impersonator_email
Loading

Possibly related PRs

Suggested reviewers: aleksdotbar, ktursunov, mitasovr

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: restoring the __override view-as login flow.
Linked Issues check ✅ Passed The PR restores ?__override= server-side, gates it behind config, preserves the real principal, and adds auditing/impersonation handling.
Out of Scope Changes check ✅ Passed The changes stay focused on the authenticator override feature, its docs, config, and tests with no obvious unrelated additions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

cyberantonz added a commit to constructorfabric/insight-front that referenced this pull request Jul 27, 2026
Companion to the authenticator change (constructorfabric/insight#1944)
restoring the operator 'view the dashboard as another user' facility:

- The historical URL keeps working: '?__override=<email>' on any page is
  consumed at boot and bounced into /auth/login?__override=...&return_to=
  (the parameter itself never enters the router). Works while already
  logged in — the authenticator revokes the presented session at the
  callback and the IdP hop is silent SSO.
- /auth/me's 'impersonator_email' (present only on view-as sessions) is
  parsed into the session store.
- A 'Viewing as X' banner renders above the content on view-as sessions,
  naming the real principal, with an exit button that just re-logs-in as
  yourself.

The override is honored server-side only where the authenticator's
'override_enabled' flag is on (dev/demo stands); everywhere else the
parameter is inert and this UI never shows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anton Zelenov <antonz@constructor.tech>
cyberantonz and others added 2 commits July 27, 2026 18:43
…rfabric#1941)

Restore the operator 'view the dashboard as another user' facility that
the gateway authz hardening (constructorfabric#1769) removed as collateral, at the only
seam compatible with gateway-authored identity: /auth/login?__override=
<email> is stored with the transient login state and applied at
/auth/callback -- after full IdP authentication and person resolution --
by resolving the target through the same Identity lookup and minting the
session + linked JWT for the target person.

Gated by a single override_enabled config flag (authenticator.
overrideEnabled Helm value), default false at every layer; dev/demo
environments only. With the flag off the parameter is inert and logged.

The session record keeps the real principal (impersonator_person_id/
email) and the real idp_sub/sid/refresh token, so audit attribution,
back-channel logout, and the background refresher keep targeting the
real IdP grant; the session is additionally indexed under the
impersonator so revoke-by-person and self 'log out everywhere' reach
it. /auth/me exposes impersonator_email for the SPA 'viewing as'
banner. Unknown targets are denied 403 and audited to the durable
sink; the override value is sanitized before logging.

Covered by e2e_override (swap + JWT sub, unknown-target 403,
flag-off inertia, impersonator revoke-all) against a second flag-off
authenticator instance in run-e2e.sh; PRD 5.16 + DESIGN DD-AUTH-09
document the contract and the accepted email-only-lookup tenant edge.

Closes constructorfabric#1941

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anton Zelenov <antonz@constructor.tech>
Two e2e additions for the token-caching concern (review follow-up):

- override A -> logout -> override B: the old cookie is refused by both
  /auth/me and /internal/authz after logout (nothing for the gateway
  exchange cache to re-serve or re-cache -- the nginx cache is keyed by
  the cookie value, and each login mints a fresh CSPRNG cookie, session
  id, and linked JWT), and the second session carries the NEW target's
  sub, matching /auth/me.
- override A -> override B WITHOUT logout (browser-style, cookie
  presented on the callback): the session-fixation guard revokes the
  presented session before minting the new one -- the old credential
  dies and the new session is the new target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anton Zelenov <antonz@constructor.tech>
@cyberantonz
cyberantonz force-pushed the feat/override-login-1941 branch from 457393b to 830101f Compare July 27, 2026 10:43
@cyberantonz
cyberantonz enabled auto-merge (squash) July 27, 2026 11:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/services/authenticator/tests/run-e2e.sh (1)

1-1: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cross-test race: revoke-all in e2e_override.rs vs. unpinned test parallelism in run-e2e.sh. The four tests in e2e_override.rs share one real-principal identity (user), and one of them calls DELETE /auth/sessions (revoke-all) for that identity; cargo's default parallel test execution means this can kill a sibling test's in-flight session mid-run.

  • src/backend/services/authenticator/tests/run-e2e.sh#L146-150: add --test-threads=1 to the cargo test -p authenticator --test e2e_override invocation so the suite runs serially.
  • src/backend/services/authenticator/tests/e2e_override.rs#L149-174: no code change needed once serialized; alternatively, use a dedicated per-test principal email to remove the shared-identity coupling entirely.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/services/authenticator/tests/run-e2e.sh` at line 1, The
e2e_override tests share a principal while revoke-all can invalidate parallel
tests; update the cargo test invocation in run-e2e.sh to pass --test-threads=1
for the e2e_override suite, leaving e2e_override.rs unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/components/backend/authenticator/DESIGN.md`:
- Line 786: Update the Redis HASH field list in the LoginState documentation to
use `return_to` instead of `redirect_to`, matching the field serialized by
`LoginState::to_fields()` and parsed by `LoginState::from_map()`; leave the
other fields unchanged.

In `@src/backend/services/authenticator/tests/e2e_override.rs`:
- Around line 149-174: Prevent the revoke-all flow from affecting concurrent
tests by using a test-specific identity instead of the shared default user in
the scenario around login_flow and DELETE /auth/sessions. Ensure the
impersonator and its view-as session still belong to that isolated identity so
the existing 401 assertion remains valid.

In `@src/backend/services/authenticator/tests/run-e2e.sh`:
- Around line 146-150: Update the e2e_override cargo test invocation in the run
script to pass --test-threads=1, ensuring tests sharing the user identity
execute serially while preserving the existing ignored and nocapture options.

---

Outside diff comments:
In `@src/backend/services/authenticator/tests/run-e2e.sh`:
- Line 1: The e2e_override tests share a principal while revoke-all can
invalidate parallel tests; update the cargo test invocation in run-e2e.sh to
pass --test-threads=1 for the e2e_override suite, leaving e2e_override.rs
unchanged.
🪄 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 Plus

Run ID: c915b18c-5d80-4fa8-8713-7a5000a9d7f1

📥 Commits

Reviewing files that changed from the base of the PR and between b0df73e and acd3d48.

📒 Files selected for processing (10)
  • charts/insight/templates/secrets.yaml
  • charts/insight/values.yaml
  • docs/components/backend/authenticator/DESIGN.md
  • docs/components/backend/authenticator/PRD.md
  • src/backend/services/authenticator/src/api/handlers.rs
  • src/backend/services/authenticator/src/config.rs
  • src/backend/services/authenticator/src/session.rs
  • src/backend/services/authenticator/tests/e2e_override.rs
  • src/backend/services/authenticator/tests/identity-stub.py
  • src/backend/services/authenticator/tests/run-e2e.sh

#### Key: `asm:login_state:{state}`

**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `redirect_to`. **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).
**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `redirect_to`, `override_email` (view-as target, DD-AUTH-09; empty on normal logins). **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Field name mismatch: redirect_to vs. actual return_to.

LoginState in session.rs names the field return_to (used in to_fields()/from_map()), not redirect_to.

📝 Proposed fix
-**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `redirect_to`, `override_email` (view-as target, DD-AUTH-09; empty on normal logins). **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).
+**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `return_to`, `override_email` (view-as target, DD-AUTH-09; empty on normal logins). **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `redirect_to`, `override_email` (view-as target, DD-AUTH-09; empty on normal logins). **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).
**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `return_to`, `override_email` (view-as target, DD-AUTH-09; empty on normal logins). **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/components/backend/authenticator/DESIGN.md` at line 786, Update the
Redis HASH field list in the LoginState documentation to use `return_to` instead
of `redirect_to`, matching the field serialized by `LoginState::to_fields()` and
parsed by `LoginState::from_map()`; leave the other fields unchanged.

Comment on lines +149 to +174
// The view-as session is reachable through the REAL principal: it is
// indexed under both persons, so the impersonator's own "log out
// everywhere" must kill it.
let cb = login_flow(&http, &auth_base, &user, None).await;
assert_eq!(cb.status(), 302);
let own_token = cookie_from(&cb).expect("normal login must set the cookie");
let csrf = csrf_token(&http, &auth_base, &own_token).await;
let all = http
.delete(format!("{auth_base}/auth/sessions"))
.header(reqwest::header::COOKIE, format!("{COOKIE}={own_token}"))
.header("X-CSRF-Token", &csrf)
.send()
.await
.unwrap();
assert_eq!(all.status(), 200, "revoke-all as the impersonator");
let dead = http
.get(format!("{auth_base}/auth/me"))
.header(reqwest::header::COOKIE, format!("{COOKIE}={token}"))
.send()
.await
.unwrap();
assert_eq!(
dead.status(),
401,
"the impersonator's revoke-all must reach their view-as session"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Revoke-all test races with sibling tests sharing the same identity.

This test authenticates user a third time and calls DELETE /auth/sessions, which revokes ALL of user's sessions. Tests 2 and 3 in this file (override_relogin_swaps_the_target_and_kills_the_old_session, override_switch_without_logout_revokes_the_presented_session) also authenticate as the same default user. Since Rust's test runner executes tests within a binary concurrently by default, and run-e2e.sh doesn't pin --test-threads=1 for this invocation, this revoke-all can intermittently kill a sibling test's in-flight session, producing flaky CI failures.

See consolidated comment for the fix location.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/services/authenticator/tests/e2e_override.rs` around lines 149 -
174, Prevent the revoke-all flow from affecting concurrent tests by using a
test-specific identity instead of the shared default user in the scenario around
login_flow and DELETE /auth/sessions. Ensure the impersonator and its view-as
session still belong to that isolated identity so the existing 401 assertion
remains valid.

Comment on lines +146 to +150
echo "==> run the __override view-as loop (#1941)"
AUTH_BASE="http://localhost:$AUTH_PORT" AUTH_BASE_DISABLED="http://localhost:$AUTH2_PORT" \
E2E_USER=dev@company.nonpresent \
cargo test -p authenticator --test e2e_override -- --ignored --nocapture

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Pin --test-threads=1 for e2e_override — its tests share a destructive, identity-scoped operation.

e2e_override.rs's first test calls DELETE /auth/sessions (revoke-all) for the shared user identity, while the sibling tests in the same binary also authenticate as that same user. Without --test-threads=1 (or RUST_TEST_THREADS=1), cargo's default parallel test execution can let the revoke-all race with and kill a sibling test's in-flight session, causing flaky CI failures.

🔧 Proposed fix
 AUTH_BASE="http://localhost:$AUTH_PORT" AUTH_BASE_DISABLED="http://localhost:$AUTH2_PORT" \
   E2E_USER=dev@company.nonpresent \
-  cargo test -p authenticator --test e2e_override -- --ignored --nocapture
+  cargo test -p authenticator --test e2e_override -- --ignored --nocapture --test-threads=1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
echo "==> run the __override view-as loop (#1941)"
AUTH_BASE="http://localhost:$AUTH_PORT" AUTH_BASE_DISABLED="http://localhost:$AUTH2_PORT" \
E2E_USER=dev@company.nonpresent \
cargo test -p authenticator --test e2e_override -- --ignored --nocapture
echo "==> run the __override view-as loop (`#1941`)"
AUTH_BASE="http://localhost:$AUTH_PORT" AUTH_BASE_DISABLED="http://localhost:$AUTH2_PORT" \
E2E_USER=dev@company.nonpresent \
cargo test -p authenticator --test e2e_override -- --ignored --nocapture --test-threads=1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/services/authenticator/tests/run-e2e.sh` around lines 146 - 150,
Update the e2e_override cargo test invocation in the run script to pass
--test-threads=1, ensuring tests sharing the user identity execute serially
while preserving the existing ignored and nocapture options.

@cyberantonz
cyberantonz merged commit 34efa74 into constructorfabric:main Jul 27, 2026
40 checks passed
cyberantonz added a commit to cyberantonz/insight that referenced this pull request Jul 27, 2026
Mirror of insight-gitops!34 (AGENTS.md sync rule for docs/deploy).

The umbrella secrets.yaml added in constructorfabric#1944 is guarded by
credentials.autoGenerate and never renders on gitops stands —
insight-authenticator-config is composed by this script. Read
authenticator.overrideEnabled from the env values (default false) and
emit the override_enabled env var into the composed Secret.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anton Zelenov <antonz@constructor.tech>
@cyberantonz
cyberantonz deleted the feat/override-login-1941 branch July 28, 2026 02:50
mitasovr pushed a commit that referenced this pull request Jul 28, 2026
#1950)

Mirror of insight-gitops!34 (AGENTS.md sync rule for docs/deploy).

The umbrella secrets.yaml added in #1944 is guarded by
credentials.autoGenerate and never renders on gitops stands —
insight-authenticator-config is composed by this script. Read
authenticator.overrideEnabled from the env values (default false) and
emit the override_enabled env var into the composed Secret.

Signed-off-by: Anton Zelenov <antonz@constructor.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
cyberantonz added a commit to cyberantonz/insight that referenced this pull request Aug 5, 2026
Companion to the authenticator change (constructorfabric#1944)
restoring the operator 'view the dashboard as another user' facility:

- The historical URL keeps working: '?__override=<email>' on any page is
  consumed at boot and bounced into /auth/login?__override=...&return_to=
  (the parameter itself never enters the router). Works while already
  logged in — the authenticator revokes the presented session at the
  callback and the IdP hop is silent SSO.
- /auth/me's 'impersonator_email' (present only on view-as sessions) is
  parsed into the session store.
- A 'Viewing as X' banner renders above the content on view-as sessions,
  naming the real principal, with an exit button that just re-logs-in as
  yourself.

The override is honored server-side only where the authenticator's
'override_enabled' flag is on (dev/demo stands); everywhere else the
parameter is inert and this UI never shows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anton Zelenov <antonz@constructor.tech>
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.

Admin 'view as another user' override (?__override=<email>) no longer works

3 participants