Skip to content

Commit

Permalink
Disable ACL by default (#1018)
Browse files Browse the repository at this point in the history
Closes #1016.
  • Loading branch information
roman-khimov authored Nov 5, 2024
2 parents f8825a2 + 8008392 commit 8a090d4
Show file tree
Hide file tree
Showing 16 changed files with 437 additions and 46 deletions.
71 changes: 71 additions & 0 deletions api/cache/eacls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package cache

import (
"fmt"
"time"

"github.com/bluele/gcache"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
"go.uber.org/zap"
)

type (
// BucketACLCache contains cache with bucket EACL.
BucketACLCache struct {
cache gcache.Cache
logger *zap.Logger
}
)

const (
// DefaultEACLCacheSize is a default maximum number of entries in cache.
DefaultEACLCacheSize = 1e3
// DefaultEACLCacheLifetime is a default lifetime of entries in cache.
DefaultEACLCacheLifetime = time.Minute
)

// DefaultBucketACLCacheConfig returns new default cache expiration values.
func DefaultBucketACLCacheConfig(logger *zap.Logger) *Config {
return &Config{
Size: DefaultEACLCacheSize,
Lifetime: DefaultEACLCacheLifetime,
Logger: logger,
}
}

// NewEACLCache creates an object of BucketACLCache.
func NewEACLCache(config *Config) *BucketACLCache {
return &BucketACLCache{
cache: gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build(),
logger: config.Logger}
}

// Get returns a cached state.
func (o *BucketACLCache) Get(id cid.ID) *eacl.Table {
entry, err := o.cache.Get(id.String())
if err != nil {
return nil
}

result, ok := entry.(*eacl.Table)
if !ok {
o.logger.Warn("invalid cache entry type",
zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result)),
)
return nil
}

return result
}

// Put puts a state to cache.
func (o *BucketACLCache) Put(id cid.ID, v *eacl.Table) error {
return o.cache.Set(id.String(), v)
}

// Delete deletes a state from cache.
func (o *BucketACLCache) Delete(id cid.ID) bool {
return o.cache.Remove(id.String())
}
14 changes: 14 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 (
// BucketACLEnabled means ACLs are enabled.
BucketACLEnabled BucketACLState = iota

// BucketACLBucketOwnerEnforced mean ACLs are disabled. Any additional objects ACL raises AccessControlListNotSupported error.
BucketACLBucketOwnerEnforced

// BucketACLBucketOwnerPreferred mean ACLs enabled. New object must be uploaded with `bucket-owner-full-control` canned ACL.
BucketACLBucketOwnerPreferred
)

