Skip to content

Move bucket ownership to bucket settings metaobject #1134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ This document outlines major changes between releases.

### Changed
- AWS SDK migrated to V2 (#1028)
- Bucket ownership settings moved from EACL to bucket settings meta objects (#1120)

### Fixed

### Updated

### Removed

## Upgrading from 0.36.1
authmate tool "reset-bucket-acl" command was updated to clean redundant EACL records. These settings were moved to
bucket settings object.

## [0.36.1] - 2025-04-09

### Fixed
Expand Down
15 changes: 15 additions & 0 deletions api/data/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,21 @@ const (
VersioningSuspended = "Suspended"
)

const (
// BucketOwnerEnforced is a enforced state.
BucketOwnerEnforced = iota
// BucketOwnerPreferred is a preferred state.
BucketOwnerPreferred
// BucketOwnerPreferredAndRestricted is a preferred state with `bucket-owner-full-control` restriction applied.
BucketOwnerPreferredAndRestricted
// BucketOwnerObjectWriter is a object writer state.
BucketOwnerObjectWriter
)

type (
// BucketOwner is bucket onwer state.
BucketOwner int

// BucketACLState is bucket ACL state.
BucketACLState uint32

Expand Down Expand Up @@ -78,6 +92,7 @@ type (
BucketSettings struct {
Versioning string `json:"versioning"`
LockConfiguration *ObjectLockConfiguration `json:"lock_configuration"`
BucketOwner BucketOwner `json:"bucket_owner"`
}

// CORSConfiguration stores CORS configuration of a request.
Expand Down
218 changes: 43 additions & 175 deletions api/handler/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,13 +294,13 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
return
}

eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo)
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
if err != nil {
h.logAndSendError(w, "could not get bucket eacl", reqInfo, err)
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
return
}

if IsBucketOwnerForced(eacl.EACL) {
if settings.BucketOwner == data.BucketOwnerEnforced {
if !isValidOwnerEnforced(r) {
h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported))
return
Expand Down Expand Up @@ -425,21 +425,21 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
return
}

eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo)
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
if err != nil {
h.logAndSendError(w, "could not get bucket eacl", reqInfo, err)
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
return
}

if IsBucketOwnerForced(eacl.EACL) {
if settings.BucketOwner == data.BucketOwnerEnforced {
if !isValidOwnerEnforced(r) {
h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported))
return
}
r.Header.Set(api.AmzACL, "")
}

if IsBucketOwnerPreferredAndRestricted(eacl.EACL) {
if settings.BucketOwner == data.BucketOwnerPreferredAndRestricted {
if !isValidOwnerPreferred(r) {
h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied))
return
Expand Down Expand Up @@ -567,6 +567,41 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
return
}

var index = -1

// This bucket setting is a part of ACL. It not useful For us inside EACL, therefore we remove it from the table.
for i, res := range astPolicy.Resources {
if res.Version == cannedACLBucketOwnerFullControl &&
len(res.Operations) == 1 &&
len(res.Operations[0].Users) == 0 &&
res.Operations[0].Users[0] == ownerPreferredAndRestrictedUserID {
index = i
break
}
}

if index >= 0 {
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
if err != nil {
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
return
}

settings.BucketOwner = data.BucketOwnerPreferredAndRestricted

p := layer.PutSettingsParams{
BktInfo: bktInfo,
Settings: settings,
}

if err = h.obj.PutBucketSettings(r.Context(), &p); err != nil {
h.logAndSendError(w, "couldn't put bucket settings", reqInfo, err)
return
}

astPolicy.Resources = slices.Delete(astPolicy.Resources, index, index+1)
}

