pluginsdk/auth/trustedheaders.go
Alex Dunmow b6d40ed8ac feat: bootstrap block/pluginsdk — the proto-first plugin SDK (P1)
Bootstrap the plugin-facing SDK module so the fleet migration (P2) becomes a
mechanical block/core/X -> block/pluginsdk/X import rewrite.

- abi/proto/v1: the single source-of-truth ABI proto tree, copied from the cms
  authoring source (cms/backend/abi/proto/v1) with go_package retargeted to
  git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1. Kills the hand-kept
  cms/core proto duplication (audit gap 4).
- abi/v1: generated Go bindings (buf generate); byte-identical to core's abi/v1
  save the embedded go_package path.
- plugin/ (registration + DI surface), plugin/wasmguest/** (transport shim,
  caps stubs, bnwasm db driver, testdata fixtures + golden .pb), and the
  guest-facing type packages: blocks (+builtin/shared/tags), templates
  (+pongo/bn), auth, settings, content, gating, crypto, rbac, video, ai,
  subscriptions, menus, datasources. Internal imports rewritten core ->
  pluginsdk; zero block/core references remain.
- README/AGENTS(+CLAUDE symlink)/Makefile: proto is the contract, Go is one
  binding; no replace directives; templates/bn is a synced copy authored in cms.

Spec: cms docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:51:00 +08:00

92 lines
3.5 KiB
Go

package auth
import (
"context"
"net/http"
"github.com/google/uuid"
)
// Trusted identity headers — the wasm plugin auth contract.
//
// Context values do NOT cross the wasm ABI, so a guest cannot see the auth
// context the host's middleware built. The SECURE way to convey the caller's
// identity to a guest is these headers, populated HOST-SIDE from the
// signature-verified principal after the host has run its RBAC guard
// (rbac.Authorize against the verified JWT). The host STRIPS any client-supplied
// copy of these headers before setting them, so a guest may trust them
// unconditionally.
//
// A guest MUST NOT decode a client-supplied cookie or Authorization/Bearer
// token to establish identity — those are attacker-controlled across the
// boundary (the host forwards the raw request), and trusting them is a
// privilege-escalation bug. Read identity ONLY from these headers (via
// TrustedHeaderMiddleware / ContextFromTrustedHeaders). See
// core/docs/wasm-abi.md ("Trusted identity headers").
//
// Values are the canonical MIME header form so a host map-set and a guest
// http.Header.Get agree without re-canonicalization surprises.
const (
// Admin principal (auth.Claims).
HeaderVerifiedUserID = "X-Bn-Verified-User-Id"
HeaderVerifiedRole = "X-Bn-Verified-Role"
HeaderVerifiedEmail = "X-Bn-Verified-Email"
// Public/community principal (auth.PublicClaims).
HeaderVerifiedPublicUserID = "X-Bn-Verified-Public-User-Id"
HeaderVerifiedPublicUsername = "X-Bn-Verified-Public-Username"
HeaderVerifiedPublicEmail = "X-Bn-Verified-Public-Email"
)
// AllTrustedHeaders lists every trusted identity header, canonical form. The
// host deletes each of these from the inbound request before injecting its own
// verified values, so a client cannot forge one.
func AllTrustedHeaders() []string {
return []string{
HeaderVerifiedUserID,
HeaderVerifiedRole,
HeaderVerifiedEmail,
HeaderVerifiedPublicUserID,
HeaderVerifiedPublicUsername,
HeaderVerifiedPublicEmail,
}
}
// ContextFromTrustedHeaders rebuilds the request's auth context from the host's
// verified identity headers. Trust model: see the const block above — these
// headers are host-controlled, so no token parsing or signature check happens
// here (the guest has no signing secret by design). A malformed user-id header
// is ignored rather than trusted.
func ContextFromTrustedHeaders(ctx context.Context, h http.Header) context.Context {
if s := h.Get(HeaderVerifiedUserID); s != "" {
if id, err := uuid.Parse(s); err == nil {
ctx = WithUser(ctx, &Claims{
UserID: id,
Email: h.Get(HeaderVerifiedEmail),
Role: h.Get(HeaderVerifiedRole),
})
}
}
if s := h.Get(HeaderVerifiedPublicUserID); s != "" {
if id, err := uuid.Parse(s); err == nil {
ctx = WithPublicUser(ctx, &PublicClaims{
UserID: id,
Email: h.Get(HeaderVerifiedPublicEmail),
Username: h.Get(HeaderVerifiedPublicUsername),
})
}
}
return ctx
}
// TrustedHeaderMiddleware wraps a guest HTTP handler so downstream RPCs can read
// auth.GetUserFromContext / GetPublicUserFromContext. Plugins whose Connect RPCs
// resolve the caller from context MUST mount this around their handler (context
// does not cross the ABI otherwise). This is the ONLY sanctioned way for a guest
// to learn the caller's identity.
func TrustedHeaderMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r.WithContext(ContextFromTrustedHeaders(r.Context(), r.Header)))
})
}