forked from TrueCloudLab/frostfs-node
[#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:
parent
ab565b1862
commit
5045b0c3d4
3 changed files with 266 additions and 22 deletions
BIN
go.sum
BIN
go.sum
Binary file not shown.
|
@ -3,44 +3,59 @@ package acl
|
||||||
import (
|
import (
|
||||||
"context"
|
"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/object"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// ContainerGetter accesses NeoFS container storage.
|
// 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 checks basic ACL rules.
|
||||||
BasicChecker struct {
|
BasicChecker struct {
|
||||||
containers ContainerGetter
|
sender SenderClassifier
|
||||||
next object.Service
|
next object.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
putStreamBasicChecker struct {
|
putStreamBasicChecker struct {
|
||||||
containers ContainerGetter
|
sender SenderClassifier
|
||||||
next object.PutObjectStreamer
|
next object.PutObjectStreamer
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreamBasicChecker struct {
|
getStreamBasicChecker struct {
|
||||||
containers ContainerGetter
|
sender SenderClassifier
|
||||||
next object.GetObjectStreamer
|
next object.GetObjectStreamer
|
||||||
}
|
}
|
||||||
|
|
||||||
searchStreamBasicChecker struct {
|
searchStreamBasicChecker struct {
|
||||||
containers ContainerGetter
|
sender SenderClassifier
|
||||||
next object.SearchObjectStreamer
|
next object.SearchObjectStreamer
|
||||||
}
|
}
|
||||||
|
|
||||||
getRangeStreamBasicChecker struct {
|
getRangeStreamBasicChecker struct {
|
||||||
containers ContainerGetter
|
sender SenderClassifier
|
||||||
next object.GetRangeObjectStreamer
|
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.
|
// 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{
|
return BasicChecker{
|
||||||
containers: cnr,
|
sender: c,
|
||||||
next: next,
|
next: next,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,9 +64,22 @@ func (b BasicChecker) Get(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request *object.GetRequest) (object.GetObjectStreamer, error) {
|
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)
|
stream, err := b.next.Get(ctx, request)
|
||||||
return getStreamBasicChecker{
|
return getStreamBasicChecker{
|
||||||
containers: b.containers,
|
sender: b.sender,
|
||||||
next: stream,
|
next: stream,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
@ -60,7 +88,7 @@ func (b BasicChecker) Put(ctx context.Context) (object.PutObjectStreamer, error)
|
||||||
streamer, err := b.next.Put(ctx)
|
streamer, err := b.next.Put(ctx)
|
||||||
|
|
||||||
return putStreamBasicChecker{
|
return putStreamBasicChecker{
|
||||||
containers: b.containers,
|
sender: b.sender,
|
||||||
next: streamer,
|
next: streamer,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
@ -78,7 +106,7 @@ func (b BasicChecker) Search(
|
||||||
|
|
||||||
stream, err := b.next.Search(ctx, request)
|
stream, err := b.next.Search(ctx, request)
|
||||||
return searchStreamBasicChecker{
|
return searchStreamBasicChecker{
|
||||||
containers: b.containers,
|
sender: b.sender,
|
||||||
next: stream,
|
next: stream,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
@ -96,7 +124,7 @@ func (b BasicChecker) GetRange(
|
||||||
|
|
||||||
stream, err := b.next.GetRange(ctx, request)
|
stream, err := b.next.GetRange(ctx, request)
|
||||||
return getRangeStreamBasicChecker{
|
return getRangeStreamBasicChecker{
|
||||||
containers: b.containers,
|
sender: b.sender,
|
||||||
next: stream,
|
next: stream,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
216
pkg/services/object/acl/classifier.go
Normal file
216
pkg/services/object/acl/classifier.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue