carlos / platform Public

The principal report: the sixth review's findings (Object Lambda's management actions, the "possibly" label, the legend's last spelling, the mutation paragraph as the logs have it)

Astra 1 (P2): Object Lambda's management actions are in the s3 namespace
(s3:PutAccessPointPolicyForObjectLambda, s3:*) while its access points
are arn:aws:s3-object-lambda:... resources, so such a grant passed the
S3 action test and failed both resource tests and was read as nothing.
An S3 action or an Object Lambda one on anything under the Object Lambda
prefix is a possible reach; two fixtures and a report assertion pin it.

Opus P3-2: the "possibly" label ends with "or anything else S3 or Object
Lambda names that is not a general-purpose bucket". Opus P3-3: the
legend covers CanonicalUser. Opus P3-1 and Astra 2: the note's mutation
paragraph says what each round's log holds and no more (rounds one to
three name failing tests, round four does not; round five's thirteen
with their edits and assertions; round six those thirteen again, the
Storage Lens exclusion put back and S3 actions kept out of the Object
Lambda prefix: all die, inventory-mutants-round6.txt). OUTSTANDING item
2 was reworded on the launch branch (0ebf1328).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 4a1a92d9cf1b65d14cb3846a4fbb40bd5086cd7f parent 36e5ff2
3 files changed, +21 −9
  • docs/superpowers/plans/2026-10-02-aws-principal-inventory.md +2 −2
  • internal/objstore/principals.go +10 −3
  • internal/objstore/principals_test.go +9 −4

CI passed — run details

