[#1628] tree: Make ACL checks the same way as for object requests

1. Do not require a request to be signed by the container owner if a
bearer token is missing
2. Do not check the system role since public requests are not expected to
be signed by IR or a container node (unlike the object requests)

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
This commit is contained in:
Pavel Karpy 2022-09-08 15:44:27 +03:00 committed by fyrchik
parent 4f18893d9b
commit 876e014b5d
5 changed files with 171 additions and 72 deletions

View file

@ -21,6 +21,7 @@ func initTreeService(c *cfg) {
c.treeService = tree.New( c.treeService = tree.New(
tree.WithContainerSource(c.cfgObject.cnrSource), tree.WithContainerSource(c.cfgObject.cnrSource),
tree.WithEACLSource(c.cfgObject.eaclSource),
tree.WithNetmapSource(c.netMapSource), tree.WithNetmapSource(c.netMapSource),
tree.WithPrivateKey(&c.key.PrivateKey), tree.WithPrivateKey(&c.key.PrivateKey),
tree.WithLogger(c.log), tree.WithLogger(c.log),

View file

@ -11,12 +11,13 @@ import (
) )
type cfg struct { type cfg struct {
log *zap.Logger log *zap.Logger
key *ecdsa.PrivateKey key *ecdsa.PrivateKey
rawPub []byte rawPub []byte
nmSource netmap.Source nmSource netmap.Source
cnrSource container.Source cnrSource container.Source
forest pilorama.Forest eaclSource container.EACLSource
forest pilorama.Forest
// replication-related parameters // replication-related parameters
replicatorChannelCapacity int replicatorChannelCapacity int
replicatorWorkerCount int replicatorWorkerCount int
@ -34,6 +35,14 @@ func WithContainerSource(src container.Source) Option {
} }
} }
// WithEACLSource sets a eACL table source for a tree service.
// This option is required.
func WithEACLSource(src container.EACLSource) Option {
return func(c *cfg) {
c.eaclSource = src
}
}
// WithNetmapSource sets a netmap source for a tree service. // WithNetmapSource sets a netmap source for a tree service.
// This option is required. // This option is required.
func WithNetmapSource(src netmap.Source) Option { func WithNetmapSource(src netmap.Source) Option {

View file

@ -7,8 +7,8 @@ import (
"fmt" "fmt"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama"
"github.com/nspcc-dev/neofs-sdk-go/container/acl"
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"
netmapSDK "github.com/nspcc-dev/neofs-sdk-go/netmap" netmapSDK "github.com/nspcc-dev/neofs-sdk-go/netmap"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -69,7 +69,7 @@ func (s *Service) Add(ctx context.Context, req *AddRequest) (*AddResponse, error
return nil, err return nil, err
} }
err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut) err := s.verifyClient(req, cid, b.GetBearerToken(), acl.OpObjectPut)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -117,7 +117,7 @@ func (s *Service) AddByPath(ctx context.Context, req *AddByPathRequest) (*AddByP
return nil, err return nil, err
} }
err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut) err := s.verifyClient(req, cid, b.GetBearerToken(), acl.OpObjectPut)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -177,7 +177,7 @@ func (s *Service) Remove(ctx context.Context, req *RemoveRequest) (*RemoveRespon
return nil, err return nil, err
} }
err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut) err := s.verifyClient(req, cid, b.GetBearerToken(), acl.OpObjectPut)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -226,7 +226,7 @@ func (s *Service) Move(ctx context.Context, req *MoveRequest) (*MoveResponse, er
return nil, err return nil, err
} }
err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut) err := s.verifyClient(req, cid, b.GetBearerToken(), acl.OpObjectPut)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -274,7 +274,7 @@ func (s *Service) GetNodeByPath(ctx context.Context, req *GetNodeByPathRequest)
return nil, err return nil, err
} }
err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationGet) err := s.verifyClient(req, cid, b.GetBearerToken(), acl.OpObjectGet)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -350,7 +350,7 @@ func (s *Service) GetSubTree(req *GetSubTreeRequest, srv TreeService_GetSubTreeS
return err return err
} }
err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationGet) err := s.verifyClient(req, cid, b.GetBearerToken(), acl.OpObjectGet)
if err != nil { if err != nil {
return err return err
} }

View file

