Files
seaweedfs/weed/s3api/s3api_acl_helper.go
T
Chris LuandGitHub b452a5e41b s3: honor a bucket owner recorded as an identity (#10567)
* s3: resolve a bucket owner recorded as an identity

The admin UI and weed shell record a bucket's owner as an identity name in
s3-identity-id and never write the account id the S3 API stores alongside it,
so such a bucket looked unowned: its ACL owner fell back to the default admin
account, and under the default BucketOwnerEnforced ownership every object
uploaded to it was stamped with that account instead of the bucket owner.

Resolve the identity to its account when no account id is recorded, in the one
place both the bucket metadata and the bucket config derive the owner from.

* s3: drop the recorded account when the bucket owner is reassigned

Changing the owner of a bucket created through the S3 API left its old account
id behind, and that outranks the identity when the owner is resolved, so the
new owner never took effect for object ownership or the bucket ACL.
2026-08-04 16:00:04 -07:00

392 lines
13 KiB
Go

package s3api
import (
"encoding/json"
"encoding/xml"
"net/http"
"strings"
"github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil"
"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/s3err"
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
)
type AccountManager interface {
GetAccountNameById(canonicalId string) string
GetAccountIdByEmail(email string) string
GetAccountIdByIdentityName(name string) string
}
// ExtractAcl extracts the acl from the request body, or from the header if request body is empty
func ExtractAcl(r *http.Request, accountManager AccountManager, ownership, bucketOwnerId, ownerId, accountId string) (grants []*s3.Grant, errCode s3err.ErrorCode) {
if r.Body != nil && r.Body != http.NoBody {
defer util_http.CloseRequest(r)
var acp s3.AccessControlPolicy
err := xmlutil.UnmarshalXML(&acp, xml.NewDecoder(r.Body), "")
if err != nil || acp.Owner == nil || acp.Owner.ID == nil {
return nil, s3err.ErrInvalidRequest
}
//owner should present && owner is immutable
if *acp.Owner.ID != ownerId {
glog.V(3).Infof("set acl denied! owner account is not consistent, request account id: %s, expect account id: %s", accountId, ownerId)
return nil, s3err.ErrAccessDenied
}
return ValidateAndTransferGrants(accountManager, acp.Grants)
} else {
_, grants, errCode = ParseAndValidateAclHeadersOrElseDefault(r, accountManager, ownership, bucketOwnerId, accountId, true)
return grants, errCode
}
}
// ParseAndValidateAclHeadersOrElseDefault will callParseAndValidateAclHeaders to get Grants, if empty, it will return Grant that grant `accountId` with `FullControl` permission
func ParseAndValidateAclHeadersOrElseDefault(r *http.Request, accountManager AccountManager, ownership, bucketOwnerId, accountId string, putAcl bool) (ownerId string, grants []*s3.Grant, errCode s3err.ErrorCode) {
ownerId, grants, errCode = ParseAndValidateAclHeaders(r, accountManager, ownership, bucketOwnerId, accountId, putAcl)
if errCode != s3err.ErrNone {
return
}
if len(grants) == 0 {
//if no acl(both customAcl and cannedAcl) specified, grant accountId(object writer) with full control permission
grants = append(grants, &s3.Grant{
Grantee: &s3.Grantee{
Type: &s3_constants.GrantTypeCanonicalUser,
ID: &accountId,
},
Permission: &s3_constants.PermissionFullControl,
})
}
return
}
// ParseAndValidateAclHeaders parse and validate acl from header
func ParseAndValidateAclHeaders(r *http.Request, accountManager AccountManager, ownership, bucketOwnerId, accountId string, putAcl bool) (ownerId string, grants []*s3.Grant, errCode s3err.ErrorCode) {
ownerId, grants, errCode = ParseAclHeaders(r, ownership, bucketOwnerId, accountId, putAcl)
if errCode != s3err.ErrNone {
return
}
if len(grants) > 0 {
grants, errCode = ValidateAndTransferGrants(accountManager, grants)
}
return
}
// ParseAclHeaders parse acl headers
// When `putAcl` is true, only `CannedAcl` is parsed, such as `PutBucketAcl` or `PutObjectAcl`
// is requested, `CustomAcl` is parsed from the request body not from headers, and only if the
// request body is empty, `CannedAcl` is parsed from the header, and will not parse `CustomAcl` from the header
//
// Since `CustomAcl` has higher priority, it will be parsed first; if `CustomAcl` does not exist, `CannedAcl` will be parsed
func ParseAclHeaders(r *http.Request, ownership, bucketOwnerId, accountId string, putAcl bool) (ownerId string, grants []*s3.Grant, errCode s3err.ErrorCode) {
if !putAcl {
errCode = ParseCustomAclHeaders(r, &grants)
if errCode != s3err.ErrNone {
return "", nil, errCode
}
}
if len(grants) > 0 {
return accountId, grants, s3err.ErrNone
}
cannedAcl := r.Header.Get(s3_constants.AmzCannedAcl)
if len(cannedAcl) == 0 {
return accountId, grants, s3err.ErrNone
}
//if canned acl specified, parse cannedAcl (lower priority to custom acl)
ownerId, grants, errCode = ParseCannedAclHeader(ownership, bucketOwnerId, accountId, cannedAcl, putAcl)
if errCode != s3err.ErrNone {
return "", nil, errCode
}
return ownerId, grants, errCode
}
func ParseCustomAclHeaders(r *http.Request, grants *[]*s3.Grant) s3err.ErrorCode {
customAclHeaders := []string{s3_constants.AmzAclFullControl, s3_constants.AmzAclRead, s3_constants.AmzAclReadAcp, s3_constants.AmzAclWrite, s3_constants.AmzAclWriteAcp}
var errCode s3err.ErrorCode
for _, customAclHeader := range customAclHeaders {
headerValue := r.Header.Get(customAclHeader)
switch customAclHeader {
case s3_constants.AmzAclRead:
errCode = ParseCustomAclHeader(headerValue, s3_constants.PermissionRead, grants)
case s3_constants.AmzAclWrite:
errCode = ParseCustomAclHeader(headerValue, s3_constants.PermissionWrite, grants)
case s3_constants.AmzAclReadAcp:
errCode = ParseCustomAclHeader(headerValue, s3_constants.PermissionReadAcp, grants)
case s3_constants.AmzAclWriteAcp:
errCode = ParseCustomAclHeader(headerValue, s3_constants.PermissionWriteAcp, grants)
case s3_constants.AmzAclFullControl:
errCode = ParseCustomAclHeader(headerValue, s3_constants.PermissionFullControl, grants)
}
if errCode != s3err.ErrNone {
return errCode
}
}
return s3err.ErrNone
}
func ParseCustomAclHeader(headerValue, permission string, grants *[]*s3.Grant) s3err.ErrorCode {
if len(headerValue) > 0 {
split := strings.Split(headerValue, ", ")
for _, grantStr := range split {
kv := strings.Split(grantStr, "=")
if len(kv) != 2 {
return s3err.ErrInvalidRequest
}
switch kv[0] {
case "id":
var accountId string
_ = json.Unmarshal([]byte(kv[1]), &accountId)
*grants = append(*grants, &s3.Grant{
Grantee: &s3.Grantee{
Type: &s3_constants.GrantTypeCanonicalUser,
ID: &accountId,
},
Permission: &permission,
})
case "emailAddress":
var emailAddress string
_ = json.Unmarshal([]byte(kv[1]), &emailAddress)
*grants = append(*grants, &s3.Grant{
Grantee: &s3.Grantee{
Type: &s3_constants.GrantTypeAmazonCustomerByEmail,
EmailAddress: &emailAddress,
},
Permission: &permission,
})
case "uri":
var groupName string
_ = json.Unmarshal([]byte(kv[1]), &groupName)
*grants = append(*grants, &s3.Grant{
Grantee: &s3.Grantee{
Type: &s3_constants.GrantTypeGroup,
URI: &groupName,
},
Permission: &permission,
})
}
}
}
return s3err.ErrNone
}
func ParseCannedAclHeader(bucketOwnership, bucketOwnerId, accountId, cannedAcl string, putAcl bool) (ownerId string, grants []*s3.Grant, err s3err.ErrorCode) {
err = s3err.ErrNone
ownerId = accountId
//objectWrite automatically has full control on current object
objectWriterFullControl := &s3.Grant{
Grantee: &s3.Grantee{
ID: &accountId,
Type: &s3_constants.GrantTypeCanonicalUser,
},
Permission: &s3_constants.PermissionFullControl,
}
switch cannedAcl {
case s3_constants.CannedAclPrivate:
grants = append(grants, objectWriterFullControl)
case s3_constants.CannedAclPublicRead:
grants = append(grants, objectWriterFullControl)
grants = append(grants, s3_constants.PublicRead...)
case s3_constants.CannedAclPublicReadWrite:
grants = append(grants, objectWriterFullControl)
grants = append(grants, s3_constants.PublicReadWrite...)
case s3_constants.CannedAclAuthenticatedRead:
grants = append(grants, objectWriterFullControl)
grants = append(grants, s3_constants.AuthenticatedRead...)
case s3_constants.CannedAclLogDeliveryWrite:
grants = append(grants, objectWriterFullControl)
grants = append(grants, s3_constants.LogDeliveryWrite...)
case s3_constants.CannedAclBucketOwnerRead:
grants = append(grants, objectWriterFullControl)
if bucketOwnerId != "" && bucketOwnerId != accountId {
grants = append(grants,
&s3.Grant{
Grantee: &s3.Grantee{
Type: &s3_constants.GrantTypeCanonicalUser,
ID: &bucketOwnerId,
},
Permission: &s3_constants.PermissionRead,
})
}
case s3_constants.CannedAclBucketOwnerFullControl:
if bucketOwnerId != "" {
// if set ownership to 'BucketOwnerPreferred' when upload object, the bucket owner will be the object owner
if !putAcl && bucketOwnership == s3_constants.OwnershipBucketOwnerPreferred {
ownerId = bucketOwnerId
grants = append(grants,
&s3.Grant{
Grantee: &s3.Grantee{
Type: &s3_constants.GrantTypeCanonicalUser,
ID: &bucketOwnerId,
},
Permission: &s3_constants.PermissionFullControl,
})
} else {
grants = append(grants, objectWriterFullControl)
if accountId != bucketOwnerId {
grants = append(grants,
&s3.Grant{
Grantee: &s3.Grantee{
Type: &s3_constants.GrantTypeCanonicalUser,
ID: &bucketOwnerId,
},
Permission: &s3_constants.PermissionFullControl,
})
}
}
}
case s3_constants.CannedAclAwsExecRead:
err = s3err.ErrNotImplemented
default:
err = s3err.ErrInvalidRequest
}
return
}
// ValidateAndTransferGrants validate grant & transfer Email-Grant to Id-Grant
func ValidateAndTransferGrants(accountManager AccountManager, grants []*s3.Grant) ([]*s3.Grant, s3err.ErrorCode) {
var result []*s3.Grant
for _, grant := range grants {
grantee := grant.Grantee
if grantee == nil || grantee.Type == nil {
glog.Warning("invalid grantee! grantee or granteeType is nil")
return nil, s3err.ErrInvalidRequest
}
switch *grantee.Type {
case s3_constants.GrantTypeGroup:
if grantee.URI == nil {
glog.Warning("invalid group grantee! group URI is nil")
return nil, s3err.ErrInvalidRequest
}
ok := s3_constants.ValidateGroup(*grantee.URI)
if !ok {
glog.Warningf("invalid group grantee! group name[%s] is not valid", *grantee.URI)
return nil, s3err.ErrInvalidRequest
}
result = append(result, grant)
case s3_constants.GrantTypeCanonicalUser:
if grantee.ID == nil {
glog.Warning("invalid canonical grantee! account id is nil")
return nil, s3err.ErrInvalidRequest
}
name := accountManager.GetAccountNameById(*grantee.ID)
if len(name) == 0 {
glog.Warningf("invalid canonical grantee! account id[%s] is not exists", *grantee.ID)
return nil, s3err.ErrInvalidRequest
}
result = append(result, grant)
case s3_constants.GrantTypeAmazonCustomerByEmail:
if grantee.EmailAddress == nil {
glog.Warning("invalid email grantee! email address is nil")
return nil, s3err.ErrInvalidRequest
}
accountId := accountManager.GetAccountIdByEmail(*grantee.EmailAddress)
if len(accountId) == 0 {
glog.Warningf("invalid email grantee! email address[%s] is not exists", *grantee.EmailAddress)
return nil, s3err.ErrInvalidRequest
}
result = append(result, &s3.Grant{
Grantee: &s3.Grantee{
Type: &s3_constants.GrantTypeCanonicalUser,
ID: &accountId,
},
Permission: grant.Permission,
})
default:
return nil, s3err.ErrInvalidRequest
}
}
return result, s3err.ErrNone
}
// buildAccessControlList converts stored ACP grants into the XML response form.
// When no grants are stored it falls back to a single full-control grant for the
// owner, matching AWS's default private ACL.
func buildAccessControlList(accountManager AccountManager, grants []*s3.Grant, ownerId, ownerDisplayName string) AccessControlList {
if len(grants) == 0 {
return AccessControlList{Grant: []Grant{{
Grantee: Grantee{
ID: ownerId,
DisplayName: ownerDisplayName,
Type: "CanonicalUser",
XMLXSI: "CanonicalUser",
XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
},
Permission: Permission(s3_constants.PermissionFullControl),
}}}
}
var acl AccessControlList
for _, grant := range grants {
localGrant := Grant{Permission: Permission(*grant.Permission)}
if grant.Grantee != nil {
localGrant.Grantee = Grantee{
Type: *grant.Grantee.Type,
XMLXSI: *grant.Grantee.Type,
XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
}
if grant.Grantee.ID != nil {
localGrant.Grantee.ID = *grant.Grantee.ID
localGrant.Grantee.DisplayName = accountManager.GetAccountNameById(*grant.Grantee.ID)
}
if grant.Grantee.URI != nil {
localGrant.Grantee.URI = *grant.Grantee.URI
}
}
acl.Grant = append(acl.Grant, localGrant)
}
return acl
}
// GetAcpGrants return grants parsed from entry
func GetAcpGrants(entryExtended map[string][]byte) []*s3.Grant {
return parseAclGrants(entryExtended[s3_constants.ExtAmzAclKey])
}
func parseAclGrants(acpBytes []byte) []*s3.Grant {
if len(acpBytes) == 0 {
return nil
}
var grants []*s3.Grant
if err := json.Unmarshal(acpBytes, &grants); err == nil {
return grants
}
return nil
}
// AssembleEntryWithAcp fill entry with owner and grants
func AssembleEntryWithAcp(objectEntry *filer_pb.Entry, objectOwner string, grants []*s3.Grant) s3err.ErrorCode {
if objectEntry.Extended == nil {
objectEntry.Extended = make(map[string][]byte)
}
if len(objectOwner) > 0 {
objectEntry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(objectOwner)
} else {
delete(objectEntry.Extended, s3_constants.ExtAmzOwnerKey)
}
if len(grants) > 0 {
grantsBytes, err := json.Marshal(grants)
if err != nil {
glog.Warning("assemble acp to entry:", err)
return s3err.ErrInvalidRequest
}
objectEntry.Extended[s3_constants.ExtAmzAclKey] = grantsBytes
} else {
delete(objectEntry.Extended, s3_constants.ExtAmzAclKey)
}
return s3err.ErrNone
}