Skip to content

feat(auth): per-IdP basic_user_claim + header shows real CH identity - #5

Merged
BorisTyshkevich merged 1 commit into
mainfrom
feat/per-idp-basic-user-claim
Jun 20, 2026
Merged

feat(auth): per-IdP basic_user_claim + header shows real CH identity#5
BorisTyshkevich merged 1 commit into
mainfrom
feat/per-idp-basic-user-claim

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What

Adds a per-IdP basic_user_claim option to config.json. For a ch_auth:basic IdP, the SPA sends the Basic username from a chosen JWT claim (e.g. GitHub nickname) instead of the default email → preferred_username → sub chain.

Also fixes the top-bar identity: it showed the raw email claim even when the actual CH user was a different claim. A shared chUsername() helper now drives both the Basic credential and the header display, and config is resolved before first paint.

Why

On a ClickHouse cluster that already has a <token> user-directory (Bearer/JWT), a static IDENTIFIED WITH http user with the same name as the token-directory's ephemeral user shadows it (the replicated access storage sorts before <token>), so the Bearer login returns 403. Giving each IdP a distinct CH username lets two IdPs coexist for the same person:

  • Google → Bearer, native <token_processor>, CH user = email
  • GitHub (Auth0) → Basic via http_authentication_servers + a username_claim=nickname verifier, CH user = github login

