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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ JWT issuer, subject, client ID, audience, scopes and Cognito groups stay distinc
Ambiguous gateway collections remain unavailable, while conflicting interpretable scope sets fail.
IAM callers retain exact ARN paths and session names.

`identity.WithCaller` installs one caller in a derived context and rejects replacement, even by an apparently identical caller.
`identity.NewContext` installs one caller in a derived context and rejects replacement, even by an apparently identical caller.
`identity.FromContext` returns anonymous when absent.
See the [caller examples](identity/caller_example_test.go).

Expand Down
2 changes: 1 addition & 1 deletion adapter_invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func prepareRequest(ctx context.Context, inv *invocation, event decodedEvent, ra
if err != nil {
return nil, err
}
ctx, err = identity.WithCaller(ctx, caller)
ctx, err = identity.NewContext(ctx, caller)
if err != nil {
return nil, invocationError("identity", ErrIdentity, "caller installation failed", identity.ErrConflict)
}
Expand Down
2 changes: 1 addition & 1 deletion adapter_invoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func TestAdapterFailuresHaveNoResult(t *testing.T) {
if err != nil {
t.Fatal(err)
}
parent, err := identity.WithCaller(t.Context(), caller)
parent, err := identity.NewContext(t.Context(), caller)
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion authn/authn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func TestContextIsolationAndConflict(t *testing.T) {
jwt := jwtCaller(t, identity.SourceLocallyVerifiedToken)
var calls int
a := authenticator(t, authn.Config{Bearer: func(context.Context, string) (identity.Caller, error) { calls++; return jwt, nil }})
ctx, err := identity.WithCaller(t.Context(), jwt)
ctx, err := identity.NewContext(t.Context(), jwt)
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion authn/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (a *Authenticator) Handler(next http.Handler) (http.Handler, error) {
writeFailure(w, err)
return
}
ctx, err := identity.WithCaller(r.Context(), caller)
ctx, err := identity.NewContext(r.Context(), caller)
if err != nil {
writeFailure(w, a.failure(identity.ErrConflict, http.StatusInternalServerError, noScheme))
return
Expand Down
2 changes: 1 addition & 1 deletion authz/rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ func TestCallbackPanicPropagates(t *testing.T) {
func TestExplicitCallerAndCallerContext(t *testing.T) {
facts := request(t)
other := iamCaller(t, "arn:aws:iam::123456789012:user/Bob")
ctx, err := identity.WithCaller(t.Context(), other)
ctx, err := identity.NewContext(t.Context(), other)
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion docs/authn.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Edge still rejects a usable response for a canceled invocation.

For custom JSON responses, use Authenticate and the Error accessors in ordinary HTTP middleware.
Serialize application envelopes directly with encoding/json/v2.
On success, install the returned caller with identity.WithCaller before dispatch.
On success, install the returned caller with identity.NewContext before dispatch.
See the [runnable examples](../authn/example_test.go) for error mapping, a custom JSON failure response and a shared action dispatcher.
Their synthetic Bearer fixture is explicitly not a production token verifier.

Expand Down
2 changes: 1 addition & 1 deletion docs/consumer-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Gateway fixtures describe shapes, not deployed authorizers.
| Boundary | Tools | Evidence |
| --- | --- | --- |
| Business rule | Identity constructors and `authz.Request` | Exact action/resource policy outcomes |
| Authenticated handler | `httptest`, `identity.WithCaller` | Behavior after admission |
| Authenticated handler | `httptest`, `identity.NewContext` | Behavior after admission |
| Authentication | Real verifier with local JWKS/STS transport | Local signature/profile/proof processing |
| Gateway conversion | Explicit SDK events or raw JSON v2 | Chosen payload format's behavior |
| Stream lifecycle | Public reader, controlled producer, incremental reads | Delivery, backpressure, cancellation and errors |
Expand Down
3 changes: 3 additions & 0 deletions docs/decisions/0012-shared-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ func FromContext(ctx context.Context) Caller
func WithCaller(ctx context.Context, caller Caller) (context.Context, error)
```

Note (2026-09-27): [decision 0026](0026-exported-api-review.md) E3 renamed `WithCaller` to `NewContext`, pairing it with `FromContext`;
its signature and semantics are unchanged.

Kind distinguishes anonymous, JWT, and IAM.
Source distinguishes no identity, gateway assertion, locally verified token, and explicitly mapped custom assertion.
JWT/IAM views have private fields and read-only accessors.
Expand Down
2 changes: 1 addition & 1 deletion docs/iamproof.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Successful callers have SourceVerifiedIAMProof, preserve the full ARN/session an
Root, user, assumed-role session and federated-user identities are represented;
representation grants no application permission.

The authentication layer must select exactly one credential, call exactly one verifier, install the returned caller using identity.WithCaller, then authorize the selected application action.
The authentication layer must select exactly one credential, call exactly one verifier, install the returned caller using identity.NewContext, then authorize the selected application action.
The [authn guide](authn.md) shows this middleware and the explicit adapter mapping iamproof.ErrInvalidProof to authn.ErrInvalidCredentials.
Never fall back to another verifier after rejection.

Expand Down
2 changes: 1 addition & 1 deletion docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ these are placement, naming, typing and documentation changes made before the fi

- [x] E1: move the action-header selector to the standard-library-only `actionheader` package as `Selector`/`NewSelector`.
- [x] E2: rename the moved sentinels to `ErrMissing`, `ErrAmbiguous` and `ErrInvalid`.
- [ ] E3: rename `identity.WithCaller` to `identity.NewContext`.
- [x] E3: rename `identity.WithCaller` to `identity.NewContext`.
- [ ] E4: type the `InvocationError` operation and limit names with exported constants.
- [ ] E5: add `String` methods to the `identity` enumerations.
- [ ] E6: complete sentinel docs, doc links, callback parameter names and `Claim.Bool` result names.
Expand Down
2 changes: 1 addition & 1 deletion examples/dispatcher/internal/orders/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func New(cfg Config) (http.Handler, error) {
writeAuthenticationError(w, err)
return
}
ctx, err := identity.WithCaller(r.Context(), caller)
ctx, err := identity.NewContext(r.Context(), caller)
if err != nil {
writeError(w, 500, "internal_error")
return
Expand Down
6 changes: 3 additions & 3 deletions identity/caller_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,20 @@ func ExampleNewJWT() {
// Output: batch-worker false [orders.read] true
}

func ExampleWithCaller() {
func ExampleNewContext() {
// This validates a synthetic identifier;
// it does not authenticate it.
caller, err := identity.NewIAM("arn:aws:sts::123456789012:assumed-role/Worker/batch-1", identity.SourceCustomAssertion)
if err != nil {
panic(err)
}
ctx, err := identity.WithCaller(context.Background(), caller)
ctx, err := identity.NewContext(context.Background(), caller)
if err != nil {
panic(err)
}
iam, _ := identity.FromContext(ctx).IAM()
fmt.Println(iam.PrincipalARN())
_, err = identity.WithCaller(ctx, caller)
_, err = identity.NewContext(ctx, caller)
fmt.Println(errors.Is(err, identity.ErrConflict))
// Output:
// arn:aws:sts::123456789012:assumed-role/Worker/batch-1
Expand Down
10 changes: 5 additions & 5 deletions identity/caller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,20 +252,20 @@ func TestCallerContextAndDiagnostics(t *testing.T) {
if got := identity.FromContext(parent); got.Kind() != identity.KindAnonymous || got.Source() != identity.SourceNone {
t.Errorf("empty context = %v", got)
}
unchanged, err := identity.WithCaller(parent, identity.Caller{})
unchanged, err := identity.NewContext(parent, identity.Caller{})
if err != nil || unchanged != parent {
t.Error("anonymous installation did not preserve empty context")
}
ctx, err := identity.WithCaller(parent, caller)
ctx, err := identity.NewContext(parent, caller)
if err != nil || ctx.Value(unrelatedKey{}) != "kept" || identity.FromContext(ctx).Kind() != identity.KindIAM {
t.Fatalf("WithCaller lost context/caller: %v", err)
t.Fatalf("NewContext lost context/caller: %v", err)
}
for _, next := range []identity.Caller{caller, {}} {
if ctx, err := identity.WithCaller(ctx, next); ctx != nil || !errors.Is(err, identity.ErrConflict) {
if ctx, err := identity.NewContext(ctx, next); ctx != nil || !errors.Is(err, identity.ErrConflict) {
t.Errorf("reinstallation = %v, %v", ctx, err)
}
}
if ctx, err := identity.WithCaller(nil, caller); ctx != nil || !errors.Is(err, identity.ErrInvalidCaller) {
if ctx, err := identity.NewContext(nil, caller); ctx != nil || !errors.Is(err, identity.ErrInvalidCaller) {
t.Errorf("nil context = %v, %v", ctx, err)
}
iam, _ := caller.IAM()
Expand Down
4 changes: 2 additions & 2 deletions identity/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ func FromContext(ctx context.Context) Caller {
return caller
}

// WithCaller installs caller in a derived context.
// NewContext returns a derived context carrying caller, for retrieval with FromContext.
// Anonymous on an empty context is a no-op.
// Any attempted installation over a nonanonymous caller, including an identical caller or anonymous, returns ErrConflict and a nil context.
// A nil context returns ErrInvalidCaller.
// Unrelated values are preserved.
func WithCaller(ctx context.Context, caller Caller) (context.Context, error) {
func NewContext(ctx context.Context, caller Caller) (context.Context, error) {
if ctx == nil {
return nil, ErrInvalidCaller
}
Expand Down
2 changes: 1 addition & 1 deletion invocation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestInvocationRejectsInheritedCaller(t *testing.T) {
if err != nil {
t.Fatal(err)
}
parent, err := identity.WithCaller(t.Context(), caller)
parent, err := identity.NewContext(t.Context(), caller)
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion streaming_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func TestStreamingConstructorsAndValidation(t *testing.T) {
if err != nil {
t.Fatal(err)
}
ctx, err := identity.WithCaller(t.Context(), caller)
ctx, err := identity.NewContext(t.Context(), caller)
if err != nil {
t.Fatal(err)
}
Expand Down
Loading