diff --git a/CHANGELOG.md b/CHANGELOG.md index a62cb60069..713bcbf57a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ Changelog for NeoFS Node - SN can respond with `CONTAINER_LOCKED` status now (#3708) - `session create-v2` cli command to create new session token v2 (#3750) - SN now support raw GET/HEAD/RANGE requests in EC containers (#3756) +- IR now serves `setAttribute` and `removeAttribute` methods of Container contract (#3733) +- SN now serves `ContainerService`'s `SetAttribute` and `RemoveAttribute` RPC (#3733) +- CLI `set-attribute` and `remove-attribute commands to `container` section (#3733) ### Fixed - IR panics at graceful shutdown (#3706) @@ -44,9 +47,10 @@ Changelog for NeoFS Node - Graveyard from metabase (#3744) ### Updated -- `github.com/nspcc-dev/neofs-contract` module to `v0.25.2-0.20251219150129-498a820b9d6b` (#3670, #3746) -- `github.com/nspcc-dev/neofs-sdk-go` module to `v1.0.0-rc.16.0.20251222201515-923817fd7d13` (#3711, #3750) +- `github.com/nspcc-dev/neofs-contract` module to `v0.25.2-0.20251223162726-c0cf83ca5e42` (#3670, #3746, #3733) +- `github.com/nspcc-dev/neofs-sdk-go` module to `v1.0.0-rc.16.0.20251224112927-a50d7e9c925a` (#3711, #3750, #3733) - `github.com/nspcc-dev/locode-db` module to `v0.8.2` (#3729) +- `github.com/nspcc-dev/neo-go` module to `v0.114.1-0.20251222145711-e174185e133e` (#3733) ### Updating from v0.50.2 Please remove the following deprecated configuration options from IR config: diff --git a/cmd/neofs-cli/modules/container/attributes.go b/cmd/neofs-cli/modules/container/attributes.go new file mode 100644 index 0000000000..403fbd335e --- /dev/null +++ b/cmd/neofs-cli/modules/container/attributes.go @@ -0,0 +1,231 @@ +package container + +import ( + "errors" + "fmt" + "time" + + internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/common" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/key" + "github.com/nspcc-dev/neofs-sdk-go/client" + apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" + "github.com/spf13/cobra" +) + +// Set attribute command flags. +const ( + setAttributeNameFlag = "attribute" + setAttributeValueFlag = "value" + setAttributeValidForFlag = "valid-for" +) + +// Set attribute command defaults. +const ( + defaultSetAttributeValidFor = time.Minute +) + +var setAttributeFlagVars struct { + id string + attribute string + value string + validFor time.Duration +} + +var setAttributeCmd = &cobra.Command{ + Use: "set-attribute", + Short: "Set attribute for container", + Long: "Set attribute for container", + Args: cobra.NoArgs, + RunE: setAttribute, +} + +func initSetAttributeCmd() { + commonflags.Init(setAttributeCmd) + + flags := setAttributeCmd.Flags() + flags.StringVar(&setAttributeFlagVars.id, commonflags.CIDFlag, "", commonflags.CIDFlagUsage) + flags.StringVar(&setAttributeFlagVars.attribute, setAttributeNameFlag, "", "attribute to be set") + flags.StringVar(&setAttributeFlagVars.value, setAttributeValueFlag, "", "value for the attribute") + flags.DurationVar(&setAttributeFlagVars.validFor, setAttributeValidForFlag, defaultSetAttributeValidFor, "request validity duration") + + for _, f := range []string{ + commonflags.CIDFlag, + setAttributeNameFlag, + setAttributeValueFlag, + } { + if err := setAttributeCmd.MarkFlagRequired(f); err != nil { + panic(fmt.Sprintf("failed to mark flag %s required: %v", f, err)) + } + } +} + +func setAttribute(cmd *cobra.Command, _ []string) error { + if setAttributeFlagVars.validFor <= 0 { + return fmt.Errorf("non-positive request validity duration %d", setAttributeFlagVars.validFor) + } + + id, err := cid.DecodeString(setAttributeFlagVars.id) + if err != nil { + return fmt.Errorf("invalid container ID: %w", err) + } + + sessionToken, err := getSession(cmd) + if err != nil { + return err + } + + pk, err := key.Get(cmd) + if err != nil { + return err + } + signer := (*neofsecdsa.SignerRFC6979)(pk) + + ctx, cancel := getAwaitContext(cmd) + defer cancel() + + cli, err := internalclient.GetSDKClientByFlag(ctx, commonflags.RPC) + if err != nil { + return err + } + defer cli.Close() + + prm := client.SetContainerAttributeParameters{ + ID: id, + Attribute: setAttributeFlagVars.attribute, + Value: setAttributeFlagVars.value, + ValidUntil: time.Now().Add(setAttributeFlagVars.validFor), + } + + signedPrm := client.GetSignedSetContainerAttributeParameters(prm) + + var prmSig neofscrypto.Signature + if err := prmSig.Calculate(signer, signedPrm); err != nil { + return fmt.Errorf("failed to sign request parameters: %w", err) + } + + var opts client.SetContainerAttributeOptions + if sessionToken != nil { + opts.AttachSessionTokenV1(*sessionToken) + } + + err = cli.SetContainerAttribute(ctx, prm, prmSig, opts) + if err != nil { + if errors.Is(err, apistatus.ErrContainerAwaitTimeout) { + err = common.ErrAwaitTimeout + } + return fmt.Errorf("client error: %w", err) + } + + cmd.Println("Attribute successfully set.") + + return nil +} + +// Remove attribute command flags. +const ( + removeAttributeNameFlag = "attribute" + removeAttributeValidForFlag = "valid-for" +) + +// Remove attribute command defaults. +const ( + defaultRemoveAttributeValidFor = time.Minute +) + +var removeAttributeFlagVars struct { + id string + attribute string + validFor time.Duration +} + +var removeAttributeCmd = &cobra.Command{ + Use: "remove-attribute", + Short: "Remove container attribute", + Long: "Remove container attribute", + Args: cobra.NoArgs, + RunE: removeAttribute, +} + +func initRemoveAttributeCmd() { + commonflags.Init(removeAttributeCmd) + + flags := removeAttributeCmd.Flags() + flags.StringVar(&removeAttributeFlagVars.id, commonflags.CIDFlag, "", commonflags.CIDFlagUsage) + flags.StringVar(&removeAttributeFlagVars.attribute, removeAttributeNameFlag, "", "attribute to be set") + flags.DurationVar(&removeAttributeFlagVars.validFor, removeAttributeValidForFlag, defaultRemoveAttributeValidFor, "request validity duration") + + for _, f := range []string{ + commonflags.CIDFlag, + removeAttributeNameFlag, + } { + if err := removeAttributeCmd.MarkFlagRequired(f); err != nil { + panic(fmt.Sprintf("failed to mark flag %s required: %v", f, err)) + } + } +} + +func removeAttribute(cmd *cobra.Command, _ []string) error { + if removeAttributeFlagVars.validFor <= 0 { + return fmt.Errorf("non-positive request validity duration %d", removeAttributeFlagVars.validFor) + } + + id, err := cid.DecodeString(removeAttributeFlagVars.id) + if err != nil { + return fmt.Errorf("invalid container ID: %w", err) + } + + sessionToken, err := getSession(cmd) + if err != nil { + return err + } + + pk, err := key.Get(cmd) + if err != nil { + return err + } + signer := (*neofsecdsa.SignerRFC6979)(pk) + + ctx, cancel := getAwaitContext(cmd) + defer cancel() + + cli, err := internalclient.GetSDKClientByFlag(ctx, commonflags.RPC) + if err != nil { + return err + } + defer cli.Close() + + prm := client.RemoveContainerAttributeParameters{ + ID: id, + Attribute: removeAttributeFlagVars.attribute, + ValidUntil: time.Now().Add(removeAttributeFlagVars.validFor), + } + + signedPrm := client.GetSignedRemoveContainerAttributeParameters(prm) + + var prmSig neofscrypto.Signature + if err := prmSig.Calculate(signer, signedPrm); err != nil { + return fmt.Errorf("failed to sign request parameters: %w", err) + } + + var opts client.RemoveContainerAttributeOptions + if sessionToken != nil { + opts.AttachSessionTokenV1(*sessionToken) + } + + err = cli.RemoveContainerAttribute(ctx, prm, prmSig, opts) + if err != nil { + if errors.Is(err, apistatus.ErrContainerAwaitTimeout) { + err = common.ErrAwaitTimeout + } + return fmt.Errorf("client error: %w", err) + } + + cmd.Println("Attribute successfully removed.") + + return nil +} diff --git a/cmd/neofs-cli/modules/container/root.go b/cmd/neofs-cli/modules/container/root.go index 14225f367e..d5d0852817 100644 --- a/cmd/neofs-cli/modules/container/root.go +++ b/cmd/neofs-cli/modules/container/root.go @@ -28,6 +28,8 @@ func init() { getExtendedACLCmd, setExtendedACLCmd, containerNodesCmd, + setAttributeCmd, + removeAttributeCmd, } Cmd.AddCommand(containerChildCommand...) @@ -40,6 +42,8 @@ func init() { initContainerGetEACLCmd() initContainerSetEACLCmd() initContainerNodesCmd() + initSetAttributeCmd() + initRemoveAttributeCmd() for _, containerCommand := range containerChildCommand { commonflags.InitAPI(containerCommand) @@ -52,6 +56,8 @@ func init() { {createContainerCmd, "PUT"}, {deleteContainerCmd, "DELETE"}, {setExtendedACLCmd, "SETEACL"}, + {setAttributeCmd, "SETATTRIBUTE"}, + {removeAttributeCmd, "REMOVEATTRIBUTE"}, } { commonflags.InitSession(el.cmd, "container "+el.verb) } diff --git a/cmd/neofs-node/container.go b/cmd/neofs-node/container.go index 4cef838c9c..314b787c92 100644 --- a/cmd/neofs-node/container.go +++ b/cmd/neofs-node/container.go @@ -577,6 +577,24 @@ func (x *containersInChain) PutEACL(ctx context.Context, eACL eacl.Table, pub, s return err } +func (x *containersInChain) SetAttribute(ctx context.Context, cnr cid.ID, attr, val string, validUntil uint64, pub, sig, sessionToken []byte) error { + err := x.cCli.SetAttribute(ctx, cnr, attr, val, validUntil, pub, sig, sessionToken) + if errors.Is(err, client.ErrTxAwaitTimeout) { + err = apistatus.ErrContainerAwaitTimeout + } + + return err +} + +func (x *containersInChain) RemoveAttribute(ctx context.Context, cnr cid.ID, attr string, validUntil uint64, pub, sig, sessionToken []byte) error { + err := x.cCli.RemoveAttribute(ctx, cnr, attr, validUntil, pub, sig, sessionToken) + if errors.Is(err, client.ErrTxAwaitTimeout) { + err = apistatus.ErrContainerAwaitTimeout + } + + return err +} + type containerPresenceChecker struct{ src containerCore.Source } // Exists implements [meta.Containers]. diff --git a/docs/cli-commands/neofs-cli_container.md b/docs/cli-commands/neofs-cli_container.md index d3bfe461a8..10c752a96d 100644 --- a/docs/cli-commands/neofs-cli_container.md +++ b/docs/cli-commands/neofs-cli_container.md @@ -29,5 +29,7 @@ Operations with containers * [neofs-cli container list](neofs-cli_container_list.md) - List all created containers * [neofs-cli container list-objects](neofs-cli_container_list-objects.md) - List existing objects in container * [neofs-cli container nodes](neofs-cli_container_nodes.md) - Show nodes for container +* [neofs-cli container remove-attribute](neofs-cli_container_remove-attribute.md) - Remove container attribute +* [neofs-cli container set-attribute](neofs-cli_container_set-attribute.md) - Set attribute for container * [neofs-cli container set-eacl](neofs-cli_container_set-eacl.md) - Set new extended ACL table for container diff --git a/docs/cli-commands/neofs-cli_container_remove-attribute.md b/docs/cli-commands/neofs-cli_container_remove-attribute.md new file mode 100644 index 0000000000..6b483efaa3 --- /dev/null +++ b/docs/cli-commands/neofs-cli_container_remove-attribute.md @@ -0,0 +1,40 @@ +## neofs-cli container remove-attribute + +Remove container attribute + +### Synopsis + +Remove contaier attribute + +``` +neofs-cli container remove-attribute [flags] +``` + +### Options + +``` + --address string Address of wallet account + --attribute string attribute to be set + --cid string Container ID. + -g, --generate-key Generate new private key + -h, --help help for remove-attribute + -r, --rpc-endpoint string Remote node address (as 'multiaddr' or ':') + --session string Filepath to a JSON- or binary-encoded token of the container REMOVEATTRIBUTE session + -t, --timeout duration Timeout for the operation (default 15s) + --ttl uint32 TTL value in request meta header (default 2) + --valid-for duration request validity duration (default 1m0s) + -w, --wallet string Path to the wallet + -x, --xhdr strings Request X-Headers in form of Key=Value +``` + +### Options inherited from parent commands + +``` + -c, --config string Config file (default is $HOME/.config/neofs-cli/config.yaml) + -v, --verbose Verbose output +``` + +### SEE ALSO + +* [neofs-cli container](neofs-cli_container.md) - Operations with containers + diff --git a/docs/cli-commands/neofs-cli_container_set-attribute.md b/docs/cli-commands/neofs-cli_container_set-attribute.md new file mode 100644 index 0000000000..cb39470b8b --- /dev/null +++ b/docs/cli-commands/neofs-cli_container_set-attribute.md @@ -0,0 +1,41 @@ +## neofs-cli container set-attribute + +Set attribute for container + +### Synopsis + +Set attribute for container + +``` +neofs-cli container set-attribute [flags] +``` + +### Options + +``` + --address string Address of wallet account + --attribute string attribute to be set + --cid string Container ID. + -g, --generate-key Generate new private key + -h, --help help for set-attribute + -r, --rpc-endpoint string Remote node address (as 'multiaddr' or ':') + --session string Filepath to a JSON- or binary-encoded token of the container SETATTRIBUTE session + -t, --timeout duration Timeout for the operation (default 15s) + --ttl uint32 TTL value in request meta header (default 2) + --valid-for duration request validity duration (default 1m0s) + --value string value for the attribute + -w, --wallet string Path to the wallet + -x, --xhdr strings Request X-Headers in form of Key=Value +``` + +### Options inherited from parent commands + +``` + -c, --config string Config file (default is $HOME/.config/neofs-cli/config.yaml) + -v, --verbose Verbose output +``` + +### SEE ALSO + +* [neofs-cli container](neofs-cli_container.md) - Operations with containers + diff --git a/go.mod b/go.mod index d3b58612f9..0b519c2eb1 100644 --- a/go.mod +++ b/go.mod @@ -19,10 +19,10 @@ require ( github.com/nspcc-dev/bbolt v0.0.0-20250911202005-807225ebb0c8 github.com/nspcc-dev/hrw/v2 v2.0.4 github.com/nspcc-dev/locode-db v0.8.2 - github.com/nspcc-dev/neo-go v0.114.0 + github.com/nspcc-dev/neo-go v0.114.1-0.20251222145711-e174185e133e github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea - github.com/nspcc-dev/neofs-contract v0.25.2-0.20251219150129-498a820b9d6b - github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251222201515-923817fd7d13 + github.com/nspcc-dev/neofs-contract v0.25.2-0.20251223162726-c0cf83ca5e42 + github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251224112927-a50d7e9c925a github.com/nspcc-dev/tzhash v1.8.3 github.com/panjf2000/ants/v2 v2.11.3 github.com/prometheus/client_golang v1.23.2 @@ -70,7 +70,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nspcc-dev/dbft v0.4.0 // indirect github.com/nspcc-dev/go-ordered-json v0.0.0-20250911084817-6fb4472993d1 // indirect - github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251112080609-3c8e29c66609 // indirect + github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251217090505-857f951d81a9 // indirect github.com/nspcc-dev/rfc6979 v0.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect diff --git a/go.sum b/go.sum index 2455ba0569..5b1081056b 100644 --- a/go.sum +++ b/go.sum @@ -191,16 +191,16 @@ github.com/nspcc-dev/hrw/v2 v2.0.4 h1:o3Zh/2aF+IgGpvt414f46Ya20WG9u9vWxVd16ErFI8 github.com/nspcc-dev/hrw/v2 v2.0.4/go.mod h1:dUjOx27zTTvoPmT5EG25vSSWL2tKS7ndAa2TPTiZwFo= github.com/nspcc-dev/locode-db v0.8.2 h1:+9+1Z7ppG+ISDLHzMND7PZ8+R4H3d04doVRyNevOpz0= github.com/nspcc-dev/locode-db v0.8.2/go.mod h1:PtAASXSG4D4Oz0js9elzTyTr8GLpOJO20qFL881Nims= -github.com/nspcc-dev/neo-go v0.114.0 h1:JxyLGlQGtzrfWvhdrUa35BGzBaadwPtLdNL5ehfOF2k= -github.com/nspcc-dev/neo-go v0.114.0/go.mod h1:visra3tXvGBgBfhMizRGEB+bUI5a/zoeqr5WQRKXFGQ= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251112080609-3c8e29c66609 h1:9jH0IXFw8rjBgBVWSJbWeEHf7XDjANBnmEas49rdAH8= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251112080609-3c8e29c66609/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go v0.114.1-0.20251222145711-e174185e133e h1:mdGTZLXefDo3zIm42z+XFBbSK3cWU6rYxlyfq1Ir0B8= +github.com/nspcc-dev/neo-go v0.114.1-0.20251222145711-e174185e133e/go.mod h1:2klaZUCv0Ut+6d4GAO3w9NbtS1bOAX0Sqc10CvWjhHI= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251217090505-857f951d81a9 h1:5+Ue5+i72uJVfHoq1+6mc6KlpriaqQaO96LtaDEsTfg= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251217090505-857f951d81a9/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea h1:mK0EMGLvunXcFyq7fBURS/CsN4MH+4nlYiqn6pTwWAU= github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea/go.mod h1:YzhD4EZmC9Z/PNyd7ysC7WXgIgURc9uCG1UWDeV027Y= -github.com/nspcc-dev/neofs-contract v0.25.2-0.20251219150129-498a820b9d6b h1:E8yWtvW2DrCEsCKFfvVRrfYu88DezqL4Hh/GPcSJXQI= -github.com/nspcc-dev/neofs-contract v0.25.2-0.20251219150129-498a820b9d6b/go.mod h1:CYX51uP2pNBCK7Q0ygD1LNsoFSHbB2F5luaBrluFkUo= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251222201515-923817fd7d13 h1:iFnvA2FRBZoIWN9xevxY7ipiIk0UtTeTsr3YkW3GayQ= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251222201515-923817fd7d13/go.mod h1:IrM1JG/klBtecZEApIf8USgLonNcarv32R1O0dj4kQI= +github.com/nspcc-dev/neofs-contract v0.25.2-0.20251223162726-c0cf83ca5e42 h1:CiNxIrnQO9JeWg9knVOicxLp5g1Cnd0bv81C3WXXxC8= +github.com/nspcc-dev/neofs-contract v0.25.2-0.20251223162726-c0cf83ca5e42/go.mod h1:9ziQViIqszec1SRE+mJX8gbAJmXEAY1g0ZE3TGVZYTc= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251224112927-a50d7e9c925a h1:sJ/511OYy5QvetpM8VmtlJrTXOh7bVgeWWPw2AdIqHE= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251224112927-a50d7e9c925a/go.mod h1:IrM1JG/klBtecZEApIf8USgLonNcarv32R1O0dj4kQI= github.com/nspcc-dev/rfc6979 v0.2.4 h1:NBgsdCjhLpEPJZqmC9rciMZDcSY297po2smeaRjw57k= github.com/nspcc-dev/rfc6979 v0.2.4/go.mod h1:86ylDw6Kss+P6v4QAJqo1Sp3mC0/Zr9G97xSjQ9TuFg= github.com/nspcc-dev/tzhash v1.8.3 h1:EWJMOL/ppdqNBvkKjHECljusopcsNu4i4kH8KctTv10= diff --git a/pkg/innerring/processors/container/common.go b/pkg/innerring/processors/container/common.go index 725c5d9de3..9a245f6673 100644 --- a/pkg/innerring/processors/container/common.go +++ b/pkg/innerring/processors/container/common.go @@ -8,6 +8,7 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/morph/client" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/session" + sessionv2 "github.com/nspcc-dev/neofs-sdk-go/session/v2" "github.com/nspcc-dev/neofs-sdk-go/user" ) @@ -19,7 +20,8 @@ var ( type signatureVerificationData struct { ownerContainer user.ID - verb session.ContainerVerb + verb session.ContainerVerb + verbV2 sessionv2.Verb idContainerSet bool idContainer cid.ID @@ -50,6 +52,13 @@ func (cp *Processor) verifySignature(v signatureVerificationData) error { var err error if len(v.binTokenSession) > 0 { + var tokV2 sessionv2.Token + err = tokV2.Unmarshal(v.binTokenSession) + if err == nil { + // TODO + return errors.New("sessions V2 are not supported yet") + } + var tok session.Container err = tok.Unmarshal(v.binTokenSession) diff --git a/pkg/innerring/processors/container/handlers.go b/pkg/innerring/processors/container/handlers.go index 5bca0ecab1..3d95a5c98c 100644 --- a/pkg/innerring/processors/container/handlers.go +++ b/pkg/innerring/processors/container/handlers.go @@ -158,3 +158,33 @@ func (cp *Processor) handleObjectPut(ev event.Event) { cp.log.Warn("object pool submission failed", zap.Error(err)) } } + +func (cp *Processor) handleSetAttribute(ev event.Event) { + req := ev.(containerEvent.SetAttributeRequest) + + cp.log.Info("notification", + zap.String("type", "set attribute"), + zap.String("container", base58.Encode(req.ID)), + zap.String("attribute", req.Attribute)) + + err := cp.pool.Submit(func() { cp.processSetAttributeRequest(req) }) + if err != nil { + cp.log.Warn("container processor worker pool drained", + zap.Int("capacity", cp.pool.Cap())) + } +} + +func (cp *Processor) handleRemoveAttribute(ev event.Event) { + req := ev.(containerEvent.RemoveAttributeRequest) + + cp.log.Info("notification", + zap.String("type", "remove attribute"), + zap.String("container", base58.Encode(req.ID)), + zap.String("attribute", req.Attribute)) + + err := cp.pool.Submit(func() { cp.processRemoveAttributeRequest(req) }) + if err != nil { + cp.log.Warn("container processor worker pool drained", + zap.Int("capacity", cp.pool.Cap())) + } +} diff --git a/pkg/innerring/processors/container/process_container.go b/pkg/innerring/processors/container/process_container.go index 03caa053e9..2180d131ca 100644 --- a/pkg/innerring/processors/container/process_container.go +++ b/pkg/innerring/processors/container/process_container.go @@ -4,15 +4,18 @@ import ( "errors" "fmt" "strings" + "time" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/network/payload" cntClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/container" "github.com/nspcc-dev/neofs-node/pkg/morph/event" containerEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/container" + "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/session" + sessionv2 "github.com/nspcc-dev/neofs-sdk-go/session/v2" "go.uber.org/zap" ) @@ -252,6 +255,149 @@ func (cp *Processor) approveDeleteContainer(e containerEvent.RemoveContainerRequ } } +func (cp *Processor) processSetAttributeRequest(req containerEvent.SetAttributeRequest) { + if !cp.alphabetState.IsAlphabet() { + cp.log.Info("non alphabet mode, ignore attribute setting") + return + } + + id, err := cid.DecodeBytes(req.ID) + if err != nil { + cp.log.Error("attribute setting check failed", + zap.Error(fmt.Errorf("invalid container ID: %w", err))) + return + } + if id.IsZero() { + cp.log.Error("attribute setting check failed", + zap.Error(cid.ErrZero)) + return + } + + err = cp.checkSetAttributeRequest(req, id) + if err != nil { + cp.log.Error("attribute setting check failed", + zap.Stringer("container", id), zap.Error(err)) + return + } + + cp.approveSetAttributeRequest(req, id) +} + +func (cp *Processor) checkSetAttributeRequest(req containerEvent.SetAttributeRequest, id cid.ID) error { + now := time.Now() + if nowUnix := now.Unix(); nowUnix > req.ValidUntil { + return fmt.Errorf("request is valid until %d (%s), now %d (%s)", req.ValidUntil, time.Unix(req.ValidUntil, 0).UTC(), nowUnix, now.UTC()) + } + + cnr, err := cp.cnrClient.Get(req.ID) + if err != nil { + return fmt.Errorf("get container by ID: %w", err) + } + + signedData := client.GetSignedSetContainerAttributeParameters(client.SetContainerAttributeParameters{ + ID: id, + Attribute: req.Attribute, + Value: req.Value, + ValidUntil: time.Unix(req.ValidUntil, 0), + }) + + err = cp.verifySignature(signatureVerificationData{ + ownerContainer: cnr.Owner(), + verb: session.VerbContainerSetAttribute, + verbV2: sessionv2.VerbContainerSetAttribute, + idContainerSet: true, + idContainer: id, + verifScript: req.VerificationScript, + binTokenSession: req.SessionToken, + invocScript: req.InvocationScript, + signedData: signedData, + }) + if err != nil { + return fmt.Errorf("authenticate client: %w", err) + } + + return nil +} + +func (cp *Processor) approveSetAttributeRequest(req containerEvent.SetAttributeRequest, id cid.ID) { + err := cp.cnrClient.Morph().NotarySignAndInvokeTX(&req.MainTransaction, false) + if err != nil { + cp.log.Error("could not approve attribute setting", + zap.Stringer("container", id), zap.Error(err)) + } +} + +func (cp *Processor) processRemoveAttributeRequest(req containerEvent.RemoveAttributeRequest) { + if !cp.alphabetState.IsAlphabet() { + cp.log.Info("non alphabet mode, ignore attribute removal") + return + } + + id, err := cid.DecodeBytes(req.ID) + if err != nil { + cp.log.Error("attribute removal check failed", + zap.Error(fmt.Errorf("invalid container ID: %w", err))) + return + } + if id.IsZero() { + cp.log.Error("attribute removal check failed", + zap.Error(cid.ErrZero)) + return + } + + err = cp.checkRemoveAttributeRequest(req, id) + if err != nil { + cp.log.Error("attribute removal check failed", + zap.Stringer("container", id), zap.Error(err)) + return + } + + cp.approveRemoveAttributeRequest(req, id) +} + +func (cp *Processor) checkRemoveAttributeRequest(req containerEvent.RemoveAttributeRequest, id cid.ID) error { + now := time.Now() + if nowUnix := now.Unix(); nowUnix > req.ValidUntil { + return fmt.Errorf("request is valid until %d (%s), now %d (%s)", req.ValidUntil, time.Unix(req.ValidUntil, 0), nowUnix, now) + } + + cnr, err := cp.cnrClient.Get(req.ID) + if err != nil { + return fmt.Errorf("get container by ID: %w", err) + } + + signedData := client.GetSignedRemoveContainerAttributeParameters(client.RemoveContainerAttributeParameters{ + ID: id, + Attribute: req.Attribute, + ValidUntil: time.Unix(req.ValidUntil, 0), + }) + + err = cp.verifySignature(signatureVerificationData{ + ownerContainer: cnr.Owner(), + verb: session.VerbContainerRemoveAttribute, + verbV2: sessionv2.VerbContainerRemoveAttribute, + idContainerSet: true, + idContainer: id, + verifScript: req.VerificationScript, + binTokenSession: req.SessionToken, + invocScript: req.InvocationScript, + signedData: signedData, + }) + if err != nil { + return fmt.Errorf("authenticate client: %w", err) + } + + return nil +} + +func (cp *Processor) approveRemoveAttributeRequest(req containerEvent.RemoveAttributeRequest, id cid.ID) { + err := cp.cnrClient.Morph().NotarySignAndInvokeTX(&req.MainTransaction, false) + if err != nil { + cp.log.Error("could not approve attribute removal", + zap.Stringer("container", id), zap.Error(err)) + } +} + func checkNNS(cnr containerSDK.Container, name, zone string) error { // fetch domain info d := cnr.ReadDomain() diff --git a/pkg/innerring/processors/container/processor.go b/pkg/innerring/processors/container/processor.go index 4dcd2176b5..38f20553a7 100644 --- a/pkg/innerring/processors/container/processor.go +++ b/pkg/innerring/processors/container/processor.go @@ -180,6 +180,16 @@ func (cp *Processor) ListenerNotaryParsers() []event.NotaryParserInfo { p.SetParser(containerEvent.RestoreAddStructsRequest) pp = append(pp, p) + // set attribute + p.SetRequestType(fschaincontracts.SetContainerAttributeMethod) + p.SetParser(containerEvent.RestoreSetAttributeRequest) + pp = append(pp, p) + + // remove attribute + p.SetRequestType(fschaincontracts.RemoveContainerAttributeMethod) + p.SetParser(containerEvent.RestoreRemoveAttributeRequest) + pp = append(pp, p) + return pp } @@ -257,6 +267,16 @@ func (cp *Processor) ListenerNotaryHandlers() []event.NotaryHandlerInfo { }) hh = append(hh, h) + // set attribute + h.SetRequestType(fschaincontracts.SetContainerAttributeMethod) + h.SetHandler(cp.handleSetAttribute) + hh = append(hh, h) + + // remove attribute + h.SetRequestType(fschaincontracts.RemoveContainerAttributeMethod) + h.SetHandler(cp.handleRemoveAttribute) + hh = append(hh, h) + return hh } diff --git a/pkg/morph/client/container/attributes.go b/pkg/morph/client/container/attributes.go new file mode 100644 index 0000000000..9f2ba00d3f --- /dev/null +++ b/pkg/morph/client/container/attributes.go @@ -0,0 +1,52 @@ +package container + +import ( + "context" + "fmt" + "strings" + + containerrpc "github.com/nspcc-dev/neofs-contract/rpc/container" + fschaincontracts "github.com/nspcc-dev/neofs-node/pkg/morph/contracts" + apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" +) + +// SetAttribute calls Container contract to set container attribute with +// parameterized credentials. If transaction is accepted for processing, +// SetAttribute waits for it to be successfully executed. Waiting is done within +// ctx, [client.ErrTxAwaitTimeout] is returned when it is done. +// +// Returns [apistatus.ErrContainerNotFound] if requested container is missing. +// +// Returns any error encountered that caused the saving to interrupt. +func (c *Client) SetAttribute(ctx context.Context, cnr cid.ID, attr, val string, validUntil uint64, pub, sig, token []byte) error { + err := c.client.CallWithAlphabetWitness(ctx, fschaincontracts.SetContainerAttributeMethod, []any{ + cnr[:], attr, val, validUntil, sig, pub, token, + }) + if err != nil { + if strings.Contains(err.Error(), containerrpc.NotFoundError) { + return apistatus.ErrContainerNotFound + } + return fmt.Errorf("could not invoke method (%s): %w", fschaincontracts.SetContainerAttributeMethod, err) + } + return nil +} + +// RemoveAttribute calls Container contract to remove container attribute with +// parameterized credentials. If transaction is accepted for processing, +// RemoveAttribute waits for it to be successfully executed. Waiting is done +// within ctx, [client.ErrTxAwaitTimeout] is returned when it is done. +// +// Returns [apistatus.ErrContainerNotFound] if requested container is missing. +func (c *Client) RemoveAttribute(ctx context.Context, cnr cid.ID, attr string, validUntil uint64, pub, sig, token []byte) error { + err := c.client.CallWithAlphabetWitness(ctx, fschaincontracts.RemoveContainerAttributeMethod, []any{ + cnr[:], attr, validUntil, sig, pub, token, + }) + if err != nil { + if strings.Contains(err.Error(), containerrpc.NotFoundError) { + return apistatus.ErrContainerNotFound + } + return fmt.Errorf("could not invoke method (%s): %w", fschaincontracts.RemoveContainerAttributeMethod, err) + } + return nil +} diff --git a/pkg/morph/client/notary.go b/pkg/morph/client/notary.go index a867b332d8..ae4168a21d 100644 --- a/pkg/morph/client/notary.go +++ b/pkg/morph/client/notary.go @@ -24,8 +24,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" "github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter" sc "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/scparser" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neofs-node/pkg/util/rand" @@ -430,7 +430,7 @@ func (c *Client) NotarySignAndInvokeTX(mainTx *transaction.Transaction, await bo if len(script) == 0 { acc = notary.FakeContractAccount(mainTx.Signers[2].Account) } else { - pubBytes, ok := vm.ParseSignatureContract(script) + pubBytes, ok := scparser.ParseSignatureContract(script) if ok { pub, err := keys.NewPublicKeyFromBytes(pubBytes, elliptic.P256()) if err != nil { @@ -438,7 +438,7 @@ func (c *Client) NotarySignAndInvokeTX(mainTx *transaction.Transaction, await bo } acc = notary.FakeSimpleAccount(pub) } else { - m, pubsBytes, ok := vm.ParseMultiSigContract(script) + m, pubsBytes, ok := scparser.ParseMultiSigContract(script) if !ok { return errors.New("failed to parse verification script of signer #2: unknown witness type") } diff --git a/pkg/morph/contracts/methods.go b/pkg/morph/contracts/methods.go index 143f99c342..c93730304f 100644 --- a/pkg/morph/contracts/methods.go +++ b/pkg/morph/contracts/methods.go @@ -2,19 +2,21 @@ package fschaincontracts // Various methods of FS chain Container contract. const ( - PayBalanceMethod = "settleContainerPayment" - UnpaidBalanceMethod = "getUnpaidContainerEpoch" - CreateContainerMethod = "create" - CreateContainerV2Method = "createV2" - RemoveContainerMethod = "remove" - PutContainerEACLMethod = "putEACL" - PutContainerReportMethod = "putReport" - GetReportsSummaryMethod = "getNodeReportSummary" - IterateContainerReportsMethod = "iterateReports" - GetTakenSpaceByUserMethod = "getTakenSpaceByUser" - GetContainerQuotaMethod = "containerQuota" - GetUserQuotaMethod = "userQuota" - AddContainerStructsMethod = "addStructs" + PayBalanceMethod = "settleContainerPayment" + UnpaidBalanceMethod = "getUnpaidContainerEpoch" + CreateContainerMethod = "create" + CreateContainerV2Method = "createV2" + RemoveContainerMethod = "remove" + PutContainerEACLMethod = "putEACL" + PutContainerReportMethod = "putReport" + GetReportsSummaryMethod = "getNodeReportSummary" + IterateContainerReportsMethod = "iterateReports" + GetTakenSpaceByUserMethod = "getTakenSpaceByUser" + GetContainerQuotaMethod = "containerQuota" + GetUserQuotaMethod = "userQuota" + AddContainerStructsMethod = "addStructs" + SetContainerAttributeMethod = "setAttribute" + RemoveContainerAttributeMethod = "removeAttribute" ) // CreateContainerParams are parameters of [CreateContainerMethod]. diff --git a/pkg/morph/event/container/notary_requests.go b/pkg/morph/event/container/notary_requests.go index 7f3badb781..217ac81948 100644 --- a/pkg/morph/event/container/notary_requests.go +++ b/pkg/morph/event/container/notary_requests.go @@ -219,3 +219,105 @@ func RestoreAddStructsRequest(notaryReq event.NotaryEvent) (event.Event, error) MainTransaction: *notaryReq.Raw().MainTransaction, }, nil } + +// SetAttributeRequest wraps attribute setting request to provide app-internal +// event. +type SetAttributeRequest struct { + event.Event + MainTransaction transaction.Transaction + + ID []byte + Attribute string + Value string + ValidUntil int64 + InvocationScript []byte + VerificationScript []byte + SessionToken []byte +} + +// RestoreSetAttributeRequest restores [SetAttributeRequest] from the notary +// one. +func RestoreSetAttributeRequest(notaryReq event.NotaryEvent) (event.Event, error) { + const argNum = 7 + args, err := getArgsFromEvent(notaryReq, argNum) + if err != nil { + return nil, err + } + + var res SetAttributeRequest + + if res.ID, err = getValueFromArg(args, argNum-1, "ID", stackitem.ByteArrayT, event.BytesFromOpcode); err != nil { + return nil, err + } + if res.Attribute, err = getValueFromArg(args, argNum-2, "attribute", stackitem.ByteArrayT, event.StringFromOpcode); err != nil { + return nil, err + } + if res.Value, err = getValueFromArg(args, argNum-3, "value", stackitem.ByteArrayT, event.StringFromOpcode); err != nil { + return nil, err + } + if res.ValidUntil, err = getValueFromArg(args, argNum-4, "request expiration time", stackitem.IntegerT, event.IntFromOpcode); err != nil { + return nil, err + } + if res.InvocationScript, err = getValueFromArg(args, argNum-5, "invocation script", stackitem.ByteArrayT, event.BytesFromOpcode); err != nil { + return nil, err + } + if res.VerificationScript, err = getValueFromArg(args, argNum-6, "verification script", stackitem.ByteArrayT, event.BytesFromOpcode); err != nil { + return nil, err + } + if res.SessionToken, err = getValueFromArg(args, argNum-7, "session token", stackitem.ByteArrayT, event.BytesFromOpcode); err != nil { + return nil, err + } + + res.MainTransaction = *notaryReq.Raw().MainTransaction + + return res, nil +} + +// RemoveAttributeRequest wraps attribute removal request to provide +// app-internal event. +type RemoveAttributeRequest struct { + event.Event + MainTransaction transaction.Transaction + + ID []byte + Attribute string + ValidUntil int64 + InvocationScript []byte + VerificationScript []byte + SessionToken []byte +} + +// RestoreRemoveAttributeRequest restores [RemoveAttributeRequest] from the +// notary one. +func RestoreRemoveAttributeRequest(notaryReq event.NotaryEvent) (event.Event, error) { + const argNum = 6 + args, err := getArgsFromEvent(notaryReq, argNum) + if err != nil { + return nil, err + } + + var res RemoveAttributeRequest + + if res.ID, err = getValueFromArg(args, argNum-1, "ID", stackitem.ByteArrayT, event.BytesFromOpcode); err != nil { + return nil, err + } + if res.Attribute, err = getValueFromArg(args, argNum-2, "attribute", stackitem.ByteArrayT, event.StringFromOpcode); err != nil { + return nil, err + } + if res.ValidUntil, err = getValueFromArg(args, argNum-3, "request expiration time", stackitem.IntegerT, event.IntFromOpcode); err != nil { + return nil, err + } + if res.InvocationScript, err = getValueFromArg(args, argNum-4, "invocation script", stackitem.ByteArrayT, event.BytesFromOpcode); err != nil { + return nil, err + } + if res.VerificationScript, err = getValueFromArg(args, argNum-5, "verification script", stackitem.ByteArrayT, event.BytesFromOpcode); err != nil { + return nil, err + } + if res.SessionToken, err = getValueFromArg(args, argNum-6, "session token", stackitem.ByteArrayT, event.BytesFromOpcode); err != nil { + return nil, err + } + + res.MainTransaction = *notaryReq.Raw().MainTransaction + + return res, nil +} diff --git a/pkg/services/container/server.go b/pkg/services/container/server.go index 89b3baa478..24bdd6c78e 100644 --- a/pkg/services/container/server.go +++ b/pkg/services/container/server.go @@ -21,6 +21,8 @@ import ( 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" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" "github.com/nspcc-dev/neofs-sdk-go/eacl" protocontainer "github.com/nspcc-dev/neofs-sdk-go/proto/container" protonetmap "github.com/nspcc-dev/neofs-sdk-go/proto/netmap" @@ -69,6 +71,16 @@ type Contract interface { // successfully executed. Waiting is performed within ctx, // [apistatus.ErrContainerAwaitTimeout] is returned when it is done. Delete(ctx context.Context, _ cid.ID, pub, sig []byte, _ *session.Container) error + // SetAttribute sends transaction setting container attribute with provided + // credentials. If transaction is accepted for processing, SetAttribute waits + // for it to be successfully executed. Waiting is performed within ctx, + // [apistatus.ErrContainerAwaitTimeout] is returned when it is done. + SetAttribute(ctx context.Context, _ cid.ID, attr, val string, validUntil uint64, pub, sig, sessionToken []byte) error + // RemoveAttribute sends transaction removing container attribute with provided + // credentials. If transaction is accepted for processing, RemoveAttribute waits + // for it to be successfully executed. Waiting is performed within ctx, + // [apistatus.ErrContainerAwaitTimeout] is returned when it is done. + RemoveAttribute(ctx context.Context, _ cid.ID, attr string, validUntil uint64, pub, sig, sessionToken []byte) error } // NetmapContract represents Netmap contract deployed in the FS chain required @@ -146,6 +158,11 @@ func (s *Server) getVerifiedSessionToken(mh *protosession.RequestMetaHeader, req return nil, nil } + st, _, err := s.getVerifiedSessionTokenWithBinary(m, reqVerb, reqCnr) + return st, err +} + +func (s *Server) getVerifiedSessionTokenWithBinary(m *protosession.SessionToken, reqVerb session.ContainerVerb, reqCnr cid.ID) (*session.Container, []byte, error) { b := make([]byte, m.MarshaledSize()) m.MarshalStable(b) @@ -157,14 +174,14 @@ func (s *Server) getVerifiedSessionToken(mh *protosession.RequestMetaHeader, req s.sessionTokenCommonCheckCache.Add(cacheKey, res) } if res.err != nil { - return nil, res.err + return nil, nil, res.err } if err := s.verifySessionTokenAgainstRequest(res.token, reqVerb, reqCnr); err != nil { - return nil, err + return nil, nil, err } - return &res.token, nil + return &res.token, b, nil } func (s *Server) decodeAndVerifySessionTokenCommon(m *protosession.SessionToken) (session.Container, error) { @@ -557,3 +574,190 @@ func (s *Server) GetExtendedACL(_ context.Context, req *protocontainer.GetExtend } return s.makeGetEACLResponse(body, util.StatusOK) } + +func (s *Server) makeSetAttributeResponse(err error) (*protocontainer.SetAttributeResponse, error) { + resp := &protocontainer.SetAttributeResponse{ + Body: &protocontainer.SetAttributeResponse_Body{ + Status: apistatus.FromError(err), + }, + } + + b := make([]byte, resp.Body.MarshaledSize()) + resp.Body.MarshalStable(b) + + signer := (*neofsecdsa.Signer)(s.signer) + + sig, err := signer.Sign(b) + if err != nil { // same as util.SignResponse + panic(err) + } + + resp.BodySignature = &refs.Signature{ + Key: neofscrypto.PublicKeyBytes(signer.Public()), + Sign: sig, + Scheme: refs.SignatureScheme_ECDSA_SHA512, + } + + return resp, nil +} + +func verifySetAttributeRequestBody(body *protocontainer.SetAttributeRequest_Body) error { + switch { + case body == nil: + return errors.New("missing request body") + case body.Parameters == nil: + return errors.New("missing parameters") + case body.Parameters.ContainerId == nil: + return errors.New("missing container ID") + case body.Parameters.Attribute == "": + return errors.New("missing attribute name") + case body.Parameters.Value == "": + return errors.New("missing attribute value") + case body.Signature == nil: + return errors.New("missing parameters' signature") + case body.SessionToken != nil && body.SessionTokenV1 != nil: + return errors.New("both session V1 and V2 tokens set") + } + + return nil +} + +func parseSetAttributeRequestBody(body *protocontainer.SetAttributeRequest_Body) (cid.ID, error) { + if err := verifySetAttributeRequestBody(body); err != nil { + return cid.ID{}, err + } + + var id cid.ID + if err := id.FromProtoMessage(body.Parameters.ContainerId); err != nil { + return cid.ID{}, fmt.Errorf("invalid container ID: %w", err) + } + + return id, nil +} + +// SetAttribute forwards attribute setting request to the underlying [Contract] +// for further processing. If session token is attached, it's verified. +func (s *Server) SetAttribute(ctx context.Context, req *protocontainer.SetAttributeRequest) (*protocontainer.SetAttributeResponse, error) { + if err := neofscrypto.VerifyMessageSignature(req.Body, req.BodySignature, nil); err != nil { + var e apistatus.SignatureVerification + e.SetMessage("invalid request signature: " + err.Error()) + return s.makeSetAttributeResponse(e) + } + + id, err := parseSetAttributeRequestBody(req.Body) + if err != nil { + var e apistatus.BadRequest + e.SetMessage(err.Error()) + return s.makeSetAttributeResponse(e) + } + + var sessionToken []byte + if req.Body.SessionToken != nil { + // TODO + return s.makeSetAttributeResponse(errors.New("sessions V2 are not supported yet")) + } else if req.Body.SessionTokenV1 != nil { + _, sessionToken, err = s.getVerifiedSessionTokenWithBinary(req.Body.SessionTokenV1, session.VerbContainerSetAttribute, id) + if err != nil { + return s.makeSetAttributeResponse(fmt.Errorf("verify session token V1: %w", err)) + } + } + + ctx, cancel := context.WithTimeout(ctx, defaultTxAwaitTimeout) + defer cancel() + + err = s.contract.SetAttribute(ctx, id, req.Body.Parameters.Attribute, req.Body.Parameters.Value, req.Body.Parameters.ValidUntil, + req.Body.Signature.Key, req.Body.Signature.Sign, sessionToken) + + return s.makeSetAttributeResponse(err) +} + +func (s *Server) makeRemoveAttributeResponse(err error) (*protocontainer.RemoveAttributeResponse, error) { + resp := &protocontainer.RemoveAttributeResponse{ + Body: &protocontainer.RemoveAttributeResponse_Body{ + Status: util.ToStatus(err), + }, + } + + b := make([]byte, resp.Body.MarshaledSize()) + resp.Body.MarshalStable(b) + + sig, err := (*neofsecdsa.Signer)(s.signer).Sign(b) + if err != nil { // same as util.SignResponse + panic(err) + } + + resp.BodySignature = &refs.Signature{ + Key: neofscrypto.PublicKeyBytes((*neofsecdsa.Signer)(s.signer).Public()), + Sign: sig, + Scheme: refs.SignatureScheme_ECDSA_SHA512, + } + + return resp, nil +} + +func verifyRemoveAttributeRequestBody(body *protocontainer.RemoveAttributeRequest_Body) error { + switch { + case body == nil: + return errors.New("missing request body") + case body.Parameters == nil: + return errors.New("missing parameters") + case body.Parameters.ContainerId == nil: + return errors.New("missing container ID") + case body.Parameters.Attribute == "": + return errors.New("missing attribute name") + case body.Signature == nil: + return errors.New("missing parameters' signature") + case body.SessionToken != nil && body.SessionTokenV1 != nil: + return errors.New("both session V1 and V2 tokens set") + } + + return nil +} + +func parseRemoveAttributeRequestBody(body *protocontainer.RemoveAttributeRequest_Body) (cid.ID, error) { + if err := verifyRemoveAttributeRequestBody(body); err != nil { + return cid.ID{}, err + } + + var id cid.ID + if err := id.FromProtoMessage(body.Parameters.ContainerId); err != nil { + return cid.ID{}, fmt.Errorf("invalid container ID: %w", err) + } + + return id, nil +} + +// RemoveAttribute forwards attribute removal request to the underlying +// [Contract] for further processing. If session token is attached, it's +// verified. +func (s *Server) RemoveAttribute(ctx context.Context, req *protocontainer.RemoveAttributeRequest) (*protocontainer.RemoveAttributeResponse, error) { + if err := neofscrypto.VerifyMessageSignature(req.Body, req.BodySignature, nil); err != nil { + return s.makeRemoveAttributeResponse(err) + } + + id, err := parseRemoveAttributeRequestBody(req.Body) + if err != nil { + var e apistatus.BadRequest + e.SetMessage(err.Error()) + return s.makeRemoveAttributeResponse(e) + } + + var sessionToken []byte + if req.Body.SessionToken != nil { + // TODO + return s.makeRemoveAttributeResponse(errors.New("sessions V2 are not supported yet")) + } else if req.Body.SessionTokenV1 != nil { + _, sessionToken, err = s.getVerifiedSessionTokenWithBinary(req.Body.SessionTokenV1, session.VerbContainerRemoveAttribute, id) + if err != nil { + return s.makeRemoveAttributeResponse(fmt.Errorf("verify session token V1: %w", err)) + } + } + + ctx, cancel := context.WithTimeout(ctx, defaultTxAwaitTimeout) + defer cancel() + + err = s.contract.RemoveAttribute(ctx, id, req.Body.Parameters.Attribute, req.Body.Parameters.ValidUntil, + req.Body.Signature.Key, req.Body.Signature.Sign, sessionToken) + + return s.makeRemoveAttributeResponse(err) +} diff --git a/pkg/services/container/server_test.go b/pkg/services/container/server_test.go index d05bece59f..a80e1607fd 100644 --- a/pkg/services/container/server_test.go +++ b/pkg/services/container/server_test.go @@ -65,6 +65,14 @@ func (unimplementedContainerContract) Delete(context.Context, cid.ID, []byte, [] panic("unimplemented") } +func (unimplementedContainerContract) SetAttribute(context.Context, cid.ID, string, string, uint64, []byte, []byte, []byte) error { + panic("unimplemented") +} + +func (unimplementedContainerContract) RemoveAttribute(context.Context, cid.ID, string, uint64, []byte, []byte, []byte) error { + panic("unimplemented") +} + type unimplementedNetmapContract struct{} func (unimplementedNetmapContract) GetEpochBlock(uint64) (uint32, error) { @@ -120,6 +128,14 @@ func (x testFSChain) InvokeContainedScript(*transaction.Transaction, *block.Head panic("unimplemented") } +func (testFSChain) SetAttribute(context.Context, cid.ID, string, string, uint64, []byte, []byte, []byte) error { + return errors.New("unimplemented") +} + +func (testFSChain) RemoveAttribute(context.Context, cid.ID, string, uint64, []byte, []byte, []byte) error { + return errors.New("unimplemented") +} + func makeDeleteRequestWithSession(t testing.TB, usr usertest.UserSigner, cnr cid.ID, st interface { ProtoMessage() *protosession.SessionToken }) *protocontainer.DeleteRequest {