[#1328] services/tree: Implement access control

Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
Evgenii Stratonikov 2022-05-05 14:00:30 +03:00 committed by fyrchik
parent 94b9e13431
commit 16e3421825
8 changed files with 332 additions and 28 deletions

View file

@ -52,16 +52,10 @@ func (s *Service) replicateLoop(ctx context.Context) {
func (s *Service) replicate(ctx context.Context, op movePair) error { func (s *Service) replicate(ctx context.Context, op movePair) error {
req := newApplyRequest(&op) req := newApplyRequest(&op)
// TODO(@fyrchik): #1328 access control err := signMessage(req, s.key)
//err := signature.SignDataWithHandler(s.key, req, func(key, sign []byte) { if err != nil {
// req.Signature = &Signature{ return fmt.Errorf("can't sign data: %w", err)
// Key: key, }
// Sign: sign,
// }
//})
//if err != nil {
// return fmt.Errorf("can't sign data: %w", err)
//}
nodes, err := s.getContainerNodes(op.cid) nodes, err := s.getContainerNodes(op.cid)
if err != nil { if err != nil {

View file

@ -6,9 +6,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama"
cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -61,7 +61,7 @@ func (s *Service) Add(_ context.Context, req *AddRequest) (*AddResponse, error)
return nil, err return nil, err
} }
err := s.verifyClient(req, cid, req.GetSignature().GetKey()) err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -91,7 +91,7 @@ func (s *Service) AddByPath(_ context.Context, req *AddByPathRequest) (*AddByPat
return nil, err return nil, err
} }
err := s.verifyClient(req, cid, req.GetSignature().GetKey()) err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -133,7 +133,7 @@ func (s *Service) Remove(_ context.Context, req *RemoveRequest) (*RemoveResponse
return nil, err return nil, err
} }
err := s.verifyClient(req, cid, req.GetSignature().GetKey()) err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -164,7 +164,7 @@ func (s *Service) Move(_ context.Context, req *MoveRequest) (*MoveResponse, erro
return nil, err return nil, err
} }
err := s.verifyClient(req, cid, req.GetSignature().GetKey()) err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -194,6 +194,11 @@ func (s *Service) GetNodeByPath(_ context.Context, req *GetNodeByPathRequest) (*
return nil, err return nil, err
} }
err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationGet)
if err != nil {
return nil, err
}
attr := b.GetPathAttribute() attr := b.GetPathAttribute()
if len(attr) == 0 { if len(attr) == 0 {
attr = pilorama.AttributeFilename attr = pilorama.AttributeFilename
@ -255,6 +260,11 @@ func (s *Service) GetSubTree(req *GetSubTreeRequest, srv TreeService_GetSubTreeS
return err return err
} }
err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationGet)
if err != nil {
return err
}
queue := []nodeDepthPair{{[]uint64{b.GetRootId()}, 0}} queue := []nodeDepthPair{{[]uint64{b.GetRootId()}, 0}}
for len(queue) != 0 { for len(queue) != 0 {
@ -293,7 +303,7 @@ func (s *Service) GetSubTree(req *GetSubTreeRequest, srv TreeService_GetSubTreeS
// Apply locally applies operation from the remote node to the tree. // Apply locally applies operation from the remote node to the tree.
func (s *Service) Apply(_ context.Context, req *ApplyRequest) (*ApplyResponse, error) { func (s *Service) Apply(_ context.Context, req *ApplyRequest) (*ApplyResponse, error) {
err := signature.VerifyServiceMessage(req) err := verifyMessage(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }

Binary file not shown.

View file

@ -36,6 +36,7 @@ message AddRequest {
string tree_id = 2; string tree_id = 2;
uint64 parent_id = 3; uint64 parent_id = 3;
repeated KeyValue meta = 4; repeated KeyValue meta = 4;
bytes bearer_token = 5;
} }
Body body = 1; Body body = 1;
@ -59,6 +60,7 @@ message AddByPathRequest {
string path_attribute = 3; string path_attribute = 3;
repeated string path = 4; repeated string path = 4;
repeated KeyValue meta = 5; repeated KeyValue meta = 5;
bytes bearer_token = 6;
} }
Body body = 1; Body body = 1;
@ -81,6 +83,7 @@ message RemoveRequest {
bytes container_id = 1; bytes container_id = 1;
string tree_id = 2; string tree_id = 2;
uint64 node_id = 3; uint64 node_id = 3;
bytes bearer_token = 4;
} }
Body body = 1; Body body = 1;
@ -104,6 +107,7 @@ message MoveRequest {
uint64 parent_id = 3; uint64 parent_id = 3;
uint64 node_id = 4; uint64 node_id = 4;
repeated KeyValue meta = 5; repeated KeyValue meta = 5;
bytes bearer_token = 6;
} }
Body body = 1; Body body = 1;
@ -128,6 +132,7 @@ message GetNodeByPathRequest {
repeated string attributes = 5; repeated string attributes = 5;
bool latest_only = 6; bool latest_only = 6;
bool all_attributes = 7; bool all_attributes = 7;
bytes bearer_token = 8;
} }
Body body = 1; Body body = 1;
@ -157,6 +162,7 @@ message GetSubTreeRequest {
// Optional depth of the traversal. Zero means return only root. // Optional depth of the traversal. Zero means return only root.
// Maximum depth is 10. // Maximum depth is 10.
uint32 depth = 4; uint32 depth = 4;
bytes bearer_token = 5;
} }
Body body = 1; Body body = 1;

