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:
@@ -603,13 +603,14 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, filePath string, dataReader
|
||||
}
|
||||
|
||||
// Create entry
|
||||
fileMode := s3a.resolveFileMode(r)
|
||||
entry := &filer_pb.Entry{
|
||||
Name: path.Base(filePath),
|
||||
IsDirectory: false,
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: now.Unix(),
|
||||
Mtime: now.Unix(),
|
||||
FileMode: 0660,
|
||||
FileMode: fileMode,
|
||||
Uid: 0,
|
||||
Gid: 0,
|
||||
Mime: mimeType,
|
||||
@@ -815,6 +816,28 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, filePath string, dataReader
|
||||
return etag, s3err.ErrNone, responseMetadata
|
||||
}
|
||||
|
||||
const defaultFileMode = uint32(0660)
|
||||
|
||||
// resolveFileMode determines the file permission mode for an S3 upload.
|
||||
// Priority: per-object X-Amz-Acl header > server default > defaultFileMode.
|
||||
func (s3a *S3ApiServer) resolveFileMode(r *http.Request) uint32 {
|
||||
if cannedAcl := r.Header.Get(s3_constants.AmzCannedAcl); cannedAcl != "" {
|
||||
switch cannedAcl {
|
||||
case s3_constants.CannedAclPublicRead, s3_constants.CannedAclAuthenticatedRead,
|
||||
s3_constants.CannedAclBucketOwnerRead:
|
||||
return 0644
|
||||
case s3_constants.CannedAclPublicReadWrite:
|
||||
return 0666
|
||||
case s3_constants.CannedAclPrivate, s3_constants.CannedAclBucketOwnerFullControl:
|
||||
return defaultFileMode
|
||||
}
|
||||
}
|
||||
if s3a.option.DefaultFileMode != 0 {
|
||||
return s3a.option.DefaultFileMode
|
||||
}
|
||||
return defaultFileMode
|
||||
}
|
||||
|
||||
func setEtag(w http.ResponseWriter, etag string) {
|
||||
if etag != "" {
|
||||
if strings.HasPrefix(etag, "\"") {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ type S3ApiServerOption struct {
|
||||
BindIp string
|
||||
GrpcPort int
|
||||
ExternalUrl string // external URL clients use, for signature verification behind a reverse proxy
|
||||
DefaultFileMode uint32 // default file permission mode for S3 uploads (e.g. 0660, 0644)
|
||||
}
|
||||
|
||||
type S3ApiServer struct {
|
||||
|
||||
Reference in New Issue
Block a user