From af093721a2632cde4ad68b548095cac318cfbc4d Mon Sep 17 00:00:00 2001 From: "changlin.shi" Date: Thu, 13 Oct 2022 17:20:06 +0800 Subject: [PATCH] add acl support for `PutObject` and `PutObjectPart` --- weed/s3api/chunked_reader_v4.go | 28 ++--- weed/s3api/s3api_acp.go | 105 ++++++++++++++++++ weed/s3api/s3api_object_handlers.go | 19 +++- weed/s3api/s3api_object_multipart_handlers.go | 14 ++- weed/s3api/s3err/s3api_errors.go | 7 +- 5 files changed, 152 insertions(+), 21 deletions(-) diff --git a/weed/s3api/chunked_reader_v4.go b/weed/s3api/chunked_reader_v4.go index 8ba1bc479..f0aa30d54 100644 --- a/weed/s3api/chunked_reader_v4.go +++ b/weed/s3api/chunked_reader_v4.go @@ -59,7 +59,7 @@ func getChunkSignature(secretKey string, seedSignature string, region string, da // // returns signature, error otherwise if the signature mismatches or any other // error while parsing and validating. -func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cred *Credential, signature string, region string, date time.Time, errCode s3err.ErrorCode) { +func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (idnt *Identity, cred *Credential, signature string, region string, date time.Time, errCode s3err.ErrorCode) { // Copy request. req := *r @@ -70,7 +70,7 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr // Parse signature version '4' header. signV4Values, errCode := parseSignV4(v4Auth) if errCode != s3err.ErrNone { - return nil, "", "", time.Time{}, errCode + return nil, nil, "", "", time.Time{}, errCode } // Payload streaming. @@ -78,18 +78,18 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr // Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD' if payload != req.Header.Get("X-Amz-Content-Sha256") { - return nil, "", "", time.Time{}, s3err.ErrContentSHA256Mismatch + return nil, nil, "", "", time.Time{}, s3err.ErrContentSHA256Mismatch } // Extract all the signed headers along with its values. extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r) if errCode != s3err.ErrNone { - return nil, "", "", time.Time{}, errCode + return nil, nil, "", "", time.Time{}, errCode } // Verify if the access key id matches. identity, cred, found := iam.lookupByAccessKey(signV4Values.Credential.accessKey) if !found { - return nil, "", "", time.Time{}, s3err.ErrInvalidAccessKeyID + return nil, nil, "", "", time.Time{}, s3err.ErrInvalidAccessKeyID } bucket, object := s3_constants.GetBucketAndObject(r) @@ -105,14 +105,14 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr var dateStr string if dateStr = req.Header.Get(http.CanonicalHeaderKey("x-amz-date")); dateStr == "" { if dateStr = r.Header.Get("Date"); dateStr == "" { - return nil, "", "", time.Time{}, s3err.ErrMissingDateHeader + return nil, nil, "", "", time.Time{}, s3err.ErrMissingDateHeader } } // Parse date header. var err error date, err = time.Parse(iso8601Format, dateStr) if err != nil { - return nil, "", "", time.Time{}, s3err.ErrMalformedDate + return nil, nil, "", "", time.Time{}, s3err.ErrMalformedDate } // Query string. @@ -132,11 +132,11 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr // Verify if signature match. if !compareSignatureV4(newSignature, signV4Values.Signature) { - return nil, "", "", time.Time{}, s3err.ErrSignatureDoesNotMatch + return nil, nil, "", "", time.Time{}, s3err.ErrSignatureDoesNotMatch } // Return calculated signature. - return cred, newSignature, region, date, s3err.ErrNone + return identity, cred, newSignature, region, date, s3err.ErrNone } const maxLineLength = 4 * humanize.KiByte // assumed <= bufio.defaultBufSize 4KiB @@ -150,20 +150,20 @@ var errMalformedEncoding = errors.New("malformed chunked encoding") // newSignV4ChunkedReader returns a new s3ChunkedReader that translates the data read from r // out of HTTP "chunked" format before returning it. // The s3ChunkedReader returns io.EOF when the final 0-length chunk is read. -func (iam *IdentityAccessManagement) newSignV4ChunkedReader(req *http.Request) (io.ReadCloser, s3err.ErrorCode) { - ident, seedSignature, region, seedDate, errCode := iam.calculateSeedSignature(req) +func (iam *IdentityAccessManagement) newSignV4ChunkedReader(req *http.Request) (io.ReadCloser, *Identity, s3err.ErrorCode) { + ident, cred, seedSignature, region, seedDate, errCode := iam.calculateSeedSignature(req) if errCode != s3err.ErrNone { - return nil, errCode + return nil, nil, errCode } return &s3ChunkedReader{ - cred: ident, + cred: cred, reader: bufio.NewReader(req.Body), seedSignature: seedSignature, seedDate: seedDate, region: region, chunkSHA256Writer: sha256.New(), state: readChunkHeader, - }, s3err.ErrNone + }, ident, s3err.ErrNone } // Represents the overall state that is required for decoding a diff --git a/weed/s3api/s3api_acp.go b/weed/s3api/s3api_acp.go index 7a76c2a67..fbbf2d080 100644 --- a/weed/s3api/s3api_acp.go +++ b/weed/s3api/s3api_acp.go @@ -1,9 +1,14 @@ package s3api import ( + "github.com/aws/aws-sdk-go/service/s3" + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3account" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3acl" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" + "github.com/seaweedfs/seaweedfs/weed/util" "net/http" ) @@ -27,3 +32,103 @@ func (s3a *S3ApiServer) checkAccessByOwnership(r *http.Request, bucket string) s } return s3err.ErrAccessDenied } + +// Check Object-Write related access +// includes: +// - PutObjectHandler +// - PutObjectPartHandler +func (s3a *S3ApiServer) checkAccessForWriteObject(r *http.Request, bucket, object string) s3err.ErrorCode { + bucketMetadata, errCode := s3a.bucketRegistry.GetBucketMetadata(bucket) + if errCode != s3err.ErrNone { + return errCode + } + + accountId := s3acl.GetAccountId(r) + if bucketMetadata.ObjectOwnership == s3_constants.OwnershipBucketOwnerEnforced { + // validate grants (only bucketOwnerFullControl acl is allowed) + _, grants, errCode := s3acl.ParseAndValidateAclHeaders(r, s3a.accountManager, bucketMetadata.ObjectOwnership, *bucketMetadata.Owner.ID, accountId, false) + if errCode != s3err.ErrNone { + return errCode + } + if len(grants) > 1 { + return s3err.AccessControlListNotSupported + } + bucketOwnerFullControlGrant := &s3.Grant{ + Permission: &s3_constants.PermissionFullControl, + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: bucketMetadata.Owner.ID, + }, + } + if len(grants) == 0 { + // set default grants + s3acl.SetAcpOwnerHeader(r, accountId) + s3acl.SetAcpGrantsHeader(r, []*s3.Grant{bucketOwnerFullControlGrant}) + return s3err.ErrNone + } + + if !s3acl.GrantEquals(bucketOwnerFullControlGrant, grants[0]) { + return s3err.AccessControlListNotSupported + } + + s3acl.SetAcpOwnerHeader(r, accountId) + s3acl.SetAcpGrantsHeader(r, []*s3.Grant{bucketOwnerFullControlGrant}) + return s3err.ErrNone + } + + //bucket access allowed + bucketAclAllowed := false + if accountId == *bucketMetadata.Owner.ID { + bucketAclAllowed = true + } else { + if len(bucketMetadata.Acl) > 0 { + reqGrants := s3acl.DetermineReqGrants(accountId, s3_constants.PermissionWrite) + bucketLoop: + for _, bucketGrant := range bucketMetadata.Acl { + for _, requiredGrant := range reqGrants { + if s3acl.GrantEquals(bucketGrant, requiredGrant) { + bucketAclAllowed = true + break bucketLoop + } + } + } + } + } + if !bucketAclAllowed { + glog.V(3).Infof("acl denied! request account id: %s", accountId) + return s3err.ErrAccessDenied + } + + //object access allowed + entry, err := getObjectEntry(s3a, bucket, object) + if err != nil { + if err != filer_pb.ErrNotFound { + return s3err.ErrInternalError + } + } else { + if entry.IsDirectory { + return s3err.ErrExistingObjectIsDirectory + } + + //Only the owner of the bucket and the owner of the object can overwrite the object + objectOwner := s3acl.GetAcpOwner(entry.Extended, *bucketMetadata.Owner.ID) + if accountId != objectOwner && accountId != *bucketMetadata.Owner.ID { + glog.V(3).Infof("acl denied! request account id: %s, expect account id: %s", accountId, *bucketMetadata.Owner.ID) + return s3err.ErrAccessDenied + } + } + + ownerId, grants, errCode := s3acl.ParseAndValidateAclHeadersOrElseDefault(r, s3a.accountManager, bucketMetadata.ObjectOwnership, *bucketMetadata.Owner.ID, accountId, false) + if errCode != s3err.ErrNone { + return errCode + } + + s3acl.SetAcpOwnerHeader(r, ownerId) + s3acl.SetAcpGrantsHeader(r, grants) + + return s3err.ErrNone +} + +func getObjectEntry(s3a *S3ApiServer, bucket, object string) (*filer_pb.Entry, error) { + return s3a.getEntry(util.Join(s3a.option.BucketsPath, bucket), object) +} diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 2fc0111a4..9885303a6 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -7,6 +7,7 @@ import ( "encoding/xml" "fmt" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3account" "github.com/seaweedfs/seaweedfs/weed/security" "github.com/seaweedfs/seaweedfs/weed/util/mem" "golang.org/x/exp/slices" @@ -72,18 +73,24 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request) rAuthType := getRequestAuthType(r) if s3a.iam.isEnabled() { var s3ErrCode s3err.ErrorCode + var identity *Identity switch rAuthType { case authTypeStreamingSigned: - dataReader, s3ErrCode = s3a.iam.newSignV4ChunkedReader(r) + dataReader, identity, s3ErrCode = s3a.iam.newSignV4ChunkedReader(r) case authTypeSignedV2, authTypePresignedV2: - _, s3ErrCode = s3a.iam.isReqAuthenticatedV2(r) + identity, s3ErrCode = s3a.iam.isReqAuthenticatedV2(r) case authTypePresigned, authTypeSigned: - _, s3ErrCode = s3a.iam.reqSignatureV4Verify(r) + identity, s3ErrCode = s3a.iam.reqSignatureV4Verify(r) + case authTypeAnonymous: + identity = IdentityAnonymous } if s3ErrCode != s3err.ErrNone { s3err.WriteErrorResponse(w, r, s3ErrCode) return } + if identity.AccountId != s3account.AccountAnonymous.Id { + r.Header.Set(s3_constants.AmzAccountId, identity.AccountId) + } } else { if authTypeStreamingSigned == rAuthType { s3err.WriteErrorResponse(w, r, s3err.ErrAuthNotSetup) @@ -92,6 +99,12 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request) } defer dataReader.Close() + errCode := s3a.checkAccessForWriteObject(r, bucket, object) + if errCode != s3err.ErrNone { + s3err.WriteErrorResponse(w, r, errCode) + return + } + objectContentType := r.Header.Get("Content-Type") if strings.HasSuffix(object, "/") && r.ContentLength == 0 { if err := s3a.mkdir( diff --git a/weed/s3api/s3api_object_multipart_handlers.go b/weed/s3api/s3api_object_multipart_handlers.go index 96000310c..38e167f6b 100644 --- a/weed/s3api/s3api_object_multipart_handlers.go +++ b/weed/s3api/s3api_object_multipart_handlers.go @@ -230,23 +230,31 @@ func (s3a *S3ApiServer) PutObjectPartHandler(w http.ResponseWriter, r *http.Requ if s3a.iam.isEnabled() { rAuthType := getRequestAuthType(r) var s3ErrCode s3err.ErrorCode + var identity *Identity switch rAuthType { case authTypeStreamingSigned: - dataReader, s3ErrCode = s3a.iam.newSignV4ChunkedReader(r) + dataReader, identity, s3ErrCode = s3a.iam.newSignV4ChunkedReader(r) case authTypeSignedV2, authTypePresignedV2: - _, s3ErrCode = s3a.iam.isReqAuthenticatedV2(r) + identity, s3ErrCode = s3a.iam.isReqAuthenticatedV2(r) case authTypePresigned, authTypeSigned: - _, s3ErrCode = s3a.iam.reqSignatureV4Verify(r) + identity, s3ErrCode = s3a.iam.reqSignatureV4Verify(r) } if s3ErrCode != s3err.ErrNone { s3err.WriteErrorResponse(w, r, s3ErrCode) return } + r.Header.Set(s3_constants.AmzAccountId, identity.AccountId) } defer dataReader.Close() glog.V(2).Infof("PutObjectPartHandler %s %s %04d", bucket, uploadID, partID) + s3ErrCode := s3a.checkAccessForWriteObject(r, bucket, object) + if s3ErrCode != s3err.ErrNone { + s3err.WriteErrorResponse(w, r, s3ErrCode) + return + } + uploadUrl := fmt.Sprintf("http://%s%s/%s/%04d.part", s3a.option.Filer.ToHttpAddress(), s3a.genUploadsFolder(bucket), uploadID, partID) diff --git a/weed/s3api/s3err/s3api_errors.go b/weed/s3api/s3err/s3api_errors.go index 0348d4ddc..cdda16e87 100644 --- a/weed/s3api/s3err/s3api_errors.go +++ b/weed/s3api/s3err/s3api_errors.go @@ -109,6 +109,7 @@ const ( ErrRequestBytesExceed OwnershipControlsNotFoundError + AccessControlListNotSupported ) // error code to APIError structure, these fields carry respective @@ -416,12 +417,16 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Simultaneous request bytes exceed limitations", HTTPStatusCode: http.StatusTooManyRequests, }, - OwnershipControlsNotFoundError: { Code: "OwnershipControlsNotFoundError", Description: "The bucket ownership controls were not found", HTTPStatusCode: http.StatusNotFound, }, + AccessControlListNotSupported: { + Code: "AccessControlListNotSupported", + Description: "The bucket does not allow ACLs", + HTTPStatusCode: http.StatusBadRequest, + }, } // GetAPIError provides API Error for input API error code.