Skip to content

Commit

Permalink
innerring: Prepare a platform for private attributes of storage nodes
Browse files Browse the repository at this point in the history
Private node attribute are coming. The term private means that the
system controls access to the declaration of certain attributes by
storage nodes. This feature will allow you to prevent unauthorized
installation of attributes that semantically require confirmation.

Until now, storage nodes were free to set format-valid attributes.
Therefore, to prepare for the feature arrival, a new attribute validator
(`netmap.NodeValidator`) is introduced: it fakes the real validation and
allows all nodes to access all attributes. In the future, verification
will be tightened.

Refs #2280.

Signed-off-by: Leonard Lyubich <[email protected]>
  • Loading branch information
cthulhu-rider committed Sep 15, 2023
1 parent 3749ce7 commit 3e2169b
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 0 deletions.
4 changes: 4 additions & 0 deletions pkg/innerring/innerring.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/nspcc-dev/neofs-node/pkg/innerring/processors/neofs"
"github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap"
nodevalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation"
attributevalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/attributes"
availabilityvalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/availability"
statevalidation "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/state"
addrvalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/structure"
Expand Down Expand Up @@ -730,6 +731,8 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper, errChan chan<-
var netMapCandidateStateValidator statevalidation.NetMapCandidateValidator
netMapCandidateStateValidator.SetNetworkSettings(netSettings)

var attributeAccessCtrl publicNodeAttributesAccessController

// create netmap processor
server.netmapProcessor, err = netmap.New(&netmap.Params{
Log: log,
Expand All @@ -755,6 +758,7 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper, errChan chan<-
&netMapCandidateStateValidator,
addrvalidator.New(),
availabilityvalidator.New(),
attributevalidator.New(attributeAccessCtrl),
locodeValidator,
),
NodeStateSettings: netSettings,
Expand Down
9 changes: 9 additions & 0 deletions pkg/innerring/netmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/state"
netmapclient "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
)

