0b7d80cee0
System means "the plane itself, in-process" (router, coordinator, adapters, lease-expiry reclaim) and is unconditionally FullControl with no token gate. But it was reachable straight from the X-Orchestra-Surface HTTP header, both in authz.HTTP's token check and in main.go's own `surface` closure (which every handler actually calls to authorize an event — it re-parses the header independently of what the HTTP middleware resolved). Since no deployment configures ORCHESTRA_SYSTEM_TOKEN (no legitimate HTTP caller should ever need one), tokens[System] is always "", so the token check was skipped entirely: any LAN request with "X-Orchestra-Surface: system" got unauthenticated full control to emit any event on any task. Both the authz.HTTP middleware and main.go's `surface` closure now downgrade System to Web before doing anything else with it, so the header can never resolve to System over HTTP regardless of token config. AUDIT.md B8.
107 lines
3.4 KiB
Go
107 lines
3.4 KiB
Go
// Package authz contains the single authorization policy used by all control
|
|
// surfaces. Clients identify a surface; the bus decides what it may emit.
|
|
package authz
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type Surface string
|
|
|
|
const (
|
|
Telegram Surface = "telegram"
|
|
Ntfy Surface = "ntfy"
|
|
TUI Surface = "tui"
|
|
Web Surface = "web"
|
|
MCP Surface = "mcp"
|
|
Maven Surface = "maven"
|
|
// System identifies the plane itself — the router, coordinator, provider
|
|
// adapters, and lease-expiry reclaim. Per invariant 2 ("the plane emits
|
|
// events, not the agent"), these are the only non-surface emitters and are
|
|
// always full control. Every event must carry an explicit Surface; there
|
|
// is no unauthenticated default, so an emitter that forgets to declare one
|
|
// is rejected at the bus rather than silently treated as trusted.
|
|
System Surface = "system"
|
|
)
|
|
|
|
type Capability int
|
|
|
|
const (
|
|
Observe Capability = iota
|
|
NotifyOnly
|
|
GatedWrite
|
|
FullControl
|
|
)
|
|
|
|
func ParseSurface(v string) Surface { return Surface(strings.ToLower(strings.TrimSpace(v))) }
|
|
func CapabilityFor(s Surface) Capability {
|
|
switch s {
|
|
case Telegram, Ntfy:
|
|
return NotifyOnly
|
|
case TUI, Web, System:
|
|
return FullControl
|
|
case MCP, Maven:
|
|
return GatedWrite
|
|
default:
|
|
return Observe
|
|
}
|
|
}
|
|
func (s Surface) CanRead() bool { return CapabilityFor(s) >= Observe }
|
|
func (s Surface) CanEmit(typ string) bool {
|
|
if CapabilityFor(s) == FullControl {
|
|
return true
|
|
}
|
|
return typ == "ApprovalRequested" && CapabilityFor(s) == GatedWrite
|
|
}
|
|
func (s Surface) RequiresApproval(typ string) bool {
|
|
return CapabilityFor(s) == GatedWrite && typ != "ApprovalRequested"
|
|
}
|
|
|
|
func AuthorizeEvent(s Surface, typ string) error {
|
|
if !s.CanEmit(typ) {
|
|
return fmt.Errorf("surface %q cannot emit %s", s, typ)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HTTP enforces the same policy at the bus boundary. Authentication is
|
|
// optional for local development; when a token is supplied, control surfaces
|
|
// must present it as a Bearer token.
|
|
func HTTP(tokens map[Surface]string, next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
s := ParseSurface(r.Header.Get("X-Orchestra-Surface"))
|
|
if s == "" {
|
|
s = Web
|
|
}
|
|
// System means "the plane itself, in-process" (router, coordinator,
|
|
// adapters, lease-expiry reclaim) and is always FullControl with no
|
|
// token gate — it must never be reachable by declaring it over HTTP.
|
|
// Without this, tokens[System] being unset (as it is by default: no
|
|
// caller ever needs a System token) makes the check at line ~78 a
|
|
// no-op, and any LAN request with this header gets unauthenticated
|
|
// full control over every task.
|
|
if s == System {
|
|
s = Web
|
|
}
|
|
if expected := tokens[s]; expected != "" && r.Header.Get("Authorization") != "Bearer "+expected {
|
|
http.Error(w, "unauthorized surface", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if (s == Telegram || s == Ntfy) && r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
http.Error(w, "notify-only surface", http.StatusForbidden)
|
|
return
|
|
}
|
|
if (s == MCP || s == Maven) && r.Method != http.MethodGet && r.Method != http.MethodHead && r.URL.Path != "/v1/events" {
|
|
// Gated clients may submit only approval requests; ordinary control
|
|
// endpoints must never become an accidental write path.
|
|
if !strings.HasSuffix(r.URL.Path, "/approval") {
|
|
http.Error(w, "approval required", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|