Record authenticated caller in the decision envelope.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 1m0s

FLEX-WP-0023-T04: provenance.caller is additive (mode required;
principal/audience/not_after when a token was reviewed). TokenReview
keeps the JWT exp. request_digest is unchanged because the caller is
not binding material.

Assistant: grok
Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267
This commit is contained in:
tegwick 2026-09-14 04:44:07 +02:00
parent e62c0cfc36
commit ca070df32d
15 changed files with 344 additions and 35 deletions

View file

@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"strings"
"time"
)
type Mode string
@ -26,6 +27,28 @@ var (
type Identity struct {
Username string
Audiences []string
NotAfter time.Time
}
// Record is the caller provenance stamped onto a decision envelope.
// Mode is always set. Principal, audience and expiry are present only when a
// token was actually reviewed.
type Record struct {
Mode Mode
Principal string
Audience string
NotAfter time.Time
}
type recordContextKey struct{}
func WithRecord(ctx context.Context, record Record) context.Context {
return context.WithValue(ctx, recordContextKey{}, record)
}
func RecordFromContext(ctx context.Context) (Record, bool) {
record, ok := ctx.Value(recordContextKey{}).(Record)
return record, ok
}
type TokenReviewer interface {
@ -77,45 +100,72 @@ func Disabled() *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 {
// The returned identity is populated whenever a token was reviewed, including
// warn-mode binding failures, so provenance can name an observed principal.
func (a *Authenticator) Authorize(ctx context.Context, authorization string, systems []string) (Identity, error) {
if a == nil || a.mode == ModeDisabled {
return nil
return Identity{}, nil
}
err := a.authorize(ctx, authorization, systems)
identity, 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 identity, nil
}
return err
return identity, err
}
func (a *Authenticator) authorize(ctx context.Context, authorization string, systems []string) error {
// Record returns the provenance object for a reviewed identity. Disabled mode
// is stated as {"mode":"disabled"} with no principal.
func (a *Authenticator) Record(identity Identity) Record {
mode := ModeDisabled
audience := ""
if a != nil {
mode = a.mode
audience = a.audience
}
record := Record{Mode: mode}
if mode == ModeDisabled || strings.TrimSpace(identity.Username) == "" {
return record
}
record.Principal = identity.Username
record.Audience = audience
record.NotAfter = identity.NotAfter
return record
}
func (a *Authenticator) authorize(ctx context.Context, authorization string, systems []string) (Identity, error) {
token, ok := strings.CutPrefix(authorization, "Bearer ")
if !ok || strings.TrimSpace(token) == "" || strings.ContainsAny(strings.TrimSpace(token), " \t\r\n") {
return ErrUnauthenticated
return Identity{}, ErrUnauthenticated
}
identity, err := a.reviewer.Review(ctx, strings.TrimSpace(token))
token = strings.TrimSpace(token)
identity, err := a.reviewer.Review(ctx, token)
if err != nil {
if errors.Is(err, ErrUnauthenticated) {
return err
return Identity{}, err
}
return Identity{}, fmt.Errorf("%w: %v", ErrUnavailable, err)
}
if identity.NotAfter.IsZero() {
if exp, ok := tokenExpiry(token); ok {
identity.NotAfter = exp
}
return fmt.Errorf("%w: %v", ErrUnavailable, err)
}
if strings.TrimSpace(identity.Username) == "" || !contains(identity.Audiences, a.audience) {
return ErrUnauthenticated
return Identity{}, ErrUnauthenticated
}
if len(systems) == 0 {
return fmt.Errorf("%w: request has no resources", ErrForbidden)
return identity, 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 identity, fmt.Errorf("%w: principal %q cannot represent system %q", ErrForbidden, identity.Username, system)
}
}
return nil
return identity, nil
}
func contains(values []string, wanted string) bool {