From 04c58bb80f461e8cea3ea4c72886448296f8300a Mon Sep 17 00:00:00 2001 From: Pavel Karpy Date: Fri, 11 Feb 2022 15:25:05 +0300 Subject: [PATCH] [#1111] object/acl: Refactor service Make all operations that related to `neofs-api-go` library be placed in `v2` packages. They parse all v2-versioned structs info `neofs-sdk-go` abstractions and pass them to the corresponding `acl`/`eacl` packages. `v2` packages are the only packages that do import `neofs-api-go` library. `eacl` and `acl` provide public functions that only accepts `sdk` structures. Signed-off-by: Pavel Karpy --- cmd/neofs-node/object.go | 30 +- pkg/services/object/acl/acl.go | 741 +++--------------- pkg/services/object/acl/acl_test.go | 83 +- pkg/services/object/acl/basic_helper.go | 20 +- pkg/services/object/acl/classifier.go | 219 ------ pkg/services/object/acl/eacl/v2/eacl_test.go | 16 +- pkg/services/object/acl/eacl/v2/headers.go | 48 +- pkg/services/object/acl/eacl/v2/localstore.go | 4 +- pkg/services/object/acl/eacl/v2/object.go | 8 +- pkg/services/object/acl/eacl/v2/opts.go | 4 +- pkg/services/object/acl/opts.go | 51 -- pkg/services/object/acl/v2/classifier.go | 148 ++++ pkg/services/object/acl/v2/errors.go | 40 + pkg/services/object/acl/v2/opts.go | 51 ++ pkg/services/object/acl/v2/request.go | 135 ++++ pkg/services/object/acl/v2/service.go | 447 +++++++++++ pkg/services/object/acl/v2/types.go | 28 + pkg/services/object/acl/v2/util.go | 197 +++++ pkg/services/object/acl/v2/util_test.go | 38 + 19 files changed, 1287 insertions(+), 1021 deletions(-) delete mode 100644 pkg/services/object/acl/classifier.go delete mode 100644 pkg/services/object/acl/opts.go create mode 100644 pkg/services/object/acl/v2/classifier.go create mode 100644 pkg/services/object/acl/v2/errors.go create mode 100644 pkg/services/object/acl/v2/opts.go create mode 100644 pkg/services/object/acl/v2/request.go create mode 100644 pkg/services/object/acl/v2/service.go create mode 100644 pkg/services/object/acl/v2/types.go create mode 100644 pkg/services/object/acl/v2/util.go create mode 100644 pkg/services/object/acl/v2/util_test.go diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index 5464a0fee71..3748372705e 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -19,6 +19,7 @@ import ( 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" getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" @@ -194,7 +195,7 @@ func initObjectService(c *cfg) { coreConstructor := (*coreClientConstructor)(clientConstructor) - var irFetcher acl.InnerRingFetcher + var irFetcher v2.InnerRingFetcher if c.cfgMorph.client.ProbeNotary() { irFetcher = &innerRingFetcherWithNotary{ @@ -345,21 +346,22 @@ func initObjectService(c *cfg) { }, ) - aclSvc := acl.New( - acl.WithSenderClassifier( - acl.NewSenderClassifier( - c.log, - irFetcher, - c.cfgNetmap.wrapper, - ), - ), - acl.WithContainerSource( + aclSvc := v2.New( + v2.WithLogger(c.log), + v2.WithIRFetcher(irFetcher), + v2.WithNetmapClient(c.cfgNetmap.wrapper), + v2.WithContainerSource( c.cfgObject.cnrSource, ), - acl.WithNextService(splitSvc), - acl.WithLocalStorage(ls), - acl.WithEACLSource(c.cfgObject.eaclSource), - acl.WithNetmapState(c.cfgNetmap.state), + v2.WithNextService(splitSvc), + v2.WithEACLChecker( + acl.NewChecker(new(acl.CheckerPrm). + SetNetmapState(c.cfgNetmap.state). + SetEACLSource(c.cfgObject.eaclSource). + SetValidator(eaclSDK.NewValidator()). + SetLocalStorage(ls), + ), + ), ) respSvc := objectService.NewResponseService( diff --git a/pkg/services/object/acl/acl.go b/pkg/services/object/acl/acl.go index 72aed16de31..e6ce799f2f3 100644 --- a/pkg/services/object/acl/acl.go +++ b/pkg/services/object/acl/acl.go @@ -1,614 +1,137 @@ package acl import ( - "context" "crypto/ecdsa" + "crypto/elliptic" "errors" "fmt" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - bearer "github.com/nspcc-dev/neofs-api-go/v2/acl" - "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" - v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature" "github.com/nspcc-dev/neofs-node/pkg/core/container" - core "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" - objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl" eaclV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl/v2" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" - objectSDKAddress "github.com/nspcc-dev/neofs-sdk-go/object/address" - objectSDKID "github.com/nspcc-dev/neofs-sdk-go/object/id" + addressSDK "github.com/nspcc-dev/neofs-sdk-go/object/address" "github.com/nspcc-dev/neofs-sdk-go/owner" - "github.com/nspcc-dev/neofs-sdk-go/util/signature" + bearerSDK "github.com/nspcc-dev/neofs-sdk-go/token" ) -type ( - // Service checks basic ACL rules. - Service struct { - *cfg - } - - putStreamBasicChecker struct { - source *Service - next objectSvc.PutObjectStream - - *eACLCfg - } - - getStreamBasicChecker struct { - objectSvc.GetObjectStream - - info requestInfo - - *eACLCfg - } - - rangeStreamBasicChecker struct { - objectSvc.GetObjectRangeStream - - info requestInfo - - *eACLCfg - } - - searchStreamBasicChecker struct { - objectSvc.SearchStream - - info requestInfo - - *eACLCfg - } - - requestInfo struct { - basicACL basicACLHelper - requestRole eaclSDK.Role - isInnerRing bool - operation eaclSDK.Operation // put, get, head, etc. - cnrOwner *owner.ID // container owner - - cid *cid.ID - - oid *objectSDKID.ID - - senderKey []byte - - bearer *bearer.BearerToken // bearer token of request - - srcRequest interface{} - } -) - -// Option represents Service constructor option. -type Option func(*cfg) - -type cfg struct { - containers core.Source - - sender SenderClassifier - - next objectSvc.ServiceServer - - *eACLCfg -} - -type eACLCfg struct { - eaclSource eacl.Source - - eACL *eaclSDK.Validator - +// CheckerPrm groups parameters for Checker +// constructor. +type CheckerPrm struct { + eaclSrc eacl.Source + validator *eaclSDK.Validator localStorage *engine.StorageEngine - - state netmap.State -} - -type accessErr struct { - requestInfo - - failedCheckTyp string -} - -var ( - ErrMalformedRequest = errors.New("malformed request") - ErrUnknownRole = errors.New("can't classify request sender") - ErrUnknownContainer = errors.New("can't fetch container info") -) - -func defaultCfg() *cfg { - return &cfg{ - eACLCfg: new(eACLCfg), - } -} - -// New is a constructor for object ACL checking service. -func New(opts ...Option) Service { - cfg := defaultCfg() - - for i := range opts { - opts[i](cfg) - } - - cfg.eACL = eaclSDK.NewValidator() - - return Service{ - cfg: cfg, - } -} - -func (b Service) Get(request *object.GetRequest, stream objectSvc.GetObjectStream) error { - idCnr, err := getContainerIDFromRequest(request) - if err != nil { - return err - } - - sTok := originalSessionToken(request.GetMetaHeader()) - - req := metaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: originalBearerToken(request.GetMetaHeader()), - src: request, - } - - reqInfo, err := b.findRequestInfo(req, idCnr, eaclSDK.OperationGet) - if err != nil { - return err - } - - reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) - useObjectIDFromSession(&reqInfo, sTok) - - if !basicACLCheck(reqInfo) { - return basicACLErr(reqInfo) - } else if !eACLCheck(request, reqInfo, b.eACLCfg) { - return eACLErr(reqInfo) - } - - return b.next.Get(request, &getStreamBasicChecker{ - GetObjectStream: stream, - info: reqInfo, - eACLCfg: b.eACLCfg, - }) -} - -func (b Service) Put(ctx context.Context) (objectSvc.PutObjectStream, error) { - streamer, err := b.next.Put(ctx) - - return putStreamBasicChecker{ - source: &b, - next: streamer, - eACLCfg: b.eACLCfg, - }, err -} - -func (b Service) Head( - ctx context.Context, - request *object.HeadRequest) (*object.HeadResponse, error) { - idCnr, err := getContainerIDFromRequest(request) - if err != nil { - return nil, err - } - - sTok := originalSessionToken(request.GetMetaHeader()) - - req := metaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: originalBearerToken(request.GetMetaHeader()), - src: request, - } - - reqInfo, err := b.findRequestInfo(req, idCnr, eaclSDK.OperationHead) - if err != nil { - return nil, err - } - - reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) - useObjectIDFromSession(&reqInfo, sTok) - - if !basicACLCheck(reqInfo) { - return nil, basicACLErr(reqInfo) - } else if !eACLCheck(request, reqInfo, b.eACLCfg) { - return nil, eACLErr(reqInfo) - } - - resp, err := b.next.Head(ctx, request) - if err == nil { - if !eACLCheck(resp, reqInfo, b.eACLCfg) { - err = eACLErr(reqInfo) - } - } - - return resp, err -} - -func (b Service) Search(request *object.SearchRequest, stream objectSvc.SearchStream) error { - var id *cid.ID - - id, err := getContainerIDFromRequest(request) - if err != nil { - return err - } - - req := metaWithToken{ - vheader: request.GetVerificationHeader(), - token: originalSessionToken(request.GetMetaHeader()), - bearer: originalBearerToken(request.GetMetaHeader()), - src: request, - } - - reqInfo, err := b.findRequestInfo(req, id, eaclSDK.OperationSearch) - if err != nil { - return err - } - - reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) - - if !basicACLCheck(reqInfo) { - return basicACLErr(reqInfo) - } else if !eACLCheck(request, reqInfo, b.eACLCfg) { - return eACLErr(reqInfo) - } - - return b.next.Search(request, &searchStreamBasicChecker{ - SearchStream: stream, - info: reqInfo, - eACLCfg: b.eACLCfg, - }) -} - -func (b Service) Delete( - ctx context.Context, - request *object.DeleteRequest) (*object.DeleteResponse, error) { - idCnr, err := getContainerIDFromRequest(request) - if err != nil { - return nil, err - } - - sTok := originalSessionToken(request.GetMetaHeader()) - - req := metaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: originalBearerToken(request.GetMetaHeader()), - src: request, - } - - reqInfo, err := b.findRequestInfo(req, idCnr, eaclSDK.OperationDelete) - if err != nil { - return nil, err - } - - reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) - useObjectIDFromSession(&reqInfo, sTok) - - if !basicACLCheck(reqInfo) { - return nil, basicACLErr(reqInfo) - } else if !eACLCheck(request, reqInfo, b.eACLCfg) { - return nil, eACLErr(reqInfo) - } - - return b.next.Delete(ctx, request) + state netmap.State } -func (b Service) GetRange(request *object.GetRangeRequest, stream objectSvc.GetObjectRangeStream) error { - idCnr, err := getContainerIDFromRequest(request) - if err != nil { - return err - } - - sTok := originalSessionToken(request.GetMetaHeader()) - - req := metaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: originalBearerToken(request.GetMetaHeader()), - src: request, - } - - reqInfo, err := b.findRequestInfo(req, idCnr, eaclSDK.OperationRange) - if err != nil { - return err - } - - reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) - useObjectIDFromSession(&reqInfo, sTok) - - if !basicACLCheck(reqInfo) { - return basicACLErr(reqInfo) - } else if !eACLCheck(request, reqInfo, b.eACLCfg) { - return eACLErr(reqInfo) - } - - return b.next.GetRange(request, &rangeStreamBasicChecker{ - GetObjectRangeStream: stream, - info: reqInfo, - eACLCfg: b.eACLCfg, - }) -} - -func (b Service) GetRangeHash( - ctx context.Context, - request *object.GetRangeHashRequest) (*object.GetRangeHashResponse, error) { - idCnr, err := getContainerIDFromRequest(request) - if err != nil { - return nil, err - } - - sTok := originalSessionToken(request.GetMetaHeader()) - - req := metaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: originalBearerToken(request.GetMetaHeader()), - src: request, - } - - reqInfo, err := b.findRequestInfo(req, idCnr, eaclSDK.OperationRangeHash) - if err != nil { - return nil, err - } - - reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) - useObjectIDFromSession(&reqInfo, sTok) - - if !basicACLCheck(reqInfo) { - return nil, basicACLErr(reqInfo) - } else if !eACLCheck(request, reqInfo, b.eACLCfg) { - return nil, eACLErr(reqInfo) - } - - return b.next.GetRangeHash(ctx, request) -} - -func (p putStreamBasicChecker) Send(request *object.PutRequest) error { - body := request.GetBody() - if body == nil { - return ErrMalformedRequest - } - - part := body.GetObjectPart() - if part, ok := part.(*object.PutObjectPartInit); ok { - idCnr, err := getContainerIDFromRequest(request) - if err != nil { - return err - } - - ownerID, err := getObjectOwnerFromMessage(request) - if err != nil { - return err - } - - sTok := request.GetMetaHeader().GetSessionToken() - - req := metaWithToken{ - vheader: request.GetVerificationHeader(), - token: sTok, - bearer: originalBearerToken(request.GetMetaHeader()), - src: request, - } - - reqInfo, err := p.source.findRequestInfo(req, idCnr, eaclSDK.OperationPut) - if err != nil { - return err - } - - reqInfo.oid = getObjectIDFromRequestBody(part) - useObjectIDFromSession(&reqInfo, sTok) - - if !basicACLCheck(reqInfo) || !stickyBitCheck(reqInfo, ownerID) { - return basicACLErr(reqInfo) - } else if !eACLCheck(request, reqInfo, p.eACLCfg) { - return eACLErr(reqInfo) - } - } - - return p.next.Send(request) +func (c *CheckerPrm) SetEACLSource(v eacl.Source) *CheckerPrm { + c.eaclSrc = v + return c } -func (p putStreamBasicChecker) CloseAndRecv() (*object.PutResponse, error) { - return p.next.CloseAndRecv() -} - -func (g *getStreamBasicChecker) Send(resp *object.GetResponse) error { - if _, ok := resp.GetBody().GetObjectPart().(*object.GetObjectPartInit); ok { - if !eACLCheck(resp, g.info, g.eACLCfg) { - return eACLErr(g.info) - } - } - - return g.GetObjectStream.Send(resp) +func (c *CheckerPrm) SetValidator(v *eaclSDK.Validator) *CheckerPrm { + c.validator = v + return c } -func (g *rangeStreamBasicChecker) Send(resp *object.GetRangeResponse) error { - if !eACLCheck(resp, g.info, g.eACLCfg) { - return eACLErr(g.info) - } - - return g.GetObjectRangeStream.Send(resp) +func (c *CheckerPrm) SetLocalStorage(v *engine.StorageEngine) *CheckerPrm { + c.localStorage = v + return c } -func (g *searchStreamBasicChecker) Send(resp *object.SearchResponse) error { - if !eACLCheck(resp, g.info, g.eACLCfg) { - return eACLErr(g.info) - } - - return g.SearchStream.Send(resp) +func (c *CheckerPrm) SetNetmapState(v netmap.State) *CheckerPrm { + c.state = v + return c } -func (b Service) findRequestInfo( - req metaWithToken, - cid *cid.ID, - op eaclSDK.Operation) (info requestInfo, err error) { - cnr, err := b.containers.Get(cid) // fetch actual container - if err != nil || cnr.OwnerID() == nil { - return info, ErrUnknownContainer - } - - // find request role and key - role, isIR, key, err := b.sender.Classify(req, cid, cnr) - if err != nil { - return info, err - } - - if role == eaclSDK.RoleUnknown { - return info, ErrUnknownRole - } - - // find verb from token if it is present - verb := sourceVerbOfRequest(req, op) - - info.basicACL = basicACLHelper(cnr.BasicACL()) - info.requestRole = role - info.isInnerRing = isIR - info.operation = verb - info.cnrOwner = cnr.OwnerID() - info.cid = cid - - // it is assumed that at the moment the key will be valid, - // otherwise the request would not pass validation - info.senderKey = key - - // add bearer token if it is present in request - info.bearer = req.bearer - - info.srcRequest = req.src - - return info, nil +// Checker implements v2.ACLChecker interfaces and provides +// ACL/eACL validation functionality. +type Checker struct { + eaclSrc eacl.Source + validator *eaclSDK.Validator + localStorage *engine.StorageEngine + state netmap.State } -func getContainerIDFromRequest(req interface{}) (id *cid.ID, err error) { - switch v := req.(type) { - case *object.GetRequest: - return cid.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil - case *object.PutRequest: - objPart := v.GetBody().GetObjectPart() - if part, ok := objPart.(*object.PutObjectPartInit); ok { - return cid.NewFromV2(part.GetHeader().GetContainerID()), nil +// NewChecker creates Checker. +// Panics if at least one of the parameter is nil. +func NewChecker(prm *CheckerPrm) *Checker { + panicOnNil := func(fieldName string, field interface{}) { + if field == nil { + panic(fmt.Sprintf("incorrect field %s (%T): %v", fieldName, field, field)) } - - return nil, errors.New("can't get cid in chunk") - case *object.HeadRequest: - return cid.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil - case *object.SearchRequest: - return cid.NewFromV2(v.GetBody().GetContainerID()), nil - case *object.DeleteRequest: - return cid.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil - case *object.GetRangeRequest: - return cid.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil - case *object.GetRangeHashRequest: - return cid.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil - default: - return nil, errors.New("unknown request type") - } -} - -func useObjectIDFromSession(req *requestInfo, token *session.Token) { - if token == nil { - return } - objCtx, ok := token.GetBody().GetContext().(*session.ObjectSessionContext) - if !ok { - return - } - - req.oid = objectSDKID.NewIDFromV2( - objCtx.GetAddress().GetObjectID(), - ) -} + panicOnNil("EACLSource", prm.eaclSrc) + panicOnNil("EACLValidator", prm.validator) + panicOnNil("LocalStorageEngine", prm.localStorage) + panicOnNil("NetmapState", prm.state) -func getObjectIDFromRequestBody(body interface{}) *objectSDKID.ID { - switch v := body.(type) { - default: - return nil - case interface { - GetObjectID() *refs.ObjectID - }: - return objectSDKID.NewIDFromV2(v.GetObjectID()) - case interface { - GetAddress() *refs.Address - }: - return objectSDKID.NewIDFromV2(v.GetAddress().GetObjectID()) + return &Checker{ + eaclSrc: prm.eaclSrc, + validator: prm.validator, + localStorage: prm.localStorage, + state: prm.state, } } -func getObjectOwnerFromMessage(req interface{}) (id *owner.ID, err error) { - switch v := req.(type) { - case *object.PutRequest: - objPart := v.GetBody().GetObjectPart() - if part, ok := objPart.(*object.PutObjectPartInit); ok { - return owner.NewIDFromV2(part.GetHeader().GetOwnerID()), nil - } - - return nil, errors.New("can't get cid in chunk") - case *object.GetResponse: - objPart := v.GetBody().GetObjectPart() - if part, ok := objPart.(*object.GetObjectPartInit); ok { - return owner.NewIDFromV2(part.GetHeader().GetOwnerID()), nil - } - - return nil, errors.New("can't get cid in chunk") - default: - return nil, errors.New("unsupported request type") - } -} - -// main check function for basic ACL -func basicACLCheck(info requestInfo) bool { +// CheckBasicACL is a main check function for basic ACL. +func (c *Checker) CheckBasicACL(info v2.RequestInfo) bool { // check basic ACL permissions var checkFn func(eaclSDK.Operation) bool - switch info.requestRole { + switch info.RequestRole() { case eaclSDK.RoleUser: - checkFn = info.basicACL.UserAllowed + checkFn = basicACLHelper(info.BasicACL()).UserAllowed case eaclSDK.RoleSystem: - checkFn = info.basicACL.SystemAllowed - if info.isInnerRing { - checkFn = info.basicACL.InnerRingAllowed + checkFn = basicACLHelper(info.BasicACL()).SystemAllowed + if info.IsInnerRing() { + checkFn = basicACLHelper(info.BasicACL()).InnerRingAllowed } case eaclSDK.RoleOthers: - checkFn = info.basicACL.OthersAllowed + checkFn = basicACLHelper(info.BasicACL()).OthersAllowed default: // log there return false } - return checkFn(info.operation) + return checkFn(info.Operation()) } -func stickyBitCheck(info requestInfo, owner *owner.ID) bool { +// StickyBitCheck validates owner field in the request if sticky bit is enabled. +func (c *Checker) StickyBitCheck(info v2.RequestInfo, owner *owner.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 == eaclSDK.RoleSystem { + if info.RequestRole() == eaclSDK.RoleSystem { return true } - if !info.basicACL.Sticky() { + if !basicACLHelper(info.BasicACL()).Sticky() { return true } - if owner == nil || len(info.senderKey) == 0 { + if owner == nil || len(info.SenderKey()) == 0 { return false } - requestSenderKey := unmarshalPublicKey(info.senderKey) + requestSenderKey := unmarshalPublicKey(info.SenderKey()) return isOwnerFromKey(owner, requestSenderKey) } -func eACLCheck(msg interface{}, reqInfo requestInfo, cfg *eACLCfg) bool { - if reqInfo.basicACL.Final() { +// CheckEACL is a main check function for extended ACL. +func (c *Checker) CheckEACL(msg interface{}, reqInfo v2.RequestInfo) bool { + if basicACLHelper(reqInfo.BasicACL()).Final() { return true } // if bearer token is not allowed, then ignore it - if !reqInfo.basicACL.BearerAllowed(reqInfo.operation) { - reqInfo.bearer = nil + if !basicACLHelper(reqInfo.BasicACL()).BearerAllowed(reqInfo.Operation()) { + reqInfo.CleanBearer() } var ( @@ -616,29 +139,29 @@ func eACLCheck(msg interface{}, reqInfo requestInfo, cfg *eACLCfg) bool { err error ) - if reqInfo.bearer == nil { - table, err = cfg.eaclSource.GetEACL(reqInfo.cid) + if reqInfo.Bearer().Empty() { + table, err = c.eaclSrc.GetEACL(reqInfo.ContainerID()) if err != nil { return errors.Is(err, container.ErrEACLNotFound) } } else { - table = eaclSDK.NewTableFromV2(reqInfo.bearer.GetBody().GetEACL()) + table = reqInfo.Bearer().EACLTable() } // if bearer token is not present, isValidBearer returns true - if !isValidBearer(reqInfo, cfg.state) { + if !isValidBearer(reqInfo, c.state) { return false } hdrSrcOpts := make([]eaclV2.Option, 0, 3) - addr := objectSDKAddress.NewAddress() - addr.SetContainerID(reqInfo.cid) - addr.SetObjectID(reqInfo.oid) + addr := addressSDK.NewAddress() + addr.SetContainerID(reqInfo.ContainerID()) + addr.SetObjectID(reqInfo.ObjectID()) hdrSrcOpts = append(hdrSrcOpts, - eaclV2.WithLocalObjectStorage(cfg.localStorage), - eaclV2.WithAddress(addr.ToV2()), + eaclV2.WithLocalObjectStorage(c.localStorage), + eaclV2.WithAddress(addr), ) if req, ok := msg.(eaclV2.Request); ok { @@ -647,16 +170,16 @@ func eACLCheck(msg interface{}, reqInfo requestInfo, cfg *eACLCfg) bool { hdrSrcOpts = append(hdrSrcOpts, eaclV2.WithServiceResponse( msg.(eaclV2.Response), - reqInfo.srcRequest.(eaclV2.Request), + reqInfo.Request().(eaclV2.Request), ), ) } - action := cfg.eACL.CalculateAction(new(eaclSDK.ValidationUnit). - WithRole(reqInfo.requestRole). - WithOperation(reqInfo.operation). - WithContainerID(reqInfo.cid). - WithSenderKey(reqInfo.senderKey). + action := c.validator.CalculateAction(new(eaclSDK.ValidationUnit). + WithRole(reqInfo.RequestRole()). + WithOperation(reqInfo.Operation()). + WithContainerID(reqInfo.ContainerID()). + WithSenderKey(reqInfo.SenderKey()). WithHeaderSource( eaclV2.NewMessageHeaderSource(hdrSrcOpts...), ). @@ -666,97 +189,39 @@ func eACLCheck(msg interface{}, reqInfo requestInfo, cfg *eACLCfg) bool { return action == eaclSDK.ActionAllow } -// sourceVerbOfRequest looks for verb in session token and if it is not found, -// returns reqVerb. -func sourceVerbOfRequest(req metaWithToken, reqVerb eaclSDK.Operation) eaclSDK.Operation { - if req.token != nil { - switch v := req.token.GetBody().GetContext().(type) { - case *session.ObjectSessionContext: - return tokenVerbToOperation(v.GetVerb()) - default: - // do nothing, return request verb - } - } - - return reqVerb -} - -func tokenVerbToOperation(verb session.ObjectSessionVerb) eaclSDK.Operation { - switch verb { - case session.ObjectVerbGet: - return eaclSDK.OperationGet - case session.ObjectVerbPut: - return eaclSDK.OperationPut - case session.ObjectVerbHead: - return eaclSDK.OperationHead - case session.ObjectVerbSearch: - return eaclSDK.OperationSearch - case session.ObjectVerbDelete: - return eaclSDK.OperationDelete - case session.ObjectVerbRange: - return eaclSDK.OperationRange - case session.ObjectVerbRangeHash: - return eaclSDK.OperationRangeHash - default: - return eaclSDK.OperationUnknown - } -} - -func (a *accessErr) Error() string { - return fmt.Sprintf("access to operation %v is denied by %s check", a.operation, a.failedCheckTyp) -} - -func basicACLErr(info requestInfo) error { - return &accessErr{ - requestInfo: info, - failedCheckTyp: "basic ACL", - } -} - -func eACLErr(info requestInfo) error { - return &accessErr{ - requestInfo: info, - failedCheckTyp: "extended ACL", - } -} - // isValidBearer returns true if bearer token correctly signed by authorized -// entity. This method might be define on whole ACL service because it will -// require to fetch current epoch to check lifetime. -func isValidBearer(reqInfo requestInfo, st netmap.State) bool { - token := reqInfo.bearer +// 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) bool { + token := reqInfo.Bearer() // 0. Check if bearer token is present in reqInfo. It might be non nil // empty structure. - if token == nil || (token.GetBody() == nil && token.GetSignature() == nil) { + if token == nil || token.Empty() { return true } // 1. First check token lifetime. Simplest verification. - if !isValidLifetime(token.GetBody().GetLifetime(), st.CurrentEpoch()) { + if !isValidLifetime(token, st.CurrentEpoch()) { return false } // 2. Then check if bearer token is signed correctly. - signWrapper := v2signature.StableMarshalerWrapper{SM: token.GetBody()} - if err := signature.VerifyDataWithSource(signWrapper, func() (key, sig []byte) { - tokenSignature := token.GetSignature() - return tokenSignature.GetKey(), tokenSignature.GetSign() - }); err != nil { + if err := token.VerifySignature(); err != nil { return false // invalid signature } // 3. Then check if container owner signed this token. - tokenIssuerKey := unmarshalPublicKey(token.GetSignature().GetKey()) - if !isOwnerFromKey(reqInfo.cnrOwner, tokenIssuerKey) { + tokenIssuerKey := unmarshalPublicKey(token.Signature().Key()) + if !isOwnerFromKey(reqInfo.ContainerOwner(), tokenIssuerKey) { // TODO: #767 in this case we can issue all owner keys from neofs.id and check once again return false } // 4. Then check if request sender has rights to use this token. - tokenOwnerField := owner.NewIDFromV2(token.GetBody().GetOwnerID()) + tokenOwnerField := token.OwnerID() if tokenOwnerField != nil { // see bearer token owner field description - requestSenderKey := unmarshalPublicKey(reqInfo.senderKey) + requestSenderKey := unmarshalPublicKey(reqInfo.SenderKey()) if !isOwnerFromKey(tokenOwnerField, requestSenderKey) { // TODO: #767 in this case we can issue all owner keys from neofs.id and check once again return false @@ -766,13 +231,13 @@ func isValidBearer(reqInfo requestInfo, st netmap.State) bool { return true } -func isValidLifetime(lifetime *bearer.TokenLifetime, epoch uint64) bool { +func isValidLifetime(t *bearerSDK.BearerToken, epoch uint64) bool { // The "exp" (expiration time) claim identifies the expiration time on // or after which the JWT MUST NOT be accepted for processing. // The "nbf" (not before) claim identifies the time before which the JWT // MUST NOT be accepted for processing // RFC 7519 sections 4.1.4, 4.1.5 - return epoch >= lifetime.GetNbf() && epoch <= lifetime.GetExp() + return epoch >= t.NotBeforeTime() && epoch <= t.Expiration() } func isOwnerFromKey(id *owner.ID, key *keys.PublicKey) bool { @@ -783,22 +248,10 @@ func isOwnerFromKey(id *owner.ID, key *keys.PublicKey) bool { return id.Equal(owner.NewIDFromPublicKey((*ecdsa.PublicKey)(key))) } -// originalBearerToken goes down to original request meta header and fetches -// bearer token from there. -func originalBearerToken(header *session.RequestMetaHeader) *bearer.BearerToken { - for header.GetOrigin() != nil { - header = header.GetOrigin() - } - - return header.GetBearerToken() -} - -// originalSessionToken goes down to original request meta header and fetches -// session token from there. -func originalSessionToken(header *session.RequestMetaHeader) *session.Token { - for header.GetOrigin() != nil { - header = header.GetOrigin() +func unmarshalPublicKey(bs []byte) *keys.PublicKey { + pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256()) + if err != nil { + return nil } - - return header.GetSessionToken() + return pub } diff --git a/pkg/services/object/acl/acl_test.go b/pkg/services/object/acl/acl_test.go index 5ee3c528f7c..fea870ea65b 100644 --- a/pkg/services/object/acl/acl_test.go +++ b/pkg/services/object/acl/acl_test.go @@ -3,71 +3,66 @@ package acl import ( "testing" - "github.com/nspcc-dev/neofs-api-go/v2/acl" - acltest "github.com/nspcc-dev/neofs-api-go/v2/acl/test" - "github.com/nspcc-dev/neofs-api-go/v2/session" - sessiontest "github.com/nspcc-dev/neofs-api-go/v2/session/test" - "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" + v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" + cidSDK "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/owner" ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test" "github.com/stretchr/testify/require" ) -func TestOriginalTokens(t *testing.T) { - sToken := sessiontest.GenerateSessionToken(false) - bToken := acltest.GenerateBearerToken(false) +type emptyEACLSource struct{} - for i := 0; i < 10; i++ { - metaHeaders := testGenerateMetaHeader(uint32(i), bToken, sToken) - require.Equal(t, sToken, originalSessionToken(metaHeaders), i) - require.Equal(t, bToken, originalBearerToken(metaHeaders), i) - } +func (e emptyEACLSource) GetEACL(_ *cidSDK.ID) (*eaclSDK.Table, error) { + return nil, nil } -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) - } +type emptyNetmapState struct{} - return metaHeader +func (e emptyNetmapState) CurrentEpoch() uint64 { + return 0 } func TestStickyCheck(t *testing.T) { + checker := NewChecker(new(CheckerPrm). + SetLocalStorage(&engine.StorageEngine{}). + SetValidator(eaclSDK.NewValidator()). + SetEACLSource(emptyEACLSource{}). + SetNetmapState(emptyNetmapState{}), + ) + t.Run("system role", func(t *testing.T) { - var info requestInfo + var info v2.RequestInfo + + info.SetSenderKey(make([]byte, 33)) // any non-empty key + info.SetRequestRole(eaclSDK.RoleSystem) + + setSticky(&info, true) - info.senderKey = make([]byte, 33) // any non-empty key - info.requestRole = eacl.RoleSystem + require.True(t, checker.StickyBitCheck(info, ownertest.ID())) - info.basicACL.SetSticky() - require.True(t, stickyBitCheck(info, ownertest.ID())) + setSticky(&info, false) - info.basicACL.ResetSticky() - require.True(t, stickyBitCheck(info, ownertest.ID())) + require.True(t, checker.StickyBitCheck(info, ownertest.ID())) }) t.Run("owner ID and/or public key emptiness", func(t *testing.T) { - var info requestInfo + var info v2.RequestInfo - info.requestRole = eacl.RoleOthers // should be non-system role + info.SetRequestRole(eaclSDK.RoleOthers) // should be non-system role assertFn := func(isSticky, withKey, withOwner, expected bool) { if isSticky { - info.basicACL.SetSticky() + setSticky(&info, true) } else { - info.basicACL.ResetSticky() + setSticky(&info, false) } if withKey { - info.senderKey = make([]byte, 33) + info.SetSenderKey(make([]byte, 33)) } else { - info.senderKey = nil + info.SetSenderKey(nil) } var ownerID *owner.ID @@ -76,7 +71,7 @@ func TestStickyCheck(t *testing.T) { ownerID = ownertest.ID() } - require.Equal(t, expected, stickyBitCheck(info, ownerID)) + require.Equal(t, expected, checker.StickyBitCheck(info, ownerID)) } assertFn(true, false, false, false) @@ -88,3 +83,15 @@ func TestStickyCheck(t *testing.T) { assertFn(false, true, true, true) }) } + +func setSticky(req *v2.RequestInfo, enabled bool) { + bh := basicACLHelper(req.BasicACL()) + + if enabled { + bh.SetSticky() + } else { + bh.ResetSticky() + } + + req.SetBasicACL(uint32(bh)) +} diff --git a/pkg/services/object/acl/basic_helper.go b/pkg/services/object/acl/basic_helper.go index 861d25b711a..ba3bae1e267 100644 --- a/pkg/services/object/acl/basic_helper.go +++ b/pkg/services/object/acl/basic_helper.go @@ -28,17 +28,15 @@ const ( const leftACLBitPos = opOffset + bitsPerOp*opNumber - 1 -var ( - order = map[eacl.Operation]uint8{ - eacl.OperationRangeHash: 0, - eacl.OperationRange: 1, - eacl.OperationSearch: 2, - eacl.OperationDelete: 3, - eacl.OperationPut: 4, - eacl.OperationHead: 5, - eacl.OperationGet: 6, - } -) +var order = map[eacl.Operation]uint8{ + eacl.OperationRangeHash: 0, + eacl.OperationRange: 1, + eacl.OperationSearch: 2, + eacl.OperationDelete: 3, + eacl.OperationPut: 4, + eacl.OperationHead: 5, + eacl.OperationGet: 6, +} // returns true if n-th left bit is set (starting at 0). func isLeftBitSet(value basicACLHelper, n uint8) bool { diff --git a/pkg/services/object/acl/classifier.go b/pkg/services/object/acl/classifier.go deleted file mode 100644 index c1acf0bc036..00000000000 --- a/pkg/services/object/acl/classifier.go +++ /dev/null @@ -1,219 +0,0 @@ -package acl - -import ( - "bytes" - "crypto/ecdsa" - "crypto/elliptic" - "fmt" - - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - bearer "github.com/nspcc-dev/neofs-api-go/v2/acl" - "github.com/nspcc-dev/neofs-api-go/v2/session" - v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature" - core "github.com/nspcc-dev/neofs-node/pkg/core/netmap" - "github.com/nspcc-dev/neofs-sdk-go/container" - 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/netmap" - "github.com/nspcc-dev/neofs-sdk-go/owner" - "github.com/nspcc-dev/neofs-sdk-go/signature" - sigutil "github.com/nspcc-dev/neofs-sdk-go/util/signature" - "go.uber.org/zap" -) - -type ( - InnerRingFetcher interface { - InnerRingKeys() ([][]byte, error) - } - - metaWithToken struct { - vheader *session.RequestVerificationHeader - token *session.Token - bearer *bearer.BearerToken - src interface{} - } - - SenderClassifier struct { - log *zap.Logger - innerRing InnerRingFetcher - netmap core.Source - } -) - -func NewSenderClassifier(l *zap.Logger, ir InnerRingFetcher, nm core.Source) SenderClassifier { - return SenderClassifier{ - log: l, - innerRing: ir, - netmap: nm, - } -} - -func (c SenderClassifier) Classify( - req metaWithToken, - cid *cid.ID, - cnr *container.Container) (role eaclSDK.Role, isIR bool, key []byte, err error) { - if cid == nil { - return 0, false, nil, fmt.Errorf("%w: container id is not set", ErrMalformedRequest) - } - - ownerID, ownerKey, err := requestOwner(req) - if err != nil { - return 0, false, nil, err - } - - ownerKeyInBytes := ownerKey.Bytes() - - // TODO: #767 get owner from neofs.id if present - - // if request owner is the same as container owner, return RoleUser - if ownerID.Equal(cnr.OwnerID()) { - return eaclSDK.RoleUser, false, ownerKeyInBytes, nil - } - - isInnerRingNode, err := c.isInnerRingKey(ownerKeyInBytes) - 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 eaclSDK.RoleSystem, true, ownerKeyInBytes, nil - } - - isContainerNode, err := c.isContainerKey(ownerKeyInBytes, cid.ToV2().GetValue(), 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 eaclSDK.RoleSystem, false, ownerKeyInBytes, nil - } - - // if none of above, return RoleOthers - return eaclSDK.RoleOthers, false, ownerKeyInBytes, nil -} - -func requestOwner(req metaWithToken) (*owner.ID, *keys.PublicKey, error) { - if req.vheader == nil { - return nil, nil, fmt.Errorf("%w: nil verification header", ErrMalformedRequest) - } - - // if session token is presented, use it as truth source - if req.token.GetBody() != nil { - // verify signature of session token - return ownerFromToken(req.token) - } - - // otherwise get original body signature - bodySignature := originalBodySignature(req.vheader) - if bodySignature == nil { - return nil, nil, fmt.Errorf("%w: nil at body signature", ErrMalformedRequest) - } - - key := unmarshalPublicKey(bodySignature.Key()) - - return owner.NewIDFromPublicKey((*ecdsa.PublicKey)(key)), key, nil -} - -func originalBodySignature(v *session.RequestVerificationHeader) *signature.Signature { - if v == nil { - return nil - } - - for v.GetOrigin() != nil { - v = v.GetOrigin() - } - - return signature.NewFromV2(v.GetBodySignature()) -} - -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, cid []byte, - 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, cid, 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, cid, cnr) -} - -func lookupKeyInContainer( - nm *netmap.Netmap, - owner, cid []byte, - cnr *container.Container) (bool, error) { - cnrNodes, err := nm.GetContainerNodes(cnr.PlacementPolicy(), cid) - if err != nil { - return false, err - } - - flatCnrNodes := cnrNodes.Flatten() // we need single array to iterate on - for i := range flatCnrNodes { - if bytes.Equal(flatCnrNodes[i].PublicKey(), owner) { - return true, nil - } - } - - return false, nil -} - -func ownerFromToken(token *session.Token) (*owner.ID, *keys.PublicKey, error) { - // 1. First check signature of session token. - signWrapper := v2signature.StableMarshalerWrapper{SM: token.GetBody()} - if err := sigutil.VerifyDataWithSource(signWrapper, func() (key, sig []byte) { - tokenSignature := token.GetSignature() - return tokenSignature.GetKey(), tokenSignature.GetSign() - }); err != nil { - return nil, nil, fmt.Errorf("%w: invalid session token signature", ErrMalformedRequest) - } - - // 2. Then check if session token owner issued the session token - tokenIssuerKey := unmarshalPublicKey(token.GetSignature().GetKey()) - tokenOwner := owner.NewIDFromV2(token.GetBody().GetOwnerID()) - - if !isOwnerFromKey(tokenOwner, tokenIssuerKey) { - // TODO: #767 in this case we can issue all owner keys from neofs.id and check once again - return nil, nil, fmt.Errorf("%w: invalid session token owner", ErrMalformedRequest) - } - - return tokenOwner, tokenIssuerKey, nil -} - -func unmarshalPublicKey(bs []byte) *keys.PublicKey { - pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256()) - if err != nil { - return nil - } - return pub -} diff --git a/pkg/services/object/acl/eacl/v2/eacl_test.go b/pkg/services/object/acl/eacl/v2/eacl_test.go index 83872877c13..19970af98ed 100644 --- a/pkg/services/object/acl/eacl/v2/eacl_test.go +++ b/pkg/services/object/acl/eacl/v2/eacl_test.go @@ -13,39 +13,39 @@ import ( cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" objectSDK "github.com/nspcc-dev/neofs-sdk-go/object" - objectSDKAddress "github.com/nspcc-dev/neofs-sdk-go/object/address" - objectSDKID "github.com/nspcc-dev/neofs-sdk-go/object/id" + addressSDK "github.com/nspcc-dev/neofs-sdk-go/object/address" + oidSDK "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/stretchr/testify/require" ) type testLocalStorage struct { t *testing.T - expAddr *objectSDKAddress.Address + expAddr *addressSDK.Address obj *object.Object } -func (s *testLocalStorage) Head(addr *objectSDKAddress.Address) (*object.Object, error) { +func (s *testLocalStorage) Head(addr *addressSDK.Address) (*object.Object, error) { require.True(s.t, addr.ContainerID().Equal(addr.ContainerID()) && addr.ObjectID().Equal(addr.ObjectID())) return s.obj, nil } -func testID(t *testing.T) *objectSDKID.ID { +func testID(t *testing.T) *oidSDK.ID { cs := [sha256.Size]byte{} _, err := rand.Read(cs[:]) require.NoError(t, err) - id := objectSDKID.NewID() + id := oidSDK.NewID() id.SetSHA256(cs) return id } -func testAddress(t *testing.T) *objectSDKAddress.Address { - addr := objectSDKAddress.NewAddress() +func testAddress(t *testing.T) *addressSDK.Address { + addr := addressSDK.NewAddress() addr.SetObjectID(testID(t)) addr.SetContainerID(cidtest.ID()) diff --git a/pkg/services/object/acl/eacl/v2/headers.go b/pkg/services/object/acl/eacl/v2/headers.go index 45fc89cb002..15db901cf75 100644 --- a/pkg/services/object/acl/eacl/v2/headers.go +++ b/pkg/services/object/acl/eacl/v2/headers.go @@ -5,13 +5,12 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/acl" 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" "github.com/nspcc-dev/neofs-node/pkg/core/object" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" - objectSDKAddress "github.com/nspcc-dev/neofs-sdk-go/object/address" - objectSDKID "github.com/nspcc-dev/neofs-sdk-go/object/id" + addressSDK "github.com/nspcc-dev/neofs-sdk-go/object/address" + oidSDK "github.com/nspcc-dev/neofs-sdk-go/object/id" sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session" ) @@ -22,11 +21,11 @@ type cfg struct { msg xHeaderSource - addr *refs.Address + addr *addressSDK.Address } type ObjectStorage interface { - Head(*objectSDKAddress.Address) (*object.Object, error) + Head(*addressSDK.Address) (*object.Object, error) } type Request interface { @@ -83,11 +82,6 @@ func requestHeaders(msg xHeaderSource) []eaclSDK.Header { } func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) { - var addr *objectSDKAddress.Address - if h.addr != nil { - addr = objectSDKAddress.NewAddressFromV2(h.addr) - } - switch m := h.msg.(type) { default: panic(fmt.Sprintf("unexpected message type %T", h.msg)) @@ -101,26 +95,26 @@ func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) { *objectV2.GetRangeRequest, *objectV2.GetRangeHashRequest, *objectV2.DeleteRequest: - return addressHeaders(objectSDKAddress.NewAddressFromV2(h.addr)), true + return addressHeaders(h.addr), true case *objectV2.PutRequest: if v, ok := req.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit); ok { oV2 := new(objectV2.Object) oV2.SetObjectID(v.GetObjectID()) oV2.SetHeader(v.GetHeader()) - if addr == nil { - addr = objectSDKAddress.NewAddress() - addr.SetContainerID(cid.NewFromV2(v.GetHeader().GetContainerID())) - addr.SetObjectID(objectSDKID.NewIDFromV2(v.GetObjectID())) + if h.addr == nil { + addr := addressSDK.NewAddress() + addr.SetContainerID(cidSDK.NewFromV2(v.GetHeader().GetContainerID())) + addr.SetObjectID(oidSDK.NewIDFromV2(v.GetObjectID())) } - hs := headersFromObject(object.NewFromV2(oV2), addr) + hs := headersFromObject(object.NewFromV2(oV2), h.addr) return hs, true } case *objectV2.SearchRequest: return []eaclSDK.Header{cidHeader( - cid.NewFromV2( + cidSDK.NewFromV2( req.GetBody().GetContainerID()), )}, true } @@ -135,7 +129,7 @@ func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) { oV2.SetObjectID(v.GetObjectID()) oV2.SetHeader(v.GetHeader()) - return headersFromObject(object.NewFromV2(oV2), addr), true + return headersFromObject(object.NewFromV2(oV2), h.addr), true } case *objectV2.HeadResponse: oV2 := new(objectV2.Object) @@ -146,7 +140,7 @@ func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) { case *objectV2.ShortHeader: hdr = new(objectV2.Header) - hdr.SetContainerID(h.addr.GetContainerID()) + hdr.SetContainerID(h.addr.ContainerID().ToV2()) hdr.SetVersion(v.GetVersion()) hdr.SetCreationEpoch(v.GetCreationEpoch()) hdr.SetOwnerID(v.GetOwnerID()) @@ -158,16 +152,14 @@ func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) { oV2.SetHeader(hdr) - return headersFromObject(object.NewFromV2(oV2), addr), true + return headersFromObject(object.NewFromV2(oV2), h.addr), true } } return nil, true } -func (h *headerSource) localObjectHeaders(addrV2 *refs.Address) ([]eaclSDK.Header, bool) { - addr := objectSDKAddress.NewAddressFromV2(addrV2) - +func (h *headerSource) localObjectHeaders(addr *addressSDK.Address) ([]eaclSDK.Header, bool) { obj, err := h.storage.Head(addr) if err == nil { return headersFromObject(obj, addr), true @@ -176,21 +168,21 @@ func (h *headerSource) localObjectHeaders(addrV2 *refs.Address) ([]eaclSDK.Heade return addressHeaders(addr), false } -func cidHeader(cid *cid.ID) eaclSDK.Header { +func cidHeader(idCnr *cidSDK.ID) eaclSDK.Header { return &sysObjHdr{ k: acl.FilterObjectContainerID, - v: cidValue(cid), + v: cidValue(idCnr), } } -func oidHeader(oid *objectSDKID.ID) eaclSDK.Header { +func oidHeader(oid *oidSDK.ID) eaclSDK.Header { return &sysObjHdr{ k: acl.FilterObjectID, v: idValue(oid), } } -func addressHeaders(addr *objectSDKAddress.Address) []eaclSDK.Header { +func addressHeaders(addr *addressSDK.Address) []eaclSDK.Header { res := make([]eaclSDK.Header, 1, 2) res[0] = cidHeader(addr.ContainerID()) diff --git a/pkg/services/object/acl/eacl/v2/localstore.go b/pkg/services/object/acl/eacl/v2/localstore.go index 1afd4089a67..66a0ae52b4b 100644 --- a/pkg/services/object/acl/eacl/v2/localstore.go +++ b/pkg/services/object/acl/eacl/v2/localstore.go @@ -5,14 +5,14 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/core/object" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" - objectSDKAddress "github.com/nspcc-dev/neofs-sdk-go/object/address" + addressSDK "github.com/nspcc-dev/neofs-sdk-go/object/address" ) type localStorage struct { ls *engine.StorageEngine } -func (s *localStorage) Head(addr *objectSDKAddress.Address) (*object.Object, error) { +func (s *localStorage) Head(addr *addressSDK.Address) (*object.Object, error) { if s.ls == nil { return nil, io.ErrUnexpectedEOF } diff --git a/pkg/services/object/acl/eacl/v2/object.go b/pkg/services/object/acl/eacl/v2/object.go index dbcfd7bb8e5..1f3c17c604a 100644 --- a/pkg/services/object/acl/eacl/v2/object.go +++ b/pkg/services/object/acl/eacl/v2/object.go @@ -7,8 +7,8 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/core/object" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" - objectSDKAddress "github.com/nspcc-dev/neofs-sdk-go/object/address" - objectSDKID "github.com/nspcc-dev/neofs-sdk-go/object/id" + objectAddressSDK "github.com/nspcc-dev/neofs-sdk-go/object/address" + oidSDK "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/nspcc-dev/neofs-sdk-go/owner" ) @@ -24,7 +24,7 @@ func (s *sysObjHdr) Value() string { return s.v } -func idValue(id *objectSDKID.ID) string { +func idValue(id *oidSDK.ID) string { return id.String() } @@ -40,7 +40,7 @@ func u64Value(v uint64) string { return strconv.FormatUint(v, 10) } -func headersFromObject(obj *object.Object, addr *objectSDKAddress.Address) []eaclSDK.Header { +func headersFromObject(obj *object.Object, addr *objectAddressSDK.Address) []eaclSDK.Header { var count int for obj := obj; obj != nil; obj = obj.GetParent() { count += 9 + len(obj.Attributes()) diff --git a/pkg/services/object/acl/eacl/v2/opts.go b/pkg/services/object/acl/eacl/v2/opts.go index c8d32eb6669..656b0831e0a 100644 --- a/pkg/services/object/acl/eacl/v2/opts.go +++ b/pkg/services/object/acl/eacl/v2/opts.go @@ -1,8 +1,8 @@ package v2 import ( - "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" + addressSDK "github.com/nspcc-dev/neofs-sdk-go/object/address" ) func WithObjectStorage(v ObjectStorage) Option { @@ -36,7 +36,7 @@ func WithServiceResponse(resp Response, req Request) Option { } } -func WithAddress(v *refs.Address) Option { +func WithAddress(v *addressSDK.Address) Option { return func(c *cfg) { c.addr = v } diff --git a/pkg/services/object/acl/opts.go b/pkg/services/object/acl/opts.go deleted file mode 100644 index 3d51c3d5ff3..00000000000 --- a/pkg/services/object/acl/opts.go +++ /dev/null @@ -1,51 +0,0 @@ -package acl - -import ( - "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" - objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" - "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl" -) - -// WithContainerSource returns option to set container source. -func WithContainerSource(v container.Source) Option { - return func(c *cfg) { - c.containers = v - } -} - -// WithSenderClassifier returns option to set sender classifier. -func WithSenderClassifier(v SenderClassifier) Option { - return func(c *cfg) { - c.sender = v - } -} - -// WithNextService returns option to set next object service. -func WithNextService(v objectSvc.ServiceServer) Option { - return func(c *cfg) { - c.next = v - } -} - -// WithEACLSource returns options to set eACL table source. -func WithEACLSource(v eacl.Source) Option { - return func(c *cfg) { - c.eACLCfg.eaclSource = v - } -} - -// WithLocalStorage returns options to set local object storage. -func WithLocalStorage(v *engine.StorageEngine) Option { - return func(c *cfg) { - c.localStorage = v - } -} - -// WithNetmapState returns options to set global netmap state. -func WithNetmapState(v netmap.State) Option { - return func(c *cfg) { - c.state = v - } -} diff --git a/pkg/services/object/acl/v2/classifier.go b/pkg/services/object/acl/v2/classifier.go new file mode 100644 index 00000000000..15e0c602355 --- /dev/null +++ b/pkg/services/object/acl/v2/classifier.go @@ -0,0 +1,148 @@ +package v2 + +import ( + "bytes" + "errors" + + core "github.com/nspcc-dev/neofs-node/pkg/core/netmap" + "github.com/nspcc-dev/neofs-sdk-go/container" + cidSDK "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/netmap" + "go.uber.org/zap" +) + +type senderClassifier struct { + log *zap.Logger + innerRing InnerRingFetcher + netmap core.Source +} + +var errContainerIDNotSet = errors.New("container id is not set") + +type classifyResult struct { + role eaclSDK.Role + isIR bool + key []byte +} + +func (c senderClassifier) classify( + req MetaWithToken, + idCnr *cidSDK.ID, + cnr *container.Container) (res *classifyResult, err error) { + if idCnr == nil { + return nil, errContainerIDNotSet + } + + ownerID, ownerKey, err := req.RequestOwner() + if err != nil { + return nil, err + } + + ownerKeyInBytes := ownerKey.Bytes() + + // TODO: #767 get owner from neofs.id if present + + // if request owner is the same as container owner, return RoleUser + if ownerID.Equal(cnr.OwnerID()) { + return &classifyResult{ + role: eaclSDK.RoleUser, + isIR: false, + key: ownerKeyInBytes, + }, nil + } + + isInnerRingNode, err := c.isInnerRingKey(ownerKeyInBytes) + 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: eaclSDK.RoleSystem, + isIR: true, + key: ownerKeyInBytes, + }, nil + } + + isContainerNode, err := c.isContainerKey(ownerKeyInBytes, idCnr.ToV2().GetValue(), 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: eaclSDK.RoleSystem, + isIR: false, + key: ownerKeyInBytes, + }, nil + } + + // if none of above, return RoleOthers + return &classifyResult{ + role: eaclSDK.RoleOthers, + key: ownerKeyInBytes, + }, 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, idCnr []byte, + 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, idCnr []byte, + cnr *container.Container) (bool, error) { + cnrNodes, err := nm.GetContainerNodes(cnr.PlacementPolicy(), idCnr) + if err != nil { + return false, err + } + + flatCnrNodes := cnrNodes.Flatten() // we need single array to iterate on + for i := range flatCnrNodes { + if bytes.Equal(flatCnrNodes[i].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 new file mode 100644 index 00000000000..1219783ebbf --- /dev/null +++ b/pkg/services/object/acl/v2/errors.go @@ -0,0 +1,40 @@ +package v2 + +import ( + "errors" + "fmt" +) + +var ( + // ErrMalformedRequest is returned when request contains + // invalid data. + ErrMalformedRequest = errors.New("malformed request") + // ErrUnknownRole is returned when role of the sender is unknown. + ErrUnknownRole = errors.New("can't classify request sender") + // ErrUnknownContainer is returned when container fetching errors appeared. + ErrUnknownContainer = errors.New("can't fetch container info") +) + +type accessErr struct { + RequestInfo + + failedCheckTyp string +} + +func (a *accessErr) Error() string { + return fmt.Sprintf("access to operation %v is denied by %s check", a.operation, a.failedCheckTyp) +} + +func basicACLErr(info RequestInfo) error { + return &accessErr{ + RequestInfo: info, + failedCheckTyp: "basic ACL", + } +} + +func eACLErr(info RequestInfo) error { + return &accessErr{ + RequestInfo: info, + failedCheckTyp: "extended ACL", + } +} diff --git a/pkg/services/object/acl/v2/opts.go b/pkg/services/object/acl/v2/opts.go new file mode 100644 index 00000000000..ec2f8024264 --- /dev/null +++ b/pkg/services/object/acl/v2/opts.go @@ -0,0 +1,51 @@ +package v2 + +import ( + "github.com/nspcc-dev/neofs-node/pkg/core/container" + netmapClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/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 + } +} + +// WithNetmapClient return option to set +// netmap client. +func WithNetmapClient(v *netmapClient.Client) 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 new file mode 100644 index 00000000000..2910d8bbebf --- /dev/null +++ b/pkg/services/object/acl/v2/request.go @@ -0,0 +1,135 @@ +package v2 + +import ( + "crypto/ecdsa" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + sessionV2 "github.com/nspcc-dev/neofs-api-go/v2/session" + containerIDSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" + eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" + oidSDK "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/nspcc-dev/neofs-sdk-go/owner" + sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session" + bearerSDK "github.com/nspcc-dev/neofs-sdk-go/token" +) + +// RequestInfo groups parsed version-independent (from SDK library) +// request information and raw API request. +type RequestInfo struct { + basicACL uint32 + requestRole eaclSDK.Role + isInnerRing bool + operation eaclSDK.Operation // put, get, head, etc. + cnrOwner *owner.ID // container owner + + idCnr *containerIDSDK.ID + + oid *oidSDK.ID + + senderKey []byte + + bearer *bearerSDK.BearerToken // bearer token of request + + srcRequest interface{} +} + +func (r *RequestInfo) SetBasicACL(basicACL uint32) { + r.basicACL = basicACL +} + +func (r *RequestInfo) SetRequestRole(requestRole eaclSDK.Role) { + r.requestRole = requestRole +} + +func (r *RequestInfo) SetSenderKey(senderKey []byte) { + r.senderKey = senderKey +} + +// Request returns raw API request. +func (r RequestInfo) Request() interface{} { + return r.srcRequest +} + +// ContainerOwner returns owner if the container. +func (r RequestInfo) ContainerOwner() *owner.ID { + return r.cnrOwner +} + +// ObjectID return object ID. +func (r RequestInfo) ObjectID() *oidSDK.ID { + return r.oid +} + +// ContainerID return container ID. +func (r RequestInfo) ContainerID() *containerIDSDK.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() *bearerSDK.BearerToken { + return r.bearer +} + +// IsInnerRing specifies if request was made by inner ring. +func (r RequestInfo) IsInnerRing() bool { + return r.isInnerRing +} + +// BasicACL returns basic ACL of the container. +func (r RequestInfo) BasicACL() uint32 { + 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() eaclSDK.Operation { + return r.operation +} + +// RequestRole returns request sender's role. +func (r RequestInfo) RequestRole() eaclSDK.Role { + return r.requestRole +} + +// MetaWithToken groups session and bearer tokens, +// verification header and raw API request. +type MetaWithToken struct { + vheader *sessionV2.RequestVerificationHeader + token *sessionSDK.Token + bearer *bearerSDK.BearerToken + src interface{} +} + +// RequestOwner returns ownerID and its public key +// according to internal meta information. +func (r MetaWithToken) RequestOwner() (*owner.ID, *keys.PublicKey, error) { + if r.vheader == nil { + return nil, nil, fmt.Errorf("%w: nil verification header", ErrMalformedRequest) + } + + // 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, fmt.Errorf("%w: nil at body signature", ErrMalformedRequest) + } + + key := unmarshalPublicKey(bodySignature.Key()) + + return owner.NewIDFromPublicKey((*ecdsa.PublicKey)(key)), key, nil +} diff --git a/pkg/services/object/acl/v2/service.go b/pkg/services/object/acl/v2/service.go new file mode 100644 index 00000000000..b24cad92e1a --- /dev/null +++ b/pkg/services/object/acl/v2/service.go @@ -0,0 +1,447 @@ +package v2 + +import ( + "context" + "fmt" + + objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-node/pkg/core/container" + netmapClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap" + "github.com/nspcc-dev/neofs-node/pkg/services/object" + cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" + eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" + sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session" + "go.uber.org/zap" +) + +// Service checks basic ACL rules. +type Service struct { + *cfg + + c senderClassifier +} + +type putStreamBasicChecker struct { + source *Service + next object.PutObjectStream +} + +type getStreamBasicChecker struct { + checker ACLChecker + + object.GetObjectStream + + info RequestInfo +} + +type rangeStreamBasicChecker struct { + checker ACLChecker + + object.GetObjectRangeStream + + info RequestInfo +} + +type searchStreamBasicChecker struct { + checker ACLChecker + + object.SearchStream + + info RequestInfo +} + +// Option represents Service constructor option. +type Option func(*cfg) + +type cfg struct { + log *zap.Logger + + containers container.Source + + checker ACLChecker + + irFetcher InnerRingFetcher + + nm *netmapClient.Client + + next object.ServiceServer +} + +func defaultCfg() *cfg { + return &cfg{ + log: zap.L(), + } +} + +// New is a constructor for object ACL checking service. +func New(opts ...Option) Service { + cfg := defaultCfg() + + for i := range opts { + opts[i](cfg) + } + + panicOnNil := func(v interface{}, name string) { + if v == nil { + panic(fmt.Sprintf("ACL service: %s is nil", name)) + } + } + + 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, + }, + } +} + +// 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 { + cid, err := getContainerIDFromRequest(request) + if err != nil { + return err + } + + sTok := originalSessionToken(request.GetMetaHeader()) + + req := MetaWithToken{ + vheader: request.GetVerificationHeader(), + token: sTok, + bearer: originalBearerToken(request.GetMetaHeader()), + src: request, + } + + reqInfo, err := b.findRequestInfo(req, cid, eaclSDK.OperationGet) + if err != nil { + return err + } + + reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) + useObjectIDFromSession(&reqInfo, sTok) + + if !b.checker.CheckBasicACL(reqInfo) { + return basicACLErr(reqInfo) + } else if !b.checker.CheckEACL(request, reqInfo) { + return eACLErr(reqInfo) + } + + return b.next.Get(request, &getStreamBasicChecker{ + GetObjectStream: stream, + info: reqInfo, + checker: b.checker, + }) +} + +func (b Service) Put(ctx context.Context) (object.PutObjectStream, error) { + streamer, err := b.next.Put(ctx) + + return putStreamBasicChecker{ + source: &b, + next: streamer, + }, err +} + +func (b Service) Head( + ctx context.Context, + request *objectV2.HeadRequest) (*objectV2.HeadResponse, error) { + cid, err := getContainerIDFromRequest(request) + if err != nil { + return nil, err + } + + sTok := originalSessionToken(request.GetMetaHeader()) + + req := MetaWithToken{ + vheader: request.GetVerificationHeader(), + token: sTok, + bearer: originalBearerToken(request.GetMetaHeader()), + src: request, + } + + reqInfo, err := b.findRequestInfo(req, cid, eaclSDK.OperationHead) + if err != nil { + return nil, err + } + + reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) + useObjectIDFromSession(&reqInfo, sTok) + + if !b.checker.CheckBasicACL(reqInfo) { + return nil, basicACLErr(reqInfo) + } else if !b.checker.CheckEACL(request, reqInfo) { + return nil, eACLErr(reqInfo) + } + + resp, err := b.next.Head(ctx, request) + if err == nil { + if !b.checker.CheckEACL(resp, reqInfo) { + err = eACLErr(reqInfo) + } + } + + return resp, err +} + +func (b Service) Search(request *objectV2.SearchRequest, stream object.SearchStream) error { + var id *cidSDK.ID + + id, err := getContainerIDFromRequest(request) + if err != nil { + return err + } + + req := MetaWithToken{ + vheader: request.GetVerificationHeader(), + token: originalSessionToken(request.GetMetaHeader()), + bearer: originalBearerToken(request.GetMetaHeader()), + src: request, + } + + reqInfo, err := b.findRequestInfo(req, id, eaclSDK.OperationSearch) + if err != nil { + return err + } + + reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) + + if !b.checker.CheckBasicACL(reqInfo) { + return basicACLErr(reqInfo) + } else if !b.checker.CheckEACL(request, reqInfo) { + return eACLErr(reqInfo) + } + + return b.next.Search(request, &searchStreamBasicChecker{ + checker: b.checker, + SearchStream: stream, + info: reqInfo, + }) +} + +func (b Service) Delete( + ctx context.Context, + request *objectV2.DeleteRequest) (*objectV2.DeleteResponse, error) { + cid, err := getContainerIDFromRequest(request) + if err != nil { + return nil, err + } + + sTok := originalSessionToken(request.GetMetaHeader()) + + req := MetaWithToken{ + vheader: request.GetVerificationHeader(), + token: sTok, + bearer: originalBearerToken(request.GetMetaHeader()), + src: request, + } + + reqInfo, err := b.findRequestInfo(req, cid, eaclSDK.OperationDelete) + if err != nil { + return nil, err + } + + reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) + useObjectIDFromSession(&reqInfo, sTok) + + if !b.checker.CheckBasicACL(reqInfo) { + return nil, basicACLErr(reqInfo) + } else if !b.checker.CheckEACL(request, reqInfo) { + return nil, eACLErr(reqInfo) + } + + return b.next.Delete(ctx, request) +} + +func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetObjectRangeStream) error { + cid, err := getContainerIDFromRequest(request) + if err != nil { + return err + } + + sTok := originalSessionToken(request.GetMetaHeader()) + + req := MetaWithToken{ + vheader: request.GetVerificationHeader(), + token: sTok, + bearer: originalBearerToken(request.GetMetaHeader()), + src: request, + } + + reqInfo, err := b.findRequestInfo(req, cid, eaclSDK.OperationRange) + if err != nil { + return err + } + + reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) + useObjectIDFromSession(&reqInfo, sTok) + + if !b.checker.CheckBasicACL(reqInfo) { + return basicACLErr(reqInfo) + } else if !b.checker.CheckEACL(request, reqInfo) { + return eACLErr(reqInfo) + } + + return b.next.GetRange(request, &rangeStreamBasicChecker{ + checker: b.checker, + GetObjectRangeStream: stream, + info: reqInfo, + }) +} + +func (b Service) GetRangeHash( + ctx context.Context, + request *objectV2.GetRangeHashRequest) (*objectV2.GetRangeHashResponse, error) { + cid, err := getContainerIDFromRequest(request) + if err != nil { + return nil, err + } + + sTok := originalSessionToken(request.GetMetaHeader()) + + req := MetaWithToken{ + vheader: request.GetVerificationHeader(), + token: sTok, + bearer: originalBearerToken(request.GetMetaHeader()), + src: request, + } + + reqInfo, err := b.findRequestInfo(req, cid, eaclSDK.OperationRangeHash) + if err != nil { + return nil, err + } + + reqInfo.oid = getObjectIDFromRequestBody(request.GetBody()) + useObjectIDFromSession(&reqInfo, sTok) + + if !b.checker.CheckBasicACL(reqInfo) { + return nil, basicACLErr(reqInfo) + } else if !b.checker.CheckEACL(request, reqInfo) { + return nil, eACLErr(reqInfo) + } + + return b.next.GetRangeHash(ctx, request) +} + +func (p putStreamBasicChecker) Send(request *objectV2.PutRequest) error { + body := request.GetBody() + if body == nil { + return ErrMalformedRequest + } + + part := body.GetObjectPart() + if part, ok := part.(*objectV2.PutObjectPartInit); ok { + cid, err := getContainerIDFromRequest(request) + if err != nil { + return err + } + + ownerID, err := getObjectOwnerFromMessage(request) + if err != nil { + return err + } + + sTok := sessionSDK.NewTokenFromV2(request.GetMetaHeader().GetSessionToken()) + + req := MetaWithToken{ + vheader: request.GetVerificationHeader(), + token: sTok, + bearer: originalBearerToken(request.GetMetaHeader()), + src: request, + } + + reqInfo, err := p.source.findRequestInfo(req, cid, eaclSDK.OperationPut) + if err != nil { + return err + } + + reqInfo.oid = getObjectIDFromRequestBody(part) + useObjectIDFromSession(&reqInfo, sTok) + + if !p.source.checker.CheckBasicACL(reqInfo) || !p.source.checker.StickyBitCheck(reqInfo, ownerID) { + return basicACLErr(reqInfo) + } else if !p.source.checker.CheckEACL(request, reqInfo) { + return eACLErr(reqInfo) + } + } + + return p.next.Send(request) +} + +func (p putStreamBasicChecker) CloseAndRecv() (*objectV2.PutResponse, error) { + return p.next.CloseAndRecv() +} + +func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error { + if _, ok := resp.GetBody().GetObjectPart().(*objectV2.GetObjectPartInit); ok { + if !g.checker.CheckEACL(resp, g.info) { + return eACLErr(g.info) + } + } + + return g.GetObjectStream.Send(resp) +} + +func (g *rangeStreamBasicChecker) Send(resp *objectV2.GetRangeResponse) error { + if !g.checker.CheckEACL(resp, g.info) { + return eACLErr(g.info) + } + + return g.GetObjectRangeStream.Send(resp) +} + +func (g *searchStreamBasicChecker) Send(resp *objectV2.SearchResponse) error { + if !g.checker.CheckEACL(resp, g.info) { + return eACLErr(g.info) + } + + return g.SearchStream.Send(resp) +} + +func (b Service) findRequestInfo( + req MetaWithToken, + cid *cidSDK.ID, + op eaclSDK.Operation) (info RequestInfo, err error) { + cnr, err := b.containers.Get(cid) // fetch actual container + if err != nil || cnr.OwnerID() == nil { + return info, ErrUnknownContainer + } + + // find request role and key + res, err := b.c.classify(req, cid, cnr) + if err != nil { + return info, err + } + + if res.role == eaclSDK.RoleUnknown { + return info, ErrUnknownRole + } + + // find verb from token if it is present + verb := sourceVerbOfRequest(req, op) + + info.basicACL = cnr.BasicACL() + info.requestRole = res.role + info.isInnerRing = res.isIR + info.operation = verb + info.cnrOwner = cnr.OwnerID() + info.idCnr = cid + + // it is assumed that at the moment the key will be valid, + // otherwise the request would not pass validation + info.senderKey = res.key + + // add bearer token if it is present in request + info.bearer = req.bearer + + info.srcRequest = req.src + + return info, nil +} diff --git a/pkg/services/object/acl/v2/types.go b/pkg/services/object/acl/v2/types.go new file mode 100644 index 00000000000..7bdae84df9f --- /dev/null +++ b/pkg/services/object/acl/v2/types.go @@ -0,0 +1,28 @@ +package v2 + +import ( + "github.com/nspcc-dev/neofs-sdk-go/owner" +) + +// 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 true only if request + // passes extended ACL validation. + CheckEACL(interface{}, RequestInfo) bool + // StickyBitCheck must return true only if sticky bit + // is disabled or enabled but request contains correct + // owner field. + StickyBitCheck(RequestInfo, *owner.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 new file mode 100644 index 00000000000..f55f58d86c9 --- /dev/null +++ b/pkg/services/object/acl/v2/util.go @@ -0,0 +1,197 @@ +package v2 + +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" + containerIDSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" + eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" + oidSDK "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/nspcc-dev/neofs-sdk-go/owner" + sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session" + "github.com/nspcc-dev/neofs-sdk-go/signature" + bearerSDK "github.com/nspcc-dev/neofs-sdk-go/token" +) + +func getContainerIDFromRequest(req interface{}) (id *containerIDSDK.ID, err error) { + switch v := req.(type) { + case *objectV2.GetRequest: + return containerIDSDK.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil + case *objectV2.PutRequest: + objPart := v.GetBody().GetObjectPart() + if part, ok := objPart.(*objectV2.PutObjectPartInit); ok { + return containerIDSDK.NewFromV2(part.GetHeader().GetContainerID()), nil + } + + return nil, errors.New("can't get container ID in chunk") + case *objectV2.HeadRequest: + return containerIDSDK.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil + case *objectV2.SearchRequest: + return containerIDSDK.NewFromV2(v.GetBody().GetContainerID()), nil + case *objectV2.DeleteRequest: + return containerIDSDK.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil + case *objectV2.GetRangeRequest: + return containerIDSDK.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil + case *objectV2.GetRangeHashRequest: + return containerIDSDK.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil + default: + return nil, errors.New("unknown request type") + } +} + +// originalBearerToken goes down to original request meta header and fetches +// bearer token from there. +func originalBearerToken(header *sessionV2.RequestMetaHeader) *bearerSDK.BearerToken { + for header.GetOrigin() != nil { + header = header.GetOrigin() + } + + return bearerSDK.NewBearerTokenFromV2(header.GetBearerToken()) +} + +// originalSessionToken goes down to original request meta header and fetches +// session token from there. +func originalSessionToken(header *sessionV2.RequestMetaHeader) *sessionSDK.Token { + for header.GetOrigin() != nil { + header = header.GetOrigin() + } + + return sessionSDK.NewTokenFromV2(header.GetSessionToken()) +} + +func getObjectIDFromRequestBody(body interface{}) *oidSDK.ID { + switch v := body.(type) { + default: + return nil + case interface { + GetObjectID() *refsV2.ObjectID + }: + return oidSDK.NewIDFromV2(v.GetObjectID()) + case interface { + GetAddress() *refsV2.Address + }: + return oidSDK.NewIDFromV2(v.GetAddress().GetObjectID()) + } +} + +func getObjectOwnerFromMessage(req interface{}) (id *owner.ID, err error) { + switch v := req.(type) { + case *objectV2.PutRequest: + objPart := v.GetBody().GetObjectPart() + if part, ok := objPart.(*objectV2.PutObjectPartInit); ok { + return owner.NewIDFromV2(part.GetHeader().GetOwnerID()), nil + } + + return nil, errors.New("can't get container ID in chunk") + case *objectV2.GetResponse: + objPart := v.GetBody().GetObjectPart() + if part, ok := objPart.(*objectV2.GetObjectPartInit); ok { + return owner.NewIDFromV2(part.GetHeader().GetOwnerID()), nil + } + + return nil, errors.New("can't get container ID in chunk") + default: + return nil, errors.New("unsupported request type") + } +} + +// sourceVerbOfRequest looks for verb in session token and if it is not found, +// returns reqVerb. +func sourceVerbOfRequest(req MetaWithToken, reqVerb eaclSDK.Operation) eaclSDK.Operation { + if req.token != nil { + switch v := req.token.Context().(type) { + case *sessionSDK.ObjectContext: + return tokenVerbToOperation(v) + default: + // do nothing, return request verb + } + } + + return reqVerb +} + +func useObjectIDFromSession(req *RequestInfo, token *sessionSDK.Token) { + if token == nil { + return + } + + objCtx, ok := token.Context().(*sessionSDK.ObjectContext) + if !ok { + return + } + + req.oid = objCtx.Address().ObjectID() +} + +func tokenVerbToOperation(ctx *sessionSDK.ObjectContext) eaclSDK.Operation { + switch { + case ctx.IsForGet(): + return eaclSDK.OperationGet + case ctx.IsForPut(): + return eaclSDK.OperationPut + case ctx.IsForHead(): + return eaclSDK.OperationHead + case ctx.IsForSearch(): + return eaclSDK.OperationSearch + case ctx.IsForDelete(): + return eaclSDK.OperationDelete + case ctx.IsForRange(): + return eaclSDK.OperationRange + case ctx.IsForRangeHash(): + return eaclSDK.OperationRangeHash + default: + return eaclSDK.OperationUnknown + } +} + +func ownerFromToken(token *sessionSDK.Token) (*owner.ID, *keys.PublicKey, error) { + // 1. First check signature of session token. + if !token.VerifySignature() { + return nil, nil, fmt.Errorf("%w: invalid session token signature", ErrMalformedRequest) + } + + // 2. Then check if session token owner issued the session token + tokenIssuerKey := unmarshalPublicKey(token.Signature().Key()) + tokenOwner := token.OwnerID() + + if !isOwnerFromKey(tokenOwner, tokenIssuerKey) { + // TODO: #767 in this case we can issue all owner keys from neofs.id and check once again + return nil, nil, fmt.Errorf("%w: invalid session token owner", ErrMalformedRequest) + } + + return tokenOwner, tokenIssuerKey, nil +} + +func originalBodySignature(v *sessionV2.RequestVerificationHeader) *signature.Signature { + if v == nil { + return nil + } + + for v.GetOrigin() != nil { + v = v.GetOrigin() + } + + return signature.NewFromV2(v.GetBodySignature()) +} + +func unmarshalPublicKey(bs []byte) *keys.PublicKey { + pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256()) + if err != nil { + return nil + } + return pub +} + +func isOwnerFromKey(id *owner.ID, key *keys.PublicKey) bool { + if id == nil || key == nil { + return false + } + + return id.Equal(owner.NewIDFromPublicKey((*ecdsa.PublicKey)(key))) +} diff --git a/pkg/services/object/acl/v2/util_test.go b/pkg/services/object/acl/v2/util_test.go new file mode 100644 index 00000000000..d17a9c41589 --- /dev/null +++ b/pkg/services/object/acl/v2/util_test.go @@ -0,0 +1,38 @@ +package v2 + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/acl" + acltest "github.com/nspcc-dev/neofs-api-go/v2/acl/test" + "github.com/nspcc-dev/neofs-api-go/v2/session" + sessiontest "github.com/nspcc-dev/neofs-api-go/v2/session/test" + sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session" + bearerSDK "github.com/nspcc-dev/neofs-sdk-go/token" + "github.com/stretchr/testify/require" +) + +func TestOriginalTokens(t *testing.T) { + sToken := sessiontest.GenerateSessionToken(false) + bToken := acltest.GenerateBearerToken(false) + + for i := 0; i < 10; i++ { + metaHeaders := testGenerateMetaHeader(uint32(i), bToken, sToken) + require.Equal(t, sessionSDK.NewTokenFromV2(sToken), originalSessionToken(metaHeaders), i) + require.Equal(t, bearerSDK.NewBearerTokenFromV2(bToken), originalBearerToken(metaHeaders), i) + } +} + +func testGenerateMetaHeader(depth uint32, b *acl.BearerToken, s *session.SessionToken) *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 +}