BIN
pkg/services/tree/service_neofs.pb.go generated Normal file

Binary file not shown.

View file

@ -7,16 +7,26 @@ import (
"fmt" "fmt"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-api-go/v2/signature" "github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" cidSDK "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"
"github.com/nspcc-dev/neofs-sdk-go/user" "github.com/nspcc-dev/neofs-sdk-go/user"
) )
func (s *Service) verifyClient(req interface{}, cid cidSDK.ID, rawKey []byte) error { type message interface {
// TODO(@fyrchik): #1328 access control SignedDataSize() int
return nil ReadSignedData([]byte) ([]byte, error)
//nolint:govet GetSignature() *Signature
err := signature.VerifyServiceMessage(req) SetSignature(*Signature)
}
// verifyClient verifies that the request for a client operation was either signed by owner
// or contains a valid bearer token.
func (s *Service) verifyClient(req message, cid cidSDK.ID, rawBearer []byte, op eacl.Operation) error {
err := verifyMessage(req)
if err != nil { if err != nil {
return err return err
} }
@ -28,7 +38,10 @@ func (s *Service) verifyClient(req interface{}, cid cidSDK.ID, rawKey []byte) er
ownerID := cnr.Value.Owner() ownerID := cnr.Value.Owner()
pub, err := keys.NewPublicKeyFromBytes(rawKey, elliptic.P256()) if len(rawBearer) == 0 { // must be signed by the owner
// No error is expected because `VerifyDataWithSource` checks the signature.
// However, we may use different algorithms in the future, thus this check.
pub, err := keys.NewPublicKeyFromBytes(req.GetSignature().GetKey(), elliptic.P256())
if err != nil { if err != nil {
return fmt.Errorf("invalid public key: %w", err) return fmt.Errorf("invalid public key: %w", err)
} }
@ -39,6 +52,81 @@ func (s *Service) verifyClient(req interface{}, cid cidSDK.ID, rawKey []byte) er
if !actualID.Equals(ownerID) { if !actualID.Equals(ownerID) {
return errors.New("`Move` request must be signed by a container owner") return errors.New("`Move` request must be signed by a container owner")
} }
return nil
}
var bt bearer.Token
if err := bt.Unmarshal(rawBearer); err != nil {
return fmt.Errorf("invalid bearer token: %w", err)
}
if !bearer.ResolveIssuer(bt).Equals(ownerID) {
return errors.New("bearer token must be signed by the container owner")
}
if !bt.AssertContainer(cid) {
return errors.New("bearer token is created for another container")
}
if !bt.VerifySignature() {
return errors.New("invalid bearer token signature")
}
tb := bt.EACLTable()
// The default action should be DENY, so we use RoleOthers to allow token issuer
// to restrict everyone not affected by the previous rules.
// This can be simplified after nspcc-dev/neofs-sdk-go#243 .
action, found := eacl.NewValidator().CalculateAction(new(eacl.ValidationUnit).
WithEACLTable(&tb).
WithContainerID(&cid).
WithRole(eacl.RoleOthers).
WithSenderKey(req.GetSignature().GetKey()).
WithOperation(op))
if !found || action != eacl.ActionAllow {
return errors.New("operation denied by bearer eACL")
}
return nil
}
func verifyMessage(m message) error {
binBody, err := m.ReadSignedData(nil)
if err != nil {
return fmt.Errorf("marshal request body: %w", err)
}
sig := m.GetSignature()
// TODO(@cthulhu-rider): #1387 use Signature message from NeoFS API to avoid conversion
var sigV2 refs.Signature
sigV2.SetKey(sig.GetKey())
sigV2.SetSign(sig.GetSign())
sigV2.SetScheme(refs.ECDSA_SHA512)
var sigSDK neofscrypto.Signature
sigSDK.ReadFromV2(sigV2)
if !sigSDK.Verify(binBody) {
return errors.New("invalid signature")
}
return nil
}
func signMessage(m message, key *ecdsa.PrivateKey) error {
binBody, err := m.ReadSignedData(nil)
if err != nil {
return err
}
keySDK := neofsecdsa.Signer(*key)
data, err := keySDK.Sign(binBody)
if err != nil {
return err
}
rawPub := make([]byte, keySDK.Public().MaxEncodedSize())
rawPub = rawPub[:keySDK.Public().Encode(rawPub)]
m.SetSignature(&Signature{
Key: rawPub,
Sign: data,
})
return nil return nil
} }

