feat(operator): gate op.console to staff and land owner setup there
The operator console (op.console.<root>) requires internal permission verification on top of Zero-Trust: a passkey is not access. requireExternal now refuses any non-admin principal arriving on the admin host, before any handler, so op.console is staff-only at the door rather than per-route — including on the passwordless demo face where Cloudflare Access is not in front. The gate is inert on the player console (console.<root>). Owner first-run setup is staff onboarding, so `felis setup` mints the one-time setup URL on op.console.<root>/setup (was console.<root>). The passkey verifier lists both console and op.console in RPOrigins so the one-time binding asserts on either face under the shared console.<root> RP-ID. Session admin-access now includes role=owner, not only admin: the owner is a superset of admin, so excluding it left IsOwner() unreachable through a passwordless session. No path assigns role=owner yet — this is forward consistency. The bootstrap summary now names console.<root> the player panel and op.console.<root> the operator console where the Owner runs setup, fixing text that told operators not to run setup there. Tests: op.console door gate (non-admin refused, player console unaffected, admin passes) and owner session admin-access; the setup-bind default-host test follows the move to op.console.
This commit is contained in:
11 files changed
+127
-35
No files matched your search
@@ -89,6 +89,18 @@ func (a *API) requireExternal(next http.Handler) http.Handler {
|
||||
writeError(w, r, errUnauthorized)
|
||||
return
|
||||
}
|
||||
// op.console door gate: the operator console is staff-only, so a request that
|
||||
// arrives on the admin host from a non-admin principal is refused HERE, before
|
||||
// any handler. Authentication alone (a player's passkey/email/bind session) is
|
||||
// not access — internal permission is verified on top of it, so possessing a
|
||||
// valid credential never "lets you in" to op.console. On the player console
|
||||
// (console.<root_domain>) hostIsAdminConsole is false, so this is inert; in
|
||||
// production Cloudflare Access already blocks non-staff at the edge and this is
|
||||
// the defense-in-depth backstop for the passwordless (no-Zero-Trust) face.
|
||||
if hostIsAdminConsole(r, a.RootDomain, a.AdminHostname) && !p.IsAdmin() {
|
||||
writeError(w, r, errForbidden)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), ctxKeyPrincipal, p)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user