-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
innerring: Prepare a platform for private attributes of storage nodes
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
1 parent
3749ce7
commit 3e2169b
Showing
4 changed files
with
337 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
pkg/innerring/processors/netmap/nodevalidation/attributes/validator.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
196 changes: 196 additions & 0 deletions
196
pkg/innerring/processors/netmap/nodevalidation/attributes/validator_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |