mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-06 06:22:05 +02:00
* s3api: persist ACLs on PutObject uploads Signed-off-by: zhaoyuchen <43179751+zhao-yc@users.noreply.github.com> * s3api: fix PutObject ACL edge cases found in review - Only enforce BucketOwnerEnforced when explicitly configured; buckets without a stored ownership control keep accepting upload ACLs - Ignore ACL query parameters on SigV2 requests, which do not sign them - Mirror signed-query ACL values into headers after authentication so grant parsing and resolveFileMode agree on presigned uploads - Validate only caller-supplied grantees against the account registry; default grants now work for accounts outside the local registry - Reject unknown grantee keys and accept comma-separated grantee lists without spaces in ParseCustomAclHeader - Guard against identities without an account * s3api: harden upload ACL parsing and authorization Signed-off-by: zhaoyuchen <43179751+zhao-yc@users.noreply.github.com> * s3api: evaluate upload ACL grantees individually in policies A comma-joined grant header or a signed query parameter reached policy conditions as one value, so a deny on a later grantee did not fire. Split grant headers into per-grantee values for policy evaluation and share the grantee pair parser with ParseCustomAclHeader. * s3api: keep raw grant header values visible to policy conditions Exact-match conditions written against the signed header value stopped matching once grantees were split for evaluation. Preserve the original wire values alongside the per-grantee values so deny policies fire on either granularity. * s3api: evaluate upload ACL grants as one canonical list in policies Conditions on s3:x-amz-grant-* now see a single comma-separated canonical grant list identical for a single line, repeated header lines, or a signed query parameter. This keeps StringEquals allows and exact-list or allowlist (StringNotEquals) denies accurate regardless of wire encoding. * s3api: preserve upload ACL denies and align policy checks Signed-off-by: zhaoyuchen <43179751+zhao-yc@users.noreply.github.com> * s3api: retain upload owner grants and literal policy values Signed-off-by: zhaoyuchen <43179751+zhao-yc@users.noreply.github.com> --------- Signed-off-by: zhaoyuchen <43179751+zhao-yc@users.noreply.github.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
425 lines
15 KiB
Go
425 lines
15 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 := strings.Join(r.Header.Values(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
|
|
}
|
|
|
|
// parseAclGranteePairs decodes a comma-separated list of quoted grantees into
|
|
// key/value pairs. Decoding each value before splitting keeps commas and equals
|
|
// signs inside quotes intact.
|
|
func parseAclGranteePairs(headerValue string) (pairs [][2]string, errCode s3err.ErrorCode) {
|
|
if headerValue == "" {
|
|
return nil, s3err.ErrNone
|
|
}
|
|
remaining := strings.TrimSpace(headerValue)
|
|
for {
|
|
key, encoded, ok := strings.Cut(remaining, "=")
|
|
if !ok {
|
|
return nil, s3err.ErrInvalidRequest
|
|
}
|
|
decoder := json.NewDecoder(strings.NewReader(encoded))
|
|
var value string
|
|
if decoder.Decode(&value) != nil || value == "" {
|
|
return nil, s3err.ErrInvalidRequest
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
switch key {
|
|
case "id", "emailAddress", "uri":
|
|
default:
|
|
return nil, s3err.ErrInvalidRequest
|
|
}
|
|
pairs = append(pairs, [2]string{key, value})
|
|
remaining = strings.TrimSpace(encoded[decoder.InputOffset():])
|
|
if remaining == "" {
|
|
break
|
|
}
|
|
if remaining[0] != ',' {
|
|
return nil, s3err.ErrInvalidRequest
|
|
}
|
|
remaining = strings.TrimSpace(remaining[1:])
|
|
}
|
|
return pairs, s3err.ErrNone
|
|
}
|
|
|
|
func ParseCustomAclHeader(headerValue, permission string, grants *[]*s3.Grant) s3err.ErrorCode {
|
|
pairs, errCode := parseAclGranteePairs(headerValue)
|
|
if errCode != s3err.ErrNone {
|
|
return errCode
|
|
}
|
|
var parsed []*s3.Grant
|
|
for i := range pairs {
|
|
grantee := &s3.Grantee{}
|
|
switch pairs[i][0] {
|
|
case "id":
|
|
grantee.Type, grantee.ID = &s3_constants.GrantTypeCanonicalUser, &pairs[i][1]
|
|
case "emailAddress":
|
|
grantee.Type, grantee.EmailAddress = &s3_constants.GrantTypeAmazonCustomerByEmail, &pairs[i][1]
|
|
case "uri":
|
|
grantee.Type, grantee.URI = &s3_constants.GrantTypeGroup, &pairs[i][1]
|
|
}
|
|
parsed = append(parsed, &s3.Grant{Grantee: grantee, Permission: &permission})
|
|
}
|
|
// Do not leave partially parsed grants behind when any list element fails.
|
|
*grants = append(*grants, parsed...)
|
|
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
|
|
}
|