package auth import ( "context" "errors" "fmt" "github.com/kami/maven/internal/ipc" ) // Sentinel errors. The wire carries ErrForbidden as codeForbidden; the others // (ErrUnenrolled distinct) floor to forbidden on the wire too — but the daemon // log can still see the distinction server-side. var ( // ErrForbidden — the caller's scope caps the requested authority (a // voice-channel caller asking for L3, a poller writing an out-of-scope // source, etc.). The wire shape: codeForbidden. ErrForbidden = errors.New("auth: forbidden") // ErrUnenrolled — the caller isn't recognized by the enrollment table at // all. A distinct sentinel so the daemon can surface "this module's // enrollment is missing" as a wiring bug, not a generic denied. ErrUnenrolled = errors.New("auth: caller not enrolled") ) // Enrollment — the impure seam mapping a connecting process to a Scope. The // floor below trusts same-uid local callers fully (the 0600-floor equivalent), // production wires the real enrollment table reading a Config / sqlite table. // // Lookup is the single impure call per dispatch; Can and Check downstream are // pure. That keeps "no module ever reads its own authority" checkable: // anywhere outside the Enrollment impl doing caller→scope resolution is a bug. type Enrollment interface { // Lookup resolves the caller's Scope. Return ErrUnenrolled when the caller // isn't recognized; ErrForbidden when recognized but refused for policy // reasons (e.g. disabled module); any other error for I/O failure. // hasCaller=false is the in-process path (no ipc.Caller attached to ctx). Lookup(ctx context.Context, c ipc.Caller, hasCaller bool) (Scope, error) } // FloorEnrollment — today's auth floor. Same as the socket's 0600 perms: any // same-uid caller is trusted as a "core" module (SurfaceCoreProcess, L3, // write-any-source). The in-process path (no Caller) is the same: it's the // daemon itself, holding the unlocked store, so it gets L3 trivially. // // This is the AUTH FLOOR, not the auth model — the spec's invariant (surface // caps authority, source-scope, step-up) is shaped in policy.go and exercised // in tests through tighter enrollments. The daemon swaps this out when the // real enrollment table lands; nothing downstream changes. type FloorEnrollment struct { // Module is the label FloorEnrollment stamps on every caller (default // "core"). Real enrollment derives this from Caller.Uid/Pid. Module string } // NewFloorEnrollment — default "core" module, full source scope, L3 cap. // This preserves the prior (pre-auth) behavior: any same-uid caller was // permitted everything. Compiles to identity authority. func NewFloorEnrollment() *FloorEnrollment { return &FloorEnrollment{Module: "core"} } // Lookup — same-uid floor. HasCaller=false ⇒ in-process path (trusted "core"); // HasCaller=true ⇒ for now we still trust (only same-uid can connect via the // 0600 socket perms). The real enrollment table replaces this with a lookup // keyed on Uid/Pid → Module entry. func (f *FloorEnrollment) Lookup(_ context.Context, _ ipc.Caller, _ bool) (Scope, error) { return Scope{ Surface: SurfaceCoreProcess, Module: f.Module, SourceScope: []string{"*"}, }, nil } // StaticEnrollment — a hand-built enrollment for tests and demos: map every // caller exact-match on Uid to a fixed Scope. Pure-ish (no I/O); the daemon // holds it and lets the operator append at runtime; tests build their own. // Used to model "a poller that can only write poll:healthcheck" — the spec's // compromised-poller scenario — without standing up the full enrollment table. type StaticEnrollment struct { // ByUid — uid-keyed scope. Mutated to add an enrolled module. ByUid map[int32]Scope // InProcess is the scope returned for in-process (HasCaller=false) calls. // nil ⇒ falls through to Default. InProcess *Scope // Default is returned when no specific entry matches. nil ⇒ ErrUnenrolled // (fail closed). Default *Scope } // Lookup walks the static map. hasCaller ⇒ ByUid → Default → ErrUnenrolled; // in-process ⇒ InProcess → Default → ErrUnenrolled. Fail closed everywhere, // because a StaticEnrollment is built deliberately and any unmatched caller // is exactly the "who is this?" case the real table answers. func (s *StaticEnrollment) Lookup(_ context.Context, c ipc.Caller, hasCaller bool) (Scope, error) { if !hasCaller { if s.InProcess != nil { return *s.InProcess, nil } if s.Default != nil { return *s.Default, nil } return Scope{}, ErrUnenrolled } if sc, ok := s.ByUid[c.Uid]; ok { return sc, nil } if s.Default != nil { return *s.Default, nil } return Scope{}, fmt.Errorf("%w (uid=%d)", ErrUnenrolled, c.Uid) }