S3: map canned ACL to file permissions and add configurable default file mode (#8886)

* S3: map canned ACL to file permissions and add configurable default file mode

S3 uploads were hardcoded to 0660 regardless of ACL headers. Now the
X-Amz-Acl header maps to Unix file permissions per-object:
- public-read, authenticated-read, bucket-owner-read → 0644
- public-read-write → 0666
- private, bucket-owner-full-control → 0660

Also adds -defaultFileMode / -s3.defaultFileMode flag to set a
server-wide default when no ACL header is present.

Closes #8874

* Address review feedback for S3 file mode feature

- Extract hardcoded 0660 to defaultFileMode constant
- Change parseDefaultFileMode to return error instead of calling Fatalf
- Add -s3.defaultFileMode flag to filer.go and mini.go (was missing)
- Add doc comment to S3Options about updating all four flag sites
- Add TestResolveFileMode with 10 test cases covering ACL mapping,
  server default, and priority ordering
This commit is contained in:
Chris Lu
2026-04-02 11:51:54 -07:00
committed by GitHub
parent e93f4e3f39
commit efbed39e25
7 changed files with 89 additions and 1 deletions

View File

@@ -301,3 +301,41 @@ func TestWithObjectWriteLockSerializesConcurrentPreconditions(t *testing.T) {
t.Fatalf("expected %d precondition failures, got %d", workers-1, preconditionFailedCount)
}
}
func TestResolveFileMode(t *testing.T) {
tests := []struct {
name string
acl string
defaultFileMode uint32
expected uint32
}{
{"no acl, no default", "", 0, 0660},
{"no acl, with default", "", 0644, 0644},
{"private", s3_constants.CannedAclPrivate, 0, 0660},
{"private overrides default", s3_constants.CannedAclPrivate, 0644, 0660},
{"public-read", s3_constants.CannedAclPublicRead, 0, 0644},
{"public-read overrides default", s3_constants.CannedAclPublicRead, 0666, 0644},
{"public-read-write", s3_constants.CannedAclPublicReadWrite, 0, 0666},
{"authenticated-read", s3_constants.CannedAclAuthenticatedRead, 0, 0644},
{"bucket-owner-read", s3_constants.CannedAclBucketOwnerRead, 0, 0644},
{"bucket-owner-full-control", s3_constants.CannedAclBucketOwnerFullControl, 0, 0660},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s3a := &S3ApiServer{
option: &S3ApiServerOption{
DefaultFileMode: tt.defaultFileMode,
},
}
req := httptest.NewRequest(http.MethodPut, "/bucket/object", nil)
if tt.acl != "" {
req.Header.Set(s3_constants.AmzCannedAcl, tt.acl)
}
got := s3a.resolveFileMode(req)
if got != tt.expected {
t.Errorf("resolveFileMode() = %04o, want %04o", got, tt.expected)
}
})
}
}