// 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" ) 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: 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 } 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) }) }