package objstore import ( "context" "encoding/json" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" ) // With no deny, the document has exactly two statements and no Condition key, // so an enforcement-off store never carries a Deny. // // This is NOT "byte for byte what it was before scanning existed": the same // change adds s3:GetObjectTagging so an app can read the verdict on its own // object, which changes the bytes by definition. Every store's policy is // rewritten once on its next grant re-walk, deliberately. func TestStorePolicyJSONShapeWithoutDeny(t *testing.T) { doc := storePolicyJSON("arn:aws:s3:::b", nil) var parsed policyDocument if err := json.Unmarshal([]byte(doc), &parsed); err != nil { t.Fatalf("not valid JSON: %v\n%s", err, doc) } // Two Allows plus the two anti-forgery Denys, which are NOT conditional on // enforcement: an app must not be able to tag its own object whether or // not this store denies reads of infected ones. The old shape asserted // "no Deny at all without enforcement", which was the belief that let the // upload-tagging route stay open. if len(parsed.Statement) != 4 { t.Fatalf("want two Allows and the two anti-forgery Denys, got %d:\n%s", len(parsed.Statement), doc) } if strings.Contains(doc, "s3:ExistingObjectTag") { t.Fatalf("a nil deny must emit no ENFORCEMENT deny:\n%s", doc) } if !strings.Contains(doc, "s3:GetObjectTagging") { t.Fatalf("an app cannot read its own verdict without GetObjectTagging:\n%s", doc) } assertNoPutObjectTagging(t, doc) } // With a deny, a third statement appears, conditioned on the engine's verdict // tag. func TestStorePolicyJSONWithDeny(t *testing.T) { doc := storePolicyJSON("arn:aws:s3:::b", &DenyTag{Key: TagVerdict, Value: string(VerdictInfected)}) var parsed policyDocument if err := json.Unmarshal([]byte(doc), &parsed); err != nil { t.Fatalf("not valid JSON: %v\n%s", err, doc) } if len(parsed.Statement) != 5 { t.Fatalf("want five statements with a deny, got %d:\n%s", len(parsed.Statement), doc) } deny := parsed.Statement[4] if deny.Effect != "Deny" || len(deny.Action) != 1 || deny.Action[0] != "s3:GetObject" { t.Fatalf("deny statement = %+v", deny) } if got, _ := deny.Condition["StringEquals"]["s3:ExistingObjectTag/"+TagVerdict].(string); got != "infected" { t.Fatalf("deny does not condition on the verdict tag: %+v", deny.Condition) } assertNoPutObjectTagging(t, doc) } // The anti-forgery property, asserted in one place — and asserted about BOTH // routes, because it turned out to have two. // // The old version of this helper searched the whole document for the string // "s3:PutObjectTagging" and failed if it appeared anywhere. That was the right // instinct aimed one level too shallow: it proved the permission was not // granted, which is not the same as proving an app cannot tag. The second // route — x-amz-tagging on the upload — names no action at all, so no search // for an action name could ever have found it. See storePolicyJSON's comment // for what a real server did with it. func assertNoPutObjectTagging(t *testing.T, doc string) { t.Helper() var parsed policyDocument if err := json.Unmarshal([]byte(doc), &parsed); err != nil { t.Fatalf("not valid JSON: %v\n%s", err, doc) } var deniesCall, deniesHeader bool for _, st := range parsed.Statement { for _, a := range st.Action { if st.Effect == "Allow" && (a == "s3:PutObjectTagging" || a == "s3:DeleteObjectTagging") { t.Fatalf("app must never be able to tag its own objects:\n%s", doc) } if st.Effect == "Deny" && a == "s3:PutObjectTagging" { deniesCall = true } } if st.Effect != "Deny" { continue } keys, _ := st.Condition["ForAnyValue:StringEquals"]["s3:RequestObjectTagKeys"].([]any) for _, k := range keys { if k == TagVerdict { deniesHeader = true } } } if !deniesCall { t.Fatalf("nothing denies the PutObjectTagging call:\n%s", doc) } if !deniesHeader { // The one a reviewer will not miss twice. t.Fatalf("nothing denies x-amz-tagging carrying %s on upload — an app can ship a payload pre-marked clean:\n%s", TagVerdict, doc) } } // The fake rewrites an EXISTING user's policy. MemProvisioner.EnsureUser // early-returns when the principal is already there, and enforcement is only // ever turned on for a store that already has one — so a fake that recorded // the policy on the create branch alone would make the next test vacuous. func TestMemProvisionerRewritesPolicyForExistingUser(t *testing.T) { ctx := context.Background() p := NewMemProvisioner("eu-west-1") if _, err := p.EnsureUser(ctx, "u", UserPath, "arn:aws:s3:::b", nil); err != nil { t.Fatal(err) } // The ENFORCEMENT deny, specifically: the anti-forgery Denys are in every // policy now, so `"Effect":"Deny"` no longer tells the two states apart. if strings.Contains(p.PolicyFor("u"), "s3:ExistingObjectTag") { t.Fatal("no enforcement deny was asked for") } if _, err := p.EnsureUser(ctx, "u", UserPath, "arn:aws:s3:::b", &DenyTag{Key: TagVerdict, Value: string(VerdictInfected)}); err != nil { t.Fatal(err) } if !strings.Contains(p.PolicyFor("u"), "s3:ExistingObjectTag") { t.Fatal("EnsureUser did not rewrite an existing user's policy") } } // Flipping enforcement on a HEALTHY, fully-delivered store must reach the // backend. // // Before EnsureUser was hoisted it could not: EnsureUser is the only // PutUserPolicy caller, Grant returned at the idempotence short-circuit before // ever reaching it, and Rotate never writes a policy at all — so enforcement // would have been permanently inert on every store it was ever turned on for. // This is the same bug internal/mail's walk fixed as "C2". func TestGrantWritesDenyOnHealthyStore(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) _, pub := testSealKeypair(t) p := NewMemProvisioner("eu-west-1") // rotateFixture leaves a fully provisioned, fully delivered store — the // exact state the short-circuit returns early on. rotateFixture(t, s, pub, p) user := UserName("bab", "titogo") // The ENFORCEMENT deny: every policy now carries the two anti-forgery // Denys, so the presence of a Deny no longer means enforcement is on. if strings.Contains(p.PolicyFor(user), "s3:ExistingObjectTag") { t.Fatal("no enforcement deny was asked for yet") } // Turn enforcement on. Read-modify-write with the real etag: Put with "" // is create-only and would return release.ErrCASConflict here. stored, etag, err := Get(ctx, s, "bab", "titogo") if err != nil || stored == nil { t.Fatalf("re-read: %+v, %v", stored, err) } stored.Scanning = &ScanBlock{ Enabled: &ScanEnabled{Engine: "clamav", At: time.Now().UTC(), By: "op@example.com"}, Enforce: &DenyTag{Key: TagVerdict, Value: string(VerdictInfected)}, } if _, err := Put(ctx, s, "bab", "titogo", stored, etag); err != nil { t.Fatal(err) } if _, err := Grant(ctx, s, p, testDeployment(), pub, "bab", "titogo", "op@example.com", GrantSourceOperator, "", time.Now().UTC()); err != nil { t.Fatal(err) } // s3:ExistingObjectTag, not `"Effect":"Deny"`: every policy carries the two // anti-forgery Denys now, so the looser check would pass with the hoist // entirely broken — a green test asserting nothing. if !strings.Contains(p.PolicyFor(user), "s3:ExistingObjectTag") { t.Fatalf("re-walking a healthy store did not write the enforcement deny — the hoist is not working:\n%s", p.PolicyFor(user)) } } // The mirror: clearing Enforce and re-walking takes the deny back out. A // `scan enforce off` that left a live Deny standing would be worse than never // having offered the switch. func TestGrantClearsDenyWhenEnforceIsCleared(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) _, pub := testSealKeypair(t) p := NewMemProvisioner("eu-west-1") rotateFixture(t, s, pub, p) user := UserName("bab", "titogo") stored, etag, err := Get(ctx, s, "bab", "titogo") if err != nil { t.Fatal(err) } stored.Scanning = &ScanBlock{Enforce: &DenyTag{Key: TagVerdict, Value: string(VerdictInfected)}} if _, err := Put(ctx, s, "bab", "titogo", stored, etag); err != nil { t.Fatal(err) } if _, err := Grant(ctx, s, p, testDeployment(), pub, "bab", "titogo", "op@example.com", GrantSourceOperator, "", time.Now().UTC()); err != nil { t.Fatal(err) } if !strings.Contains(p.PolicyFor(user), "s3:ExistingObjectTag") { t.Fatal("setup: the enforcement deny was never written") } stored, etag, err = Get(ctx, s, "bab", "titogo") if err != nil { t.Fatal(err) } stored.Scanning.Enforce = nil if _, err := Put(ctx, s, "bab", "titogo", stored, etag); err != nil { t.Fatal(err) } if _, err := Grant(ctx, s, p, testDeployment(), pub, "bab", "titogo", "op@example.com", GrantSourceOperator, "", time.Now().UTC()); err != nil { t.Fatal(err) } if strings.Contains(p.PolicyFor(user), "s3:ExistingObjectTag") { t.Fatalf("clearing Enforce left a live enforcement Deny standing:\n%s", p.PolicyFor(user)) } // And clearing enforcement must NOT have taken the anti-forgery Denys with // it — they are unconditional, and a store that stops enforcing is exactly // where a forged clean tag would go unnoticed longest. assertNoPutObjectTagging(t, p.PolicyFor(user)) }