diff --git a/docs/superpowers/plans/2026-10-02-aws-principal-inventory.md b/docs/superpowers/plans/2026-10-02-aws-principal-inventory.md
index c050927d8..97cf5362c 100644
--- a/docs/superpowers/plans/2026-10-02-aws-principal-inventory.md
+++ b/docs/superpowers/plans/2026-10-02-aws-principal-inventory.md
@@ -1,6 +1,6 @@
# The AWS principal report, and what a completeness claim needs
-Date: 2026-10-02. Revised the same day after its first review (Astra: reject; three P1s, two P2s, one P3), and on 2026-10-03 after its second (Astra: reject; six P2s) its third (Opus: accept with fixes, two P2s required; Astra: reject, four P2s, two P3s) its fourth (Opus: accept with fixes, two P2s required; Astra: reject, three P2s) and its fifth (both accept with fixes: one P2 each on the possible-reach rule's text reading, and the host role's reach in decision 14). Status: a report for a person, built on `feat/carloku-principal-inventory-20261002`. **It is not wired, and it is no longer offered as the proof a bucket's ledger needs.** Nothing was run against AWS.
+Date: 2026-10-02. Revised the same day after its first review (Astra: reject; three P1s, two P2s, one P3), and on 2026-10-03 after its second (Astra: reject; six P2s) its third (Opus: accept with fixes, two P2s required; Astra: reject, four P2s, two P3s) its fourth (Opus: accept with fixes, two P2s required; Astra: reject, three P2s), its fifth (both accept with fixes: one P2 each on the possible-reach rule's text reading, and the host role's reach in decision 14) and its sixth (Opus: accept; Astra: accept with fixes, one P2: Object Lambda's management actions, which are in the s3 namespace, on an Object Lambda resource). Status: a report for a person, built on `feat/carloku-principal-inventory-20261002`. **It is not wired, and it is no longer offered as the proof a bucket's ledger needs.** Nothing was run against AWS.
## Why this exists
@@ -27,7 +27,7 @@ Use: a person investigating who reaches a store name today, in support of the de
Tests: `go test ./internal/objstore -run 'TestPolicyPatterns|TestInventory|TestPlatformPrincipals|TestNothingOnAWS|TestAnAccessPoint'`, against a stub of the listing. No test reads a real account, and the stub is built from SDK structs, not from IAM's wire format. Both matchers read a resource pattern as text and, when it is an incomplete ARN, as padded fields too, and report a reach under either; which reading IAM gives an incomplete pattern, and whether its wildcards run across colons, is on the list to verify against a real account. The text reading over-reports, and in one common case relabels: any star before a literal an object key can hold runs into one of this bucket's keys, so a grant on `arn:aws:s3:*:123456789012:accesspoint/*` is listed as direct rather than possible, and an incomplete ARN of another service with a star (`arn:*:function:thumbs`) is listed as direct; a question mark cannot run into a key that way and is listed as possible.
-Mutation checks, in a tree copy, each a one-line edit of `principals.go` run against the inventory tests. The logs of the first four rounds (`inventory-mutants-round{1,2,3,4}.txt` in the run directory's logs) record for each mutation its description and whether the tests failed, not which test; their counts are as logged (forty after the first review, one equivalent; twelve after the second; the third's log holds the K, L and M entries with replacements for the ones that first failed to compile; ten after the fourth), and nothing more is claimed of them. The fifth round's log (`inventory-mutants-round5.txt`) records for each mutation the exact edit and the failing tests' names and first assertion, which is the evidence a kill needs; it reruns the fourth round's ten on the changed code and adds the possible-reach rule's text test (the S3 test dropped; the account form dropped; the non-colon requirement dropped; the padded reading dropped; the Object Lambda test dropped). Any that survive are named there.
+Mutation checks, in a tree copy, each a one-line edit of `principals.go` run against the inventory tests, logged in the run directory's logs as `inventory-mutants-round{1..6}.txt`. Rounds one to three record each mutation and the failing tests' names (forty after the first review, one equivalent; twelve after the second; the third's K, L and M entries, with replacements for the ones that first failed to compile); round four records ten mutations and only whether the tests failed. Round five records thirteen mutations against the changed code, each with its exact edit and the failing tests' first assertions: six on the possible-reach text test (the S3 test, the account form, the region form, the non-colon requirement, the padded reading and the Object Lambda test each dropped), the two readings dropped from the direct matcher, the three empty condition shapes accepted, a mixed group propagated as possible, and a nil page dereferenced; round four's Storage Lens exclusion, operator-with-no-key and empty-condition mutations were not rerun as such (the last two are among the thirteen in their round-five form; the exclusion has no counterpart in a rule that reads text). Round six records those thirteen again, the Storage Lens exclusion put back, and S3 actions kept out of the Object Lambda prefix. Every recorded mutation dies; the counts are as logged and nothing more is claimed of them.
## What a completeness claim needs instead (not built; needs the owner)
diff --git a/internal/objstore/principals.go b/internal/objstore/principals.go
index 3caea8e66..6e7bcb99f 100644
--- a/internal/objstore/principals.go
+++ b/internal/objstore/principals.go
@@ -146,6 +146,9 @@ type PrincipalGrant struct {
// "Federated:<name>" a web identity provider named by its domain
// (cognito-identity.amazonaws.com,
// accounts.google.com, ...), the same;
+ // "CanonicalUser:<id>" an S3 canonical user id, which a trust policy
+ // is not expected to carry (reported as read if
+ // one does);
// "*" anyone the role's conditions, if any, let in.
AssumableBy []string `json:"assumable_by,omitempty"`
}
@@ -880,7 +883,7 @@ func trustPrincipals(raw string) ([]string, error) {
// possiblyPrefix marks, in a Via entry, a reach through an S3 resource
// that is not the bucket itself and whose relation to the bucket the
// report cannot see.
-const possiblyPrefix = "possibly, through an S3 resource that is not the bucket (an access point, an Access Grants instance, a job, a Storage Lens configuration): "
+const possiblyPrefix = "possibly, through an S3 resource that is not the bucket (an access point, an Access Grants instance, a job, a Storage Lens configuration, or anything else S3 or Object Lambda names that is not a general-purpose bucket): "
// reachKind is how a document reaches a bucket: directly (the bucket's
// ARN or an object under it), or possibly, through any S3 resource that
@@ -971,7 +974,8 @@ func arnFields(pattern string) []string {
// that is not a colon (a star or a question mark can stand for that
// character, so a wildcard that runs across a colon counts); an Object
// Lambda resource is anything the pattern can name under
-// "arn:<partition>:s3-object-lambda:". A pattern that cannot reach those
+// "arn:<partition>:s3-object-lambda:", under an S3 action (its
+// management actions) or an Object Lambda one. A pattern that cannot reach those
// prefixes as text (another service's ARN, a bucket's own ARN with its
// empty region and account, a word) is not an access path.
func resourceIsAnAccessPath(pattern, partition string, s3, lambda bool) bool {
@@ -984,7 +988,10 @@ func resourceIsAnAccessPath(pattern, partition string, s3, lambda bool) bool {
if s3 && (continuesPastAColon(p, "arn:"+partition+":s3:") || continuesPastAColon(p, "arn:"+partition+":s3::")) {
return true
}
- if lambda && wildcardLive(p, "arn:"+partition+":s3-object-lambda:") != nil {
+ // Object Lambda's management actions are in the s3 namespace
+ // (s3:PutAccessPointPolicyForObjectLambda) and its data actions in
+ // its own; either kind on one of its resources is a possible reach.
+ if (s3 || lambda) && wildcardLive(p, "arn:"+partition+":s3-object-lambda:") != nil {
return true
}
}
diff --git a/internal/objstore/principals_test.go b/internal/objstore/principals_test.go
index 0ee7c7c7d..ff47d774e 100644
--- a/internal/objstore/principals_test.go
+++ b/internal/objstore/principals_test.go
@@ -425,9 +425,12 @@ func TestAnAccessPointGrantIsAPossibleReach(t *testing.T) {
// (A star in the account field can, read as text, run into the
// resource, so this pattern also reaches an object whose key holds
// ":accesspoint/": a direct reach, which is the stronger answer.)
- "any access point": {allow(`"s3:GetObject"`, `"arn:aws:s3:*:*:accesspoint/*"`), reachDirect},
- "a multi-region access point": {allow(`"s3:GetObject"`, `"arn:aws:s3::123456789012:accesspoint/mfzwi23gnjvgw.mrap/object/*"`), reachPossible},
- "an Object Lambda access point": {allow(`"s3-object-lambda:GetObject"`, `"arn:aws:s3-object-lambda:eu-west-1:123456789012:accesspoint/thumbs"`), reachPossible},
+ "any access point": {allow(`"s3:GetObject"`, `"arn:aws:s3:*:*:accesspoint/*"`), reachDirect},
+ "a multi-region access point": {allow(`"s3:GetObject"`, `"arn:aws:s3::123456789012:accesspoint/mfzwi23gnjvgw.mrap/object/*"`), reachPossible},
+ "an Object Lambda access point": {allow(`"s3-object-lambda:GetObject"`, `"arn:aws:s3-object-lambda:eu-west-1:123456789012:accesspoint/thumbs"`), reachPossible},
+ // Object Lambda's management actions are in the s3 namespace.
+ "an Object Lambda access point's policy": {allow(`"s3:PutAccessPointPolicyForObjectLambda"`, `"arn:aws:s3-object-lambda:eu-west-1:123456789012:accesspoint/readers"`), reachPossible},
+ "all of s3 on an Object Lambda access point": {allow(`"s3:*"`, `"arn:aws:s3-object-lambda:eu-west-1:123456789012:accesspoint/readers"`), reachPossible},
"Object Lambda on everything": {allow(`"s3-object-lambda:*"`, `"*"`), reachPossible},
"the bucket itself beside an access point": {allow(`"s3:GetObject"`, `["arn:aws:s3:eu-west-1:123456789012:accesspoint/readers/object/*","arn:aws:s3:::carlos-store-bac-blog/*"]`), reachDirect},
"another service's resource": {allow(`"s3:GetObject"`, `"arn:aws:lambda:eu-west-1:123456789012:function:thumbs"`), reachNone},
@@ -484,6 +487,7 @@ func TestAnAccessPointGrantIsAPossibleReach(t *testing.T) {
invUser("in-the-ap-group", []string{"ap-readers"}, nil),
invUser("in-the-direct-group", []string{"direct-readers"}, nil),
invUser("in-the-mixed-group", []string{"mixed-readers"}, nil),
+ invUser("over-an-object-lambda-policy", nil, map[string]string{"ol": allow(`"s3:PutAccessPointPolicyForObjectLambda"`, `"arn:aws:s3-object-lambda:eu-west-1:123456789012:accesspoint/readers"`)}),
},
GroupDetailList: []iamtypes.GroupDetail{
invGroup("ap-readers", map[string]string{"ap": allow(`"s3:GetObject"`, `"arn:aws:s3:eu-west-1:123456789012:accesspoint/readers/object/*"`)}),
@@ -502,7 +506,8 @@ func TestAnAccessPointGrantIsAPossibleReach(t *testing.T) {
if !reflect.DeepEqual(via[invAccount+"user/through-an-access-point"], []string{possiblyPrefix + "inline:ap"}) ||
!reflect.DeepEqual(via[invAccount+"user/in-the-ap-group"], []string{possiblyPrefix + "group:" + invAccount + "group/ap-readers"}) ||
!reflect.DeepEqual(via[invAccount+"user/in-the-direct-group"], []string{"group:" + invAccount + "group/direct-readers"}) ||
- !reflect.DeepEqual(via[invAccount+"user/in-the-mixed-group"], []string{"group:" + invAccount + "group/mixed-readers"}) {
+ !reflect.DeepEqual(via[invAccount+"user/in-the-mixed-group"], []string{"group:" + invAccount + "group/mixed-readers"}) ||
+ !reflect.DeepEqual(via[invAccount+"user/over-an-object-lambda-policy"], []string{possiblyPrefix + "inline:ol"}) {
t.Fatalf("the report: %v", via)
}
}