From ff0f552741c53344912d7d1222f40a97e29040db Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 19 Sep 2023 22:27:23 +0400 Subject: [PATCH 1/2] WIP: Refactor ACL checker of the storage node Behavior change: response headers are no longer processed because eACL may include rules over request headers only. Signed-off-by: Leonard Lyubich --- cmd/neofs-node/node.go | 157 +++ cmd/neofs-node/object.go | 24 +- pkg/services/object/acl/acl.go | 710 ++++++++--- pkg/services/object/acl/acl_test.go | 176 ++- pkg/services/object/acl/eacl/v2/eacl_test.go | 165 --- pkg/services/object/acl/eacl/v2/headers.go | 241 ---- pkg/services/object/acl/eacl/v2/localstore.go | 21 - pkg/services/object/acl/eacl/v2/object.go | 92 -- pkg/services/object/acl/eacl/v2/opts.go | 50 - pkg/services/object/acl/eacl/v2/xheader.go | 61 - pkg/services/object/acl/errors.go | 234 ++++ pkg/services/object/acl/headers.go | 154 +++ pkg/services/object/acl/v2/classifier.go | 136 --- pkg/services/object/acl/v2/errors.go | 80 +- pkg/services/object/acl/v2/opts.go | 51 - pkg/services/object/acl/v2/request.go | 139 --- pkg/services/object/acl/v2/service.go | 1035 +++++++++++------ pkg/services/object/acl/v2/types.go | 28 - pkg/services/object/acl/v2/util.go | 237 ++-- pkg/services/object/acl/v2/util_test.go | 139 --- 20 files changed, 2009 insertions(+), 1921 deletions(-) create mode 100644 cmd/neofs-node/node.go delete mode 100644 pkg/services/object/acl/eacl/v2/eacl_test.go delete mode 100644 pkg/services/object/acl/eacl/v2/headers.go delete mode 100644 pkg/services/object/acl/eacl/v2/localstore.go delete mode 100644 pkg/services/object/acl/eacl/v2/object.go delete mode 100644 pkg/services/object/acl/eacl/v2/opts.go delete mode 100644 pkg/services/object/acl/eacl/v2/xheader.go create mode 100644 pkg/services/object/acl/errors.go create mode 100644 pkg/services/object/acl/headers.go delete mode 100644 pkg/services/object/acl/v2/classifier.go delete mode 100644 pkg/services/object/acl/v2/opts.go delete mode 100644 pkg/services/object/acl/v2/request.go delete mode 100644 pkg/services/object/acl/v2/types.go delete mode 100644 pkg/services/object/acl/v2/util_test.go diff --git a/cmd/neofs-node/node.go b/cmd/neofs-node/node.go new file mode 100644 index 0000000000..9aba2f2fbc --- /dev/null +++ b/cmd/neofs-node/node.go @@ -0,0 +1,157 @@ +package main + +import ( + "bytes" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neofs-node/pkg/core/netmap" + "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" + aclservice "github.com/nspcc-dev/neofs-node/pkg/services/object/acl" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + netmapSDK "github.com/nspcc-dev/neofs-sdk-go/netmap" + objectSDK "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/nspcc-dev/neofs-sdk-go/user" +) + +// node is wrapper over cfg providing interface needed by app components. +type node struct { + c *cfg + + innerRing *cachedIRFetcher +} + +func newNode(c *cfg) *node { + return &node{ + c: c, + innerRing: newCachedIRFetcher(&innerRingFetcherWithNotary{ + sidechain: c.cfgMorph.client, + }), + } +} + +func (x *node) GetContainerInfo(id cid.ID) (aclservice.ContainerInfo, error) { + var res aclservice.ContainerInfo + + cnr, err := x.c.cfgObject.cnrSource.Get(id) + if err != nil { + return res, err + } + + res.Owner = cnr.Value.Owner() + res.BasicACL = cnr.Value.BasicACL() + + return res, nil +} + +func (x *node) GetExtendedACL(cnr cid.ID) (eacl.Table, error) { + eACL, err := x.c.cfgObject.eaclSource.GetEACL(cnr) + if err != nil { + return eacl.Table{}, err + } + + return *eACL.Value, nil +} + +func (x *node) CurrentEpoch() (uint64, error) { + return x.c.cfgNetmap.state.CurrentEpoch(), nil +} + +func (x *node) ResolveUserByPublicKey(bPublicKey []byte) (user.ID, error) { + buf := io.NewBufBinWriter() + emit.CheckSig(buf.BinWriter, bPublicKey) + h := hash.Hash160(buf.Bytes()) + + var res user.ID + res.SetScriptHash(h) + + return res, nil +} + +func (x *node) IsUserPublicKey(usr user.ID, bPublicKey []byte) (bool, error) { + buf := io.NewBufBinWriter() + emit.CheckSig(buf.BinWriter, bPublicKey) + h := hash.Hash160(buf.Bytes()) + + return bytes.Equal(h.BytesBE(), usr.WalletBytes()[1:1+util.Uint160Size]), nil +} + +func (x *node) IsInnerRingPublicKey(bPublicKey []byte) (bool, error) { + bKeys, err := x.innerRing.InnerRingKeys() + if err != nil { + return false, err + } + + for i := range bKeys { + if bytes.Equal(bKeys[i], bPublicKey) { + return true, nil + } + } + + return false, nil +} + +func (x *node) IsContainerNodePublicKey(cnrID cid.ID, bPublicKey []byte) (bool, error) { + cnr, err := x.c.cfgObject.cnrSource.Get(cnrID) + if err != nil { + return false, fmt.Errorf("read container info: %w", err) + } + + nm, err := netmap.GetLatestNetworkMap(x.c.netMapSource) + if err != nil { + return false, err + } + + cnrPolicy := cnr.Value.PlacementPolicy() + + ok, err := isContainerNodePublicKey(bPublicKey, *nm, cnrID, cnrPolicy) + if err != nil { + return false, err + } else if ok { + return true, nil + } + + // also check with previous epoch netmap: after epoch tick storage node may + // become out-of-container and try to migrate data + nm, err = netmap.GetPreviousNetworkMap(x.c.netMapSource) + if err != nil { + return false, err + } + + return isContainerNodePublicKey(bPublicKey, *nm, cnrID, cnrPolicy) +} + +func isContainerNodePublicKey(bPublicKey []byte, nm netmapSDK.NetMap, cnrID cid.ID, cnrPolicy netmapSDK.PlacementPolicy) (bool, error) { + nodeLists, err := nm.ContainerNodes(cnrPolicy, cnrID) + if err != nil { + return false, err + } + + for i := range nodeLists { + for j := range nodeLists[i] { + if bytes.Equal(nodeLists[i][j].PublicKey(), bPublicKey) { + return true, nil + } + } + } + + return false, nil +} + +func (x *node) ReadLocalObjectHeaders(cnr cid.ID, id oid.ID) (objectSDK.Object, error) { + var addr oid.Address + addr.SetContainer(cnr) + addr.SetObject(id) + + hdr, err := engine.Head(x.c.cfgObject.cfgLocalStorage.localStorage, addr) + if err != nil { + return objectSDK.Object{}, err + } + + return *hdr, nil +} diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index b966e9ce6d..530a2f5598 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -18,7 +18,6 @@ import ( cntClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/container" objectTransportGRPC "github.com/nspcc-dev/neofs-node/pkg/network/transport/object/grpc" objectService "github.com/nspcc-dev/neofs-node/pkg/services/object" - "github.com/nspcc-dev/neofs-node/pkg/services/object/acl" v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" deletesvcV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/delete/v2" @@ -36,7 +35,6 @@ import ( truststorage "github.com/nspcc-dev/neofs-node/pkg/services/reputation/local/storage" "github.com/nspcc-dev/neofs-sdk-go/client" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" objectSDK "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" apireputation "github.com/nspcc-dev/neofs-sdk-go/reputation" @@ -174,10 +172,6 @@ func initObjectService(c *cfg) { basicConstructor: c.putClientCache, } - irFetcher := &innerRingFetcherWithNotary{ - sidechain: c.cfgMorph.client, - } - c.replicator = replicator.New( replicator.WithLogger(c.log), replicator.WithPutTimeout( @@ -326,23 +320,7 @@ func initObjectService(c *cfg) { }, ) - aclSvc := v2.New( - v2.WithLogger(c.log), - v2.WithIRFetcher(newCachedIRFetcher(irFetcher)), - v2.WithNetmapSource(c.netMapSource), - v2.WithContainerSource( - c.cfgObject.cnrSource, - ), - v2.WithNextService(splitSvc), - v2.WithEACLChecker( - acl.NewChecker(new(acl.CheckerPrm). - SetNetmapState(c.cfgNetmap.state). - SetEACLSource(c.cfgObject.eaclSource). - SetValidator(eaclSDK.NewValidator()). - SetLocalStorage(ls), - ), - ), - ) + aclSvc := v2.New(newNode(c), splitSvc) var commonSvc objectService.Common commonSvc.Init(&c.internals, aclSvc) diff --git a/pkg/services/object/acl/acl.go b/pkg/services/object/acl/acl.go index 5360b06228..05ce668be9 100644 --- a/pkg/services/object/acl/acl.go +++ b/pkg/services/object/acl/acl.go @@ -1,264 +1,630 @@ package acl import ( - "crypto/ecdsa" - "crypto/elliptic" + "bytes" "errors" "fmt" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neofs-node/pkg/core/container" - "github.com/nspcc-dev/neofs-node/pkg/core/netmap" - "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" - eaclV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl/v2" - v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" + "github.com/nspcc-dev/neofs-sdk-go/bearer" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" "github.com/nspcc-dev/neofs-sdk-go/container/acl" - eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/nspcc-dev/neofs-sdk-go/session" "github.com/nspcc-dev/neofs-sdk-go/user" ) -// CheckerPrm groups parameters for Checker -// constructor. -type CheckerPrm struct { - eaclSrc container.EACLSource - validator *eaclSDK.Validator - localStorage *engine.StorageEngine - state netmap.State +// ContainerInfo groups information about NeoFS container processed by +// [Checker]. +type ContainerInfo struct { + BasicACL acl.Basic + Owner user.ID } -func (c *CheckerPrm) SetEACLSource(v container.EACLSource) *CheckerPrm { - c.eaclSrc = v - return c +// NeoFS provides access to the NeoFS network used by [Checker] to process. +type NeoFS interface { + // GetContainerInfo reads information about the NeoFS container by its ID. + // Returns [apistatus.ErrContainerNotFound] if the container is missing. + GetContainerInfo(cid.ID) (ContainerInfo, error) + + // GetExtendedACL reads extended ACL of the referenced NeoFS container. Called + // only for containers with extendable ACL. Returns [apistatus.ErrEACLNotFound] + // if the eACL is missing. + GetExtendedACL(cid.ID) (eacl.Table, error) + + // CurrentEpoch reads number of the current NeoFS epoch. + CurrentEpoch() (uint64, error) + + // ResolveUserByPublicKey resolves NeoFS user by the given binary public key. + ResolveUserByPublicKey(bPublicKey []byte) (user.ID, error) + + // IsUserPublicKey checks whether specified binary-encoded public key + // belongs to the specified NeoFS user. + IsUserPublicKey(usr user.ID, bPublicKey []byte) (bool, error) + + // IsInnerRingPublicKey checks whether specified binary-encoded public key + // belongs to any NeoFS Inner Ring member. + IsInnerRingPublicKey(bPublicKey []byte) (bool, error) + + // IsContainerNodePublicKey check whether specified binary-encoded public key + // belongs to any storage node selected to the referenced NeoFS container. + IsContainerNodePublicKey(cnr cid.ID, bPublicKey []byte) (bool, error) } -func (c *CheckerPrm) SetValidator(v *eaclSDK.Validator) *CheckerPrm { - c.validator = v - return c +// Node represents storage node within which [Checker] works. +type Node interface { + NeoFS + + // ReadLocalObjectHeaders reads all headers of the referenced object stored on + // the Node. Returns any error encountered that prevented object to be read. + ReadLocalObjectHeaders(cnr cid.ID, id oid.ID) (object.Object, error) } -func (c *CheckerPrm) SetLocalStorage(v *engine.StorageEngine) *CheckerPrm { - c.localStorage = v - return c +// Checker manages access to the NeoFS object(s) requested by external clients +// via upstream storage node. +type Checker struct { + node Node } -func (c *CheckerPrm) SetNetmapState(v netmap.State) *CheckerPrm { - c.state = v - return c +// NewChecker constructs Checker for the specified Node instance. +func NewChecker(node Node) *Checker { + if node == nil { + panic("nil node arg") + } + + return &Checker{ + node: node, + } } -// Checker implements v2.ACLChecker interfaces and provides -// ACL/eACL validation functionality. -type Checker struct { - eaclSrc container.EACLSource - validator *eaclSDK.Validator - localStorage *engine.StorageEngine - state netmap.State +// ContainerRequest groups information about client request related to the +// particular NeoFS container to be processed by [Checker]. +type ContainerRequest struct { + cnr cid.ID + + payload ContainerOpPayload + + bClientPubKey []byte + + reqHeaders RequestHeaders + + bearerToken *bearer.Token + + sessionToken *session.Object } -// Various EACL check errors. -var ( - errEACLDeniedByRule = errors.New("denied by rule") - errBearerExpired = errors.New("bearer token has expired") - errBearerInvalidSignature = errors.New("bearer token has invalid signature") - errBearerInvalidContainerID = errors.New("bearer token was created for another container") - errBearerNotSignedByOwner = errors.New("bearer token is not signed by the container owner") - errBearerInvalidOwner = errors.New("bearer token owner differs from the request sender") -) +// ContainerOpPayload groups information about particular operation within the +// upstream NeoFS container. +type ContainerOpPayload struct { + op acl.Op -// NewChecker creates Checker. -// Panics if at least one of the parameter is nil. -func NewChecker(prm *CheckerPrm) *Checker { - panicOnNil := func(fieldName string, field any) { - if field == nil { - panic(fmt.Sprintf("incorrect field %s (%T): %v", fieldName, field, field)) - } + objID *oid.ID + objHdrs *object.Object +} + +// PutRequest constructs OpPayload for the request to store object with +// specified header into the upstream container. +func PutRequest(objHdr object.Object) ContainerOpPayload { + var objID *oid.ID + + if id, set := objHdr.ID(); set { + objID = &id } - panicOnNil("EACLSource", prm.eaclSrc) - panicOnNil("EACLValidator", prm.validator) - panicOnNil("LocalStorageEngine", prm.localStorage) - panicOnNil("NetmapState", prm.state) + return ContainerOpPayload{ + op: acl.OpObjectPut, + objID: objID, + objHdrs: &objHdr, + } +} - return &Checker{ - eaclSrc: prm.eaclSrc, - validator: prm.validator, - localStorage: prm.localStorage, - state: prm.state, +// DeleteRequest constructs OpPayload for the request to delete object with +// specified ID from the upstream container. +func DeleteRequest(obj oid.ID) ContainerOpPayload { + return ContainerOpPayload{ + op: acl.OpObjectDelete, + objID: &obj, } } -// CheckBasicACL is a main check function for basic ACL. -func (c *Checker) CheckBasicACL(info v2.RequestInfo) bool { - // check basic ACL permissions - return info.BasicACL().IsOpAllowed(info.Operation(), info.RequestRole()) +// SearchRequest constructs OpPayload for the request to search for object in +// the upstream container. +func SearchRequest() ContainerOpPayload { + return ContainerOpPayload{ + op: acl.OpObjectSearch, + } } -// StickyBitCheck validates owner field in the request if sticky bit is enabled. -func (c *Checker) StickyBitCheck(info v2.RequestInfo, owner user.ID) bool { - // According to NeoFS specification sticky bit has no effect on system nodes - // for correct intra-container work with objects (in particular, replication). - if info.RequestRole() == acl.RoleContainer { - return true +// GetRequest constructs OpPayload for the request to get object with the +// specified ID from the upstream container. +func GetRequest(obj oid.ID) ContainerOpPayload { + return ContainerOpPayload{ + op: acl.OpObjectGet, + objID: &obj, + } +} + +// GetResponse constructs OpPayload for the response carrying given header of +// the requested object with the specified ID from the upstream container. +func GetResponse(obj oid.ID, hdr object.Object) ContainerOpPayload { + return ContainerOpPayload{ + op: acl.OpObjectGet, + objID: &obj, + objHdrs: &hdr, } +} + +// HeadRequest constructs OpPayload for the request to get header of the object +// with the specified ID from the upstream container. +func HeadRequest(obj oid.ID) ContainerOpPayload { + return ContainerOpPayload{ + op: acl.OpObjectHead, + objID: &obj, + } +} - if !info.BasicACL().Sticky() { - return true +// HeadResponse constructs OpPayload for the response to the request to get +// header of the object with the specified ID from the upstream container. +func HeadResponse(obj oid.ID, hdr object.Object) ContainerOpPayload { + return ContainerOpPayload{ + op: acl.OpObjectHead, + objID: &obj, + objHdrs: &hdr, } +} - if len(info.SenderKey()) == 0 { - return false +// PayloadRangeRequest constructs OpPayload for the request to get payload range +// of the object with the specified ID from the upstream container. +func PayloadRangeRequest(obj oid.ID) ContainerOpPayload { + return ContainerOpPayload{ + op: acl.OpObjectRange, + objID: &obj, } +} - return isOwnerFromKey(owner, info.SenderKey()) +// HashPayloadRangeRequest constructs OpPayload for the request to hash payload +// range of the object with the specified ID from the upstream container. +func HashPayloadRangeRequest(obj oid.ID) ContainerOpPayload { + return ContainerOpPayload{ + op: acl.OpObjectHash, + objID: &obj, + } } -// CheckEACL is a main check function for extended ACL. -func (c *Checker) CheckEACL(msg any, reqInfo v2.RequestInfo) error { - basicACL := reqInfo.BasicACL() - if !basicACL.Extendable() { - return nil +// NewContainerRequest constructs new ContainerRequest instance. +// +// Binary public key of the client MUST be non-empty. ContainerOpPayload MUST be +// initialized by one of the constructors. RequestHeaders headers MUST be +// non-nil. +func NewContainerRequest(cnr cid.ID, clientPubKey []byte, payload ContainerOpPayload, reqHeaders RequestHeaders) ContainerRequest { + switch { + case payload == ContainerOpPayload{}: + panic("uninitialized op payload") + case len(clientPubKey) == 0: + panic("empty client public key arg") + case reqHeaders == nil: + panic("nil request headers") } - // if bearer token is not allowed, then ignore it - if !basicACL.AllowedBearerRules(reqInfo.Operation()) { - reqInfo.CleanBearer() + return ContainerRequest{ + cnr: cnr, + payload: payload, + bClientPubKey: clientPubKey, + reqHeaders: reqHeaders, } +} - var table eaclSDK.Table - cnr := reqInfo.ContainerID() +// SetSessionToken sets unchecked session token attached to the request if any. +func (x *ContainerRequest) SetSessionToken(sessionToken session.Object) { + x.sessionToken = &sessionToken +} - bearerTok := reqInfo.Bearer() - if bearerTok == nil { - eaclInfo, err := c.eaclSrc.GetEACL(cnr) +// SetBearerToken sets unchecked bearer token attached to the request if any. +func (x *ContainerRequest) SetBearerToken(bearerToken bearer.Token) { + x.bearerToken = &bearerToken +} + +// CheckAccess checks whether ContainerRequest is compliant with the access +// policy of the container. Returns: +// - nil if operation is allowed +// - [AccessDeniedError] if operation is prohibited +// - [InvalidRequestError] if the request is invalid +// - [apistatus.ErrSessionTokenExpired] if specified session token expired +// - [apistatus.ErrContainerNotFound] if requested container is missing in the NeoFS +// - [ErrNotEnoughData] if there are not enough inputs to determine the exact +// result. Currently, can only happen with [HeadRequest] and [GetRequest]. For +// these cases it is necessary to check the response +func (c *Checker) CheckAccess(req ContainerRequest) error { + var curEpoch uint64 + var err error + + if req.sessionToken != nil { + curEpoch, err = c.node.CurrentEpoch() + if err != nil { + return fmt.Errorf("get current NeoFS epoch: %w", err) + } + + req.bClientPubKey, err = c.verifySessionTokenAndGetIssuerPublicKey(*req.sessionToken, curEpoch, req.bClientPubKey, req.payload.op, req.cnr, req.payload.objID) if err != nil { - if errors.Is(err, apistatus.ErrEACLNotFound) { - return nil - } return err } + } - table = *eaclInfo.Value - } else { - table = bearerTok.EACLTable() + cnrInfo, err := c.node.GetContainerInfo(req.cnr) + if err != nil { + return fmt.Errorf("read requested container: %w", err) } - // if bearer token is not present, isValidBearer returns true - if err := isValidBearer(reqInfo, c.state); err != nil { - return err + clientUsr, err := c.node.ResolveUserByPublicKey(req.bClientPubKey) + if err != nil { + return fmt.Errorf("resolve client user by its public key: %w", err) } - hdrSrcOpts := make([]eaclV2.Option, 0, 3) + clientRole, err := c.resolveClientRole(req.bClientPubKey, clientUsr, req.cnr, cnrInfo.Owner) + if err != nil { + return fmt.Errorf("resolve client role: %w", err) + } - hdrSrcOpts = append(hdrSrcOpts, - eaclV2.WithLocalObjectStorage(c.localStorage), - eaclV2.WithCID(cnr), - eaclV2.WithOID(reqInfo.ObjectID()), - ) + if !cnrInfo.BasicACL.IsOpAllowed(req.payload.op, clientRole) { + return newAccessDeniedError(newBasicRuleError(newForbiddenClientRoleError(clientRole))) + } - if req, ok := msg.(eaclV2.Request); ok { - hdrSrcOpts = append(hdrSrcOpts, eaclV2.WithServiceRequest(req)) - } else { - hdrSrcOpts = append(hdrSrcOpts, - eaclV2.WithServiceResponse( - msg.(eaclV2.Response), - reqInfo.Request().(eaclV2.Request), - ), - ) + // According to NeoFS specification, sticky rule has no effect on system nodes + // for correct intra-container work with objects (in particular, replication). + if clientRole != acl.RoleContainer && req.payload.op == acl.OpObjectPut && cnrInfo.BasicACL.Sticky() { + if req.payload.objHdrs == nil { + return newAccessDeniedError(newBasicRuleError(newStickyAccessRuleError(errMissingObjectHeader))) + } + + objOwner := req.payload.objHdrs.OwnerID() + if objOwner == nil { + return newAccessDeniedError(newBasicRuleError(newStickyAccessRuleError(errMissingObjectOwner))) + } else if !objOwner.Equals(cnrInfo.Owner) { + return newAccessDeniedError(newBasicRuleError(newStickyAccessRuleError(errObjectOwnerAuth))) + } } - hdrSrc, err := eaclV2.NewMessageHeaderSource(hdrSrcOpts...) + if cnrInfo.BasicACL.Extendable() { + // if bearer token is not allowed, it is just ignored + if req.bearerToken != nil && cnrInfo.BasicACL.AllowedBearerRules(req.payload.op) { + if curEpoch == 0 { + curEpoch, err = c.node.CurrentEpoch() + if err != nil { + return fmt.Errorf("get current NeoFS epoch: %w", err) + } + } + + eACL, err := c.getVerifiedBearerEACL(*req.bearerToken, curEpoch, req.cnr, cnrInfo.Owner, clientUsr) + if err != nil { + return fmt.Errorf("eACL from bearer token: %w", err) + } + + return c.checkEACLAccess(eACL, req.cnr, req.payload, req.reqHeaders, req.bClientPubKey, clientRole) + } else { + eACL, err := c.node.GetExtendedACL(req.cnr) + switch { + default: + return fmt.Errorf("read eACL of the requested container from NeoFS: %w", err) + case err == nil: + return c.checkEACLAccess(eACL, req.cnr, req.payload, req.reqHeaders, req.bClientPubKey, clientRole) + case errors.Is(err, apistatus.ErrEACLNotFound): + // unset rules have no effect + } + } + } + + return nil +} + +func (c *Checker) resolveClientRole(bClientPubKey []byte, clientUsr user.ID, cnrID cid.ID, cnrOwner user.ID) (acl.Role, error) { + if clientUsr.Equals(cnrOwner) { + return acl.RoleOwner, nil + } + + clientIsInnerRing, err := c.node.IsInnerRingPublicKey(bClientPubKey) if err != nil { - return fmt.Errorf("can't parse headers: %w", err) + return 0, fmt.Errorf("check Inner Ring public keys: %w", err) } - var eaclRole eaclSDK.Role - switch op := reqInfo.RequestRole(); op { - default: - eaclRole = eaclSDK.Role(op) - case acl.RoleOwner: - eaclRole = eaclSDK.RoleUser - case acl.RoleInnerRing, acl.RoleContainer: - eaclRole = eaclSDK.RoleSystem - case acl.RoleOthers: - eaclRole = eaclSDK.RoleOthers + if clientIsInnerRing { + return acl.RoleInnerRing, nil } - action, _ := c.validator.CalculateAction(new(eaclSDK.ValidationUnit). - WithRole(eaclRole). - WithOperation(eaclSDK.Operation(reqInfo.Operation())). - WithContainerID(&cnr). - WithSenderKey(reqInfo.SenderKey()). - WithHeaderSource(hdrSrc). - WithEACLTable(&table), - ) + clientIsStorageNode, err := c.node.IsContainerNodePublicKey(cnrID, bClientPubKey) + if err != nil { + return 0, fmt.Errorf("check container nodes' public keys: %w", err) + } - if action != eaclSDK.ActionAllow { - return errEACLDeniedByRule + if clientIsStorageNode { + return acl.RoleContainer, nil } - return nil + + return acl.RoleOthers, nil } -// isValidBearer checks whether bearer token was correctly signed by authorized -// entity. This method might be defined on whole ACL service because it will -// require fetching current epoch to check lifetime. -func isValidBearer(reqInfo v2.RequestInfo, st netmap.State) error { - ownerCnr := reqInfo.ContainerOwner() +func (c *Checker) verifySessionTokenAndGetIssuerPublicKey( + sessionToken session.Object, + curEpoch uint64, + bClientPubKey []byte, + op acl.Op, + cnr cid.ID, + obj *oid.ID, +) ([]byte, error) { + if !sessionToken.VerifySignature() { + return nil, newInvalidRequestError(newInvalidSessionTokenError(errInvalidSignature)) + } + + bIssuerPubKey := sessionToken.IssuerPublicKeyBytes() + issuerUsr := sessionToken.Issuer() + + isIssuerKey, err := c.node.IsUserPublicKey(issuerUsr, bIssuerPubKey) + if err != nil { + return nil, fmt.Errorf("check session issuer key binding: %w", err) + } + + if !isIssuerKey { + return nil, newInvalidRequestError(newInvalidSessionTokenError(errSessionIssuerAuth)) + } - token := reqInfo.Bearer() + if op != acl.OpObjectPut && op != acl.OpObjectDelete { + // PUT and DELETE are specific: sessions in such requests may be dynamic, i.e. + // opened using SessionService.Create and subject for the dynamically created + // session private key located on the local node. Corresponding checks are + // expected to be done at the other app layer. + var clientPubKey neofsecdsa.PublicKey - // 0. Check if bearer token is present in reqInfo. - if token == nil { - return nil + err = clientPubKey.Decode(bClientPubKey) + if err != nil { + return nil, newInvalidRequestError(newInvalidSessionTokenError(fmt.Errorf("decode public key from the signature: %w", err))) + } + + if !sessionToken.AssertAuthKey(&clientPubKey) { + return nil, newInvalidRequestError(newInvalidSessionTokenError(errSessionClientAuth)) + } } - // 1. First check token lifetime. Simplest verification. - if token.InvalidAt(st.CurrentEpoch()) { - return errBearerExpired + if !sessionToken.AssertContainer(cnr) { + return nil, newInvalidRequestError(newInvalidSessionTokenError(errSessionContainerMismatch)) } - // 2. Then check if bearer token is signed correctly. - if !token.VerifySignature() { - return errBearerInvalidSignature + if obj != nil && !sessionToken.AssertObject(*obj) { + // if session relates to object's removal, we don't check relation of the + // tombstone to the session here since user can't predict tomb's ID. + if op != acl.OpObjectPut || !sessionToken.AssertVerb(session.VerbObjectDelete) { + return nil, newInvalidRequestError(newInvalidSessionTokenError(errSessionObjectMismatch)) + } + } + + verbMatches := false + + switch op { + case acl.OpObjectPut: + verbMatches = sessionToken.AssertVerb(session.VerbObjectPut, session.VerbObjectDelete) + case acl.OpObjectDelete: + verbMatches = sessionToken.AssertVerb(session.VerbObjectDelete) + case acl.OpObjectGet: + verbMatches = sessionToken.AssertVerb(session.VerbObjectGet) + case acl.OpObjectHead: + verbMatches = sessionToken.AssertVerb( + session.VerbObjectHead, + session.VerbObjectGet, + session.VerbObjectDelete, + session.VerbObjectRange, + session.VerbObjectRangeHash) + case acl.OpObjectSearch: + verbMatches = sessionToken.AssertVerb(session.VerbObjectSearch, session.VerbObjectDelete) + case acl.OpObjectRange: + verbMatches = sessionToken.AssertVerb(session.VerbObjectRange, session.VerbObjectRangeHash) + case acl.OpObjectHash: + verbMatches = sessionToken.AssertVerb(session.VerbObjectRangeHash) } - // 3. Then check if container is either empty or equal to the container in the request. - cnr, isSet := token.EACLTable().CID() - if isSet && !cnr.Equals(reqInfo.ContainerID()) { - return errBearerInvalidContainerID + if !verbMatches { + return nil, newInvalidRequestError(newInvalidSessionTokenError(errSessionOpMismatch)) } - // 4. Then check if container owner signed this token. - if !token.ResolveIssuer().Equals(ownerCnr) { - // TODO: #767 in this case we can issue all owner keys from neofs.id and check once again - return errBearerNotSignedByOwner + if sessionToken.ExpiredAt(curEpoch) { + return nil, newInvalidRequestError(newInvalidSessionTokenError(apistatus.ErrSessionTokenExpired)) } - // 5. Then check if request sender has rights to use this token. - pubKey, err := keys.NewPublicKeyFromBytes(reqInfo.SenderKey(), elliptic.P256()) + if sessionToken.InvalidAt(curEpoch) { + return nil, newInvalidRequestError(newInvalidSessionTokenError(newInvalidAtEpochError(curEpoch))) + } + + return bIssuerPubKey, nil +} + +func (c *Checker) getVerifiedBearerEACL(bearerToken bearer.Token, curEpoch uint64, cnrID cid.ID, cnrOwner, clientUsr user.ID) (eacl.Table, error) { + eACL := bearerToken.EACLTable() + + if !bearerToken.VerifySignature() { + return eACL, newInvalidBearerTokenError(errInvalidSignature) + } + + bIssuerKey := bearerToken.SigningKeyBytes() + + issuerUsr, err := c.node.ResolveUserByPublicKey(bIssuerKey) if err != nil { - return fmt.Errorf("decode sender public key: %w", err) + return eACL, fmt.Errorf("resolve bearer user by public key from the token signature: %w", err) } - usrSender := user.ResolveFromECDSAPublicKey(ecdsa.PublicKey(*pubKey)) + if !issuerUsr.Equals(cnrOwner) { + return eACL, newInvalidBearerTokenError(errBearerIssuerAuth) + } - if !token.AssertUser(usrSender) { - // TODO: #767 in this case we can issue all owner keys from neofs.id and check once again - return errBearerInvalidOwner + if !bearerToken.AssertUser(clientUsr) { + return eACL, newInvalidBearerTokenError(errBearerClientAuth) + } + + cnrInToken, isSet := eACL.CID() + if isSet && !cnrInToken.Equals(cnrID) { + return eACL, newInvalidBearerTokenError(errBearerContainerMismatch) + } + + if bearerToken.InvalidAt(curEpoch) { + return eACL, newInvalidBearerTokenError(newInvalidAtEpochError(curEpoch)) + } + + return eACL, nil +} + +func (c *Checker) checkEACLAccess(eACL eacl.Table, cnr cid.ID, payload ContainerOpPayload, reqHeaders RequestHeaders, bClientPubKey []byte, clientRole acl.Role) error { + objHdrs := newObjectHeadersContext(c.node, cnr, payload) + + records := eACL.Records() + for i := range records { + var denyOnMatch bool + + switch action := records[i].Action(); action { + default: + return fmt.Errorf("process record #%d: unsupported action #%d", i, action) + case eacl.ActionDeny: + denyOnMatch = true + case eacl.ActionAllow: + denyOnMatch = false + } + + var op acl.Op + + switch records[i].Operation() { + default: + return fmt.Errorf("process record #%d: unsupported operation #%d", i, op) + case eacl.OperationGet: + op = acl.OpObjectGet + case eacl.OperationHead: + op = acl.OpObjectHead + case eacl.OperationPut: + op = acl.OpObjectPut + case eacl.OperationDelete: + op = acl.OpObjectDelete + case eacl.OperationSearch: + op = acl.OpObjectSearch + case eacl.OperationRange: + op = acl.OpObjectRange + case eacl.OperationRangeHash: + op = acl.OpObjectHash + } + + if op != payload.op { + continue + } + + matchesClient, err := ruleMatchesClient(records[i], clientRole, bClientPubKey) + if err != nil { + return fmt.Errorf("process record #%d: %w", i, err) + } + + if !matchesClient { + continue + } + + matchesResource, err := ruleMatchesResource(records[i], &objHdrs, reqHeaders) + if err != nil { + return fmt.Errorf("process record #%d: %w", i, err) + } + + if matchesResource { + if denyOnMatch { + return newAccessDeniedError(newExtendedRuleError(i)) + } + + return nil + } } return nil } -func isOwnerFromKey(id user.ID, key []byte) bool { - if key == nil { - return false +// checks whether given access rule matches client with specified role and +// binary public key. +func ruleMatchesClient(rec eacl.Record, clientRole acl.Role, bClientPubKey []byte) (bool, error) { + targets := rec.Targets() + for i := range targets { + if targetKeys := targets[i].BinaryKeys(); len(targetKeys) != 0 { + for i := range targetKeys { + if bytes.Equal(targetKeys[i], bClientPubKey) { + return true, nil + } + } + continue + } + + targetRole := targets[i].Role() + switch targetRole { + default: + return false, fmt.Errorf("process target #%d: unsupported subject role #%d", i, targetRole) + case eacl.RoleUnknown, eacl.RoleSystem: + // system role access modifications have been deprecated + case eacl.RoleUser: + if clientRole == acl.RoleOwner { + return true, nil + } + case eacl.RoleOthers: + if clientRole == acl.RoleOthers { + return true, nil + } + } } - pubKey, err := keys.NewPublicKeyFromBytes(key, elliptic.P256()) - if err != nil { - return false + return false, nil +} + +// checks whether given access rule matches the resource represented by +// specified headers. Returns [ErrNotEnoughData] if object headers are +// unavailable at the moment but may be so in other setting (e.g. GET and HEAD +// have no object header in request but have one in response). +func ruleMatchesResource(rec eacl.Record, objHeaders *objectHeadersContext, reqHeaders RequestHeaders) (bool, error) { + filters := rec.Filters() + matchedFilters := 0 + for i := range filters { + var hdrValue string + + switch hdrType := filters[i].From(); hdrType { + default: + return false, fmt.Errorf("process filter #%d: unsupported type of headers #%d", i, hdrType) + case eacl.HeaderFromService: + // ignored by storage nodes + continue + case eacl.HeaderFromRequest: + hdrValue = reqHeaders.GetRequestHeaderByKey(filters[i].Key()) + if hdrValue == "" { + continue + } + case eacl.HeaderFromObject: + var err error + hdrValue, err = objHeaders.getObjectHeaderByKey(filters[i].Key()) + if err != nil { + switch { + default: + return false, err + case errors.Is(err, errHeaderNotAvailable): + if objHeaders.op == acl.OpObjectGet || objHeaders.op == acl.OpObjectHead { + return false, ErrNotEnoughData + } + + continue + case errors.Is(err, errHeaderNotFound): + // process empty object header value then + } + } + } + + switch matcher := filters[i].Matcher(); matcher { + default: + return false, fmt.Errorf("process filter #%d: unsupported matcher #%d", i, matcher) + case eacl.MatchStringEqual: + if hdrValue == filters[i].Value() { + matchedFilters++ + } + case eacl.MatchStringNotEqual: + if hdrValue != filters[i].Value() { + matchedFilters++ + } + } } - return id.Equals(user.ResolveFromECDSAPublicKey(ecdsa.PublicKey(*pubKey))) + return matchedFilters == len(filters), nil } diff --git a/pkg/services/object/acl/acl_test.go b/pkg/services/object/acl/acl_test.go index 0052af2eb1..5e27de055d 100644 --- a/pkg/services/object/acl/acl_test.go +++ b/pkg/services/object/acl/acl_test.go @@ -1,90 +1,150 @@ package acl import ( + "bytes" "testing" - "github.com/nspcc-dev/neofs-node/pkg/core/container" - "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" - v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" - "github.com/nspcc-dev/neofs-sdk-go/container/acl" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/nspcc-dev/neofs-sdk-go/user" - usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" ) -type emptyEACLSource struct{} +type testNode struct { + // for args testing -func (e emptyEACLSource) GetEACL(_ cid.ID) (*container.EACL, error) { - return nil, nil + tb testing.TB + + clientUsr user.ID + clientPubKey []byte + + requestedCnr cid.ID + requestedObj *oid.ID + + // dynamic state + + containerInfoErr error + containerInfo ContainerInfo + + curEpochErr error + curEpoch uint64 + + eACLErr error + eACL eacl.Table + + usrErr error + usr user.ID + usrPubKey []byte + + innerRingErr error + innerRingKey []byte + + containerNodeErr error + containerNodeKey []byte + + localStorageErr error + localStorageHeader object.Object } -type emptyNetmapState struct{} +func newTestNode(tb testing.TB, requestedContainer cid.ID, clientUsr user.ID, clientPubKey []byte) *testNode { + return &testNode{ + tb: tb, + clientUsr: clientUsr, + clientPubKey: clientPubKey, + requestedCnr: requestedContainer, + } +} -func (e emptyNetmapState) CurrentEpoch() uint64 { - return 0 +func (x *testNode) setContainerInfo(info ContainerInfo) { + x.containerInfo = info + x.containerInfoErr = nil } -func TestStickyCheck(t *testing.T) { - checker := NewChecker(new(CheckerPrm). - SetLocalStorage(&engine.StorageEngine{}). - SetValidator(eaclSDK.NewValidator()). - SetEACLSource(emptyEACLSource{}). - SetNetmapState(emptyNetmapState{}), - ) +func (x *testNode) setContainerInfoError(err error) { + x.containerInfoErr = err +} - t.Run("system role", func(t *testing.T) { - var info v2.RequestInfo +func (x *testNode) GetContainerInfo(cnr cid.ID) (ContainerInfo, error) { + require.True(x.tb, cnr.Equals(x.requestedCnr), "only requested container's info should be read") + return x.containerInfo, x.containerInfoErr +} - info.SetSenderKey(make([]byte, 33)) // any non-empty key - info.SetRequestRole(acl.RoleContainer) +func (x *testNode) setEACL(eACL eacl.Table) { + x.eACL = eACL + x.eACLErr = nil +} - require.True(t, checker.StickyBitCheck(info, usertest.ID(t))) +func (x *testNode) setEACLError(err error) { + x.eACLErr = err +} - var basicACL acl.Basic - basicACL.MakeSticky() +func (x *testNode) GetExtendedACL(cnr cid.ID) (eacl.Table, error) { + require.True(x.tb, cnr.Equals(x.requestedCnr), "only requested container's eACL should be read") + return x.eACL, x.eACLErr +} + +func (x *testNode) setCurrentEpoch(epoch uint64) { + x.curEpoch = epoch + x.curEpochErr = nil +} - info.SetBasicACL(basicACL) +func (x *testNode) setCurrentEpochError(err error) { + x.curEpochErr = err +} - require.True(t, checker.StickyBitCheck(info, usertest.ID(t))) - }) +func (x *testNode) CurrentEpoch() (uint64, error) { + if x.curEpochErr != nil { + return 0, x.curEpochErr + } - t.Run("owner ID and/or public key emptiness", func(t *testing.T) { - var info v2.RequestInfo + return x.curEpoch, nil +} + +func (x *testNode) setUserPublicKey(usr user.ID, bPublicKey []byte) { + x.usr = usr + x.usrPubKey = bPublicKey + x.usrErr = nil +} - info.SetRequestRole(acl.RoleOthers) // should be non-system role +func (x *testNode) ResolveUserByPublicKey(bPublicKey []byte) (user.ID, error) { + require.Equal(x.tb, x.clientPubKey, bPublicKey, "only client public key must be processed") + return x.usr, x.usrErr +} - assertFn := func(isSticky, withKey, withOwner, expected bool) { - info := info - if isSticky { - var basicACL acl.Basic - basicACL.MakeSticky() +func (x *testNode) IsUserPublicKey(usr user.ID, bPublicKey []byte) (bool, error) { + require.Equal(x.tb, x.clientUsr, usr, "only client user must be processed") + require.Equal(x.tb, x.clientPubKey, bPublicKey, "only client public key must be processed") + return bytes.Equal(x.usrPubKey, bPublicKey), x.usrErr +} - info.SetBasicACL(basicACL) - } +func (x *testNode) IsInnerRingPublicKey(bPublicKey []byte) (bool, error) { + require.Equal(x.tb, x.clientPubKey, bPublicKey, "only client public key must be checked as Inner Ring") + return bytes.Equal(x.innerRingKey, bPublicKey), x.innerRingErr +} - if withKey { - info.SetSenderKey(make([]byte, 33)) - } else { - info.SetSenderKey(nil) - } +func (x *testNode) IsContainerNodePublicKey(cnr cid.ID, bPublicKey []byte) (bool, error) { + require.True(x.tb, cnr.Equals(x.requestedCnr), "only requested container's node keys should be read") + require.Equal(x.tb, x.clientPubKey, bPublicKey, "only client public key must be checked as container node") + return bytes.Equal(x.containerNodeKey, bPublicKey), x.containerNodeErr +} - var ownerID user.ID +func (x *testNode) setLocalObjectHeader(hdr object.Object) { + x.localStorageHeader = hdr + x.localStorageErr = nil +} - if withOwner { - ownerID = usertest.ID(t) - } +func (x *testNode) setLocalStorageError(err error) { + x.localStorageErr = err +} - require.Equal(t, expected, checker.StickyBitCheck(info, ownerID)) - } +func (x *testNode) ReadLocalObjectHeaders(cnr cid.ID, id oid.ID) (object.Object, error) { + require.True(x.tb, cnr.Equals(x.requestedCnr), "only requested container should be read from local storage") + require.NotNil(x.tb, x.requestedObj, "only operations with single object context should touch local storage") + require.Equal(x.tb, *x.requestedObj, id, "only requested object should be read form local storage") + return x.localStorageHeader, x.localStorageErr +} - assertFn(true, false, false, false) - assertFn(true, true, false, false) - assertFn(true, false, true, false) - assertFn(false, false, false, true) - assertFn(false, true, false, true) - assertFn(false, false, true, true) - assertFn(false, true, true, true) - }) +func TestNeoFSFailures(t *testing.T) { } diff --git a/pkg/services/object/acl/eacl/v2/eacl_test.go b/pkg/services/object/acl/eacl/v2/eacl_test.go deleted file mode 100644 index 1105b3bdc2..0000000000 --- a/pkg/services/object/acl/eacl/v2/eacl_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package v2 - -import ( - "crypto/ecdsa" - "errors" - "testing" - - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/session" - eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" - "github.com/nspcc-dev/neofs-sdk-go/object" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" - oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" - "github.com/stretchr/testify/require" -) - -type testLocalStorage struct { - t *testing.T - - expAddr oid.Address - - obj *object.Object - - err error -} - -func (s *testLocalStorage) Head(addr oid.Address) (*object.Object, error) { - require.True(s.t, addr.Container().Equals(s.expAddr.Container())) - require.True(s.t, addr.Object().Equals(s.expAddr.Object())) - - return s.obj, s.err -} - -func testXHeaders(strs ...string) []session.XHeader { - res := make([]session.XHeader, len(strs)/2) - - for i := 0; i < len(strs); i += 2 { - res[i/2].SetKey(strs[i]) - res[i/2].SetValue(strs[i+1]) - } - - return res -} - -func TestHeadRequest(t *testing.T) { - req := new(objectV2.HeadRequest) - - meta := new(session.RequestMetaHeader) - req.SetMetaHeader(meta) - - body := new(objectV2.HeadRequestBody) - req.SetBody(body) - - addr := oidtest.Address() - - var addrV2 refs.Address - addr.WriteToV2(&addrV2) - - body.SetAddress(&addrV2) - - xKey := "x-key" - xVal := "x-val" - xHdrs := testXHeaders( - xKey, xVal, - ) - - meta.SetXHeaders(xHdrs) - - obj := object.New() - - attrKey := "attr_key" - attrVal := "attr_val" - var attr object.Attribute - attr.SetKey(attrKey) - attr.SetValue(attrVal) - obj.SetAttributes(attr) - - table := new(eaclSDK.Table) - - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - senderKey := priv.PublicKey() - - r := eaclSDK.NewRecord() - r.SetOperation(eaclSDK.OperationHead) - r.SetAction(eaclSDK.ActionDeny) - r.AddFilter(eaclSDK.HeaderFromObject, eaclSDK.MatchStringEqual, attrKey, attrVal) - r.AddFilter(eaclSDK.HeaderFromRequest, eaclSDK.MatchStringEqual, xKey, xVal) - eaclSDK.AddFormedTarget(r, eaclSDK.RoleUnknown, (ecdsa.PublicKey)(*senderKey)) - - table.AddRecord(r) - - lStorage := &testLocalStorage{ - t: t, - expAddr: addr, - obj: obj, - } - - id := addr.Object() - - newSource := func(t *testing.T) eaclSDK.TypedHeaderSource { - hdrSrc, err := NewMessageHeaderSource( - WithObjectStorage(lStorage), - WithServiceRequest(req), - WithCID(addr.Container()), - WithOID(&id)) - require.NoError(t, err) - return hdrSrc - } - - cnr := addr.Container() - - unit := new(eaclSDK.ValidationUnit). - WithContainerID(&cnr). - WithOperation(eaclSDK.OperationHead). - WithSenderKey(senderKey.Bytes()). - WithEACLTable(table) - - validator := eaclSDK.NewValidator() - - checkAction(t, eaclSDK.ActionDeny, validator, unit.WithHeaderSource(newSource(t))) - - meta.SetXHeaders(nil) - - checkDefaultAction(t, validator, unit.WithHeaderSource(newSource(t))) - - meta.SetXHeaders(xHdrs) - - obj.SetAttributes() - - checkDefaultAction(t, validator, unit.WithHeaderSource(newSource(t))) - - lStorage.err = errors.New("any error") - - checkDefaultAction(t, validator, unit.WithHeaderSource(newSource(t))) - - r.SetAction(eaclSDK.ActionAllow) - - rID := eaclSDK.NewRecord() - rID.SetOperation(eaclSDK.OperationHead) - rID.SetAction(eaclSDK.ActionDeny) - rID.AddObjectIDFilter(eaclSDK.MatchStringEqual, addr.Object()) - eaclSDK.AddFormedTarget(rID, eaclSDK.RoleUnknown, (ecdsa.PublicKey)(*senderKey)) - - table = eaclSDK.NewTable() - table.AddRecord(r) - table.AddRecord(rID) - - unit.WithEACLTable(table) - checkDefaultAction(t, validator, unit.WithHeaderSource(newSource(t))) -} - -func checkAction(t *testing.T, expected eaclSDK.Action, v *eaclSDK.Validator, u *eaclSDK.ValidationUnit) { - actual, fromRule := v.CalculateAction(u) - require.True(t, fromRule) - require.Equal(t, expected, actual) -} - -func checkDefaultAction(t *testing.T, v *eaclSDK.Validator, u *eaclSDK.ValidationUnit) { - actual, fromRule := v.CalculateAction(u) - require.False(t, fromRule) - require.Equal(t, eaclSDK.ActionAllow, actual) -} diff --git a/pkg/services/object/acl/eacl/v2/headers.go b/pkg/services/object/acl/eacl/v2/headers.go deleted file mode 100644 index 54d3569ac5..0000000000 --- a/pkg/services/object/acl/eacl/v2/headers.go +++ /dev/null @@ -1,241 +0,0 @@ -package v2 - -import ( - "errors" - "fmt" - - "github.com/nspcc-dev/neofs-api-go/v2/acl" - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - refsV2 "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/session" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" - "github.com/nspcc-dev/neofs-sdk-go/object" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" - "github.com/nspcc-dev/neofs-sdk-go/user" -) - -type Option func(*cfg) - -type cfg struct { - storage ObjectStorage - - msg xHeaderSource - - cnr cid.ID - obj *oid.ID -} - -type ObjectStorage interface { - Head(oid.Address) (*object.Object, error) -} - -type Request interface { - GetMetaHeader() *session.RequestMetaHeader -} - -type Response interface { - GetMetaHeader() *session.ResponseMetaHeader -} - -type headerSource struct { - requestHeaders []eaclSDK.Header - objectHeaders []eaclSDK.Header - - incompleteObjectHeaders bool -} - -func defaultCfg() *cfg { - return &cfg{ - storage: new(localStorage), - } -} - -func NewMessageHeaderSource(opts ...Option) (eaclSDK.TypedHeaderSource, error) { - cfg := defaultCfg() - - for i := range opts { - opts[i](cfg) - } - - if cfg.msg == nil { - return nil, errors.New("message is not provided") - } - - var res headerSource - - err := cfg.readObjectHeaders(&res) - if err != nil { - return nil, err - } - - res.requestHeaders = requestHeaders(cfg.msg) - - return res, nil -} - -func (h headerSource) HeadersOfType(typ eaclSDK.FilterHeaderType) ([]eaclSDK.Header, bool) { - switch typ { - default: - return nil, true - case eaclSDK.HeaderFromRequest: - return h.requestHeaders, true - case eaclSDK.HeaderFromObject: - return h.objectHeaders, !h.incompleteObjectHeaders - } -} - -type xHeader session.XHeader - -func (x xHeader) Key() string { - return (*session.XHeader)(&x).GetKey() -} - -func (x xHeader) Value() string { - return (*session.XHeader)(&x).GetValue() -} - -func requestHeaders(msg xHeaderSource) []eaclSDK.Header { - return msg.GetXHeaders() -} - -var errMissingOID = errors.New("object ID is missing") - -func (h *cfg) readObjectHeaders(dst *headerSource) error { - switch m := h.msg.(type) { - default: - panic(fmt.Sprintf("unexpected message type %T", h.msg)) - case requestXHeaderSource: - switch req := m.req.(type) { - case - *objectV2.GetRequest, - *objectV2.HeadRequest: - if h.obj == nil { - return errMissingOID - } - - objHeaders, completed := h.localObjectHeaders(h.cnr, h.obj) - - dst.objectHeaders = objHeaders - dst.incompleteObjectHeaders = !completed - case - *objectV2.GetRangeRequest, - *objectV2.GetRangeHashRequest, - *objectV2.DeleteRequest: - if h.obj == nil { - return errMissingOID - } - - dst.objectHeaders = addressHeaders(h.cnr, h.obj) - case *objectV2.PutRequest: - if v, ok := req.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit); ok { - oV2 := new(objectV2.Object) - oV2.SetObjectID(v.GetObjectID()) - oV2.SetHeader(v.GetHeader()) - - dst.objectHeaders = headersFromObject(object.NewFromV2(oV2), h.cnr, h.obj) - } - case *objectV2.SearchRequest: - cnrV2 := req.GetBody().GetContainerID() - var cnr cid.ID - - if cnrV2 != nil { - if err := cnr.ReadFromV2(*cnrV2); err != nil { - return fmt.Errorf("can't parse container ID: %w", err) - } - } - - dst.objectHeaders = []eaclSDK.Header{cidHeader(cnr)} - } - case responseXHeaderSource: - switch resp := m.resp.(type) { - default: - objectHeaders, completed := h.localObjectHeaders(h.cnr, h.obj) - - dst.objectHeaders = objectHeaders - dst.incompleteObjectHeaders = !completed - case *objectV2.GetResponse: - if v, ok := resp.GetBody().GetObjectPart().(*objectV2.GetObjectPartInit); ok { - oV2 := new(objectV2.Object) - oV2.SetObjectID(v.GetObjectID()) - oV2.SetHeader(v.GetHeader()) - - dst.objectHeaders = headersFromObject(object.NewFromV2(oV2), h.cnr, h.obj) - } - case *objectV2.HeadResponse: - oV2 := new(objectV2.Object) - - var hdr *objectV2.Header - - switch v := resp.GetBody().GetHeaderPart().(type) { - case *objectV2.ShortHeader: - hdr = new(objectV2.Header) - - var idV2 refsV2.ContainerID - h.cnr.WriteToV2(&idV2) - - hdr.SetContainerID(&idV2) - hdr.SetVersion(v.GetVersion()) - hdr.SetCreationEpoch(v.GetCreationEpoch()) - hdr.SetOwnerID(v.GetOwnerID()) - hdr.SetObjectType(v.GetObjectType()) - hdr.SetPayloadLength(v.GetPayloadLength()) - case *objectV2.HeaderWithSignature: - hdr = v.GetHeader() - } - - oV2.SetHeader(hdr) - - dst.objectHeaders = headersFromObject(object.NewFromV2(oV2), h.cnr, h.obj) - } - } - - return nil -} - -func (h *cfg) localObjectHeaders(cnr cid.ID, idObj *oid.ID) ([]eaclSDK.Header, bool) { - if idObj != nil { - var addr oid.Address - addr.SetContainer(cnr) - addr.SetObject(*idObj) - - obj, err := h.storage.Head(addr) - if err == nil { - return headersFromObject(obj, cnr, idObj), true - } - } - - return addressHeaders(cnr, idObj), false -} - -func cidHeader(idCnr cid.ID) sysObjHdr { - return sysObjHdr{ - k: acl.FilterObjectContainerID, - v: idCnr.EncodeToString(), - } -} - -func oidHeader(obj oid.ID) sysObjHdr { - return sysObjHdr{ - k: acl.FilterObjectID, - v: obj.EncodeToString(), - } -} - -func ownerIDHeader(ownerID user.ID) sysObjHdr { - return sysObjHdr{ - k: acl.FilterObjectOwnerID, - v: ownerID.EncodeToString(), - } -} - -func addressHeaders(cnr cid.ID, oid *oid.ID) []eaclSDK.Header { - hh := make([]eaclSDK.Header, 0, 2) - hh = append(hh, cidHeader(cnr)) - - if oid != nil { - hh = append(hh, oidHeader(*oid)) - } - - return hh -} diff --git a/pkg/services/object/acl/eacl/v2/localstore.go b/pkg/services/object/acl/eacl/v2/localstore.go deleted file mode 100644 index 74192c588e..0000000000 --- a/pkg/services/object/acl/eacl/v2/localstore.go +++ /dev/null @@ -1,21 +0,0 @@ -package v2 - -import ( - "io" - - "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" - objectSDK "github.com/nspcc-dev/neofs-sdk-go/object" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" -) - -type localStorage struct { - ls *engine.StorageEngine -} - -func (s *localStorage) Head(addr oid.Address) (*objectSDK.Object, error) { - if s.ls == nil { - return nil, io.ErrUnexpectedEOF - } - - return engine.Head(s.ls, addr) -} diff --git a/pkg/services/object/acl/eacl/v2/object.go b/pkg/services/object/acl/eacl/v2/object.go deleted file mode 100644 index 4a1e043eff..0000000000 --- a/pkg/services/object/acl/eacl/v2/object.go +++ /dev/null @@ -1,92 +0,0 @@ -package v2 - -import ( - "strconv" - - "github.com/nspcc-dev/neofs-api-go/v2/acl" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" - "github.com/nspcc-dev/neofs-sdk-go/object" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" -) - -type sysObjHdr struct { - k, v string -} - -func (s sysObjHdr) Key() string { - return s.k -} - -func (s sysObjHdr) Value() string { - return s.v -} - -func u64Value(v uint64) string { - return strconv.FormatUint(v, 10) -} - -func headersFromObject(obj *object.Object, cnr cid.ID, oid *oid.ID) []eaclSDK.Header { - var count int - for obj := obj; obj != nil; obj = obj.Parent() { - count += 9 + len(obj.Attributes()) - } - - res := make([]eaclSDK.Header, 0, count) - for ; obj != nil; obj = obj.Parent() { - res = append(res, - cidHeader(cnr), - // creation epoch - sysObjHdr{ - k: acl.FilterObjectCreationEpoch, - v: u64Value(obj.CreationEpoch()), - }, - // payload size - sysObjHdr{ - k: acl.FilterObjectPayloadLength, - v: u64Value(obj.PayloadSize()), - }, - // object version - sysObjHdr{ - k: acl.FilterObjectVersion, - v: obj.Version().String(), - }, - // object type - sysObjHdr{ - k: acl.FilterObjectType, - v: obj.Type().String(), - }, - ) - - if oid != nil { - res = append(res, oidHeader(*oid)) - } - - if idOwner := obj.OwnerID(); idOwner != nil { - res = append(res, ownerIDHeader(*idOwner)) - } - - cs, ok := obj.PayloadChecksum() - if ok { - res = append(res, sysObjHdr{ - k: acl.FilterObjectPayloadHash, - v: cs.String(), - }) - } - - cs, ok = obj.PayloadHomomorphicHash() - if ok { - res = append(res, sysObjHdr{ - k: acl.FilterObjectHomomorphicHash, - v: cs.String(), - }) - } - - attrs := obj.Attributes() - for i := range attrs { - res = append(res, &attrs[i]) // only pointer attrs can implement eaclSDK.Header interface - } - } - - return res -} diff --git a/pkg/services/object/acl/eacl/v2/opts.go b/pkg/services/object/acl/eacl/v2/opts.go deleted file mode 100644 index 4a653757fe..0000000000 --- a/pkg/services/object/acl/eacl/v2/opts.go +++ /dev/null @@ -1,50 +0,0 @@ -package v2 - -import ( - "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" -) - -func WithObjectStorage(v ObjectStorage) Option { - return func(c *cfg) { - c.storage = v - } -} - -func WithLocalObjectStorage(v *engine.StorageEngine) Option { - return func(c *cfg) { - c.storage = &localStorage{ - ls: v, - } - } -} - -func WithServiceRequest(v Request) Option { - return func(c *cfg) { - c.msg = requestXHeaderSource{ - req: v, - } - } -} - -func WithServiceResponse(resp Response, req Request) Option { - return func(c *cfg) { - c.msg = responseXHeaderSource{ - resp: resp, - req: req, - } - } -} - -func WithCID(v cid.ID) Option { - return func(c *cfg) { - c.cnr = v - } -} - -func WithOID(v *oid.ID) Option { - return func(c *cfg) { - c.obj = v - } -} diff --git a/pkg/services/object/acl/eacl/v2/xheader.go b/pkg/services/object/acl/eacl/v2/xheader.go deleted file mode 100644 index aa6f5f9da8..0000000000 --- a/pkg/services/object/acl/eacl/v2/xheader.go +++ /dev/null @@ -1,61 +0,0 @@ -package v2 - -import ( - "github.com/nspcc-dev/neofs-api-go/v2/session" - eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" -) - -type xHeaderSource interface { - GetXHeaders() []eaclSDK.Header -} - -type requestXHeaderSource struct { - req Request -} - -type responseXHeaderSource struct { - resp Response - - req Request -} - -func (s requestXHeaderSource) GetXHeaders() []eaclSDK.Header { - ln := 0 - - for meta := s.req.GetMetaHeader(); meta != nil; meta = meta.GetOrigin() { - ln += len(meta.GetXHeaders()) - } - - res := make([]eaclSDK.Header, 0, ln) - for meta := s.req.GetMetaHeader(); meta != nil; meta = meta.GetOrigin() { - x := meta.GetXHeaders() - for i := range x { - res = append(res, (xHeader)(x[i])) - } - } - - return res -} - -func (s responseXHeaderSource) GetXHeaders() []eaclSDK.Header { - ln := 0 - xHdrs := make([][]session.XHeader, 0) - - for meta := s.req.GetMetaHeader(); meta != nil; meta = meta.GetOrigin() { - x := meta.GetXHeaders() - - ln += len(x) - - xHdrs = append(xHdrs, x) - } - - res := make([]eaclSDK.Header, 0, ln) - - for i := range xHdrs { - for j := range xHdrs[i] { - res = append(res, xHeader(xHdrs[i][j])) - } - } - - return res -} diff --git a/pkg/services/object/acl/errors.go b/pkg/services/object/acl/errors.go new file mode 100644 index 0000000000..d7b0aef2cb --- /dev/null +++ b/pkg/services/object/acl/errors.go @@ -0,0 +1,234 @@ +package acl + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neofs-sdk-go/container/acl" +) + +// Various generic errors. +var ( + // errInvalidSignature is returned when digital signature is considered invalid. + errInvalidSignature = errors.New("invalid signature") + // errMissingObjectHeader is returned when object headers is missing. + errMissingObjectHeader = errors.New("missing object header") + // errMissingObjectOwner is returned when object owner is missing. + errMissingObjectOwner = errors.New("missing object owner") + // errObjectOwnerAuth is returned when object owner's authorization failed. + errObjectOwnerAuth = errors.New("object owner authorization failed") +) + +// invalidAtEpochError is returned when some entity is invalid at particular +// epoch. +type invalidAtEpochError struct { + epoch uint64 +} + +// newInvalidAtEpochError constructs invalidAtEpochError with the given epoch. +func newInvalidAtEpochError(epoch uint64) invalidAtEpochError { + return invalidAtEpochError{ + epoch: epoch, + } +} + +func (x invalidAtEpochError) Error() string { + return fmt.Sprintf("invalid at epoch #%d", x.epoch) +} + +// ErrNotEnoughData is returned when the current data is insufficient for +// verification. +var ErrNotEnoughData = errors.New("not enough data") + +// InvalidRequestError is an error returned when request data is considered +// invalid for some reason. The reason is wrapped compatible with [errors] +// package functionality. +type InvalidRequestError struct { + cause error +} + +// newInvalidRequestError constructs InvalidRequestError with the given cause. +func newInvalidRequestError(cause error) InvalidRequestError { + return InvalidRequestError{ + cause: cause, + } +} + +func (x InvalidRequestError) Error() string { + return fmt.Sprintf("invalid request: %s", x.cause) +} + +// Unwrap unwraps the InvalidRequestError cause. +func (x InvalidRequestError) Unwrap() error { + return x.cause +} + +// AccessDeniedError is returned when access is denied for some reason. The +// reason is wrapped compatible with [errors] package functionality. +type AccessDeniedError struct { + cause error +} + +// newAccessDeniedError constructs AccessDeniedError with the given cause. +func newAccessDeniedError(cause error) AccessDeniedError { + return AccessDeniedError{ + cause: cause, + } +} + +func (x AccessDeniedError) Error() string { + return fmt.Sprintf("access denied: %s", x.cause) +} + +// Unwrap unwraps the AccessDeniedError cause. +func (x AccessDeniedError) Unwrap() error { + return x.cause +} + +// basicAccessRuleError is triggered by some basic access rule. The reason is +// wrapped compatible with [errors] package functionality. +type basicAccessRuleError struct { + cause error +} + +// newBasicRuleError constructs basicAccessRuleError with the given cause. +func newBasicRuleError(cause error) basicAccessRuleError { + return basicAccessRuleError{ + cause: cause, + } +} + +func (x basicAccessRuleError) Error() string { + return fmt.Sprintf("basic access rule triggered: %s", x.cause) +} + +// Unwrap unwraps the basicAccessRuleError cause. +func (x basicAccessRuleError) Unwrap() error { + return x.cause +} + +// forbiddenClientRoleError is returned on forbidden client role. +type forbiddenClientRoleError acl.Role + +// newForbiddenClientRoleError constructs forbiddenClientRoleError for the given +// client role. +func newForbiddenClientRoleError(role acl.Role) forbiddenClientRoleError { + return forbiddenClientRoleError(role) +} + +func (x forbiddenClientRoleError) Error() string { + return fmt.Sprintf("forbidden client role %s", acl.Role(x)) +} + +// stickyAccessRuleError is triggered by the sticky access rule. The reason is +// wrapped compatible with [errors] package functionality. +type stickyAccessRuleError struct { + cause error +} + +// newStickyAccessRuleError constructs stickyAccessRuleError with the given cause. +func newStickyAccessRuleError(cause error) stickyAccessRuleError { + return stickyAccessRuleError{ + cause: cause, + } +} + +func (x stickyAccessRuleError) Error() string { + return fmt.Sprintf("basic access rule triggered: %s", x.cause) +} + +// Unwrap unwraps the basicAccessRuleError cause. +func (x stickyAccessRuleError) Unwrap() error { + return x.cause +} + +// extendedAccessRuleError is triggered by some extended access rule. +type extendedAccessRuleError struct { + index int +} + +// newExtendedRuleError constructs extendedAccessRuleError for the given list +// index. +func newExtendedRuleError(index int) extendedAccessRuleError { + return extendedAccessRuleError{ + index: index, + } +} + +func (x extendedAccessRuleError) Error() string { + return fmt.Sprintf("access rule #%d triggered", x.index) +} + +// invalidBearerTokenError is returned when a bearer token is considered invalid +// for some reason. The reason is wrapped compatible with [errors] package +// functionality. +type invalidBearerTokenError struct { + cause error +} + +// Various bearer errors. +var ( + // errBearerIssuerAuth is returned when bearer issuer authorization failed. + errBearerIssuerAuth = errors.New("bearer token is not issued by the owner of the requested container") + // errBearerClientAuth is returned when bearer client authorization failed. + errBearerClientAuth = errors.New("bearer token is not subject for the client") + // errBearerContainerMismatch is returned when bearer context does not apply to + // the requested container. + errBearerContainerMismatch = errors.New("bearer token does not target the requested container") +) + +// newInvalidBearerTokenError constructs invalidBearerTokenError with the given cause. +func newInvalidBearerTokenError(cause error) invalidBearerTokenError { + return invalidBearerTokenError{ + cause: cause, + } +} + +func (x invalidBearerTokenError) Error() string { + return fmt.Sprintf("invalid bearer token: %s", x.cause) +} + +// Unwrap unwraps the invalidBearerTokenError cause. +func (x invalidBearerTokenError) Unwrap() error { + return x.cause +} + +// invalidSessionTokenError is returned when a session token is considered +// invalid for some reason. The reason is wrapped compatible with [errors] +// package functionality. +type invalidSessionTokenError struct { + cause error +} + +// Various session errors. +var ( + // errSessionIssuerAuth is returned when session issuer authorization failed. + errSessionIssuerAuth = errors.New("session token is signed by the key not bound to the issuer") + // errSessionClientAuth is returned when session client authorization failed. + errSessionClientAuth = errors.New("session is not subject for the client") + // errSessionOpMismatch is returned when session context does not apply to the + // requested operation. + errSessionOpMismatch = errors.New("session does not target the requested container") + // errSessionContainerMismatch is returned when session context does not apply + // to the requested container. + errSessionContainerMismatch = errors.New("session does not target the requested container") + // errSessionObjectMismatch is returned when session context does not apply to + // the requested object. + errSessionObjectMismatch = errors.New("session does not target the requested object") +) + +// newInvalidSessionTokenError constructs invalidSessionTokenError with the given cause. +func newInvalidSessionTokenError(cause error) invalidSessionTokenError { + return invalidSessionTokenError{ + cause: cause, + } +} + +func (x invalidSessionTokenError) Error() string { + return fmt.Sprintf("invalid session token: %s", x.cause) +} + +// Unwrap unwraps the invalidSessionTokenError cause. +func (x invalidSessionTokenError) Unwrap() error { + return x.cause +} diff --git a/pkg/services/object/acl/headers.go b/pkg/services/object/acl/headers.go new file mode 100644 index 0000000000..b9f2da4667 --- /dev/null +++ b/pkg/services/object/acl/headers.go @@ -0,0 +1,154 @@ +package acl + +import ( + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + + v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" + "github.com/nspcc-dev/neofs-sdk-go/container/acl" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/nspcc-dev/neofs-sdk-go/version" +) + +// RequestHeaders represents key-value header map of the request. +type RequestHeaders interface { + // GetRequestHeaderByKey returns value of the request header by key or zero if + // the header is missing. + GetRequestHeaderByKey(key string) string +} + +var ( + errHeaderNotAvailable = errors.New("unavailable header") + errHeaderNotFound = errors.New("header not found") +) + +type objectHeadersContext struct { + // static + + node Node + + op acl.Op + + cnr cid.ID + + objID *oid.ID + + // dynamic + + localObjUnavailable bool + + objHeaders *object.Object +} + +func newObjectHeadersContext(node Node, cnr cid.ID, payload ContainerOpPayload) objectHeadersContext { + return objectHeadersContext{ + node: node, + op: payload.op, + cnr: cnr, + objID: payload.objID, + objHeaders: payload.objHdrs, + } +} + +// returns string value of the object header described by the specified key. If +// object headers are not presented in the op payload, local object from the node +// is used (if available). Returns: +// - errHeaderNotAvailable if value cannot be gotten in all possible ways +// - errHeaderNotFound if object header with the specified key is missing +// in the object +func (x *objectHeadersContext) getObjectHeaderByKey(key string) (string, error) { + if key == v2acl.FilterObjectContainerID { + return x.cnr.EncodeToString(), nil + } + + if x.objHeaders == nil { + if x.localObjUnavailable { + // we already tried, see code below + return "", errHeaderNotAvailable + } + + if x.objID == nil { + // operation context is wider than single object, so we cannot process + // headers of the particular object + return "", errHeaderNotAvailable + } + + // try to get object headers from the local node's storage + hdr, err := x.node.ReadLocalObjectHeaders(x.cnr, *x.objID) // nil checked in switch above + if err != nil { + // cache failure of local storage op to prevent undesired repeat: + // recall will stop on switch above + x.localObjUnavailable = true + return "", fmt.Errorf("%w: %v", errHeaderNotAvailable, err) + } + + x.objHeaders = &hdr + } + + if !strings.HasPrefix(key, v2acl.ObjectFilterPrefix) { + attrs := x.objHeaders.Attributes() + for i := range attrs { + if attrs[i].Key() == key { + return attrs[i].Value(), nil + } + } + + return "", errHeaderNotFound + } + + switch key { + default: + return "", fmt.Errorf("unsupported header %q", key) + case v2acl.FilterObjectVersion: + ver := x.objHeaders.Version() + if ver == nil { + return "", errHeaderNotFound + } + + return version.EncodeToString(*ver), nil + case v2acl.FilterObjectID: + if x.objID == nil { + if x.op != acl.OpObjectPut { + // PUT request is the only case when object ID may be missing while header is + // available (see PutRequest func), so any other case is worth panic + panic(fmt.Sprintf("missing object ID in op %s", x.op)) + } + + return "", errHeaderNotFound + } + + return x.objID.EncodeToString(), nil + case v2acl.FilterObjectOwnerID: + owner := x.objHeaders.OwnerID() + if owner == nil { + return "", errHeaderNotFound + } + + return owner.EncodeToString(), nil + case v2acl.FilterObjectCreationEpoch: + return strconv.FormatUint(x.objHeaders.CreationEpoch(), 10), nil + case v2acl.FilterObjectPayloadLength: + return strconv.FormatUint(x.objHeaders.PayloadSize(), 10), nil + case v2acl.FilterObjectType: + return x.objHeaders.Type().EncodeToString(), nil + case v2acl.FilterObjectPayloadHash: + cs, set := x.objHeaders.PayloadChecksum() + if !set { + return "", errHeaderNotFound + } + + return hex.EncodeToString(cs.Value()), nil + case v2acl.FilterObjectHomomorphicHash: + cs, set := x.objHeaders.PayloadChecksum() + if !set { + return "", errHeaderNotFound + } + + return hex.EncodeToString(cs.Value()), nil + } +} diff --git a/pkg/services/object/acl/v2/classifier.go b/pkg/services/object/acl/v2/classifier.go deleted file mode 100644 index d4901a3903..0000000000 --- a/pkg/services/object/acl/v2/classifier.go +++ /dev/null @@ -1,136 +0,0 @@ -package v2 - -import ( - "bytes" - - core "github.com/nspcc-dev/neofs-node/pkg/core/netmap" - "github.com/nspcc-dev/neofs-sdk-go/container" - "github.com/nspcc-dev/neofs-sdk-go/container/acl" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - "github.com/nspcc-dev/neofs-sdk-go/netmap" - "go.uber.org/zap" -) - -type senderClassifier struct { - log *zap.Logger - innerRing InnerRingFetcher - netmap core.Source -} - -type classifyResult struct { - role acl.Role - key []byte -} - -func (c senderClassifier) classify( - req MetaWithToken, - idCnr cid.ID, - cnr container.Container) (res *classifyResult, err error) { - ownerID, ownerKey, err := req.RequestOwner() - if err != nil { - return nil, err - } - - // TODO: #767 get owner from neofs.id if present - - // if request owner is the same as container owner, return RoleUser - if ownerID.Equals(cnr.Owner()) { - return &classifyResult{ - role: acl.RoleOwner, - key: ownerKey, - }, nil - } - - isInnerRingNode, err := c.isInnerRingKey(ownerKey) - if err != nil { - // do not throw error, try best case matching - c.log.Debug("can't check if request from inner ring", - zap.String("error", err.Error())) - } else if isInnerRingNode { - return &classifyResult{ - role: acl.RoleInnerRing, - key: ownerKey, - }, nil - } - - isContainerNode, err := c.isContainerKey(ownerKey, idCnr, cnr) - if err != nil { - // error might happen if request has `RoleOther` key and placement - // is not possible for previous epoch, so - // do not throw error, try best case matching - c.log.Debug("can't check if request from container node", - zap.String("error", err.Error())) - } else if isContainerNode { - return &classifyResult{ - role: acl.RoleContainer, - key: ownerKey, - }, nil - } - - // if none of above, return RoleOthers - return &classifyResult{ - role: acl.RoleOthers, - key: ownerKey, - }, nil -} - -func (c senderClassifier) isInnerRingKey(owner []byte) (bool, error) { - innerRingKeys, err := c.innerRing.InnerRingKeys() - if err != nil { - return false, err - } - - // if request owner key in the inner ring list, return RoleSystem - for i := range innerRingKeys { - if bytes.Equal(innerRingKeys[i], owner) { - return true, nil - } - } - - return false, nil -} - -func (c senderClassifier) isContainerKey( - owner []byte, idCnr cid.ID, - cnr container.Container) (bool, error) { - nm, err := core.GetLatestNetworkMap(c.netmap) // first check current netmap - if err != nil { - return false, err - } - - in, err := lookupKeyInContainer(nm, owner, idCnr, cnr) - if err != nil { - return false, err - } else if in { - return true, nil - } - - // then check previous netmap, this can happen in-between epoch change - // when node migrates data from last epoch container - nm, err = core.GetPreviousNetworkMap(c.netmap) - if err != nil { - return false, err - } - - return lookupKeyInContainer(nm, owner, idCnr, cnr) -} - -func lookupKeyInContainer( - nm *netmap.NetMap, - owner []byte, idCnr cid.ID, - cnr container.Container) (bool, error) { - cnrVectors, err := nm.ContainerNodes(cnr.PlacementPolicy(), idCnr) - if err != nil { - return false, err - } - - for i := range cnrVectors { - for j := range cnrVectors[i] { - if bytes.Equal(cnrVectors[i][j].PublicKey(), owner) { - return true, nil - } - } - } - - return false, nil -} diff --git a/pkg/services/object/acl/v2/errors.go b/pkg/services/object/acl/v2/errors.go index 432894df89..5169ebcd44 100644 --- a/pkg/services/object/acl/v2/errors.go +++ b/pkg/services/object/acl/v2/errors.go @@ -1,39 +1,71 @@ -package v2 +package v2acl import ( + "errors" "fmt" - - apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" ) -const invalidRequestMessage = "malformed request" +// Various generic errors. +var ( + // errMissingRequestBody is returned when request body field is missing. + errMissingRequestBody = errors.New("missing request body") + // errMissingRequestVerificationHeader is returned when request verification header is missing. + errMissingRequestVerificationHeader = errors.New("missing request verification header") + // errMultipleStreamInitializers is returned when initial message of the stream + // is repeated while must be single. + errMultipleStreamInitializers = errors.New("multiple stream initializers") + // errMissingBodySignature is returned when request body is missing. + errMissingBodySignature = errors.New("missing body signature") + // errMissingPublicKey is returned when public key is missing. + errMissingPublicKey = errors.New("missing public key") + // errMissingObjectAddress is returned when object address is missing. + errMissingObjectAddress = errors.New("missing object address") + // errMissingContainerID is returned when container ID is missing. + errMissingContainerID = errors.New("missing container ID") + // errMissingObjectID is returned when object ID is missing. + errMissingObjectID = errors.New("missing object ID") + // errMissingObjectHeader is returned when object headers is missing. + errMissingObjectHeader = errors.New("missing object header") +) -func malformedRequestError(reason string) error { - return fmt.Errorf("%s: %s", invalidRequestMessage, reason) +// newInvalidRequestError wraps the error caused invalid request error +// compatible with [errors] package functionality. +func newInvalidRequestError(cause error) error { + return fmt.Errorf("invalid request: %w", cause) } -var ( - errEmptyBody = malformedRequestError("empty body") - errEmptyVerificationHeader = malformedRequestError("empty verification header") - errEmptyBodySig = malformedRequestError("empty at body signature") - errInvalidSessionSig = malformedRequestError("invalid session token signature") - errInvalidSessionOwner = malformedRequestError("invalid session token owner") - errInvalidVerb = malformedRequestError("session token verb is invalid") -) +// newInvalidRequestBodyError wraps the error caused invalid request body error +// compatible with [errors] package functionality. +func newInvalidRequestBodyError(cause error) error { + return fmt.Errorf("invalid request body: %w", cause) +} -const accessDeniedACLReasonFmt = "access to operation %s is denied by basic ACL check" -const accessDeniedEACLReasonFmt = "access to operation %s is denied by extended ACL check: %v" +// newInvalidMetaHeaderError wraps the error caused invalid meta header error +// compatible with [errors] package functionality. +func newInvalidMetaHeaderError(cause error) error { + return fmt.Errorf("invalid request meta header: %w", cause) +} -func basicACLErr(info RequestInfo) error { - var errAccessDenied apistatus.ObjectAccessDenied - errAccessDenied.WriteReason(fmt.Sprintf(accessDeniedACLReasonFmt, info.operation)) +// newInvalidVerificationHeaderError wraps the error caused invalid verification +// header error compatible with [errors] package functionality. +func newInvalidVerificationHeaderError(cause error) error { + return fmt.Errorf("invalid request verification header: %w", cause) +} - return errAccessDenied +// newInvalidObjectAddressError wraps the error caused invalid object address +// error compatible with [errors] package functionality. +func newInvalidObjectAddressError(cause error) error { + return fmt.Errorf("invalid address: %w", cause) } -func eACLErr(info RequestInfo, err error) error { - var errAccessDenied apistatus.ObjectAccessDenied - errAccessDenied.WriteReason(fmt.Sprintf(accessDeniedEACLReasonFmt, info.operation, err)) +// newInvalidContainerIDError wraps the error caused invalid container ID error +// compatible with [errors] package functionality. +func newInvalidContainerIDError(cause error) error { + return fmt.Errorf("invalid container ID: %w", cause) +} - return errAccessDenied +// newInvalidObjectIDError wraps the error caused invalid object ID error +// compatible with [errors] package functionality. +func newInvalidObjectIDError(cause error) error { + return fmt.Errorf("invalid object ID: %w", cause) } diff --git a/pkg/services/object/acl/v2/opts.go b/pkg/services/object/acl/v2/opts.go deleted file mode 100644 index 40d14c5964..0000000000 --- a/pkg/services/object/acl/v2/opts.go +++ /dev/null @@ -1,51 +0,0 @@ -package v2 - -import ( - "github.com/nspcc-dev/neofs-node/pkg/core/container" - "github.com/nspcc-dev/neofs-node/pkg/core/netmap" - objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" - "go.uber.org/zap" -) - -// WithLogger returns option to set logger. -func WithLogger(v *zap.Logger) Option { - return func(c *cfg) { - c.log = v - } -} - -// WithNetmapSource return option to set -// netmap source. -func WithNetmapSource(v netmap.Source) Option { - return func(c *cfg) { - c.nm = v - } -} - -// WithContainerSource returns option to set container source. -func WithContainerSource(v container.Source) Option { - return func(c *cfg) { - c.containers = v - } -} - -// WithNextService returns option to set next object service. -func WithNextService(v objectSvc.ServiceServer) Option { - return func(c *cfg) { - c.next = v - } -} - -// WithEACLChecker returns option to set eACL checker. -func WithEACLChecker(v ACLChecker) Option { - return func(c *cfg) { - c.checker = v - } -} - -// WithIRFetcher returns option to set inner ring fetcher. -func WithIRFetcher(v InnerRingFetcher) Option { - return func(c *cfg) { - c.irFetcher = v - } -} diff --git a/pkg/services/object/acl/v2/request.go b/pkg/services/object/acl/v2/request.go deleted file mode 100644 index 16985a83e6..0000000000 --- a/pkg/services/object/acl/v2/request.go +++ /dev/null @@ -1,139 +0,0 @@ -package v2 - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "fmt" - - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - sessionV2 "github.com/nspcc-dev/neofs-api-go/v2/session" - "github.com/nspcc-dev/neofs-sdk-go/bearer" - "github.com/nspcc-dev/neofs-sdk-go/container/acl" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" - sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session" - "github.com/nspcc-dev/neofs-sdk-go/user" -) - -// RequestInfo groups parsed version-independent (from SDK library) -// request information and raw API request. -type RequestInfo struct { - basicACL acl.Basic - requestRole acl.Role - operation acl.Op // put, get, head, etc. - cnrOwner user.ID // container owner - - idCnr cid.ID - - // optional for some request - // e.g. Put, Search - obj *oid.ID - - senderKey []byte - - bearer *bearer.Token // bearer token of request - - srcRequest any -} - -func (r *RequestInfo) SetBasicACL(basicACL acl.Basic) { - r.basicACL = basicACL -} - -func (r *RequestInfo) SetRequestRole(requestRole acl.Role) { - r.requestRole = requestRole -} - -func (r *RequestInfo) SetSenderKey(senderKey []byte) { - r.senderKey = senderKey -} - -// Request returns raw API request. -func (r RequestInfo) Request() any { - return r.srcRequest -} - -// ContainerOwner returns owner if the container. -func (r RequestInfo) ContainerOwner() user.ID { - return r.cnrOwner -} - -// ObjectID return object ID. -func (r RequestInfo) ObjectID() *oid.ID { - return r.obj -} - -// ContainerID return container ID. -func (r RequestInfo) ContainerID() cid.ID { - return r.idCnr -} - -// CleanBearer forces cleaning bearer token information. -func (r *RequestInfo) CleanBearer() { - r.bearer = nil -} - -// Bearer returns bearer token of the request. -func (r RequestInfo) Bearer() *bearer.Token { - return r.bearer -} - -// BasicACL returns basic ACL of the container. -func (r RequestInfo) BasicACL() acl.Basic { - return r.basicACL -} - -// SenderKey returns public key of the request's sender. -func (r RequestInfo) SenderKey() []byte { - return r.senderKey -} - -// Operation returns request's operation. -func (r RequestInfo) Operation() acl.Op { - return r.operation -} - -// RequestRole returns request sender's role. -func (r RequestInfo) RequestRole() acl.Role { - return r.requestRole -} - -// MetaWithToken groups session and bearer tokens, -// verification header and raw API request. -type MetaWithToken struct { - vheader *sessionV2.RequestVerificationHeader - token *sessionSDK.Object - bearer *bearer.Token - src any -} - -// RequestOwner returns ownerID and its public key -// according to internal meta information. -func (r MetaWithToken) RequestOwner() (*user.ID, []byte, error) { - if r.vheader == nil { - return nil, nil, errEmptyVerificationHeader - } - - // if session token is presented, use it as truth source - if r.token != nil { - // verify signature of session token - return ownerFromToken(r.token) - } - - // otherwise get original body signature - bodySignature := originalBodySignature(r.vheader) - if bodySignature == nil { - return nil, nil, errEmptyBodySig - } - - key := bodySignature.GetKey() - - pubKey, err := keys.NewPublicKeyFromBytes(key, elliptic.P256()) - if err != nil { - return nil, nil, fmt.Errorf("decode public key: %w", err) - } - - idSender := user.ResolveFromECDSAPublicKey(ecdsa.PublicKey(*pubKey)) - - return &idSender, key, nil -} diff --git a/pkg/services/object/acl/v2/service.go b/pkg/services/object/acl/v2/service.go index 298b1a8f7b..5ba573d0f6 100644 --- a/pkg/services/object/acl/v2/service.go +++ b/pkg/services/object/acl/v2/service.go @@ -1,4 +1,4 @@ -package v2 +package v2acl import ( "context" @@ -6,602 +6,899 @@ import ( "fmt" objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - "github.com/nspcc-dev/neofs-node/pkg/core/container" - "github.com/nspcc-dev/neofs-node/pkg/core/netmap" "github.com/nspcc-dev/neofs-node/pkg/services/object" + aclservice "github.com/nspcc-dev/neofs-node/pkg/services/object/acl" + "github.com/nspcc-dev/neofs-sdk-go/bearer" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" - "github.com/nspcc-dev/neofs-sdk-go/container/acl" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + objectSDK "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session" - "github.com/nspcc-dev/neofs-sdk-go/user" - "go.uber.org/zap" ) -// Service checks basic ACL rules. +// Service is an intermediate handler of object operations that performs +// access checks. type Service struct { - *cfg + checker *aclservice.Checker - c senderClassifier + nextHandler object.ServiceServer } -type putStreamBasicChecker struct { - source *Service - next object.PutObjectStream +// New constructs Service working on top of the provided [aclservice.Node] and +// managing access for all incoming requests. If there is access, any request is +// passed on to the specified next handler, and if there is no access, +// processing is terminated. +func New(node aclservice.Node, nextHandler object.ServiceServer) *Service { + return &Service{ + checker: aclservice.NewChecker(node), + nextHandler: nextHandler, + } } -type getStreamBasicChecker struct { - checker ACLChecker - +// getStreamWithAccessCheck provides object read stream that performs additional +// access control: it handles object and response headers from incoming response +// messages that are missing in the original request. +type getStreamWithAccessCheck struct { + // target stream object.GetObjectStream - info RequestInfo -} + checker *aclservice.Checker -type rangeStreamBasicChecker struct { - checker ACLChecker + // original request's data + request request + cnr cid.ID + objID oid.ID + bClientPubKey []byte + sessionToken *sessionSDK.Object + bearerToken *bearer.Token - object.GetObjectRangeStream + // dynamic context - info RequestInfo + processedInitialPart bool } -type searchStreamBasicChecker struct { - checker ACLChecker +func newGetStreamWithAccessCheck( + targetStream object.GetObjectStream, + checker *aclservice.Checker, + request request, + cnr cid.ID, + objID oid.ID, + bClientPubKey []byte, + sessionToken *sessionSDK.Object, + bearerToken *bearer.Token, +) *getStreamWithAccessCheck { + return &getStreamWithAccessCheck{ + GetObjectStream: targetStream, + checker: checker, + request: request, + cnr: cnr, + objID: objID, + bClientPubKey: bClientPubKey, + sessionToken: sessionToken, + bearerToken: bearerToken, + } +} - object.SearchStream +// Send intercepts and verifies a message carrying object headers by passing +// them to the underlying [aclservice.Checker]. Other messages and those that +// pass the check are forwarded to the underlying target stream. +func (x *getStreamWithAccessCheck) Send(resp *objectV2.GetResponse) error { + body := resp.GetBody() + if body == nil { + panic("missing response body") + } - info RequestInfo -} + var hdrMsg *objectV2.Header -// Option represents Service constructor option. -type Option func(*cfg) + switch part := body.GetObjectPart().(type) { + default: + panic(fmt.Sprintf("unexpected part of the response stream %T", body.GetObjectPart())) + case *objectV2.GetObjectPartChunk, *objectV2.SplitInfo: + // just send any message without object headers untouched + return x.GetObjectStream.Send(resp) + case *objectV2.GetObjectPartInit: + if x.processedInitialPart { + panic(errMultipleStreamInitializers) + } -type cfg struct { - log *zap.Logger + x.processedInitialPart = true - containers container.Source + hdrMsg = part.GetHeader() + if hdrMsg == nil { + panic("missing object header in the response message") + } + } - checker ACLChecker + // embed header into object message because objectSDK package doesn't work + // with header type + var objMsg objectV2.Object + objMsg.SetHeader(hdrMsg) - irFetcher InnerRingFetcher + hdr := objectSDK.NewFromV2(&objMsg) - nm netmap.Source + r := aclservice.NewContainerRequest(x.cnr, x.bClientPubKey, aclservice.GetResponse(x.objID, *hdr), wrapRequest(x.request)) - next object.ServiceServer -} + if x.bearerToken != nil { + r.SetBearerToken(*x.bearerToken) + } -func defaultCfg() *cfg { - return &cfg{ - log: zap.L(), + if x.sessionToken != nil { + r.SetSessionToken(*x.sessionToken) } -} -// New is a constructor for object ACL checking service. -func New(opts ...Option) Service { - cfg := defaultCfg() + err := x.checker.CheckAccess(r) + if err != nil { + var errAccessDenied aclservice.AccessDeniedError - for i := range opts { - opts[i](cfg) - } + switch { + default: + // here err may be protocol status and returned directly + return err + case errors.As(err, &errAccessDenied): + var e apistatus.ObjectAccessDenied + e.WriteReason(errAccessDenied.Error()) - panicOnNil := func(v any, name string) { - if v == nil { - panic(fmt.Sprintf("ACL service: %s is nil", name)) + return e + case errors.Is(err, aclservice.ErrNotEnoughData): + panic(fmt.Sprintf("unexpected error from ACL checker: %v", err)) } } - panicOnNil(cfg.next, "next Service") - panicOnNil(cfg.nm, "netmap client") - panicOnNil(cfg.irFetcher, "inner Ring fetcher") - panicOnNil(cfg.checker, "acl checker") - panicOnNil(cfg.containers, "container source") - - return Service{ - cfg: cfg, - c: senderClassifier{ - log: cfg.log, - innerRing: cfg.irFetcher, - netmap: cfg.nm, - }, - } + return x.GetObjectStream.Send(resp) } -// Get implements ServiceServer interface, makes ACL checks and calls -// next Get method in the ServiceServer pipeline. -func (b Service) Get(request *objectV2.GetRequest, stream object.GetObjectStream) error { - cnr, err := getContainerIDFromRequest(request) +// Get performs access control for the given request. If access is granted, +// processing is transferred to the underlying handler. Access checks are also +// performed for response messages that carry additional data. Returns: +// - [apistatus.ObjectAccessDenied] on access denial +// - [apistatus.ErrSessionTokenExpired] if session token is specified but expired +// - [apistatus.ErrContainerNotFound] if requested container is missing in the network +func (b *Service) Get(req *objectV2.GetRequest, stream object.GetObjectStream) error { + bClientPubKey, err := binaryClientPublicKeyFromRequest(req) if err != nil { return err } - obj, err := getObjectIDFromRequestBody(request.GetBody()) + reqBody := req.GetBody() + if reqBody == nil { + return newInvalidRequestError( + newInvalidRequestBodyError(errMissingRequestBody)) + } + + addr := reqBody.GetAddress() + if addr == nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError(errMissingObjectAddress))) + } + + cnrMsg := addr.GetContainerID() + if cnrMsg == nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(errMissingContainerID)))) + } + + objIDMsg := addr.GetObjectID() + if objIDMsg == nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidObjectIDError(errMissingObjectID)))) + } + + var cnr cid.ID + + err = cnr.ReadFromV2(*cnrMsg) if err != nil { - return err + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(err)))) } - sTok, err := originalSessionToken(request.GetMetaHeader()) + var objID oid.ID + + err = objID.ReadFromV2(*objIDMsg) if err != nil { - return err + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidObjectIDError(err)))) } - if sTok != nil { - err = assertSessionRelation(*sTok, cnr, obj) - if err != nil { - return err - } + bearerToken, err := bearerTokenFromRequest(req) + if err != nil { + return err } - bTok, err := originalBearerToken(request.GetMetaHeader()) + sessionToken, err := sessionTokenFromRequest(req) if err != nil { return err } - req := MetaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: bTok, - src: request, + r := aclservice.NewContainerRequest(cnr, bClientPubKey, aclservice.GetRequest(objID), wrapRequest(req)) + + if bearerToken != nil { + r.SetBearerToken(*bearerToken) } - reqInfo, err := b.findRequestInfo(req, cnr, acl.OpObjectGet) - if err != nil { - return err + if sessionToken != nil { + r.SetSessionToken(*sessionToken) } - reqInfo.obj = obj + err = b.checker.CheckAccess(r) + if err != nil { + var errAccessDenied aclservice.AccessDeniedError - if !b.checker.CheckBasicACL(reqInfo) { - return basicACLErr(reqInfo) - } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { - return eACLErr(reqInfo, err) + switch { + default: + // here err may be protocol status and returned directly + return err + case errors.As(err, &errAccessDenied): + var e apistatus.ObjectAccessDenied + e.WriteReason(errAccessDenied.Error()) + + return e + case errors.Is(err, aclservice.ErrNotEnoughData): + // response is needed, continue + } } - return b.next.Get(request, &getStreamBasicChecker{ - GetObjectStream: stream, - info: reqInfo, - checker: b.checker, - }) + checkStream := newGetStreamWithAccessCheck(stream, b.checker, req, cnr, objID, bClientPubKey, sessionToken, bearerToken) + + return b.nextHandler.Get(req, checkStream) } -func (b Service) Put(ctx context.Context) (object.PutObjectStream, error) { - streamer, err := b.next.Put(ctx) +// putStreamWithAccessCheck provides object write stream that performs access +// control: it handles object and request headers from incoming request +// messages. +type putStreamWithAccessCheck struct { + // target stream + object.PutObjectStream + + checker *aclservice.Checker + + // dynamic context - return putStreamBasicChecker{ - source: &b, - next: streamer, - }, err + processedInitialPart bool } -func (b Service) Head( - ctx context.Context, - request *objectV2.HeadRequest) (*objectV2.HeadResponse, error) { - cnr, err := getContainerIDFromRequest(request) - if err != nil { - return nil, err +func newPutStreamWithAccessCheck(targetStream object.PutObjectStream, checker *aclservice.Checker) *putStreamWithAccessCheck { + return &putStreamWithAccessCheck{ + PutObjectStream: targetStream, + checker: checker, } +} - obj, err := getObjectIDFromRequestBody(request.GetBody()) +// Send intercepts and verifies a message carrying object headers by passing +// them to the underlying [aclservice.Checker]. Other messages and those that +// pass the check are forwarded to the underlying target stream. +func (x *putStreamWithAccessCheck) Send(req *objectV2.PutRequest) error { + bClientPubKey, err := binaryClientPublicKeyFromRequest(req) if err != nil { - return nil, err + return err } - sTok, err := originalSessionToken(request.GetMetaHeader()) - if err != nil { - return nil, err + reqBody := req.GetBody() + if reqBody == nil { + return newInvalidRequestError( + newInvalidRequestBodyError(errMissingRequestBody)) } - if sTok != nil { - err = assertSessionRelation(*sTok, cnr, obj) - if err != nil { - return nil, err - } + initPart, ok := reqBody.GetObjectPart().(*objectV2.PutObjectPartInit) + if !ok { + return x.PutObjectStream.Send(req) } - bTok, err := originalBearerToken(request.GetMetaHeader()) + if x.processedInitialPart { + return newInvalidRequestError(errMultipleStreamInitializers) + } + + x.processedInitialPart = true + + hdrMsg := initPart.GetHeader() + if hdrMsg == nil { + return newInvalidRequestError( + newInvalidRequestBodyError(errMissingObjectHeader)) + } + + cnrMsg := hdrMsg.GetContainerID() + if cnrMsg == nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidContainerIDError(errMissingContainerID))) + } + + var cnr cid.ID + + err = cnr.ReadFromV2(*cnrMsg) if err != nil { - return nil, err + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(err)))) } - req := MetaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: bTok, - src: request, + bearerToken, err := bearerTokenFromRequest(req) + if err != nil { + return err } - reqInfo, err := b.findRequestInfo(req, cnr, acl.OpObjectHead) + sessionToken, err := sessionTokenFromRequest(req) if err != nil { - return nil, err + return err } - reqInfo.obj = obj + // embed header into object message because objectSDK package doesn't work + // with header type + var objMsg objectV2.Object + objMsg.SetHeader(hdrMsg) + objMsg.SetObjectID(initPart.GetObjectID()) + objMsg.SetSignature(initPart.GetSignature()) + + hdr := objectSDK.NewFromV2(&objMsg) - if !b.checker.CheckBasicACL(reqInfo) { - return nil, basicACLErr(reqInfo) - } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { - return nil, eACLErr(reqInfo, err) + r := aclservice.NewContainerRequest(cnr, bClientPubKey, aclservice.PutRequest(*hdr), wrapRequest(req)) + + if bearerToken != nil { + r.SetBearerToken(*bearerToken) + } + + if sessionToken != nil { + r.SetSessionToken(*sessionToken) } - resp, err := b.next.Head(ctx, request) - if err == nil { - if err = b.checker.CheckEACL(resp, reqInfo); err != nil { - err = eACLErr(reqInfo, err) + err = x.checker.CheckAccess(r) + if err != nil { + var errAccessDenied aclservice.AccessDeniedError + + switch { + default: + // here err may be protocol status and returned directly + return err + case errors.As(err, &errAccessDenied): + var e apistatus.ObjectAccessDenied + e.WriteReason(errAccessDenied.Error()) + + return e + case errors.Is(err, aclservice.ErrNotEnoughData): + panic(fmt.Sprintf("unexpected error from ACL checker: %v", err)) } } - return resp, err + return x.PutObjectStream.Send(req) } -func (b Service) Search(request *objectV2.SearchRequest, stream object.SearchStream) error { - id, err := getContainerIDFromRequest(request) +func (b Service) Put(ctx context.Context) (object.PutObjectStream, error) { + stream, err := b.nextHandler.Put(ctx) if err != nil { - return err + return nil, err } - sTok, err := originalSessionToken(request.GetMetaHeader()) + return newPutStreamWithAccessCheck(stream, b.checker), nil +} + +func (b Service) Head(ctx context.Context, req *objectV2.HeadRequest) (*objectV2.HeadResponse, error) { + bClientPubKey, err := binaryClientPublicKeyFromRequest(req) if err != nil { - return err + return nil, err } - if sTok != nil { - err = assertSessionRelation(*sTok, id, nil) - if err != nil { - return err - } + reqBody := req.GetBody() + if reqBody == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError(errMissingRequestBody)) } - bTok, err := originalBearerToken(request.GetMetaHeader()) - if err != nil { - return err + addr := reqBody.GetAddress() + if addr == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError(errMissingObjectAddress))) } - req := MetaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: bTok, - src: request, + cnrMsg := addr.GetContainerID() + if cnrMsg == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(errMissingContainerID)))) } - reqInfo, err := b.findRequestInfo(req, id, acl.OpObjectSearch) - if err != nil { - return err + objIDMsg := addr.GetObjectID() + if objIDMsg == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidObjectIDError(errMissingObjectID)))) } - if !b.checker.CheckBasicACL(reqInfo) { - return basicACLErr(reqInfo) - } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { - return eACLErr(reqInfo, err) + var cnr cid.ID + + err = cnr.ReadFromV2(*cnrMsg) + if err != nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(err)))) } - return b.next.Search(request, &searchStreamBasicChecker{ - checker: b.checker, - SearchStream: stream, - info: reqInfo, - }) -} + var objID oid.ID -func (b Service) Delete( - ctx context.Context, - request *objectV2.DeleteRequest) (*objectV2.DeleteResponse, error) { - cnr, err := getContainerIDFromRequest(request) + err = objID.ReadFromV2(*objIDMsg) if err != nil { - return nil, err + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidObjectIDError(err)))) } - obj, err := getObjectIDFromRequestBody(request.GetBody()) + bearerToken, err := bearerTokenFromRequest(req) if err != nil { return nil, err } - sTok, err := originalSessionToken(request.GetMetaHeader()) + sessionToken, err := sessionTokenFromRequest(req) if err != nil { return nil, err } - if sTok != nil { - err = assertSessionRelation(*sTok, cnr, obj) - if err != nil { + r := aclservice.NewContainerRequest(cnr, bClientPubKey, aclservice.HeadRequest(objID), wrapRequest(req)) + + if bearerToken != nil { + r.SetBearerToken(*bearerToken) + } + + if sessionToken != nil { + r.SetSessionToken(*sessionToken) + } + + err = b.checker.CheckAccess(r) + if err != nil { + var errAccessDenied aclservice.AccessDeniedError + + switch { + default: + // here err may be protocol status and returned directly return nil, err + case errors.As(err, &errAccessDenied): + var e apistatus.ObjectAccessDenied + e.WriteReason(errAccessDenied.Error()) + + return nil, e + case errors.Is(err, aclservice.ErrNotEnoughData): + // response is needed, continue } } - bTok, err := originalBearerToken(request.GetMetaHeader()) + resp, err := b.nextHandler.Head(ctx, req) if err != nil { return nil, err } - req := MetaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: bTok, - src: request, + body := resp.GetBody() + if body == nil { + panic("missing response body") } - reqInfo, err := b.findRequestInfo(req, cnr, acl.OpObjectDelete) - if err != nil { - return nil, err + var hdrMsg *objectV2.Header + + switch part := body.GetHeaderPart().(type) { + default: + panic(fmt.Sprintf("unexpected payload of the response %T", body.GetHeaderPart())) + case *objectV2.SplitInfo: + // just return response without object headers untouched + return resp, nil + case *objectV2.HeaderWithSignature: + hdrMsg = part.GetHeader() + if hdrMsg == nil { + panic("missing object header in the response message") + } + case *objectV2.ShortHeader: + hdrMsg = new(objectV2.Header) + hdrMsg.SetVersion(part.GetVersion()) + hdrMsg.SetOwnerID(part.GetOwnerID()) + hdrMsg.SetObjectType(part.GetObjectType()) + hdrMsg.SetCreationEpoch(part.GetCreationEpoch()) + hdrMsg.SetPayloadLength(part.GetPayloadLength()) + hdrMsg.SetPayloadHash(part.GetPayloadHash()) + hdrMsg.SetHomomorphicHash(part.GetHomomorphicHash()) } - reqInfo.obj = obj + // embed header into object message because objectSDK package doesn't work + // with header type + var objMsg objectV2.Object + objMsg.SetHeader(hdrMsg) - if !b.checker.CheckBasicACL(reqInfo) { - return nil, basicACLErr(reqInfo) - } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { - return nil, eACLErr(reqInfo, err) + hdr := objectSDK.NewFromV2(&objMsg) + + r = aclservice.NewContainerRequest(cnr, bClientPubKey, aclservice.HeadResponse(objID, *hdr), wrapRequest(req)) + + if bearerToken != nil { + r.SetBearerToken(*bearerToken) } - return b.next.Delete(ctx, request) + if sessionToken != nil { + r.SetSessionToken(*sessionToken) + } + + err = b.checker.CheckAccess(r) + if err != nil { + var errAccessDenied aclservice.AccessDeniedError + + switch { + default: + // here err may be protocol status and returned directly + return nil, err + case errors.As(err, &errAccessDenied): + var e apistatus.ObjectAccessDenied + e.WriteReason(errAccessDenied.Error()) + + return nil, e + case errors.Is(err, aclservice.ErrNotEnoughData): + panic(fmt.Sprintf("unexpected error from ACL checker: %v", err)) + } + } + + return resp, nil } -func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetObjectRangeStream) error { - cnr, err := getContainerIDFromRequest(request) +func (b Service) Search(req *objectV2.SearchRequest, stream object.SearchStream) error { + bClientPubKey, err := binaryClientPublicKeyFromRequest(req) if err != nil { return err } - obj, err := getObjectIDFromRequestBody(request.GetBody()) + reqBody := req.GetBody() + if reqBody == nil { + return newInvalidRequestError( + newInvalidRequestBodyError(errMissingRequestBody)) + } + + cnrMsg := reqBody.GetContainerID() + if cnrMsg == nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidContainerIDError(errMissingContainerID))) + } + + var cnr cid.ID + + err = cnr.ReadFromV2(*cnrMsg) + if err != nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(err)))) + } + + bearerToken, err := bearerTokenFromRequest(req) if err != nil { return err } - sTok, err := originalSessionToken(request.GetMetaHeader()) + sessionToken, err := sessionTokenFromRequest(req) if err != nil { return err } - if sTok != nil { - err = assertSessionRelation(*sTok, cnr, obj) - if err != nil { + r := aclservice.NewContainerRequest(cnr, bClientPubKey, aclservice.SearchRequest(), wrapRequest(req)) + + if bearerToken != nil { + r.SetBearerToken(*bearerToken) + } + + if sessionToken != nil { + r.SetSessionToken(*sessionToken) + } + + err = b.checker.CheckAccess(r) + if err != nil { + var errAccessDenied aclservice.AccessDeniedError + + switch { + default: + // here err may be protocol status and returned directly return err + case errors.As(err, &errAccessDenied): + var e apistatus.ObjectAccessDenied + e.WriteReason(errAccessDenied.Error()) + + return e + case errors.Is(err, aclservice.ErrNotEnoughData): + panic(fmt.Sprintf("unexpected error from ACL checker: %v", err)) } } - bTok, err := originalBearerToken(request.GetMetaHeader()) + return b.nextHandler.Search(req, stream) +} + +func (b Service) Delete(ctx context.Context, req *objectV2.DeleteRequest) (*objectV2.DeleteResponse, error) { + bClientPubKey, err := binaryClientPublicKeyFromRequest(req) if err != nil { - return err + return nil, err } - req := MetaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: bTok, - src: request, + reqBody := req.GetBody() + if reqBody == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError(errMissingRequestBody)) } - reqInfo, err := b.findRequestInfo(req, cnr, acl.OpObjectRange) - if err != nil { - return err + addr := reqBody.GetAddress() + if addr == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError(errMissingObjectAddress))) } - reqInfo.obj = obj + cnrMsg := addr.GetContainerID() + if cnrMsg == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(errMissingContainerID)))) + } - if !b.checker.CheckBasicACL(reqInfo) { - return basicACLErr(reqInfo) - } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { - return eACLErr(reqInfo, err) + objIDMsg := addr.GetObjectID() + if objIDMsg == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidObjectIDError(errMissingObjectID)))) } - return b.next.GetRange(request, &rangeStreamBasicChecker{ - checker: b.checker, - GetObjectRangeStream: stream, - info: reqInfo, - }) -} + var cnr cid.ID -func (b Service) GetRangeHash( - ctx context.Context, - request *objectV2.GetRangeHashRequest) (*objectV2.GetRangeHashResponse, error) { - cnr, err := getContainerIDFromRequest(request) + err = cnr.ReadFromV2(*cnrMsg) if err != nil { - return nil, err + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(err)))) + } + + var objID oid.ID + + err = objID.ReadFromV2(*objIDMsg) + if err != nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidObjectIDError(err)))) } - obj, err := getObjectIDFromRequestBody(request.GetBody()) + bearerToken, err := bearerTokenFromRequest(req) if err != nil { return nil, err } - sTok, err := originalSessionToken(request.GetMetaHeader()) + sessionToken, err := sessionTokenFromRequest(req) if err != nil { return nil, err } - if sTok != nil { - err = assertSessionRelation(*sTok, cnr, obj) - if err != nil { + r := aclservice.NewContainerRequest(cnr, bClientPubKey, aclservice.DeleteRequest(objID), wrapRequest(req)) + + if bearerToken != nil { + r.SetBearerToken(*bearerToken) + } + + if sessionToken != nil { + r.SetSessionToken(*sessionToken) + } + + err = b.checker.CheckAccess(r) + if err != nil { + var errAccessDenied aclservice.AccessDeniedError + + switch { + default: + // here err may be protocol status and returned directly return nil, err + case errors.As(err, &errAccessDenied): + var e apistatus.ObjectAccessDenied + e.WriteReason(errAccessDenied.Error()) + + return nil, e + case errors.Is(err, aclservice.ErrNotEnoughData): + panic(fmt.Sprintf("unexpected error from ACL checker: %v", err)) } } - bTok, err := originalBearerToken(request.GetMetaHeader()) + return b.nextHandler.Delete(ctx, req) +} + +func (b Service) GetRange(req *objectV2.GetRangeRequest, stream object.GetObjectRangeStream) error { + bClientPubKey, err := binaryClientPublicKeyFromRequest(req) if err != nil { - return nil, err + return err } - req := MetaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: bTok, - src: request, + reqBody := req.GetBody() + if reqBody == nil { + return newInvalidRequestError( + newInvalidRequestBodyError(errMissingRequestBody)) } - reqInfo, err := b.findRequestInfo(req, cnr, acl.OpObjectHash) - if err != nil { - return nil, err + addr := reqBody.GetAddress() + if addr == nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError(errMissingObjectAddress))) } - reqInfo.obj = obj + cnrMsg := addr.GetContainerID() + if cnrMsg == nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(errMissingContainerID)))) + } - if !b.checker.CheckBasicACL(reqInfo) { - return nil, basicACLErr(reqInfo) - } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { - return nil, eACLErr(reqInfo, err) + objIDMsg := addr.GetObjectID() + if objIDMsg == nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidObjectIDError(errMissingObjectID)))) } - return b.next.GetRangeHash(ctx, request) -} + var cnr cid.ID -func (p putStreamBasicChecker) Send(request *objectV2.PutRequest) error { - body := request.GetBody() - if body == nil { - return errEmptyBody + err = cnr.ReadFromV2(*cnrMsg) + if err != nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(err)))) } - part := body.GetObjectPart() - if part, ok := part.(*objectV2.PutObjectPartInit); ok { - cnr, err := getContainerIDFromRequest(request) - if err != nil { - return err - } + var objID oid.ID - idV2 := part.GetHeader().GetOwnerID() - if idV2 == nil { - return errors.New("missing object owner") - } - - var idOwner user.ID + err = objID.ReadFromV2(*objIDMsg) + if err != nil { + return newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidObjectIDError(err)))) + } - err = idOwner.ReadFromV2(*idV2) - if err != nil { - return fmt.Errorf("invalid object owner: %w", err) - } + bearerToken, err := bearerTokenFromRequest(req) + if err != nil { + return err + } - objV2 := part.GetObjectID() - var obj *oid.ID + sessionToken, err := sessionTokenFromRequest(req) + if err != nil { + return err + } - if objV2 != nil { - obj = new(oid.ID) + r := aclservice.NewContainerRequest(cnr, bClientPubKey, aclservice.PayloadRangeRequest(objID), wrapRequest(req)) - err = obj.ReadFromV2(*objV2) - if err != nil { - return err - } - } + if bearerToken != nil { + r.SetBearerToken(*bearerToken) + } - sTok, err := originalSessionToken(request.GetMetaHeader()) - if err != nil { - return err - } + if sessionToken != nil { + r.SetSessionToken(*sessionToken) + } - if sTok != nil { - if sTok.AssertVerb(sessionSDK.VerbObjectDelete) { - // if session relates to object's removal, we don't check - // relation of the tombstone to the session here since user - // can't predict tomb's ID. - err = assertSessionRelation(*sTok, cnr, nil) - } else { - err = assertSessionRelation(*sTok, cnr, obj) - } - - if err != nil { - return err - } - } + err = b.checker.CheckAccess(r) + if err != nil { + var errAccessDenied aclservice.AccessDeniedError - bTok, err := originalBearerToken(request.GetMetaHeader()) - if err != nil { + switch { + default: + // here err may be protocol status and returned directly return err - } + case errors.As(err, &errAccessDenied): + var e apistatus.ObjectAccessDenied + e.WriteReason(errAccessDenied.Error()) - req := MetaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: bTok, - src: request, + return e + case errors.Is(err, aclservice.ErrNotEnoughData): + panic(fmt.Sprintf("unexpected error from ACL checker: %v", err)) } + } - reqInfo, err := p.source.findRequestInfo(req, cnr, acl.OpObjectPut) - if err != nil { - return err - } + return b.nextHandler.GetRange(req, stream) +} - reqInfo.obj = obj +func (b Service) GetRangeHash(ctx context.Context, req *objectV2.GetRangeHashRequest) (*objectV2.GetRangeHashResponse, error) { + bClientPubKey, err := binaryClientPublicKeyFromRequest(req) + if err != nil { + return nil, err + } - if !p.source.checker.CheckBasicACL(reqInfo) || !p.source.checker.StickyBitCheck(reqInfo, idOwner) { - return basicACLErr(reqInfo) - } else if err := p.source.checker.CheckEACL(request, reqInfo); err != nil { - return eACLErr(reqInfo, err) - } + reqBody := req.GetBody() + if reqBody == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError(errMissingRequestBody)) } - return p.next.Send(request) -} + addr := reqBody.GetAddress() + if addr == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError(errMissingObjectAddress))) + } -func (p putStreamBasicChecker) CloseAndRecv() (*objectV2.PutResponse, error) { - return p.next.CloseAndRecv() -} + cnrMsg := addr.GetContainerID() + if cnrMsg == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(errMissingContainerID)))) + } -func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error { - if _, ok := resp.GetBody().GetObjectPart().(*objectV2.GetObjectPartInit); ok { - if err := g.checker.CheckEACL(resp, g.info); err != nil { - return eACLErr(g.info, err) - } + objIDMsg := addr.GetObjectID() + if objIDMsg == nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidObjectIDError(errMissingObjectID)))) } - return g.GetObjectStream.Send(resp) -} + var cnr cid.ID -func (g *rangeStreamBasicChecker) Send(resp *objectV2.GetRangeResponse) error { - if err := g.checker.CheckEACL(resp, g.info); err != nil { - return eACLErr(g.info, err) + err = cnr.ReadFromV2(*cnrMsg) + if err != nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidContainerIDError(err)))) } - return g.GetObjectRangeStream.Send(resp) -} + var objID oid.ID -func (g *searchStreamBasicChecker) Send(resp *objectV2.SearchResponse) error { - if err := g.checker.CheckEACL(resp, g.info); err != nil { - return eACLErr(g.info, err) + err = objID.ReadFromV2(*objIDMsg) + if err != nil { + return nil, newInvalidRequestError( + newInvalidRequestBodyError( + newInvalidObjectAddressError( + newInvalidObjectIDError(err)))) } - return g.SearchStream.Send(resp) -} + bearerToken, err := bearerTokenFromRequest(req) + if err != nil { + return nil, err + } -func (b Service) findRequestInfo(req MetaWithToken, idCnr cid.ID, op acl.Op) (info RequestInfo, err error) { - cnr, err := b.containers.Get(idCnr) // fetch actual container + sessionToken, err := sessionTokenFromRequest(req) if err != nil { - return info, err + return nil, err } - if req.token != nil { - currentEpoch, err := b.nm.Epoch() - if err != nil { - return info, errors.New("can't fetch current epoch") - } - if req.token.ExpiredAt(currentEpoch) { - return info, apistatus.SessionTokenExpired{} - } - if req.token.InvalidAt(currentEpoch) { - return info, fmt.Errorf("%s: token is invalid at %d epoch)", - invalidRequestMessage, currentEpoch) - } + r := aclservice.NewContainerRequest(cnr, bClientPubKey, aclservice.HashPayloadRangeRequest(objID), wrapRequest(req)) - if !assertVerb(*req.token, op) { - return info, errInvalidVerb - } + if bearerToken != nil { + r.SetBearerToken(*bearerToken) } - // find request role and key - res, err := b.c.classify(req, idCnr, cnr.Value) - if err != nil { - return info, err + if sessionToken != nil { + r.SetSessionToken(*sessionToken) } - info.basicACL = cnr.Value.BasicACL() - info.requestRole = res.role - info.operation = op - info.cnrOwner = cnr.Value.Owner() - info.idCnr = idCnr - - // it is assumed that at the moment the key will be valid, - // otherwise the request would not pass validation - info.senderKey = res.key + err = b.checker.CheckAccess(r) + if err != nil { + var errAccessDenied aclservice.AccessDeniedError - // add bearer token if it is present in request - info.bearer = req.bearer + switch { + default: + // here err may be protocol status and returned directly + return nil, err + case errors.As(err, &errAccessDenied): + var e apistatus.ObjectAccessDenied + e.WriteReason(errAccessDenied.Error()) - info.srcRequest = req.src + return nil, e + case errors.Is(err, aclservice.ErrNotEnoughData): + panic(fmt.Sprintf("unexpected error from ACL checker: %v", err)) + } + } - return info, nil + return b.nextHandler.GetRangeHash(ctx, req) } diff --git a/pkg/services/object/acl/v2/types.go b/pkg/services/object/acl/v2/types.go deleted file mode 100644 index 1807854631..0000000000 --- a/pkg/services/object/acl/v2/types.go +++ /dev/null @@ -1,28 +0,0 @@ -package v2 - -import ( - "github.com/nspcc-dev/neofs-sdk-go/user" -) - -// ACLChecker is an interface that must provide -// ACL related checks. -type ACLChecker interface { - // CheckBasicACL must return true only if request - // passes basic ACL validation. - CheckBasicACL(RequestInfo) bool - // CheckEACL must return non-nil error if request - // doesn't pass extended ACL validation. - CheckEACL(any, RequestInfo) error - // StickyBitCheck must return true only if sticky bit - // is disabled or enabled but request contains correct - // owner field. - StickyBitCheck(RequestInfo, user.ID) bool -} - -// InnerRingFetcher is an interface that must provide -// Inner Ring information. -type InnerRingFetcher interface { - // InnerRingKeys must return list of public keys of - // the actual inner ring. - InnerRingKeys() ([][]byte, error) -} diff --git a/pkg/services/object/acl/v2/util.go b/pkg/services/object/acl/v2/util.go index bb5641bbc5..53fd2e6eea 100644 --- a/pkg/services/object/acl/v2/util.go +++ b/pkg/services/object/acl/v2/util.go @@ -1,204 +1,137 @@ -package v2 +package v2acl import ( - "crypto/ecdsa" - "crypto/elliptic" - "errors" "fmt" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - refsV2 "github.com/nspcc-dev/neofs-api-go/v2/refs" - sessionV2 "github.com/nspcc-dev/neofs-api-go/v2/session" + v2session "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-sdk-go/bearer" - "github.com/nspcc-dev/neofs-sdk-go/container/acl" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" - sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session" - "github.com/nspcc-dev/neofs-sdk-go/user" + "github.com/nspcc-dev/neofs-sdk-go/session" ) -var errMissingContainerID = errors.New("missing container ID") - -func getContainerIDFromRequest(req any) (cid.ID, error) { - var idV2 *refsV2.ContainerID - var id cid.ID - - switch v := req.(type) { - case *objectV2.GetRequest: - idV2 = v.GetBody().GetAddress().GetContainerID() - case *objectV2.PutRequest: - part, ok := v.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit) - if !ok { - return cid.ID{}, errors.New("can't get container ID in chunk") - } - - idV2 = part.GetHeader().GetContainerID() - case *objectV2.HeadRequest: - idV2 = v.GetBody().GetAddress().GetContainerID() - case *objectV2.SearchRequest: - idV2 = v.GetBody().GetContainerID() - case *objectV2.DeleteRequest: - idV2 = v.GetBody().GetAddress().GetContainerID() - case *objectV2.GetRangeRequest: - idV2 = v.GetBody().GetAddress().GetContainerID() - case *objectV2.GetRangeHashRequest: - idV2 = v.GetBody().GetAddress().GetContainerID() - default: - return cid.ID{}, errors.New("unknown request type") - } - - if idV2 == nil { - return cid.ID{}, errMissingContainerID - } - - return id, id.ReadFromV2(*idV2) +// request is a common interface of all object requests used to implement common +// utilities. +type request interface { + GetMetaHeader() *v2session.RequestMetaHeader + GetVerificationHeader() *v2session.RequestVerificationHeader } -// originalBearerToken goes down to original request meta header and fetches -// bearer token from there. -func originalBearerToken(header *sessionV2.RequestMetaHeader) (*bearer.Token, error) { - for header.GetOrigin() != nil { - header = header.GetOrigin() +// sessionTokenFromRequest returns session token attached to the request by +// original client. Returns both nil if the token is missing. +func sessionTokenFromRequest(req request) (*session.Object, error) { + metaHeader := req.GetMetaHeader() + for metaHeader.GetOrigin() != nil { + metaHeader = metaHeader.GetOrigin() } - tokV2 := header.GetBearerToken() - if tokV2 == nil { + if metaHeader == nil { return nil, nil } - var tok bearer.Token - return &tok, tok.ReadFromV2(*tokV2) -} - -// originalSessionToken goes down to original request meta header and fetches -// session token from there. -func originalSessionToken(header *sessionV2.RequestMetaHeader) (*sessionSDK.Object, error) { - for header.GetOrigin() != nil { - header = header.GetOrigin() - } - - tokV2 := header.GetSessionToken() - if tokV2 == nil { + sessionTokenMsg := metaHeader.GetSessionToken() + if sessionTokenMsg == nil { return nil, nil } - var tok sessionSDK.Object + var sessionToken session.Object - err := tok.ReadFromV2(*tokV2) + err := sessionToken.ReadFromV2(*sessionTokenMsg) if err != nil { - return nil, fmt.Errorf("invalid session token: %w", err) + return nil, newInvalidRequestError( + newInvalidMetaHeaderError( + fmt.Errorf("invalid session token field: %w", err))) } - return &tok, nil + return &sessionToken, nil } -// getObjectIDFromRequestBody decodes oid.ID from the common interface of the -// object reference's holders. Returns an error if object ID is missing in the request. -func getObjectIDFromRequestBody(body interface{ GetAddress() *refsV2.Address }) (*oid.ID, error) { - idV2 := body.GetAddress().GetObjectID() - if idV2 == nil { - return nil, errors.New("missing object ID") +// bearerTokenFromRequest returns bearer token attached to the request by +// original client. Returns both nil if the token is missing. +func bearerTokenFromRequest(req request) (*bearer.Token, error) { + metaHeader := req.GetMetaHeader() + for metaHeader.GetOrigin() != nil { + metaHeader = metaHeader.GetOrigin() } - var id oid.ID - - err := id.ReadFromV2(*idV2) - if err != nil { - return nil, err + if metaHeader == nil { + return nil, nil } - return &id, nil -} - -func ownerFromToken(token *sessionSDK.Object) (*user.ID, []byte, error) { - // 1. First check signature of session token. - if !token.VerifySignature() { - return nil, nil, errInvalidSessionSig + bearerTokenMsg := metaHeader.GetBearerToken() + if bearerTokenMsg == nil { + return nil, nil } - // 2. Then check if session token owner issued the session token - tokenIssuer := token.Issuer() - key := token.IssuerPublicKeyBytes() + var bearerToken bearer.Token - if !isOwnerFromKey(tokenIssuer, key) { - // TODO: #767 in this case we can issue all owner keys from neofs.id and check once again - return nil, nil, errInvalidSessionOwner + err := bearerToken.ReadFromV2(*bearerTokenMsg) + if err != nil { + return nil, newInvalidRequestError( + newInvalidMetaHeaderError( + fmt.Errorf("invalid bearer token field: %w", err))) } - return &tokenIssuer, key, nil + return &bearerToken, nil } -func originalBodySignature(v *sessionV2.RequestVerificationHeader) *refsV2.Signature { - if v == nil { - return nil +// binaryClientPublicKeyFromRequest pulls binary-encoded public key used by +// original client to sign the request. If there is no error, the key is always +// non-empty. +func binaryClientPublicKeyFromRequest(req request) ([]byte, error) { + verificationHeader := req.GetVerificationHeader() + for verificationHeader.GetOrigin() != nil { + verificationHeader = verificationHeader.GetOrigin() } - for v.GetOrigin() != nil { - v = v.GetOrigin() + if verificationHeader == nil { + return nil, newInvalidRequestError( + newInvalidVerificationHeaderError(errMissingRequestVerificationHeader)) } - return v.GetBodySignature() -} - -func isOwnerFromKey(id user.ID, key []byte) bool { - if key == nil { - return false + sig := verificationHeader.GetBodySignature() + if sig == nil { + return nil, newInvalidRequestError( + newInvalidVerificationHeaderError(errMissingBodySignature)) } - pubKey, err := keys.NewPublicKeyFromBytes(key, elliptic.P256()) - if err != nil { - return false + bSigKey := sig.GetKey() + if len(bSigKey) == 0 { + return nil, newInvalidRequestError( + newInvalidVerificationHeaderError( + fmt.Errorf("invalid body signature: %w", errMissingPublicKey))) } - return id.Equals(user.ResolveFromECDSAPublicKey(ecdsa.PublicKey(*pubKey))) + return bSigKey, nil +} + +// requestWrapper is a wrapper over request providing needed interface. +type requestWrapper struct { + xHeaders []v2session.XHeader } -// assertVerb checks that token verb corresponds to op. -func assertVerb(tok sessionSDK.Object, op acl.Op) bool { - //nolint:exhaustive - switch op { - case acl.OpObjectPut: - return tok.AssertVerb(sessionSDK.VerbObjectPut, sessionSDK.VerbObjectDelete) - case acl.OpObjectDelete: - return tok.AssertVerb(sessionSDK.VerbObjectDelete) - case acl.OpObjectGet: - return tok.AssertVerb(sessionSDK.VerbObjectGet) - case acl.OpObjectHead: - return tok.AssertVerb( - sessionSDK.VerbObjectHead, - sessionSDK.VerbObjectGet, - sessionSDK.VerbObjectDelete, - sessionSDK.VerbObjectRange, - sessionSDK.VerbObjectRangeHash) - case acl.OpObjectSearch: - return tok.AssertVerb(sessionSDK.VerbObjectSearch, sessionSDK.VerbObjectDelete) - case acl.OpObjectRange: - return tok.AssertVerb(sessionSDK.VerbObjectRange, sessionSDK.VerbObjectRangeHash) - case acl.OpObjectHash: - return tok.AssertVerb(sessionSDK.VerbObjectRangeHash) +// wrapRequest wraps given request into requestWrapper. +func wrapRequest(r request) requestWrapper { + metaHeader := r.GetMetaHeader() + for metaHeader.GetOrigin() != nil { + metaHeader = metaHeader.GetOrigin() } - return false -} + var w requestWrapper -// assertSessionRelation checks if given token describing the NeoFS session -// relates to the given container and optional object. Missing object -// means that the context isn't bound to any NeoFS object in the container. -// Returns no error iff relation is correct. Criteria: -// -// session is bound to the given container -// object is not specified or session is bound to this object -// -// Session MUST be bound to the particular container, otherwise behavior is undefined. -func assertSessionRelation(tok sessionSDK.Object, cnr cid.ID, obj *oid.ID) error { - if !tok.AssertContainer(cnr) { - return errors.New("requested container is not related to the session") + if metaHeader != nil { + w.xHeaders = metaHeader.GetXHeaders() } - if obj != nil && !tok.AssertObject(*obj) { - return errors.New("requested object is not related to the session") + return w +} + +// GetRequestHeaderByKey looks up for X-header value set in the underlying +// request meta header by key. Returns zero if the X-header is missing. +func (x requestWrapper) GetRequestHeaderByKey(key string) string { + for i := range x.xHeaders { + if x.xHeaders[i].GetKey() == key { + return x.xHeaders[i].GetValue() + } } - return nil + return "" } diff --git a/pkg/services/object/acl/v2/util_test.go b/pkg/services/object/acl/v2/util_test.go deleted file mode 100644 index 2951219560..0000000000 --- a/pkg/services/object/acl/v2/util_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package v2 - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "testing" - - "github.com/nspcc-dev/neofs-api-go/v2/acl" - "github.com/nspcc-dev/neofs-api-go/v2/session" - bearertest "github.com/nspcc-dev/neofs-sdk-go/bearer/test" - aclsdk "github.com/nspcc-dev/neofs-sdk-go/container/acl" - cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" - oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" - sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session" - sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" - "github.com/nspcc-dev/neofs-sdk-go/user" - "github.com/stretchr/testify/require" -) - -func TestOriginalTokens(t *testing.T) { - pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - signer := user.NewAutoIDSigner(*pk) - - sToken := sessiontest.ObjectSigned(signer) - bToken := bearertest.Token(t) - - require.NoError(t, bToken.Sign(signer)) - - var bTokenV2 acl.BearerToken - bToken.WriteToV2(&bTokenV2) - // This line is needed because SDK uses some custom format for - // reserved filters, so `cid.ID` is not converted to string immediately. - require.NoError(t, bToken.ReadFromV2(bTokenV2)) - - var sTokenV2 session.Token - sToken.WriteToV2(&sTokenV2) - - for i := 0; i < 10; i++ { - metaHeaders := testGenerateMetaHeader(uint32(i), &bTokenV2, &sTokenV2) - res, err := originalSessionToken(metaHeaders) - require.NoError(t, err) - require.Equal(t, sToken, *res, i) - - bTok, err := originalBearerToken(metaHeaders) - require.NoError(t, err) - require.Equal(t, &bToken, bTok, i) - } -} - -func testGenerateMetaHeader(depth uint32, b *acl.BearerToken, s *session.Token) *session.RequestMetaHeader { - metaHeader := new(session.RequestMetaHeader) - metaHeader.SetBearerToken(b) - metaHeader.SetSessionToken(s) - - for i := uint32(0); i < depth; i++ { - link := metaHeader - metaHeader = new(session.RequestMetaHeader) - metaHeader.SetOrigin(link) - } - - return metaHeader -} - -func TestIsVerbCompatible(t *testing.T) { - // Source: https://nspcc.ru/upload/neofs-spec-latest.pdf#page=28 - table := map[aclsdk.Op][]sessionSDK.ObjectVerb{ - aclsdk.OpObjectPut: {sessionSDK.VerbObjectPut, sessionSDK.VerbObjectDelete}, - aclsdk.OpObjectDelete: {sessionSDK.VerbObjectDelete}, - aclsdk.OpObjectGet: {sessionSDK.VerbObjectGet}, - aclsdk.OpObjectHead: { - sessionSDK.VerbObjectHead, - sessionSDK.VerbObjectGet, - sessionSDK.VerbObjectDelete, - sessionSDK.VerbObjectRange, - sessionSDK.VerbObjectRangeHash, - }, - aclsdk.OpObjectRange: {sessionSDK.VerbObjectRange, sessionSDK.VerbObjectRangeHash}, - aclsdk.OpObjectHash: {sessionSDK.VerbObjectRangeHash}, - aclsdk.OpObjectSearch: {sessionSDK.VerbObjectSearch, sessionSDK.VerbObjectDelete}, - } - - verbs := []sessionSDK.ObjectVerb{ - sessionSDK.VerbObjectPut, - sessionSDK.VerbObjectDelete, - sessionSDK.VerbObjectHead, - sessionSDK.VerbObjectRange, - sessionSDK.VerbObjectRangeHash, - sessionSDK.VerbObjectGet, - sessionSDK.VerbObjectSearch, - } - - var tok sessionSDK.Object - - for op, list := range table { - for _, verb := range verbs { - var contains bool - for _, v := range list { - if v == verb { - contains = true - break - } - } - - tok.ForVerb(verb) - - require.Equal(t, contains, assertVerb(tok, op), - "%v in token, %s executing", verb, op) - } - } -} - -func TestAssertSessionRelation(t *testing.T) { - var tok sessionSDK.Object - cnr := cidtest.ID() - cnrOther := cidtest.ID() - obj := oidtest.ID() - objOther := oidtest.ID() - - // make sure ids differ, otherwise test won't work correctly - require.False(t, cnrOther.Equals(cnr)) - require.False(t, objOther.Equals(obj)) - - // bind session to the container (required) - tok.BindContainer(cnr) - - // test container-global session - require.NoError(t, assertSessionRelation(tok, cnr, nil)) - require.NoError(t, assertSessionRelation(tok, cnr, &obj)) - require.Error(t, assertSessionRelation(tok, cnrOther, nil)) - require.Error(t, assertSessionRelation(tok, cnrOther, &obj)) - - // limit the session to the particular object - tok.LimitByObjects(obj) - - // test fixed object session (here obj arg must be non-nil everywhere) - require.NoError(t, assertSessionRelation(tok, cnr, &obj)) - require.Error(t, assertSessionRelation(tok, cnr, &objOther)) -} From fb43a5fd21fe3649150161c8f26b54400332e633 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 10 Oct 2023 15:12:02 +0400 Subject: [PATCH 2/2] *: Upgrade to the latest NeoFS SDK revision Adopt eACL changes. Signed-off-by: Leonard Lyubich --- cmd/neofs-cli/internal/common/eacl.go | 16 +- cmd/neofs-cli/modules/acl/extended/create.go | 7 +- .../modules/acl/extended/create_test.go | 23 +- cmd/neofs-cli/modules/acl/extended/print.go | 5 +- cmd/neofs-cli/modules/bearer/create.go | 6 +- cmd/neofs-cli/modules/container/get_eacl.go | 3 +- cmd/neofs-cli/modules/container/set_eacl.go | 12 +- cmd/neofs-cli/modules/util/acl.go | 228 ++++++++---------- cmd/neofs-cli/modules/util/convert_eacl.go | 3 +- cmd/neofs-node/container.go | 2 +- cmd/neofs-node/object.go | 7 +- go.mod | 26 +- go.sum | 51 ++-- .../processors/container/process_eacl.go | 12 +- pkg/morph/client/container/eacl.go | 2 +- pkg/morph/client/container/eacl_set.go | 7 +- pkg/services/container/morph/executor.go | 22 +- pkg/services/container/morph/executor_test.go | 8 + pkg/services/object/acl/acl.go | 84 ++----- pkg/services/tree/signature.go | 32 +-- pkg/services/tree/signature_test.go | 46 +--- 21 files changed, 250 insertions(+), 352 deletions(-) diff --git a/cmd/neofs-cli/internal/common/eacl.go b/cmd/neofs-cli/internal/common/eacl.go index 7ad07b9475..ca9edc47d1 100644 --- a/cmd/neofs-cli/internal/common/eacl.go +++ b/cmd/neofs-cli/internal/common/eacl.go @@ -4,16 +4,14 @@ import ( "errors" "os" - "github.com/nspcc-dev/neofs-node/pkg/core/version" "github.com/nspcc-dev/neofs-sdk-go/eacl" - versionSDK "github.com/nspcc-dev/neofs-sdk-go/version" "github.com/spf13/cobra" ) var errUnsupportedEACLFormat = errors.New("unsupported eACL format") // ReadEACL reads extended ACL table from eaclPath. -func ReadEACL(cmd *cobra.Command, eaclPath string) *eacl.Table { +func ReadEACL(cmd *cobra.Command, eaclPath string) eacl.Table { _, err := os.Stat(eaclPath) // check if `eaclPath` is an existing file if err != nil { ExitOnErr(cmd, "", errors.New("incorrect path to file with EACL")) @@ -24,26 +22,18 @@ func ReadEACL(cmd *cobra.Command, eaclPath string) *eacl.Table { data, err := os.ReadFile(eaclPath) ExitOnErr(cmd, "can't read file with EACL: %w", err) - table := eacl.NewTable() + var table eacl.Table if err = table.UnmarshalJSON(data); err == nil { - validateAndFixEACLVersion(table) PrintVerbose(cmd, "Parsed JSON encoded EACL table") return table } if err = table.Unmarshal(data); err == nil { - validateAndFixEACLVersion(table) PrintVerbose(cmd, "Parsed binary encoded EACL table") return table } ExitOnErr(cmd, "", errUnsupportedEACLFormat) - return nil -} - -func validateAndFixEACLVersion(table *eacl.Table) { - if !version.IsValid(table.Version()) { - table.SetVersion(versionSDK.Current()) - } + return table } diff --git a/cmd/neofs-cli/modules/acl/extended/create.go b/cmd/neofs-cli/modules/acl/extended/create.go index 600a5d4ceb..a5d64eee37 100644 --- a/cmd/neofs-cli/modules/acl/extended/create.go +++ b/cmd/neofs-cli/modules/acl/extended/create.go @@ -10,7 +10,6 @@ import ( "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/util" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/spf13/cobra" ) @@ -84,13 +83,13 @@ func createEACL(cmd *cobra.Command, _ []string) { os.Exit(1) } - tb := eacl.NewTable() - common.ExitOnErr(cmd, "unable to parse provided rules: %w", util.ParseEACLRules(tb, rules)) + tb, err := util.ParseEACLRules(rules) + common.ExitOnErr(cmd, "unable to parse provided rules: %w", err) err = util.ValidateEACLTable(tb) common.ExitOnErr(cmd, "table validation: %w", err) - tb.SetCID(containerID) + tb.LimitByContainer(containerID) data, err := tb.MarshalJSON() if err != nil { diff --git a/cmd/neofs-cli/modules/acl/extended/create_test.go b/cmd/neofs-cli/modules/acl/extended/create_test.go index 16d4cc023a..8825520a64 100644 --- a/cmd/neofs-cli/modules/acl/extended/create_test.go +++ b/cmd/neofs-cli/modules/acl/extended/create_test.go @@ -1,6 +1,7 @@ package extended import ( + "fmt" "testing" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/util" @@ -59,32 +60,18 @@ func TestParseTable(t *testing.T) { }, } - eaclTable := eacl.NewTable() - for _, test := range tests { t.Run(test.name, func(t *testing.T) { - err := util.ParseEACLRule(eaclTable, test.rule) + recs, err := util.ParseEACLRule(test.rule) ok := len(test.jsonRecord) > 0 require.Equal(t, ok, err == nil, err) if ok { - expectedRecord := eacl.NewRecord() - err = expectedRecord.UnmarshalJSON([]byte(test.jsonRecord)) + var expectedTable eacl.Table + err = expectedTable.UnmarshalJSON([]byte(fmt.Sprintf(`{"records": [%s]}`, test.jsonRecord))) require.NoError(t, err) - actualRecord := eaclTable.Records()[len(eaclTable.Records())-1] - - equalRecords(t, expectedRecord, &actualRecord) + require.Equal(t, expectedTable.Records(), recs) } }) } } - -func equalRecords(t *testing.T, r1, r2 *eacl.Record) { - d1, err := r1.Marshal() - require.NoError(t, err) - - d2, err := r2.Marshal() - require.NoError(t, err) - - require.Equal(t, d1, d2) -} diff --git a/cmd/neofs-cli/modules/acl/extended/print.go b/cmd/neofs-cli/modules/acl/extended/print.go index 6f380d026b..6fb94071e3 100644 --- a/cmd/neofs-cli/modules/acl/extended/print.go +++ b/cmd/neofs-cli/modules/acl/extended/print.go @@ -26,14 +26,15 @@ func init() { func printEACL(cmd *cobra.Command, _ []string) { file, _ := cmd.Flags().GetString("file") - eaclTable := new(eacl.Table) + var eaclTable eacl.Table data, err := os.ReadFile(file) common.ExitOnErr(cmd, "can't read file with EACL: %w", err) if strings.HasSuffix(file, ".json") { common.ExitOnErr(cmd, "unable to parse json: %w", eaclTable.UnmarshalJSON(data)) } else { rules := strings.Split(strings.TrimSpace(string(data)), "\n") - common.ExitOnErr(cmd, "can't parse file with EACL: %w", util.ParseEACLRules(eaclTable, rules)) + eaclTable, err = util.ParseEACLRules(rules) + common.ExitOnErr(cmd, "can't parse file with EACL: %w", err) } util.PrettyPrintTableEACL(cmd, eaclTable) } diff --git a/cmd/neofs-cli/modules/bearer/create.go b/cmd/neofs-cli/modules/bearer/create.go index 48a3f0848c..ba684556c8 100644 --- a/cmd/neofs-cli/modules/bearer/create.go +++ b/cmd/neofs-cli/modules/bearer/create.go @@ -112,11 +112,11 @@ func createToken(cmd *cobra.Command, _ []string) { eaclPath, _ := cmd.Flags().GetString(eaclFlag) if eaclPath != "" { - table := eaclSDK.NewTable() + var table eaclSDK.Table raw, err := os.ReadFile(eaclPath) common.ExitOnErr(cmd, "can't read extended ACL file: %w", err) - common.ExitOnErr(cmd, "can't parse extended ACL: %w", json.Unmarshal(raw, table)) - b.SetEACLTable(*table) + common.ExitOnErr(cmd, "can't parse extended ACL: %w", json.Unmarshal(raw, &table)) + b.SetEACLTable(table) } var data []byte diff --git a/cmd/neofs-cli/modules/container/get_eacl.go b/cmd/neofs-cli/modules/container/get_eacl.go index 32c3e18adb..8777488657 100644 --- a/cmd/neofs-cli/modules/container/get_eacl.go +++ b/cmd/neofs-cli/modules/container/get_eacl.go @@ -43,8 +43,7 @@ var getExtendedACLCmd = &cobra.Command{ data, err = eaclTable.MarshalJSON() common.ExitOnErr(cmd, "can't encode to JSON: %w", err) } else { - data, err = eaclTable.Marshal() - common.ExitOnErr(cmd, "can't encode to binary: %w", err) + data = eaclTable.Marshal() } cmd.Println("dumping data to file:", containerPathTo) diff --git a/cmd/neofs-cli/modules/container/set_eacl.go b/cmd/neofs-cli/modules/container/set_eacl.go index 68761ca9a6..af4bd65471 100644 --- a/cmd/neofs-cli/modules/container/set_eacl.go +++ b/cmd/neofs-cli/modules/container/set_eacl.go @@ -34,7 +34,7 @@ Container ID in EACL table will be substituted with ID from the CLI.`, tok := getSession(cmd) - eaclTable.SetCID(id) + eaclTable.LimitByContainer(id) pk := key.GetOrGenerate(cmd) cli := internalclient.GetSDKClientByFlag(ctx, cmd, commonflags.RPC) @@ -86,7 +86,7 @@ Container ID in EACL table will be substituted with ID from the CLI.`, var setEACLPrm internalclient.SetEACLPrm setEACLPrm.SetClient(cli) - setEACLPrm.SetTable(*eaclTable) + setEACLPrm.SetTable(eaclTable) setEACLPrm.SetPrivateKey(*pk) if tok != nil { @@ -99,8 +99,7 @@ Container ID in EACL table will be substituted with ID from the CLI.`, cmd.Println("eACL modification request accepted for processing (the operation may not be completed yet)") if containerAwait { - exp, err := eaclTable.Marshal() - common.ExitOnErr(cmd, "broken EACL table: %w", err) + exp := eaclTable.Marshal() cmd.Println("awaiting...") @@ -124,10 +123,7 @@ Container ID in EACL table will be substituted with ID from the CLI.`, if err == nil { // compare binary values because EACL could have been set already table := res.EACL() - got, err := table.Marshal() - if err != nil { - continue - } + got := table.Marshal() if bytes.Equal(exp, got) { cmd.Println("EACL has been persisted on sidechain") diff --git a/cmd/neofs-cli/modules/util/acl.go b/cmd/neofs-cli/modules/util/acl.go index 12949042b3..e810ac7fd7 100644 --- a/cmd/neofs-cli/modules/util/acl.go +++ b/cmd/neofs-cli/modules/util/acl.go @@ -2,7 +2,6 @@ package util import ( "bytes" - "crypto/ecdsa" "encoding/hex" "errors" "fmt" @@ -12,6 +11,8 @@ import ( "github.com/flynn-archive/go-shlex" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neofs-sdk-go/container/acl" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" @@ -58,7 +59,7 @@ func boolToString(b bool) string { } // PrettyPrintTableEACL print extended ACL in table format. -func PrettyPrintTableEACL(cmd *cobra.Command, table *eacl.Table) { +func PrettyPrintTableEACL(cmd *cobra.Command, table eacl.Table) { out := tablewriter.NewWriter(cmd.OutOrStdout()) out.SetHeader([]string{"Operation", "Action", "Filters", "Targets"}) out.SetAlignment(tablewriter.ALIGN_CENTER) @@ -68,50 +69,34 @@ func PrettyPrintTableEACL(cmd *cobra.Command, table *eacl.Table) { for _, r := range table.Records() { out.Append([]string{ - r.Operation().String(), + r.Op().String(), r.Action().String(), eaclFiltersToString(r.Filters()), - eaclTargetsToString(r.Targets()), + eaclTargetToString(r), }) } out.Render() } -func eaclTargetsToString(ts []eacl.Target) string { +func eaclTargetToString(r eacl.Record) string { b := bytes.NewBuffer(nil) - for _, t := range ts { - keysExists := len(t.BinaryKeys()) > 0 - switch t.Role() { - case eacl.RoleUser: - b.WriteString("User") - if keysExists { - b.WriteString(": ") - } - case eacl.RoleSystem: - b.WriteString("System") - if keysExists { - b.WriteString(": ") - } - case eacl.RoleOthers: - b.WriteString("Others") - if keysExists { - b.WriteString(": ") - } - default: - b.WriteString("Unknown") - if keysExists { - b.WriteString(": ") - } - } - for i, pub := range t.BinaryKeys() { - if i != 0 { - b.WriteString(" ") - } - b.WriteString(hex.EncodeToString(pub)) - b.WriteString("\n") + switch { + case r.IsForRole(eacl.RoleContainerOwner): + b.WriteString("User") + case r.IsForRole(eacl.RoleSystem): + b.WriteString("System") + case r.IsForRole(eacl.RoleOthers): + b.WriteString("Others") + } + + for i, pub := range r.TargetBinaryKeys() { + if i != 0 { + b.WriteString(" ") } + b.WriteString(hex.EncodeToString(pub)) + b.WriteString("\n") } return b.String() @@ -122,7 +107,7 @@ func eaclFiltersToString(fs []eacl.Filter) string { tw := tabwriter.NewWriter(b, 0, 0, 1, ' ', 0) for _, f := range fs { - switch f.From() { + switch f.HeaderType() { case eacl.HeaderFromObject: _, _ = tw.Write([]byte("O:\t")) case eacl.HeaderFromRequest: @@ -133,17 +118,16 @@ func eaclFiltersToString(fs []eacl.Filter) string { _, _ = tw.Write([]byte(" \t")) } - _, _ = tw.Write([]byte(f.Key())) + _, _ = tw.Write([]byte(f.HeaderKey())) switch f.Matcher() { case eacl.MatchStringEqual: _, _ = tw.Write([]byte("\t==\t")) case eacl.MatchStringNotEqual: _, _ = tw.Write([]byte("\t!=\t")) - case eacl.MatchUnknown: } - _, _ = tw.Write([]byte(f.Value() + "\t")) + _, _ = tw.Write([]byte(f.HeaderValue() + "\t")) _, _ = tw.Write([]byte("\n")) } @@ -164,75 +148,91 @@ func eaclFiltersToString(fs []eacl.Filter) string { // Uses ParseEACLRule. // //nolint:godot -func ParseEACLRules(table *eacl.Table, rules []string) error { +func ParseEACLRules(rules []string) (eacl.Table, error) { if len(rules) == 0 { - return errors.New("no extended ACL rules has been provided") + return eacl.Table{}, errors.New("no extended ACL rules has been provided") } + var records []eacl.Record + for _, ruleStr := range rules { - err := ParseEACLRule(table, ruleStr) + rs, err := ParseEACLRule(ruleStr) if err != nil { - return fmt.Errorf("can't create extended acl record from rule '%s': %v", ruleStr, err) + return eacl.Table{}, fmt.Errorf("can't create extended acl record from rule '%s': %v", ruleStr, err) } + + records = append(records, rs...) } - return nil + + return eacl.New(records), nil } // ParseEACLRule parses eACL table from the following form: // [ ...] [ ...] // // Examples: -// allow get req:X-Header=123 obj:Attr=value others:0xkey1,key2 system:key3 user:key4 +// allow get req:X-Header=123 obj:Attr=value user others pubkey:0xkey1,0xkey2 // //nolint:godot -func ParseEACLRule(table *eacl.Table, rule string) error { +func ParseEACLRule(rule string) ([]eacl.Record, error) { r, err := shlex.Split(rule) if err != nil { - return fmt.Errorf("can't parse rule '%s': %v", rule, err) + return nil, fmt.Errorf("can't parse rule '%s': %v", rule, err) } - return parseEACLTable(table, r) + return parseEACLRecords(r) } -func parseEACLTable(tb *eacl.Table, args []string) error { +func parseEACLRecords(args []string) ([]eacl.Record, error) { if len(args) < 2 { - return errors.New("at least 2 arguments must be provided") + return nil, errors.New("at least 2 arguments must be provided") } var action eacl.Action - if !action.DecodeString(strings.ToUpper(args[0])) { - return errors.New("invalid action (expected 'allow' or 'deny')") - } - ops, err := eaclOperationsFromString(args[1]) - if err != nil { - return err + switch args[0] { + default: + return nil, errors.New("invalid action (expected 'allow' or 'deny')") + case "allow": + action = eacl.ActionAllow + case "deny": + action = eacl.ActionDeny } - r, err := parseEACLRecord(args[2:]) - if err != nil { - return err - } + ss := strings.Split(args[1], ",") + ops := make([]acl.Op, len(ss)) - r.SetAction(action) - - for _, op := range ops { - r := *r - r.SetOperation(op) - tb.AddRecord(&r) + for i := range ss { + switch ss[i] { + default: + return nil, fmt.Errorf("unsupported operation: %s", ss[i]) + case "get": + ops[i] = acl.OpObjectGet + case "head": + ops[i] = acl.OpObjectHead + case "put": + ops[i] = acl.OpObjectPut + case "delete": + ops[i] = acl.OpObjectDelete + case "search": + ops[i] = acl.OpObjectSearch + case "getrange": + ops[i] = acl.OpObjectRange + case "getrangehash": + ops[i] = acl.OpObjectHash + } } - return nil -} - -func parseEACLRecord(args []string) (*eacl.Record, error) { - r := new(eacl.Record) - for i := range args { - ss := strings.SplitN(args[i], ":", 2) + var records []eacl.Record + var roles []eacl.Role + var pubKeys []neofscrypto.PublicKey + var filters []eacl.Filter + for _, argN := range args[2:] { + ss := strings.SplitN(argN, ":", 2) switch prefix := strings.ToLower(ss[0]); prefix { case "req", "obj": // filters if len(ss) != 2 { - return nil, fmt.Errorf("invalid filter or target: %s", args[i]) + return nil, fmt.Errorf("invalid filter or target: %s", argN) } i := strings.Index(ss[1], "=") @@ -241,14 +241,14 @@ func parseEACLRecord(args []string) (*eacl.Record, error) { } var key, value string - var op eacl.Match + var matcher eacl.Matcher if 0 < i && ss[1][i-1] == '!' { key = ss[1][:i-1] - op = eacl.MatchStringNotEqual + matcher = eacl.MatchStringNotEqual } else { key = ss[1][:i] - op = eacl.MatchStringEqual + matcher = eacl.MatchStringEqual } value = ss[1][i+1:] @@ -258,50 +258,44 @@ func parseEACLRecord(args []string) (*eacl.Record, error) { typ = eacl.HeaderFromObject } - r.AddFilter(typ, op, key, value) - case "others", "system", "user", "pubkey": // targets - var err error - - var pubs []ecdsa.PublicKey - if len(ss) == 2 { - pubs, err = parseKeyList(ss[1]) - if err != nil { - return nil, err - } + filters = append(filters, eacl.NewFilter(typ, key, matcher, value)) + case "pubkey": + if len(ss) < 2 { + return nil, errors.New("missing public keys") } - var role eacl.Role - if prefix != "pubkey" { - role, err = eaclRoleFromString(prefix) - if err != nil { - return nil, err - } + pubs, err := parseKeyList(ss[1]) + if err != nil { + return nil, err } - eacl.AddFormedTarget(r, role, pubs...) - + pubKeys = append(pubKeys, pubs...) + case "user": + roles = append(roles, eacl.RoleContainerOwner) + case "others": + roles = append(roles, eacl.RoleOthers) + case "system": + return nil, errors.New("system role access must not be modified") default: return nil, fmt.Errorf("invalid prefix: %s", ss[0]) } } - return r, nil -} + if len(roles)+len(pubKeys) == 0 { + return nil, errors.New("neither roles nor public keys are specified") + } -// eaclRoleFromString parses eacl.Role from string. -func eaclRoleFromString(s string) (eacl.Role, error) { - var r eacl.Role - if !r.DecodeString(strings.ToUpper(s)) { - return r, fmt.Errorf("unexpected role %s", s) + for i := range ops { + records = append(records, eacl.NewRecord(action, ops[i], eacl.NewTarget(roles, pubKeys), filters...)) } - return r, nil + return records, nil } // parseKeyList parses list of hex-encoded public keys separated by comma. -func parseKeyList(s string) ([]ecdsa.PublicKey, error) { +func parseKeyList(s string) ([]neofscrypto.PublicKey, error) { ss := strings.Split(s, ",") - pubs := make([]ecdsa.PublicKey, len(ss)) + pubs := make([]neofscrypto.PublicKey, len(ss)) for i := range ss { st := strings.TrimPrefix(ss[i], "0x") pub, err := keys.NewPublicKeyFromString(st) @@ -309,34 +303,18 @@ func parseKeyList(s string) ([]ecdsa.PublicKey, error) { return nil, fmt.Errorf("invalid public key '%s': %w", ss[i], err) } - pubs[i] = ecdsa.PublicKey(*pub) + pubs[i] = (*neofsecdsa.PublicKey)(pub) } return pubs, nil } -// eaclOperationsFromString parses list of eacl.Operation separated by comma. -func eaclOperationsFromString(s string) ([]eacl.Operation, error) { - ss := strings.Split(s, ",") - ops := make([]eacl.Operation, len(ss)) - - for i := range ss { - if !ops[i].DecodeString(strings.ToUpper(ss[i])) { - return nil, fmt.Errorf("invalid operation: %s", ss[i]) - } - } - - return ops, nil -} - // ValidateEACLTable validates eACL table: // - eACL table must not modify [eacl.RoleSystem] access. -func ValidateEACLTable(t *eacl.Table) error { +func ValidateEACLTable(t eacl.Table) error { for _, record := range t.Records() { - for _, target := range record.Targets() { - if target.Role() == eacl.RoleSystem { - return errors.New("it is prohibited to modify system access") - } + if record.IsForRole(eacl.RoleSystem) { + return errors.New("it is prohibited to modify system access") } } diff --git a/cmd/neofs-cli/modules/util/convert_eacl.go b/cmd/neofs-cli/modules/util/convert_eacl.go index 54d8586c6d..98c583206e 100644 --- a/cmd/neofs-cli/modules/util/convert_eacl.go +++ b/cmd/neofs-cli/modules/util/convert_eacl.go @@ -39,8 +39,7 @@ func convertEACLTable(cmd *cobra.Command, _ []string) { data, err = table.MarshalJSON() common.ExitOnErr(cmd, "can't JSON encode extended ACL table: %w", err) } else { - data, err = table.Marshal() - common.ExitOnErr(cmd, "can't binary encode extended ACL table: %w", err) + data = table.Marshal() } if len(to) == 0 { diff --git a/cmd/neofs-node/container.go b/cmd/neofs-node/container.go index 1f77219768..1dc14c0c0d 100644 --- a/cmd/neofs-node/container.go +++ b/cmd/neofs-node/container.go @@ -682,7 +682,7 @@ func (m morphContainerWriter) PutEACL(eaclInfo containerCore.EACL) error { } if m.cacheEnabled { - id, _ := eaclInfo.Value.CID() + id, _ := eaclInfo.Value.Container() m.eacls.InvalidateEACL(id) } diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index 530a2f5598..66ec305ea1 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -357,12 +357,7 @@ func (s *morphEACLFetcher) GetEACL(cnr cid.ID) (*containercore.EACL, error) { return nil, err } - binTable, err := eaclInfo.Value.Marshal() - if err != nil { - return nil, fmt.Errorf("marshal eACL table: %w", err) - } - - if !eaclInfo.Signature.Verify(binTable) { + if !eaclInfo.Signature.Verify(eaclInfo.Value.Marshal()) { // TODO(@cthulhu-rider): #1387 use "const" error return nil, errors.New("invalid signature of the eACL table") } diff --git a/go.mod b/go.mod index f509cbf336..afe43efcca 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/chzyer/readline v1.5.1 github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 github.com/google/go-github/v39 v39.2.0 - github.com/google/uuid v1.3.0 - github.com/hashicorp/golang-lru/v2 v2.0.2 + github.com/google/uuid v1.3.1 + github.com/hashicorp/golang-lru/v2 v2.0.6 github.com/klauspost/compress v1.16.7 github.com/mitchellh/go-homedir v1.1.0 github.com/mr-tron/base58 v1.2.0 @@ -18,19 +18,19 @@ require ( github.com/nspcc-dev/neo-go v0.102.0 github.com/nspcc-dev/neofs-api-go/v2 v2.14.0 github.com/nspcc-dev/neofs-contract v0.16.0 - github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11 + github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11.0.20231010110833-c2f3f5e5ffa1 github.com/nspcc-dev/tzhash v1.7.1 github.com/olekukonko/tablewriter v0.0.5 github.com/panjf2000/ants/v2 v2.8.2 github.com/paulmach/orb v0.2.2 - github.com/prometheus/client_golang v1.13.0 + github.com/prometheus/client_golang v1.14.0 github.com/spf13/cast v1.5.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.14.0 github.com/stretchr/testify v1.8.4 go.etcd.io/bbolt v1.3.7 - go.uber.org/zap v1.24.0 + go.uber.org/zap v1.26.0 golang.org/x/sys v0.12.0 golang.org/x/term v0.11.0 google.golang.org/grpc v1.57.0 @@ -40,7 +40,6 @@ require ( require ( github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20221202181307-76fa05c21b12 // indirect - github.com/benbjohnson/clock v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/consensys/bavard v0.1.13 // indirect @@ -50,18 +49,18 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/golang/snappy v0.0.3 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect - github.com/hashicorp/golang-lru v0.6.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/holiman/uint256 v1.2.0 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/go-cid v0.3.2 // indirect github.com/klauspost/cpuid/v2 v2.2.2 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect @@ -83,7 +82,7 @@ require ( github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -93,16 +92,15 @@ require ( github.com/subosito/gotenv v1.4.1 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect github.com/twmb/murmur3 v1.1.5 // indirect - github.com/urfave/cli v1.22.5 // indirect + github.com/urfave/cli v1.22.12 // indirect go.uber.org/atomic v1.10.0 // indirect - go.uber.org/multierr v1.9.0 // indirect + go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.12.0 // indirect golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/text v0.12.0 // indirect - golang.org/x/time v0.1.0 // indirect golang.org/x/tools v0.11.1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 30f20da532..6a088e94ee 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,7 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CityOfZion/neo-go v0.62.1-pre.0.20191114145240-e740fbe708f8/go.mod h1:MJCkWUBhi9pn/CrYO1Q3P687y2KeahrOPS9BD9LDGb0= github.com/CityOfZion/neo-go v0.70.1-pre.0.20191209120015-fccb0085941e/go.mod h1:0enZl0az8xA6PVkwzEOwPWVJGqlt/GO4hA4kmQ5Xzig= @@ -58,7 +59,6 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521073959-f0d4d129b7f1/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20221202181307-76fa05c21b12 h1:npHgfD4Tl2WJS3AJaMUi5ynGDPUBfkg3U3fCzDyXZ+4= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20221202181307-76fa05c21b12/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -184,8 +184,8 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -224,8 +224,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -235,10 +235,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= -github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU= -github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.6 h1:3xi/Cafd1NaoEnS/yDssIiuVeDVywU0QdFGl3aQaQHM= +github.com/hashicorp/golang-lru/v2 v2.0.6/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= @@ -246,8 +246,9 @@ github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ipfs/go-cid v0.3.2 h1:OGgOd+JCFM+y1DjWPmVH+2/4POtpDzwcr7VgnB7mZXc= github.com/ipfs/go-cid v0.3.2/go.mod h1:gQ8pKqT/sUxGY+tIwy1RPpAojYu7jAyCp5Tz1svoupw= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -301,8 +302,9 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= @@ -378,8 +380,8 @@ github.com/nspcc-dev/neofs-crypto v0.4.0 h1:5LlrUAM5O0k1+sH/sktBtrgfWtq1pgpDs09f github.com/nspcc-dev/neofs-crypto v0.4.0/go.mod h1:6XJ8kbXgOfevbI2WMruOtI+qUJXNwSGM/E9eClXxPHs= github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211201182451-a5b61c4f6477/go.mod h1:dfMtQWmBHYpl9Dez23TGtIUKiFvCIxUZq/CkSIhEpz4= github.com/nspcc-dev/neofs-sdk-go v0.0.0-20220113123743-7f3162110659/go.mod h1:/jay1lr3w7NQd/VDBkEhkJmDmyPNsu4W+QV2obsUV40= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11 h1:QOc8ZRN5DXlAeRPh5QG9u8rMLgoeRNiZF5/vL7QupWg= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11/go.mod h1:W+ImTNRnSNMH8w43H1knCcIqwu7dLHePXtlJNZ7EFIs= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11.0.20231010110833-c2f3f5e5ffa1 h1:iFfR5YeE6LoND0H+hWP7vdj4PAWpTKs7r8c0L5drT9U= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11.0.20231010110833-c2f3f5e5ffa1/go.mod h1:4LToeC6jfed7PWoJXDTaj9sS4W+C1xm2SbCe709VR8U= github.com/nspcc-dev/rfc6979 v0.1.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE= github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= @@ -425,13 +427,15 @@ github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNk github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= @@ -483,6 +487,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -494,8 +499,9 @@ github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKk github.com/twmb/murmur3 v1.1.5 h1:i9OLS9fkuLzBXjt6dptlAEyk58fJsSTXbRg3SgVyqgk= github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -522,15 +528,15 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -738,8 +744,7 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= golang.org/x/tools v0.0.0-20180318012157-96caea41033d/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/pkg/innerring/processors/container/process_eacl.go b/pkg/innerring/processors/container/process_eacl.go index 5ed78dd194..894d35a9cc 100644 --- a/pkg/innerring/processors/container/process_eacl.go +++ b/pkg/innerring/processors/container/process_eacl.go @@ -33,7 +33,7 @@ func (cp *Processor) checkSetEACL(e container.SetEACL) error { binTable := e.Table() // unmarshal table - table := eacl.NewTable() + var table eacl.Table err := table.Unmarshal(binTable) if err != nil { @@ -45,7 +45,7 @@ func (cp *Processor) checkSetEACL(e container.SetEACL) error { return fmt.Errorf("table validation: %w", err) } - idCnr, ok := table.CID() + idCnr, ok := table.Container() if !ok { return errors.New("missing container ID in eACL table") } @@ -98,12 +98,10 @@ func (cp *Processor) approveSetEACL(e container.SetEACL) { } } -func validateEACl(t *eacl.Table) error { +func validateEACl(t eacl.Table) error { for _, record := range t.Records() { - for _, target := range record.Targets() { - if target.Role() == eacl.RoleSystem { - return errors.New("it is prohibited to modify system access") - } + if record.IsForRole(eacl.RoleSystem) { + return errors.New("it is prohibited to modify system access") } } diff --git a/pkg/morph/client/container/eacl.go b/pkg/morph/client/container/eacl.go index 2111c4e4b9..3fcd3e7add 100644 --- a/pkg/morph/client/container/eacl.go +++ b/pkg/morph/client/container/eacl.go @@ -71,7 +71,7 @@ func (c *Client) GetEACL(cnr cid.ID) (*container.EACL, error) { var res container.EACL - res.Value = eacl.NewTable() + res.Value = new(eacl.Table) if err = res.Value.Unmarshal(rawEACL); err != nil { return nil, err } diff --git a/pkg/morph/client/container/eacl_set.go b/pkg/morph/client/container/eacl_set.go index d41fd901b9..37306bcbfa 100644 --- a/pkg/morph/client/container/eacl_set.go +++ b/pkg/morph/client/container/eacl_set.go @@ -16,13 +16,8 @@ func PutEACL(c *Client, eaclInfo containercore.EACL) error { return errNilArgument } - data, err := eaclInfo.Value.Marshal() - if err != nil { - return fmt.Errorf("can't marshal eacl table: %w", err) - } - var prm PutEACLPrm - prm.SetTable(data) + prm.SetTable(eaclInfo.Value.Marshal()) if eaclInfo.Session != nil { prm.SetToken(eaclInfo.Session.Marshal()) diff --git a/pkg/services/container/morph/executor.go b/pkg/services/container/morph/executor.go index e0ead40fc3..24ccd42cd7 100644 --- a/pkg/services/container/morph/executor.go +++ b/pkg/services/container/morph/executor.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/nspcc-dev/neofs-api-go/v2/acl" "github.com/nspcc-dev/neofs-api-go/v2/container" "github.com/nspcc-dev/neofs-api-go/v2/refs" sessionV2 "github.com/nspcc-dev/neofs-api-go/v2/session" @@ -212,11 +213,23 @@ func (s *morphExecutor) SetExtendedACL(_ context.Context, tokV2 *sessionV2.Token return nil, errors.New("missing signature") } + eACLV2 := body.GetEACL() + if eACLV2 == nil { + return nil, errors.New("missing eACL") + } + + var eACL eaclSDK.Table + + err := eACL.ReadFromV2(*eACLV2) + if err != nil { + return nil, fmt.Errorf("decode eACL: %w", err) + } + eaclInfo := containercore.EACL{ - Value: eaclSDK.NewTableFromV2(body.GetEACL()), + Value: &eACL, } - err := eaclInfo.Signature.ReadFromV2(*sigV2) + err = eaclInfo.Signature.ReadFromV2(*sigV2) if err != nil { return nil, fmt.Errorf("can't read signature: %w", err) } @@ -267,8 +280,11 @@ func (s *morphExecutor) GetExtendedACL(_ context.Context, body *container.GetExt eaclInfo.Session.WriteToV2(tokV2) } + var eACLV2 acl.Table + eaclInfo.Value.WriteToV2(&eACLV2) + res := new(container.GetExtendedACLResponseBody) - res.SetEACL(eaclInfo.Value.ToV2()) + res.SetEACL(&eACLV2) res.SetSignature(&sigV2) res.SetSessionToken(tokV2) diff --git a/pkg/services/container/morph/executor_test.go b/pkg/services/container/morph/executor_test.go index 8d256f8c86..8dc4e33630 100644 --- a/pkg/services/container/morph/executor_test.go +++ b/pkg/services/container/morph/executor_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-api-go/v2/acl" "github.com/nspcc-dev/neofs-api-go/v2/container" "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/session" @@ -15,6 +16,7 @@ import ( cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" containertest "github.com/nspcc-dev/neofs-sdk-go/container/test" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + eacltest "github.com/nspcc-dev/neofs-sdk-go/eacl/test" sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" "github.com/nspcc-dev/neofs-sdk-go/user" "github.com/stretchr/testify/require" @@ -99,7 +101,13 @@ func TestInvalidToken(t *testing.T) { { name: "setEACL", op: func(e containerSvc.ServiceExecutor, tokV2 *session.Token) (err error) { + eACL := eacltest.Table(t) + + var eACLV2 acl.Table + eACL.WriteToV2(&eACLV2) + var reqBody container.SetExtendedACLRequestBody + reqBody.SetEACL(&eACLV2) reqBody.SetSignature(new(refs.Signature)) sign(&reqBody) diff --git a/pkg/services/object/acl/acl.go b/pkg/services/object/acl/acl.go index 05ce668be9..bdd6098a0a 100644 --- a/pkg/services/object/acl/acl.go +++ b/pkg/services/object/acl/acl.go @@ -459,7 +459,7 @@ func (c *Checker) getVerifiedBearerEACL(bearerToken bearer.Token, curEpoch uint6 return eACL, newInvalidBearerTokenError(errBearerClientAuth) } - cnrInToken, isSet := eACL.CID() + cnrInToken, isSet := eACL.Container() if isSet && !cnrInToken.Equals(cnrID) { return eACL, newInvalidBearerTokenError(errBearerContainerMismatch) } @@ -487,37 +487,11 @@ func (c *Checker) checkEACLAccess(eACL eacl.Table, cnr cid.ID, payload Container denyOnMatch = false } - var op acl.Op - - switch records[i].Operation() { - default: - return fmt.Errorf("process record #%d: unsupported operation #%d", i, op) - case eacl.OperationGet: - op = acl.OpObjectGet - case eacl.OperationHead: - op = acl.OpObjectHead - case eacl.OperationPut: - op = acl.OpObjectPut - case eacl.OperationDelete: - op = acl.OpObjectDelete - case eacl.OperationSearch: - op = acl.OpObjectSearch - case eacl.OperationRange: - op = acl.OpObjectRange - case eacl.OperationRangeHash: - op = acl.OpObjectHash - } - - if op != payload.op { + if !records[i].IsForOp(payload.op) { continue } - matchesClient, err := ruleMatchesClient(records[i], clientRole, bClientPubKey) - if err != nil { - return fmt.Errorf("process record #%d: %w", i, err) - } - - if !matchesClient { + if !ruleMatchesClient(records[i], clientRole, bClientPubKey) { continue } @@ -540,36 +514,26 @@ func (c *Checker) checkEACLAccess(eACL eacl.Table, cnr cid.ID, payload Container // checks whether given access rule matches client with specified role and // binary public key. -func ruleMatchesClient(rec eacl.Record, clientRole acl.Role, bClientPubKey []byte) (bool, error) { - targets := rec.Targets() - for i := range targets { - if targetKeys := targets[i].BinaryKeys(); len(targetKeys) != 0 { - for i := range targetKeys { - if bytes.Equal(targetKeys[i], bClientPubKey) { - return true, nil - } - } - continue +func ruleMatchesClient(rec eacl.Record, clientRole acl.Role, bClientPubKey []byte) bool { + bKeys := rec.TargetBinaryKeys() + for i := range bKeys { + if bytes.Equal(bKeys[i], bClientPubKey) { + return true } + } - targetRole := targets[i].Role() - switch targetRole { - default: - return false, fmt.Errorf("process target #%d: unsupported subject role #%d", i, targetRole) - case eacl.RoleUnknown, eacl.RoleSystem: - // system role access modifications have been deprecated - case eacl.RoleUser: - if clientRole == acl.RoleOwner { - return true, nil - } - case eacl.RoleOthers: - if clientRole == acl.RoleOthers { - return true, nil - } - } + switch clientRole { + default: + panic(fmt.Sprintf("unexpected role %v", clientRole)) + case acl.RoleContainer, acl.RoleInnerRing: + // system role access modifications have been deprecated + case acl.RoleOwner: + return rec.IsForRole(eacl.RoleContainerOwner) + case acl.RoleOthers: + return rec.IsForRole(eacl.RoleOthers) } - return false, nil + return false } // checks whether given access rule matches the resource represented by @@ -582,20 +546,20 @@ func ruleMatchesResource(rec eacl.Record, objHeaders *objectHeadersContext, reqH for i := range filters { var hdrValue string - switch hdrType := filters[i].From(); hdrType { + switch hdrType := filters[i].HeaderType(); hdrType { default: return false, fmt.Errorf("process filter #%d: unsupported type of headers #%d", i, hdrType) case eacl.HeaderFromService: // ignored by storage nodes continue case eacl.HeaderFromRequest: - hdrValue = reqHeaders.GetRequestHeaderByKey(filters[i].Key()) + hdrValue = reqHeaders.GetRequestHeaderByKey(filters[i].HeaderKey()) if hdrValue == "" { continue } case eacl.HeaderFromObject: var err error - hdrValue, err = objHeaders.getObjectHeaderByKey(filters[i].Key()) + hdrValue, err = objHeaders.getObjectHeaderByKey(filters[i].HeaderKey()) if err != nil { switch { default: @@ -616,11 +580,11 @@ func ruleMatchesResource(rec eacl.Record, objHeaders *objectHeadersContext, reqH default: return false, fmt.Errorf("process filter #%d: unsupported matcher #%d", i, matcher) case eacl.MatchStringEqual: - if hdrValue == filters[i].Value() { + if hdrValue == filters[i].HeaderValue() { matchedFilters++ } case eacl.MatchStringNotEqual: - if hdrValue != filters[i].Value() { + if hdrValue != filters[i].HeaderValue() { matchedFilters++ } } diff --git a/pkg/services/tree/signature.go b/pkg/services/tree/signature.go index d8476103af..6a2bb69cae 100644 --- a/pkg/services/tree/signature.go +++ b/pkg/services/tree/signature.go @@ -31,7 +31,7 @@ func basicACLErr(op acl.Op) error { return fmt.Errorf("access to operation %s is denied by basic ACL check", op) } -func eACLErr(op eacl.Operation, err error) error { +func eACLErr(op acl.Op, err error) error { return fmt.Errorf("access to operation %s is denied by extended ACL check: %w", op, err) } @@ -181,12 +181,12 @@ func roleFromReq(cnr *core.Container, req message) (acl.Role, error) { return role, nil } -func eACLOp(op acl.Op) eacl.Operation { +func eACLOp(op acl.Op) acl.Op { switch op { case acl.OpObjectGet: - return eacl.OperationGet + return acl.OpObjectGet case acl.OpObjectPut: - return eacl.OperationPut + return acl.OpObjectPut default: panic(fmt.Sprintf("unexpected tree service ACL operation: %s", op)) } @@ -195,7 +195,7 @@ func eACLOp(op acl.Op) eacl.Operation { func eACLRole(role acl.Role) eacl.Role { switch role { case acl.RoleOwner: - return eacl.RoleUser + return eacl.RoleContainerOwner case acl.RoleOthers: return eacl.RoleOthers default: @@ -213,10 +213,10 @@ var errNoAllowRules = errors.New("not found allowing rules for the request") // therefore, filtering leads to unexpected results. // The code was copied with the minor updates from the SDK repo: // https://github.com/nspcc-dev/neofs-sdk-go/blob/43a57d42dd50dc60465bfd3482f7f12bcfcf3411/eacl/validator.go#L28. -func checkEACL(tb eacl.Table, signer []byte, role eacl.Role, op eacl.Operation) error { +func checkEACL(tb eacl.Table, signer []byte, role eacl.Role, op acl.Op) error { for _, record := range tb.Records() { // check type of operation - if record.Operation() != op { + if !record.IsForOp(op) { continue } @@ -239,20 +239,12 @@ func checkEACL(tb eacl.Table, signer []byte, role eacl.Role, op eacl.Operation) } func targetMatches(rec eacl.Record, role eacl.Role, signer []byte) bool { - for _, target := range rec.Targets() { - // check public key match - if pubs := target.BinaryKeys(); len(pubs) != 0 { - for _, key := range pubs { - if bytes.Equal(key, signer) { - return true - } - } - - continue - } + if rec.IsForRole(role) { + return true + } - // check target group match - if role == target.Role() { + for _, key := range rec.TargetBinaryKeys() { + if bytes.Equal(key, signer) { return true } } diff --git a/pkg/services/tree/signature_test.go b/pkg/services/tree/signature_test.go index 20cd0bea47..11893888c5 100644 --- a/pkg/services/tree/signature_test.go +++ b/pkg/services/tree/signature_test.go @@ -14,6 +14,7 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/container/acl" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" netmapSDK "github.com/nspcc-dev/neofs-sdk-go/netmap" @@ -202,43 +203,20 @@ func TestMessageSign(t *testing.T) { } func testBearerToken(cid cid.ID, forPutGet, forGet *keys.PublicKey) bearer.Token { - tgtGet := eaclSDK.NewTarget() - tgtGet.SetRole(eaclSDK.RoleUnknown) - tgtGet.SetBinaryKeys([][]byte{forPutGet.Bytes(), forGet.Bytes()}) - - rGet := eaclSDK.NewRecord() - rGet.SetAction(eaclSDK.ActionAllow) - rGet.SetOperation(eaclSDK.OperationGet) - rGet.SetTargets(*tgtGet) - - tgtPut := eaclSDK.NewTarget() - tgtPut.SetRole(eaclSDK.RoleUnknown) - tgtPut.SetBinaryKeys([][]byte{forPutGet.Bytes()}) - - rPut := eaclSDK.NewRecord() - rPut.SetAction(eaclSDK.ActionAllow) - rPut.SetOperation(eaclSDK.OperationPut) - rPut.SetTargets(*tgtPut) - - tb := eaclSDK.NewTable() - tb.AddRecord(rGet) - tb.AddRecord(rPut) - - tgt := eaclSDK.NewTarget() - tgt.SetRole(eaclSDK.RoleOthers) - - for _, op := range []eaclSDK.Operation{eaclSDK.OperationGet, eaclSDK.OperationPut} { - r := eaclSDK.NewRecord() - r.SetAction(eaclSDK.ActionDeny) - r.SetTargets(*tgt) - r.SetOperation(op) - tb.AddRecord(r) - } + tb := eaclSDK.New([]eaclSDK.Record{ + eaclSDK.NewRecord(eaclSDK.ActionAllow, acl.OpObjectGet, eaclSDK.NewTargetWithKeys([]neofscrypto.PublicKey{ + (*neofsecdsa.PublicKeyRFC6979)(forPutGet), + (*neofsecdsa.PublicKeyRFC6979)(forGet), + })), + eaclSDK.NewRecord(eaclSDK.ActionAllow, acl.OpObjectPut, eaclSDK.NewTargetWithKey((*neofsecdsa.PublicKey)(forPutGet))), + eaclSDK.NewRecord(eaclSDK.ActionDeny, acl.OpObjectGet, eaclSDK.NewTargetWithRole(eaclSDK.RoleOthers)), + eaclSDK.NewRecord(eaclSDK.ActionDeny, acl.OpObjectPut, eaclSDK.NewTargetWithRole(eaclSDK.RoleOthers)), + }) - tb.SetCID(cid) + tb.LimitByContainer(cid) var b bearer.Token - b.SetEACLTable(*tb) + b.SetEACLTable(tb) return b }