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 | 5 + pkg/services/object/acl/{acl.go => basic.go} | 72 +++++-- pkg/services/object/acl/classifier.go | 216 +++++++++++++++++++ 3 files changed, 271 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 7a59fb51c..ff4236fc3 100644 --- a/go.sum +++ b/go.sum @@ -151,6 +151,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -174,6 +175,7 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -392,6 +394,7 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -460,6 +463,7 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -533,6 +537,7 @@ google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEt google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0 h1:Q3Ui3V3/CVinFWFiW39Iw0kMuVrRzYX0wN6OPFp0lTA= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 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 d88a4a0d4..fb6289109 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 000000000..dfe1e646e --- /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 +}