diff --git a/api/data/info.go b/api/data/info.go index 5739f6f..faa746e 100644 --- a/api/data/info.go +++ b/api/data/info.go @@ -8,6 +8,7 @@ import ( cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" ) const ( @@ -61,9 +62,10 @@ type ( // BucketSettings stores settings such as versioning. BucketSettings struct { - Versioning string `json:"versioning"` - LockConfiguration *ObjectLockConfiguration `json:"lock_configuration"` - APE bool `json:"ape"` + Versioning string + LockConfiguration *ObjectLockConfiguration + CannedACL string + OwnerKey *keys.PublicKey } // CORSConfiguration stores CORS configuration of a request. diff --git a/api/errors/errors.go b/api/errors/errors.go index 68d2eac..3304e67 100644 --- a/api/errors/errors.go +++ b/api/errors/errors.go @@ -26,6 +26,7 @@ type ( const ( _ ErrorCode = iota ErrAccessDenied + ErrAccessControlListNotSupported ErrBadDigest ErrEntityTooSmall ErrEntityTooLarge @@ -376,6 +377,12 @@ var errorCodes = errorCodeMap{ Description: "Access Denied.", HTTPStatusCode: http.StatusForbidden, }, + ErrAccessControlListNotSupported: { + ErrCode: ErrAccessControlListNotSupported, + Code: "AccessControlListNotSupported", + Description: "The bucket does not allow ACLs.", + HTTPStatusCode: http.StatusBadRequest, + }, ErrBadDigest: { ErrCode: ErrBadDigest, Code: "BadDigest", diff --git a/api/handler/acl.go b/api/handler/acl.go index 51ae2f3..66d2dd6 100644 --- a/api/handler/acl.go +++ b/api/handler/acl.go @@ -257,6 +257,20 @@ func (h *handler) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) { return } + settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err) + return + } + + if bktInfo.APEEnabled || len(settings.CannedACL) != 0 { + if err = middleware.EncodeToResponse(w, h.encodeBucketCannedACL(ctx, bktInfo, settings)); err != nil { + h.logAndSendError(w, "something went wrong", reqInfo, err) + return + } + return + } + bucketACL, err := h.obj.GetBucketACL(ctx, bktInfo) if err != nil { h.logAndSendError(w, "could not fetch bucket acl", reqInfo, err) @@ -269,6 +283,54 @@ func (h *handler) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) { } } +func (h *handler) encodeBucketCannedACL(ctx context.Context, bktInfo *data.BucketInfo, settings *data.BucketSettings) *AccessControlPolicy { + ownerDisplayName := bktInfo.Owner.EncodeToString() + ownerEncodedID := ownerDisplayName + + if settings.OwnerKey == nil { + h.reqLogger(ctx).Warn(logs.BucketOwnerKeyIsMissing, zap.String("owner", bktInfo.Owner.String())) + } else { + ownerDisplayName = settings.OwnerKey.Address() + ownerEncodedID = hex.EncodeToString(settings.OwnerKey.Bytes()) + } + + res := &AccessControlPolicy{Owner: Owner{ + ID: ownerEncodedID, + DisplayName: ownerDisplayName, + }} + + res.AccessControlList = []*Grant{{ + Grantee: &Grantee{ + ID: ownerEncodedID, + DisplayName: ownerDisplayName, + Type: acpCanonicalUser, + }, + Permission: aclFullControl, + }} + + switch settings.CannedACL { + case basicACLPublic: + res.AccessControlList = append(res.AccessControlList, &Grant{ + Grantee: &Grantee{ + URI: allUsersGroup, + Type: acpGroup, + }, + Permission: aclWrite, + }) + fallthrough + case basicACLReadOnly: + res.AccessControlList = append(res.AccessControlList, &Grant{ + Grantee: &Grantee{ + URI: allUsersGroup, + Type: acpGroup, + }, + Permission: aclRead, + }) + } + + return res +} + func (h *handler) bearerTokenIssuerKey(ctx context.Context) (*keys.PublicKey, error) { box, err := middleware.GetBoxData(ctx) if err != nil { @@ -288,6 +350,24 @@ func (h *handler) bearerTokenIssuerKey(ctx context.Context) (*keys.PublicKey, er func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) { reqInfo := middleware.GetReqInfo(r.Context()) + + bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) + if err != nil { + h.logAndSendError(w, "could not get bucket info", reqInfo, err) + return + } + + settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err) + return + } + + if bktInfo.APEEnabled || len(settings.CannedACL) != 0 { + h.putBucketACLAPEHandler(w, r, reqInfo, bktInfo, settings) + return + } + key, err := h.bearerTokenIssuerKey(r.Context()) if err != nil { h.logAndSendError(w, "couldn't get bearer token issuer key", reqInfo, err) @@ -319,12 +399,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 @@ -332,6 +406,64 @@ func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +func (h *handler) putBucketACLAPEHandler(w http.ResponseWriter, r *http.Request, reqInfo *middleware.ReqInfo, bktInfo *data.BucketInfo, settings *data.BucketSettings) { + ctx := r.Context() + + defer func() { + if errBody := r.Body.Close(); errBody != nil { + h.reqLogger(r.Context()).Warn(logs.CouldNotCloseRequestBody, zap.Error(errBody)) + } + }() + + written, err := io.Copy(io.Discard, r.Body) + if err != nil { + h.logAndSendError(w, "couldn't read request body", reqInfo, err) + return + } + + if written != 0 || len(r.Header.Get(api.AmzACL)) == 0 { + h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported)) + return + } + + cannedACL, err := parseCannedACL(r.Header) + if err != nil { + h.logAndSendError(w, "could not parse canned ACL", reqInfo, err) + return + } + + key, err := h.bearerTokenIssuerKey(ctx) + if err != nil { + h.logAndSendError(w, "couldn't get bearer token issuer key", reqInfo, err) + return + } + + chainRules := bucketCannedACLToAPERules(cannedACL, reqInfo, key, bktInfo.CID) + + target := engine.NamespaceTarget(reqInfo.Namespace) + for _, chainPolicy := range chainRules { + if err = h.ape.AddChain(target, chainPolicy); err != nil { + h.logAndSendError(w, "failed to add morph rule chain", reqInfo, err, zap.String("chain_id", string(chainPolicy.ID))) + return + } + } + + settings.CannedACL = cannedACL + + sp := &layer.PutSettingsParams{ + BktInfo: bktInfo, + Settings: settings, + } + + if err = h.obj.PutBucketSettings(ctx, sp); err != nil { + h.logAndSendError(w, "couldn't save bucket settings", reqInfo, err, + zap.String("container_id", bktInfo.CID.EncodeToString())) + return + } + + w.WriteHeader(http.StatusOK) +} + func (h *handler) updateBucketACL(r *http.Request, astChild *ast, bktInfo *data.BucketInfo, sessionToken *session.Container) (bool, error) { bucketACL, err := h.obj.GetBucketACL(r.Context(), bktInfo) if err != nil { @@ -380,6 +512,22 @@ func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) { return } + apeEnabled := bktInfo.APEEnabled + + if !apeEnabled { + settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err) + return + } + apeEnabled = len(settings.CannedACL) != 0 + } + + if apeEnabled { + h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported)) + return + } + bucketACL, err := h.obj.GetBucketACL(ctx, bktInfo) if err != nil { h.logAndSendError(w, "could not fetch bucket acl", reqInfo, err) @@ -406,6 +554,29 @@ func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqInfo := middleware.GetReqInfo(ctx) + + bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) + if err != nil { + h.logAndSendError(w, "could not get bucket info", reqInfo, err) + return + } + + apeEnabled := bktInfo.APEEnabled + + if !apeEnabled { + settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err) + return + } + apeEnabled = len(settings.CannedACL) != 0 + } + + if apeEnabled { + h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported)) + return + } + versionID := reqInfo.URL.Query().Get(api.QueryVersionID) key, err := h.bearerTokenIssuerKey(ctx) if err != nil { @@ -419,12 +590,6 @@ func (h *handler) PutObjectACLHandler(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 - } - p := &layer.HeadObjectParams{ BktInfo: bktInfo, Object: reqInfo.ObjectName, diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go index c396b5f..1f8eefc 100644 --- a/api/handler/acl_test.go +++ b/api/handler/acl_test.go @@ -16,9 +16,11 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" s3errors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" @@ -1303,7 +1305,7 @@ func TestPutBucketACL(t *testing.T) { bktName := "bucket-for-acl" box, _ := createAccessBox(t) - bktInfo := createBucket(t, tc, bktName, box) + bktInfo := createBucketOldACL(tc, bktName, box) header := map[string]string{api.AmzACL: "public-read"} putBucketACL(t, tc, bktName, box, header) @@ -1488,12 +1490,56 @@ func createAccessBox(t *testing.T) (*accessbox.Box, *keys.PrivateKey) { return box, key } -func createBucket(t *testing.T, hc *handlerContext, bktName string, box *accessbox.Box) *data.BucketInfo { +func createBucket(hc *handlerContext, bktName string) (*data.BucketInfo, *accessbox.Box) { + box, _ := createAccessBox(hc.t) + w := createBucketBase(hc, bktName, box) - assertStatus(t, w, http.StatusOK) + assertStatus(hc.t, w, http.StatusOK) bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName) - require.NoError(t, err) + require.NoError(hc.t, err) + return bktInfo, box +} + +func createBucketOldACL(hc *handlerContext, bktName string, box *accessbox.Box) *data.BucketInfo { + w := createBucketBase(hc, bktName, box) + assertStatus(hc.t, w, http.StatusOK) + + cnrID, err := hc.tp.ContainerID(bktName) + require.NoError(hc.t, err) + + cnr, err := hc.tp.Container(hc.Context(), cnrID) + require.NoError(hc.t, err) + cnr.SetBasicACL(acl.PublicRWExtended) + cnr.SetAttribute(layer.AttributeAPEEnabled, "false") + hc.tp.SetContainer(cnrID, cnr) + table := eacl.NewTable() + table.SetCID(cnrID) + + key, err := hc.h.bearerTokenIssuerKey(hc.Context()) + require.NoError(hc.t, err) + for _, op := range fullOps { + table.AddRecord(getAllowRecord(op, key)) + } + + for _, op := range fullOps { + table.AddRecord(getOthersRecord(op, eacl.ActionDeny)) + } + err = hc.tp.SetContainerEACL(hc.Context(), *table, nil) + require.NoError(hc.t, err) + + bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName) + require.NoError(hc.t, err) + + settings, err := hc.tree.GetSettingsNode(hc.Context(), bktInfo) + require.NoError(hc.t, err) + settings.CannedACL = "" + err = hc.Layer().PutBucketSettings(hc.Context(), &layer.PutSettingsParams{BktInfo: bktInfo, Settings: settings}) + require.NoError(hc.t, err) + + bktInfo, err = hc.Layer().GetBucketInfo(hc.Context(), bktName) + require.NoError(hc.t, err) + return bktInfo } diff --git a/api/handler/delete_test.go b/api/handler/delete_test.go index 6f60dbb..55cba0d 100644 --- a/api/handler/delete_test.go +++ b/api/handler/delete_test.go @@ -168,7 +168,7 @@ func TestDeleteDeletedObject(t *testing.T) { }) t.Run("versioned bucket not found obj", func(t *testing.T) { - bktName, objName := "bucket-versioned-for-removal", "object-to-delete" + bktName, objName := "bucket-versioned-for-removal-not-found", "object-to-delete" _, objInfo := createVersionedBucketAndObject(t, tc, bktName, objName) versionID, isDeleteMarker := deleteObject(t, tc, bktName, objName, objInfo.VersionID()) diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go index 44e2397..26b6d38 100644 --- a/api/handler/handlers_test.go +++ b/api/handler/handlers_test.go @@ -21,7 +21,6 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" @@ -267,15 +266,7 @@ func (a *apeMock) DeletePolicy(namespace string, cnrID cid.ID) error { } func createTestBucket(hc *handlerContext, bktName string) *data.BucketInfo { - _, err := hc.MockedPool().CreateContainer(hc.Context(), layer.PrmContainerCreate{ - Creator: hc.owner, - Name: bktName, - BasicACL: acl.PublicRWExtended, - }) - require.NoError(hc.t, err) - - bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName) - require.NoError(hc.t, err) + bktInfo, _ := createBucket(hc, bktName) return bktInfo } @@ -297,11 +288,15 @@ func createTestBucketWithLock(hc *handlerContext, bktName string, conf *data.Obj HomomorphicHashDisabled: res.HomomorphicHashDisabled, } + key, err := keys.NewPrivateKey() + require.NoError(hc.t, err) + sp := &layer.PutSettingsParams{ BktInfo: bktInfo, Settings: &data.BucketSettings{ Versioning: data.VersioningEnabled, LockConfiguration: conf, + OwnerKey: key.PublicKey(), }, } diff --git a/api/handler/put.go b/api/handler/put.go index 24135b0..c30745b 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -833,8 +833,12 @@ func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { } sp := &layer.PutSettingsParams{ - BktInfo: bktInfo, - Settings: &data.BucketSettings{APE: true}, + BktInfo: bktInfo, + Settings: &data.BucketSettings{ + CannedACL: cannedACL, + OwnerKey: key, + Versioning: data.VersioningUnversioned, + }, } if p.ObjectLockEnabled { diff --git a/api/handler/put_test.go b/api/handler/put_test.go index d519de5..c195f14 100644 --- a/api/handler/put_test.go +++ b/api/handler/put_test.go @@ -372,8 +372,7 @@ func TestCreateBucket(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bkt-name" - box, _ := createAccessBox(t) - createBucket(t, hc, bktName, box) + _, box := createBucket(hc, bktName) createBucketAssertS3Error(hc, bktName, box, s3errors.ErrBucketAlreadyOwnedByYou) box2, _ := createAccessBox(t) diff --git a/api/layer/container.go b/api/layer/container.go index e03845c..1105fc1 100644 --- a/api/layer/container.go +++ b/api/layer/container.go @@ -28,7 +28,7 @@ type ( const ( attributeLocationConstraint = ".s3-location-constraint" - attributeAPEEnabled = ".s3-APE-enabled" + AttributeAPEEnabled = ".s3-APE-enabled" AttributeLockEnabled = "LockEnabled" ) @@ -75,7 +75,7 @@ func (n *layer) containerInfo(ctx context.Context, idCnr cid.ID) (*data.BucketIn } } - APEEnabled := cnr.Attribute(attributeAPEEnabled) + APEEnabled := cnr.Attribute(AttributeAPEEnabled) if len(APEEnabled) > 0 { info.APEEnabled, err = strconv.ParseBool(APEEnabled) if err != nil { @@ -136,7 +136,7 @@ func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da attributes := [][2]string{ {attributeLocationConstraint, p.LocationConstraint}, - {attributeAPEEnabled, "true"}, + {AttributeAPEEnabled, "true"}, } if p.ObjectLockEnabled { diff --git a/api/layer/frostfs_mock.go b/api/layer/frostfs_mock.go index 58db92f..193654e 100644 --- a/api/layer/frostfs_mock.go +++ b/api/layer/frostfs_mock.go @@ -136,6 +136,10 @@ func (t *TestFrostFS) ContainerID(name string) (cid.ID, error) { return cid.ID{}, fmt.Errorf("not found") } +func (t *TestFrostFS) SetContainer(cnrID cid.ID, cnr *container.Container) { + t.containers[cnrID.EncodeToString()] = cnr +} + func (t *TestFrostFS) CreateContainer(_ context.Context, prm PrmContainerCreate) (*ContainerCreateResult, error) { var cnr container.Container cnr.Init() diff --git a/internal/logs/logs.go b/internal/logs/logs.go index 7a55f26..8a316bf 100644 --- a/internal/logs/logs.go +++ b/internal/logs/logs.go @@ -142,4 +142,7 @@ const ( CouldntPutAccessBoxIntoCache = "couldn't put accessbox into cache" InvalidAccessBoxCacheRemovingCheckInterval = "invalid accessbox check removing interval, using default value" CouldNotParseContainerAPEEnabledAttribute = "could not parse container APE enabled attribute" + CouldNotCloseRequestBody = "could not close request body" + BucketOwnerKeyIsMissing = "bucket owner key is missing" + SettingsNodeInvalidOwnerKey = "settings node: invalid owner key" ) diff --git a/pkg/service/tree/tree.go b/pkg/service/tree/tree.go index 7f94220..50fc311 100644 --- a/pkg/service/tree/tree.go +++ b/pkg/service/tree/tree.go @@ -2,6 +2,7 @@ package tree import ( "context" + "encoding/hex" "errors" "fmt" "io" @@ -16,6 +17,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "go.uber.org/zap" "golang.org/x/exp/maps" ) @@ -78,7 +80,8 @@ var ( const ( versioningKV = "Versioning" - apeKV = "APE" + cannedACLKV = "cannedACL" + ownerKeyKV = "ownerKey" lockConfigurationKV = "LockConfiguration" oidKV = "OID" @@ -333,7 +336,7 @@ func newPartInfo(node NodeResponse) (*data.PartInfo, error) { } func (c *Tree) GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) { - keysToReturn := []string{versioningKV, lockConfigurationKV, apeKV} + keysToReturn := []string{versioningKV, lockConfigurationKV, cannedACLKV, ownerKeyKV} node, err := c.getSystemNode(ctx, bktInfo, []string{settingsFileName}, keysToReturn) if err != nil { return nil, fmt.Errorf("couldn't get node: %w", err) @@ -350,8 +353,12 @@ func (c *Tree) GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (* } } - if ape, ok := node.Get(apeKV); ok { - settings.APE, _ = strconv.ParseBool(ape) + settings.CannedACL, _ = node.Get(cannedACLKV) + + if ownerKeyHex, ok := node.Get(ownerKeyKV); ok { + if settings.OwnerKey, err = keys.NewPublicKeyFromString(ownerKeyHex); err != nil { + c.reqLogger(ctx).Error(logs.SettingsNodeInvalidOwnerKey, zap.Error(err)) + } } return settings, nil @@ -1389,7 +1396,8 @@ func metaFromSettings(settings *data.BucketSettings) map[string]string { results[FileNameKey] = settingsFileName results[versioningKV] = settings.Versioning results[lockConfigurationKV] = encodeLockConfiguration(settings.LockConfiguration) - results[apeKV] = strconv.FormatBool(settings.APE) + results[cannedACLKV] = settings.CannedACL + results[ownerKeyKV] = hex.EncodeToString(settings.OwnerKey.Bytes()) return results } diff --git a/pkg/service/tree/tree_test.go b/pkg/service/tree/tree_test.go index 9d3f7d7..cfb4605 100644 --- a/pkg/service/tree/tree_test.go +++ b/pkg/service/tree/tree_test.go @@ -9,6 +9,7 @@ import ( cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) @@ -111,6 +112,9 @@ func TestTreeServiceSettings(t *testing.T) { CID: cidtest.ID(), } + key, err := keys.NewPrivateKey() + require.NoError(t, err) + settings := &data.BucketSettings{ Versioning: "Versioning", LockConfiguration: &data.ObjectLockConfiguration{ @@ -122,6 +126,7 @@ func TestTreeServiceSettings(t *testing.T) { }, }, }, + OwnerKey: key.PublicKey(), } err = treeService.PutSettingsNode(ctx, bktInfo, settings)