perUserHomes: home directory not materialized at login — fresh users fail the peering residency check #146

Closed
opened 2026-08-09 01:47:41 +00:00 by Cordy · 2 comments
Owner

Fourth live find of the #106 dogfood. A brand-new local-auth user (Niko-test on the beta instance) logged in successfully, appeared in the receivers allowlist, peer names matched — and inbound transfers still failed STATUS_UNKNOWN_RECIPIENT.

Cause: peerRecipients.Exists implements "allow-listed AND resident here" with residency = Stat("/home/<recipient>") succeeding. But logging in does not create the home directory — it is materialized lazily by the first write. A user who has authenticated but never uploaded anything is invisible to peering (and their own Personal view 404s until first write, visible in the request log as a GET /api/v1/files 404 loop).

Intent vs implementation: residency-by-login is the design's evident intent; residency-by-first-upload is what's implemented. The gap only shows for fresh users, which is exactly the "new colleague receives their first file via peering" scenario the feature is for.

Fix options:

  1. Materialize /home/<user> at successful login (all auth modes). Small, matches intent, also fixes the 404 loop on a fresh user's Personal view. Recommended.
  2. Have the delivery path MkdirAll implicitly — rejected: it would make the residency check meaningless (any allow-listed name becomes "resident").

Found because the beta instance uses local auth where users only exist after /setup; on the OIDC dogfood every user had long since uploaded something.

Fourth live find of the #106 dogfood. A brand-new local-auth user (`Niko-test` on the beta instance) logged in successfully, appeared in the receivers allowlist, peer names matched — and inbound transfers still failed `STATUS_UNKNOWN_RECIPIENT`. **Cause:** `peerRecipients.Exists` implements "allow-listed AND resident here" with residency = `Stat("/home/<recipient>")` succeeding. But logging in does not create the home directory — it is materialized lazily by the first write. A user who has authenticated but never uploaded anything is invisible to peering (and their own Personal view 404s until first write, visible in the request log as a `GET /api/v1/files 404` loop). **Intent vs implementation:** residency-by-login is the design's evident intent; residency-by-first-upload is what's implemented. The gap only shows for fresh users, which is exactly the "new colleague receives their first file via peering" scenario the feature is for. **Fix options:** 1. **Materialize `/home/<user>` at successful login** (all auth modes). Small, matches intent, also fixes the 404 loop on a fresh user's Personal view. Recommended. 2. Have the delivery path `MkdirAll` implicitly — rejected: it would make the residency check meaningless (any allow-listed name becomes "resident"). Found because the beta instance uses local auth where users only exist after `/setup`; on the OIDC dogfood every user had long since uploaded something.
Author
Owner

Live confirmation plus a sharpening: the recipient on the beta instance is an admin, and admins are deliberately unscoped (scope.go: "Admins are NOT scoped: they address the whole backend directly"). So an admin's uploads land at the backend root and never materialize /home/<user> — for admin recipients the lazy-materialization gap isn't just "hasn't uploaded yet," it's permanent: no amount of normal usage ever creates their home. The workaround used in the dogfood was manually creating home/<user> from the admin's root view.

This strengthens option 1: materialize /home/<user> at successful login for every authenticated user, admin or not — login is the one event both scoped and unscoped users share. It also means the fix must not route through the scoped store (admins bypass it); do the MkdirAll against the unscoped store at the auth layer.

Live confirmation plus a sharpening: the recipient on the beta instance is an **admin**, and admins are deliberately unscoped (scope.go: "Admins are NOT scoped: they address the whole backend directly"). So an admin's uploads land at the backend root and *never* materialize `/home/<user>` — for admin recipients the lazy-materialization gap isn't just "hasn't uploaded yet," it's **permanent**: no amount of normal usage ever creates their home. The workaround used in the dogfood was manually creating `home/<user>` from the admin's root view. This strengthens option 1: **materialize `/home/<user>` at successful login for every authenticated user, admin or not** — login is the one event both scoped and unscoped users share. It also means the fix must not route through the scoped store (admins bypass it); do the MkdirAll against the unscoped store at the auth layer.
Cordy closed this issue 2026-08-09 02:49:04 +00:00
Author
Owner

Shipped in v0.4.12 (PR #149), live on the dogfood. Option 1 as recommended: auth.WithHomeEnsure — a Provider decorator with the inner Provider embedded (so Challenge and any future interface methods forward untouched; only Authenticate is intercepted). On first successful authentication per username it materializes /home/<username> through a callback wired to the unscoped store (Mkdir /home then Mkdir /home/<user>, errors ignored — the same lazy-ensure semantics as scope). Gated on HomesEnabled(); sits at the top of the provider chain so every auth mode (local, OIDC, LDAP, Basic/WebDAV) and both scoped and unscoped users pass through it. Fresh users are peering-visible from their first login; admins are no longer permanently invisible. Unit-tested: fires exactly once per user, never for unauthenticated requests.

Shipped in **v0.4.12** (PR #149), live on the dogfood. Option 1 as recommended: `auth.WithHomeEnsure` — a Provider decorator with the inner Provider **embedded** (so `Challenge` and any future interface methods forward untouched; only `Authenticate` is intercepted). On first successful authentication per username it materializes `/home/<username>` through a callback wired to the **unscoped** store (`Mkdir /home` then `Mkdir /home/<user>`, errors ignored — the same lazy-ensure semantics as scope). Gated on `HomesEnabled()`; sits at the top of the provider chain so every auth mode (local, OIDC, LDAP, Basic/WebDAV) and both scoped and unscoped users pass through it. Fresh users are peering-visible from their first login; admins are no longer permanently invisible. Unit-tested: fires exactly once per user, never for unauthenticated requests.
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#146
No description provided.