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 271 additions and 22 deletions
5
go.sum
5
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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
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