feat(auth): bootstrap collab admin from proxy.is_admin + dev-mode honesty #21
No reviewers
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_collab!21
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/admin-bootstrap"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #20.
Summary
Two coupled fixes that make collab's identity contract honest in both auth modes:
Proxy mode — bootstrap admin from
hero_proxy.users.is_adminon firstuser.me, with a first-user-on-empty-DB fallback when proxy is unreachable. Closes the chicken-and-egg deadlock where the deployer can't do anything after a fresh install.Dev mode — drop any
X-Hero-Userthe proxy injected, so the picker actually appears even when the operator hasuser_networksrows configured inhero_proxy.proxy.db. Closes the "why doesn't the picker show?" trap.These are coupled because they are the two halves of one architectural property: hero_proxy injects identity; hero_collab decides whether and how to act on it. Splitting them would leave collab in a half-honest state.
Changes
crates/hero_collab_server/src/handlers/user.rsquery_proxy_is_admin(identifier) -> Option<bool>(bounded 3s proxy lookup, mirrorsfederation::fetch_federated_users). +grant_system_admin(db, user_id)(idempotent INSERT-OR-IGNORE intogroups/group_members/group_rights). Bootstrap branch inuser.mefirst-INSERT path that grants admin per the rules below.crates/hero_collab_server/src/main.rshandle_rpcthat mapshero_usertoNonewhenstate.auth_mode == AuthMode::Dev, with atracing::debugline so operators can see why the header was dropped.Net: +170 lines, 0 removals. No schema change. Uses already-imported
hero_proxy_sdk.Bootstrap admin rules (priority order)
is_admin = 1→ grant. The proxy is the directory of "who deployed this stack"; if they're admin there, they're admin here.hero_osis's "first user_create gets Owner role." Bootstrap convenience for fresh installs where proxy might also be coming up.The bootstrap branch only runs on the first-INSERT path of
user.me(when no row exists for the identifier yet). Subsequent calls hit the cache hot-path and skip the bootstrap entirely.Dev-mode shim semantics
In dev mode, collab is the source of truth for identity. The 4-line shim explicitly drops upstream
X-Hero-Userso the picker contract is honest:Side effect: bootstrap admin doesn't fire in dev mode (because the shim makes
effective_hero_user = None→ no first-INSERT branch). That's correct — dev mode users come from--seed-dev-usersor the picker, not from federation.Behaviour matrix
proxy.is_admin=1user_networks--seed-dev-users(4 picker users)Test plan
cargo build --release -p hero_collab_servercleanworkspace.createfrom auto-provisioned sameh fails with PermissionDeniedright=adminrow ingroups/group_rightsafter first user.me, granted automatically becauseproxy.users.is_admin = 1for samehuser_networksconfigured for loopback: picker now appears (was silently bypassed before)user_networks: picker appears (unchanged)user_networksconfigured: identity injection still works (unchanged)Risk notes
3s proxy lookup latency on first user.me per new user. Acceptable because it only happens once per user (cached afterward); same pattern collab already uses for
collab.users.availablefederation. Could be tightened to 1-2s if it bites.Bootstrap admin race on empty DB. If proxy is unreachable AND DB is wiped AND multiple proxy-authed users hit user.me simultaneously, only the first one wins (others hit the populated-DB branch and don't auto-grant). In practice the operator gets there first because they're the one running the proxy. Acceptable trade-off for the bootstrap convenience.
No ability to revoke proxy.is_admin federation later. If the proxy demotes a user from admin, collab's grant persists. Out of scope for this PR — would need a separate sync/reconcile path. File as follow-up if it becomes a real concern.
Two coupled changes that make collab honest about identity in both auth modes. ## Bootstrap admin (proxy mode) Auto-provisioned users via `user.me` landed with zero rights — the deployer (proxy admin) couldn't `workspace.create`, invite anyone, or do anything until someone manually granted admin via SQL. Chicken-and-egg on every fresh deployment. Two-tier bootstrap rule (in priority order): 1. Query hero_proxy via `users_list` (3s timeout, mirrors federation::fetch_federated_users). If proxy reports the user `is_admin=1`, mirror that into a collab system `admins` group with `right=admin`. 2. If proxy is unreachable AND collab.users has no other users (excluding the row we just inserted), grant admin anyway — first-user-on-empty-DB pattern, mirrors hero_osis's `first user_create gets Owner role`. Otherwise: no grant. Joining users in a populated deployment land without admin and wait for an invite, which is the operator-deployed model collab is designed for. Two new helpers in `handlers/user.rs`: * `query_proxy_is_admin(identifier) -> Option<bool>` — bounded proxy lookup. Returns `Some(true|false)` if proxy reachable + user found; `None` if proxy unreachable, times out, or user not in proxy.users. Caller falls back to local heuristic on `None`. * `grant_system_admin(db, user_id)` — idempotent INSERT-OR-IGNORE into `groups`, `group_members`, `group_rights`. Safe to call multiple times; bootstrap re-entry produces no duplicates. The bootstrap branch runs only on the first-INSERT path of `user.me` (when no row exists for the identifier yet). Subsequent `user.me` calls hit the cache hot-path and skip the bootstrap entirely. ## Dev-mode honesty (dev mode) The natural counterpart: collab is now the source of truth for identity in `--auth-mode=dev`. Drop any X-Hero-User the proxy injected (typically via `user_networks` IP-auto-login) so the picker actually appears. Without this guard, dev mode silently bypassed the picker whenever the operator had loopback rows in `hero_proxy.user_networks` — confusing because user_networks is a hero_proxy concept the collab operator may not even know is active. Since `--auth-mode=dev` is an explicit operator statement of "I want pickers / multi-user testing here," we honor that statement by ignoring the upstream identity entirely. Implementation: 4-line shim at the top of `handle_rpc` that maps `hero_user` to `None` when `state.auth_mode == AuthMode::Dev`, emitting a tracing::debug. Proxy mode is unchanged. ## Why coupled in one PR These are the two halves of a single architectural property: hero_proxy injects identity for IPs it auto-trusts; hero_collab decides whether to act on that. In proxy mode it acts (with the new bootstrap admin). In dev mode it explicitly doesn't. Splitting them would leave collab in a half-honest state for one merge. ## No schema change. Uses already-imported `hero_proxy_sdk`.