Files
seaweedfs/weed/s3api/s3api_acl_helper.go
T
Chris Lu d9b69a7f76 s3api: fix PutObjectAcl permission scoping and owner grants (#11587)
PutObjectAcl had four authorization and ownership bugs:

- The handler embedded the resource path into the action
  (WriteAcp:bucket/object), and authRequest/CanDo then scoped it to
  the request's bucket/object again. A bucket-wide WriteAcp:bucket
  grant could never match, so legitimate owners got 403.
- After authRequest succeeded via an IAM or bucket policy, a leftover
  identity.CanDo gate re-checked only the legacy Actions list, denying
  identities authorized purely by policies.
- For canned and default ACLs, ExtractAcl generated the FULL_CONTROL
  grant for the requesting account instead of the object owner. An
  admin setting private/public-read on another account's object left
  the owner metadata intact but reassigned full control to the admin.
- Objects without stored owner metadata (e.g. written via the filer
  outside S3) fell back to treating the requester as the owner, so any
  user with a WriteAcp grant could take them over. Non-admins are now
  denied; admins keep the takeover fallback.

Grantee validation now also accepts the object's stored owner even when
that account has been removed from the registry, so canned/XML ACLs for
retired owners keep working.
2026-10-04 19:41:10 +08:00

412 lines
14 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
}
// aclOwnerAccountManager recognizes the stored resource owner as a valid
// grantee even if that account is no longer registered, while every other
// unknown account id is still rejected by the wrapped registry lookup.
type aclOwnerAccountManager struct {
AccountManager
ownerId string
}
func (m aclOwnerAccountManager) GetAccountNameById(canonicalId string) string {
name := m.AccountManager.GetAccountNameById(canonicalId)
if name == "" && canonicalId != "" && canonicalId == m.ownerId {
return canonicalId
}
return name
}
// 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) {
accountManager = aclOwnerAccountManager{AccountManager: accountManager, ownerId: ownerId}
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 {
// Canned and default ACLs grant FULL_CONTROL to the resource owner,
// not the requesting account: an admin updating another account's
// object must not take over its full-control grant.
_, grants, errCode = ParseAndValidateAclHeadersOrElseDefault(r, accountManager, ownership, bucketOwnerId, ownerId, 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
}