@ -8,7 +8,10 @@ import (
"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/refs" "github.com/nspcc-dev/neofs-api-go/v2/refs"
core "github.com/nspcc-dev/neofs-node/pkg/core/container"
"github.com/nspcc-dev/neofs-sdk-go/bearer" "github.com/nspcc-dev/neofs-sdk-go/bearer"
"github.com/nspcc-dev/neofs-sdk-go/client"
"github.com/nspcc-dev/neofs-sdk-go/container/acl"
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" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa"
@ -23,9 +26,20 @@ type message interface {
SetSignature(*Signature) SetSignature(*Signature)
} }
// verifyClient verifies that the request for a client operation was either signed by owner func basicACLErr(op acl.Op) error {
// or contains a valid bearer token. return fmt.Errorf("access to operation %s is denied by basic ACL check", op)
func (s *Service) verifyClient(req message, cid cidSDK.ID, rawBearer []byte, op eacl.Operation) error { }
func eACLErr(op eacl.Operation, err error) error {
return fmt.Errorf("access to operation %s is denied by extended ACL check: %w", op, err)
}
// verifyClient verifies if the request for a client operation
// was signed by a key allowed by (e)ACL rules.
// Operation must be one of:
// - 1. ObjectPut;
// - 2. ObjectGet.
func (s *Service) verifyClient(req message, cid cidSDK.ID, rawBearer []byte, op acl.Op) error {
err := verifyMessage(req) err := verifyMessage(req)
if err != nil { if err != nil {
return err return err
@ -36,53 +50,66 @@ func (s *Service) verifyClient(req message, cid cidSDK.ID, rawBearer []byte, op
return fmt.Errorf("can't get container %s: %w", cid, err) return fmt.Errorf("can't get container %s: %w", cid, err)
} }
ownerID := cnr.Value.Owner() role, err := roleFromReq(cnr, req)
if err != nil {
return fmt.Errorf("can't get request role: %w", err)
}
if len(rawBearer) == 0 { // must be signed by the owner basicACL := cnr.Value.BasicACL()
// 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 {
return fmt.Errorf("invalid public key: %w", err)
}
var actualID user.ID if !basicACL.IsOpAllowed(op, role) {
user.IDFromKey(&actualID, (ecdsa.PublicKey)(*pub)) return basicACLErr(op)
}
if !actualID.Equals(ownerID) { if !basicACL.Extendable() {
return errors.New("request must be signed by a container owner")
}
return nil return nil
} }
var bt bearer.Token eaclOp := eACLOp(op)
if err := bt.Unmarshal(rawBearer); err != nil {
return fmt.Errorf("invalid bearer token: %w", err) var tb eacl.Table
} if len(rawBearer) != 0 && basicACL.AllowedBearerRules(op) {
if !bearer.ResolveIssuer(bt).Equals(ownerID) { var bt bearer.Token
return errors.New("bearer token must be signed by the container owner") if err = bt.Unmarshal(rawBearer); err != nil {
} return eACLErr(eaclOp, fmt.Errorf("invalid bearer token: %w", err))
if !bt.AssertContainer(cid) { }
return errors.New("bearer token is created for another container") if !bearer.ResolveIssuer(bt).Equals(cnr.Value.Owner()) {
} return eACLErr(eaclOp, errors.New("bearer token must be signed by the container owner"))
if !bt.VerifySignature() { }
return errors.New("invalid bearer token signature") if !bt.AssertContainer(cid) {
return eACLErr(eaclOp, errors.New("bearer token is created for another container"))
}
if !bt.VerifySignature() {
return eACLErr(eaclOp, errors.New("invalid bearer token signature"))
}
tb = bt.EACLTable()
} else {
tbCore, err := s.eaclSource.GetEACL(cid)
if err != nil {
if client.IsErrEACLNotFound(err) {
return nil
}
return fmt.Errorf("get eACL table: %w", err)
}
tb = *tbCore.Value
} }
tb := bt.EACLTable() // The default action should be DENY.
// 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). action, found := eacl.NewValidator().CalculateAction(new(eacl.ValidationUnit).
WithEACLTable(&tb). WithEACLTable(&tb).
WithContainerID(&cid). WithContainerID(&cid).
WithRole(eacl.RoleOthers). WithRole(eACLRole(role)).
WithSenderKey(req.GetSignature().GetKey()). WithSenderKey(req.GetSignature().GetKey()).
WithOperation(op)) WithOperation(eaclOp))
if !found || action != eacl.ActionAllow { if !found {
return errors.New("operation denied by bearer eACL") return eACLErr(eaclOp, errors.New("not found allowing rules for the request"))
} else if action != eacl.ActionAllow {
return eACLErr(eaclOp, errors.New("DENY eACL rule"))
} }
return nil return nil
} }
@ -132,3 +159,44 @@ func signMessage(m message, key *ecdsa.PrivateKey) error {
return nil return nil
} }
func roleFromReq(cnr *core.Container, req message) (acl.Role, error) {
role := acl.RoleOthers
owner := cnr.Value.Owner()
pub, err := keys.NewPublicKeyFromBytes(req.GetSignature().GetKey(), elliptic.P256())
if err != nil {
return role, fmt.Errorf("invalid public key: %w", err)
}
var reqSigner user.ID
user.IDFromKey(&reqSigner, (ecdsa.PublicKey)(*pub))
if reqSigner.Equals(owner) {
role = acl.RoleOwner
}
return role, nil
}
func eACLOp(op acl.Op) eacl.Operation {
switch op {
case acl.OpObjectGet:
return eacl.OperationGet
case acl.OpObjectPut:
return eacl.OperationPut
default:
panic(fmt.Sprintf("unexpected tree service ACL operation: %s", op))
}
}
func eACLRole(role acl.Role) eacl.Role {
switch role {
case acl.RoleOwner:
return eacl.RoleUser
case acl.RoleOthers:
return eacl.RoleOthers
default:
panic(fmt.Sprintf("unexpected tree service ACL role: %s", role))
}
}

