diff --git a/cmd/neofs-node/accounting.go b/cmd/neofs-node/accounting.go index 0ab2daf8db..966902b23e 100644 --- a/cmd/neofs-node/accounting.go +++ b/cmd/neofs-node/accounting.go @@ -4,7 +4,6 @@ import ( accountingGRPC "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc" "github.com/nspcc-dev/neofs-node/pkg/morph/client/balance" accountingService "github.com/nspcc-dev/neofs-node/pkg/services/accounting" - accounting "github.com/nspcc-dev/neofs-node/pkg/services/accounting/morph" ) func initAccountingService(c *cfg) { @@ -15,17 +14,7 @@ func initAccountingService(c *cfg) { balanceMorphWrapper, err := balance.NewFromMorph(c.cfgMorph.client, c.shared.basics.balanceSH, 0) fatalOnErr(err) - server := accountingService.New( - accountingService.NewSignService( - &c.key.PrivateKey, - accountingService.NewResponseService( - accountingService.NewExecutionService( - accounting.NewExecutor(balanceMorphWrapper), - ), - c.respSvc, - ), - ), - ) + server := accountingService.New(&c.key.PrivateKey, c.networkState, balanceMorphWrapper) for _, srv := range c.cfgGRPC.servers { accountingGRPC.RegisterAccountingServiceServer(srv, server) diff --git a/cmd/neofs-node/config.go b/cmd/neofs-node/config.go index 4951d05f0c..f92f79bdcd 100644 --- a/cmd/neofs-node/config.go +++ b/cmd/neofs-node/config.go @@ -16,7 +16,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/keys" neogoutil "github.com/nspcc-dev/neo-go/pkg/util" - netmapV2 "github.com/nspcc-dev/neofs-api-go/v2/netmap" "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config" apiclientconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/apiclient" contractsconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/contracts" @@ -418,21 +417,22 @@ type cfg struct { cfgObject cfgObject } -// ReadCurrentNetMap reads network map which has been cached at the -// latest epoch. Returns an error if value has not been cached yet. +// GetNetworkMap reads network map which has been cached at the latest epoch. +// Returns an error if value has not been cached yet. // // Provides interface for NetmapService server. -func (c *cfg) ReadCurrentNetMap(msg *netmapV2.NetMap) error { +func (c *cfg) GetNetworkMap() (netmap.NetMap, error) { val := c.netMap.Load() if val == nil { - return errors.New("missing local network map") + return netmap.NetMap{}, errors.New("missing local network map") } - val.(netmap.NetMap).WriteToV2(msg) - - return nil + return val.(netmap.NetMap), nil } +// CurrentEpoch returns the latest cached epoch. +func (c *cfg) CurrentEpoch() uint64 { return c.networkState.CurrentEpoch() } + type cfgGRPC struct { listeners []net.Listener @@ -831,14 +831,11 @@ func (c *cfg) reloadObjectPoolSizes() { c.cfgObject.pool.replication.Tune(c.cfgObject.pool.replicatorPoolSize) } -func (c *cfg) LocalNodeInfo() (*netmapV2.NodeInfo, error) { +func (c *cfg) LocalNodeInfo() (netmap.NodeInfo, error) { c.cfgNodeInfo.localInfoLock.RLock() defer c.cfgNodeInfo.localInfoLock.RUnlock() - var res netmapV2.NodeInfo - c.cfgNodeInfo.localInfo.WriteToV2(&res) - - return &res, nil + return c.cfgNodeInfo.localInfo, nil } // handleLocalNodeInfoFromNetwork rewrites cached node info from the NeoFS network map. diff --git a/cmd/neofs-node/container.go b/cmd/neofs-node/container.go index c09614c99a..37536e02b0 100644 --- a/cmd/neofs-node/container.go +++ b/cmd/neofs-node/container.go @@ -8,8 +8,13 @@ import ( "errors" "fmt" - containerV2 "github.com/nspcc-dev/neofs-api-go/v2/container" - containerGRPC "github.com/nspcc-dev/neofs-api-go/v2/container/grpc" + apicontainer "github.com/nspcc-dev/neofs-api-go/v2/container" + protocontainer "github.com/nspcc-dev/neofs-api-go/v2/container/grpc" + apirefs "github.com/nspcc-dev/neofs-api-go/v2/refs" + refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + "github.com/nspcc-dev/neofs-api-go/v2/signature" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" containerrpc "github.com/nspcc-dev/neofs-contract/rpc/container" "github.com/nspcc-dev/neofs-node/pkg/core/client" containerCore "github.com/nspcc-dev/neofs-node/pkg/core/container" @@ -24,13 +29,16 @@ import ( loadroute "github.com/nspcc-dev/neofs-node/pkg/services/container/announcement/load/route" placementrouter "github.com/nspcc-dev/neofs-node/pkg/services/container/announcement/load/route/placement" loadstorage "github.com/nspcc-dev/neofs-node/pkg/services/container/announcement/load/storage" - containerMorph "github.com/nspcc-dev/neofs-node/pkg/services/container/morph" + "github.com/nspcc-dev/neofs-node/pkg/services/util" apiClient "github.com/nspcc-dev/neofs-sdk-go/client" containerSDK "github.com/nspcc-dev/neofs-sdk-go/container" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/nspcc-dev/neofs-sdk-go/netmap" 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" + "github.com/nspcc-dev/neofs-sdk-go/version" "go.uber.org/zap" ) @@ -46,17 +54,16 @@ func initContainerService(c *cfg) { cnrCli := c.shared.basics.cCli - cnrRdr := new(morphContainerReader) - cnrWrt := &morphContainerWriter{neoClient: cnrCli} + cnrs := &containersInChain{contractClient: cnrCli} cnrSrc := cntClient.AsContainerSource(cnrCli) eaclFetcher := &morphEACLFetcher{cnrCli} if c.shared.basics.ttl <= 0 { c.cfgObject.eaclSource = eaclFetcher - cnrRdr.eacl = eaclFetcher + cnrs.eacl = eaclFetcher c.cfgObject.cnrSource = cnrSrc - cnrRdr.get = cnrSrc - cnrRdr.lister = cnrCli + cnrs.get = cnrSrc + cnrs.lister = cnrCli } else { cnrCache := c.shared.basics.containerCache cnrListCache := c.shared.basics.containerListCache @@ -108,12 +115,11 @@ func initContainerService(c *cfg) { c.cfgObject.eaclSource = eaclCache c.cfgObject.cnrSource = cnrCache - cnrRdr.lister = cnrListCache - cnrRdr.eacl = c.cfgObject.eaclSource - cnrRdr.get = c.cfgObject.cnrSource - - cnrWrt.cacheEnabled = true - cnrWrt.eacls = eaclCache + cnrs.lister = cnrListCache + cnrs.eacl = c.cfgObject.eaclSource + cnrs.get = c.cfgObject.cnrSource + cnrs.cacheEnabled = true + cnrs.eacls = eaclCache } estimationsLogger := c.log.With(zap.String("component", "container_estimations")) @@ -186,24 +192,16 @@ func initContainerService(c *cfg) { }) }) - server := containerService.New( - containerService.NewSignService( - &c.key.PrivateKey, - containerService.NewResponseService( - &usedSpaceService{ - Server: containerService.NewExecutionService(containerMorph.NewExecutor(cnrRdr, cnrWrt, c.networkState)), - loadWriterProvider: loadRouter, - loadPlacementBuilder: loadPlacementBuilder, - routeBuilder: routeBuilder, - cfg: c, - }, - c.respSvc, - ), - ), - ) + server := &usedSpaceService{ + ContainerServiceServer: containerService.New(&c.key.PrivateKey, c.networkState, cnrs), + loadWriterProvider: loadRouter, + loadPlacementBuilder: loadPlacementBuilder, + routeBuilder: routeBuilder, + cfg: c, + } for _, srv := range c.cfgGRPC.servers { - containerGRPC.RegisterContainerServiceServer(srv, server) + protocontainer.RegisterContainerServiceServer(srv, server) } } @@ -468,7 +466,7 @@ func (d *localStorageLoad) Iterate(f loadcontroller.UsedSpaceFilter, h loadcontr } type usedSpaceService struct { - containerService.Server + protocontainer.ContainerServiceServer loadWriterProvider loadcontroller.WriterProvider @@ -518,10 +516,37 @@ func (c *usedSpaceService) ExternalAddresses() []string { return c.cfg.ExternalAddresses() } -func (c *usedSpaceService) AnnounceUsedSpace(ctx context.Context, req *containerV2.AnnounceUsedSpaceRequest) (*containerV2.AnnounceUsedSpaceResponse, error) { +func (c *usedSpaceService) makeResponse(body *protocontainer.AnnounceUsedSpaceResponse_Body, st *protostatus.Status) (*protocontainer.AnnounceUsedSpaceResponse, error) { + v := version.Current() + var v2 apirefs.Version + v.WriteToV2(&v2) + resp := &protocontainer.AnnounceUsedSpaceResponse{ + Body: body, + MetaHeader: &protosession.ResponseMetaHeader{ + Version: v2.ToGRPCMessage().(*refs.Version), + Epoch: c.cfg.networkState.CurrentEpoch(), + Status: st, + }, + } + return util.SignResponse(&c.cfg.key.PrivateKey, resp, apicontainer.AnnounceUsedSpaceResponse{}), nil +} + +func (c *usedSpaceService) makeStatusResponse(err error) (*protocontainer.AnnounceUsedSpaceResponse, error) { + return c.makeResponse(nil, util.ToStatus(err)) +} + +func (c *usedSpaceService) AnnounceUsedSpace(ctx context.Context, req *protocontainer.AnnounceUsedSpaceRequest) (*protocontainer.AnnounceUsedSpaceResponse, error) { + putReq := new(apicontainer.AnnounceUsedSpaceRequest) + if err := putReq.FromGRPCMessage(req); err != nil { + return nil, err + } + if err := signature.VerifyServiceMessage(putReq); err != nil { + return c.makeStatusResponse(util.ToRequestSignatureVerificationError(err)) + } + var passedRoute []loadroute.ServerInfo - for hdr := req.GetVerificationHeader(); hdr != nil; hdr = hdr.GetOrigin() { + for hdr := req.GetVerifyHeader(); hdr != nil; hdr = hdr.GetOrigin() { passedRoute = append(passedRoute, &containerOnlyKeyRemoteServerInfo{ key: hdr.GetBodySignature().GetKey(), }) @@ -535,28 +560,27 @@ func (c *usedSpaceService) AnnounceUsedSpace(ctx context.Context, req *container w, err := c.loadWriterProvider.InitWriter(loadroute.NewRouteContext(ctx, passedRoute)) if err != nil { - return nil, fmt.Errorf("could not initialize container's used space writer: %w", err) + return c.makeStatusResponse(fmt.Errorf("could not initialize container's used space writer: %w", err)) } var est containerSDK.SizeEstimation - for _, aV2 := range req.GetBody().GetAnnouncements() { - err = est.ReadFromV2(aV2) + for _, a := range req.GetBody().GetAnnouncements() { + var a2 apicontainer.UsedSpaceAnnouncement + if err := a2.FromGRPCMessage(a); err != nil { + panic(err) + } + err = est.ReadFromV2(a2) if err != nil { - return nil, fmt.Errorf("invalid size announcement: %w", err) + return c.makeStatusResponse(fmt.Errorf("invalid size announcement: %w", err)) } if err := c.processLoadValue(ctx, est, passedRoute, w); err != nil { - return nil, err + return c.makeStatusResponse(err) } } - respBody := new(containerV2.AnnounceUsedSpaceResponseBody) - - resp := new(containerV2.AnnounceUsedSpaceResponse) - resp.SetBody(respBody) - - return resp, nil + return c.makeResponse(nil, util.StatusOK) } var errNodeOutsideContainer = errors.New("node outside the container") @@ -619,8 +643,7 @@ func (c *usedSpaceService) processLoadValue(_ context.Context, a containerSDK.Si return nil } -// implements interface required by container service provided by morph executor. -type morphContainerReader struct { +type containersInChain struct { eacl containerCore.EACLSource get containerCore.Source @@ -628,44 +651,82 @@ type morphContainerReader struct { lister interface { List(*user.ID) ([]cid.ID, error) } + + contractClient *cntClient.Client + + cacheEnabled bool + eacls *ttlEACLStorage } -func (x *morphContainerReader) Get(id cid.ID) (*containerCore.Container, error) { - return x.get.Get(id) +func (x *containersInChain) Get(id cid.ID) (containerSDK.Container, error) { + c, err := x.get.Get(id) + if err != nil { + return containerSDK.Container{}, err + } + return c.Value, nil } -func (x *morphContainerReader) GetEACL(id cid.ID) (*containerCore.EACL, error) { - return x.eacl.GetEACL(id) +func (x *containersInChain) GetEACL(id cid.ID) (eacl.Table, error) { + c, err := x.eacl.GetEACL(id) + if err != nil { + return eacl.Table{}, err + } + return *c.Value, nil } -func (x *morphContainerReader) List(id *user.ID) ([]cid.ID, error) { - return x.lister.List(id) +func (x *containersInChain) List(id user.ID) ([]cid.ID, error) { + return x.lister.List(&id) } -type morphContainerWriter struct { - neoClient *cntClient.Client +func (x *containersInChain) Put(cnr containerSDK.Container, pub, sig []byte, st *session.Container) (cid.ID, error) { + data := cnr.Marshal() + d := cnr.ReadDomain() - cacheEnabled bool - eacls *ttlEACLStorage -} + var prm cntClient.PutPrm + prm.SetContainer(data) + prm.SetName(d.Name()) + prm.SetZone(d.Zone()) + prm.SetKey(pub) + prm.SetSignature(sig) + if st != nil { + prm.SetToken(st.Marshal()) + } + if v := cnr.Attribute("__NEOFS__METAINFO_CONSISTENCY"); v == "optimistic" || v == "strict" { + prm.EnableMeta() + } + if err := x.contractClient.Put(prm); err != nil { + return cid.ID{}, err + } -func (m morphContainerWriter) Put(cnr containerCore.Container) (*cid.ID, error) { - return cntClient.Put(m.neoClient, cnr) + return cid.NewFromMarshalledContainer(data), nil } -func (m morphContainerWriter) Delete(witness containerCore.RemovalWitness) error { - return cntClient.Delete(m.neoClient, witness) +func (x *containersInChain) Delete(id cid.ID, _, sig []byte, st *session.Container) error { + var prm cntClient.DeletePrm + prm.SetCID(id[:]) + prm.SetSignature(sig) + prm.RequireAlphabetSignature() + if st != nil { + prm.SetToken(st.Marshal()) + } + return x.contractClient.Delete(prm) } -func (m morphContainerWriter) PutEACL(eaclInfo containerCore.EACL) error { - err := cntClient.PutEACL(m.neoClient, eaclInfo) - if err != nil { +func (x *containersInChain) PutEACL(eACL eacl.Table, pub, sig []byte, st *session.Container) error { + var prm cntClient.PutEACLPrm + prm.SetTable(eACL.Marshal()) + prm.SetKey(pub) + prm.SetSignature(sig) + prm.RequireAlphabetSignature() + if st != nil { + prm.SetToken(st.Marshal()) + } + if err := x.contractClient.PutEACL(prm); err != nil { return err } - if m.cacheEnabled { - id := eaclInfo.Value.GetCID() - m.eacls.InvalidateEACL(id) + if x.cacheEnabled { + x.eacls.InvalidateEACL(eACL.GetCID()) } return nil diff --git a/cmd/neofs-node/netmap.go b/cmd/neofs-node/netmap.go index 302f4dcee2..1eaf5c7286 100644 --- a/cmd/neofs-node/netmap.go +++ b/cmd/neofs-node/netmap.go @@ -8,7 +8,6 @@ import ( netmapV2 "github.com/nspcc-dev/neofs-api-go/v2/netmap" netmapGRPC "github.com/nspcc-dev/neofs-api-go/v2/netmap/grpc" - "github.com/nspcc-dev/neofs-node/pkg/core/netmap" "github.com/nspcc-dev/neofs-node/pkg/metrics" nmClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap" "github.com/nspcc-dev/neofs-node/pkg/morph/event" @@ -17,7 +16,6 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/services/control" netmapService "github.com/nspcc-dev/neofs-node/pkg/services/netmap" netmapSDK "github.com/nspcc-dev/neofs-sdk-go/netmap" - "github.com/nspcc-dev/neofs-sdk-go/version" "go.uber.org/zap" ) @@ -171,24 +169,7 @@ func initNetmapService(c *cfg) { initNetmapState(c) - server := netmapService.New( - netmapService.NewSignService( - &c.key.PrivateKey, - netmapService.NewResponseService( - netmapService.NewExecutionService( - c, - c.apiVersion, - &netInfo{ - netState: c.cfgNetmap.state, - magic: c.cfgMorph.client, - morphClientNetMap: c.cfgNetmap.wrapper, - msPerBlockRdr: c.cfgMorph.client.MsPerBlock, - }, - ), - c.respSvc, - ), - ), - ) + server := netmapService.New(&c.key.PrivateKey, c) for _, srv := range c.cfgGRPC.servers { netmapGRPC.RegisterNetmapServiceServer(srv, server) @@ -416,66 +397,50 @@ func (c *cfg) updateNetMapState(stateSetter func(*nmClient.UpdatePeerPrm)) error return c.cfgNetmap.wrapper.UpdatePeerState(prm) } -type netInfo struct { - netState netmap.State - - magic interface { - MagicNumber() (uint32, error) +func (c *cfg) GetNetworkInfo() (netmapSDK.NetworkInfo, error) { + magic, err := c.cfgMorph.client.MagicNumber() + if err != nil { + return netmapSDK.NetworkInfo{}, err } - morphClientNetMap *nmClient.Client - - msPerBlockRdr func() (int64, error) -} + msPerBlock, err := c.cfgMorph.client.MsPerBlock() + if err != nil { + return netmapSDK.NetworkInfo{}, fmt.Errorf("ms per block: %w", err) + } -func (n *netInfo) Dump(ver version.Version) (*netmapSDK.NetworkInfo, error) { - magic, err := n.magic.MagicNumber() + netInfoMorph, err := c.cfgNetmap.wrapper.ReadNetworkConfiguration() if err != nil { - return nil, err + return netmapSDK.NetworkInfo{}, fmt.Errorf("read network configuration using netmap contract client: %w", err) } var ni netmapSDK.NetworkInfo - ni.SetCurrentEpoch(n.netState.CurrentEpoch()) + ni.SetCurrentEpoch(c.networkState.CurrentEpoch()) ni.SetMagicNumber(uint64(magic)) - - netInfoMorph, err := n.morphClientNetMap.ReadNetworkConfiguration() - if err != nil { - return nil, fmt.Errorf("read network configuration using netmap contract client: %w", err) + ni.SetMsPerBlock(msPerBlock) + ni.SetMaxObjectSize(netInfoMorph.MaxObjectSize) + ni.SetStoragePrice(netInfoMorph.StoragePrice) + ni.SetAuditFee(netInfoMorph.AuditFee) + ni.SetEpochDuration(netInfoMorph.EpochDuration) + ni.SetContainerFee(netInfoMorph.ContainerFee) + ni.SetNamedContainerFee(netInfoMorph.ContainerAliasFee) + ni.SetNumberOfEigenTrustIterations(netInfoMorph.EigenTrustIterations) + ni.SetEigenTrustAlpha(netInfoMorph.EigenTrustAlpha) + ni.SetIRCandidateFee(netInfoMorph.IRCandidateFee) + ni.SetWithdrawalFee(netInfoMorph.WithdrawalFee) + + if netInfoMorph.HomomorphicHashingDisabled { + ni.DisableHomomorphicHashing() } - if mjr := ver.Major(); mjr > 2 || mjr == 2 && ver.Minor() > 9 { - msPerBlock, err := n.msPerBlockRdr() - if err != nil { - return nil, fmt.Errorf("ms per block: %w", err) - } - - ni.SetMsPerBlock(msPerBlock) - - ni.SetMaxObjectSize(netInfoMorph.MaxObjectSize) - ni.SetStoragePrice(netInfoMorph.StoragePrice) - ni.SetAuditFee(netInfoMorph.AuditFee) - ni.SetEpochDuration(netInfoMorph.EpochDuration) - ni.SetContainerFee(netInfoMorph.ContainerFee) - ni.SetNamedContainerFee(netInfoMorph.ContainerAliasFee) - ni.SetNumberOfEigenTrustIterations(netInfoMorph.EigenTrustIterations) - ni.SetEigenTrustAlpha(netInfoMorph.EigenTrustAlpha) - ni.SetIRCandidateFee(netInfoMorph.IRCandidateFee) - ni.SetWithdrawalFee(netInfoMorph.WithdrawalFee) - - if netInfoMorph.HomomorphicHashingDisabled { - ni.DisableHomomorphicHashing() - } - - if netInfoMorph.MaintenanceModeAllowed { - ni.AllowMaintenanceMode() - } + if netInfoMorph.MaintenanceModeAllowed { + ni.AllowMaintenanceMode() + } - for i := range netInfoMorph.Raw { - ni.SetRawNetworkParameter(netInfoMorph.Raw[i].Name, netInfoMorph.Raw[i].Value) - } + for i := range netInfoMorph.Raw { + ni.SetRawNetworkParameter(netInfoMorph.Raw[i].Name, netInfoMorph.Raw[i].Value) } - return &ni, nil + return ni, nil } func (c *cfg) reloadNodeAttributes() error { diff --git a/cmd/neofs-node/reputation.go b/cmd/neofs-node/reputation.go index bd464bf189..07f92a46fa 100644 --- a/cmd/neofs-node/reputation.go +++ b/cmd/neofs-node/reputation.go @@ -4,9 +4,13 @@ import ( "context" "fmt" + apirefs "github.com/nspcc-dev/neofs-api-go/v2/refs" + refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" v2reputation "github.com/nspcc-dev/neofs-api-go/v2/reputation" - v2reputationgrpc "github.com/nspcc-dev/neofs-api-go/v2/reputation/grpc" - "github.com/nspcc-dev/neofs-api-go/v2/session" + protoreputation "github.com/nspcc-dev/neofs-api-go/v2/reputation/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + "github.com/nspcc-dev/neofs-api-go/v2/signature" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" "github.com/nspcc-dev/neofs-node/cmd/neofs-node/reputation/common" intermediatereputation "github.com/nspcc-dev/neofs-node/cmd/neofs-node/reputation/intermediate" localreputation "github.com/nspcc-dev/neofs-node/cmd/neofs-node/reputation/local" @@ -26,8 +30,9 @@ import ( localtrustcontroller "github.com/nspcc-dev/neofs-node/pkg/services/reputation/local/controller" localroutes "github.com/nspcc-dev/neofs-node/pkg/services/reputation/local/routes" truststorage "github.com/nspcc-dev/neofs-node/pkg/services/reputation/local/storage" - reputationrpc "github.com/nspcc-dev/neofs-node/pkg/services/reputation/rpc" + "github.com/nspcc-dev/neofs-node/pkg/services/util" apireputation "github.com/nspcc-dev/neofs-sdk-go/reputation" + "github.com/nspcc-dev/neofs-sdk-go/version" "go.uber.org/zap" ) @@ -193,24 +198,16 @@ func initReputationService(c *cfg) { }, ) - server := reputationrpc.New( - reputationrpc.NewSignService( - &c.key.PrivateKey, - reputationrpc.NewResponseService( - &reputationServer{ - cfg: c, - log: c.log, - localRouter: localTrustRouter, - intermediateRouter: intermediateTrustRouter, - routeBuilder: localRouteBuilder, - }, - c.respSvc, - ), - ), - ) + server := &reputationServer{ + cfg: c, + log: c.log, + localRouter: localTrustRouter, + intermediateRouter: intermediateTrustRouter, + routeBuilder: localRouteBuilder, + } for _, srv := range c.cfgGRPC.servers { - v2reputationgrpc.RegisterReputationServiceServer(srv, server) + protoreputation.RegisterReputationServiceServer(srv, server) } // initialize eigen trust block timer @@ -261,8 +258,34 @@ type reputationServer struct { routeBuilder reputationrouter.Builder } -func (s *reputationServer) AnnounceLocalTrust(ctx context.Context, req *v2reputation.AnnounceLocalTrustRequest) (*v2reputation.AnnounceLocalTrustResponse, error) { - passedRoute := reverseRoute(req.GetVerificationHeader()) +func (s *reputationServer) makeResponseMetaHeader(st *protostatus.Status) *protosession.ResponseMetaHeader { + v := version.Current() + var v2 apirefs.Version + v.WriteToV2(&v2) + return &protosession.ResponseMetaHeader{ + Version: v2.ToGRPCMessage().(*refs.Version), + Epoch: s.networkState.CurrentEpoch(), + Status: st, + } +} + +func (s *reputationServer) makeLocalResponse(err error) (*protoreputation.AnnounceLocalTrustResponse, error) { + resp := &protoreputation.AnnounceLocalTrustResponse{ + MetaHeader: s.makeResponseMetaHeader(util.ToStatus(err)), + } + return util.SignResponse(&s.key.PrivateKey, resp, v2reputation.AnnounceLocalTrustResponse{}), nil +} + +func (s *reputationServer) AnnounceLocalTrust(ctx context.Context, req *protoreputation.AnnounceLocalTrustRequest) (*protoreputation.AnnounceLocalTrustResponse, error) { + req2 := new(v2reputation.AnnounceLocalTrustRequest) + if err := req2.FromGRPCMessage(req); err != nil { + return nil, err + } + if err := signature.VerifyServiceMessage(req2); err != nil { + return s.makeLocalResponse(util.ToRequestSignatureVerificationError(err)) + } + + passedRoute := reverseRoute(req.GetVerifyHeader()) passedRoute = append(passedRoute, s) body := req.GetBody() @@ -274,24 +297,36 @@ func (s *reputationServer) AnnounceLocalTrust(ctx context.Context, req *v2reputa w, err := s.localRouter.InitWriter(reputationrouter.NewRouteContext(eCtx, passedRoute)) if err != nil { - return nil, fmt.Errorf("could not initialize local trust writer: %w", err) + return s.makeLocalResponse(fmt.Errorf("could not initialize local trust writer: %w", err)) } for _, trust := range body.GetTrusts() { - err = s.processLocalTrust(body.GetEpoch(), apiToLocalTrust(&trust, passedRoute[0].PublicKey()), passedRoute, w) + err = s.processLocalTrust(body.GetEpoch(), apiToLocalTrust(trust, passedRoute[0].PublicKey()), passedRoute, w) if err != nil { - return nil, fmt.Errorf("could not write one of local trusts: %w", err) + return s.makeLocalResponse(fmt.Errorf("could not write one of local trusts: %w", err)) } } - resp := new(v2reputation.AnnounceLocalTrustResponse) - resp.SetBody(new(v2reputation.AnnounceLocalTrustResponseBody)) + return s.makeLocalResponse(util.StatusOKErr) +} - return resp, nil +func (s *reputationServer) makeIntermediateResponse(err error) (*protoreputation.AnnounceIntermediateResultResponse, error) { + resp := &protoreputation.AnnounceIntermediateResultResponse{ + MetaHeader: s.makeResponseMetaHeader(util.ToStatus(err)), + } + return util.SignResponse(&s.key.PrivateKey, resp, v2reputation.AnnounceIntermediateResultResponse{}), nil } -func (s *reputationServer) AnnounceIntermediateResult(ctx context.Context, req *v2reputation.AnnounceIntermediateResultRequest) (*v2reputation.AnnounceIntermediateResultResponse, error) { - passedRoute := reverseRoute(req.GetVerificationHeader()) +func (s *reputationServer) AnnounceIntermediateResult(ctx context.Context, req *protoreputation.AnnounceIntermediateResultRequest) (*protoreputation.AnnounceIntermediateResultResponse, error) { + req2 := new(v2reputation.AnnounceIntermediateResultRequest) + if err := req2.FromGRPCMessage(req); err != nil { + return nil, err + } + if err := signature.VerifyServiceMessage(req2); err != nil { + return s.makeIntermediateResponse(util.ToRequestSignatureVerificationError(err)) + } + + passedRoute := reverseRoute(req.GetVerifyHeader()) passedRoute = append(passedRoute, s) body := req.GetBody() @@ -300,7 +335,7 @@ func (s *reputationServer) AnnounceIntermediateResult(ctx context.Context, req * w, err := s.intermediateRouter.InitWriter(reputationrouter.NewRouteContext(eiCtx, passedRoute)) if err != nil { - return nil, fmt.Errorf("could not initialize trust writer: %w", err) + return s.makeIntermediateResponse(fmt.Errorf("could not initialize trust writer: %w", err)) } v2Trust := body.GetTrust() @@ -309,13 +344,10 @@ func (s *reputationServer) AnnounceIntermediateResult(ctx context.Context, req * err = w.Write(trust) if err != nil { - return nil, fmt.Errorf("could not write trust: %w", err) + return s.makeIntermediateResponse(fmt.Errorf("could not write trust: %w", err)) } - resp := new(v2reputation.AnnounceIntermediateResultResponse) - resp.SetBody(new(v2reputation.AnnounceIntermediateResultResponseBody)) - - return resp, nil + return s.makeIntermediateResponse(util.StatusOKErr) } func (s *reputationServer) processLocalTrust(epoch uint64, t reputation.Trust, @@ -328,8 +360,9 @@ func (s *reputationServer) processLocalTrust(epoch uint64, t reputation.Trust, return w.Write(t) } -// apiToLocalTrust converts v2 Trust to local reputation.Trust, adding trustingPeer. -func apiToLocalTrust(t *v2reputation.Trust, trustingPeer []byte) reputation.Trust { +// apiToLocalTrust converts protoreputation.Trust to local reputation.Trust, +// adding trustingPeer. +func apiToLocalTrust(t *protoreputation.Trust, trustingPeer []byte) reputation.Trust { var trusted, trusting apireputation.PeerID trusted.SetPublicKey(t.GetPeer().GetPublicKey()) trusting.SetPublicKey(trustingPeer) @@ -343,7 +376,7 @@ func apiToLocalTrust(t *v2reputation.Trust, trustingPeer []byte) reputation.Trus return localTrust } -func reverseRoute(hdr *session.RequestVerificationHeader) (passedRoute []reputationcommon.ServerInfo) { +func reverseRoute(hdr *protosession.RequestVerificationHeader) (passedRoute []reputationcommon.ServerInfo) { for hdr != nil { passedRoute = append(passedRoute, &common.OnlyKeyRemoteServerInfo{ Key: hdr.GetBodySignature().GetKey(), diff --git a/cmd/neofs-node/session.go b/cmd/neofs-node/session.go index b6d22f098a..21802c68a1 100644 --- a/cmd/neofs-node/session.go +++ b/cmd/neofs-node/session.go @@ -1,11 +1,9 @@ package main import ( - "context" "fmt" "time" - "github.com/nspcc-dev/neofs-api-go/v2/session" sessionGRPC "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" nodeconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/node" "github.com/nspcc-dev/neofs-node/pkg/morph/event" @@ -18,7 +16,7 @@ import ( ) type sessionStorage interface { - Create(ctx context.Context, body *session.CreateRequestBody) (*session.CreateResponseBody, error) + sessionSvc.KeyStorage Get(ownerID user.ID, tokenID []byte) *storage.PrivateToken RemoveOld(epoch uint64) @@ -49,15 +47,7 @@ func initSessionService(c *cfg) { c.privateTokenStore.RemoveOld(ev.(netmap.NewEpoch).EpochNumber()) }) - server := sessionSvc.New( - sessionSvc.NewSignService( - &c.key.PrivateKey, - sessionSvc.NewResponseService( - sessionSvc.NewExecutionService(c.privateTokenStore, c.log), - c.respSvc, - ), - ), - ) + server := sessionSvc.New(&c.key.PrivateKey, c, c.privateTokenStore) for _, srv := range c.cfgGRPC.servers { sessionGRPC.RegisterSessionServiceServer(srv, server) diff --git a/pkg/core/container/delete.go b/pkg/core/container/delete.go deleted file mode 100644 index 592509a8b5..0000000000 --- a/pkg/core/container/delete.go +++ /dev/null @@ -1,50 +0,0 @@ -package container - -import ( - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - "github.com/nspcc-dev/neofs-sdk-go/session" -) - -// RemovalWitness groups the information required -// to prove and verify the removal of a container. -type RemovalWitness struct { - cnr cid.ID - - sig []byte - - token *session.Container -} - -// ContainerID returns the identifier of the container -// to be removed. -func (x RemovalWitness) ContainerID() cid.ID { - return x.cnr -} - -// SetContainerID sets the identifier of the container -// to be removed. -func (x *RemovalWitness) SetContainerID(id cid.ID) { - x.cnr = id -} - -// Signature returns the signature of the container identifier. -func (x RemovalWitness) Signature() []byte { - return x.sig -} - -// SetSignature sets a signature of the container identifier. -func (x *RemovalWitness) SetSignature(sig []byte) { - x.sig = sig -} - -// SessionToken returns the token of the session within -// which the container was removed. -func (x RemovalWitness) SessionToken() *session.Container { - return x.token -} - -// SetSessionToken sets the token of the session within -// which the container was removed. -func (x *RemovalWitness) SetSessionToken(tok *session.Container) { - x.token = tok -} diff --git a/pkg/morph/client/container/delete.go b/pkg/morph/client/container/delete.go index a9bc7c682a..049610b589 100644 --- a/pkg/morph/client/container/delete.go +++ b/pkg/morph/client/container/delete.go @@ -3,31 +3,9 @@ package container import ( "fmt" - core "github.com/nspcc-dev/neofs-node/pkg/core/container" "github.com/nspcc-dev/neofs-node/pkg/morph/client" ) -// Delete marshals container ID, and passes it to Wrapper's Delete method -// along with signature and session token. -// -// Returns error if container ID is nil. -func Delete(c *Client, witness core.RemovalWitness) error { - id := witness.ContainerID() - binCnr := id[:] - - var prm DeletePrm - - prm.SetCID(binCnr) - prm.SetSignature(witness.Signature()) - prm.RequireAlphabetSignature() - - if tok := witness.SessionToken(); tok != nil { - prm.SetToken(tok.Marshal()) - } - - return c.Delete(prm) -} - // DeletePrm groups parameters of Delete client operation. type DeletePrm struct { cnr []byte diff --git a/pkg/morph/client/container/eacl_set.go b/pkg/morph/client/container/eacl_set.go index afb086d2d6..b82293c631 100644 --- a/pkg/morph/client/container/eacl_set.go +++ b/pkg/morph/client/container/eacl_set.go @@ -3,33 +3,9 @@ package container import ( "fmt" - containercore "github.com/nspcc-dev/neofs-node/pkg/core/container" "github.com/nspcc-dev/neofs-node/pkg/morph/client" ) -// PutEACL marshals table, and passes it to Wrapper's PutEACLBinary method -// along with sig.Key() and sig.Sign(). -// -// Returns error if table is nil. -func PutEACL(c *Client, eaclInfo containercore.EACL) error { - if eaclInfo.Value == nil { - return errNilArgument - } - - var prm PutEACLPrm - prm.SetTable(eaclInfo.Value.Marshal()) - - if eaclInfo.Session != nil { - prm.SetToken(eaclInfo.Session.Marshal()) - } - - prm.SetKey(eaclInfo.Signature.PublicKeyBytes()) - prm.SetSignature(eaclInfo.Signature.Value()) - prm.RequireAlphabetSignature() - - return c.PutEACL(prm) -} - // PutEACLPrm groups parameters of PutEACL operation. type PutEACLPrm struct { table []byte diff --git a/pkg/morph/client/container/put.go b/pkg/morph/client/container/put.go index fa8205ed1c..4a0221f69f 100644 --- a/pkg/morph/client/container/put.go +++ b/pkg/morph/client/container/put.go @@ -3,47 +3,9 @@ package container import ( "fmt" - containercore "github.com/nspcc-dev/neofs-node/pkg/core/container" "github.com/nspcc-dev/neofs-node/pkg/morph/client" - containerSDK "github.com/nspcc-dev/neofs-sdk-go/container" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" ) -// Put marshals container, and passes it to Wrapper's Put method -// along with sig.Key() and sig.Sign(). -// -// Returns error if container is nil. -func Put(c *Client, cnr containercore.Container) (*cid.ID, error) { - data := cnr.Value.Marshal() - - d := cnr.Value.ReadDomain() - - var prm PutPrm - prm.SetContainer(data) - prm.SetName(d.Name()) - prm.SetZone(d.Zone()) - switch metaAttribute(cnr.Value) { - case "optimistic", "strict": - prm.EnableMeta() - } - - if cnr.Session != nil { - prm.SetToken(cnr.Session.Marshal()) - } - - prm.SetKey(cnr.Signature.PublicKeyBytes()) - prm.SetSignature(cnr.Signature.Value()) - - err := c.Put(prm) - if err != nil { - return nil, err - } - - id := cid.NewFromMarshalledContainer(data) - - return &id, nil -} - // PutPrm groups parameters of Put operation. type PutPrm struct { cnr []byte @@ -118,7 +80,3 @@ func (c *Client) Put(p PutPrm) error { } return nil } - -func metaAttribute(cnr containerSDK.Container) string { - return cnr.Attribute("__NEOFS__METAINFO_CONSISTENCY") -} diff --git a/pkg/services/accounting/executor.go b/pkg/services/accounting/executor.go deleted file mode 100644 index 4a2c14ccbd..0000000000 --- a/pkg/services/accounting/executor.go +++ /dev/null @@ -1,35 +0,0 @@ -package accounting - -import ( - "context" - "fmt" - - "github.com/nspcc-dev/neofs-api-go/v2/accounting" -) - -type ServiceExecutor interface { - Balance(context.Context, *accounting.BalanceRequestBody) (*accounting.BalanceResponseBody, error) -} - -type executorSvc struct { - exec ServiceExecutor -} - -// NewExecutionService wraps ServiceExecutor and returns Accounting Service interface. -func NewExecutionService(exec ServiceExecutor) Server { - return &executorSvc{ - exec: exec, - } -} - -func (s *executorSvc) Balance(ctx context.Context, req *accounting.BalanceRequest) (*accounting.BalanceResponse, error) { - respBody, err := s.exec.Balance(ctx, req.GetBody()) - if err != nil { - return nil, fmt.Errorf("could not execute Balance request: %w", err) - } - - resp := new(accounting.BalanceResponse) - resp.SetBody(respBody) - - return resp, nil -} diff --git a/pkg/services/accounting/morph/executor.go b/pkg/services/accounting/morph/executor.go deleted file mode 100644 index 8fa0b3a359..0000000000 --- a/pkg/services/accounting/morph/executor.go +++ /dev/null @@ -1,55 +0,0 @@ -package accounting - -import ( - "context" - "errors" - "fmt" - - "github.com/nspcc-dev/neofs-api-go/v2/accounting" - "github.com/nspcc-dev/neofs-node/pkg/morph/client/balance" - accountingSvc "github.com/nspcc-dev/neofs-node/pkg/services/accounting" - "github.com/nspcc-dev/neofs-sdk-go/user" -) - -type morphExecutor struct { - client *balance.Client -} - -func NewExecutor(client *balance.Client) accountingSvc.ServiceExecutor { - return &morphExecutor{ - client: client, - } -} - -func (s *morphExecutor) Balance(_ context.Context, body *accounting.BalanceRequestBody) (*accounting.BalanceResponseBody, error) { - idV2 := body.GetOwnerID() - if idV2 == nil { - return nil, errors.New("missing account") - } - - var id user.ID - - err := id.ReadFromV2(*idV2) - if err != nil { - return nil, fmt.Errorf("invalid account: %w", err) - } - - amount, err := s.client.BalanceOf(id) - if err != nil { - return nil, err - } - - balancePrecision, err := s.client.Decimals() - if err != nil { - return nil, err - } - - dec := new(accounting.Decimal) - dec.SetValue(amount.Int64()) - dec.SetPrecision(balancePrecision) - - res := new(accounting.BalanceResponseBody) - res.SetBalance(dec) - - return res, nil -} diff --git a/pkg/services/accounting/response.go b/pkg/services/accounting/response.go deleted file mode 100644 index 2b05c40d87..0000000000 --- a/pkg/services/accounting/response.go +++ /dev/null @@ -1,37 +0,0 @@ -package accounting - -import ( - "context" - - "github.com/nspcc-dev/neofs-api-go/v2/accounting" - "github.com/nspcc-dev/neofs-node/pkg/services/util" - "github.com/nspcc-dev/neofs-node/pkg/services/util/response" -) - -type responseService struct { - respSvc *response.Service - - svc Server -} - -// NewResponseService returns accounting service instance that passes internal service -// call to response service. -func NewResponseService(accSvc Server, respSvc *response.Service) Server { - return &responseService{ - respSvc: respSvc, - svc: accSvc, - } -} - -func (s *responseService) Balance(ctx context.Context, req *accounting.BalanceRequest) (*accounting.BalanceResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Balance(ctx, req.(*accounting.BalanceRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*accounting.BalanceResponse), nil -} diff --git a/pkg/services/accounting/server.go b/pkg/services/accounting/server.go index e356b21474..ce49ca932d 100644 --- a/pkg/services/accounting/server.go +++ b/pkg/services/accounting/server.go @@ -2,39 +2,107 @@ package accounting import ( "context" + "crypto/ecdsa" + "errors" + "fmt" + "math/big" - "github.com/nspcc-dev/neofs-api-go/v2/accounting" + apiaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting" protoaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc" + apirefs "github.com/nspcc-dev/neofs-api-go/v2/refs" + refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + "github.com/nspcc-dev/neofs-api-go/v2/signature" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" + "github.com/nspcc-dev/neofs-node/pkg/core/netmap" + "github.com/nspcc-dev/neofs-node/pkg/services/util" + "github.com/nspcc-dev/neofs-sdk-go/user" + "github.com/nspcc-dev/neofs-sdk-go/version" ) -// Server is an interface of the NeoFS API Accounting service server. -type Server interface { - Balance(context.Context, *accounting.BalanceRequest) (*accounting.BalanceResponse, error) +// BalanceContract groups ops of the Balance contract deployed in the FS chain +// required to serve NeoFS API Accounting service. +type BalanceContract interface { + // Decimals returns the number of decimals used by NeoFS tokens. + Decimals() (uint32, error) + // BalanceOf returns current balance of the referenced user in NeoFS tokens. + BalanceOf(user.ID) (*big.Int, error) } type server struct { protoaccounting.UnimplementedAccountingServiceServer - srv Server + signer *ecdsa.PrivateKey + net netmap.State + contract BalanceContract } -// New returns protoaccounting.AccountingServiceServer based on the Server. -func New(c Server) protoaccounting.AccountingServiceServer { +// New provides protoaccounting.AccountingServiceServer based on specified +// [BalanceContract]. +// +// All response messages are signed using specified signer and have current +// epoch in the meta header. +func New(s *ecdsa.PrivateKey, net netmap.State, c BalanceContract) protoaccounting.AccountingServiceServer { return &server{ - srv: c, + signer: s, + net: net, + contract: c, } } -// Balance converts gRPC BalanceRequest message and passes it to internal Accounting service. -func (s *server) Balance(ctx context.Context, req *protoaccounting.BalanceRequest) (*protoaccounting.BalanceResponse, error) { - balReq := new(accounting.BalanceRequest) +func (s *server) makeBalanceResponse(body *protoaccounting.BalanceResponse_Body, st *protostatus.Status) (*protoaccounting.BalanceResponse, error) { + v := version.Current() + var v2 apirefs.Version + v.WriteToV2(&v2) + resp := &protoaccounting.BalanceResponse{ + Body: body, + MetaHeader: &protosession.ResponseMetaHeader{ + Version: v2.ToGRPCMessage().(*refs.Version), + Epoch: s.net.CurrentEpoch(), + Status: st, + }, + } + return util.SignResponse(s.signer, resp, apiaccounting.BalanceResponse{}), nil +} + +func (s *server) makeFailedBalanceResponse(err error) (*protoaccounting.BalanceResponse, error) { + return s.makeBalanceResponse(nil, util.ToStatus(err)) +} + +// Balance gets current balance of the requested user using underlying +// [BalanceContract] and returns result in the response. +func (s *server) Balance(_ context.Context, req *protoaccounting.BalanceRequest) (*protoaccounting.BalanceResponse, error) { + balReq := new(apiaccounting.BalanceRequest) if err := balReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(balReq); err != nil { + return s.makeFailedBalanceResponse(util.ToRequestSignatureVerificationError(err)) + } + + mUsr := req.GetBody().GetOwnerId() + if mUsr == nil { + return s.makeFailedBalanceResponse(errors.New("missing account")) + } + var id2 apirefs.OwnerID + if err := id2.FromGRPCMessage(mUsr); err != nil { + panic(err) + } + var id user.ID + if err := id.ReadFromV2(id2); err != nil { + return s.makeFailedBalanceResponse(fmt.Errorf("invalid account: %w", err)) + } - resp, err := s.srv.Balance(ctx, balReq) + bal, err := s.contract.BalanceOf(id) if err != nil { - return nil, err + return s.makeFailedBalanceResponse(err) + } + ds, err := s.contract.Decimals() + if err != nil { + return s.makeFailedBalanceResponse(err) } - return resp.ToGRPCMessage().(*protoaccounting.BalanceResponse), nil + body := &protoaccounting.BalanceResponse_Body{ + Balance: &protoaccounting.Decimal{Value: bal.Int64(), Precision: ds}, + } + return s.makeBalanceResponse(body, util.StatusOK) } diff --git a/pkg/services/accounting/sign.go b/pkg/services/accounting/sign.go deleted file mode 100644 index 1379727084..0000000000 --- a/pkg/services/accounting/sign.go +++ /dev/null @@ -1,38 +0,0 @@ -package accounting - -import ( - "context" - "crypto/ecdsa" - - "github.com/nspcc-dev/neofs-api-go/v2/accounting" - "github.com/nspcc-dev/neofs-node/pkg/services/util" -) - -type signService struct { - sigSvc *util.SignService - - svc Server -} - -func NewSignService(key *ecdsa.PrivateKey, svc Server) Server { - return &signService{ - sigSvc: util.NewUnarySignService(key), - svc: svc, - } -} - -func (s *signService) Balance(ctx context.Context, req *accounting.BalanceRequest) (*accounting.BalanceResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Balance(ctx, req.(*accounting.BalanceRequest)) - }, - func() util.ResponseMessage { - return new(accounting.BalanceResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*accounting.BalanceResponse), nil -} diff --git a/pkg/services/container/executor.go b/pkg/services/container/executor.go deleted file mode 100644 index 3fffadb95e..0000000000 --- a/pkg/services/container/executor.go +++ /dev/null @@ -1,118 +0,0 @@ -package container - -import ( - "context" - "fmt" - - "github.com/nspcc-dev/neofs-api-go/v2/container" - "github.com/nspcc-dev/neofs-api-go/v2/session" -) - -type ServiceExecutor interface { - Put(context.Context, *session.Token, *container.PutRequestBody) (*container.PutResponseBody, error) - Delete(context.Context, *session.Token, *container.DeleteRequestBody) (*container.DeleteResponseBody, error) - Get(context.Context, *container.GetRequestBody) (*container.GetResponseBody, error) - List(context.Context, *container.ListRequestBody) (*container.ListResponseBody, error) - SetExtendedACL(context.Context, *session.Token, *container.SetExtendedACLRequestBody) (*container.SetExtendedACLResponseBody, error) - GetExtendedACL(context.Context, *container.GetExtendedACLRequestBody) (*container.GetExtendedACLResponseBody, error) -} - -type executorSvc struct { - Server - - exec ServiceExecutor -} - -// NewExecutionService wraps ServiceExecutor and returns Container Service interface. -func NewExecutionService(exec ServiceExecutor) Server { - return &executorSvc{ - exec: exec, - } -} - -func (s *executorSvc) Put(ctx context.Context, req *container.PutRequest) (*container.PutResponse, error) { - meta := req.GetMetaHeader() - for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() { - meta = origin - } - - respBody, err := s.exec.Put(ctx, meta.GetSessionToken(), req.GetBody()) - if err != nil { - return nil, fmt.Errorf("could not execute Put request: %w", err) - } - - resp := new(container.PutResponse) - resp.SetBody(respBody) - - return resp, nil -} - -func (s *executorSvc) Delete(ctx context.Context, req *container.DeleteRequest) (*container.DeleteResponse, error) { - meta := req.GetMetaHeader() - for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() { - meta = origin - } - - respBody, err := s.exec.Delete(ctx, meta.GetSessionToken(), req.GetBody()) - if err != nil { - return nil, fmt.Errorf("could not execute Delete request: %w", err) - } - - resp := new(container.DeleteResponse) - resp.SetBody(respBody) - - return resp, nil -} - -func (s *executorSvc) Get(ctx context.Context, req *container.GetRequest) (*container.GetResponse, error) { - respBody, err := s.exec.Get(ctx, req.GetBody()) - if err != nil { - return nil, fmt.Errorf("could not execute Get request: %w", err) - } - - resp := new(container.GetResponse) - resp.SetBody(respBody) - - return resp, nil -} - -func (s *executorSvc) List(ctx context.Context, req *container.ListRequest) (*container.ListResponse, error) { - respBody, err := s.exec.List(ctx, req.GetBody()) - if err != nil { - return nil, fmt.Errorf("could not execute List request: %w", err) - } - - resp := new(container.ListResponse) - resp.SetBody(respBody) - - return resp, nil -} - -func (s *executorSvc) SetExtendedACL(ctx context.Context, req *container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) { - meta := req.GetMetaHeader() - for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() { - meta = origin - } - - respBody, err := s.exec.SetExtendedACL(ctx, meta.GetSessionToken(), req.GetBody()) - if err != nil { - return nil, fmt.Errorf("could not execute SetEACL request: %w", err) - } - - resp := new(container.SetExtendedACLResponse) - resp.SetBody(respBody) - - return resp, nil -} - -func (s *executorSvc) GetExtendedACL(ctx context.Context, req *container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) { - respBody, err := s.exec.GetExtendedACL(ctx, req.GetBody()) - if err != nil { - return nil, fmt.Errorf("could not execute GetEACL request: %w", err) - } - - resp := new(container.GetExtendedACLResponse) - resp.SetBody(respBody) - - return resp, nil -} diff --git a/pkg/services/container/morph/executor.go b/pkg/services/container/morph/executor.go deleted file mode 100644 index ab16715d59..0000000000 --- a/pkg/services/container/morph/executor.go +++ /dev/null @@ -1,410 +0,0 @@ -package container - -import ( - "bytes" - "context" - "crypto/ecdsa" - "errors" - "fmt" - - "github.com/mr-tron/base58" - "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" - "github.com/nspcc-dev/neofs-api-go/v2/util/signature" - containercore "github.com/nspcc-dev/neofs-node/pkg/core/container" - "github.com/nspcc-dev/neofs-node/pkg/core/netmap" - containerSvc "github.com/nspcc-dev/neofs-node/pkg/services/container" - apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" - eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" - "github.com/nspcc-dev/neofs-sdk-go/session" - "github.com/nspcc-dev/neofs-sdk-go/user" -) - -type morphExecutor struct { - rdr Reader - wrt Writer - epocher netmap.State -} - -// Reader is an interface of read-only container storage. -type Reader interface { - containercore.Source - containercore.EACLSource - - // List returns a list of container identifiers belonging - // to the specified user of NeoFS system. Returns the identifiers - // of all NeoFS containers if pointer to owner identifier is nil. - List(*user.ID) ([]cid.ID, error) -} - -// Writer is an interface of container storage updater. -type Writer interface { - // Put stores specified container in FS chain. - Put(containercore.Container) (*cid.ID, error) - // Delete removes specified container from FS chain. - Delete(containercore.RemovalWitness) error - // PutEACL updates extended ACL table of specified container in FS chain. - PutEACL(containercore.EACL) error -} - -func NewExecutor(rdr Reader, wrt Writer, epocher netmap.State) containerSvc.ServiceExecutor { - return &morphExecutor{ - rdr: rdr, - wrt: wrt, - epocher: epocher, - } -} - -func (s *morphExecutor) Put(_ context.Context, tokV2 *sessionV2.Token, body *container.PutRequestBody) (*container.PutResponseBody, error) { - sigV2 := body.GetSignature() - if sigV2 == nil { - // TODO(@cthulhu-rider): #1387 use "const" error - return nil, errors.New("missing signature") - } - - cnrV2 := body.GetContainer() - if cnrV2 == nil { - return nil, errors.New("missing container field") - } - - var cnr containercore.Container - - err := cnr.Value.ReadFromV2(*cnrV2) - if err != nil { - return nil, fmt.Errorf("invalid container: %w", err) - } - - err = cnr.Signature.ReadFromV2(*sigV2) - if err != nil { - return nil, fmt.Errorf("can't read signature: %w", err) - } - - if tokV2 != nil { - cnr.Session = new(session.Container) - - err := cnr.Session.ReadFromV2(*tokV2) - if err != nil { - return nil, fmt.Errorf("invalid session token: %w", err) - } - - err = s.validateToken(tokV2, nil, sessionV2.ContainerVerbPut) - if err != nil { - return nil, fmt.Errorf("session token validation: %w", err) - } - } - - idCnr, err := s.wrt.Put(cnr) - if err != nil { - return nil, err - } - - var idCnrV2 refs.ContainerID - idCnr.WriteToV2(&idCnrV2) - - res := new(container.PutResponseBody) - res.SetContainerID(&idCnrV2) - - return res, nil -} - -func (s *morphExecutor) Delete(_ context.Context, tokV2 *sessionV2.Token, body *container.DeleteRequestBody) (*container.DeleteResponseBody, error) { - idV2 := body.GetContainerID() - if idV2 == nil { - return nil, errors.New("missing container ID") - } - - var id cid.ID - - err := id.ReadFromV2(*idV2) - if err != nil { - return nil, fmt.Errorf("invalid container ID: %w", err) - } - - sig := body.GetSignature().GetSign() - - var tok *session.Container - - if tokV2 != nil { - tok = new(session.Container) - - err := tok.ReadFromV2(*tokV2) - if err != nil { - return nil, fmt.Errorf("invalid session token: %w", err) - } - - err = s.validateToken(tokV2, body.GetContainerID(), sessionV2.ContainerVerbDelete) - if err != nil { - return nil, fmt.Errorf("session token validation: %w", err) - } - } - - var rmWitness containercore.RemovalWitness - - rmWitness.SetContainerID(id) - rmWitness.SetSignature(sig) - rmWitness.SetSessionToken(tok) - - err = s.wrt.Delete(rmWitness) - if err != nil { - return nil, err - } - - return new(container.DeleteResponseBody), nil -} - -func (s *morphExecutor) Get(_ context.Context, body *container.GetRequestBody) (*container.GetResponseBody, error) { - idV2 := body.GetContainerID() - if idV2 == nil { - return nil, errors.New("missing container ID") - } - - var id cid.ID - - err := id.ReadFromV2(*idV2) - if err != nil { - return nil, fmt.Errorf("invalid container ID: %w", err) - } - - cnr, err := s.rdr.Get(id) - if err != nil { - return nil, err - } - - sigV2 := new(refs.Signature) - cnr.Signature.WriteToV2(sigV2) - - var tokV2 *sessionV2.Token - - if cnr.Session != nil { - tokV2 = new(sessionV2.Token) - - cnr.Session.WriteToV2(tokV2) - } - - var cnrV2 container.Container - cnr.Value.WriteToV2(&cnrV2) - - res := new(container.GetResponseBody) - res.SetContainer(&cnrV2) - res.SetSignature(sigV2) - res.SetSessionToken(tokV2) - - return res, nil -} - -func (s *morphExecutor) List(_ context.Context, body *container.ListRequestBody) (*container.ListResponseBody, error) { - idV2 := body.GetOwnerID() - if idV2 == nil { - return nil, fmt.Errorf("missing user ID") - } - - var id user.ID - - err := id.ReadFromV2(*idV2) - if err != nil { - return nil, fmt.Errorf("invalid user ID: %w", err) - } - - cnrs, err := s.rdr.List(&id) - if err != nil { - return nil, err - } - - cidList := make([]refs.ContainerID, len(cnrs)) - for i := range cnrs { - cnrs[i].WriteToV2(&cidList[i]) - } - - res := new(container.ListResponseBody) - res.SetContainerIDs(cidList) - - return res, nil -} - -func (s *morphExecutor) SetExtendedACL(_ context.Context, tokV2 *sessionV2.Token, body *container.SetExtendedACLRequestBody) (*container.SetExtendedACLResponseBody, error) { - sigV2 := body.GetSignature() - if sigV2 == nil { - // TODO(@cthulhu-rider): #1387 use "const" error - return nil, errors.New("missing signature") - } - - var table eaclSDK.Table - if eacl := body.GetEACL(); eacl != nil { - if err := table.ReadFromV2(*body.GetEACL()); err != nil { - return nil, fmt.Errorf("invalid eACL: %w", err) - } - } - - eaclInfo := containercore.EACL{ - Value: &table, - } - - err := eaclInfo.Signature.ReadFromV2(*sigV2) - if err != nil { - return nil, fmt.Errorf("can't read signature: %w", err) - } - - if tokV2 != nil { - eaclInfo.Session = new(session.Container) - - err := eaclInfo.Session.ReadFromV2(*tokV2) - if err != nil { - return nil, fmt.Errorf("invalid session token: %w", err) - } - - err = s.validateToken(tokV2, body.GetEACL().GetContainerID(), sessionV2.ContainerVerbSetEACL) - if err != nil { - return nil, fmt.Errorf("session token validation: %w", err) - } - } - - err = s.wrt.PutEACL(eaclInfo) - if err != nil { - return nil, err - } - - return new(container.SetExtendedACLResponseBody), nil -} - -func (s *morphExecutor) GetExtendedACL(_ context.Context, body *container.GetExtendedACLRequestBody) (*container.GetExtendedACLResponseBody, error) { - idV2 := body.GetContainerID() - if idV2 == nil { - return nil, errors.New("missing container ID") - } - - var id cid.ID - - err := id.ReadFromV2(*idV2) - if err != nil { - return nil, fmt.Errorf("invalid container ID: %w", err) - } - - eaclInfo, err := s.rdr.GetEACL(id) - if err != nil { - return nil, err - } - - var sigV2 refs.Signature - eaclInfo.Signature.WriteToV2(&sigV2) - - var tokV2 *sessionV2.Token - - if eaclInfo.Session != nil { - tokV2 = new(sessionV2.Token) - - eaclInfo.Session.WriteToV2(tokV2) - } - - res := new(container.GetExtendedACLResponseBody) - res.SetEACL(eaclInfo.Value.ToV2()) - res.SetSignature(&sigV2) - res.SetSessionToken(tokV2) - - return res, nil -} - -type sessionDataSource struct { - t *sessionV2.Token - size int -} - -func (d sessionDataSource) ReadSignedData(buff []byte) ([]byte, error) { - if len(buff) < d.size { - buff = make([]byte, d.size) - } - - res := d.t.GetBody().StableMarshal(buff) - - return res[:d.size], nil -} - -func (d sessionDataSource) SignedDataSize() int { - return d.size -} - -func newDataSource(t *sessionV2.Token) sessionDataSource { - return sessionDataSource{ - t: t, - size: t.GetBody().StableSize(), - } -} - -func (s *morphExecutor) validateToken(t *sessionV2.Token, cIDV2 *refs.ContainerID, op sessionV2.ContainerSessionVerb) error { - c := t.GetBody().GetContext() - cc, ok := c.(*sessionV2.ContainerSessionContext) - if !ok { - return errors.New("session is not container-related") - } - - if verb := cc.Verb(); verb != op { - return fmt.Errorf("wrong container session operation: %s", verb) - } - - err := signature.VerifyDataWithSource(newDataSource(t), t.GetSignature) - if err != nil { - return fmt.Errorf("incorrect token signature: %w", err) - } - - lt := t.GetBody().GetLifetime() - if lt == nil { - return errors.New("lifetime not set") - } - - currEpoch := s.epocher.CurrentEpoch() - exp := lt.GetExp() - iat := lt.GetIat() - nbf := lt.GetNbf() - - switch { - case exp < currEpoch: - return apistatus.SessionTokenExpired{} - case iat > currEpoch: - return fmt.Errorf("token should not be issued yet: IAt: %d, current epoch: %d", iat, currEpoch) - case nbf > currEpoch: - return fmt.Errorf("token is not valid yet: NBf: %d, current epoch: %d", nbf, currEpoch) - } - - if cIDV2 == nil { // can be nil for PUT - return nil - } - - var cIDRequested cid.ID - - err = cIDRequested.ReadFromV2(*cIDV2) - if err != nil { - return fmt.Errorf("invalid container ID: %w", err) - } - - cnr, err := s.rdr.Get(cIDRequested) - if err != nil { - return fmt.Errorf("reading container from the network: %w", err) - } - - owner := cnr.Value.Owner() - if issuer := t.GetBody().GetOwnerID().GetValue(); !bytes.Equal(owner[:], issuer) { - return fmt.Errorf("session was not issued by the container owner, issuer: %s", base58.Encode(issuer)) - } - - var keyFromToken neofsecdsa.PublicKey - - err = keyFromToken.Decode(t.GetSignature().GetKey()) - if err != nil { - return fmt.Errorf("decoding key from signature: %w", err) - } - - userFromToken := user.NewFromECDSAPublicKey(ecdsa.PublicKey(keyFromToken)) - if cnr.Value.Owner() != userFromToken { - return fmt.Errorf("session token signer differs container owner: signer: %s, owner: %s", userFromToken, cnr.Value.Owner()) - } - - if !cc.Wildcard() { - if sessionCID := cc.ContainerID().GetValue(); !bytes.Equal(sessionCID, cIDV2.GetValue()) { - return fmt.Errorf("wrong container: %s", base58.Encode(sessionCID)) - } - } - - return nil -} diff --git a/pkg/services/container/morph/executor_test.go b/pkg/services/container/morph/executor_test.go deleted file mode 100644 index f87667e38a..0000000000 --- a/pkg/services/container/morph/executor_test.go +++ /dev/null @@ -1,394 +0,0 @@ -package container_test - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha256" - "testing" - - "github.com/google/uuid" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "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" - containerCore "github.com/nspcc-dev/neofs-node/pkg/core/container" - containerSvc "github.com/nspcc-dev/neofs-node/pkg/services/container" - containerSvcMorph "github.com/nspcc-dev/neofs-node/pkg/services/container/morph" - apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" - containerSDK "github.com/nspcc-dev/neofs-sdk-go/container" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - 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" - neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" - 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" - usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" - "github.com/stretchr/testify/require" -) - -const ( - iat = 10 - nbf = 20 - exp = 30 -) - -type mock struct { - cnr containerSDK.Container - epoch uint64 -} - -func (m mock) CurrentEpoch() uint64 { - return m.epoch -} - -func (m mock) Get(_ cid.ID) (*containerCore.Container, error) { - return &containerCore.Container{Value: m.cnr}, nil -} - -func (m mock) GetEACL(_ cid.ID) (*containerCore.EACL, error) { - return nil, nil -} - -func (m mock) List(_ *user.ID) ([]cid.ID, error) { - return nil, nil -} - -func (m mock) Put(_ containerCore.Container) (*cid.ID, error) { - return new(cid.ID), nil -} - -func (m mock) Delete(_ containerCore.RemovalWitness) error { - return nil -} - -func (m mock) PutEACL(_ containerCore.EACL) error { - return nil -} - -func TestExecutor(t *testing.T) { - cnr := cidtest.ID() - - var cnrV2 refs.ContainerID - cnr.WriteToV2(&cnrV2) - - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - - signer := user.NewAutoIDSigner(priv.PrivateKey) - - sign := func(reqBody interface { - StableMarshal([]byte) []byte - SetSignature(signature *refs.Signature) - }) { - var sig neofscrypto.Signature - require.NoError(t, sig.Calculate(signer, reqBody.StableMarshal(nil))) - - var sigV2 refs.Signature - sig.WriteToV2(&sigV2) - reqBody.SetSignature(&sigV2) - } - - tok := sessiontest.Container() - // sdk generates broken chronologic - tok.SetIat(iat) - tok.SetNbf(nbf) - tok.SetExp(exp) - tok.ApplyOnlyTo(cnr) - require.NoError(t, tok.Sign(signer)) - - var tokV2 session.Token - tok.WriteToV2(&tokV2) - - realContainer := containertest.Container() - realContainer.SetOwner(tok.Issuer()) - - m := mock{cnr: realContainer, epoch: 25} - e := containerSvcMorph.NewExecutor(m, m, m) - - tests := []struct { - name string - op func(e containerSvc.ServiceExecutor, tokV2 *session.Token) error - }{ - { - name: "put", - op: func(e containerSvc.ServiceExecutor, tokV2 *session.Token) (err error) { - var reqBody container.PutRequestBody - - cnr := containertest.Container() - - var cnrV2 container.Container - cnr.WriteToV2(&cnrV2) - - reqBody.SetContainer(&cnrV2) - sign(&reqBody) - - _, err = e.Put(context.TODO(), tokV2, &reqBody) - return - }, - }, - { - name: "delete", - op: func(e containerSvc.ServiceExecutor, tokV2 *session.Token) (err error) { - var reqBody container.DeleteRequestBody - reqBody.SetContainerID(&cnrV2) - sign(&reqBody) - - cc, ok := tokV2.GetBody().GetContext().(*session.ContainerSessionContext) - if ok { - cc.SetVerb(session.ContainerVerbDelete) - signV2Token(t, signer, tokV2) - } - - _, err = e.Delete(context.TODO(), tokV2, &reqBody) - return - }, - }, - { - name: "setEACL", - op: func(e containerSvc.ServiceExecutor, tokV2 *session.Token) (err error) { - var reqBody container.SetExtendedACLRequestBody - reqBody.SetSignature(new(refs.Signature)) - sign(&reqBody) - - cc, ok := tokV2.GetBody().GetContext().(*session.ContainerSessionContext) - if ok { - cc.SetVerb(session.ContainerVerbSetEACL) - signV2Token(t, signer, tokV2) - } - - _, err = e.SetExtendedACL(context.TODO(), tokV2, &reqBody) - return - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - tok := generateToken(t, new(session.ObjectSessionContext), signer) - require.Error(t, test.op(e, tok)) - - require.NoError(t, test.op(e, &tokV2)) - - require.NoError(t, test.op(e, nil)) - }) - } -} - -func TestValidateToken(t *testing.T) { - cID := cidtest.ID() - var cIDV2 refs.ContainerID - cID.WriteToV2(&cIDV2) - - priv, err := keys.NewPrivateKey() - require.NoError(t, err) - - signer := user.NewAutoIDSigner(priv.PrivateKey) - - tok := sessiontest.Container() - // sdk generates broken chronologic - tok.SetIat(iat) - tok.SetNbf(nbf) - tok.SetExp(exp) - tok.ApplyOnlyTo(cID) - tok.ForVerb(sessionsdk.VerbContainerDelete) - require.NoError(t, tok.Sign(signer)) - - cnr := containertest.Container() - cnr.SetOwner(tok.Issuer()) - - var cnrV2 container.Container - cnr.WriteToV2(&cnrV2) - - m := &mock{cnr: cnr} - e := containerSvcMorph.NewExecutor(m, m, m) - - t.Run("non-container token", func(t *testing.T) { - var reqBody container.DeleteRequestBody - reqBody.SetContainerID(&cIDV2) - - var tokV2 session.Token - objectSession := sessiontest.Object() - require.NoError(t, objectSession.Sign(signer)) - - objectSession.WriteToV2(&tokV2) - - _, err = e.Delete(context.TODO(), &tokV2, &reqBody) - require.Error(t, err) - }) - - t.Run("wrong verb token", func(t *testing.T) { - var reqBody container.DeleteRequestBody - reqBody.SetContainerID(&cIDV2) - - var tokCopy sessionsdk.Container - tok.CopyTo(&tokCopy) - tokCopy.ForVerb(sessionsdk.VerbContainerPut) - - var tokV2 session.Token - tokCopy.WriteToV2(&tokV2) - - _, err = e.Delete(context.TODO(), &tokV2, &reqBody) - require.Error(t, err) - }) - - t.Run("incorrect cID", func(t *testing.T) { - var reqBody container.DeleteRequestBody - reqBody.SetContainerID(&cIDV2) - - var tokV2 session.Token - var cIDV2 refs.ContainerID - cc := new(session.ContainerSessionContext) - b := new(session.TokenBody) - - cIDV2.SetValue(make([]byte, sha256.Size+1)) - cc.SetContainerID(&cIDV2) - b.SetContext(cc) - tokV2.SetBody(b) - - _, err = e.Delete(context.TODO(), &tokV2, &reqBody) - require.Error(t, err) - }) - - t.Run("different container ID", func(t *testing.T) { - var reqBody container.DeleteRequestBody - reqBody.SetContainerID(&cIDV2) - - var tokCopy sessionsdk.Container - tok.CopyTo(&tokCopy) - tokCopy.ApplyOnlyTo(cidtest.ID()) - - require.NoError(t, tokCopy.Sign(signer)) - - var tokV2 session.Token - tokCopy.WriteToV2(&tokV2) - - _, err = e.Delete(context.TODO(), &tokV2, &reqBody) - require.Error(t, err) - }) - - t.Run("different issuer", func(t *testing.T) { - var reqBody container.DeleteRequestBody - reqBody.SetContainerID(&cIDV2) - - var tokV2 session.Token - tok.WriteToV2(&tokV2) - - var ownerV2Wrong refs.OwnerID - ownerWrong := usertest.ID() - ownerWrong.WriteToV2(&ownerV2Wrong) - - tokV2.GetBody().SetOwnerID(&ownerV2Wrong) - - _, err = e.Delete(context.TODO(), &tokV2, &reqBody) - require.Error(t, err) - }) - - t.Run("incorrect signature", func(t *testing.T) { - var reqBody container.DeleteRequestBody - reqBody.SetContainerID(&cIDV2) - - var tokV2 session.Token - tok.WriteToV2(&tokV2) - - var wrongSig refs.Signature - tokV2.SetSignature(&wrongSig) - - _, err = e.Delete(context.TODO(), &tokV2, &reqBody) - require.Error(t, err) - }) - - t.Run("wildcard support", func(t *testing.T) { - var reqBody container.DeleteRequestBody - reqBody.SetContainerID(&cIDV2) - - var tok sessionsdk.Container - - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - tok.SetIat(iat) - tok.SetNbf(nbf) - tok.SetExp(exp) - tok.ForVerb(sessionsdk.VerbContainerDelete) - tok.SetID(uuid.New()) - tok.SetAuthKey((*neofsecdsa.PublicKey)(&priv.PublicKey)) - require.NoError(t, tok.Sign(signer)) - - var tokV2 session.Token - tok.WriteToV2(&tokV2) - - m := &mock{cnr: cnr, epoch: 25} - e := containerSvcMorph.NewExecutor(m, m, m) - - t.Run("wrong owner", func(t *testing.T) { - m.cnr = containertest.Container() - - _, err := e.Delete(context.TODO(), &tokV2, &reqBody) - require.Error(t, err) - }) - - t.Run("correct owner", func(t *testing.T) { - m.cnr = cnr - - _, err := e.Delete(context.TODO(), &tokV2, &reqBody) - require.NoError(t, err) - }) - }) - - t.Run("token lifetime", func(t *testing.T) { - var reqBody container.DeleteRequestBody - reqBody.SetContainerID(&cIDV2) - - var tokV2 session.Token - tok.WriteToV2(&tokV2) - - t.Run("IAt too big", func(t *testing.T) { - m.epoch = iat - 1 - - _, err = e.Delete(context.TODO(), &tokV2, &reqBody) - require.ErrorContains(t, err, "should not be issued yet") - }) - - t.Run("NBf too small", func(t *testing.T) { - m.epoch = nbf - 1 - - _, err = e.Delete(context.TODO(), &tokV2, &reqBody) - require.ErrorContains(t, err, "not valid yet") - }) - - t.Run("expired", func(t *testing.T) { - m.epoch = exp + 1 - - _, err = e.Delete(context.TODO(), &tokV2, &reqBody) - require.ErrorAs(t, err, new(apistatus.SessionTokenExpired)) - }) - }) -} - -func generateToken(t *testing.T, ctx session.TokenContext, signer user.Signer) *session.Token { - body := new(session.TokenBody) - body.SetContext(ctx) - - tok := new(session.Token) - tok.SetBody(body) - - signV2Token(t, signer, tok) - - return tok -} - -func signV2Token(t *testing.T, signer user.Signer, tokV2 *session.Token) { - sig, err := signer.Sign(tokV2.GetBody().StableMarshal(nil)) - require.NoError(t, err) - - var sigV2 refs.Signature - sigV2.SetKey(neofscrypto.PublicKeyBytes(signer.Public())) - sigV2.SetScheme(refs.SignatureScheme(signer.Scheme())) - sigV2.SetSign(sig) - - tokV2.SetSignature(&sigV2) -} diff --git a/pkg/services/container/response.go b/pkg/services/container/response.go deleted file mode 100644 index 2a3bb63364..0000000000 --- a/pkg/services/container/response.go +++ /dev/null @@ -1,115 +0,0 @@ -package container - -import ( - "context" - - "github.com/nspcc-dev/neofs-api-go/v2/container" - "github.com/nspcc-dev/neofs-node/pkg/services/util" - "github.com/nspcc-dev/neofs-node/pkg/services/util/response" -) - -type responseService struct { - respSvc *response.Service - - svc Server -} - -// NewResponseService returns container service instance that passes internal service -// call to response service. -func NewResponseService(cnrSvc Server, respSvc *response.Service) Server { - return &responseService{ - respSvc: respSvc, - svc: cnrSvc, - } -} - -func (s *responseService) Put(ctx context.Context, req *container.PutRequest) (*container.PutResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Put(ctx, req.(*container.PutRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.PutResponse), nil -} - -func (s *responseService) Delete(ctx context.Context, req *container.DeleteRequest) (*container.DeleteResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Delete(ctx, req.(*container.DeleteRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.DeleteResponse), nil -} - -func (s *responseService) Get(ctx context.Context, req *container.GetRequest) (*container.GetResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Get(ctx, req.(*container.GetRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.GetResponse), nil -} - -func (s *responseService) List(ctx context.Context, req *container.ListRequest) (*container.ListResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.List(ctx, req.(*container.ListRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.ListResponse), nil -} - -func (s *responseService) SetExtendedACL(ctx context.Context, req *container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.SetExtendedACL(ctx, req.(*container.SetExtendedACLRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.SetExtendedACLResponse), nil -} - -func (s *responseService) GetExtendedACL(ctx context.Context, req *container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.GetExtendedACL(ctx, req.(*container.GetExtendedACLRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.GetExtendedACLResponse), nil -} - -func (s *responseService) AnnounceUsedSpace(ctx context.Context, req *container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.AnnounceUsedSpace(ctx, req.(*container.AnnounceUsedSpaceRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.AnnounceUsedSpaceResponse), nil -} diff --git a/pkg/services/container/server.go b/pkg/services/container/server.go index d3c8298e35..ec0a6c4f32 100644 --- a/pkg/services/container/server.go +++ b/pkg/services/container/server.go @@ -2,137 +2,504 @@ package container import ( "context" + "crypto/ecdsa" + "errors" + "fmt" - "github.com/nspcc-dev/neofs-api-go/v2/container" + apiacl "github.com/nspcc-dev/neofs-api-go/v2/acl" + protoacl "github.com/nspcc-dev/neofs-api-go/v2/acl/grpc" + apicontainer "github.com/nspcc-dev/neofs-api-go/v2/container" protocontainer "github.com/nspcc-dev/neofs-api-go/v2/container/grpc" + apirefs "github.com/nspcc-dev/neofs-api-go/v2/refs" + refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + apisession "github.com/nspcc-dev/neofs-api-go/v2/session" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + "github.com/nspcc-dev/neofs-api-go/v2/signature" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" + "github.com/nspcc-dev/neofs-node/pkg/core/netmap" + "github.com/nspcc-dev/neofs-node/pkg/services/util" + apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" + "github.com/nspcc-dev/neofs-sdk-go/container" + 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/session" + "github.com/nspcc-dev/neofs-sdk-go/user" + "github.com/nspcc-dev/neofs-sdk-go/version" ) -// Server is an interface of the NeoFS API Container service server. -type Server interface { - Put(context.Context, *container.PutRequest) (*container.PutResponse, error) - Get(context.Context, *container.GetRequest) (*container.GetResponse, error) - Delete(context.Context, *container.DeleteRequest) (*container.DeleteResponse, error) - List(context.Context, *container.ListRequest) (*container.ListResponse, error) - SetExtendedACL(context.Context, *container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) - GetExtendedACL(context.Context, *container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) - AnnounceUsedSpace(context.Context, *container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) +// Contract groups ops of the Container contract deployed in the FS chain +// required to serve NeoFS API Container service. +type Contract interface { + // Put sends transaction creating container with provided credentials. If + // accepted, transaction is processed async. Returns container ID to check the + // creation status. + Put(_ container.Container, pub, sig []byte, _ *session.Container) (cid.ID, error) + // Get returns container by its ID. Returns [apistatus.ErrContainerNotFound] + // error if container is missing. + Get(cid.ID) (container.Container, error) + // List returns IDs of all container belonging to the given user. + // + // Callers do not modify the result. + List(user.ID) ([]cid.ID, error) + // PutEACL sends transaction setting container's eACL with provided credentials. + // If accepted, transaction is processed async. + PutEACL(_ eacl.Table, pub, sig []byte, _ *session.Container) error + // GetEACL returns eACL of the container by its ID. Returns + // [apistatus.ErrEACLNotFound] error if eACL is missing. + GetEACL(cid.ID) (eacl.Table, error) + // Delete sends transaction removing referenced container with provided + // credentials. If accepted, transaction is processed async. + Delete(_ cid.ID, pub, sig []byte, _ *session.Container) error } -// Server wraps NeoFS API Container service and -// provides gRPC Container service server interface. type server struct { protocontainer.UnimplementedContainerServiceServer - srv Server + signer *ecdsa.PrivateKey + net netmap.State + contract Contract } -// New returns protocontainer.ContainerServiceServer based on the Server. -func New(c Server) protocontainer.ContainerServiceServer { +// New provides protocontainer.ContainerServiceServer based on specified +// [Contract]. +// +// All response messages are signed using specified signer and have current +// epoch in the meta header. +func New(s *ecdsa.PrivateKey, net netmap.State, c Contract) protocontainer.ContainerServiceServer { return &server{ - srv: c, + signer: s, + net: net, + contract: c, } } -// Put converts gRPC PutRequest message and passes it to internal Container service. -func (s *server) Put(ctx context.Context, req *protocontainer.PutRequest) (*protocontainer.PutResponse, error) { - putReq := new(container.PutRequest) +func (s *server) makeResponseMetaHeader(st *protostatus.Status) *protosession.ResponseMetaHeader { + v := version.Current() + var v2 apirefs.Version + v.WriteToV2(&v2) + return &protosession.ResponseMetaHeader{ + Version: v2.ToGRPCMessage().(*refs.Version), + Epoch: s.net.CurrentEpoch(), + Status: st, + } +} + +// decodes the container session token from the request and checks its +// signature, lifetime and applicability to this operation as per request. +// Returns both nil if token is not attached to the request. +func (s *server) getVerifiedSessionToken(req interface { + GetMetaHeader() *protosession.RequestMetaHeader +}) (*session.Container, error) { + mh := req.GetMetaHeader() + for omh := mh.GetOrigin(); omh != nil; omh = mh.GetOrigin() { + mh = omh + } + m := mh.GetSessionToken() + if m == nil { + return nil, nil + } + + var st2 apisession.Token + if err := st2.FromGRPCMessage(m); err != nil { + panic(err) + } + var token session.Container + if err := token.ReadFromV2(st2); err != nil { + return nil, fmt.Errorf("decode: %w", err) + } + + if !token.VerifySignature() { + return nil, errors.New("invalid signature") + } + + var expVerb session.ContainerVerb + switch req.(type) { + default: + panic(fmt.Sprintf("unexpected request type %T", req)) + case *protocontainer.PutRequest: + expVerb = session.VerbContainerPut + case *protocontainer.DeleteRequest: + expVerb = session.VerbContainerDelete + case *protocontainer.SetExtendedACLRequest: + expVerb = session.VerbContainerSetEACL + } + if !token.AssertVerb(expVerb) { + // must be checked by ReadFromV2, so NPE is OK here + verb := m.Body.Context.(*protosession.SessionToken_Body_Container).Container.Verb + return nil, fmt.Errorf("wrong container session operation: %s", verb) + } + + cur := s.net.CurrentEpoch() + lt := m.Body.Lifetime // must be checked by ReadFromV2, so NPE is OK here + if exp := lt.Exp; exp < cur { + return nil, apistatus.ErrSessionTokenExpired + } + if iat := lt.Iat; iat > cur { + return nil, fmt.Errorf("token should not be issued yet: IAt: %d, current epoch: %d", iat, cur) + } + if nbf := lt.Nbf; nbf > cur { + return nil, fmt.Errorf("token is not valid yet: NBf: %d, current epoch: %d", nbf, cur) + } + + return &token, nil +} + +func (s *server) checkSessionIssuer(id cid.ID, token session.Container) error { + cnr, err := s.contract.Get(id) + if err != nil { + return fmt.Errorf("get container by ID: %w", err) + } + + var token2 apisession.Token // TODO: get signature directly from token on SDK upgrade + token.WriteToV2(&token2) + mSig := token2.GetSignature() + + var pub neofsecdsa.PublicKey + if err := pub.Decode(mSig.GetKey()); err != nil { + return fmt.Errorf("invalid public key in the session token signature: %w", err) + } + + issuer := token.Issuer() + if signer := user.NewFromECDSAPublicKey(ecdsa.PublicKey(pub)); signer != issuer { + return errors.New("session token is signed not by its issuer") + } + + if owner := cnr.Owner(); issuer != owner { + return errors.New("session was not issued by the container owner") + } + + return nil +} + +func (s *server) makePutResponse(body *protocontainer.PutResponse_Body, st *protostatus.Status) (*protocontainer.PutResponse, error) { + resp := &protocontainer.PutResponse{ + Body: body, + MetaHeader: s.makeResponseMetaHeader(st), + } + return util.SignResponse(s.signer, resp, apicontainer.PutResponse{}), nil +} + +func (s *server) makeFailedPutResponse(err error) (*protocontainer.PutResponse, error) { + return s.makePutResponse(nil, util.ToStatus(err)) +} + +// Put forwards container creation request to the underlying [Contract] for +// further processing. If session token is attached, it's verified. Returns ID +// to check request status in the response. +func (s *server) Put(_ context.Context, req *protocontainer.PutRequest) (*protocontainer.PutResponse, error) { + putReq := new(apicontainer.PutRequest) if err := putReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(putReq); err != nil { + return s.makeFailedPutResponse(util.ToRequestSignatureVerificationError(err)) + } - resp, err := s.srv.Put(ctx, putReq) + reqBody := req.GetBody() + mSig := reqBody.GetSignature() + if mSig == nil { + return s.makeFailedPutResponse(errors.New("missing container signature")) + } + mCnr := reqBody.GetContainer() + if mCnr == nil { + return s.makeFailedPutResponse(errors.New("missing container")) + } + + var cnr container.Container + var cnr2 apicontainer.Container + if err := cnr2.FromGRPCMessage(mCnr); err != nil { + panic(err) + } + if err := cnr.ReadFromV2(cnr2); err != nil { + return s.makeFailedPutResponse(fmt.Errorf("invalid container: %w", err)) + } + + st, err := s.getVerifiedSessionToken(req) if err != nil { - return nil, err + return s.makeFailedPutResponse(fmt.Errorf("verify session token: %w", err)) + } + + id, err := s.contract.Put(cnr, mSig.Key, mSig.Sign, st) + if err != nil { + return s.makeFailedPutResponse(err) + } + + var id2 apirefs.ContainerID + id.WriteToV2(&id2) + respBody := &protocontainer.PutResponse_Body{ + ContainerId: id2.ToGRPCMessage().(*refs.ContainerID), } + return s.makePutResponse(respBody, util.StatusOK) +} - return resp.ToGRPCMessage().(*protocontainer.PutResponse), nil +func (s *server) makeDeleteResponse(err error) (*protocontainer.DeleteResponse, error) { + resp := &protocontainer.DeleteResponse{ + MetaHeader: s.makeResponseMetaHeader(util.ToStatus(err)), + } + return util.SignResponse(s.signer, resp, apicontainer.DeleteResponse{}), nil } -// Delete converts gRPC DeleteRequest message and passes it to internal Container service. -func (s *server) Delete(ctx context.Context, req *protocontainer.DeleteRequest) (*protocontainer.DeleteResponse, error) { - delReq := new(container.DeleteRequest) +// Delete forwards container removal request to the underlying [Contract] for +// further processing. If session token is attached, it's verified. +func (s *server) Delete(_ context.Context, req *protocontainer.DeleteRequest) (*protocontainer.DeleteResponse, error) { + delReq := new(apicontainer.DeleteRequest) if err := delReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(delReq); err != nil { + return s.makeDeleteResponse(util.ToRequestSignatureVerificationError(err)) + } - resp, err := s.srv.Delete(ctx, delReq) + reqBody := req.GetBody() + mSig := reqBody.GetSignature() + if mSig == nil { + return s.makeDeleteResponse(errors.New("missing ID signature")) + } + mID := reqBody.GetContainerId() + if mID == nil { + return s.makeDeleteResponse(errors.New("missing ID")) + } + + var id2 apirefs.ContainerID + if err := id2.FromGRPCMessage(mID); err != nil { + panic(err) + } + var id cid.ID + if err := id.ReadFromV2(id2); err != nil { + return s.makeDeleteResponse(fmt.Errorf("invalid ID: %w", err)) + } + + st, err := s.getVerifiedSessionToken(req) if err != nil { - return nil, err + return s.makeDeleteResponse(fmt.Errorf("verify session token: %w", err)) + } + if st != nil { + if err := s.checkSessionIssuer(id, *st); err != nil { + return s.makeDeleteResponse(fmt.Errorf("verify session issuer: %w", err)) + } + if !st.AppliedTo(id) { + return s.makeDeleteResponse(errors.New("session is not applied to requested container")) + } } - return resp.ToGRPCMessage().(*protocontainer.DeleteResponse), nil + if err := s.contract.Delete(id, mSig.Key, mSig.Sign, st); err != nil { + return s.makeDeleteResponse(err) + } + + return s.makeDeleteResponse(util.StatusOKErr) +} + +func (s *server) makeGetResponse(body *protocontainer.GetResponse_Body, st *protostatus.Status) (*protocontainer.GetResponse, error) { + resp := &protocontainer.GetResponse{ + Body: body, + MetaHeader: s.makeResponseMetaHeader(st), + } + return util.SignResponse(s.signer, resp, apicontainer.GetResponse{}), nil +} + +func (s *server) makeFailedGetResponse(err error) (*protocontainer.GetResponse, error) { + return s.makeGetResponse(nil, util.ToStatus(err)) } -// Get converts gRPC GetRequest message and passes it to internal Container service. -func (s *server) Get(ctx context.Context, req *protocontainer.GetRequest) (*protocontainer.GetResponse, error) { - getReq := new(container.GetRequest) +// Get requests container from the underlying [Contract] and returns it in the +// response. +func (s *server) Get(_ context.Context, req *protocontainer.GetRequest) (*protocontainer.GetResponse, error) { + getReq := new(apicontainer.GetRequest) if err := getReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(getReq); err != nil { + return s.makeFailedGetResponse(util.ToRequestSignatureVerificationError(err)) + } - resp, err := s.srv.Get(ctx, getReq) + mID := req.GetBody().GetContainerId() + if mID == nil { + return s.makeFailedGetResponse(errors.New("missing ID")) + } + + var id2 apirefs.ContainerID + if err := id2.FromGRPCMessage(mID); err != nil { + panic(err) + } + var id cid.ID + if err := id.ReadFromV2(id2); err != nil { + return s.makeFailedGetResponse(fmt.Errorf("invalid ID: %w", err)) + } + + cnr, err := s.contract.Get(id) if err != nil { - return nil, err + return s.makeFailedGetResponse(err) + } + + var cnr2 apicontainer.Container + cnr.WriteToV2(&cnr2) + body := &protocontainer.GetResponse_Body{ + Container: cnr2.ToGRPCMessage().(*protocontainer.Container), + } + return s.makeGetResponse(body, nil) +} + +func (s *server) makeListResponse(body *protocontainer.ListResponse_Body, st *protostatus.Status) (*protocontainer.ListResponse, error) { + resp := &protocontainer.ListResponse{ + Body: body, + MetaHeader: s.makeResponseMetaHeader(st), } + return util.SignResponse(s.signer, resp, apicontainer.ListResponse{}), nil +} - return resp.ToGRPCMessage().(*protocontainer.GetResponse), nil +func (s *server) makeFailedListResponse(err error) (*protocontainer.ListResponse, error) { + return s.makeListResponse(nil, util.ToStatus(err)) } -// List converts gRPC ListRequest message and passes it to internal Container service. -func (s *server) List(ctx context.Context, req *protocontainer.ListRequest) (*protocontainer.ListResponse, error) { - listReq := new(container.ListRequest) +// List lists user containers from the underlying [Contract] and returns their +// IDs in the response. +func (s *server) List(_ context.Context, req *protocontainer.ListRequest) (*protocontainer.ListResponse, error) { + listReq := new(apicontainer.ListRequest) if err := listReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(listReq); err != nil { + return s.makeFailedListResponse(util.ToRequestSignatureVerificationError(err)) + } + + mID := req.GetBody().GetOwnerId() + if mID == nil { + return s.makeFailedListResponse(errors.New("missing user")) + } - resp, err := s.srv.List(ctx, listReq) + var id2 apirefs.OwnerID + if err := id2.FromGRPCMessage(mID); err != nil { + panic(err) + } + var id user.ID + if err := id.ReadFromV2(id2); err != nil { + return s.makeFailedListResponse(fmt.Errorf("invalid user: %w", err)) + } + + cs, err := s.contract.List(id) if err != nil { - return nil, err + return s.makeFailedListResponse(err) + } + + if len(cs) == 0 { + return s.makeListResponse(nil, util.StatusOK) + } + + cs2 := make([]apirefs.ContainerID, len(cs)) + for i := range cs { + cs[i].WriteToV2(&cs2[i]) + } + body := &protocontainer.ListResponse_Body{ + ContainerIds: make([]*refs.ContainerID, len(cs)), } + for i := range cs { + body.ContainerIds[i] = cs2[i].ToGRPCMessage().(*refs.ContainerID) + } + return s.makeListResponse(body, util.StatusOK) +} - return resp.ToGRPCMessage().(*protocontainer.ListResponse), nil +func (s *server) makeSetEACLResponse(err error) (*protocontainer.SetExtendedACLResponse, error) { + resp := &protocontainer.SetExtendedACLResponse{ + MetaHeader: s.makeResponseMetaHeader(util.ToStatus(err)), + } + return util.SignResponse(s.signer, resp, apicontainer.SetExtendedACLResponse{}), nil } -// SetExtendedACL converts gRPC SetExtendedACLRequest message and passes it to internal Container service. -func (s *server) SetExtendedACL(ctx context.Context, req *protocontainer.SetExtendedACLRequest) (*protocontainer.SetExtendedACLResponse, error) { - setEACLReq := new(container.SetExtendedACLRequest) +// SetExtendedACL forwards eACL setting request to the underlying [Contract] +// for further processing. If session token is attached, it's verified. +func (s *server) SetExtendedACL(_ context.Context, req *protocontainer.SetExtendedACLRequest) (*protocontainer.SetExtendedACLResponse, error) { + setEACLReq := new(apicontainer.SetExtendedACLRequest) if err := setEACLReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(setEACLReq); err != nil { + return s.makeSetEACLResponse(util.ToRequestSignatureVerificationError(err)) + } - resp, err := s.srv.SetExtendedACL(ctx, setEACLReq) + reqBody := req.GetBody() + mSig := reqBody.GetSignature() + if mSig == nil { + return s.makeSetEACLResponse(errors.New("missing eACL signature")) + } + mEACL := reqBody.GetEacl() + if mEACL == nil { + return s.makeSetEACLResponse(errors.New("missing eACL")) + } + + var eACL2 apiacl.Table + if err := eACL2.FromGRPCMessage(mEACL); err != nil { + panic(err) + } + var eACL eacl.Table + if err := eACL.ReadFromV2(eACL2); err != nil { + return s.makeSetEACLResponse(fmt.Errorf("invalid eACL: %w", err)) + } + + st, err := s.getVerifiedSessionToken(req) if err != nil { - return nil, err + return s.makeSetEACLResponse(fmt.Errorf("verify session token: %w", err)) + } + if st != nil { + id := eACL.GetCID() + if err := s.checkSessionIssuer(id, *st); err != nil { + return s.makeSetEACLResponse(fmt.Errorf("verify session issuer: %w", err)) + } + if !st.AppliedTo(id) { + return s.makeSetEACLResponse(errors.New("session is not applied to requested container")) + } + } + + if err := s.contract.PutEACL(eACL, mSig.Key, mSig.Sign, st); err != nil { + return s.makeSetEACLResponse(err) } - return resp.ToGRPCMessage().(*protocontainer.SetExtendedACLResponse), nil + return s.makeSetEACLResponse(util.StatusOKErr) } -// GetExtendedACL converts gRPC GetExtendedACLRequest message and passes it to internal Container service. -func (s *server) GetExtendedACL(ctx context.Context, req *protocontainer.GetExtendedACLRequest) (*protocontainer.GetExtendedACLResponse, error) { - getEACLReq := new(container.GetExtendedACLRequest) - if err := getEACLReq.FromGRPCMessage(req); err != nil { - return nil, err +func (s *server) makeGetEACLResponse(body *protocontainer.GetExtendedACLResponse_Body, st *protostatus.Status) (*protocontainer.GetExtendedACLResponse, error) { + resp := &protocontainer.GetExtendedACLResponse{ + Body: body, + MetaHeader: s.makeResponseMetaHeader(st), } + return util.SignResponse(s.signer, resp, apicontainer.GetExtendedACLResponse{}), nil +} - resp, err := s.srv.GetExtendedACL(ctx, getEACLReq) - if err != nil { +func (s *server) makeFailedGetEACLResponse(err error) (*protocontainer.GetExtendedACLResponse, error) { + return s.makeGetEACLResponse(nil, util.ToStatus(err)) +} + +// GetExtendedACL read eACL of the requested container from the underlying +// [Contract] and returns the result in the response. +func (s *server) GetExtendedACL(_ context.Context, req *protocontainer.GetExtendedACLRequest) (*protocontainer.GetExtendedACLResponse, error) { + getEACLReq := new(apicontainer.GetExtendedACLRequest) + if err := getEACLReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(getEACLReq); err != nil { + return s.makeFailedGetEACLResponse(util.ToRequestSignatureVerificationError(err)) + } - return resp.ToGRPCMessage().(*protocontainer.GetExtendedACLResponse), nil -} + mID := req.GetBody().GetContainerId() + if mID == nil { + return s.makeFailedGetEACLResponse(errors.New("missing ID")) + } -// AnnounceUsedSpace converts gRPC AnnounceUsedSpaceRequest message and passes it to internal Container service. -func (s *server) AnnounceUsedSpace(ctx context.Context, req *protocontainer.AnnounceUsedSpaceRequest) (*protocontainer.AnnounceUsedSpaceResponse, error) { - announceReq := new(container.AnnounceUsedSpaceRequest) - if err := announceReq.FromGRPCMessage(req); err != nil { - return nil, err + var id2 apirefs.ContainerID + if err := id2.FromGRPCMessage(mID); err != nil { + panic(err) + } + var id cid.ID + if err := id.ReadFromV2(id2); err != nil { + return s.makeFailedGetEACLResponse(fmt.Errorf("invalid ID: %w", err)) } - resp, err := s.srv.AnnounceUsedSpace(ctx, announceReq) + eACL, err := s.contract.GetEACL(id) if err != nil { - return nil, err + return s.makeFailedGetEACLResponse(err) } - return resp.ToGRPCMessage().(*protocontainer.AnnounceUsedSpaceResponse), nil + body := &protocontainer.GetExtendedACLResponse_Body{ + Eacl: eACL.ToV2().ToGRPCMessage().(*protoacl.EACLTable), + } + return s.makeGetEACLResponse(body, util.StatusOK) } diff --git a/pkg/services/container/server_test.go b/pkg/services/container/server_test.go new file mode 100644 index 0000000000..75923cc5b6 --- /dev/null +++ b/pkg/services/container/server_test.go @@ -0,0 +1,278 @@ +package container_test + +import ( + "context" + "errors" + "testing" + + "github.com/google/uuid" + apicontainer "github.com/nspcc-dev/neofs-api-go/v2/container" + protocontainer "github.com/nspcc-dev/neofs-api-go/v2/container/grpc" + apirefs "github.com/nspcc-dev/neofs-api-go/v2/refs" + refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + apisession "github.com/nspcc-dev/neofs-api-go/v2/session" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + "github.com/nspcc-dev/neofs-api-go/v2/signature" + containerSvc "github.com/nspcc-dev/neofs-node/pkg/services/container" + "github.com/nspcc-dev/neofs-sdk-go/container" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/session" + "github.com/nspcc-dev/neofs-sdk-go/user" + usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" + "github.com/stretchr/testify/require" +) + +type testFSChain struct { + cnr container.Container + epoch uint64 +} + +func (x testFSChain) CurrentEpoch() uint64 { return x.epoch } + +func (testFSChain) Put(container.Container, []byte, []byte, *session.Container) (cid.ID, error) { + return cid.ID{}, errors.New("unimplemented") +} + +func (x testFSChain) Get(cid.ID) (container.Container, error) { + return x.cnr, nil +} + +func (testFSChain) List(user.ID) ([]cid.ID, error) { + return nil, errors.New("unimplemented") +} + +func (testFSChain) PutEACL(eacl.Table, []byte, []byte, *session.Container) error { + return errors.New("unimplemented") +} + +func (testFSChain) GetEACL(cid.ID) (eacl.Table, error) { + return eacl.Table{}, errors.New("unimplemented") +} + +func (testFSChain) Delete(cid.ID, []byte, []byte, *session.Container) error { + return errors.New("unimplemented") +} + +func makeDeleteRequestWithSession(t testing.TB, usr usertest.UserSigner, cnr cid.ID, st interface { + WriteToV2(token *apisession.Token) +}) *protocontainer.DeleteRequest { + var st2 apisession.Token + st.WriteToV2(&st2) + return makeDeleteRequestWithSessionMessage(t, usr, cnr, st2.ToGRPCMessage().(*protosession.SessionToken)) +} + +func makeDeleteRequestWithSessionMessage(t testing.TB, usr usertest.UserSigner, cnr cid.ID, st *protosession.SessionToken) *protocontainer.DeleteRequest { + var cnr2 apirefs.ContainerID + cnr.WriteToV2(&cnr2) + + req := &protocontainer.DeleteRequest{ + Body: &protocontainer.DeleteRequest_Body{ + ContainerId: cnr2.ToGRPCMessage().(*refs.ContainerID), + Signature: new(refs.SignatureRFC6979), + }, + MetaHeader: &protosession.RequestMetaHeader{ + SessionToken: st, + }, + } + + var req2 apicontainer.DeleteRequest + require.NoError(t, req2.FromGRPCMessage(req)) + require.NoError(t, signature.SignServiceMessage(&usr.ECDSAPrivateKey, &req2)) + + return req2.ToGRPCMessage().(*protocontainer.DeleteRequest) +} + +func TestServer_Delete(t *testing.T) { + ctx := context.Background() + usr := usertest.User() + + const anyEpoch = 10 + var cnr container.Container + cnr.SetOwner(usr.ID) + + m := &testFSChain{ + cnr: cnr, + epoch: anyEpoch, + } + svc := containerSvc.New(&usr.ECDSAPrivateKey, m, m) + + t.Run("session", func(t *testing.T) { + t.Run("failure", func(t *testing.T) { + t.Run("non-container", func(t *testing.T) { + var st session.Object + st.SetIssuer(usr.ID) + st.SetID(uuid.New()) + st.SetAuthKey(neofscryptotest.Signer().Public()) + st.SetIat(anyEpoch) + st.SetNbf(anyEpoch) + st.SetExp(anyEpoch) + require.NoError(t, st.Sign(usr)) + + req := makeDeleteRequestWithSession(t, usr, cidtest.ID(), st) + resp, err := svc.Delete(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Nil(t, resp.Body) + + require.NotNil(t, resp.MetaHeader) + require.NotNil(t, resp.MetaHeader.Status) + sts := resp.MetaHeader.Status + require.EqualValues(t, 1024, sts.Code, st) + require.Equal(t, "invalid context *session.ObjectSessionContext", sts.Message) + require.Zero(t, sts.Details) + }) + t.Run("wrong verb", func(t *testing.T) { + var st session.Container + st.SetIssuer(usr.ID) + st.SetID(uuid.New()) + st.SetAuthKey(neofscryptotest.Signer().Public()) + st.SetIat(anyEpoch) + st.SetNbf(anyEpoch) + st.SetExp(anyEpoch) + + st.ForVerb(session.VerbContainerPut) + + require.NoError(t, st.Sign(usr)) + + req := makeDeleteRequestWithSession(t, usr, cidtest.ID(), st) + resp, err := svc.Delete(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Nil(t, resp.Body) + + require.NotNil(t, resp.MetaHeader) + require.NotNil(t, resp.MetaHeader.Status) + sts := resp.MetaHeader.Status + require.EqualValues(t, 1024, sts.Code, st) + require.Equal(t, "wrong container session operation: PUT", sts.Message) + require.Zero(t, sts.Details) + }) + t.Run("container ID mismatch", func(t *testing.T) { + var st session.Container + st.SetIssuer(usr.ID) + st.SetID(uuid.New()) + st.SetAuthKey(neofscryptotest.Signer().Public()) + st.SetIat(anyEpoch) + st.SetNbf(anyEpoch) + st.SetExp(anyEpoch) + st.ForVerb(session.VerbContainerDelete) + + reqCnr := cidtest.ID() + st.ApplyOnlyTo(cidtest.OtherID(reqCnr)) + + require.NoError(t, st.Sign(usr)) + + req := makeDeleteRequestWithSession(t, usr, reqCnr, st) + resp, err := svc.Delete(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Nil(t, resp.Body) + + require.NotNil(t, resp.MetaHeader) + require.NotNil(t, resp.MetaHeader.Status) + sts := resp.MetaHeader.Status + require.EqualValues(t, 1024, sts.Code, st) + require.Equal(t, "session is not applied to requested container", sts.Message) + require.Zero(t, sts.Details) + }) + t.Run("non-owner issuer", func(t *testing.T) { + var st session.Container + st.SetID(uuid.New()) + st.SetAuthKey(neofscryptotest.Signer().Public()) + st.SetIat(anyEpoch) + st.SetNbf(anyEpoch) + st.SetExp(anyEpoch) + st.ForVerb(session.VerbContainerDelete) + + otherUsr := usertest.User() + require.NoError(t, st.Sign(otherUsr)) + + req := makeDeleteRequestWithSession(t, usr, cidtest.ID(), st) + resp, err := svc.Delete(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Nil(t, resp.Body) + + require.NotNil(t, resp.MetaHeader) + require.NotNil(t, resp.MetaHeader.Status) + sts := resp.MetaHeader.Status + require.EqualValues(t, 1024, sts.Code, st) + require.Equal(t, "session was not issued by the container owner", sts.Message) + require.Zero(t, sts.Details) + }) + t.Run("incorrect signature", func(t *testing.T) { + var st session.Container + st.SetID(uuid.New()) + st.SetAuthKey(neofscryptotest.Signer().Public()) + st.SetIat(anyEpoch) + st.SetNbf(anyEpoch) + st.SetExp(anyEpoch) + st.ForVerb(session.VerbContainerDelete) + require.NoError(t, st.Sign(usr)) + + var st2 apisession.Token + st.WriteToV2(&st2) + mst := st2.ToGRPCMessage().(*protosession.SessionToken) + require.NotEmpty(t, mst.Signature.Sign) + mst.Signature.Sign[0]++ + + req := makeDeleteRequestWithSessionMessage(t, usr, cidtest.ID(), mst) + resp, err := svc.Delete(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Nil(t, resp.Body) + + require.NotNil(t, resp.MetaHeader) + require.NotNil(t, resp.MetaHeader.Status) + sts := resp.MetaHeader.Status + require.EqualValues(t, 1024, sts.Code, st) + require.Equal(t, "invalid signature", sts.Message) + require.Zero(t, sts.Details) + }) + t.Run("token lifetime", func(t *testing.T) { + for _, tc := range []struct { + name string + iat, nbf, exp, cur uint64 + code uint32 + msg string + }{ + {name: "iat future", iat: 11, nbf: 10, exp: 10, cur: 10, code: 1024, + msg: "token should not be issued yet: IAt: 11, current epoch: 10"}, + {name: "nbf future", iat: 10, nbf: 11, exp: 10, cur: 10, code: 1024, + msg: "token is not valid yet: NBf: 11, current epoch: 10"}, + {name: "expired", iat: 10, nbf: 10, exp: 9, cur: 10, code: 4097, + msg: "expired session token"}, + } { + var st session.Container + st.SetIssuer(usr.ID) + st.SetID(uuid.New()) + st.SetAuthKey(neofscryptotest.Signer().Public()) + st.ForVerb(session.VerbContainerDelete) + + m.epoch = tc.cur + st.SetIat(tc.iat) + st.SetNbf(tc.nbf) + st.SetExp(tc.exp) + + require.NoError(t, st.Sign(usr)) + + req := makeDeleteRequestWithSession(t, usr, cidtest.ID(), st) + resp, err := svc.Delete(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Nil(t, resp.Body) + + require.NotNil(t, resp.MetaHeader) + require.NotNil(t, resp.MetaHeader.Status) + sts := resp.MetaHeader.Status + require.EqualValues(t, tc.code, sts.Code, st) + require.Equal(t, tc.msg, sts.Message) + require.Zero(t, sts.Details) + } + }) + }) + }) +} diff --git a/pkg/services/container/sign.go b/pkg/services/container/sign.go deleted file mode 100644 index 5508acad6c..0000000000 --- a/pkg/services/container/sign.go +++ /dev/null @@ -1,134 +0,0 @@ -package container - -import ( - "context" - "crypto/ecdsa" - - "github.com/nspcc-dev/neofs-api-go/v2/container" - "github.com/nspcc-dev/neofs-node/pkg/services/util" -) - -type signService struct { - sigSvc *util.SignService - - svc Server -} - -func NewSignService(key *ecdsa.PrivateKey, svc Server) Server { - return &signService{ - sigSvc: util.NewUnarySignService(key), - svc: svc, - } -} - -func (s *signService) Put(ctx context.Context, req *container.PutRequest) (*container.PutResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Put(ctx, req.(*container.PutRequest)) - }, - func() util.ResponseMessage { - return new(container.PutResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.PutResponse), nil -} - -func (s *signService) Delete(ctx context.Context, req *container.DeleteRequest) (*container.DeleteResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Delete(ctx, req.(*container.DeleteRequest)) - }, - func() util.ResponseMessage { - return new(container.DeleteResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.DeleteResponse), nil -} - -func (s *signService) Get(ctx context.Context, req *container.GetRequest) (*container.GetResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Get(ctx, req.(*container.GetRequest)) - }, - func() util.ResponseMessage { - return new(container.GetResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.GetResponse), nil -} - -func (s *signService) List(ctx context.Context, req *container.ListRequest) (*container.ListResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.List(ctx, req.(*container.ListRequest)) - }, - func() util.ResponseMessage { - return new(container.ListResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.ListResponse), nil -} - -func (s *signService) SetExtendedACL(ctx context.Context, req *container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.SetExtendedACL(ctx, req.(*container.SetExtendedACLRequest)) - }, - func() util.ResponseMessage { - return new(container.SetExtendedACLResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.SetExtendedACLResponse), nil -} - -func (s *signService) GetExtendedACL(ctx context.Context, req *container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.GetExtendedACL(ctx, req.(*container.GetExtendedACLRequest)) - }, - func() util.ResponseMessage { - return new(container.GetExtendedACLResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.GetExtendedACLResponse), nil -} - -func (s *signService) AnnounceUsedSpace(ctx context.Context, req *container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.AnnounceUsedSpace(ctx, req.(*container.AnnounceUsedSpaceRequest)) - }, - func() util.ResponseMessage { - return new(container.AnnounceUsedSpaceResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*container.AnnounceUsedSpaceResponse), nil -} diff --git a/pkg/services/netmap/executor.go b/pkg/services/netmap/executor.go deleted file mode 100644 index fd9cbfc54a..0000000000 --- a/pkg/services/netmap/executor.go +++ /dev/null @@ -1,147 +0,0 @@ -package netmap - -import ( - "context" - "errors" - "fmt" - - "github.com/nspcc-dev/neofs-api-go/v2/netmap" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-node/pkg/core/version" - netmapSDK "github.com/nspcc-dev/neofs-sdk-go/netmap" - versionsdk "github.com/nspcc-dev/neofs-sdk-go/version" -) - -type executorSvc struct { - version refs.Version - - state NodeState - - netInfo NetworkInfo -} - -// NodeState encapsulates information -// about current node state. -type NodeState interface { - // LocalNodeInfo must return current node state - // in NeoFS API v2 NodeInfo structure. - LocalNodeInfo() (*netmap.NodeInfo, error) - - // ReadCurrentNetMap reads current local network map of the storage node - // into the given parameter. Returns any error encountered which prevented - // the network map to be read. - ReadCurrentNetMap(*netmap.NetMap) error -} - -// NetworkInfo encapsulates source of the -// recent information about the NeoFS network. -type NetworkInfo interface { - // Dump must return recent network information in NeoFS API v2 NetworkInfo structure. - // - // If protocol version is <=2.9, MillisecondsPerBlock and network config should be unset. - Dump(versionsdk.Version) (*netmapSDK.NetworkInfo, error) -} - -func NewExecutionService(s NodeState, v versionsdk.Version, netInfo NetworkInfo) Server { - if s == nil || netInfo == nil || !version.IsValid(v) { - // this should never happen, otherwise it programmers bug - panic("can't create netmap execution service") - } - - res := &executorSvc{ - state: s, - netInfo: netInfo, - } - - v.WriteToV2(&res.version) - - return res -} - -func (s *executorSvc) LocalNodeInfo( - _ context.Context, - req *netmap.LocalNodeInfoRequest) (*netmap.LocalNodeInfoResponse, error) { - verV2 := req.GetMetaHeader().GetVersion() - if verV2 == nil { - return nil, errors.New("missing version") - } - - var ver versionsdk.Version - if err := ver.ReadFromV2(*verV2); err != nil { - return nil, fmt.Errorf("can't read version: %w", err) - } - - ni, err := s.state.LocalNodeInfo() - if err != nil { - return nil, err - } - - if addrNum := ni.NumberOfAddresses(); addrNum > 0 && ver.Minor() <= 7 { - ni2 := new(netmap.NodeInfo) - ni2.SetPublicKey(ni.GetPublicKey()) - ni2.SetState(ni.GetState()) - ni2.SetAttributes(ni.GetAttributes()) - ni.IterateAddresses(func(s string) bool { - ni2.SetAddresses(s) - return true - }) - - ni = ni2 - } - - body := new(netmap.LocalNodeInfoResponseBody) - body.SetVersion(&s.version) - body.SetNodeInfo(ni) - - resp := new(netmap.LocalNodeInfoResponse) - resp.SetBody(body) - - return resp, nil -} - -func (s *executorSvc) NetworkInfo( - _ context.Context, - req *netmap.NetworkInfoRequest) (*netmap.NetworkInfoResponse, error) { - verV2 := req.GetMetaHeader().GetVersion() - if verV2 == nil { - return nil, errors.New("missing protocol version in meta header") - } - - var ver versionsdk.Version - if err := ver.ReadFromV2(*verV2); err != nil { - return nil, fmt.Errorf("can't read version: %w", err) - } - - ni, err := s.netInfo.Dump(ver) - if err != nil { - return nil, err - } - - var niV2 netmap.NetworkInfo - ni.WriteToV2(&niV2) - - body := new(netmap.NetworkInfoResponseBody) - body.SetNetworkInfo(&niV2) - - resp := new(netmap.NetworkInfoResponse) - resp.SetBody(body) - - return resp, nil -} - -func (s *executorSvc) Snapshot(_ context.Context, _ *netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) { - var nm netmap.NetMap - - err := s.state.ReadCurrentNetMap(&nm) - if err != nil { - return nil, fmt.Errorf("read current local network map: %w", err) - } - - body := new(netmap.SnapshotResponseBody) - body.SetNetMap(&nm) - - resp := new(netmap.SnapshotResponse) - resp.SetBody(body) - - return resp, nil -} diff --git a/pkg/services/netmap/response.go b/pkg/services/netmap/response.go deleted file mode 100644 index 2e51b8262a..0000000000 --- a/pkg/services/netmap/response.go +++ /dev/null @@ -1,63 +0,0 @@ -package netmap - -import ( - "context" - - "github.com/nspcc-dev/neofs-api-go/v2/netmap" - "github.com/nspcc-dev/neofs-node/pkg/services/util" - "github.com/nspcc-dev/neofs-node/pkg/services/util/response" -) - -type responseService struct { - respSvc *response.Service - - svc Server -} - -// NewResponseService returns netmap service instance that passes internal service -// call to response service. -func NewResponseService(nmSvc Server, respSvc *response.Service) Server { - return &responseService{ - respSvc: respSvc, - svc: nmSvc, - } -} - -func (s *responseService) LocalNodeInfo(ctx context.Context, req *netmap.LocalNodeInfoRequest) (*netmap.LocalNodeInfoResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.LocalNodeInfo(ctx, req.(*netmap.LocalNodeInfoRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*netmap.LocalNodeInfoResponse), nil -} - -func (s *responseService) NetworkInfo(ctx context.Context, req *netmap.NetworkInfoRequest) (*netmap.NetworkInfoResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.NetworkInfo(ctx, req.(*netmap.NetworkInfoRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*netmap.NetworkInfoResponse), nil -} - -func (s *responseService) Snapshot(ctx context.Context, req *netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Snapshot(ctx, req.(*netmap.SnapshotRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*netmap.SnapshotResponse), nil -} diff --git a/pkg/services/netmap/server.go b/pkg/services/netmap/server.go index 68714a4a04..de5b070d23 100644 --- a/pkg/services/netmap/server.go +++ b/pkg/services/netmap/server.go @@ -2,73 +2,170 @@ package netmap import ( "context" + "crypto/ecdsa" - "github.com/nspcc-dev/neofs-api-go/v2/netmap" + apinetmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" protonetmap "github.com/nspcc-dev/neofs-api-go/v2/netmap/grpc" + apirefs "github.com/nspcc-dev/neofs-api-go/v2/refs" + refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + "github.com/nspcc-dev/neofs-api-go/v2/signature" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" + netmapcore "github.com/nspcc-dev/neofs-node/pkg/core/netmap" + "github.com/nspcc-dev/neofs-node/pkg/services/util" + "github.com/nspcc-dev/neofs-sdk-go/netmap" + "github.com/nspcc-dev/neofs-sdk-go/version" ) -// Server is an interface of the NeoFS API Netmap service server. -type Server interface { - LocalNodeInfo(context.Context, *netmap.LocalNodeInfoRequest) (*netmap.LocalNodeInfoResponse, error) - NetworkInfo(context.Context, *netmap.NetworkInfoRequest) (*netmap.NetworkInfoResponse, error) - Snapshot(context.Context, *netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) +// Contract groups ops of the Netmap contract deployed in the FS chain required +// to serve NeoFS API Netmap service. +type Contract interface { + netmapcore.State + // LocalNodeInfo returns local node's settings along with its network status. + LocalNodeInfo() (netmap.NodeInfo, error) + // GetNetworkInfo returns the current network configuration. + GetNetworkInfo() (netmap.NetworkInfo, error) + // GetNetworkMap current network map. + GetNetworkMap() (netmap.NetMap, error) } type server struct { protonetmap.UnimplementedNetmapServiceServer - srv Server + signer *ecdsa.PrivateKey + contract Contract } -// New returns protonetmap.NetmapServiceServer based on the Server. -func New(c Server) protonetmap.NetmapServiceServer { +// New provides protocontainer.NetmapServiceServer based on specified +// [Contract]. +// +// All response messages are signed using specified signer and have current +// epoch in the meta header. +func New(s *ecdsa.PrivateKey, c Contract) protonetmap.NetmapServiceServer { return &server{ - srv: c, + signer: s, + contract: c, } } -// LocalNodeInfo converts gRPC request message and passes it to internal netmap service. -func (s server) LocalNodeInfo( - ctx context.Context, - req *protonetmap.LocalNodeInfoRequest) (*protonetmap.LocalNodeInfoResponse, error) { - nodeInfoReq := new(netmap.LocalNodeInfoRequest) +func currentProtoVersion() *refs.Version { + v := version.Current() + var v2 apirefs.Version + v.WriteToV2(&v2) + return v2.ToGRPCMessage().(*refs.Version) +} + +func (s *server) makeResponseMetaHeader(st *protostatus.Status) *protosession.ResponseMetaHeader { + return &protosession.ResponseMetaHeader{ + Version: currentProtoVersion(), + Epoch: s.contract.CurrentEpoch(), + Status: st, + } +} + +func (s *server) makeNodeInfoResponse(body *protonetmap.LocalNodeInfoResponse_Body, st *protostatus.Status) (*protonetmap.LocalNodeInfoResponse, error) { + resp := &protonetmap.LocalNodeInfoResponse{ + Body: body, + MetaHeader: s.makeResponseMetaHeader(st), + } + return util.SignResponse(s.signer, resp, apinetmap.LocalNodeInfoResponse{}), nil +} + +func (s *server) makeStatusNodeInfoResponse(err error) (*protonetmap.LocalNodeInfoResponse, error) { + return s.makeNodeInfoResponse(nil, util.ToStatus(err)) +} + +// LocalNodeInfo returns current state of the local node from the underlying +// [NodeState]. +func (s server) LocalNodeInfo(_ context.Context, req *protonetmap.LocalNodeInfoRequest) (*protonetmap.LocalNodeInfoResponse, error) { + nodeInfoReq := new(apinetmap.LocalNodeInfoRequest) if err := nodeInfoReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(nodeInfoReq); err != nil { + return s.makeStatusNodeInfoResponse(util.ToRequestSignatureVerificationError(err)) + } - resp, err := s.srv.LocalNodeInfo(ctx, nodeInfoReq) + n, err := s.contract.LocalNodeInfo() if err != nil { - return nil, err + return s.makeStatusNodeInfoResponse(err) } - return resp.ToGRPCMessage().(*protonetmap.LocalNodeInfoResponse), nil + var n2 apinetmap.NodeInfo + n.WriteToV2(&n2) + body := &protonetmap.LocalNodeInfoResponse_Body{ + Version: currentProtoVersion(), + NodeInfo: n2.ToGRPCMessage().(*protonetmap.NodeInfo), + } + return s.makeNodeInfoResponse(body, util.StatusOK) } -// NetworkInfo converts gRPC request message and passes it to internal netmap service. -func (s *server) NetworkInfo(ctx context.Context, req *protonetmap.NetworkInfoRequest) (*protonetmap.NetworkInfoResponse, error) { - netInfoReq := new(netmap.NetworkInfoRequest) +func (s *server) makeNetInfoResponse(body *protonetmap.NetworkInfoResponse_Body, st *protostatus.Status) (*protonetmap.NetworkInfoResponse, error) { + resp := &protonetmap.NetworkInfoResponse{ + Body: body, + MetaHeader: s.makeResponseMetaHeader(st), + } + return util.SignResponse(s.signer, resp, apinetmap.NetworkInfoResponse{}), nil +} + +func (s *server) makeStatusNetInfoResponse(err error) (*protonetmap.NetworkInfoResponse, error) { + return s.makeNetInfoResponse(nil, util.ToStatus(err)) +} + +// NetworkInfo returns current network configuration from the underlying +// [Contract]. +func (s *server) NetworkInfo(_ context.Context, req *protonetmap.NetworkInfoRequest) (*protonetmap.NetworkInfoResponse, error) { + netInfoReq := new(apinetmap.NetworkInfoRequest) if err := netInfoReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(netInfoReq); err != nil { + return s.makeStatusNetInfoResponse(util.ToRequestSignatureVerificationError(err)) + } - resp, err := s.srv.NetworkInfo(ctx, netInfoReq) + n, err := s.contract.GetNetworkInfo() if err != nil { - return nil, err + return s.makeStatusNetInfoResponse(err) + } + + var n2 apinetmap.NetworkInfo + n.WriteToV2(&n2) + body := &protonetmap.NetworkInfoResponse_Body{ + NetworkInfo: n2.ToGRPCMessage().(*protonetmap.NetworkInfo), } + return s.makeNetInfoResponse(body, util.StatusOK) +} + +func (s *server) makeNetmapResponse(body *protonetmap.NetmapSnapshotResponse_Body, st *protostatus.Status) (*protonetmap.NetmapSnapshotResponse, error) { + resp := &protonetmap.NetmapSnapshotResponse{ + Body: body, + MetaHeader: s.makeResponseMetaHeader(st), + } + return util.SignResponse(s.signer, resp, apinetmap.SnapshotResponse{}), nil +} - return resp.ToGRPCMessage().(*protonetmap.NetworkInfoResponse), nil +func (s *server) makeStatusNetmapResponse(err error) (*protonetmap.NetmapSnapshotResponse, error) { + return s.makeNetmapResponse(nil, util.ToStatus(err)) } -// NetmapSnapshot converts gRPC request message and passes it to internal netmap service. -func (s *server) NetmapSnapshot(ctx context.Context, req *protonetmap.NetmapSnapshotRequest) (*protonetmap.NetmapSnapshotResponse, error) { - snapshotReq := new(netmap.SnapshotRequest) +// NetmapSnapshot returns current network map from the underlying [Contract]. +func (s *server) NetmapSnapshot(_ context.Context, req *protonetmap.NetmapSnapshotRequest) (*protonetmap.NetmapSnapshotResponse, error) { + snapshotReq := new(apinetmap.SnapshotRequest) if err := snapshotReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(snapshotReq); err != nil { + return s.makeStatusNetmapResponse(util.ToRequestSignatureVerificationError(err)) + } - resp, err := s.srv.Snapshot(ctx, snapshotReq) + n, err := s.contract.GetNetworkMap() if err != nil { - return nil, err + return s.makeStatusNetmapResponse(err) } - return resp.ToGRPCMessage().(*protonetmap.NetmapSnapshotResponse), nil + var n2 apinetmap.NetMap + n.WriteToV2(&n2) + body := &protonetmap.NetmapSnapshotResponse_Body{ + Netmap: n2.ToGRPCMessage().(*protonetmap.Netmap), + } + return s.makeNetmapResponse(body, util.StatusOK) } diff --git a/pkg/services/netmap/sign.go b/pkg/services/netmap/sign.go deleted file mode 100644 index 34c425a0b3..0000000000 --- a/pkg/services/netmap/sign.go +++ /dev/null @@ -1,72 +0,0 @@ -package netmap - -import ( - "context" - "crypto/ecdsa" - - "github.com/nspcc-dev/neofs-api-go/v2/netmap" - "github.com/nspcc-dev/neofs-node/pkg/services/util" -) - -type signService struct { - sigSvc *util.SignService - - svc Server -} - -func NewSignService(key *ecdsa.PrivateKey, svc Server) Server { - return &signService{ - sigSvc: util.NewUnarySignService(key), - svc: svc, - } -} - -func (s *signService) LocalNodeInfo( - ctx context.Context, - req *netmap.LocalNodeInfoRequest) (*netmap.LocalNodeInfoResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.LocalNodeInfo(ctx, req.(*netmap.LocalNodeInfoRequest)) - }, - func() util.ResponseMessage { - return new(netmap.LocalNodeInfoResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*netmap.LocalNodeInfoResponse), nil -} - -func (s *signService) NetworkInfo(ctx context.Context, req *netmap.NetworkInfoRequest) (*netmap.NetworkInfoResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.NetworkInfo(ctx, req.(*netmap.NetworkInfoRequest)) - }, - func() util.ResponseMessage { - return new(netmap.NetworkInfoResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*netmap.NetworkInfoResponse), nil -} - -func (s *signService) Snapshot(ctx context.Context, req *netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Snapshot(ctx, req.(*netmap.SnapshotRequest)) - }, - func() util.ResponseMessage { - return new(netmap.SnapshotResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*netmap.SnapshotResponse), nil -} diff --git a/pkg/services/object/util/key_test.go b/pkg/services/object/util/key_test.go index d106fcc339..cc41f5e741 100644 --- a/pkg/services/object/util/key_test.go +++ b/pkg/services/object/util/key_test.go @@ -1,8 +1,6 @@ package util_test import ( - "context" - "crypto/elliptic" "testing" "github.com/google/uuid" @@ -12,6 +10,7 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/services/object/util" tokenStorage "github.com/nspcc-dev/neofs-node/pkg/services/session/storage/temporary" neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" "github.com/nspcc-dev/neofs-sdk-go/session" "github.com/nspcc-dev/neofs-sdk-go/user" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" @@ -70,17 +69,13 @@ func createToken(t *testing.T, store *tokenStorage.TokenStore, owner user.ID, ex req.SetOwnerID(&ownerV2) req.SetExpiration(exp) - resp, err := store.Create(context.Background(), req) + key := neofscryptotest.ECDSAPrivateKey() + id := uuid.New() + err := store.Store(key, owner, id[:], exp) require.NoError(t, err) - pub, err := keys.NewPublicKeyFromBytes(resp.GetSessionKey(), elliptic.P256()) - require.NoError(t, err) - - var id uuid.UUID - require.NoError(t, id.UnmarshalBinary(resp.GetID())) - var tok session.Object - tok.SetAuthKey((*neofsecdsa.PublicKey)(pub)) + tok.SetAuthKey((*neofsecdsa.PublicKey)(&key.PublicKey)) tok.SetID(id) return tok diff --git a/pkg/services/reputation/rpc/response.go b/pkg/services/reputation/rpc/response.go deleted file mode 100644 index 97a4fecbf4..0000000000 --- a/pkg/services/reputation/rpc/response.go +++ /dev/null @@ -1,50 +0,0 @@ -package reputationrpc - -import ( - "context" - - "github.com/nspcc-dev/neofs-api-go/v2/reputation" - "github.com/nspcc-dev/neofs-node/pkg/services/util" - "github.com/nspcc-dev/neofs-node/pkg/services/util/response" -) - -type responseService struct { - respSvc *response.Service - - svc Server -} - -// NewResponseService returns reputation service server instance that passes -// internal service call to response service. -func NewResponseService(cnrSvc Server, respSvc *response.Service) Server { - return &responseService{ - respSvc: respSvc, - svc: cnrSvc, - } -} - -func (s *responseService) AnnounceLocalTrust(ctx context.Context, req *reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.AnnounceLocalTrust(ctx, req.(*reputation.AnnounceLocalTrustRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*reputation.AnnounceLocalTrustResponse), nil -} - -func (s *responseService) AnnounceIntermediateResult(ctx context.Context, req *reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.AnnounceIntermediateResult(ctx, req.(*reputation.AnnounceIntermediateResultRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*reputation.AnnounceIntermediateResultResponse), nil -} diff --git a/pkg/services/reputation/rpc/server.go b/pkg/services/reputation/rpc/server.go deleted file mode 100644 index c533c5bb0e..0000000000 --- a/pkg/services/reputation/rpc/server.go +++ /dev/null @@ -1,54 +0,0 @@ -package reputationrpc - -import ( - "context" - - "github.com/nspcc-dev/neofs-api-go/v2/reputation" - protoreputation "github.com/nspcc-dev/neofs-api-go/v2/reputation/grpc" -) - -// Server is an interface of the NeoFS API v2 Reputation service server. -type Server interface { - AnnounceLocalTrust(context.Context, *reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) - AnnounceIntermediateResult(context.Context, *reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) -} - -type server struct { - protoreputation.ReputationServiceServer - srv Server -} - -// New returns protoreputation.ReputationServiceServer based on the Server. -func New(srv Server) protoreputation.ReputationServiceServer { - return &server{ - srv: srv, - } -} - -func (s *server) AnnounceLocalTrust(ctx context.Context, r *protoreputation.AnnounceLocalTrustRequest) (*protoreputation.AnnounceLocalTrustResponse, error) { - req := new(reputation.AnnounceLocalTrustRequest) - if err := req.FromGRPCMessage(r); err != nil { - return nil, err - } - - resp, err := s.srv.AnnounceLocalTrust(ctx, req) - if err != nil { - return nil, err - } - - return resp.ToGRPCMessage().(*protoreputation.AnnounceLocalTrustResponse), nil -} - -func (s *server) AnnounceIntermediateResult(ctx context.Context, r *protoreputation.AnnounceIntermediateResultRequest) (*protoreputation.AnnounceIntermediateResultResponse, error) { - req := new(reputation.AnnounceIntermediateResultRequest) - if err := req.FromGRPCMessage(r); err != nil { - return nil, err - } - - resp, err := s.srv.AnnounceIntermediateResult(ctx, req) - if err != nil { - return nil, err - } - - return resp.ToGRPCMessage().(*protoreputation.AnnounceIntermediateResultResponse), nil -} diff --git a/pkg/services/reputation/rpc/sign.go b/pkg/services/reputation/rpc/sign.go deleted file mode 100644 index afebb828f9..0000000000 --- a/pkg/services/reputation/rpc/sign.go +++ /dev/null @@ -1,54 +0,0 @@ -package reputationrpc - -import ( - "context" - "crypto/ecdsa" - - "github.com/nspcc-dev/neofs-api-go/v2/reputation" - "github.com/nspcc-dev/neofs-node/pkg/services/util" -) - -type signService struct { - sigSvc *util.SignService - - svc Server -} - -func NewSignService(key *ecdsa.PrivateKey, svc Server) Server { - return &signService{ - sigSvc: util.NewUnarySignService(key), - svc: svc, - } -} - -func (s *signService) AnnounceLocalTrust(ctx context.Context, req *reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.AnnounceLocalTrust(ctx, req.(*reputation.AnnounceLocalTrustRequest)) - }, - func() util.ResponseMessage { - return new(reputation.AnnounceLocalTrustResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*reputation.AnnounceLocalTrustResponse), nil -} - -func (s *signService) AnnounceIntermediateResult(ctx context.Context, req *reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.AnnounceIntermediateResult(ctx, req.(*reputation.AnnounceIntermediateResultRequest)) - }, - func() util.ResponseMessage { - return new(reputation.AnnounceIntermediateResultResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*reputation.AnnounceIntermediateResultResponse), nil -} diff --git a/pkg/services/session/executor.go b/pkg/services/session/executor.go deleted file mode 100644 index 06e3cbcf51..0000000000 --- a/pkg/services/session/executor.go +++ /dev/null @@ -1,44 +0,0 @@ -package session - -import ( - "context" - "fmt" - - "github.com/nspcc-dev/neofs-api-go/v2/session" - "go.uber.org/zap" -) - -type ServiceExecutor interface { - Create(context.Context, *session.CreateRequestBody) (*session.CreateResponseBody, error) -} - -type executorSvc struct { - exec ServiceExecutor - - log *zap.Logger -} - -// NewExecutionService wraps ServiceExecutor and returns Session Service interface. -func NewExecutionService(exec ServiceExecutor, l *zap.Logger) Server { - return &executorSvc{ - exec: exec, - log: l, - } -} - -func (s *executorSvc) Create(ctx context.Context, req *session.CreateRequest) (*session.CreateResponse, error) { - s.log.Debug("serving request...", - zap.String("component", "SessionService"), - zap.String("request", "Create"), - ) - - respBody, err := s.exec.Create(ctx, req.GetBody()) - if err != nil { - return nil, fmt.Errorf("could not execute Create request: %w", err) - } - - resp := new(session.CreateResponse) - resp.SetBody(respBody) - - return resp, nil -} diff --git a/pkg/services/session/response.go b/pkg/services/session/response.go deleted file mode 100644 index 3fed39849a..0000000000 --- a/pkg/services/session/response.go +++ /dev/null @@ -1,37 +0,0 @@ -package session - -import ( - "context" - - "github.com/nspcc-dev/neofs-api-go/v2/session" - "github.com/nspcc-dev/neofs-node/pkg/services/util" - "github.com/nspcc-dev/neofs-node/pkg/services/util/response" -) - -type responseService struct { - respSvc *response.Service - - svc Server -} - -// NewResponseService returns session service instance that passes internal service -// call to response service. -func NewResponseService(ssSvc Server, respSvc *response.Service) Server { - return &responseService{ - respSvc: respSvc, - svc: ssSvc, - } -} - -func (s *responseService) Create(ctx context.Context, req *session.CreateRequest) (*session.CreateResponse, error) { - resp, err := s.respSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Create(ctx, req.(*session.CreateRequest)) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*session.CreateResponse), nil -} diff --git a/pkg/services/session/server.go b/pkg/services/session/server.go index 3b58cdb5c3..f375f62cff 100644 --- a/pkg/services/session/server.go +++ b/pkg/services/session/server.go @@ -2,39 +2,110 @@ package session import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "fmt" - "github.com/nspcc-dev/neofs-api-go/v2/session" + "github.com/google/uuid" + apirefs "github.com/nspcc-dev/neofs-api-go/v2/refs" + refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + apisession "github.com/nspcc-dev/neofs-api-go/v2/session" protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + "github.com/nspcc-dev/neofs-api-go/v2/signature" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" + "github.com/nspcc-dev/neofs-node/pkg/core/netmap" + "github.com/nspcc-dev/neofs-node/pkg/services/util" + 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/user" + "github.com/nspcc-dev/neofs-sdk-go/version" ) -// Server is an interface of the NeoFS API Session service server. -type Server interface { - Create(context.Context, *session.CreateRequest) (*session.CreateResponse, error) +// KeyStorage represents private keys stored on the local node side. +type KeyStorage interface { + // Store saves given private key by specified user and locally unique IDs along + // with its expiration time. + Store(key ecdsa.PrivateKey, _ user.ID, id []byte, exp uint64) error } type server struct { protosession.UnimplementedSessionServiceServer - srv Server + signer *ecdsa.PrivateKey + net netmap.State + keys KeyStorage } -// New returns protosession.SessionServiceServer based on the Server. -func New(c Server) protosession.SessionServiceServer { +// New provides protosession.SessionServiceServer based on specified [KeyStorage]. +// +// All response messages are signed using specified signer and have current +// epoch in the meta header. +func New(s *ecdsa.PrivateKey, net netmap.State, ks KeyStorage) protosession.SessionServiceServer { return &server{ - srv: c, + signer: s, + net: net, + keys: ks, } } -// Create converts gRPC CreateRequest message and passes it to internal Session service. -func (s *server) Create(ctx context.Context, req *protosession.CreateRequest) (*protosession.CreateResponse, error) { - createReq := new(session.CreateRequest) +func (s *server) makeCreateResponse(body *protosession.CreateResponse_Body, st *protostatus.Status) (*protosession.CreateResponse, error) { + v := version.Current() + var v2 apirefs.Version + v.WriteToV2(&v2) + resp := &protosession.CreateResponse{ + Body: body, + MetaHeader: &protosession.ResponseMetaHeader{ + Version: v2.ToGRPCMessage().(*refs.Version), + Epoch: s.net.CurrentEpoch(), + Status: st, + }, + } + return util.SignResponse(s.signer, resp, apisession.CreateResponse{}), nil +} + +func (s *server) makeFailedCreateResponse(err error) (*protosession.CreateResponse, error) { + return s.makeCreateResponse(nil, util.ToStatus(err)) +} + +// Create generates new private session key and saves it in the underlying +// [KeyStorage]. +func (s *server) Create(_ context.Context, req *protosession.CreateRequest) (*protosession.CreateResponse, error) { + createReq := new(apisession.CreateRequest) if err := createReq.FromGRPCMessage(req); err != nil { return nil, err } + if err := signature.VerifyServiceMessage(createReq); err != nil { + return s.makeFailedCreateResponse(err) + } + + reqBody := req.GetBody() + mUsr := reqBody.GetOwnerId() + if mUsr == nil { + return s.makeFailedCreateResponse(errors.New("missing account")) + } + var usr2 apirefs.OwnerID + if err := usr2.FromGRPCMessage(mUsr); err != nil { + panic(err) + } + var usr user.ID + if err := usr.ReadFromV2(usr2); err != nil { + return s.makeFailedCreateResponse(fmt.Errorf("invalid account: %w", err)) + } - resp, err := s.srv.Create(ctx, createReq) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return nil, err + return s.makeFailedCreateResponse(fmt.Errorf("generate private key: %w", err)) } - return resp.ToGRPCMessage().(*protosession.CreateResponse), nil + uid := uuid.New() + if err := s.keys.Store(*key, usr, uid[:], reqBody.Expiration); err != nil { + return s.makeFailedCreateResponse(fmt.Errorf("store private key locally: %w", err)) + } + + body := &protosession.CreateResponse_Body{ + Id: uid[:], + SessionKey: neofscrypto.PublicKeyBytes((*neofsecdsa.PublicKey)(&key.PublicKey)), + } + return s.makeCreateResponse(body, util.StatusOK) } diff --git a/pkg/services/session/sign.go b/pkg/services/session/sign.go deleted file mode 100644 index 4ff51a7931..0000000000 --- a/pkg/services/session/sign.go +++ /dev/null @@ -1,38 +0,0 @@ -package session - -import ( - "context" - "crypto/ecdsa" - - "github.com/nspcc-dev/neofs-api-go/v2/session" - "github.com/nspcc-dev/neofs-node/pkg/services/util" -) - -type signService struct { - sigSvc *util.SignService - - svc Server -} - -func NewSignService(key *ecdsa.PrivateKey, svc Server) Server { - return &signService{ - sigSvc: util.NewUnarySignService(key), - svc: svc, - } -} - -func (s *signService) Create(ctx context.Context, req *session.CreateRequest) (*session.CreateResponse, error) { - resp, err := s.sigSvc.HandleUnaryRequest(ctx, req, - func(ctx context.Context, req any) (util.ResponseMessage, error) { - return s.svc.Create(ctx, req.(*session.CreateRequest)) - }, - func() util.ResponseMessage { - return new(session.CreateResponse) - }, - ) - if err != nil { - return nil, err - } - - return resp.(*session.CreateResponse), nil -} diff --git a/pkg/services/session/storage/persistent/executor.go b/pkg/services/session/storage/persistent/executor.go index 331995c058..59c350a1f2 100644 --- a/pkg/services/session/storage/persistent/executor.go +++ b/pkg/services/session/storage/persistent/executor.go @@ -1,60 +1,31 @@ package persistent import ( - "context" - "errors" + "crypto/ecdsa" "fmt" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neofs-api-go/v2/session" - "github.com/nspcc-dev/neofs-node/pkg/services/session/storage" "github.com/nspcc-dev/neofs-sdk-go/user" "go.etcd.io/bbolt" ) -// Create inits a new private session token using information -// from corresponding request, saves it to bolt database (and -// encrypts private keys if storage has been configured so). -// Returns response that is filled with just created token's -// ID and public key for it. -func (s *TokenStore) Create(_ context.Context, body *session.CreateRequestBody) (*session.CreateResponseBody, error) { - idV2 := body.GetOwnerID() - if idV2 == nil { - return nil, errors.New("missing owner") - } - - var id user.ID - - err := id.ReadFromV2(*idV2) +// Store saves parameterized private key in the underlying Bolt database. +// Private keys are encrypted if TokenStore has been configured to. +func (s *TokenStore) Store(sk ecdsa.PrivateKey, usr user.ID, id []byte, exp uint64) error { + value, err := s.packToken(exp, &sk) if err != nil { - return nil, fmt.Errorf("invalid owner: %w", err) - } - - uidBytes, err := storage.NewTokenID() - if err != nil { - return nil, fmt.Errorf("could not generate token ID: %w", err) - } - - sk, err := keys.NewPrivateKey() - if err != nil { - return nil, err - } - - value, err := s.packToken(body.GetExpiration(), &sk.PrivateKey) - if err != nil { - return nil, err + return err } err = s.db.Update(func(tx *bbolt.Tx) error { rootBucket := tx.Bucket(sessionsBucket) - ownerBucket, err := rootBucket.CreateBucketIfNotExists(id[:]) + ownerBucket, err := rootBucket.CreateBucketIfNotExists(usr[:]) if err != nil { return fmt.Errorf( - "could not get/create %s owner bucket: %w", id, err) + "could not get/create %s owner bucket: %w", usr, err) } - err = ownerBucket.Put(uidBytes, value) + err = ownerBucket.Put(id, value) if err != nil { return fmt.Errorf("could not put session token for %s oid: %w", id, err) } @@ -62,12 +33,8 @@ func (s *TokenStore) Create(_ context.Context, body *session.CreateRequestBody) return nil }) if err != nil { - return nil, fmt.Errorf("could not save token to persistent storage: %w", err) + return fmt.Errorf("could not save token to persistent storage: %w", err) } - res := new(session.CreateResponseBody) - res.SetID(uidBytes) - res.SetSessionKey(sk.PublicKey().Bytes()) - - return res, nil + return nil } diff --git a/pkg/services/session/storage/persistent/executor_test.go b/pkg/services/session/storage/persistent/executor_test.go index 0abd39db4c..e327bdf2a3 100644 --- a/pkg/services/session/storage/persistent/executor_test.go +++ b/pkg/services/session/storage/persistent/executor_test.go @@ -2,15 +2,12 @@ package persistent import ( "bytes" - "context" "crypto/ecdsa" - "crypto/elliptic" + "crypto/rand" "path/filepath" "testing" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/session" + "github.com/nspcc-dev/neofs-sdk-go/user" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" @@ -22,41 +19,37 @@ func TestTokenStore(t *testing.T) { defer ts.Close() - owner := usertest.ID() - - var ownerV2 refs.OwnerID - owner.WriteToV2(&ownerV2) - - req := new(session.CreateRequestBody) - req.SetOwnerID(&ownerV2) - const tokenNumber = 5 type tok struct { - id []byte - key []byte + owner user.ID + id []byte + key ecdsa.PrivateKey } tokens := make([]tok, 0, tokenNumber) for i := range tokenNumber { - req.SetExpiration(uint64(i)) + usr := usertest.User() + sessionID := make([]byte, 32) // any len + _, _ = rand.Read(sessionID) - res, err := ts.Create(context.Background(), req) + err := ts.Store(usr.ECDSAPrivateKey, usr.ID, sessionID, uint64(i)) require.NoError(t, err) tokens = append(tokens, tok{ - id: res.GetID(), - key: res.GetSessionKey(), + owner: usr.ID, + id: sessionID, + key: usr.ECDSAPrivateKey, }) } for i, token := range tokens { - savedToken := ts.Get(owner, token.id) + savedToken := ts.Get(token.owner, token.id) require.Equal(t, uint64(i), savedToken.ExpiredAt()) - - equalKeys(t, token.key, savedToken.SessionKey()) + require.NotNil(t, savedToken.SessionKey()) + require.Equal(t, token.key, *savedToken.SessionKey()) } } @@ -66,23 +59,13 @@ func TestTokenStore_Persistent(t *testing.T) { ts, err := NewTokenStore(path) require.NoError(t, err) - idOwner := usertest.ID() - - var idOwnerV2 refs.OwnerID - idOwner.WriteToV2(&idOwnerV2) - + sessionID := make([]byte, 64) // any len + owner := usertest.User() const exp = 12345 - req := new(session.CreateRequestBody) - req.SetOwnerID(&idOwnerV2) - req.SetExpiration(exp) - - res, err := ts.Create(context.Background(), req) + err = ts.Store(owner.ECDSAPrivateKey, owner.ID, sessionID, exp) require.NoError(t, err) - id := res.GetID() - pubKey := res.GetSessionKey() - // close db (stop the node) require.NoError(t, ts.Close()) @@ -92,15 +75,19 @@ func TestTokenStore_Persistent(t *testing.T) { defer ts.Close() - savedToken := ts.Get(idOwner, id) + savedToken := ts.Get(owner.ID, sessionID) - equalKeys(t, pubKey, savedToken.SessionKey()) + require.EqualValues(t, exp, savedToken.ExpiredAt()) + require.NotNil(t, savedToken.SessionKey()) + require.Equal(t, owner.ECDSAPrivateKey, *savedToken.SessionKey()) } func TestTokenStore_RemoveOld(t *testing.T) { tests := []*struct { - epoch uint64 - id, key []byte + epoch uint64 + owner user.ID + id []byte + key ecdsa.PrivateKey }{ { epoch: 1, @@ -127,22 +114,16 @@ func TestTokenStore_RemoveOld(t *testing.T) { defer ts.Close() - owner := usertest.ID() - - var ownerV2 refs.OwnerID - owner.WriteToV2(&ownerV2) - - req := new(session.CreateRequestBody) - req.SetOwnerID(&ownerV2) - for _, test := range tests { - req.SetExpiration(test.epoch) + test.id = make([]byte, 32) // any len + _, _ = rand.Read(test.id) + owner := usertest.User() - res, err := ts.Create(context.Background(), req) + err := ts.Store(owner.ECDSAPrivateKey, owner.ID, test.id, test.epoch) require.NoError(t, err) - test.id = res.GetID() - test.key = res.GetSessionKey() + test.owner = owner.ID + test.key = owner.ECDSAPrivateKey } const currEpoch = 3 @@ -150,12 +131,14 @@ func TestTokenStore_RemoveOld(t *testing.T) { ts.RemoveOld(currEpoch) for _, test := range tests { - token := ts.Get(owner, test.id) + token := ts.Get(test.owner, test.id) if test.epoch <= currEpoch { require.Nil(t, token) } else { - equalKeys(t, test.key, token.SessionKey()) + require.EqualValues(t, test.epoch, token.ExpiredAt()) + require.NotNil(t, token.SessionKey()) + require.Equal(t, test.key, *token.SessionKey()) } } } @@ -221,12 +204,3 @@ func TestBolt_Cursor(t *testing.T) { t.Fatal("unexpectedly skipped '2' value") } } - -func equalKeys(t *testing.T, sessionKey []byte, savedPrivateKey *ecdsa.PrivateKey) { - returnedPubKey, err := keys.NewPublicKeyFromBytes(sessionKey, elliptic.P256()) - require.NoError(t, err) - - savedPubKey := (keys.PublicKey)(savedPrivateKey.PublicKey) - - require.Equal(t, true, returnedPubKey.Equal(&savedPubKey)) -} diff --git a/pkg/services/session/storage/temporary/executor.go b/pkg/services/session/storage/temporary/executor.go index 3f207baffb..5d65a550ee 100644 --- a/pkg/services/session/storage/temporary/executor.go +++ b/pkg/services/session/storage/temporary/executor.go @@ -1,52 +1,22 @@ package temporary import ( - "context" - "errors" - "fmt" + "crypto/ecdsa" "github.com/mr-tron/base58" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-node/pkg/services/session/storage" "github.com/nspcc-dev/neofs-sdk-go/user" ) -func (s *TokenStore) Create(_ context.Context, body *session.CreateRequestBody) (*session.CreateResponseBody, error) { - idV2 := body.GetOwnerID() - if idV2 == nil { - return nil, errors.New("missing owner") - } - - var id user.ID - - err := id.ReadFromV2(*idV2) - if err != nil { - return nil, fmt.Errorf("invalid owner: %w", err) - } - - uidBytes, err := storage.NewTokenID() - if err != nil { - return nil, fmt.Errorf("could not generate token ID: %w", err) - } - - sk, err := keys.NewPrivateKey() - if err != nil { - return nil, err - } - +// Store saves parameterized private key in-memory. +func (s *TokenStore) Store(sk ecdsa.PrivateKey, usr user.ID, id []byte, exp uint64) error { s.mtx.Lock() s.tokens[key{ - tokenID: base58.Encode(uidBytes), - ownerID: base58.Encode(id[:]), - }] = storage.NewPrivateToken(&sk.PrivateKey, body.GetExpiration()) + tokenID: base58.Encode(id), + ownerID: base58.Encode(usr[:]), + }] = storage.NewPrivateToken(&sk, exp) s.mtx.Unlock() - - res := new(session.CreateResponseBody) - res.SetID(uidBytes) - res.SetSessionKey(sk.PublicKey().Bytes()) - - return res, nil + return nil } func (s *TokenStore) Close() error { diff --git a/pkg/services/util/sign.go b/pkg/services/util/sign.go index d3f1b8dbe3..d135647b4b 100644 --- a/pkg/services/util/sign.go +++ b/pkg/services/util/sign.go @@ -6,9 +6,13 @@ import ( "errors" "fmt" + "github.com/nspcc-dev/neofs-api-go/v2/rpc/grpc" "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-api-go/v2/signature" + "github.com/nspcc-dev/neofs-api-go/v2/status" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" + "google.golang.org/protobuf/proto" ) type RequestMessage interface { @@ -154,6 +158,23 @@ func (s *SignService) HandleServerStreamRequest( return nil } +func SignResponse[R proto.Message, RV2 any, RV2PTR interface { + *RV2 + ToGRPCMessage() grpc.Message + FromGRPCMessage(message grpc.Message) error +}](signer *ecdsa.PrivateKey, r R, _ RV2) R { + r2 := RV2PTR(new(RV2)) + if err := r2.FromGRPCMessage(r); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + if err := signature.SignServiceMessage(signer, r2); err != nil { + // We can't pass this error as NeoFS status code since response will be unsigned. + // Isn't expected in practice, so panic is ok here. + panic(err) + } + return r2.ToGRPCMessage().(R) +} + func (s *SignService) HandleUnaryRequest(ctx context.Context, req any, handler UnaryHandler, blankResp ResponseConstructor) (ResponseMessage, error) { var ( resp ResponseMessage @@ -162,10 +183,7 @@ func (s *SignService) HandleUnaryRequest(ctx context.Context, req any, handler U // verify request signatures if err = signature.VerifyServiceMessage(req); err != nil { - var sigErr apistatus.SignatureVerification - sigErr.SetMessage(err.Error()) - - err = sigErr + err = ToRequestSignatureVerificationError(err) } else { // process request resp, err = handler(ctx, req) @@ -188,10 +206,36 @@ func (s *SignService) HandleUnaryRequest(ctx context.Context, req any, handler U } func setStatusV2(resp ResponseMessage, err error) { + session.SetStatus(resp, statusFromErr(err)) +} + +func statusFromErr(err error) *status.Status { // unwrap error for e := errors.Unwrap(err); e != nil; e = errors.Unwrap(err) { err = e } + return apistatus.ErrorToV2(err) +} + +var ( + // StatusOK is a missing response status field meaning OK in NeoFS protocol. It + // allows to make code more clear instead of passing nil. + StatusOK *protostatus.Status + // StatusOKErr is an error corresponding to [StatusOK]. It allows to make code + // more clear instead of passing nil. + StatusOKErr error +) + +// ToStatus unwraps the deepest error from err and converts it into the response +// status. +func ToStatus(err error) *protostatus.Status { + return statusFromErr(err).ToGRPCMessage().(*protostatus.Status) +} - session.SetStatus(resp, apistatus.ErrorToV2(err)) +// ToRequestSignatureVerificationError constructs status error describing +// request signature verification failure with the given cause. +func ToRequestSignatureVerificationError(cause error) error { + var err apistatus.SignatureVerification + err.SetMessage(cause.Error()) + return err }