[#32] Add request sender classifier

ACL has to classify request senders by roles:
- owner of the container,
- request from container or inner ring node,
- any other request.

According to this roles ACL checker use different
bits of basic ACL to grant or deny access.

Signed-off-by: Alex Vanin <alexey@nspcc.ru>
This commit is contained in:
Alex Vanin 2020-09-21 16:33:49 +03:00
parent ab565b1862
commit 5045b0c3d4
3 changed files with 266 additions and 22 deletions

BIN
go.sum

Binary file not shown.

View file

@ -3,45 +3,60 @@ package acl
import (
"context"
"github.com/nspcc-dev/neofs-api-go/v2/acl"
"github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/pkg/errors"
)
type (
// ContainerGetter accesses NeoFS container storage.
ContainerGetter interface{}
ContainerGetter interface {
Get(*refs.ContainerID) (*container.Container, error)
}
Classifier interface {
Classify(RequestV2, *refs.ContainerID) acl.Role
}
// BasicChecker checks basic ACL rules.
BasicChecker struct {
containers ContainerGetter
next object.Service
sender SenderClassifier
next object.Service
}
putStreamBasicChecker struct {
containers ContainerGetter
next object.PutObjectStreamer
sender SenderClassifier
next object.PutObjectStreamer
}
getStreamBasicChecker struct {
containers ContainerGetter
next object.GetObjectStreamer
sender SenderClassifier
next object.GetObjectStreamer
}
searchStreamBasicChecker struct {
containers ContainerGetter
next object.SearchObjectStreamer
sender SenderClassifier
next object.SearchObjectStreamer
}
getRangeStreamBasicChecker struct {
containers ContainerGetter
next object.GetRangeObjectStreamer
sender SenderClassifier
next object.GetRangeObjectStreamer
}
)
var (
ErrMalformedRequest = errors.New("malformed request")
ErrUnknownRole = errors.New("can't classify request sender")
)
// NewBasicChecker is a constructor for basic ACL checker of object requests.
func NewBasicChecker(cnr ContainerGetter, next object.Service) BasicChecker {
func NewBasicChecker(c SenderClassifier, next object.Service) BasicChecker {
return BasicChecker{
containers: cnr,
next: next,
sender: c,
next: next,
}
}
@ -49,10 +64,23 @@ func (b BasicChecker) Get(
ctx context.Context,
request *object.GetRequest) (object.GetObjectStreamer, error) {
// get container address and do not panic at malformed request
var addr *refs.Address
if body := request.GetBody(); body == nil {
return nil, ErrMalformedRequest
} else {
addr = body.GetAddress()
}
role := b.sender.Classify(request, addr.GetContainerID())
if role == acl.RoleUnknown {
return nil, ErrUnknownRole
}
stream, err := b.next.Get(ctx, request)
return getStreamBasicChecker{
containers: b.containers,
next: stream,
sender: b.sender,
next: stream,
}, err
}
@ -60,8 +88,8 @@ func (b BasicChecker) Put(ctx context.Context) (object.PutObjectStreamer, error)
streamer, err := b.next.Put(ctx)
return putStreamBasicChecker{
containers: b.containers,
next: streamer,
sender: b.sender,
next: streamer,
}, err
}
@ -78,8 +106,8 @@ func (b BasicChecker) Search(
stream, err := b.next.Search(ctx, request)
return searchStreamBasicChecker{
containers: b.containers,
next: stream,
sender: b.sender,
next: stream,
}, err
}
@ -96,8 +124,8 @@ func (b BasicChecker) GetRange(
stream, err := b.next.GetRange(ctx, request)
return getRangeStreamBasicChecker{
containers: b.containers,
next: stream,
sender: b.sender,
next: stream,
}, err
}

View file

@ -0,0 +1,216 @@
package acl
import (
"bytes"
"crypto/ecdsa"
"github.com/nspcc-dev/neofs-api-go/pkg/netmap"
sdk "github.com/nspcc-dev/neofs-api-go/pkg/owner"
"github.com/nspcc-dev/neofs-api-go/v2/acl"
"github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-api-go/v2/session"
crypto "github.com/nspcc-dev/neofs-crypto"
"github.com/pkg/errors"
)
type (
// ContainerFetcher accesses NeoFS container storage.
// fixme: use core.container interface implementation
ContainerFetcher interface {
Fetch(*refs.ContainerID) (*container.Container, error)
}
// fixme: use core.netmap interface implementation
NetmapFetcher interface {
Current() (netmap.Netmap, error)
Previous(int) (netmap.Netmap, error)
}
InnerRingFetcher interface {
InnerRingKeys() ([][]byte, error)
}
RequestV2 interface {
GetMetaHeader() *session.RequestMetaHeader
GetVerificationHeader() *session.RequestVerificationHeader
}
SenderClassifier struct {
containers ContainerFetcher
innerRing InnerRingFetcher
netmap NetmapFetcher
}
)
// fixme: update classifier constructor
func NewSenderClassifier() SenderClassifier {
return SenderClassifier{}
}
func (c SenderClassifier) Classify(req RequestV2, cid *refs.ContainerID) acl.Role {
if cid == nil || req == nil {
// log there
return acl.RoleUnknown
}
ownerID, ownerKey, err := requestOwner(req)
if err != nil || ownerID == nil || ownerKey == nil {
// log there
return acl.RoleUnknown
}
// todo: get owner from neofs.id if present
// fetch actual container
cnr, err := c.containers.Fetch(cid)
if err != nil || cnr.GetOwnerID() == nil {
// log there
return acl.RoleUnknown
}
// if request owner is the same as container owner, return RoleUser
if bytes.Equal(cnr.GetOwnerID().GetValue(), cid.GetValue()) {
return acl.RoleUser
}
ownerKeyInBytes := crypto.MarshalPublicKey(ownerKey)
isInnerRingNode, err := c.isInnerRingKey(ownerKeyInBytes)
if err != nil {
// log there
return acl.RoleUnknown
} else if isInnerRingNode {
return acl.RoleSystem
}
isContainerNode, err := c.isContainerKey(ownerKeyInBytes, cid.GetValue(), cnr)
if err != nil {
// log there
return acl.RoleUnknown
} else if isContainerNode {
return acl.RoleSystem
}
// if none of above, return RoleOthers
return acl.RoleOthers
}
func requestOwner(req RequestV2) (*refs.OwnerID, *ecdsa.PublicKey, error) {
var (
meta = req.GetMetaHeader()
verify = req.GetVerificationHeader()
)
if meta == nil || verify == nil {
return nil, nil, errors.Wrap(ErrMalformedRequest, "nil at meta or verify header")
}
// if session token is presented, use it as truth source
if token := meta.GetSessionToken(); token != nil {
body := token.GetBody()
if body == nil {
return nil, nil, errors.Wrap(ErrMalformedRequest, "nil at session token body")
}
signature := token.GetSignature()
if signature == nil {
return nil, nil, errors.Wrap(ErrMalformedRequest, "nil at signature")
}
return body.GetOwnerID(), crypto.UnmarshalPublicKey(signature.GetKey()), nil
}
// otherwise get original body signature
bodySignature := originalBodySignature(verify)
if bodySignature == nil {
return nil, nil, errors.Wrap(ErrMalformedRequest, "nil at body signature")
}
key := crypto.UnmarshalPublicKey(bodySignature.GetKey())
neo3wallet, err := sdk.NEO3WalletFromPublicKey(key)
if err != nil {
return nil, nil, errors.Wrap(err, "can't create neo3 wallet")
}
// form owner from public key
owner := new(refs.OwnerID)
owner.SetValue(neo3wallet.Bytes())
return owner, key, nil
}
func originalBodySignature(v *session.RequestVerificationHeader) *refs.Signature {
if v == nil {
return nil
}
for v.GetOrigin() != nil {
v = v.GetOrigin()
}
return v.GetBodySignature()
}
func (c SenderClassifier) isInnerRingKey(owner []byte) (bool, error) {
innerRingKeys, err := c.innerRing.InnerRingKeys()
if err != nil {
return false, err
}
// if request owner key in the inner ring list, return RoleSystem
for i := range innerRingKeys {
if bytes.Equal(innerRingKeys[i], owner) {
return true, nil
}
}
return false, nil
}
func (c SenderClassifier) isContainerKey(
owner, cid []byte,
cnr *container.Container) (bool, error) {
// first check current netmap
nm, err := c.netmap.Current()
if err != nil {
return false, err
}
in, err := lookupKeyInContainer(nm, owner, cid, cnr)
if err != nil {
return false, err
} else if in {
return true, nil
}
// then check previous netmap, this can happen in-between epoch change
// when node migrates data from last epoch container
nm, err = c.netmap.Previous(1)
if err != nil {
return false, err
}
return lookupKeyInContainer(nm, owner, cid, cnr)
}
func lookupKeyInContainer(
nm netmap.Netmap,
owner, cid []byte,
cnr *container.Container) (bool, error) {
cnrNodes, err := nm.GetContainerNodes(cnr.GetPlacementPolicy(), cid)
if err != nil {
return false, err
}
flatCnrNodes := cnrNodes.Flatten() // we need single array to iterate on
for i := range flatCnrNodes {
if bytes.Equal(flatCnrNodes[i].InfoV2.GetPublicKey(), owner) {
return true, nil
}
}
return false, nil
}