carlos / platform Public

objstore: an app could mark its own object clean

Every verdict this platform reports rests on one asymmetry: the console
can write the carlos-scan tag and the app cannot. storePolicyJSON's
comment said so in capitals — "s3:PutObjectTagging is NOT, and MUST
NEVER BE ... that absence is the whole anti-forgery property" — and
AGENTS.md carries the same rule. The absence was real. It was also not
the whole property.

THERE ARE TWO WAYS TO PUT A TAG ON AN OBJECT. Leaving the permission out
of the Allow closes one: PutObjectTagging, the explicit call. The other
is the x-amz-tagging header on PutObject itself, which sets tags during
the upload and never names the tagging action at all. No search for an
action name can find it, which is exactly why three reviews and a
purpose-built helper missed it — assertNoPutObjectTagging grepped the
document for the string "s3:PutObjectTagging" and passed.

Against MinIO RELEASE.2025-09-07T16-13-09Z, stood up from CI's own
contract fixture and given today's policy verbatim, the upload succeeded
with carlos-scan=clean attached.

What that buys an attacker: the sweep reads the tag before scanning and
skips any object whose tagged version matches the current one
(clamav.go's Verdict). The version is etag@last-modified — the etag is
the app's own MD5 and the last-modified is the server clock at upload,
so an app can upload, HEAD its own object, and retry until the stamp it
guessed matches. The object is then never scanned, and `carlos store
scan scan status` reports it clean, permanently. The audit sentence the
whole feature exists to produce becomes a lie the platform tells
confidently.

THE OBVIOUS FIX DOES NOT WORK, which is the part worth remembering.
Adding an explicit Deny on s3:PutObjectTagging and s3:DeleteObjectTagging
was the first thing tried and the upload still succeeded: the gateway
never attributed the header to that action. Only denying s3:PutObject
itself, conditioned on the request carrying one of our tag keys
(ForAnyValue:StringEquals on s3:RequestObjectTagKeys), closed it. Both
Denys are here — the first costs nothing where the permission was
already absent, and it is the one AWS's own documentation says should
have sufficed.

Unconditional, not gated on enforcement. A store that is not enforcing
is exactly where a forged clean tag would sit unnoticed longest, and
these statements have nothing to do with whether infected objects are
readable.

The tests had encoded the old belief in five places — "two statements
when enforcement is off", `"Effect":"Deny"` used as a proxy for
"enforcement is on", and the grep helper. All five now say what they
meant. assertNoPutObjectTagging checks both routes and is called from
the AWS provisioner's assertion too, so a policy change cannot pass by
only satisfying one file.

TestDeliveredContractMinio gains the live half, against the real server
CI already runs on every push (.amadan/ci.d/30-minio-contract): the
tagging route in both forms, and — the other thing that could only be
settled against a real server — that HeadObject and ListObjectsV2 agree
on the version string. They do. The sweep binds its tag to the LIST
version and the status door heads the object to find its own, so a
gateway that reported those differently would make every sweep verdict
read as unknown and put the two paths into a rescan loop. That question
was going to be answered by deploying; it is answered by CI instead, on
every push rather than once.

Verified end to end: the policy the code generates, handed to MinIO
verbatim as the scoped user's policy, and the full contract test green
against it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEkJh1ESqiA84quAHJV4K6
Paul Campbell pushed by paul@keymail.dev 5065c999df9468d8636246e754e4834b8d1f1c79 parent 8cec4dd
4 files changed, +263 −35
  • internal/objstore/aws.go +54 −8
  • internal/objstore/aws_test.go +10 −4
  • internal/objstore/contract_test.go +129 −0
  • internal/objstore/enforce_test.go +70 −23
diff --git a/internal/objstore/aws.go b/internal/objstore/aws.go
index c0db4317d..438821bde 100644
--- a/internal/objstore/aws.go
+++ b/internal/objstore/aws.go
@@ -113,9 +113,12 @@ type policyStatement struct {
Effect string `json:"Effect"`
Action []string `json:"Action"`
Resource string `json:"Resource"`
- // Condition is omitted entirely when nil, so a store with no enforcement
- // carries exactly the two Allow statements and no Condition key at all.
- Condition map[string]map[string]string `json:"Condition,omitempty"`
+ // Condition is omitted entirely when nil. The value side is `any` because
+ // IAM condition values are sometimes a string (StringEquals on one tag)
+ // and sometimes a list (ForAnyValue:StringEquals over tag KEYS); a
+ // map[string]string could only express the first, and the second is what
+ // closes the upload-tagging route below.
+ Condition map[string]map[string]any `json:"Condition,omitempty"`
}
type policyDocument struct {
@@ -129,10 +132,34 @@ type policyDocument struct {
// infected (spec §8).
//
// s3:GetObjectTagging is in the Allow so an app can read the verdict on its
-// own object. s3:PutObjectTagging is NOT, and MUST NEVER BE: the deny below
-// is only meaningful while an app cannot mark its own object clean. That
-// absence is the whole anti-forgery property, and it is load-bearing rather
-// than an oversight.
+// own object. s3:PutObjectTagging is NOT, and MUST NEVER BE: every verdict
+// this platform reports is only worth anything while an app cannot mark its
+// own object clean.
+//
+// ABSENCE IS NOT ENOUGH, which cost this design a real hole. There are TWO
+// ways to put a tag on an object, and leaving the permission out of the Allow
+// only closes one of them: PutObjectTagging, the explicit call. The other is
+// the `x-amz-tagging` header on PutObject itself, which sets tags during the
+// upload without ever naming the tagging action. AWS documents that header as
+// requiring s3:PutObjectTagging; a gateway that does not enforce that reading
+// hands any app with s3:PutObject the ability to upload a payload already
+// marked clean — and the sweep, finding a version-matching clean tag, will
+// never scan it. `carlos store scan status` then reports it clean forever.
+//
+// This is not hypothetical. Against MinIO RELEASE.2025-09-07T16-13-09Z, run
+// from CI's own contract fixture (.amadan/ci.d/30-minio-contract), the upload
+// succeeded with the tag attached. Adding an explicit Deny on
+// s3:PutObjectTagging — the obvious fix, and the first one tried — did NOT
+// close it, because the gateway never attributed the header to that action.
+// Only denying s3:PutObject itself when the request carries one of our tag
+// keys did. Both Denys are therefore below, unconditionally, for every store
+// whether or not enforcement is on: an explicit Deny costs nothing where the
+// permission was already absent, and it is the only form that travels to a
+// backend whose IAM does not read the header the way AWS's does.
+//
+// TestDeliveredContractMinio holds the live half of this against a real
+// server on every push. Do not weaken either statement without watching that
+// test fail first.
//
// Adding GetObjectTagging changes this document for every existing store,
// which is picked up on the next grant re-walk. That is a deliberate one-time
@@ -155,12 +182,31 @@ func storePolicyJSON(bucketARN string, deny *DenyTag) string {
}, Resource: bucketARN + "/*"},
},
}
+ // The two anti-forgery Denys, always. See above for why the second one
+ // exists and why the first is not sufficient on its own.
+ doc.Statement = append(doc.Statement,
+ policyStatement{
+ Effect: "Deny",
+ Action: []string{"s3:PutObjectTagging", "s3:DeleteObjectTagging"},
+ Resource: bucketARN + "/*",
+ },
+ policyStatement{
+ Effect: "Deny",
+ Action: []string{"s3:PutObject"},
+ Resource: bucketARN + "/*",
+ Condition: map[string]map[string]any{
+ "ForAnyValue:StringEquals": {
+ "s3:RequestObjectTagKeys": []string{TagVerdict, TagVersion, TagAt},
+ },
+ },
+ },
+ )
if deny != nil {
doc.Statement = append(doc.Statement, policyStatement{
Effect: "Deny",
Action: []string{"s3:GetObject"},
Resource: bucketARN + "/*",
- Condition: map[string]map[string]string{
+ Condition: map[string]map[string]any{
"StringEquals": {"s3:ExistingObjectTag/" + deny.Key: deny.Value},
},
})
diff --git a/internal/objstore/aws_test.go b/internal/objstore/aws_test.go
index 053208e14..f8f5efa31 100644
--- a/internal/objstore/aws_test.go
+++ b/internal/objstore/aws_test.go
@@ -540,8 +540,12 @@ func assertStorePolicyDocument(t *testing.T, doc, bucketARN string) {
if parsed.Version != "2012-10-17" {
t.Fatalf("Version = %q", parsed.Version)
}
- if len(parsed.Statement) != 2 {
- t.Fatalf("Statement has %d entries, want 2:\n%s", len(parsed.Statement), doc)
+ // Two Allows, then the two anti-forgery Denys that every store carries
+ // whether or not enforcement is on. This helper checks the Allow half
+ // exactly; assertNoPutObjectTagging (enforce_test.go) owns the Deny half,
+ // including the x-amz-tagging route that an action-name search cannot see.
+ if len(parsed.Statement) != 4 {
+ t.Fatalf("Statement has %d entries, want 4 (two Allows, two anti-forgery Denys):\n%s", len(parsed.Statement), doc)
}
list, objects := parsed.Statement[0], parsed.Statement[1]
if list.Effect != "Allow" || len(list.Action) != 1 || list.Action[0] != "s3:ListBucket" || list.Resource != bucketARN {
@@ -549,8 +553,9 @@ func assertStorePolicyDocument(t *testing.T, doc, bucketARN string) {
}
// s3:GetObjectTagging is here so an app can read the scan verdict on its
// own object. s3:PutObjectTagging is deliberately absent and must stay
- // absent: the scanning Deny statement is only meaningful while an app
- // cannot mark its own object clean.
+ // absent — but absence alone was never the whole property, and believing
+ // it was left the x-amz-tagging upload route open. storePolicyJSON's
+ // comment has the story; the Denys are what actually close it.
wantActions := map[string]bool{
"s3:GetObject": true, "s3:PutObject": true, "s3:DeleteObject": true,
"s3:GetObjectTagging": true,
@@ -563,6 +568,7 @@ func assertStorePolicyDocument(t *testing.T, doc, bucketARN string) {
t.Fatalf("unexpected action %q in object statement", a)
}
}
+ assertNoPutObjectTagging(t, doc)
}
// smithyAPIErrorStub is a minimal smithy.APIError for exercising the code
diff --git a/internal/objstore/contract_test.go b/internal/objstore/contract_test.go
index ae328d283..495f1c483 100644
--- a/internal/objstore/contract_test.go
+++ b/internal/objstore/contract_test.go
@@ -107,6 +107,117 @@ func TestDeliveredContractMinio(t *testing.T) {
}
})
+ // THE VERSION A VERDICT BINDS TO HAS TO BE THE SAME STRING FROM BOTH
+ // DOORS, and this is the only place that can be checked against a real
+ // server rather than argued about.
+ //
+ // The sweep never calls HeadObject: it takes ETag, size and last-modified
+ // off the listing page (sweep.go:31) and binds its tag to that
+ // ScanObject.Version(). Status does the opposite — it heads the object to
+ // find the current version for itself (clamav.go:199) — and then compares
+ // the tag it read against ITS version, character for character
+ // (`taggedVersion != obj.Version()`).
+ //
+ // So if a gateway reports last-modified differently on the two calls —
+ // rounding one where it truncates the other — a tag the sweep just wrote
+ // reads as "nothing is known" to every door, the drain rescans, re-tags
+ // with the Head version, and the next sweep finds THAT stale and rescans
+ // again. A scan ping-pong that costs money and never converges, from two
+ // strings that differ in a second.
+ //
+ // RFC3339 truncates, so the two agree unless the server rounds. That is a
+ // narrow window, which is exactly why it wants a test and not a memo.
+ t.Run("head and list agree on the version", func(t *testing.T) {
+ lst, err := client.ListObjectsV2(ctx, &awss3.ListObjectsV2Input{
+ Bucket: aws.String(bucket),
+ Prefix: aws.String(objectKey),
+ })
+ if err != nil {
+ t.Fatalf("ListObjectsV2: %v", err)
+ }
+ var listed ScanObject
+ for _, o := range lst.Contents {
+ if aws.ToString(o.Key) != objectKey {
+ continue
+ }
+ // Exactly what s3Lister.ListPage builds (sweep.go).
+ listed = ScanObject{Key: objectKey, ETag: normalizeETag(aws.ToString(o.ETag))}
+ if o.LastModified != nil {
+ listed.Modified = *o.LastModified
+ }
+ if o.Size != nil {
+ listed.Size = *o.Size
+ }
+ }
+ if listed.ETag == "" {
+ t.Fatalf("ListObjectsV2 did not return %q", objectKey)
+ }
+
+ head, err := client.HeadObject(ctx, &awss3.HeadObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(objectKey),
+ })
+ if err != nil {
+ t.Fatalf("HeadObject: %v", err)
+ }
+ // Exactly what clamavScanner.Status builds (clamav.go:212-219).
+ headed := ScanObject{Key: objectKey, ETag: normalizeETag(aws.ToString(head.ETag))}
+ if head.LastModified != nil {
+ headed.Modified = *head.LastModified
+ }
+
+ if listed.Version() != headed.Version() {
+ t.Fatalf("the sweep and the status door would bind different versions to the same bytes:\n list: %s\n head: %s\nEvery verdict the sweep writes reads as unknown, forever.", listed.Version(), headed.Version())
+ }
+ // Size only comes from the listing in the sweep's path, but Status
+ // reports it too and the drain uses it to decide the ceiling.
+ if head.ContentLength != nil && listed.Size != *head.ContentLength {
+ t.Fatalf("size disagrees: list %d, head %d", listed.Size, *head.ContentLength)
+ }
+ })
+
+ // THE ANTI-FORGERY PROPERTY, against a real IAM rather than a policy
+ // document. Everything about a clean verdict rests on one asymmetry: the
+ // console can write the carlos-scan tag and the app cannot. The app's
+ // Allow has no s3:PutObjectTagging and never will (aws.go's policy, and
+ // the standing rule in AGENTS.md) — but "the permission is absent" is only
+ // half a proof. The other half is that the server actually requires it,
+ // by BOTH routes, and that half has never been exercised anywhere.
+ //
+ // The second route is the one that would be missed. x-amz-tagging on a
+ // PutObject sets tags without any call named PutObjectTagging, and an
+ // implementation that only gates the explicit call would hand an app the
+ // ability to upload a payload pre-marked clean.
+ t.Run("the app cannot tag its own object", func(t *testing.T) {
+ t.Run("not by PutObjectTagging", func(t *testing.T) {
+ _, err := client.PutObjectTagging(ctx, &awss3.PutObjectTaggingInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(objectKey),
+ Tagging: &s3types.Tagging{TagSet: []s3types.Tag{
+ {Key: aws.String("carlos-scan"), Value: aws.String("clean")},
+ }},
+ })
+ requireDenied(t, err, "PutObjectTagging")
+ })
+
+ t.Run("and not by x-amz-tagging on upload", func(t *testing.T) {
+ _, err := client.PutObject(ctx, &awss3.PutObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String("contract-test/pre-tagged.txt"),
+ Body: bytes.NewReader([]byte("payload claiming to be clean")),
+ Tagging: aws.String("carlos-scan=clean"),
+ })
+ // Clean up if it somehow succeeded, so the delete subtest below
+ // is not reasoning about a bucket this one left dirty.
+ if err == nil {
+ _, _ = client.DeleteObject(ctx, &awss3.DeleteObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String("contract-test/pre-tagged.txt"),
+ })
+ }
+ requireDenied(t, err, "PutObject with x-amz-tagging")
+ })
+ })
t.Run("delete removes it", func(t *testing.T) {
_, err := client.DeleteObject(ctx, &awss3.DeleteObjectInput{
Bucket: aws.String(bucket),
@@ -176,3 +287,21 @@ func keyNames(objs []s3types.Object) []string {
}
return names
}
+
+// requireDenied is the assertion both tagging routes need: not merely "it
+// failed", but AccessDenied specifically. A malformed request, a 404 or a
+// signature problem would also return an error, and any of them would let a
+// broken policy pass this test while proving nothing about permission.
+func requireDenied(t *testing.T, err error, what string) {
+ t.Helper()
+ if err == nil {
+ t.Fatalf("%s SUCCEEDED — an app can mark its own object clean, and every clean verdict this deployment reports is forgeable", what)
+ }
+ var apiErr smithy.APIError
+ if !errors.As(err, &apiErr) {
+ t.Fatalf("%s: err = %v (%T), want a smithy.APIError carrying AccessDenied", what, err, err)
+ }
+ if apiErr.ErrorCode() != "AccessDenied" {
+ t.Fatalf("%s: error code = %q, want AccessDenied (message: %s)", what, apiErr.ErrorCode(), apiErr.ErrorMessage())
+ }
+}
diff --git a/internal/objstore/enforce_test.go b/internal/objstore/enforce_test.go
index 7f35baaeb..9e64f6de0 100644
--- a/internal/objstore/enforce_test.go
+++ b/internal/objstore/enforce_test.go
@@ -24,15 +24,17 @@ func TestStorePolicyJSONShapeWithoutDeny(t *testing.T) {
if err := json.Unmarshal([]byte(doc), &parsed); err != nil {
t.Fatalf("not valid JSON: %v\n%s", err, doc)
}
- if len(parsed.Statement) != 2 {
- t.Fatalf("want exactly two statements without a deny, got %d:\n%s",
+ // 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, "Condition") {
- t.Fatalf("a nil deny must emit no Condition key:\n%s", doc)
- }
- if strings.Contains(doc, `"Effect":"Deny"`) {
- t.Fatalf("a nil deny must emit no Deny statement:\n%s", 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)
@@ -50,26 +52,63 @@ func TestStorePolicyJSONWithDeny(t *testing.T) {
if err := json.Unmarshal([]byte(doc), &parsed); err != nil {
t.Fatalf("not valid JSON: %v\n%s", err, doc)
}
- if len(parsed.Statement) != 3 {
- t.Fatalf("want three statements with a deny, got %d:\n%s", len(parsed.Statement), 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[2]
+ 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]; got != "infected" {
+ 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: the app must never be able
-// to tag its own objects, or it could mark an infected object clean and walk
-// straight through the deny.
+// 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()
- if strings.Contains(doc, "s3:PutObjectTagging") {
- t.Fatalf("app must never be able to tag its own objects:\n%s", doc)
+
+ 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)
}
}
@@ -84,14 +123,16 @@ func TestMemProvisionerRewritesPolicyForExistingUser(t *testing.T) {
if _, err := p.EnsureUser(ctx, "u", UserPath, "arn:aws:s3:::b", nil); err != nil {
t.Fatal(err)
}
- if strings.Contains(p.PolicyFor("u"), `"Effect":"Deny"`) {
- t.Fatal("no deny was asked for")
+ // 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"), `"Effect":"Deny"`) {
+ if !strings.Contains(p.PolicyFor("u"), "s3:ExistingObjectTag") {
t.Fatal("EnsureUser did not rewrite an existing user's policy")
}
}
@@ -114,8 +155,10 @@ func TestGrantWritesDenyOnHealthyStore(t *testing.T) {
// exact state the short-circuit returns early on.
rotateFixture(t, s, pub, p)
user := UserName("bab", "titogo")
- if strings.Contains(p.PolicyFor(user), `"Effect":"Deny"`) {
- t.Fatal("no deny was asked for yet")
+ // 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 ""
@@ -181,7 +224,11 @@ func TestGrantClearsDenyWhenEnforceIsCleared(t *testing.T) {
"bab", "titogo", "op@example.com", "", time.Now().UTC()); err != nil {
t.Fatal(err)
}
- if strings.Contains(p.PolicyFor(user), `"Effect":"Deny"`) {
- t.Fatalf("clearing Enforce left a live Deny standing:\n%s", p.PolicyFor(user))
+ 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))
}