flex-auth/internal/callerauth/auth.go
tegwick 1e1e077b27 Implement inbound caller authentication (ADR 0004); close T03 and T05
TokenReview-based caller identity with audience-scoped tokens and exact
resource.system to ServiceAccount bindings, per ops-warden's recommendation.
Deletes the unwired tenant-engine live-roles adapter (T03) and adds
make verify-posture (T05). Source implements A2; running digest is still A0
until promotion, so tenancy.current.A stays 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:22:52 +02:00

125 lines
3.6 KiB
Go

// Package callerauth authenticates protected systems before flex-auth evaluates
// the authorization request they submit.
package callerauth
import (
"context"
"errors"
"fmt"
"strings"
)
type Mode string
const (
ModeDisabled Mode = "disabled"
ModeWarn Mode = "warn"
ModeEnforce Mode = "enforce"
)
var (
ErrUnauthenticated = errors.New("caller is not authenticated")
ErrForbidden = errors.New("caller is not allowed to represent the requested system")
ErrUnavailable = errors.New("caller identity service is unavailable")
)
type Identity struct {
Username string
Audiences []string
}
type TokenReviewer interface {
Review(context.Context, string) (Identity, error)
}
type WarningFunc func(string, ...any)
type Authenticator struct {
mode Mode
reviewer TokenReviewer
audience string
bindings map[string]string
warnf WarningFunc
}
func New(mode Mode, reviewer TokenReviewer, audience string, bindings map[string]string, warnf WarningFunc) (*Authenticator, error) {
switch mode {
case ModeDisabled:
return &Authenticator{mode: mode}, nil
case ModeWarn, ModeEnforce:
default:
return nil, fmt.Errorf("unsupported caller-auth mode %q", mode)
}
if reviewer == nil {
return nil, fmt.Errorf("token reviewer is required in %s mode", mode)
}
if strings.TrimSpace(audience) == "" {
return nil, fmt.Errorf("caller audience is required in %s mode", mode)
}
if len(bindings) == 0 {
return nil, fmt.Errorf("at least one caller binding is required in %s mode", mode)
}
copyBindings := make(map[string]string, len(bindings))
for system, principal := range bindings {
if strings.TrimSpace(system) == "" || strings.TrimSpace(principal) == "" {
return nil, fmt.Errorf("caller bindings require non-empty system and principal")
}
copyBindings[system] = principal
}
return &Authenticator{mode: mode, reviewer: reviewer, audience: audience, bindings: copyBindings, warnf: warnf}, nil
}
func Disabled() *Authenticator {
authenticator, _ := New(ModeDisabled, nil, "", nil, nil)
return authenticator
}
// Authorize verifies the bearer token and binds every resource.system value to
// the authenticated workload principal. Warn mode records the same failures but
// permits the request so callers can be migrated before enforcement is enabled.
func (a *Authenticator) Authorize(ctx context.Context, authorization string, systems []string) error {
if a == nil || a.mode == ModeDisabled {
return nil
}
err := a.authorize(ctx, authorization, systems)
if err != nil && a.mode == ModeWarn {
if a.warnf != nil {
a.warnf("caller authentication warning: %v", err)
}
return nil
}
return err
}
func (a *Authenticator) authorize(ctx context.Context, authorization string, systems []string) error {
token, ok := strings.CutPrefix(authorization, "Bearer ")
if !ok || strings.TrimSpace(token) == "" || strings.ContainsAny(strings.TrimSpace(token), " \t\r\n") {
return ErrUnauthenticated
}
identity, err := a.reviewer.Review(ctx, strings.TrimSpace(token))
if err != nil {
return fmt.Errorf("%w: %v", ErrUnavailable, err)
}
if strings.TrimSpace(identity.Username) == "" || !contains(identity.Audiences, a.audience) {
return ErrUnauthenticated
}
if len(systems) == 0 {
return fmt.Errorf("%w: request has no resources", ErrForbidden)
}
for _, system := range systems {
expected, found := a.bindings[system]
if !found || expected != identity.Username {
return fmt.Errorf("%w: principal %q cannot represent system %q", ErrForbidden, identity.Username, system)
}
}
return nil
}
func contains(values []string, wanted string) bool {
for _, value := range values {
if value == wanted {
return true
}
}
return false
}