v0.165.0: Indítópult megosztása — guest launcher via capability URL (+ optional password, QR)

Mint a 160-bit capability URL (/s/<token>) serving a standalone read-only guest
launcher: same tiles, opens apps in new tabs, no account, no admin session.
Information only, zero control — every privilege stays behind each app's own auth.

- /s/ pre-auth pass-through (after the claim gate) + session-CSRF exemption; guest
  password POST carries its own pre-auth HMAC CSRF.
- Constant-time token match; empty stored token = disabled = byte-identical mux 404.
- Optional per-share password: separate bcrypt hash + own attempt map; signed cookie
  = HMAC(token|passwordHash) keyed with web.session_secret, so rotate/change invalidates.
- Guest labels ride the v0.164.0 ruling; never expose internal state vocabulary.
- Token redacted in logs (/s/<redacted>); never in CHANGELOG/REPORT/CONTEXT.
- Admin modal: copy-link, QR (go-qrcode), set/clear password, rotate, disable.
- Tests: Groups A-G (14) + 3 red-proofs verified red.
This commit is contained in:
2026-07-24 12:08:43 +02:00
parent 8e5edb2865
commit 15206314ab
19 changed files with 1341 additions and 115 deletions
+38
View File
@@ -278,6 +278,44 @@ load the monogram shows through (the launcher does NOT use the app-placeholder h
are `<a target="_blank" rel="noopener">` links; stopped/degraded apps render greyed + unclickable with
the Hungarian state badge. Empty state links to `/stacks`.
#### Indítópult megosztása — guest launcher via capability URL (v0.165.0)
The admin launcher's **"Indítópult megosztása"** button mints a **capability URL** —
`https://<host>/s/<token>`, where `token` is a 160-bit `crypto/rand` value
(`newShareToken`, base64.RawURLEncoding, 27 chars) — that serves a **standalone, read-only guest
launcher** with **no account and no admin session**. The link grants **information only, zero
control**: app names + public URLs; every privilege stays behind each app's own auth and the
controller admin password. The tile visual is shared with the admin launcher via the `launch_tile`
template partial; the app slice comes from the extracted `Server.launcherApps()` helper.
- **Routing** (`internal/web/share.go`, `share_handlers.go`): `/s/<token>` joins the RequireAuth
pre-auth allowlist **after** the claim-gate block (an unclaimed box never serves the guest page —
the claim gate stays supreme) and is exempted from session CSRF (the guest password POST carries a
pre-auth HMAC CSRF, `validShareCSRF`, mirroring the claim POST). Token match is
`subtle.ConstantTimeCompare`; an empty stored token (= sharing OFF, there is no separate flag)
matches nothing, so a wrong/disabled token returns a **byte-identical mux-default 404** (`share404`).
Guest responses set `X-Robots-Tag: noindex, nofollow` / `Referrer-Policy: no-referrer` /
`Cache-Control: no-store`. The token is a secret: the ServeHTTP debug line and the 404 WARN redact
`/s/` paths to `/s/<redacted>`.
- **Optional per-share password** (`settings.LauncherSharePasswordHash`): a SEPARATE bcrypt credential
(never the admin `PasswordHash`), guarded by its OWN per-IP 5/1-min attempt map (`shareAttempts`,
never the admin `loginAttempts`). A correct password mints a signed gate cookie =
HMAC-SHA256(`token|passwordHash`) keyed with the persisted, box-scoped `web.session_secret` — so
rotating the token OR changing the password invalidates every outstanding cookie with no bookkeeping.
- **Guest state labels** ride the v0.164.0 ruling and never expose internal vocabulary: clickable ⇔
`isOperationalState && !routeUnpublished` (operational AND route actually published, so a tap never
dead-ends); `StateStopped` ⇒ "A tulajdonos leállította"; any other non-clickable state ⇒
"Átmenetileg nem elérhető". Empty ⇒ "Jelenleg nincs elérhető alkalmazás." (`buildGuestApps` is the
pure, tested mapping; templates `launcher_shared.html` + `launcher_share_password.html`).
- **Admin modal** (in `launcher.html`): current link + copy button, QR code
(`GET /launcher/share/qr.png`, ~256px PNG via `github.com/skip2/go-qrcode`, admin-authed, `no-store`),
set/clear share password, "Új link készítése" (rotate), "Megosztás kikapcsolása" (clears token AND
password). The management POSTs live under `/launcher/share/*` and ride the normal admin session +
session CSRF; rotate/disable use the inline `data-confirm` (felhomConfirm) affordance.
Design ruling: member accounts are superseded by this capability-URL model; per-member tile
visibility is parked under the SSO arc.
#### Dashboard "Megnyitás" Button
Running apps on the Vezérlőpult now show a "Megnyitás ↗" button that opens the app's subdomain in a new tab. The `Subdomains` map is built in `dashboardHandler` from `app.yaml` env or metadata fallback.