Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
c3611c9
feat: add shared group directories and NSS wrapper
aktech Mar 27, 2026
5fd6fc8
fix: correct NSS GID to 1000 (jovyan) and always create ~/shared dir
aktech Mar 27, 2026
1686945
fix: store groups in auth_state and always create ~/shared/<group> dirs
aktech Mar 27, 2026
d1f0b57
feat: add in-cluster NFS server for shared storage on RWO-only clusters
aktech Mar 27, 2026
57abb06
fix: add DaemonSet to install nfs-common on k3s worker nodes
aktech Mar 27, 2026
a13d11f
fix: use alpine:3 sleep for DaemonSet pause container
aktech Mar 27, 2026
75caabe
fix: NFS PV path /exports not / (overlayfs cannot be exported)
aktech Mar 27, 2026
5b86834
fix: remove spawner.user.groups (DetachedInstanceError in async); add…
aktech Mar 27, 2026
fc7a169
fix: address code review findings (I1-I7, C1-C3, M1, N3)
aktech Mar 27, 2026
56c22cf
docs: add JupyterLab profiles design spec
aktech Mar 27, 2026
f46cdc4
Revert "docs: add JupyterLab profiles design spec"
aktech Mar 27, 2026
7809fa3
feat: add JupyterLab profiles for CPU/RAM resource sizing (closes #31)
aktech Mar 27, 2026
afca811
Merge branch 'main' into feat/user-shared-volumes
aktech Mar 29, 2026
13aa6e7
fix: add descriptive names to default server profiles
aktech Mar 29, 2026
6ce50bf
Merge pull request #33 from nebari-dev/fix/profile-descriptions
aktech Mar 29, 2026
303cab8
merge: resolve conflict — keep cookie fix from main
aktech Mar 30, 2026
8d536d8
Merge branch 'main' into feat/user-shared-volumes
aktech Apr 16, 2026
425c7f8
Merge remote-tracking branch 'origin/main' into feat/user-shared-volumes
aktech Apr 27, 2026
67a452e
split: move JupyterLab profiles to separate branch
aktech Apr 27, 2026
38ea366
test: add k3d-based e2e smoke test
aktech Apr 27, 2026
e23f1b1
test: add NFS-backed shared-storage e2e tests
aktech Apr 27, 2026
34564c9
refactor(tests/e2e): split conftest into deep modules
aktech Apr 27, 2026
ec48c19
ci: speed up e2e — disable z2jh prePuller + cache kindest/node
aktech Apr 27, 2026
e836206
ci: fix kindest/node cache-save step under set -e
aktech Apr 27, 2026
9de8ef3
ci: drop kindest/node cache attempt
aktech Apr 27, 2026
1c89ab2
test(e2e): expand shared-storage suite to full permission contract
aktech Apr 30, 2026
e39b3b2
ci: cache singleuser image across runs to skip ~73s cold pull
aktech Apr 30, 2026
1aaa99c
docs(shared-storage): position external RWX as primary, mark in-clust…
aktech May 1, 2026
b6504f8
docs(shared-storage): correct provider list — only longhorn is NIC-pr…
aktech May 1, 2026
5e95e8d
fix(nebi-envs): re-fetch auth_state when access token is stale
aktech May 6, 2026
064c7bd
Merge branch 'main' into fix/nebi-envs-stale-token
aktech May 6, 2026
3756212
feat: forward access token via Authorization Bearer header
aktech May 6, 2026
61e30b9
fix(values): default forwardAccessToken=false to avoid jhub-apps loop
aktech May 6, 2026
e906f78
feat: pin jhub-apps to PR #676 + default forwardAccessToken=true
aktech May 8, 2026
3c5d01d
chore: bump hub image to sha-e906f78 (carries patched jhub-apps)
aktech May 8, 2026
8bf06bf
chore: pin hub image to PR-53 merge-commit sha-9381aab
aktech May 8, 2026
a566ce2
chore: bump jhub-apps git pin to include Starlette 1.0 TemplateRespon…
aktech May 8, 2026
179ebd8
chore: bump hub image to sha-ab37dda (Starlette 1.0 TemplateResponse …
aktech May 8, 2026
37eddfc
test(unit): add pytest harness for jupyterhub config modules
aktech May 14, 2026
16407c2
feat(auth): switch hub to KeyCloakOAuthenticator (GenericOAuthenticator)
aktech May 14, 2026
d93c5ae
chore(chart): flip NebariApp to enforceAtGateway=false, hub OAuth cal…
aktech May 14, 2026
161ab2d
revert(deps): drop jhub-apps git pin + relax pyjwt — hub owns OAuth now
aktech May 14, 2026
2f11f26
chore: bump hub image to sha-ae0969a (GenericOAuthenticator + jhub-ap…
aktech May 14, 2026
9cb2474
fix(auth): request openid + profile + email + groups scopes
aktech May 14, 2026
fec1737
chore: bump hub image to sha-ffb035a (openid scope fix)
aktech May 14, 2026
76856ca
fix(deps): cap starlette<1 — jhub-apps 2025.11.1 uses legacy Template…
aktech May 14, 2026
03234f1
chore: bump hub image to sha-8676046 (starlette<1 cap)
aktech May 14, 2026
8ceaf0d
docs: mark HANDOFF-stale-token.md resolved (GenericOAuthenticator swi…
aktech May 14, 2026
c2443b0
feat(auth): auto-login + KC end-session with id_token_hint
aktech May 15, 2026
e2d14ed
chore: bump hub image to sha-0e393cb (auto_login + KC end-session)
aktech May 15, 2026
c987087
fix(auth): override LogoutHandler.get to inject id_token_hint
aktech May 15, 2026
7182292
chore: bump hub image to sha-38305c6 (logout id_token_hint fix)
aktech May 15, 2026
8b395d5
fix(auth): monkey-patch LogoutHandler.get so /hub/logout uses our han…
aktech May 15, 2026
f3788a4
chore: bump hub image to sha-8ff8d0a (logout monkey-patch)
aktech May 15, 2026
4cff508
fix(auth): use OAuthenticator.logout_handler hook (no monkey-patch)
aktech May 15, 2026
dbf3d4c
chore: bump hub image to sha-239effb (logout_handler hook fix)
aktech May 15, 2026
87174a9
fix(auth): stash logout pieces on class attr (not via c. traitlets)
aktech May 15, 2026
0550865
chore: bump hub image to sha-2c816a2 (logout class-attr fix)
aktech May 15, 2026
34e845b
fix(auth): override LogoutHandler.get to capture id_token before user…
aktech May 15, 2026
71fc4dc
chore: bump hub image to sha-67880ee (logout id_token capture in get)
aktech May 15, 2026
6649850
fix(spawner): pin pvc_name_template to claim-{username}
aktech May 15, 2026
8b8c190
fix(auth): implement refresh_user to rotate KC refresh_token in auth_…
aktech May 15, 2026
19801ea
fix(e2e): drop japps service_workers 4 -> 1 to unstick hub bootup
aktech May 15, 2026
65c51eb
chore: remove EnvoyOIDC-era stale-token fallbacks made dead by refres…
aktech May 15, 2026
81b01a8
refactor(auth): bundle KC strings into KeyCloakConfig dataclass
aktech May 15, 2026
37e7626
chore: untrack HANDOFF-stale-token.md and ignore future handoff notes
aktech May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Helm dependencies (regenerated via helm dependency update)
charts/

# Working handoff notes — never commit.
HANDOFF*.md

# OS
.DS_Store

Expand All @@ -21,3 +24,4 @@ venv/

# Pixi
.pixi/
.venv-unit/
1 change: 1 addition & 0 deletions .helmignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ docs/
# Testing
tests/
*_test.yaml
.venv-unit/
433 changes: 252 additions & 181 deletions config/jupyterhub/00-gateway-auth.py

Large diffs are not rendered by default.

64 changes: 14 additions & 50 deletions config/jupyterhub/01-spawner.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@
c.KubeSpawner.storage_pvc_ensure = True
c.KubeSpawner.storage_capacity = get_config("custom.storage-capacity", "20Gi")
c.KubeSpawner.storage_access_modes = ["ReadWriteOnce"]
# Without this override, KubeSpawner's default template is
# `claim-{username}--{servername}`, so for jhub-apps named servers it ensures
# a per-server PVC — while the `volumes` block below mounts the per-user
# `claim-{username}`. Users with a pre-existing per-user PVC (legacy
# JupyterLab launch) survived this mismatch; fresh users hit
# FailedScheduling because the per-user PVC is never auto-created. Forcing
# the template to `{username}` makes ensure + mount converge on the same
# RWO claim, which the pod-affinity rule below keeps to a single node.
c.KubeSpawner.pvc_name_template = "claim-{username}"
c.KubeSpawner.volumes = [
{
"name": "home",
Expand Down Expand Up @@ -392,32 +401,6 @@ def get_nebi_jwt(refresh_token, access_token, keycloak_url, nebi_cid, hub_cid, h
return nebi_jwt


def _fetch_fresh_auth_state(username):
"""Fetch the latest auth_state for a user via the JupyterHub API.

Uses JUPYTERHUB_API_TOKEN (which has admin:auth_state scope) to read
the user's auth_state, which refresh_user() keeps updated with fresh
Envoy cookies on browser requests.

Returns the auth_state dict, or None on failure.
"""
api_url = os.environ.get("JUPYTERHUB_API_URL", "")
api_token = os.environ.get("JUPYTERHUB_API_TOKEN", "")
if not api_url or not api_token:
log.warning("JUPYTERHUB_API_URL/TOKEN not set, cannot fetch fresh auth_state")
return None
try:
req = Request(
f"{api_url}/users/{username}",
headers={"Authorization": f"token {api_token}"},
)
with urlopen(req, timeout=10) as resp:
return json.loads(resp.read()).get("auth_state")
except Exception as exc:
log.error("Failed to fetch fresh auth_state for %s: %s", username, exc)
return None


# ---------------------------------------------------------------------------
# Nebi auto-authentication (pre-spawn hook)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -455,26 +438,6 @@ async def _nebi_pre_spawn_hook(spawner):
refresh_token = auth_state.get("refresh_token")
access_token = auth_state.get("access_token") or ""

# The access token from auth_state may be stale (refresh_user updates
# it on browser requests, but the spawn is an internal API call).
# Check expiry and re-fetch from the hub API if needed.
if access_token and not refresh_token:
claims = _decode_jwt_claims(access_token)
import time as _time
exp = claims.get("exp", 0)
remaining = exp - int(_time.time()) if exp else 0
if remaining < 30:
log.info(
"Access token for %s expires in %ds, fetching fresh auth_state from hub API",
spawner.user.name, remaining,
)
fresh_state = await asyncio.to_thread(
_fetch_fresh_auth_state, spawner.user.name,
)
if fresh_state:
access_token = fresh_state.get("access_token") or access_token
refresh_token = fresh_state.get("refresh_token") or refresh_token

if not refresh_token and not access_token:
log.warning("No access_token or refresh_token for %s, skipping", spawner.user.name)
return
Expand Down Expand Up @@ -561,9 +524,10 @@ async def _nebi_pre_spawn_hook(spawner):
def _get_user_groups(auth_state):
"""Extract and filter user groups from auth_state.

Reads groups stored in auth_state by EnvoyOIDCAuthenticator (from Keycloak
IdToken groups claim). Applies the allowlist if configured.
Uses Path(g).name (last component) like classic Nebari so /projects/myproj → myproj.
Reads groups stored in auth_state by KeyCloakOAuthenticator (from
Keycloak's IdToken `groups` claim). Applies the allowlist if
configured. Uses Path(g).name (last component) like classic Nebari so
/projects/myproj → myproj.
Deduplicates to prevent duplicate mountPath entries in the pod spec.
Note: does NOT fall back to spawner.user.groups — accessing that SQLAlchemy
relationship from an async hook causes DetachedInstanceError.
Expand Down Expand Up @@ -758,7 +722,7 @@ async def _pre_spawn_hook(spawner):
else:
log.debug("pre-spawn: Nebi auto-auth not configured, skipping")

# 2. Resolve groups from auth_state (stored by EnvoyOIDCAuthenticator)
# 2. Resolve groups from auth_state (stored by KeyCloakOAuthenticator)
groups = _get_user_groups(auth_state)
log.info("pre-spawn: user %s resolved groups: %s", username, groups)

Expand Down
Loading
Loading