diff --git a/go.sum b/go.sum index 7a59fb51..ff4236fc 100644 Binary files a/go.sum and b/go.sum differ 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 +}