if _, err = h.updateBucketACL(r, astPolicy, bktInfo, token); err != nil {
h.logAndSendError(w, "could not update bucket acl", reqInfo, err)
return
Expand Down Expand Up @@ -1024,26 +1059,6 @@ func formRecords(resource *astResource) ([]*eacl.Record, error) {
for i := len(resource.Operations) - 1; i >= 0; i-- {
astOp := resource.Operations[i]

if len(astOp.Users) == 1 {
var markerRecord *eacl.Record

switch astOp.Users[0] {
case ownerEnforcedUserID:
markerRecord = BucketOwnerEnforcedRecord()
case ownerPreferredUserID:
markerRecord = BucketOwnerPreferredRecord()
case ownerObjectWriterUserID:
markerRecord = BucketACLObjectWriterRecord()
case ownerPreferredAndRestrictedUserID:
markerRecord = BucketOwnerPreferredAndRestrictedRecord()
}

if markerRecord != nil {
res = append(res, markerRecord)
return res, nil
}
}

record := eacl.ConstructRecord(astOp.Action, astOp.Op, []eacl.Target{})
if astOp.IsGroupGrantee() {
record.SetTargets(eacl.NewTargetByRole(eacl.RoleOthers))
Expand Down Expand Up @@ -1696,8 +1711,6 @@ func bucketACLToTable(acp *AccessControlPolicy) (*eacl.Table, error) {
records = append(records, *getOthersRecord(op, eacl.ActionDeny))
}

records = append(records, *BucketOwnerEnforcedRecord())

table := eacl.ConstructTable(records)

return &table, nil
Expand Down Expand Up @@ -1737,20 +1750,6 @@ func getOthersRecord(op eacl.Operation, action eacl.Action) *eacl.Record {
return &record
}

// BucketOwnerEnforcedRecord generates special marker record for OwnerEnforced policy.
func BucketOwnerEnforcedRecord() *eacl.Record {
var markerRecord = eacl.ConstructRecord(eacl.ActionDeny, eacl.OperationPut,
[]eacl.Target{
eacl.NewTargetByAccounts([]user.ID{ownerEnforcedUserID}),
},
[]eacl.Filter{
eacl.ConstructFilter(eacl.HeaderFromRequest, amzBucketOwnerField, eacl.MatchStringNotEqual, amzBucketOwnerEnforced),
}...,
)

return &markerRecord
}

func isValidOwnerEnforced(r *http.Request) bool {
if cannedACL := r.Header.Get(api.AmzACL); cannedACL != "" {
switch cannedACL {
Expand All @@ -1766,120 +1765,12 @@ func isValidOwnerEnforced(r *http.Request) bool {
return true
}

// BucketACLObjectWriterRecord generates special marker record for OwnerWriter policy.
func BucketACLObjectWriterRecord() *eacl.Record {
var markerRecord = eacl.ConstructRecord(eacl.ActionDeny, eacl.OperationPut,
[]eacl.Target{eacl.NewTargetByAccounts([]user.ID{ownerObjectWriterUserID})},
[]eacl.Filter{
eacl.ConstructFilter(eacl.HeaderFromRequest, amzBucketOwnerField, eacl.MatchStringNotEqual, amzBucketOwnerObjectWriter),
}...,
)

return &markerRecord
}

// IsBucketOwnerForced checks special marker record for OwnerForced policy.
func IsBucketOwnerForced(table *eacl.Table) bool {
if table == nil {
return false
}

for _, r := range table.Records() {
if r.Action() == eacl.ActionDeny && r.Operation() == eacl.OperationPut {
for _, f := range r.Filters() {
if f.Key() == amzBucketOwnerField &&
f.Value() == amzBucketOwnerEnforced &&
f.From() == eacl.HeaderFromRequest &&
f.Matcher() == eacl.MatchStringNotEqual {
if len(r.Targets()) == 1 && len(r.Targets()[0].Accounts()) == 1 {
return r.Targets()[0].Accounts()[0] == ownerEnforcedUserID
}
}
}
}
}

return false
}

// BucketOwnerPreferredRecord generates special marker record for OwnerPreferred policy.
func BucketOwnerPreferredRecord() *eacl.Record {
var markerRecord = eacl.ConstructRecord(eacl.ActionDeny, eacl.OperationPut,
[]eacl.Target{eacl.NewTargetByAccounts([]user.ID{ownerPreferredUserID})},
[]eacl.Filter{
eacl.ConstructFilter(eacl.HeaderFromRequest, amzBucketOwnerField, eacl.MatchStringNotEqual, amzBucketOwnerPreferred),
}...,
)

return &markerRecord
}

// IsBucketOwnerPreferred checks special marker record for OwnerPreferred policy.
func IsBucketOwnerPreferred(table *eacl.Table) bool {
if table == nil {
return false
}

for _, r := range table.Records() {
if r.Action() == eacl.ActionDeny && r.Operation() == eacl.OperationPut {
for _, f := range r.Filters() {
if f.Key() == amzBucketOwnerField &&
f.Value() == amzBucketOwnerPreferred &&
f.From() == eacl.HeaderFromRequest &&
f.Matcher() == eacl.MatchStringNotEqual {
if len(r.Targets()) == 1 && len(r.Targets()[0].Accounts()) == 1 {
return r.Targets()[0].Accounts()[0] == ownerPreferredUserID
}
}
}
}
}

return false
}

// BucketOwnerPreferredRecord generates special marker record for OwnerPreferred policy and sets flag for bucket owner full control acl restriction.
func BucketOwnerPreferredAndRestrictedRecord() *eacl.Record {
var markerRecord = eacl.ConstructRecord(eacl.ActionDeny, eacl.OperationPut,
[]eacl.Target{eacl.NewTargetByAccounts([]user.ID{ownerPreferredAndRestrictedUserID})},
[]eacl.Filter{
eacl.ConstructFilter(eacl.HeaderFromObject, amzBucketOwnerField, eacl.MatchStringEqual, cannedACLBucketOwnerFullControl),
}...,
)

return &markerRecord
}

// IsBucketOwnerPreferredAndRestricted checks special marker record and check ALC bucket owner full control flag for OwnerPreferred policy.
func IsBucketOwnerPreferredAndRestricted(table *eacl.Table) bool {
if table == nil {
return false
}

for _, r := range table.Records() {
if r.Action() == eacl.ActionDeny && r.Operation() == eacl.OperationPut {
for _, f := range r.Filters() {
if f.Key() == amzBucketOwnerField &&
f.Value() == cannedACLBucketOwnerFullControl &&
f.From() == eacl.HeaderFromObject &&
f.Matcher() == eacl.MatchStringEqual {
if len(r.Targets()) == 1 && len(r.Targets()[0].Accounts()) == 1 {
return r.Targets()[0].Accounts()[0] == ownerPreferredAndRestrictedUserID
}
}
}
}
}

return false
}

func isValidOwnerPreferred(r *http.Request) bool {
cannedACL := r.Header.Get(api.AmzACL)
return cannedACL == cannedACLBucketOwnerFullControl
}

func updateBucketOwnership(records []eacl.Record, newRecord *eacl.Record) eacl.Table {
func UpdateBucketOwnership(records []eacl.Record, newRecord *eacl.Record) eacl.Table {
var (
rowID = -1
)
Expand Down Expand Up @@ -1911,26 +1802,3 @@ func updateBucketOwnership(records []eacl.Record, newRecord *eacl.Record) eacl.T

return eacl.ConstructTable(records)
}

func isBucketOwnerObjectWriter(table *eacl.Table) bool {
if table == nil {
return false
}

for _, r := range table.Records() {
if r.Action() == eacl.ActionDeny && r.Operation() == eacl.OperationPut {
for _, f := range r.Filters() {
if f.Key() == amzBucketOwnerField &&
f.Value() == amzBucketOwnerObjectWriter &&
f.From() == eacl.HeaderFromRequest &&
f.Matcher() == eacl.MatchStringNotEqual {
if len(r.Targets()) == 1 && len(r.Targets()[0].Accounts()) == 1 {
return r.Targets()[0].Accounts()[0] == ownerObjectWriterUserID
}
}
}
}
}

return false
}
1 change: 0 additions & 1 deletion api/handler/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1182,7 +1182,6 @@ func TestBucketAclToTable(t *testing.T) {
for _, op := range fullOps {
records = append(records, *getOthersRecord(op, eacl.ActionDeny))
}
records = append(records, *BucketOwnerEnforcedRecord())

actualTable, err := bucketACLToTable(acl)
require.NoError(t, err)
Expand Down
10 changes: 2 additions & 8 deletions api/handler/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,8 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
return
}

eacl, err := h.obj.GetBucketACL(r.Context(), dstBktInfo)
if err != nil {
h.logAndSendError(w, "could not get bucket eacl", reqInfo, err)
return
}

if containsACL {
if IsBucketOwnerForced(eacl.EACL) {
if settings.BucketOwner == data.BucketOwnerEnforced {
if !isValidOwnerEnforced(r) {
h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported))
return
Expand All @@ -144,7 +138,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
}
}

if IsBucketOwnerPreferredAndRestricted(eacl.EACL) {
if settings.BucketOwner == data.BucketOwnerPreferredAndRestricted {
if !isValidOwnerPreferred(r) {
h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied))
return
Expand Down
8 changes: 4 additions & 4 deletions api/handler/multipart_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,14 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
Data: &layer.UploadData{},
}

eacl, err := h.obj.GetBucketACL(r.Context(), bktInfo)
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
if err != nil {
h.logAndSendError(w, "could not get bucket eacl", reqInfo, err)
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
return
}

if containsACLHeaders(r) {
if IsBucketOwnerForced(eacl.EACL) {
if settings.BucketOwner == data.BucketOwnerEnforced {
if !isValidOwnerEnforced(r) {
h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported))
return
Expand All @@ -139,7 +139,7 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
p.Data.ACLHeaders = formACLHeadersForMultipart(r.Header)
}

if IsBucketOwnerPreferredAndRestricted(eacl.EACL) {
if settings.BucketOwner == data.BucketOwnerPreferredAndRestricted {
if !isValidOwnerPreferred(r) {
h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied))
return
Expand Down
Loading
Loading