Record authenticated caller in the decision envelope.
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:
parent
e62c0cfc36
commit
ca070df32d
15 changed files with 344 additions and 35 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue