diff --git a/session/container.go b/session/container.go new file mode 100644 index 0000000..cb2f86b --- /dev/null +++ b/session/container.go @@ -0,0 +1,139 @@ +package session + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/session" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" +) + +// ContainerContext represents NeoFS API v2-compatible +// context of the container session. +// +// It is a wrapper over session.ContainerSessionContext +// which allows to abstract from details of the message +// structure. +type ContainerContext session.ContainerSessionContext + +// NewContainerContext creates and returns blank ContainerSessionContext. +// +// Defaults: +// - not bound to any operation; +// - applied to all containers. +func NewContainerContext() *ContainerContext { + v2 := new(session.ContainerSessionContext) + v2.SetWildcard(true) + + return ContainerContextFromV2(v2) +} + +// ContainerContextFromV2 wraps session.ContainerSessionContext +// into ContainerContext. +func ContainerContextFromV2(v *session.ContainerSessionContext) *ContainerContext { + return (*ContainerContext)(v) +} + +// ToV2 converts ContainerContext to session.ContainerSessionContext +// message structure. +func (x *ContainerContext) ToV2() *session.ContainerSessionContext { + return (*session.ContainerSessionContext)(x) +} + +// ApplyTo specifies which container the ContainerContext applies to. +// +// If id is nil, ContainerContext is applied to all containers. +func (x *ContainerContext) ApplyTo(id *cid.ID) { + v2 := (*session.ContainerSessionContext)(x) + + v2.SetWildcard(id == nil) + v2.SetContainerID(id.ToV2()) +} + +// ActOnAllContainers is a helper function that conveniently +// applies ContainerContext to all containers. +func ApplyToAllContainers(c *ContainerContext) { + c.ApplyTo(nil) +} + +// Container returns identifier of the container +// to which the ContainerContext applies. +// +// Returns nil if ContainerContext is applied to +// all containers. +func (x *ContainerContext) Container() *cid.ID { + v2 := (*session.ContainerSessionContext)(x) + + if v2.Wildcard() { + return nil + } + + return cid.NewFromV2(v2.ContainerID()) +} + +func (x *ContainerContext) forVerb(v session.ContainerSessionVerb) { + (*session.ContainerSessionContext)(x). + SetVerb(v) +} + +func (x *ContainerContext) isForVerb(v session.ContainerSessionVerb) bool { + return (*session.ContainerSessionContext)(x). + Verb() == v +} + +// ForPut binds the ContainerContext to +// PUT operation. +func (x *ContainerContext) ForPut() { + x.forVerb(session.ContainerVerbPut) +} + +// IsForPut checks if ContainerContext is bound to +// PUT operation. +func (x *ContainerContext) IsForPut() bool { + return x.isForVerb(session.ContainerVerbPut) +} + +// ForDelete binds the ContainerContext to +// DELETE operation. +func (x *ContainerContext) ForDelete() { + x.forVerb(session.ContainerVerbDelete) +} + +// IsForDelete checks if ContainerContext is bound to +// DELETE operation. +func (x *ContainerContext) IsForDelete() bool { + return x.isForVerb(session.ContainerVerbDelete) +} + +// ForSetEACL binds the ContainerContext to +// SETEACL operation. +func (x *ContainerContext) ForSetEACL() { + x.forVerb(session.ContainerVerbSetEACL) +} + +// IsForSetEACL checks if ContainerContext is bound to +// SETEACL operation. +func (x *ContainerContext) IsForSetEACL() bool { + return x.isForVerb(session.ContainerVerbSetEACL) +} + +// Marshal marshals ContainerContext into a protobuf binary form. +func (x *ContainerContext) Marshal(bs ...[]byte) ([]byte, error) { + var buf []byte + if len(bs) > 0 { + buf = bs[0] + } + return x.ToV2().StableMarshal(buf) +} + +// Unmarshal unmarshals protobuf binary representation of ContainerContext. +func (x *ContainerContext) Unmarshal(data []byte) error { + return x.ToV2().Unmarshal(data) +} + +// MarshalJSON encodes ContainerContext to protobuf JSON format. +func (x *ContainerContext) MarshalJSON() ([]byte, error) { + return x.ToV2().MarshalJSON() +} + +// UnmarshalJSON decodes ContainerContext from protobuf JSON format. +func (x *ContainerContext) UnmarshalJSON(data []byte) error { + return x.ToV2().UnmarshalJSON(data) +} diff --git a/session/container_test.go b/session/container_test.go new file mode 100644 index 0000000..16fdb21 --- /dev/null +++ b/session/container_test.go @@ -0,0 +1,112 @@ +package session_test + +import ( + "testing" + + v2session "github.com/nspcc-dev/neofs-api-go/v2/session" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + "github.com/nspcc-dev/neofs-sdk-go/session" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" + "github.com/stretchr/testify/require" +) + +func TestContainerContextVerbs(t *testing.T) { + c := session.NewContainerContext() + + assert := func(setter func(), getter func() bool, verb v2session.ContainerSessionVerb) { + setter() + + require.True(t, getter()) + + require.Equal(t, verb, c.ToV2().Verb()) + } + + t.Run("PUT", func(t *testing.T) { + assert(c.ForPut, c.IsForPut, v2session.ContainerVerbPut) + }) + + t.Run("DELETE", func(t *testing.T) { + assert(c.ForDelete, c.IsForDelete, v2session.ContainerVerbDelete) + }) + + t.Run("SETEACL", func(t *testing.T) { + assert(c.ForSetEACL, c.IsForSetEACL, v2session.ContainerVerbSetEACL) + }) +} + +func TestContainerContext_ApplyTo(t *testing.T) { + c := session.NewContainerContext() + id := cidtest.GenerateID() + + t.Run("method", func(t *testing.T) { + c.ApplyTo(id) + + require.Equal(t, id, c.Container()) + + c.ApplyTo(nil) + + require.Nil(t, c.Container()) + }) + + t.Run("helper functions", func(t *testing.T) { + c.ApplyTo(id) + + session.ApplyToAllContainers(c) + + require.Nil(t, c.Container()) + }) +} + +func TestFilter_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *session.ContainerContext + + require.Nil(t, x.ToV2()) + }) + + t.Run("default values", func(t *testing.T) { + c := session.NewContainerContext() + + // check initial values + require.Nil(t, c.Container()) + + for _, op := range []func() bool{ + c.IsForPut, + c.IsForDelete, + c.IsForSetEACL, + } { + require.False(t, op()) + } + + // convert to v2 message + cV2 := c.ToV2() + + require.Equal(t, v2session.ContainerVerbUnknown, cV2.Verb()) + require.True(t, cV2.Wildcard()) + require.Nil(t, cV2.ContainerID()) + }) +} + +func TestContainerContextEncoding(t *testing.T) { + c := sessiontest.ContainerContext() + + t.Run("binary", func(t *testing.T) { + data, err := c.Marshal() + require.NoError(t, err) + + c2 := session.NewContainerContext() + require.NoError(t, c2.Unmarshal(data)) + + require.Equal(t, c, c2) + }) + + t.Run("json", func(t *testing.T) { + data, err := c.MarshalJSON() + require.NoError(t, err) + + c2 := session.NewContainerContext() + require.NoError(t, c2.UnmarshalJSON(data)) + + require.Equal(t, c, c2) + }) +} diff --git a/session/session.go b/session/session.go new file mode 100644 index 0000000..623586d --- /dev/null +++ b/session/session.go @@ -0,0 +1,282 @@ +package session + +import ( + "crypto/ecdsa" + + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/nspcc-dev/neofs-api-go/v2/session" + v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature" + "github.com/nspcc-dev/neofs-sdk-go/owner" + "github.com/nspcc-dev/neofs-sdk-go/signature" + sigutil "github.com/nspcc-dev/neofs-sdk-go/util/signature" +) + +// Token represents NeoFS API v2-compatible +// session token. +type Token session.SessionToken + +// NewTokenFromV2 wraps session.SessionToken message structure +// into Token. +// +// Nil session.SessionToken converts to nil. +func NewTokenFromV2(tV2 *session.SessionToken) *Token { + return (*Token)(tV2) +} + +// NewToken creates and returns blank Token. +// +// Defaults: +// - body: nil; +// - id: nil; +// - ownerId: nil; +// - sessionKey: nil; +// - exp: 0; +// - iat: 0; +// - nbf: 0; +func NewToken() *Token { + return NewTokenFromV2(new(session.SessionToken)) +} + +// ToV2 converts Token to session.SessionToken message structure. +// +// Nil Token converts to nil. +func (t *Token) ToV2() *session.SessionToken { + return (*session.SessionToken)(t) +} + +func (t *Token) setBodyField(setter func(*session.SessionTokenBody)) { + token := (*session.SessionToken)(t) + body := token.GetBody() + + if body == nil { + body = new(session.SessionTokenBody) + token.SetBody(body) + } + + setter(body) +} + +// ID returns Token identifier. +func (t *Token) ID() []byte { + return (*session.SessionToken)(t). + GetBody(). + GetID() +} + +// SetID sets Token identifier. +func (t *Token) SetID(v []byte) { + t.setBodyField(func(body *session.SessionTokenBody) { + body.SetID(v) + }) +} + +// OwnerID returns Token's owner identifier. +func (t *Token) OwnerID() *owner.ID { + return owner.NewIDFromV2( + (*session.SessionToken)(t). + GetBody(). + GetOwnerID(), + ) +} + +// SetOwnerID sets Token's owner identifier. +func (t *Token) SetOwnerID(v *owner.ID) { + t.setBodyField(func(body *session.SessionTokenBody) { + body.SetOwnerID(v.ToV2()) + }) +} + +// SessionKey returns public key of the session +// in a binary format. +func (t *Token) SessionKey() []byte { + return (*session.SessionToken)(t). + GetBody(). + GetSessionKey() +} + +// SetSessionKey sets public key of the session +// in a binary format. +func (t *Token) SetSessionKey(v []byte) { + t.setBodyField(func(body *session.SessionTokenBody) { + body.SetSessionKey(v) + }) +} + +func (t *Token) setLifetimeField(f func(*session.TokenLifetime)) { + t.setBodyField(func(body *session.SessionTokenBody) { + lt := body.GetLifetime() + if lt == nil { + lt = new(session.TokenLifetime) + body.SetLifetime(lt) + } + + f(lt) + }) +} + +// Exp returns epoch number of the token expiration. +func (t *Token) Exp() uint64 { + return (*session.SessionToken)(t). + GetBody(). + GetLifetime(). + GetExp() +} + +// SetExp sets epoch number of the token expiration. +func (t *Token) SetExp(exp uint64) { + t.setLifetimeField(func(lt *session.TokenLifetime) { + lt.SetExp(exp) + }) +} + +// Nbf returns starting epoch number of the token. +func (t *Token) Nbf() uint64 { + return (*session.SessionToken)(t). + GetBody(). + GetLifetime(). + GetNbf() +} + +// SetNbf sets starting epoch number of the token. +func (t *Token) SetNbf(nbf uint64) { + t.setLifetimeField(func(lt *session.TokenLifetime) { + lt.SetNbf(nbf) + }) +} + +// Iat returns starting epoch number of the token. +func (t *Token) Iat() uint64 { + return (*session.SessionToken)(t). + GetBody(). + GetLifetime(). + GetIat() +} + +// SetIat sets the number of the epoch in which the token was issued. +func (t *Token) SetIat(iat uint64) { + t.setLifetimeField(func(lt *session.TokenLifetime) { + lt.SetIat(iat) + }) +} + +// Sign calculates and writes signature of the Token data. +// +// Returns signature calculation errors. +func (t *Token) Sign(key *ecdsa.PrivateKey) error { + tV2 := (*session.SessionToken)(t) + + signedData := v2signature.StableMarshalerWrapper{ + SM: tV2.GetBody(), + } + + return sigutil.SignDataWithHandler(key, signedData, func(key, sig []byte) { + tSig := tV2.GetSignature() + if tSig == nil { + tSig = new(refs.Signature) + } + + tSig.SetKey(key) + tSig.SetSign(sig) + + tV2.SetSignature(tSig) + }) +} + +// VerifySignature checks if token signature is +// presented and valid. +func (t *Token) VerifySignature() bool { + tV2 := (*session.SessionToken)(t) + + signedData := v2signature.StableMarshalerWrapper{ + SM: tV2.GetBody(), + } + + return sigutil.VerifyDataWithSource(signedData, func() (key, sig []byte) { + tSig := tV2.GetSignature() + return tSig.GetKey(), tSig.GetSign() + }) == nil +} + +// Signature returns Token signature. +func (t *Token) Signature() *signature.Signature { + return signature.NewFromV2( + (*session.SessionToken)(t). + GetSignature(), + ) +} + +// SetContext sets context of the Token. +// +// Supported contexts: +// - *ContainerContext. +// +// Resets context if it is not supported. +func (t *Token) SetContext(v interface{}) { + var cV2 session.SessionTokenContext + + switch c := v.(type) { + case *ContainerContext: + cV2 = c.ToV2() + } + + t.setBodyField(func(body *session.SessionTokenBody) { + body.SetContext(cV2) + }) +} + +// Context returns context of the Token. +// +// Supports same contexts as SetContext. +// +// Returns nil if context is not supported. +func (t *Token) Context() interface{} { + switch v := (*session.SessionToken)(t). + GetBody(). + GetContext(); c := v.(type) { + default: + return nil + case *session.ContainerSessionContext: + return ContainerContextFromV2(c) + } +} + +// GetContainerContext is a helper function that casts +// Token context to ContainerContext. +// +// Returns nil if context is not a ContainerContext. +func GetContainerContext(t *Token) *ContainerContext { + c, _ := t.Context().(*ContainerContext) + return c +} + +// Marshal marshals Token into a protobuf binary form. +// +// Buffer is allocated when the argument is empty. +// Otherwise, the first buffer is used. +func (t *Token) Marshal(bs ...[]byte) ([]byte, error) { + var buf []byte + if len(bs) > 0 { + buf = bs[0] + } + + return (*session.SessionToken)(t). + StableMarshal(buf) +} + +// Unmarshal unmarshals protobuf binary representation of Token. +func (t *Token) Unmarshal(data []byte) error { + return (*session.SessionToken)(t). + Unmarshal(data) +} + +// MarshalJSON encodes Token to protobuf JSON format. +func (t *Token) MarshalJSON() ([]byte, error) { + return (*session.SessionToken)(t). + MarshalJSON() +} + +// UnmarshalJSON decodes Token from protobuf JSON format. +func (t *Token) UnmarshalJSON(data []byte) error { + return (*session.SessionToken)(t). + UnmarshalJSON(data) +} diff --git a/session/session_test.go b/session/session_test.go new file mode 100644 index 0000000..3d9490c --- /dev/null +++ b/session/session_test.go @@ -0,0 +1,202 @@ +package session_test + +import ( + "testing" + + sessionv2 "github.com/nspcc-dev/neofs-api-go/v2/session" + ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test" + "github.com/nspcc-dev/neofs-sdk-go/session" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" + "github.com/stretchr/testify/require" +) + +func TestSessionToken_SetID(t *testing.T) { + token := session.NewToken() + + id := []byte{1, 2, 3} + token.SetID(id) + + require.Equal(t, id, token.ID()) +} + +func TestSessionToken_SetOwnerID(t *testing.T) { + token := session.NewToken() + + ownerID := ownertest.GenerateID() + + token.SetOwnerID(ownerID) + + require.Equal(t, ownerID, token.OwnerID()) +} + +func TestSessionToken_SetSessionKey(t *testing.T) { + token := session.NewToken() + + key := []byte{1, 2, 3} + token.SetSessionKey(key) + + require.Equal(t, key, token.SessionKey()) +} + +func TestSessionTokenEncoding(t *testing.T) { + tok := sessiontest.Generate() + + t.Run("binary", func(t *testing.T) { + data, err := tok.Marshal() + require.NoError(t, err) + + tok2 := session.NewToken() + require.NoError(t, tok2.Unmarshal(data)) + + require.Equal(t, tok, tok2) + }) + + t.Run("json", func(t *testing.T) { + data, err := tok.MarshalJSON() + require.NoError(t, err) + + tok2 := session.NewToken() + require.NoError(t, tok2.UnmarshalJSON(data)) + + require.Equal(t, tok, tok2) + }) +} + +func TestToken_VerifySignature(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var tok *session.Token + + require.False(t, tok.VerifySignature()) + }) + + t.Run("unsigned", func(t *testing.T) { + tok := sessiontest.Generate() + + require.False(t, tok.VerifySignature()) + }) + + t.Run("signed", func(t *testing.T) { + tok := sessiontest.GenerateSigned() + + require.True(t, tok.VerifySignature()) + }) +} + +var unsupportedContexts = []interface{}{ + 123, + true, + session.NewToken(), +} + +var nonContainerContexts = unsupportedContexts + +func TestToken_Context(t *testing.T) { + tok := session.NewToken() + + for _, item := range []struct { + ctx interface{} + v2assert func(interface{}) + }{ + { + ctx: sessiontest.ContainerContext(), + v2assert: func(c interface{}) { + require.Equal(t, c.(*session.ContainerContext).ToV2(), tok.ToV2().GetBody().GetContext()) + }, + }, + } { + tok.SetContext(item.ctx) + + require.Equal(t, item.ctx, tok.Context()) + + item.v2assert(item.ctx) + } + + for _, c := range unsupportedContexts { + tok.SetContext(c) + + require.Nil(t, tok.Context()) + } +} + +func TestGetContainerContext(t *testing.T) { + tok := session.NewToken() + + c := sessiontest.ContainerContext() + + tok.SetContext(c) + + require.Equal(t, c, session.GetContainerContext(tok)) + + for _, c := range nonContainerContexts { + tok.SetContext(c) + + require.Nil(t, session.GetContainerContext(tok)) + } +} + +func TestToken_Exp(t *testing.T) { + tok := session.NewToken() + + const exp = 11 + + tok.SetExp(exp) + + require.EqualValues(t, exp, tok.Exp()) +} + +func TestToken_Nbf(t *testing.T) { + tok := session.NewToken() + + const nbf = 22 + + tok.SetNbf(nbf) + + require.EqualValues(t, nbf, tok.Nbf()) +} + +func TestToken_Iat(t *testing.T) { + tok := session.NewToken() + + const iat = 33 + + tok.SetIat(iat) + + require.EqualValues(t, iat, tok.Iat()) +} + +func TestNewTokenFromV2(t *testing.T) { + t.Run("from nil", func(t *testing.T) { + var x *sessionv2.SessionToken + + require.Nil(t, session.NewTokenFromV2(x)) + }) +} + +func TestToken_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *session.Token + + require.Nil(t, x.ToV2()) + }) +} + +func TestNewToken(t *testing.T) { + t.Run("default values", func(t *testing.T) { + token := session.NewToken() + + // check initial values + require.Nil(t, token.Signature()) + require.Nil(t, token.OwnerID()) + require.Nil(t, token.SessionKey()) + require.Nil(t, token.ID()) + require.Zero(t, token.Exp()) + require.Zero(t, token.Iat()) + require.Zero(t, token.Nbf()) + + // convert to v2 message + tokenV2 := token.ToV2() + + require.Nil(t, tokenV2.GetSignature()) + require.Nil(t, tokenV2.GetBody()) + }) +} diff --git a/session/test/container.go b/session/test/container.go new file mode 100644 index 0000000..9c3f67f --- /dev/null +++ b/session/test/container.go @@ -0,0 +1,26 @@ +package sessiontest + +import ( + "math/rand" + + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + "github.com/nspcc-dev/neofs-sdk-go/session" +) + +// ContainerContext returns session.ContainerContext +// which applies to random operation on a random container. +func ContainerContext() *session.ContainerContext { + c := session.NewContainerContext() + + setters := []func(){ + c.ForPut, + c.ForDelete, + c.ForSetEACL, + } + + setters[rand.Uint32()%uint32(len(setters))]() + + c.ApplyTo(cidtest.GenerateID()) + + return c +} diff --git a/session/test/token.go b/session/test/token.go new file mode 100644 index 0000000..5c63a7c --- /dev/null +++ b/session/test/token.go @@ -0,0 +1,64 @@ +package sessiontest + +import ( + "math/rand" + + "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-sdk-go/owner" + "github.com/nspcc-dev/neofs-sdk-go/session" +) + +var p *keys.PrivateKey + +func init() { + var err error + + p, err = keys.NewPrivateKey() + if err != nil { + panic(err) + } +} + +// Generate returns random session.Token. +// +// Resulting token is unsigned. +func Generate() *session.Token { + tok := session.NewToken() + + uid, err := uuid.New().MarshalBinary() + if err != nil { + panic(err) + } + + w := new(owner.NEO3Wallet) + rand.Read(w.Bytes()) + + ownerID := owner.NewID() + ownerID.SetNeo3Wallet(w) + + keyBin := p.PublicKey().Bytes() + + tok.SetID(uid) + tok.SetOwnerID(ownerID) + tok.SetSessionKey(keyBin) + tok.SetExp(11) + tok.SetNbf(22) + tok.SetIat(33) + + return tok +} + +// GenerateSigned returns signed random session.Token. +// +// Panics if token could not be signed (actually unexpected). +func GenerateSigned() *session.Token { + tok := Generate() + + err := tok.Sign(&p.PrivateKey) + if err != nil { + panic(err) + } + + return tok +}