type (
// BucketACLState is bucket ACL state.
BucketACLState uint32

// BucketInfo stores basic bucket data.
BucketInfo struct {
Name string
Expand Down
158 changes: 147 additions & 11 deletions api/handler/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,12 @@ type bucketPolicy struct {
}

type statement struct {
Sid string `json:"Sid"`
Effect string `json:"Effect"`
Principal principal `json:"Principal"`
Action stringOrSlice `json:"Action"`
Resource stringOrSlice `json:"Resource"`
Sid string `json:"Sid"`
Effect string `json:"Effect"`
Principal principal `json:"Principal"`
Action stringOrSlice `json:"Action"`
Resource stringOrSlice `json:"Resource"`
Condition map[string]map[string]string `json:"Condition"`
}

type principal struct {
Expand Down Expand Up @@ -241,6 +242,26 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
return
}

bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
if err != nil {
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
return
}

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

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

list := &AccessControlPolicy{}
if r.ContentLength == 0 {
list, err = parseACLHeaders(r.Header, iss)
Expand All @@ -260,12 +281,6 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
return
}

bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
if err != nil {
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
return
}

if _, err = h.updateBucketACL(r, astBucket, bktInfo, token); err != nil {
h.logAndSendError(w, "could not update bucket acl", reqInfo, err)
return
Expand Down Expand Up @@ -364,6 +379,20 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
return
}

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

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

p := &layer.HeadObjectParams{
BktInfo: bktInfo,
Object: reqInfo.ObjectName,
Expand Down Expand Up @@ -618,6 +647,7 @@ func formGrantee(granteeType, value string) (*Grantee, error) {
func addPredefinedACP(acp *AccessControlPolicy, cannedACL string) (*AccessControlPolicy, error) {
switch cannedACL {
case basicACLPrivate:
case cannedACLBucketOwnerFullControl:
case basicACLPublic:
acp.AccessControlList = append(acp.AccessControlList, &Grant{
Grantee: &Grantee{
Expand Down Expand Up @@ -935,6 +965,16 @@ func tryServiceRecord(record eacl.Record) *ServiceRecord {
func formRecords(resource *astResource) ([]*eacl.Record, error) {
var res []*eacl.Record

if resource.Version == amzBucketOwnerEnforced && resource.Object == amzBucketOwnerEnforced {
res = append(res, bucketOwnerEnforcedRecord())
return res, nil
}

if resource.Version == aclEnabledObjectWriter && resource.Object == aclEnabledObjectWriter {
res = append(res, bucketACLObjectWriterRecord())
return res, nil
}

for i := len(resource.Operations) - 1; i >= 0; i-- {
astOp := resource.Operations[i]
record := eacl.NewRecord()
Expand Down Expand Up @@ -1017,6 +1057,40 @@ func policyToAst(bktPolicy *bucketPolicy) (*ast, error) {
rr := make(map[string]*astResource)

for _, state := range bktPolicy.Statement {
if state.Sid == "BucketOwnerEnforced" &&
state.Action.Equal(stringOrSlice{values: []string{"*"}}) &&
state.Effect == "Deny" &&
state.Resource.Equal(stringOrSlice{values: []string{"*"}}) {
if conditionObj, ok := state.Condition["StringNotEquals"]; ok {
if val := conditionObj["s3:x-amz-object-ownership"]; val == amzBucketOwnerEnforced {
rr[amzBucketOwnerEnforced] = &astResource{
resourceInfo: resourceInfo{
Version: amzBucketOwnerEnforced,
Object: amzBucketOwnerEnforced,
},
}

continue
}
}

return nil, fmt.Errorf("unsupported ownership: %v", state.Principal)
}

if state.Sid == "BucketEnableACL" &&
state.Action.Equal(stringOrSlice{values: []string{"s3:PutObject"}}) &&
state.Effect == "Allow" &&
state.Resource.Equal(stringOrSlice{values: []string{"*"}}) {
rr[aclEnabledObjectWriter] = &astResource{
resourceInfo: resourceInfo{
Version: aclEnabledObjectWriter,
Object: aclEnabledObjectWriter,
},
}

continue
}

if state.Principal.AWS != "" && state.Principal.AWS != allUsersWildcard ||
state.Principal.AWS == "" && state.Principal.CanonicalUser == "" {
return nil, fmt.Errorf("unsupported principal: %v", state.Principal)
Expand Down Expand Up @@ -1550,6 +1624,8 @@ func bucketACLToTable(acp *AccessControlPolicy) (*eacl.Table, error) {
table.AddRecord(getOthersRecord(op, eacl.ActionDeny))
}

table.AddRecord(bucketOwnerEnforcedRecord())

return table, nil
}

Expand Down Expand Up @@ -1580,3 +1656,63 @@ func getOthersRecord(op eacl.Operation, action eacl.Action) *eacl.Record {
eacl.AddFormedTarget(record, eacl.RoleOthers)
return record
}

func bucketOwnerEnforcedRecord() *eacl.Record {
var markerRecord = eacl.CreateRecord(eacl.ActionDeny, eacl.OperationPut)
markerRecord.AddFilter(
eacl.HeaderFromRequest,
eacl.MatchStringNotEqual,
amzBucketOwnerField,
amzBucketOwnerEnforced,
)

return markerRecord
}

func isValidOwnerEnforced(r *http.Request) bool {
if cannedACL := r.Header.Get(api.AmzACL); cannedACL != "" {
switch cannedACL {
case basicACLPrivate:
return true
case cannedACLBucketOwnerFullControl:
return true
default:
return false
}
}

return true
}

func bucketACLObjectWriterRecord() *eacl.Record {
var markerRecord = eacl.CreateRecord(eacl.ActionAllow, eacl.OperationPut)
markerRecord.AddFilter(
eacl.HeaderFromRequest,
eacl.MatchStringEqual,
amzBucketOwnerField,
aclEnabledObjectWriter,
)

return markerRecord
}

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 {
return true
}
}
}
}

return false
}
22 changes: 18 additions & 4 deletions api/handler/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,7 @@ func TestBucketAclToTable(t *testing.T) {
for _, op := range fullOps {
expectedTable.AddRecord(getOthersRecord(op, eacl.ActionDeny))
}
expectedTable.AddRecord(bucketOwnerEnforcedRecord())

actualTable, err := bucketACLToTable(acl)
require.NoError(t, err)
Expand Down Expand Up @@ -1356,10 +1357,23 @@ func TestPutBucketACL(t *testing.T) {
bktInfo := createBucket(t, tc, bktName, box)

header := map[string]string{api.AmzACL: "public-read"}
putBucketACL(t, tc, bktName, box, header)
// ACLs disabled.
putBucketACL(t, tc, bktName, box, header, http.StatusBadRequest)

aclPolicy := &bucketPolicy{
Statement: []statement{{
Sid: "BucketEnableACL",
Effect: "Allow",
Action: stringOrSlice{values: []string{"s3:PutObject"}},
Resource: stringOrSlice{values: []string{"*"}},
}},
}
putBucketPolicy(tc, bktName, aclPolicy, box, http.StatusOK)

// ACLs enabled.
putBucketACL(t, tc, bktName, box, header, http.StatusOK)
header = map[string]string{api.AmzACL: "private"}
putBucketACL(t, tc, bktName, box, header)
putBucketACL(t, tc, bktName, box, header, http.StatusOK)
checkLastRecords(t, tc, bktInfo, eacl.ActionDeny)
}

Expand Down Expand Up @@ -1481,15 +1495,15 @@ func createBucket(t *testing.T, tc *handlerContext, bktName string, box *accessb
return bktInfo
}

func putBucketACL(t *testing.T, tc *handlerContext, bktName string, box *accessbox.Box, header map[string]string) {
func putBucketACL(t *testing.T, tc *handlerContext, bktName string, box *accessbox.Box, header map[string]string, status int) {
w, r := prepareTestRequest(tc, bktName, "", nil)
for key, val := range header {
r.Header.Set(key, val)
}
ctx := context.WithValue(r.Context(), api.BoxData, box)
r = r.WithContext(ctx)
tc.Handler().PutBucketACLHandler(w, r)
assertStatus(t, w, http.StatusOK)
assertStatus(t, w, status)
}

func generateRecord(action eacl.Action, op eacl.Operation, targets []eacl.Target) *eacl.Record {
Expand Down
8 changes: 8 additions & 0 deletions api/handler/api.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package handler

import (
"context"
"errors"
"time"

"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neofs-s3-gw/api"
"github.com/nspcc-dev/neofs-s3-gw/api/data"
"github.com/nspcc-dev/neofs-s3-gw/api/layer"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -45,6 +48,11 @@ type (
// Returns [models.ErrNotFound] if policy not found.
GetPlacementPolicy(userAddr util.Uint160, policyName string) (*netmap.PlacementPolicy, error)
}

// ACLStateProvider get bucket ACL state.
ACLStateProvider interface {
GetState(ctx context.Context, idCnr cid.ID) (data.BucketACLState, error)
}
)

const (
Expand Down
Loading

0 comments on commit 8a090d4

Please sign in to comment.