Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func WithGatewayIdentity(enabled bool) Option {

// WithIdentityClaimsBudget sets the gateway identity claims allowance in bytes.
// The default is 256 KiB;
// New and NewStreaming reject settings outside 1..6 MiB.
// New and [NewStreaming] reject settings outside 1..6 MiB.
// Each claim value costs 64 bytes plus name, string and exact-number text bytes.
// Dedicated gateway scopes consume the same allowance.
// Raw claim JSON is also limited to this byte length.
Expand Down
10 changes: 5 additions & 5 deletions authn/authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (
)

// VerifyFunc authenticates a credential without its HTTP scheme.
// It must return ErrInvalidCredentials for a definite rejection;
// It must return [ErrInvalidCredentials] for a definite rejection;
// unknown errors are treated as unavailable.
// Success requires JWT/SourceLocallyVerifiedToken for Bearer or IAM/SourceVerifiedIAMProof for EdgeIAM.
// Constructors alone do not verify tokens.
type VerifyFunc func(context.Context, string) (identity.Caller, error)
type VerifyFunc func(ctx context.Context, credential string) (identity.Caller, error)

// Config supplies explicit verifier dependencies and HTTP framing policy.
type Config struct {
Expand Down Expand Up @@ -44,7 +44,7 @@ type Authenticator struct {
}

// New validates and copies cfg without I/O or invoking a verifier.
// It returns a nil Authenticator and ErrInvalidConfiguration for invalid configuration.
// It returns a nil Authenticator and [ErrInvalidConfiguration] for invalid configuration.
func New(cfg Config) (*Authenticator, error) {
if cfg.Bearer == nil && cfg.IAMProof == nil || cfg.MaxAuthorizationBytes < 0 || cfg.MaxAuthorizationBytes > 1<<20 {
return nil, ErrInvalidConfiguration
Expand Down Expand Up @@ -79,10 +79,10 @@ func New(cfg Config) (*Authenticator, error) {
// Callers must not mutate headers concurrently.
// Every failure returns an anonymous zero caller.
//
// An established caller fails with identity.ErrConflict before credential work.
// An established caller fails with [identity.ErrConflict] before credential work.
// Runtime failures are *Error with sanitized categories;
// caller cancellation returns ctx.Err directly.
// Nil contexts and nil/zero Authenticators return ErrInvalidConfiguration.
// Nil contexts and nil/zero Authenticators return [ErrInvalidConfiguration].
// No provider diagnostic or context cause is exposed.
func (a *Authenticator) Authenticate(ctx context.Context, headers http.Header) (identity.Caller, error) {
if a == nil || a.cfg.MaxAuthorizationBytes == 0 || ctx == nil {
Expand Down
8 changes: 4 additions & 4 deletions authn/cognito.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ type CognitoVerifier struct {
}

// NewCognitoVerifier validates and copies cfg without I/O.
// Invalid configuration returns a nil verifier and ErrInvalidConfiguration.
// Invalid configuration returns a nil verifier and [ErrInvalidConfiguration].
// It trusts the configured issuer endpoint;
// it does not establish that the operator supplied an AWS URL.
func NewCognitoVerifier(cfg CognitoConfig) (*CognitoVerifier, error) {
Expand Down Expand Up @@ -139,8 +139,8 @@ func NewCognitoVerifier(cfg CognitoConfig) (*CognitoVerifier, error) {

// Verify returns one locally verified caller without installing it in context.
// It accepts access tokens only, with integer exp/iat and optional nbf, and a string aud when present.
// Rejections match ErrInvalidCredentials;
// unavailable JWKS matches ErrUnavailable.
// Rejections match [ErrInvalidCredentials];
// unavailable JWKS matches [ErrUnavailable].
// Caller cancellation returns ctx.Err, never its cause.
// Every failure returns a zero caller and sanitized diagnostics.
func (v *CognitoVerifier) Verify(ctx context.Context, token string) (identity.Caller, error) {
Expand All @@ -160,7 +160,7 @@ func (v *CognitoVerifier) Verify(ctx context.Context, token string) (identity.Ca
// Warm obtains a fresh usable JWKS snapshot, sharing any required fetch.
// It obeys refresh/backoff gates;
// it does not force refresh, validate a token or start a background refresh loop.
// Failures are ErrUnavailable, caller ctx.Err or ErrInvalidConfiguration for a nil context or unconfigured verifier.
// Failures are [ErrUnavailable], caller ctx.Err or [ErrInvalidConfiguration] for a nil context or unconfigured verifier.
func (v *CognitoVerifier) Warm(ctx context.Context) error {
if v == nil || v.http == nil || ctx == nil {
return ErrInvalidConfiguration
Expand Down
4 changes: 2 additions & 2 deletions authn/doc.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Package authn selects one configured Bearer or EdgeIAM verifier from an HTTP Authorization header and transports the verified caller to protected handlers.
// NewCognitoVerifier provides local RS256 Cognito access-token verification with direct JSON v2 and bounded JWKS caching.
// [NewCognitoVerifier] provides local RS256 Cognito access-token verification with direct JSON v2 and bounded JWKS caching.
// It does not check token revocation, authorize actions, or infer trust from a gateway, a TLS field, an action header or forwarded headers.
//
// Use New to configure explicit verifier functions.
Expand All @@ -8,7 +8,7 @@
// Public routes omit that middleware;
// CORS and preflight policy belong to the gateway or outer middleware.
//
// Verifiers must authenticate credentials before returning a caller and classify rejection with ErrInvalidCredentials.
// Verifiers must authenticate credentials before returning a caller and classify rejection with [ErrInvalidCredentials].
// Adapt iamproof.ErrInvalidProof explicitly when wiring an IAM proof verifier;
// this package does not import AWS packages.
// Never retry another verifier after rejection.
Expand Down
38 changes: 29 additions & 9 deletions authn/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,35 @@ import (
)

// Inspectable failure categories never contain credentials or provider errors.
// Match them with [errors.Is].
// Request-time failures are wrapped in an [*Error] whose [Error.StatusCode] is noted below.
var (
// ErrInvalidConfiguration means a constructor rejected its configuration, or a method was called on a nil or zero value or with a nil context.
// It is returned directly, not wrapped in an [*Error].
ErrInvalidConfiguration = errors.New("authn: invalid configuration")
ErrMissingCredentials = errors.New("authn: missing credentials")

// ErrMissingCredentials means the request has no Authorization field (401 with all configured challenges).
ErrMissingCredentials = errors.New("authn: missing credentials")

// ErrMalformedCredentials means the Authorization field is repeated, comma-combined, empty or not valid scheme framing (400).
ErrMalformedCredentials = errors.New("authn: malformed credentials")
ErrUnsupportedScheme = errors.New("authn: unsupported scheme")
ErrHeaderTooLarge = errors.New("authn: authorization header too large")
ErrInvalidCredentials = errors.New("authn: invalid credentials")
ErrUnavailable = errors.New("authn: verification unavailable")
ErrVerifierContract = errors.New("authn: invalid verifier result")

// ErrUnsupportedScheme means the credential uses a well-formed scheme that is not configured (401 with all configured challenges).
ErrUnsupportedScheme = errors.New("authn: unsupported scheme")

// ErrHeaderTooLarge means the Authorization field exceeds [Config.MaxAuthorizationBytes] (431, without a challenge).
ErrHeaderTooLarge = errors.New("authn: authorization header too large")

// ErrInvalidCredentials means the selected verifier definitely rejected the credential (401 with that scheme's challenge).
// A [VerifyFunc] returns it to classify a rejection.
ErrInvalidCredentials = errors.New("authn: invalid credentials")

// ErrUnavailable means verification could not complete, for example because a dependency failed or a verifier returned an unclassified error (503, without a challenge).
ErrUnavailable = errors.New("authn: verification unavailable")

// ErrVerifierContract means a verifier reported success with an unacceptable caller, such as the wrong kind or source (500).
// It indicates a composition fault, not a client error.
ErrVerifierContract = errors.New("authn: invalid verifier result")
)

// Error describes a sanitized HTTP authentication failure.
Expand All @@ -31,15 +51,15 @@ type Error struct {
// Error returns a diagnostic containing only the sanitized category.
func (e *Error) Error() string { return e.Unwrap().Error() }

// Unwrap exposes only a package category or identity.ErrConflict.
// Unwrap exposes only a package category or [identity.ErrConflict].
func (e *Error) Unwrap() error {
if e == nil || e.category == nil {
return ErrVerifierContract
}
return e.category
}

// StatusCode returns the recommended HTTP status, or 500 for a zero Error.
// StatusCode returns the recommended HTTP status, or 500 for a zero [Error].
func (e *Error) StatusCode() int {
if e == nil || e.status == 0 {
return http.StatusInternalServerError
Expand All @@ -55,7 +75,7 @@ func (e *Error) Challenges() []string {
return slices.Clone(e.challenges)
}

// GoString returns the same sanitized diagnostic used by Error.
// GoString returns the same sanitized diagnostic used by [Error.Error].
func (e *Error) GoString() string { return e.Error() }

func (a *Authenticator) failure(category error, status int, selected scheme) *Error {
Expand Down
2 changes: 1 addition & 1 deletion authz/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// Construct rules once and share them across requests.
// Custom checks must be concurrency-safe, honor their context and return false, nil for ordinary denial.
// Every callback error becomes ErrUnavailable.
// Every callback error becomes [ErrUnavailable].
// All and Any evaluate left to right and stop on an observed error;
// keep mandatory application guards outside alternative permission branches.
//
Expand Down
14 changes: 7 additions & 7 deletions authz/predicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// Sources matches any supplied nonanonymous producer.
// Empty lists, SourceNone and unknown sources return ErrInvalidConfiguration.
// Empty lists, SourceNone and unknown sources return [ErrInvalidConfiguration].
// Source is trusted-code attribution, not an unforgeable credential.
// Combine this broad predicate with permission checks.
// The input slice is copied.
Expand All @@ -32,7 +32,7 @@ func Sources(allowed ...identity.Source) (Rule, error) {
}

// JWTSubject matches an exact issuer and subject.
// Both must be nonempty UTF-8 or construction returns ErrInvalidConfiguration.
// Both must be nonempty UTF-8 or construction returns [ErrInvalidConfiguration].
// A subject does not prove a human identity.
// Wrong kind or missing subject does not match.
func JWTSubject(issuer, subject string) (Rule, error) {
Expand All @@ -47,7 +47,7 @@ func JWTSubject(issuer, subject string) (Rule, error) {
}

// JWTClient matches an exact issuer and client_id, without interpreting audience or subject as a client ID or inferring a machine grant.
// Both arguments must be nonempty UTF-8 or construction returns ErrInvalidConfiguration.
// Both arguments must be nonempty UTF-8 or construction returns [ErrInvalidConfiguration].
func JWTClient(issuer, clientID string) (Rule, error) {
if !validIdentifier(issuer) || !validIdentifier(clientID) {
return Rule{}, ErrInvalidConfiguration
Expand All @@ -62,7 +62,7 @@ func JWTClient(issuer, clientID string) (Rule, error) {
// JWTScopes requires all supplied exact OAuth scope tokens from the exact issuer.
// It does not interpret wildcards.
// Missing or unavailable scopes do not match.
// Issuer must be nonempty UTF-8 and required must contain at least one valid RFC 6749 scope token, otherwise construction returns ErrInvalidConfiguration.
// Issuer must be nonempty UTF-8 and required must contain at least one valid RFC 6749 scope token, otherwise construction returns [ErrInvalidConfiguration].
// The input slice is copied.
// Use Any for alternative scope requirements.
func JWTScopes(issuer string, required ...string) (Rule, error) {
Expand All @@ -73,7 +73,7 @@ func JWTScopes(issuer string, required ...string) (Rule, error) {
// Flattened gateway strings remain unavailable and never match.
// Groups are distinct from scopes and application grants.
// Issuer and each required value must be nonempty UTF-8;
// an empty list or invalid value returns ErrInvalidConfiguration.
// an empty list or invalid value returns [ErrInvalidConfiguration].
// The input slice is copied.
func CognitoGroups(issuer string, required ...string) (Rule, error) {
return jwtSet(issuer, required, identity.JWT.CognitoGroups, validIdentifier)
Expand Down Expand Up @@ -128,7 +128,7 @@ func validScope(s string) bool {
// IAMPrincipal matches the full caller ARN literally, including path and session.
// A root ARN matches only that root caller, not the account.
// Valid literal * and ? in user paths retain their meaning as characters.
// Unsupported caller forms (including bare role ARNs) return ErrInvalidConfiguration, following NewIAM's grammar.
// Unsupported caller forms (including bare role ARNs) return [ErrInvalidConfiguration], following NewIAM's grammar.
// This does not evaluate AWS IAM permissions or bind a unique identity across deletion and recreation of the principal.
func IAMPrincipal(principalARN string) (Rule, error) {
if _, err := identity.NewIAM(principalARN, identity.SourceCustomAssertion); err != nil {
Expand All @@ -141,7 +141,7 @@ func IAMPrincipal(principalARN string) (Rule, error) {
}

// IAMRoleSessions matches assumed-role callers with the exact partition, account and role name, allowing any valid session name.
// Invalid components, wildcards and role paths return ErrInvalidConfiguration.
// Invalid components, wildcards and role paths return [ErrInvalidConfiguration].
// This is a role-name policy: recreating the same role name can match again.
// It does not bind a unique role ID, identify the original actor, fabricate a role ARN or evaluate IAM policies.
func IAMRoleSessions(partition, accountID, roleName string) (Rule, error) {
Expand Down
35 changes: 24 additions & 11 deletions authz/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,25 @@ import (
)

// Authorization errors contain no request facts or callback diagnostics.
// Match them with [errors.Is].
var (
// ErrInvalidConfiguration means a rule constructor rejected its arguments, or [Rule.Authorize] was called on a zero Rule or with a nil context.
ErrInvalidConfiguration = errors.New("authz: invalid configuration")
ErrInvalidRequest = errors.New("authz: invalid request")
ErrUnauthenticated = errors.New("authz: unauthenticated")
ErrDenied = errors.New("authz: denied")
ErrUnavailable = errors.New("authz: unavailable")

// ErrInvalidRequest means the [Request] has an empty or non-UTF-8 action or a non-UTF-8 resource.
// No check runs.
ErrInvalidRequest = errors.New("authz: invalid request")

// ErrUnauthenticated means the request caller is anonymous.
// No check runs, even a permissive custom one.
ErrUnauthenticated = errors.New("authz: unauthenticated")

// ErrDenied means the rule evaluated without error and did not match.
ErrDenied = errors.New("authz: denied")

// ErrUnavailable means a [CheckFunc] returned an error, whatever its boolean result.
// Evaluation stops at that node rather than trying another branch.
ErrUnavailable = errors.New("authz: unavailable")
)

// Request supplies facts passed by value to each check.
Expand All @@ -32,11 +45,11 @@ type Request struct {

// CheckFunc decides whether a request matches.
// Return false, nil for denial;
// any error becomes ErrUnavailable, even when accompanied by true.
// any error becomes [ErrUnavailable], even when accompanied by true.
// Checks run synchronously, must honor ctx and must be safe for concurrent use when shared.
// The caller owns dependency deadlines.
// Panics propagate.
type CheckFunc func(context.Context, Request) (bool, error)
type CheckFunc func(ctx context.Context, request Request) (bool, error)

// Rule holds immutable private configuration.
// Copies may be shared concurrently.
Expand Down Expand Up @@ -66,7 +79,7 @@ type node struct {
}

// Check constructs a rule from a nonnil callback.
// A nil callback returns ErrInvalidConfiguration.
// A nil callback returns [ErrInvalidConfiguration].
// Reusing a check does not deduplicate its evaluation.
func Check(check CheckFunc) (Rule, error) {
if check == nil {
Expand All @@ -77,14 +90,14 @@ func Check(check CheckFunc) (Rule, error) {

// All requires every child to match, evaluating left to right.
// It stops on the first nonmatch or error.
// Empty, unconfigured or oversized trees return ErrInvalidConfiguration.
// Empty, unconfigured or oversized trees return [ErrInvalidConfiguration].
// The input slice is copied.
func All(rules ...Rule) (Rule, error) { return combine(opAll, rules) }

// Any requires one child to match, evaluating left to right.
// It stops on the first match or error;
// an error never falls through to another permission path.
// Empty, unconfigured or oversized trees return ErrInvalidConfiguration.
// Empty, unconfigured or oversized trees return [ErrInvalidConfiguration].
// The input slice is copied.
// Mandatory guards belong in an outer All.
func Any(rules ...Rule) (Rule, error) { return combine(opAny, rules) }
Expand All @@ -109,8 +122,8 @@ func combine(op operation, rules []Rule) (Rule, error) {
// Authorize returns nil only for an explicit allow.
// It checks rule/context configuration, cancellation, request validity and a nonanonymous caller in that order.
// No check runs for an anonymous or invalid request.
// Nonmatches return ErrDenied;
// all callback errors are sanitized to ErrUnavailable.
// Nonmatches return [ErrDenied];
// all callback errors are sanitized to [ErrUnavailable].
// Cancellation is checked between nodes and before returning, and returns ctx.Err(), never context.Cause.
// No results are cached or installed in context.
func (r Rule) Authorize(ctx context.Context, request Request) error {
Expand Down
16 changes: 16 additions & 0 deletions docs/decisions/0026-exported-api-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,19 @@ The Cognito verifier stays in `authn`.
E1 moves the selector and renames its type and constructor;
E2 then renames the moved sentinels, so each PR is a single kind of change.
This is compatible with decision 0011's intent: the new package holds only the existing selector, and action metadata remains consumer-owned.

## Implementation evidence

Each item landed as its own stacked PR, in order E1 to E6, and each passed gofmt, vet, module verification, the full race suite, the external consumer check, both Linux builds and the whitespace check on Go 1.27.1.

- E1: `go list -deps ./examples/dispatcher/native` no longer includes the root adapter or `aws-lambda-go/events`, and `actionheader` depends only on the standard library.
Parsing behavior and tests moved unchanged;
the gateway-format and composition tests remain in `edge`.
- E2 and E3 are renames with unchanged behavior.
Decision 0012 carries a dated note about the E3 rename.
- E4 adds `Operation` and `Resource` constants, used by every internal call site;
error text is unchanged.
- E5 adds `String` methods with tests for every declared value, one undeclared value per type and the sanitized `Caller` text.
- E6 documents every sentinel (including `authn`'s HTTP statuses, checked against the code), names callback parameters and `Claim.Bool` results, and adds doc links.
Links are added only for camel-case identifiers, methods and qualified module names, to avoid linking ordinary words such as `Any` or `Request`;
a go/doc-based check found every link in the six public packages resolves.
Loading
Loading