Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Changelog for NeoFS Node
- `neofs-adm balance container-status` command (#3693)
- Container IDs to JSON output of ADM `fschain dump-containers` command (#3789)
- Support for IDs from JSON to ADM `fschain restore-containers` command (#3789)
- IR supports new session token v2 (#3671)
- SN supports new session token v2 for container and object operations (#3671)

### Fixed
- IR panics at graceful shutdown (#3706)
Expand Down Expand Up @@ -59,7 +61,7 @@ Changelog for NeoFS Node

### Updated
- `github.com/nspcc-dev/neofs-contract` module to `v0.26.0` (#3670, #3746, #3733, #3780, #3782)
- `github.com/nspcc-dev/neofs-sdk-go` module to `v1.0.0-rc.16.0.20260126114348-87674e46ef14` (#3711, #3750, #3733, #3775, #3772, #3787, #3784)
- `github.com/nspcc-dev/neofs-sdk-go` module to `v1.0.0-rc.16.0.20260127152410-12dbac67e506` (#3711, #3750, #3733, #3775, #3772, #3787, #3784, #3671)
- `github.com/nspcc-dev/locode-db` module to `v0.8.2` (#3729)
- `github.com/nspcc-dev/neo-go` module to `v0.116.0` (#3733, #3769, #3779)

Expand Down
6 changes: 6 additions & 0 deletions cmd/neofs-cli/internal/commonflags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ const (

OIDFlag = "oid"
OIDFlagUsage = "Object ID."

SessionSubjectFlag = "session-subjects"
SessionSubjectFlagUsage = "Session subject user IDs (optional, defaults to current node)"

SessionSubjectNNSFlag = "session-subjects-nns"
SessionSubjectNNSFlagUsage = "Session subject NNS names (optional, defaults to current node)"
)

// Init adds common flags to the command:
Expand Down
2 changes: 1 addition & 1 deletion cmd/neofs-cli/modules/container/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ It will be stored in FS chain when inner ring will accepts it.`,
if tokAny != nil {
switch tok := tokAny.(type) {
case *sessionv2.Token:
cnr.SetOwner(tok.Issuer())
cnr.SetOwner(tok.OriginalIssuer())
case *session.Container:
cnr.SetOwner(tok.Issuer())
default:
Expand Down
4 changes: 2 additions & 2 deletions cmd/neofs-cli/modules/container/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ Only owner of the container has a permission to remove container.`,

switch tok := tokAny.(type) {
case *sessionv2.Token:
if tok.Issuer() != owner {
return fmt.Errorf("session issuer differs with the container owner: expected %s, has %s", owner, tok.Issuer())
if tok.OriginalIssuer() != owner {
return fmt.Errorf("session original issuer differs with the container owner: expected %s, has %s", owner, tok.OriginalIssuer())
}
case *session.Container:
if tok.Issuer() != owner {
Expand Down
4 changes: 2 additions & 2 deletions cmd/neofs-cli/modules/container/set_eacl.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ Container ID in EACL table will be substituted with ID from the CLI.`,

switch tok := tokAny.(type) {
case *sessionv2.Token:
if tok.Issuer() != owner {
return fmt.Errorf("session issuer differs with the container owner: expected %s, has %s", owner, tok.Issuer())
if tok.OriginalIssuer() != owner {
return fmt.Errorf("session original issuer differs with the container owner: expected %s, has %s", owner, tok.OriginalIssuer())
}
case *session.Container:
if tok.Issuer() != owner {
Expand Down
12 changes: 11 additions & 1 deletion cmd/neofs-cli/modules/container/util_session_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,20 @@ func getSessionAnyVersion(cmd *cobra.Command) (any, error) {
return nil, nil
}

// noopNNSResolver is a no-operation NNS name resolver that
// always returns that the user exists for any NNS name.
// We don't have NNS resolution in the CLI, so this resolver
// is used to skip issuer validation for NNS subjects.
type noopNNSResolver struct{}

func (r noopNNSResolver) HasUser(string, user.ID) (bool, error) {
return true, nil
}

func validateSessionV2ForContainer(cmd *cobra.Command, tok *session.Token, key *ecdsa.PrivateKey, cnrID cid.ID, verb session.Verb) error {
common.PrintVerbose(cmd, "Validating V2 session token...")

if err := tok.Validate(); err != nil {
if err := tok.Validate(noopNNSResolver{}); err != nil {
return fmt.Errorf("invalid V2 session token: %w", err)
}

Expand Down
9 changes: 8 additions & 1 deletion cmd/neofs-cli/modules/object/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ func initObjectDeleteCmd() {
flags.StringSlice(commonflags.OIDFlag, nil, commonflags.OIDFlagUsage)
flags.Bool(binaryFlag, false, "Deserialize object structure from given file.")
flags.String(fileFlag, "", "File with object payload")
flags.StringSlice(commonflags.SessionSubjectFlag, nil, commonflags.SessionSubjectFlagUsage)
flags.StringSlice(commonflags.SessionSubjectNNSFlag, nil, commonflags.SessionSubjectNNSFlagUsage)

_ = objectDelCmd.MarkFlagRequired(commonflags.CIDFlag)
_ = objectDelCmd.MarkFlagRequired(commonflags.OIDFlag)
Expand Down Expand Up @@ -104,7 +106,12 @@ func deleteObject(cmd *cobra.Command, _ []string) error {
var statusErr error
for _, addr := range objAddrs {
id := addr.Object()
err := ReadOrOpenSessionViaClient(ctx, cmd, &prm, cli, pk, cnr, id)
subjects, err := parseSessionSubjects(cmd, ctx, cli)
if err != nil {
return err
}

err = ReadOrOpenSessionViaClient(ctx, cmd, &prm, cli, pk, subjects, cnr, id)
if err != nil {
return err
}
Expand Down
9 changes: 8 additions & 1 deletion cmd/neofs-cli/modules/object/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ var objectLockCmd = &cobra.Command{
}
defer cli.Close()

err = ReadOrOpenSessionViaClient(ctx, cmd, &prm, cli, key, cnr)
subjects, err := parseSessionSubjects(cmd, ctx, cli)
if err != nil {
return err
}
err = ReadOrOpenSessionViaClient(ctx, cmd, &prm, cli, key, subjects, cnr)
if err != nil {
return err
}
Expand Down Expand Up @@ -132,4 +136,7 @@ func initCommandObjectLock() {

ff.Uint64(commonflags.Lifetime, 0, "Lock lifetime")
objectLockCmd.MarkFlagsOneRequired(commonflags.ExpireAt, commonflags.Lifetime)

ff.StringSlice(commonflags.SessionSubjectFlag, nil, commonflags.SessionSubjectFlagUsage)
ff.StringSlice(commonflags.SessionSubjectNNSFlag, nil, commonflags.SessionSubjectNNSFlagUsage)
}
8 changes: 7 additions & 1 deletion cmd/neofs-cli/modules/object/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ func initObjectPutCmd() {
flags.Bool(noProgressFlag, false, "Do not show progress bar")

flags.Bool(binaryFlag, false, "Deserialize object structure from given file.")
flags.StringSlice(commonflags.SessionSubjectFlag, nil, commonflags.SessionSubjectFlagUsage)
flags.StringSlice(commonflags.SessionSubjectNNSFlag, nil, commonflags.SessionSubjectNNSFlagUsage)
objectPutCmd.MarkFlagsMutuallyExclusive(commonflags.ExpireAt, commonflags.Lifetime)
}

Expand Down Expand Up @@ -129,7 +131,11 @@ func putObject(cmd *cobra.Command, _ []string) error {
}
defer func() { _ = cli.Close() }()

err = ReadOrOpenSessionViaClient(ctx, cmd, &prm, cli, pk, cnr)
subjects, err := parseSessionSubjects(cmd, ctx, cli)
if err != nil {
return err
}
err = ReadOrOpenSessionViaClient(ctx, cmd, &prm, cli, pk, subjects, cnr)
if err != nil {
return err
}
Expand Down
154 changes: 107 additions & 47 deletions cmd/neofs-cli/modules/object/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ package object
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"errors"
"fmt"
"io"
"os"
"strings"
"time"

internal "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/common"
"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags"
sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session"
icrypto "github.com/nspcc-dev/neofs-node/internal/crypto"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
"github.com/nspcc-dev/neofs-sdk-go/client"
Expand Down Expand Up @@ -205,7 +205,7 @@ func getSession(cmd *cobra.Command) (*session.Object, error) {
// *internal.PayloadRangePrm
// *internal.HashPayloadRangesPrm
func _readVerifiedSession(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) error {
isV2, err := tryReadSessionV2(cmd, dst, key, cnr, obj)
isV2, err := tryReadSessionV2(cmd, dst, key, cnr)
if err != nil {
return fmt.Errorf("v2 session validation failed: %w", err)
}
Expand Down Expand Up @@ -276,8 +276,8 @@ func getVerifiedSession(cmd *cobra.Command, cmdVerb session.ObjectVerb, key *ecd
// ReadOrOpenSessionViaClient tries to read session from the file (V2 first, then V1),
// specified in commonflags.SessionToken flag, finalizes structures of the decoded token
// and write the result into provided SessionPrm.
// If file is missing, ReadOrOpenSessionViaClient calls OpenSessionViaClient.
func ReadOrOpenSessionViaClient(ctx context.Context, cmd *cobra.Command, dst SessionPrm, cli *client.Client, key *ecdsa.PrivateKey, cnr cid.ID, objs ...oid.ID) error {
// If file is missing, CreateSessionV2 is called to create V2 token.
func ReadOrOpenSessionViaClient(ctx context.Context, cmd *cobra.Command, dst SessionPrm, cli *client.Client, key *ecdsa.PrivateKey, subjects []sessionv2.Target, cnr cid.ID, objs ...oid.ID) error {
path, _ := cmd.Flags().GetString(commonflags.SessionToken)

if path != "" {
Expand All @@ -296,47 +296,13 @@ func ReadOrOpenSessionViaClient(ctx context.Context, cmd *cobra.Command, dst Ses
}
}

err := OpenSessionViaClient(ctx, cmd, dst, cli, key, cnr, objs...)
err := CreateSessionV2(ctx, cmd, dst, cli, key, subjects, cnr)
if err != nil {
return err
}
return nil
}

// OpenSessionViaClient opens object session with the remote node, finalizes
// structure of the session token and writes the result into the provided
// SessionPrm. Also writes provided client connection to the SessionPrm.
//
// SessionPrm MUST be one of:
//
// *internal.PutObjectPrm
// *internal.DeleteObjectPrm
func OpenSessionViaClient(ctx context.Context, cmd *cobra.Command, dst SessionPrm, cli *client.Client, key *ecdsa.PrivateKey, cnr cid.ID, objs ...oid.ID) error {
var tok session.Object

const sessionLifetime = 10 // in NeoFS epochs

common.PrintVerbose(cmd, "Opening remote session with the node...")
currEpoch, err := internal.GetCurrentEpoch(ctx, viper.GetString(commonflags.RPC))
if err != nil {
return fmt.Errorf("can't fetch current epoch: %w", err)
}
exp := currEpoch + sessionLifetime
err = sessionCli.CreateSession(ctx, &tok, cli, *key, exp, currEpoch)
if err != nil {
return fmt.Errorf("open remote session: %w", err)
}

common.PrintVerbose(cmd, "Session successfully opened.")

err = finalizeSession(cmd, dst, &tok, key, cnr, objs...)
if err != nil {
return err
}

return nil
}

// specifies session verb, binds the session to the given container and limits
// the session by the given objects (if specified). After all data is written,
// signs session using provided private key and writes the session into the
Expand Down Expand Up @@ -381,30 +347,80 @@ func finalizeSession(cmd *cobra.Command, dst SessionPrm, tok *session.Object, ke
return nil
}

// CreateSessionV2 creates V2 token locally and
// writes the result into the provided SessionPrm.
// noopNNSResolver is a no-operation NNS name resolver that
// always returns that the user exists for any NNS name.
// We don't have NNS resolution in the CLI, so this resolver
// is used to skip issuer validation for NNS subjects.
type noopNNSResolver struct{}

func (r noopNNSResolver) HasUser(string, user.ID) (bool, error) {
return true, nil
}

// CreateSessionV2 opens object session with the remote node, finalizes
// structure of the session token v2 and writes the result into the provided
// SessionPrm. Also writes provided client connection to the SessionPrm.
//
// SessionPrm MUST be one of:
//
// *internal.PutObjectPrm
// *internal.DeleteObjectPrm
func CreateSessionV2(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, subjects []sessionv2.Target, cnr cid.ID, objs ...oid.ID) error {
func CreateSessionV2(ctx context.Context, cmd *cobra.Command, dst SessionPrm, cli *client.Client, key *ecdsa.PrivateKey, subjects []sessionv2.Target, cnr cid.ID) error {
const defaultTokenExp = 10 * time.Hour

common.PrintVerbose(cmd, "Creating V2 session token locally...")
var tok sessionv2.Token
signer := user.NewAutoIDSigner(*key)

currentTime := time.Now()
tok.SetVersion(sessionv2.TokenCurrentVersion)
tok.SetNonce(sessionv2.RandomNonce())
tok.SetIat(currentTime)
// allow 10s clock skew, because time isn't synchronous over the network
tok.SetIat(currentTime.Add(-10 * time.Second))
tok.SetNbf(currentTime)
tok.SetExp(currentTime.Add(10 * time.Hour))
tok.SetExp(currentTime.Add(defaultTokenExp))
tok.SetFinal(true)
err := tok.SetSubjects(subjects)
if err != nil {
return fmt.Errorf("set subjects: %w", err)
}

common.PrintVerbose(cmd, "Creating server-side session key via RPC...")

ni, err := cli.NetworkInfo(ctx, client.PrmNetworkInfo{})
if err != nil {
return fmt.Errorf("get network info: %w", err)
}
epochInMs := int64(ni.EpochDuration()) * ni.MsPerBlock()
if epochInMs == 0 {
return errors.New("invalid network configuration: epoch duration is zero")
}
epochLifetime := (defaultTokenExp.Milliseconds() + epochInMs - 1) / epochInMs
epochExp := ni.CurrentEpoch() + uint64(epochLifetime)
common.PrintVerbose(cmd, "Current epoch: %d", ni.CurrentEpoch())
common.PrintVerbose(cmd, "Token expiration epoch: %d", epochExp)

var sessionPrm client.PrmSessionCreate
sessionPrm.SetExp(epochExp)

sessionRes, err := cli.SessionCreate(ctx, signer, sessionPrm)
if err != nil {
return fmt.Errorf("create server-side session key: %w", err)
}

var keySession neofsecdsa.PublicKey
err = keySession.Decode(sessionRes.PublicKey())
if err != nil {
return fmt.Errorf("decode public session key: %w", err)
}

serverUserID := user.NewFromECDSAPublicKey((ecdsa.PublicKey)(keySession))
err = tok.AddSubject(sessionv2.NewTargetUser(serverUserID))
if err != nil {
return fmt.Errorf("add server-side session key as subject: %w", err)
}
common.PrintVerbose(cmd, "Server-side session key created as last subject: %s", serverUserID)

var verb sessionv2.Verb
switch dst.(type) {
case *client.PrmObjectPutInit:
Expand All @@ -428,7 +444,7 @@ func CreateSessionV2(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey,
return fmt.Errorf("sign V2 session: %w", err)
}

if err := tok.Validate(); err != nil {
if err := tok.Validate(noopNNSResolver{}); err != nil {
return fmt.Errorf("invalid V2 session token after creation: %w", err)
}

Expand All @@ -442,7 +458,7 @@ func CreateSessionV2(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey,
func finalizeSessionV2(cmd *cobra.Command, dst SessionPrm, tok *sessionv2.Token, key *ecdsa.PrivateKey, cnr cid.ID) error {
common.PrintVerbose(cmd, "Finalizing V2 session token...")

if err := tok.Validate(); err != nil {
if err := tok.Validate(noopNNSResolver{}); err != nil {
return fmt.Errorf("invalid V2 session token: %w", err)
}

Expand Down Expand Up @@ -487,3 +503,47 @@ func openFileForPayload(name string) (io.WriteCloser, error) {
}
return f, nil
}

func parseSessionSubjects(cmd *cobra.Command, ctx context.Context, cli *client.Client) ([]sessionv2.Target, error) {
sessionSubjects, _ := cmd.Flags().GetStringSlice(commonflags.SessionSubjectFlag)
sessionSubjectsNNS, _ := cmd.Flags().GetStringSlice(commonflags.SessionSubjectNNSFlag)

if len(sessionSubjects) > 0 || len(sessionSubjectsNNS) > 0 {
common.PrintVerbose(cmd, "Using session subjects from command line flags")
subjects := make([]sessionv2.Target, 0, len(sessionSubjects)+len(sessionSubjectsNNS))

// Parse user IDs
for _, subj := range sessionSubjects {
userID, err := user.DecodeString(subj)
if err != nil {
return nil, fmt.Errorf("failed to decode user ID '%s': %w", subj, err)
}
subjects = append(subjects, sessionv2.NewTargetUser(userID))
}

// Parse NNS names
for _, nnsName := range sessionSubjectsNNS {
if nnsName == "" {
return nil, fmt.Errorf("NNS name cannot be empty")
}
subjects = append(subjects, sessionv2.NewTargetNamed(nnsName))
}

return subjects, nil
}

common.PrintVerbose(cmd, "Using default session subjects (only target node)")
res, err := cli.EndpointInfo(ctx, client.PrmEndpointInfo{})
if err != nil {
return nil, fmt.Errorf("get endpoint info: %w", err)
}

neoPubKey, err := keys.NewPublicKeyFromBytes(res.NodeInfo().PublicKey(), elliptic.P256())
if err != nil {
return nil, fmt.Errorf("parse node public key: %w", err)
}

ecdsaPubKey := (*ecdsa.PublicKey)(neoPubKey)
userID := user.NewFromECDSAPublicKey(*ecdsaPubKey)
return []sessionv2.Target{sessionv2.NewTargetUser(userID)}, nil
}
Loading
Loading