View file

@ -7,11 +7,12 @@ import (
"testing" "testing"
"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/acl" aclV2 "github.com/nspcc-dev/neofs-api-go/v2/acl"
containercore "github.com/nspcc-dev/neofs-node/pkg/core/container" containercore "github.com/nspcc-dev/neofs-node/pkg/core/container"
"github.com/nspcc-dev/neofs-node/pkg/core/netmap" "github.com/nspcc-dev/neofs-node/pkg/core/netmap"
"github.com/nspcc-dev/neofs-sdk-go/bearer" "github.com/nspcc-dev/neofs-sdk-go/bearer"
"github.com/nspcc-dev/neofs-sdk-go/container" "github.com/nspcc-dev/neofs-sdk-go/container"
"github.com/nspcc-dev/neofs-sdk-go/container/acl"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl"
@ -63,15 +64,17 @@ func TestMessageSign(t *testing.T) {
var ownerID user.ID var ownerID user.ID
user.IDFromKey(&ownerID, (ecdsa.PublicKey)(*privs[0].PublicKey())) user.IDFromKey(&ownerID, (ecdsa.PublicKey)(*privs[0].PublicKey()))
cnr := &containercore.Container{
Value: testContainer(ownerID),
}
s := &Service{ s := &Service{
cfg: cfg{ cfg: cfg{
log: zaptest.NewLogger(t), log: zaptest.NewLogger(t),
key: &privs[0].PrivateKey, key: &privs[0].PrivateKey,
nmSource: dummyNetmapSource{}, nmSource: dummyNetmapSource{},
cnrSource: dummyContainerSource{ cnrSource: dummyContainerSource{
cid1.String(): &containercore.Container{ cid1.String(): cnr,
Value: testContainer(ownerID),
},
}, },
}, },
} }
@ -90,26 +93,43 @@ func TestMessageSign(t *testing.T) {
}, },
} }
op := acl.OpObjectPut
cnr.Value.SetBasicACL(acl.PublicRW)
t.Run("missing signature, no panic", func(t *testing.T) { t.Run("missing signature, no panic", func(t *testing.T) {
require.Error(t, s.verifyClient(req, cid2, nil, eaclSDK.OperationUnknown)) require.Error(t, s.verifyClient(req, cid2, nil, op))
}) })
require.NoError(t, signMessage(req, &privs[0].PrivateKey)) require.NoError(t, signMessage(req, &privs[0].PrivateKey))
require.NoError(t, s.verifyClient(req, cid1, nil, eaclSDK.OperationUnknown)) require.NoError(t, s.verifyClient(req, cid1, nil, op))
t.Run("invalid CID", func(t *testing.T) { t.Run("invalid CID", func(t *testing.T) {
require.Error(t, s.verifyClient(req, cid2, nil, eaclSDK.OperationUnknown)) require.Error(t, s.verifyClient(req, cid2, nil, op))
}) })
cnr.Value.SetBasicACL(acl.Private)
t.Run("extension disabled", func(t *testing.T) {
require.NoError(t, signMessage(req, &privs[0].PrivateKey))
require.Error(t, s.verifyClient(req, cid2, nil, op))
})
t.Run("invalid key", func(t *testing.T) { t.Run("invalid key", func(t *testing.T) {
require.NoError(t, signMessage(req, &privs[1].PrivateKey)) require.NoError(t, signMessage(req, &privs[1].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, nil, eaclSDK.OperationUnknown)) require.Error(t, s.verifyClient(req, cid1, nil, op))
}) })
t.Run("bearer", func(t *testing.T) { t.Run("bearer", func(t *testing.T) {
bACL := acl.PrivateExtended
bACL.AllowBearerRules(op)
cnr.Value.SetBasicACL(bACL)
bACL.DisableExtension()
t.Run("invalid bearer", func(t *testing.T) { t.Run("invalid bearer", func(t *testing.T) {
req.Body.BearerToken = []byte{0xFF} req.Body.BearerToken = []byte{0xFF}
require.NoError(t, signMessage(req, &privs[0].PrivateKey)) require.NoError(t, signMessage(req, &privs[0].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut))
}) })
t.Run("invalid bearer CID", func(t *testing.T) { t.Run("invalid bearer CID", func(t *testing.T) {
@ -118,7 +138,7 @@ func TestMessageSign(t *testing.T) {
req.Body.BearerToken = bt.Marshal() req.Body.BearerToken = bt.Marshal()
require.NoError(t, signMessage(req, &privs[1].PrivateKey)) require.NoError(t, signMessage(req, &privs[1].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut))
}) })
t.Run("invalid bearer owner", func(t *testing.T) { t.Run("invalid bearer owner", func(t *testing.T) {
bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey())
@ -126,47 +146,48 @@ func TestMessageSign(t *testing.T) {
req.Body.BearerToken = bt.Marshal() req.Body.BearerToken = bt.Marshal()
require.NoError(t, signMessage(req, &privs[1].PrivateKey)) require.NoError(t, signMessage(req, &privs[1].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut))
}) })
t.Run("invalid bearer signature", func(t *testing.T) { t.Run("invalid bearer signature", func(t *testing.T) {
bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey())
require.NoError(t, bt.Sign(privs[0].PrivateKey)) require.NoError(t, bt.Sign(privs[0].PrivateKey))
var bv2 acl.BearerToken var bv2 aclV2.BearerToken
bt.WriteToV2(&bv2) bt.WriteToV2(&bv2)
bv2.GetSignature().SetSign([]byte{1, 2, 3}) bv2.GetSignature().SetSign([]byte{1, 2, 3})
req.Body.BearerToken = bv2.StableMarshal(nil) req.Body.BearerToken = bv2.StableMarshal(nil)
require.NoError(t, signMessage(req, &privs[1].PrivateKey)) require.NoError(t, signMessage(req, &privs[1].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut))
}) })
bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey())
require.NoError(t, bt.Sign(privs[0].PrivateKey)) require.NoError(t, bt.Sign(privs[0].PrivateKey))
req.Body.BearerToken = bt.Marshal() req.Body.BearerToken = bt.Marshal()
cnr.Value.SetBasicACL(acl.PublicRWExtended)
t.Run("put and get", func(t *testing.T) { t.Run("put and get", func(t *testing.T) {
require.NoError(t, signMessage(req, &privs[1].PrivateKey)) 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(), acl.OpObjectPut))
require.NoError(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationGet)) require.NoError(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectGet))
}) })
t.Run("only get", func(t *testing.T) { t.Run("only get", func(t *testing.T) {
require.NoError(t, signMessage(req, &privs[2].PrivateKey)) require.NoError(t, signMessage(req, &privs[2].PrivateKey))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut))
require.NoError(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationGet)) require.NoError(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectGet))
}) })
t.Run("none", func(t *testing.T) { t.Run("none", func(t *testing.T) {
require.NoError(t, signMessage(req, &privs[3].PrivateKey)) 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(), acl.OpObjectPut))
require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationGet)) require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectGet))
}) })
}) })
} }
func testBearerToken(cid cid.ID, forPut, forGet *keys.PublicKey) bearer.Token { func testBearerToken(cid cid.ID, forPutGet, forGet *keys.PublicKey) bearer.Token {
tgtGet := eaclSDK.NewTarget() tgtGet := eaclSDK.NewTarget()
tgtGet.SetRole(eaclSDK.RoleUnknown) tgtGet.SetRole(eaclSDK.RoleUnknown)
tgtGet.SetBinaryKeys([][]byte{forPut.Bytes(), forGet.Bytes()}) tgtGet.SetBinaryKeys([][]byte{forPutGet.Bytes(), forGet.Bytes()})
rGet := eaclSDK.NewRecord() rGet := eaclSDK.NewRecord()
rGet.SetAction(eaclSDK.ActionAllow) rGet.SetAction(eaclSDK.ActionAllow)
@ -175,7 +196,7 @@ func testBearerToken(cid cid.ID, forPut, forGet *keys.PublicKey) bearer.Token {
tgtPut := eaclSDK.NewTarget() tgtPut := eaclSDK.NewTarget()
tgtPut.SetRole(eaclSDK.RoleUnknown) tgtPut.SetRole(eaclSDK.RoleUnknown)
tgtPut.SetBinaryKeys([][]byte{forPut.Bytes()}) tgtPut.SetBinaryKeys([][]byte{forPutGet.Bytes()})
rPut := eaclSDK.NewRecord() rPut := eaclSDK.NewRecord()
rPut.SetAction(eaclSDK.ActionAllow) rPut.SetAction(eaclSDK.ActionAllow)