diff --git a/api/handler/acl.go b/api/handler/acl.go index 3b5e3e0c..98e2722d 100644 --- a/api/handler/acl.go +++ b/api/handler/acl.go @@ -299,7 +299,7 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) { return } - if isBucketOwnerForced(eacl.EACL) { + if IsBucketOwnerForced(eacl.EACL) { if !isValidOwnerEnforced(r) { h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) return @@ -430,7 +430,7 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) { return } - if isBucketOwnerForced(eacl.EACL) { + if IsBucketOwnerForced(eacl.EACL) { if !isValidOwnerEnforced(r) { h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) return @@ -438,7 +438,7 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) { r.Header.Set(api.AmzACL, "") } - if isBucketOwnerPreferredAndRestricted(eacl.EACL) { + if IsBucketOwnerPreferredAndRestricted(eacl.EACL) { if !isValidOwnerPreferred(r) { h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) return @@ -1026,13 +1026,13 @@ func formRecords(resource *astResource) ([]*eacl.Record, error) { switch astOp.Users[0] { case ownerEnforcedUserID: - markerRecord = bucketOwnerEnforcedRecord() + markerRecord = BucketOwnerEnforcedRecord() case ownerPreferredUserID: - markerRecord = bucketOwnerPreferredRecord() + markerRecord = BucketOwnerPreferredRecord() case ownerObjectWriterUserID: - markerRecord = bucketACLObjectWriterRecord() + markerRecord = BucketACLObjectWriterRecord() case ownerPreferredAndRestrictedUserID: - markerRecord = bucketOwnerPreferredAndRestrictedRecord() + markerRecord = BucketOwnerPreferredAndRestrictedRecord() } if markerRecord != nil { @@ -1687,7 +1687,7 @@ func bucketACLToTable(acp *AccessControlPolicy) (*eacl.Table, error) { table.AddRecord(getOthersRecord(op, eacl.ActionDeny)) } - table.AddRecord(bucketOwnerEnforcedRecord()) + table.AddRecord(BucketOwnerEnforcedRecord()) return table, nil } @@ -1720,7 +1720,8 @@ func getOthersRecord(op eacl.Operation, action eacl.Action) *eacl.Record { return record } -func bucketOwnerEnforcedRecord() *eacl.Record { +// BucketOwnerEnforcedRecord generates special marker record for OwnerEnforced policy. +func BucketOwnerEnforcedRecord() *eacl.Record { var markerRecord = eacl.CreateRecord(eacl.ActionDeny, eacl.OperationPut) markerRecord.AddFilter( eacl.HeaderFromRequest, @@ -1751,7 +1752,8 @@ func isValidOwnerEnforced(r *http.Request) bool { return true } -func bucketACLObjectWriterRecord() *eacl.Record { +// BucketACLObjectWriterRecord generates special marker record for OwnerWriter policy. +func BucketACLObjectWriterRecord() *eacl.Record { var markerRecord = eacl.CreateRecord(eacl.ActionDeny, eacl.OperationPut) markerRecord.AddFilter( eacl.HeaderFromRequest, @@ -1767,7 +1769,8 @@ func bucketACLObjectWriterRecord() *eacl.Record { return markerRecord } -func isBucketOwnerForced(table *eacl.Table) bool { +// IsBucketOwnerForced checks special marker record for OwnerForced policy. +func IsBucketOwnerForced(table *eacl.Table) bool { if table == nil { return false } @@ -1790,7 +1793,8 @@ func isBucketOwnerForced(table *eacl.Table) bool { return false } -func bucketOwnerPreferredRecord() *eacl.Record { +// BucketOwnerPreferredRecord generates special marker record for OwnerPreferred policy. +func BucketOwnerPreferredRecord() *eacl.Record { var markerRecord = eacl.CreateRecord(eacl.ActionDeny, eacl.OperationPut) markerRecord.AddFilter( eacl.HeaderFromRequest, @@ -1806,7 +1810,8 @@ func bucketOwnerPreferredRecord() *eacl.Record { return markerRecord } -func isBucketOwnerPreferred(table *eacl.Table) bool { +// IsBucketOwnerPreferred checks special marker record for OwnerPreferred policy. +func IsBucketOwnerPreferred(table *eacl.Table) bool { if table == nil { return false } @@ -1829,7 +1834,8 @@ func isBucketOwnerPreferred(table *eacl.Table) bool { return false } -func bucketOwnerPreferredAndRestrictedRecord() *eacl.Record { +// 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.CreateRecord(eacl.ActionDeny, eacl.OperationPut) markerRecord.AddFilter( eacl.HeaderFromObject, @@ -1845,7 +1851,8 @@ func bucketOwnerPreferredAndRestrictedRecord() *eacl.Record { return markerRecord } -func isBucketOwnerPreferredAndRestricted(table *eacl.Table) bool { +// 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 } diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go index b0e5293f..32a78ed8 100644 --- a/api/handler/acl_test.go +++ b/api/handler/acl_test.go @@ -1185,7 +1185,7 @@ func TestBucketAclToTable(t *testing.T) { for _, op := range fullOps { expectedTable.AddRecord(getOthersRecord(op, eacl.ActionDeny)) } - expectedTable.AddRecord(bucketOwnerEnforcedRecord()) + expectedTable.AddRecord(BucketOwnerEnforcedRecord()) actualTable, err := bucketACLToTable(acl) require.NoError(t, err) diff --git a/api/handler/copy.go b/api/handler/copy.go index 90b3425a..895221a7 100644 --- a/api/handler/copy.go +++ b/api/handler/copy.go @@ -97,7 +97,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { } if containsACL { - if isBucketOwnerForced(eacl.EACL) { + if IsBucketOwnerForced(eacl.EACL) { if !isValidOwnerEnforced(r) { h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) return @@ -111,7 +111,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { } } - if isBucketOwnerPreferredAndRestricted(eacl.EACL) { + if IsBucketOwnerPreferredAndRestricted(eacl.EACL) { if !isValidOwnerPreferred(r) { h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) return @@ -225,7 +225,8 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { return } - if containsACL { + // In some cases upper in the code, we change ACL headers. We have to check all headers one more time. + if containsACLHeaders(r) { newEaclTable, err := h.getNewEAclTable(r, dstBktInfo, dstObjInfo) if err != nil { h.logAndSendError(w, "could not get new eacl table", reqInfo, err) diff --git a/api/handler/multipart_upload.go b/api/handler/multipart_upload.go index 70416d1e..f90c1330 100644 --- a/api/handler/multipart_upload.go +++ b/api/handler/multipart_upload.go @@ -119,7 +119,7 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re } if containsACLHeaders(r) { - if isBucketOwnerForced(eacl.EACL) { + if IsBucketOwnerForced(eacl.EACL) { if !isValidOwnerEnforced(r) { h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) return @@ -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 IsBucketOwnerPreferredAndRestricted(eacl.EACL) { if !isValidOwnerPreferred(r) { h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) return diff --git a/api/handler/ownership.go b/api/handler/ownership.go index af8de936..e6279351 100644 --- a/api/handler/ownership.go +++ b/api/handler/ownership.go @@ -56,11 +56,11 @@ func (h *handler) PutBucketOwnershipControlsHandler(w http.ResponseWriter, r *ht switch params.Rules[0].ObjectOwnership { case amzBucketOwnerEnforced: - rec = bucketOwnerEnforcedRecord() + rec = BucketOwnerEnforcedRecord() case amzBucketOwnerPreferred: - rec = bucketOwnerPreferredRecord() + rec = BucketOwnerPreferredRecord() case amzBucketOwnerObjectWriter: - rec = bucketACLObjectWriterRecord() + rec = BucketACLObjectWriterRecord() default: h.logAndSendError(w, "invalid ownership", reqInfo, s3errors.GetAPIError(s3errors.ErrBadRequest)) return @@ -135,11 +135,11 @@ func (h *handler) GetBucketOwnershipControlsHandler(w http.ResponseWriter, r *ht return } - if isBucketOwnerForced(bucketACL.EACL) { + if IsBucketOwnerForced(bucketACL.EACL) { response = &putBucketOwnershipControlsParams{ Rules: []objectOwnershipRules{{ObjectOwnership: amzBucketOwnerEnforced}}, } - } else if isBucketOwnerPreferred(bucketACL.EACL) { + } else if IsBucketOwnerPreferred(bucketACL.EACL) { response = &putBucketOwnershipControlsParams{ Rules: []objectOwnershipRules{{ObjectOwnership: amzBucketOwnerPreferred}}, } diff --git a/api/handler/put.go b/api/handler/put.go index 94cd3cc4..794dbebd 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -216,7 +216,7 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { } if containsACL { - if isBucketOwnerForced(eacl.EACL) { + if IsBucketOwnerForced(eacl.EACL) { if !isValidOwnerEnforced(r) { h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) return @@ -225,7 +225,7 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { } } - if isBucketOwnerPreferredAndRestricted(eacl.EACL) { + if IsBucketOwnerPreferredAndRestricted(eacl.EACL) { if !isValidOwnerPreferred(r) { h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) return @@ -474,7 +474,7 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { } if containsACL { - if isBucketOwnerForced(eacl.EACL) { + if IsBucketOwnerForced(eacl.EACL) { if !isValidOwnerEnforced(r) { h.logAndSendError(w, "access control list not supported", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessControlListNotSupported)) return @@ -483,7 +483,7 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { } } - if isBucketOwnerPreferredAndRestricted(eacl.EACL) { + if IsBucketOwnerPreferredAndRestricted(eacl.EACL) { if !isValidOwnerPreferred(r) { h.logAndSendError(w, "header x-amz-acl:bucket-owner-full-control must be set", reqInfo, s3errors.GetAPIError(s3errors.ErrAccessDenied)) return diff --git a/authmate/authmate.go b/authmate/authmate.go index 456232d1..c2ecb2c7 100644 --- a/authmate/authmate.go +++ b/authmate/authmate.go @@ -78,6 +78,12 @@ type NeoFS interface { // // It returns any error encountered which prevented computing epochs. TimeToEpoch(context.Context, time.Time) (uint64, uint64, error) + + // SetContainerEACL updates container EACL. + SetContainerEACL(ctx context.Context, table eacl.Table, sessionToken *session.Container) error + + // ContainerEACL gets container EACL. + ContainerEACL(ctx context.Context, containerID cid.ID) (*eacl.Table, error) } // Agent contains client communicating with NeoFS and logger. diff --git a/cmd/s3-authmate/main.go b/cmd/s3-authmate/main.go index bc844787..55037319 100644 --- a/cmd/s3-authmate/main.go +++ b/cmd/s3-authmate/main.go @@ -14,12 +14,14 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neofs-s3-gw/api" + "github.com/nspcc-dev/neofs-s3-gw/api/handler" "github.com/nspcc-dev/neofs-s3-gw/authmate" "github.com/nspcc-dev/neofs-s3-gw/internal/neofs" "github.com/nspcc-dev/neofs-s3-gw/internal/version" "github.com/nspcc-dev/neofs-s3-gw/internal/wallet" "github.com/nspcc-dev/neofs-sdk-go/client" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/nspcc-dev/neofs-sdk-go/pool" "github.com/nspcc-dev/neofs-sdk-go/user" "github.com/spf13/viper" @@ -166,6 +168,7 @@ func appCommands() []*cli.Command { return []*cli.Command{ issueSecret(), obtainSecret(), + resetBucketEACL(), } } @@ -617,3 +620,126 @@ func createNeoFS(ctx context.Context, log *zap.Logger, cfg PoolConfig, anonSigne return neofs.NewAuthmateNeoFS(neoFS), nil } + +func resetBucketEACL() *cli.Command { + command := &cli.Command{ + Name: "reset-bucket-acl", + Usage: "Reset bucket ACL to default state", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "wallet", + Value: "", + Usage: "path to the container owner wallet", + Required: true, + Destination: &walletPathFlag, + }, + &cli.StringFlag{ + Name: "peer", + Value: "", + Usage: "address of neofs peer to connect to", + Required: true, + Destination: &peerAddressFlag, + }, + &cli.StringFlag{ + Name: "container-id", + Usage: "neofs container id to update eacl", + Required: true, + Destination: &containerIDFlag, + }, + }, + Action: func(_ *cli.Context) error { + ctx, log := prepare() + + password := wallet.GetPassword(viper.GetViper(), envWalletPassphrase) + if password == nil { + var empty string + password = &empty + } + + key, err := wallet.GetKeyFromPath(walletPathFlag, accountAddressFlag, password) + if err != nil { + return cli.Exit(fmt.Sprintf("failed to load neofs private key: %s", err), 1) + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + poolCfg := PoolConfig{ + Key: &key.PrivateKey, + Address: peerAddressFlag, + } + + anonKey, err := keys.NewPrivateKey() + if err != nil { + log.Fatal("obtainSecret: couldn't generate random key", zap.Error(err)) + } + anonSigner := user.NewAutoIDSignerRFC6979(anonKey.PrivateKey) + + neoFS, err := createNeoFS(ctx, log, poolCfg, anonSigner, slicerEnabledFlag) + if err != nil { + return cli.Exit(fmt.Sprintf("failed to create NeoFS component: %s", err), 1) + } + + var containerID cid.ID + if err = containerID.DecodeString(containerIDFlag); err != nil { + return cli.Exit(fmt.Sprintf("failed to parse auth container id: %s", err), 1) + } + + var ( + newEACLTable eacl.Table + ownerID = user.NewFromScriptHash(key.GetScriptHash()) + targetOwner eacl.Target + ) + + newEACLTable.SetCID(containerID) + targetOwner.SetAccounts([]user.ID{ownerID}) + + for op := eacl.OperationGet; op <= eacl.OperationRangeHash; op++ { + record := eacl.NewRecord() + record.SetOperation(op) + record.SetAction(eacl.ActionAllow) + record.SetTargets(targetOwner) + + newEACLTable.AddRecord(record) + } + + for op := eacl.OperationGet; op <= eacl.OperationRangeHash; op++ { + record := eacl.NewRecord() + record.SetOperation(op) + record.SetAction(eacl.ActionDeny) + eacl.AddFormedTarget(record, eacl.RoleOthers) + + newEACLTable.AddRecord(record) + } + + oldEacl, err := neoFS.ContainerEACL(ctx, containerID) + if err != nil { + return cli.Exit(fmt.Sprintf("failed to obtain old neofs EACL: %s", err), 1) + } + + if handler.IsBucketOwnerForced(oldEacl) { + newEACLTable.AddRecord(handler.BucketOwnerEnforcedRecord()) + } + + if handler.IsBucketOwnerPreferred(oldEacl) { + newEACLTable.AddRecord(handler.BucketOwnerPreferredRecord()) + } + + if handler.IsBucketOwnerPreferredAndRestricted(oldEacl) { + newEACLTable.AddRecord(handler.BucketOwnerPreferredAndRestrictedRecord()) + } + + var tcancel context.CancelFunc + ctx, tcancel = context.WithTimeout(ctx, timeoutFlag) + defer tcancel() + + if err = neoFS.SetContainerEACL(ctx, newEACLTable, nil); err != nil { + return cli.Exit(fmt.Sprintf("failed to setup eacl: %s", err), 1) + } + + return nil + }, + } + + return command +} diff --git a/internal/neofs/neofs.go b/internal/neofs/neofs.go index f0abf2e4..30dcde42 100644 --- a/internal/neofs/neofs.go +++ b/internal/neofs/neofs.go @@ -691,6 +691,16 @@ func (x *AuthmateNeoFS) CreateObject(ctx context.Context, prm tokens.PrmObjectCr }) } +// SetContainerEACL implements authmate.NeoFS interface method. +func (x *AuthmateNeoFS) SetContainerEACL(ctx context.Context, table eacl.Table, sessionToken *session.Container) error { + return x.neoFS.SetContainerEACL(ctx, table, sessionToken) +} + +// ContainerEACL implements authmate.NeoFS interface method. +func (x *AuthmateNeoFS) ContainerEACL(ctx context.Context, containerID cid.ID) (*eacl.Table, error) { + return x.neoFS.ContainerEACL(ctx, containerID) +} + // PoolStatistic is a mediator which implements authmate.NeoFS through pool.Pool. type PoolStatistic struct { poolStat *stat.PoolStat