/*
Expand All @@ -27,3 +28,11 @@ func (s *networkSettings) MaintenanceModeAllowed() error {

return state.ErrMaintenanceModeDisallowed
}

// [attributevalidator.AttributeAccessController] that allows all nodes to
// freely use any attributes.
type publicNodeAttributesAccessController struct{}

func (x publicNodeAttributesAccessController) CheckNodeAccessToAttribute(_ neofscrypto.PublicKey, _, _ string) error {
return nil
}
128 changes: 128 additions & 0 deletions pkg/innerring/processors/netmap/nodevalidation/attributes/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package attributevalidator

import (
"errors"
"fmt"
"unicode/utf8"

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/netmap"
)

// ErrAccessDenied is returned when access is denied.
var ErrAccessDenied = errors.New("node is not allowed to use attribute")

// AttributeAccessController provides access control over the use of attributes
// by storage nodes.
type AttributeAccessController interface {
// CheckNodeAccessToAttribute checks whether storage node with the given key is
// allowed to have specified key-value attribute or not. Returns
// [ErrAccessDenied] when access is denied, or any other error encountered which
// prevented access check.
//
// Key to the attribute is a non-empty UTF-8 string. Value of the attribute is
// any non-empty string.
CheckNodeAccessToAttribute(nodeKey neofscrypto.PublicKey, attrKey, attrValue string) error
}

// Validator validates node attributes declared by the storage nodes on their
// attempts to enter the NeoFS network map.
type Validator struct {
ctrl AttributeAccessController
}

// New returns new Validator that checks storage nodes against provided
// [AttributeAccessController].
func New(ctrl AttributeAccessController) *Validator {
return &Validator{
ctrl: ctrl,
}
}

// various errors useful for testing.
var (
errInvalidNodeBinaryKey = errors.New("invalid node binary key")
errEmptyAttributeKey = errors.New("empty attribute key")
errEmptyAttributeValue = errors.New("empty value of the attribute")
errNonUTF8AttributeKey = errors.New("attribute key is not a valid UTF-8 string")
errDuplicatedAttributeKey = errors.New("duplicated attribute key")
)

// VerifyAndUpdate checks allowance of the storage node represented by the given
// descriptor to use declared attributes. Returns an error if at least one of
// the attributes is forbidden to be used by the storage node or access check
// cannot be done at the moment.
//
// VerifyAndUpdate also pre-checks following NeoFS protocol requirements:
// - public key binary must be a compressed ECDSA public key
// - all keys must be non-empty unique valid UTF-8 string
// - all values must be non-empty strings
//
// VerifyAndUpdate does not mutate the argument.
func (x *Validator) VerifyAndUpdate(info *netmap.NodeInfo) error {
return x.verifyAndUpdate(info)
}

// interface of [netmap.NodeInfo] used by [Validator] to bypass SDK protection
// to compose obviously invalid instance. Needed for testing only.
type nodeInfo interface {
PublicKey() []byte
NumberOfAttributes() int
IterateAttributes(func(string, string))
}

func (x *Validator) verifyAndUpdate(n nodeInfo) error {
var nodePubKey neofsecdsa.PublicKey

err := nodePubKey.Decode(n.PublicKey())
if err != nil {
return fmt.Errorf("%w: %v", errInvalidNodeBinaryKey, err)
}

mAttr := make(map[string]string, n.NumberOfAttributes())

n.IterateAttributes(func(key, value string) {
if err != nil {
return
}

if key == "" {
err = errEmptyAttributeKey
return
}

_, was := mAttr[key]
if was {
err = fmt.Errorf("%w %s", errDuplicatedAttributeKey, key)
return
}

if !utf8.ValidString(key) {
err = fmt.Errorf("%w: %q", errNonUTF8AttributeKey, key)
return
}

if value == "" {
err = fmt.Errorf("%w %q", errEmptyAttributeValue, value)
return
}

mAttr[key] = value
})
if err != nil {
return err
}

for k, v := range mAttr {
err = x.ctrl.CheckNodeAccessToAttribute(&nodePubKey, k, v)
if err != nil {
if errors.Is(err, ErrAccessDenied) {
return fmt.Errorf("%w %q=%q", ErrAccessDenied, k, v)
}
return fmt.Errorf("check access to use attribute %q=%q: %w", k, v, err)
}
}

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package attributevalidator

import (
"errors"
"fmt"
"testing"
"unicode/utf8"

neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/crypto/test"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/stretchr/testify/require"
)

type testController struct {
tb testing.TB

nodeKey neofscrypto.PublicKey
mAttr map[string]string

mProcessedAttr map[string]struct{}

retErr error
}

func newTestController(tb testing.TB, nodeKey neofscrypto.PublicKey, mAttr map[string]string) *testController {
return &testController{
tb: tb,
nodeKey: nodeKey,
mAttr: mAttr,
mProcessedAttr: make(map[string]struct{}),
}
}

func (x *testController) setRetErr(err error) {
x.retErr = err
}

func (x *testController) CheckNodeAccessToAttribute(nodeKey neofscrypto.PublicKey, attrKey, attrValue string) error {
require.Equal(x.tb, x.nodeKey, nodeKey, "node public key must be decoded from the original binary")
require.NotEmpty(x.tb, attrKey, "empty attribute key must not be passed", attrKey)
require.True(x.tb, utf8.ValidString(attrKey), "invalid UTF-8 attribute key must not be passed", attrKey)
require.NotEmptyf(x.tb, attrValue, "empty attribute value must not be passed", attrValue)

_, ok := x.mProcessedAttr[attrKey]
require.False(x.tb, ok, "attribute key must be passed once", attrKey)

x.mProcessedAttr[attrKey] = struct{}{}

val, ok := x.mAttr[attrKey]
require.True(x.tb, ok, "attribute key must be from the original attributes", attrKey)
require.Equal(x.tb, val, attrValue, "attribute value must be from the original attributes", attrValue)

return x.retErr
}

func newNodeInfo(bNodeKey []byte, mAttr map[string]string) netmap.NodeInfo {
var res netmap.NodeInfo
res.SetPublicKey(bNodeKey)

for k, v := range mAttr {
res.SetAttribute(k, v)
}

return res
}

type unprotectedNodeInfo struct {
bNodeKey []byte
attrs [][2]string
}

func (x unprotectedNodeInfo) PublicKey() []byte {
return x.bNodeKey
}

func (x unprotectedNodeInfo) NumberOfAttributes() int {
return len(x.attrs)
}

func (x unprotectedNodeInfo) IterateAttributes(f func(string, string)) {
for _, a := range x.attrs {
f(a[0], a[1])
}
}

func newUnprotectedNodeInfo(bNodeKey []byte, attrs [][2]string) nodeInfo {
return unprotectedNodeInfo{
bNodeKey: bNodeKey,
attrs: attrs,
}
}

func TestValidator_VerifyAndUpdate(t *testing.T) {
nodeKey := test.RandomSigner(t).Public()
bNodeKey := neofscrypto.PublicKeyBytes(nodeKey)

t.Run("invalid node key", func(t *testing.T) {
mAttr := map[string]string{
"any_key": "any_value",
}

node := newNodeInfo([]byte("definitely not a key"), mAttr)
ctrl := newTestController(t, nodeKey, mAttr)
v := New(ctrl)

err := v.VerifyAndUpdate(&node)
require.ErrorIs(t, err, errInvalidNodeBinaryKey)
})

for _, tc := range []struct {
desc string
attrs [][2]string
expectedErr error
}{
{
desc: "empty key",
attrs: [][2]string{{"", "any_value"}},
expectedErr: errEmptyAttributeKey,
},
{
desc: "not a UTF-8 key",
attrs: [][2]string{
{"\xed\xa0\x80", "any_value"}, // U+D800 UTF-16 high surrogate mentioned in RFC 3629
},
expectedErr: errNonUTF8AttributeKey,
},
{
desc: "empty value",
attrs: [][2]string{{"any_key", ""}},
expectedErr: errEmptyAttributeValue,
},
{
desc: "duplicated attribute",
attrs: [][2]string{
{"any_key", "val1"},
{"any_key", "val2"},
},
expectedErr: errDuplicatedAttributeKey,
},
} {
t.Run(fmt.Sprintf("invalid attributes (%s)", tc.desc), func(t *testing.T) {
node := newUnprotectedNodeInfo(bNodeKey, tc.attrs)

mAttr := make(map[string]string)
for _, a := range tc.attrs {
mAttr[a[0]] = a[1]
}

ctrl := newTestController(t, nodeKey, mAttr)
v := New(ctrl)

err := v.verifyAndUpdate(node)
require.ErrorIs(t, err, tc.expectedErr, tc.desc)
})
}

t.Run("failed check", func(t *testing.T) {
checkErr := func(expectedErr error) {
mAttr := map[string]string{
"any_key1": "any_value2",
"any_key2": "any_value2",
}

node := newNodeInfo(bNodeKey, mAttr)
ctrl := newTestController(t, nodeKey, mAttr)
v := New(ctrl)

ctrl.setRetErr(expectedErr)
err := v.VerifyAndUpdate(&node)
require.ErrorIs(t, err, expectedErr)
}

t.Run("access denied", func(t *testing.T) {
checkErr(ErrAccessDenied)
})

t.Run("other reason", func(t *testing.T) {
anyErr := errors.New("any error")
checkErr(anyErr)
})
})

mAttr := map[string]string{
"any_key1": "any_value1",
"any_key2": "any_value2",
}

node := newNodeInfo(bNodeKey, mAttr)
ctrl := newTestController(t, nodeKey, mAttr)
v := New(ctrl)

err := v.VerifyAndUpdate(&node)
require.NoError(t, err)
require.Len(t, ctrl.mProcessedAttr, len(mAttr))
}

0 comments on commit 3e2169b

Please sign in to comment.