From 5045b0c3d4ce5b585fccb6cf72b7ed4083ede753 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Mon, 21 Sep 2020 16:33:49 +0300 Subject: [PATCH] [#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 --- go.sum | Bin 58606 -> 59034 bytes pkg/services/object/acl/{acl.go => basic.go} | 72 +++++-- pkg/services/object/acl/classifier.go | 216 +++++++++++++++++++ 3 files changed, 266 insertions(+), 22 deletions(-) rename pkg/services/object/acl/{acl.go => basic.go} (62%) create mode 100644 pkg/services/object/acl/classifier.go diff --git a/go.sum b/go.sum index 7a59fb51c984c4e35b5438c4e973771d0e87b022..ff4236fc38eeffebaa9d5b91fb64feb5979f5171 100644 GIT binary patch delta 297 zcmaENl6lry<_%Y@^c6A;t%|dvoXXv^EWL8djg38X14_!=$}-&}%ZgL|OI&=-GqQud zoy@gE%8Y}3C;M5cZ@z4`kwXup(ICS$$jR5s)i5=)B-1B5Go#42EGWz=(lNWhqbgb7 z%Q!H_#ZNoMF*$v*p`+Afac92G&CYuR!InB(W&}I?hg%pMBxiVrWv65%8k>|Snt4}h zdsc<#|klm~2*?nkWs{6dYC_;^~+e;A|AC?^ssm;}>O=Xzm|Yoa(Yq1F+OOvnxBW2(vOoyiv0^mPZ@2<+1M_vm(%V79%JSQ~&?~ diff --git a/pkg/services/object/acl/acl.go b/pkg/services/object/acl/basic.go similarity index 62% rename from pkg/services/object/acl/acl.go rename to pkg/services/object/acl/basic.go index d88a4a0d..fb628910 100644 --- a/pkg/services/object/acl/acl.go +++ b/pkg/services/object/acl/basic.go @@ -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 } diff --git a/pkg/services/object/acl/classifier.go b/pkg/services/object/acl/classifier.go new file mode 100644 index 00000000..dfe1e646 --- /dev/null +++ b/pkg/services/object/acl/classifier.go @@ -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 +}