This sidesteps the single-<token>-processor limitation (Altinity/ClickHouse#1928) without changing the OSS-CH Basic path (default behavior is unchanged when basic_user_claim is unset).

Verified live

  • antalya: GooglecurrentUser()=btyshkevich@altinity.com; GitHubcurrentUser()=BorisTyshkevich — both work in the same deployment, header shows the matching identity.
  • github (OSS CH): unchanged default (email) Basic path still works.

Changes

  • net/oauth-config.js: normalizeEntrybasicUserClaim (default '')
  • ui/app.js: chUsername() helper; authHeader + app.email() use it; ensureConfig sets app.basicUserClaim
  • main.js: await ensureConfig() before renderApp() when already signed in
  • tests: oauth-config (default + explicit claim), app (authHeader + header identity), main (fake-app)

npm test361 passing, oauth-config.js + app.js at 100% lines.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef

Adds a per-IdP `basic_user_claim` to config.json so a `ch_auth:basic` IdP
can map to a CH username taken from any JWT claim (e.g. GitHub `nickname`)
instead of the default email→preferred_username→sub chain.

Why: on a ClickHouse cluster that already has a `<token>` user-directory
(Bearer), a static `IDENTIFIED WITH http` user with the SAME name as the
token-directory's ephemeral user shadows it (replicated storage sorts before
`<token>`), so the Bearer login 403s. Giving each IdP a DISTINCT CH username
lets two IdPs (Bearer token-processor + Basic http_authentication_servers)
serve the same person side by side — without the single-`<token>`-processor
limit (Altinity/ClickHouse#1928).

Also fixes the header showing the raw `email` claim instead of the actual CH
user: a shared `chUsername()` helper now drives both the Basic credential and
the header display (they can no longer disagree), and main() resolves config
before first paint so the identity is correct immediately.

- net/oauth-config.js: normalizeEntry → `basicUserClaim` (default '')
- ui/app.js: `chUsername()` helper; authHeader + app.email() use it;
  ensureConfig sets app.basicUserClaim
- main.js: await ensureConfig() before renderApp when already signed in
- tests: oauth-config, app (authHeader + header identity), main fake-app

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef
@BorisTyshkevich
BorisTyshkevich merged commit ee9da00 into main Jun 20, 2026
2 checks passed
@BorisTyshkevich
BorisTyshkevich deleted the feat/per-idp-basic-user-claim branch June 22, 2026 16:43
BorisTyshkevich added a commit that referenced this pull request Jun 23, 2026
…t, qualified fallback, doc-cache, single tokenize (#26, #27)

Five findings from a follow-up manual review; verified live on otel.

Correctness:
- Hover docs now resolve the hovered word from the string/comment-masked text
  (host.maskedValue()), so hovering a function/keyword inside a string or comment
  no longer pops a phantom doc card — consistent with signature help (#1).
- Signature help strips the optional-param brackets ClickHouse uses
  (`name(a, b[, c])`) before splitting on commas, so args render cleanly and the
  active-arg highlight aligns (was showing `offset[` / `length]` for substring) (#2).
- completionContext only flags `qualified` when a real identifier precedes the
  dot; a bare dot (`.col`, `count().c`) now falls back to normal completion
  instead of an empty dropdown (#4).
- loadEntityDoc returns null on a query FAILURE vs '' for genuinely-no-doc, and
  entityDoc caches the latter but drops the former — a transient error no longer
  permanently suppresses a function's hover/footer doc for the session (#8).

Performance:
- The keystroke path tokenizes the buffer ONCE and shares the token list between
  the syntax highlighter and the literal mask, instead of two full passes per
  keystroke. maskFromTokens()/renderTokensInto() consume a token list; the editor
  memoizes it by (text, refData) so it re-tokenizes when server keyword/func sets
  arrive after connect (#5).

Tests cover each: hover-in-literal, bracketed-param signature split, bare-dot
fallback, failed-doc retry, and re-highlight-on-refData-change (the shared-token
cache invalidation). All gated layers stay 100/100/100/100 (ui glue within its
functions>=95 / branches>=90 floor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBS74oUsXarGkCRQKEFLu
BorisTyshkevich added a commit that referenced this pull request Aug 5, 2026
… retire dashboardTreeRevision

app.currentWorkspace/app.mainSurface become signal-backed accessor pairs
(peeking getter, notifying setter) so a mutation is its own notification —
the #426/#427 bug class (a write site forgetting to bump the invalidation
counter) becomes structurally impossible. app.committedWorkspace
(ReadonlySignal<StoredWorkspaceV5|null>) and app.treeNavigation
(a computed structural key over kind/dashboardId/currentMember) are the two
tracked reads app-shell.ts's tab-count/tree/Library effects subscribe
through, replacing state.dashboardTreeRevision.

app.currentWorkspace's setter is asymmetric (no null): a transitional null
publication is a named departure operation owned by a new closure-private
surface-retirement coordinator in app.ts (retireToWorkspaceLoading/
-Missing/-Failure/-Login), which batches the publication atomically with
disposing any live shell so a surface never repaints against transitional
state — five independent review passes each found a different call site
where this raced disposal, hence one coordinator with exclusive mutation
authority rather than five patches. SurfaceStatePort/DashboardApp/TabsApp/
DashboardTreeApp are narrowed to readonly on currentWorkspace (the fourth,
DashboardTreeApp, was documented read-only but not type-readonly before
this change).

app.reloadDashboardRoute() (a post-commit fold-and-reassign that would
double-publish once the aggregate is signal-backed) is deleted;
afterLibraryChange's Dashboard branch calls the render-only
app.renderCurrentSurface() instead.

Tests: tests/unit/surface-lifecycle-arch.test.ts (a static-source scan
backing the coordinator's compile/scan-layered "no lifecycle bypass"
claim) and tests/unit/surface-accessor-contracts.test.ts (@ts-expect-error
fixtures for the asymmetric setter and the four narrowed ports) are new,
plus a new app.test.ts describe block covering the issue's Tests #1/#3/#5
and the plan's invariant map (delivery-only no-ops, adversarial-id
collision-freedom, failure-path status/null ordering, the four-arm
live-shell no-repaint sweep, one-commit-exactly-once settlement, and the
mixed-snapshot batch-ordering regression). fake-app.ts's makeApp() installs
real per-call signals on the returned object (object spread evaluates an
accessor pair into a plain value, so the fake needs the same defineProperty
treatment createApp() gets natively) for reactivity parity in fixtures.

No persisted/schema change, no user-visible behavior change.

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