View file

@ -0,0 +1,206 @@
package tree
import (
"crypto/ecdsa"
"crypto/sha256"
"errors"
"testing"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-api-go/v2/acl"
containercore "github.com/nspcc-dev/neofs-node/pkg/core/container"
"github.com/nspcc-dev/neofs-node/pkg/core/netmap"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
"github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl"
netmapSDK "github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/nspcc-dev/neofs-sdk-go/user"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
type dummyNetmapSource struct {
netmap.Source
}
type dummyContainerSource map[string]*containercore.Container
func (s dummyContainerSource) Get(id cid.ID) (*containercore.Container, error) {
cnt, ok := s[id.String()]
if !ok {
return nil, errors.New("container not found")
}
return cnt, nil
}
func testContainer(owner user.ID) container.Container {
var r netmapSDK.ReplicaDescriptor
r.SetNumberOfObjects(1)
var pp netmapSDK.PlacementPolicy
pp.AddReplicas(r)
var cnt container.Container
cnt.SetOwner(owner)
cnt.SetPlacementPolicy(pp)
return cnt
}
func TestMessageSign(t *testing.T) {
privs := make([]*keys.PrivateKey, 4)
for i := range privs {
p, err := keys.NewPrivateKey()
require.NoError(t, err)
privs[i] = p
}
cid1 := cidtest.ID()
cid2 := cidtest.ID()
var ownerID user.ID
user.IDFromKey(&ownerID, (ecdsa.PublicKey)(*privs[0].PublicKey()))
s := &Service{
cfg: cfg{
log: zaptest.NewLogger(t),
key: &privs[0].PrivateKey,
nmSource: dummyNetmapSource{},
cnrSource: dummyContainerSource{
cid1.String(): &containercore.Container{
Value: testContainer(ownerID),
},
},
},
}
rawCID1 := make([]byte, sha256.Size)
cid1.Encode(rawCID1)
req := &MoveRequest{
Body: &MoveRequest_Body{
ContainerId: rawCID1,
ParentId: 1,
NodeId: 2,
Meta: []*KeyValue{
{Key: "kkk", Value: []byte("vvv")},
},
},
}
t.Run("missing signature, no panic", func(t *testing.T) {
require.Error(t, s.verifyClient(req, cid2, nil, eaclSDK.OperationUnknown))
})
require.NoError(t, signMessage(req, &privs[0].PrivateKey))
require.NoError(t, s.verifyClient(req, cid1, nil, eaclSDK.OperationUnknown))
t.Run("invalid CID", func(t *testing.T) {
require.Error(t, s.verifyClient(req, cid2, nil, eaclSDK.OperationUnknown))
})
t.Run("invalid key", func(t *testing.T) {
require.NoError(t, signMessage(req, &privs[1].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, nil, eaclSDK.OperationUnknown))
})
t.Run("bearer", func(t *testing.T) {
t.Run("invalid bearer", func(t *testing.T) {
req.Body.BearerToken = []byte{0xFF}
require.NoError(t, signMessage(req, &privs[0].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut))
})
t.Run("invalid bearer CID", func(t *testing.T) {
bt := testBearerToken(cid2, privs[1].PublicKey(), privs[2].PublicKey())
require.NoError(t, bt.Sign(privs[0].PrivateKey))
req.Body.BearerToken = bt.Marshal()
require.NoError(t, signMessage(req, &privs[1].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut))
})
t.Run("invalid bearer owner", func(t *testing.T) {
bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey())
require.NoError(t, bt.Sign(privs[1].PrivateKey))
req.Body.BearerToken = bt.Marshal()
require.NoError(t, signMessage(req, &privs[1].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut))
})
t.Run("invalid bearer signature", func(t *testing.T) {
bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey())
require.NoError(t, bt.Sign(privs[0].PrivateKey))
var bv2 acl.BearerToken
bt.WriteToV2(&bv2)
bv2.GetSignature().SetSign([]byte{1, 2, 3})
req.Body.BearerToken = bv2.StableMarshal(nil)
require.NoError(t, signMessage(req, &privs[1].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut))
})
bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey())
require.NoError(t, bt.Sign(privs[0].PrivateKey))
req.Body.BearerToken = bt.Marshal()
t.Run("put and get", func(t *testing.T) {
require.NoError(t, signMessage(req, &privs[1].PrivateKey))
require.NoError(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut))
require.NoError(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationGet))
})
t.Run("only get", func(t *testing.T) {
require.NoError(t, signMessage(req, &privs[2].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut))
require.NoError(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationGet))
})
t.Run("none", func(t *testing.T) {
require.NoError(t, signMessage(req, &privs[3].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationGet))
})
})
}
func testBearerToken(cid cid.ID, forPut, forGet *keys.PublicKey) bearer.Token {
tgtGet := eaclSDK.NewTarget()
tgtGet.SetRole(eaclSDK.RoleUnknown)
tgtGet.SetBinaryKeys([][]byte{forPut.Bytes(), forGet.Bytes()})
rGet := eaclSDK.NewRecord()
rGet.SetAction(eaclSDK.ActionAllow)
rGet.SetOperation(eaclSDK.OperationGet)
rGet.SetTargets(*tgtGet)
tgtPut := eaclSDK.NewTarget()
tgtPut.SetRole(eaclSDK.RoleUnknown)
tgtPut.SetBinaryKeys([][]byte{forPut.Bytes()})
rPut := eaclSDK.NewRecord()
rPut.SetAction(eaclSDK.ActionAllow)
rPut.SetOperation(eaclSDK.OperationPut)
rPut.SetTargets(*tgtPut)
tb := eaclSDK.NewTable()
tb.AddRecord(rGet)
tb.AddRecord(rPut)
tgt := eaclSDK.NewTarget()
tgt.SetRole(eaclSDK.RoleOthers)
for _, op := range []eaclSDK.Operation{eaclSDK.OperationGet, eaclSDK.OperationPut} {
r := eaclSDK.NewRecord()
r.SetAction(eaclSDK.ActionDeny)
r.SetTargets(*tgt)
r.SetOperation(op)
tb.AddRecord(r)
}
tb.SetCID(cid)
var b bearer.Token
b.SetEACLTable(*tb)
return b
}

BIN
pkg/services/tree/types_neofs.pb.go generated Normal file

Binary file not shown.