From 2caaf199cbc187a503b65c1c713c3b618a726511 Mon Sep 17 00:00:00 2001 From: AJ Beck Date: Sun, 27 Sep 2026 15:32:11 +0100 Subject: [PATCH] feat(identity): add String methods to identity enumerations Kind, Source, ClaimKind, Representation and IAMPrincipalType now format as stable lowercase names, with TypeName(n) for undeclared values. Caller.String uses them, so a caller prints as identity.Caller{kind:iam,source:verified_iam_proof} instead of numeric codes. Diagnostics still exclude identifiers and claims. --- docs/plan.md | 2 +- identity/caller.go | 4 +-- identity/names.go | 67 ++++++++++++++++++++++++++++++++++++++++ identity/names_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 identity/names.go create mode 100644 identity/names_test.go diff --git a/docs/plan.md b/docs/plan.md index 5de7711..289073d 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -380,7 +380,7 @@ these are placement, naming, typing and documentation changes made before the fi - [x] E2: rename the moved sentinels to `ErrMissing`, `ErrAmbiguous` and `ErrInvalid`. - [x] E3: rename `identity.WithCaller` to `identity.NewContext`. - [x] E4: type the `InvocationError` operation and limit names with exported constants. -- [ ] E5: add `String` methods to the `identity` enumerations. +- [x] E5: add `String` methods to the `identity` enumerations. - [ ] E6: complete sentinel docs, doc links, callback parameter names and `Claim.Bool` result names. Acceptance: each PR passes the full AGENTS.md validation set, and examples, guides and the external consumer check use the new names. diff --git a/identity/caller.go b/identity/caller.go index 73d7ee1..20bdb64 100644 --- a/identity/caller.go +++ b/identity/caller.go @@ -1,7 +1,5 @@ package identity -import "strconv" - // Kind identifies the caller's mutually exclusive identity representation. type Kind uint8 @@ -71,7 +69,7 @@ func (c Caller) IAM() (IAM, bool) { // String describes kind and source without exposing identifiers or claims. func (c Caller) String() string { - return "identity.Caller{kind:" + strconv.Itoa(int(c.Kind())) + ",source:" + strconv.Itoa(int(c.source)) + "}" + return "identity.Caller{kind:" + c.Kind().String() + ",source:" + c.source.String() + "}" } // GoString returns the same sanitized description as String. diff --git a/identity/names.go b/identity/names.go new file mode 100644 index 0000000..86fba01 --- /dev/null +++ b/identity/names.go @@ -0,0 +1,67 @@ +package identity + +import "strconv" + +var ( + kindNames = [...]string{ + KindAnonymous: "anonymous", + KindJWT: "jwt", + KindIAM: "iam", + } + sourceNames = [...]string{ + SourceNone: "none", + SourceGatewayAssertion: "gateway_assertion", + SourceLocallyVerifiedToken: "locally_verified_token", + SourceCustomAssertion: "custom_assertion", + SourceVerifiedIAMProof: "verified_iam_proof", + } + claimKindNames = [...]string{ + ClaimInvalid: "invalid", + ClaimNull: "null", + ClaimBoolean: "boolean", + ClaimString: "string", + ClaimNumber: "number", + ClaimArray: "array", + ClaimObject: "object", + } + representationNames = [...]string{ + RepresentationUnknown: "unknown", + RepresentationJSON: "json", + RepresentationDecoded: "decoded", + RepresentationGatewayText: "gateway_text", + } + iamPrincipalTypeNames = [...]string{ + IAMPrincipalUnknown: "unknown", + IAMPrincipalRoot: "root", + IAMPrincipalUser: "user", + IAMPrincipalAssumedRole: "assumed_role", + IAMPrincipalFederatedUser: "federated_user", + } +) + +// enumName returns names[v], or typeName(v) for a value outside the declared set. +func enumName(names []string, typeName string, v uint8) string { + if int(v) < len(names) { + return names[v] + } + return typeName + "(" + strconv.Itoa(int(v)) + ")" +} + +// String returns a stable lowercase name such as "jwt", or "Kind(n)" for an undeclared value. +func (k Kind) String() string { return enumName(kindNames[:], "Kind", uint8(k)) } + +// String returns a stable lowercase name such as "verified_iam_proof", or "Source(n)" for an undeclared value. +func (s Source) String() string { return enumName(sourceNames[:], "Source", uint8(s)) } + +// String returns a stable lowercase name such as "number", or "ClaimKind(n)" for an undeclared value. +func (k ClaimKind) String() string { return enumName(claimKindNames[:], "ClaimKind", uint8(k)) } + +// String returns a stable lowercase name such as "gateway_text", or "Representation(n)" for an undeclared value. +func (r Representation) String() string { + return enumName(representationNames[:], "Representation", uint8(r)) +} + +// String returns a stable lowercase name such as "assumed_role", or "IAMPrincipalType(n)" for an undeclared value. +func (t IAMPrincipalType) String() string { + return enumName(iamPrincipalTypeNames[:], "IAMPrincipalType", uint8(t)) +} diff --git a/identity/names_test.go b/identity/names_test.go new file mode 100644 index 0000000..8404704 --- /dev/null +++ b/identity/names_test.go @@ -0,0 +1,70 @@ +package identity_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/asteroid-computing/go-lambda-edge/identity" +) + +func TestEnumerationStrings(t *testing.T) { + for _, tt := range []struct { + value fmt.Stringer + want string + }{ + {identity.KindAnonymous, "anonymous"}, + {identity.KindJWT, "jwt"}, + {identity.KindIAM, "iam"}, + {identity.Kind(3), "Kind(3)"}, + {identity.SourceNone, "none"}, + {identity.SourceGatewayAssertion, "gateway_assertion"}, + {identity.SourceLocallyVerifiedToken, "locally_verified_token"}, + {identity.SourceCustomAssertion, "custom_assertion"}, + {identity.SourceVerifiedIAMProof, "verified_iam_proof"}, + {identity.Source(255), "Source(255)"}, + {identity.ClaimInvalid, "invalid"}, + {identity.ClaimNull, "null"}, + {identity.ClaimBoolean, "boolean"}, + {identity.ClaimString, "string"}, + {identity.ClaimNumber, "number"}, + {identity.ClaimArray, "array"}, + {identity.ClaimObject, "object"}, + {identity.ClaimKind(7), "ClaimKind(7)"}, + {identity.RepresentationUnknown, "unknown"}, + {identity.RepresentationJSON, "json"}, + {identity.RepresentationDecoded, "decoded"}, + {identity.RepresentationGatewayText, "gateway_text"}, + {identity.Representation(4), "Representation(4)"}, + {identity.IAMPrincipalUnknown, "unknown"}, + {identity.IAMPrincipalRoot, "root"}, + {identity.IAMPrincipalUser, "user"}, + {identity.IAMPrincipalAssumedRole, "assumed_role"}, + {identity.IAMPrincipalFederatedUser, "federated_user"}, + {identity.IAMPrincipalType(5), "IAMPrincipalType(5)"}, + } { + if got := tt.value.String(); got != tt.want { + t.Errorf("%T(%d).String() = %q, want %q", tt.value, tt.value, got, tt.want) + } + if got := fmt.Sprintf("%v", tt.value); got != tt.want { + t.Errorf("fmt %%v of %T = %q, want %q", tt.value, got, tt.want) + } + } +} + +func TestCallerStringNamesKindAndSourceOnly(t *testing.T) { + caller, err := identity.NewIAM("arn:aws:iam::123456789012:user/Alice", identity.SourceVerifiedIAMProof) + if err != nil { + t.Fatal(err) + } + const want = "identity.Caller{kind:iam,source:verified_iam_proof}" + if got := caller.String(); got != want { + t.Errorf("Caller.String() = %q, want %q", got, want) + } + if got := fmt.Sprintf("%#v", caller); got != want || strings.Contains(got, "Alice") || strings.Contains(got, "123456789012") { + t.Errorf("Caller GoString = %q, want %q without identifiers", got, want) + } + if got, want := (identity.Caller{}).String(), "identity.Caller{kind:anonymous,source:none}"; got != want { + t.Errorf("anonymous Caller.String() = %q, want %q", got, want) + } +}