forked from TrueCloudLab/frostfs-node
429 lines
10 KiB
Go
429 lines
10 KiB
Go
|
package object
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"crypto/ecdsa"
|
||
|
|
||
|
"github.com/nspcc-dev/neofs-api-go/acl"
|
||
|
"github.com/nspcc-dev/neofs-api-go/object"
|
||
|
"github.com/nspcc-dev/neofs-api-go/refs"
|
||
|
"github.com/nspcc-dev/neofs-api-go/service"
|
||
|
crypto "github.com/nspcc-dev/neofs-crypto"
|
||
|
libacl "github.com/nspcc-dev/neofs-node/lib/acl"
|
||
|
"github.com/nspcc-dev/neofs-node/lib/implementations"
|
||
|
"github.com/nspcc-dev/neofs-node/lib/ir"
|
||
|
"github.com/nspcc-dev/neofs-node/lib/localstore"
|
||
|
"github.com/nspcc-dev/neofs-node/lib/transport"
|
||
|
"github.com/pkg/errors"
|
||
|
"go.uber.org/zap"
|
||
|
)
|
||
|
|
||
|
type (
|
||
|
// RequestTargeter is an interface of request's ACL target calculator.
|
||
|
RequestTargeter interface {
|
||
|
Target(context.Context, serviceRequest) acl.Target
|
||
|
}
|
||
|
|
||
|
// aclPreProcessor is an implementation of requestPreProcessor interface.
|
||
|
aclPreProcessor struct {
|
||
|
log *zap.Logger
|
||
|
|
||
|
aclInfoReceiver aclInfoReceiver
|
||
|
|
||
|
basicChecker libacl.BasicChecker
|
||
|
|
||
|
reqActionCalc requestActionCalculator
|
||
|
|
||
|
localStore localstore.Localstore
|
||
|
|
||
|
extACLSource libacl.ExtendedACLSource
|
||
|
|
||
|
bearerVerifier bearerTokenVerifier
|
||
|
}
|
||
|
|
||
|
targetFinder struct {
|
||
|
log *zap.Logger
|
||
|
|
||
|
irStorage ir.Storage
|
||
|
cnrLister implementations.ContainerNodesLister
|
||
|
cnrOwnerChecker implementations.ContainerOwnerChecker
|
||
|
}
|
||
|
)
|
||
|
|
||
|
type objectHeadersSource interface {
|
||
|
getHeaders() (*Object, bool)
|
||
|
}
|
||
|
|
||
|
type requestActionCalculator interface {
|
||
|
calculateRequestAction(context.Context, requestActionParams) acl.ExtendedACLAction
|
||
|
}
|
||
|
|
||
|
type aclInfoReceiver struct {
|
||
|
basicACLGetter implementations.BasicACLGetter
|
||
|
|
||
|
basicChecker libacl.BasicChecker
|
||
|
|
||
|
targetFinder RequestTargeter
|
||
|
}
|
||
|
|
||
|
type aclInfo struct {
|
||
|
rule uint32
|
||
|
|
||
|
checkExtended bool
|
||
|
|
||
|
checkBearer bool
|
||
|
|
||
|
target acl.Target
|
||
|
}
|
||
|
|
||
|
type reqActionCalc struct {
|
||
|
extACLChecker libacl.ExtendedACLChecker
|
||
|
|
||
|
log *zap.Logger
|
||
|
}
|
||
|
|
||
|
type serviceRequestInfo struct {
|
||
|
target acl.Target
|
||
|
|
||
|
req serviceRequest
|
||
|
|
||
|
objHdrSrc objectHeadersSource
|
||
|
}
|
||
|
|
||
|
type requestObjHdrSrc struct {
|
||
|
req serviceRequest
|
||
|
|
||
|
ls localstore.Localstore
|
||
|
}
|
||
|
|
||
|
type eaclFromBearer struct {
|
||
|
bearer service.BearerToken
|
||
|
}
|
||
|
|
||
|
var _ requestPreProcessor = (*aclPreProcessor)(nil)
|
||
|
|
||
|
var errMissingSignatures = errors.New("empty signature list")
|
||
|
|
||
|
func (p *aclPreProcessor) preProcess(ctx context.Context, req serviceRequest) error {
|
||
|
if req == nil {
|
||
|
panic(pmEmptyServiceRequest)
|
||
|
}
|
||
|
|
||
|
// fetch ACL info
|
||
|
aclInfo, err := p.aclInfoReceiver.getACLInfo(ctx, req)
|
||
|
if err != nil {
|
||
|
p.log.Warn("can't get acl of the container", zap.Stringer("cid", req.CID()))
|
||
|
return errAccessDenied
|
||
|
}
|
||
|
|
||
|
// check basic ACL permissions
|
||
|
allow, err := p.basicChecker.Action(aclInfo.rule, req.Type(), aclInfo.target)
|
||
|
if err != nil || !allow {
|
||
|
return errAccessDenied
|
||
|
}
|
||
|
|
||
|
if aclInfo.target != acl.Target_System &&
|
||
|
p.basicChecker.Sticky(aclInfo.rule) &&
|
||
|
!checkObjectRequestOwnerMatch(req) {
|
||
|
return errAccessDenied
|
||
|
}
|
||
|
|
||
|
if !aclInfo.checkBearer && !aclInfo.checkExtended {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
actionParams := requestActionParams{
|
||
|
eaclSrc: p.extACLSource,
|
||
|
request: req,
|
||
|
objHdrSrc: &requestObjHdrSrc{
|
||
|
req: req,
|
||
|
ls: p.localStore,
|
||
|
},
|
||
|
target: aclInfo.target,
|
||
|
}
|
||
|
|
||
|
if aclInfo.checkBearer {
|
||
|
bearer := req.GetBearerToken()
|
||
|
|
||
|
if err := p.bearerVerifier.verifyBearerToken(ctx, req.CID(), bearer); err != nil {
|
||
|
p.log.Warn("bearer token verification failure",
|
||
|
zap.String("error", err.Error()),
|
||
|
)
|
||
|
|
||
|
return errAccessDenied
|
||
|
}
|
||
|
|
||
|
actionParams.eaclSrc = eaclFromBearer{
|
||
|
bearer: bearer,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if p.reqActionCalc.calculateRequestAction(ctx, actionParams) != acl.ActionAllow {
|
||
|
return errAccessDenied
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (t *targetFinder) Target(ctx context.Context, req serviceRequest) acl.Target {
|
||
|
ownerID, ownerKey, err := requestOwner(req)
|
||
|
if err != nil {
|
||
|
t.log.Warn("could not get request owner",
|
||
|
zap.String("error", err.Error()),
|
||
|
)
|
||
|
|
||
|
return acl.Target_Unknown
|
||
|
} else if ownerKey == nil {
|
||
|
t.log.Warn("signature with nil public key detected")
|
||
|
return acl.Target_Unknown
|
||
|
}
|
||
|
|
||
|
// if request from container owner then return Target_User
|
||
|
isOwner, err := t.cnrOwnerChecker.IsContainerOwner(ctx, req.CID(), ownerID)
|
||
|
if err != nil {
|
||
|
t.log.Warn("can't check container owner", zap.String("err", err.Error()))
|
||
|
return acl.Target_Unknown
|
||
|
} else if isOwner {
|
||
|
return acl.Target_User
|
||
|
}
|
||
|
|
||
|
ownerKeyBytes := crypto.MarshalPublicKey(ownerKey)
|
||
|
|
||
|
// if request from inner ring then return Target_System
|
||
|
isIRKey, err := ir.IsInnerRingKey(t.irStorage, ownerKeyBytes)
|
||
|
if err != nil {
|
||
|
t.log.Warn("could not verify the key belongs to the node", zap.String("err", err.Error()))
|
||
|
return acl.Target_Unknown
|
||
|
} else if isIRKey {
|
||
|
return acl.Target_System
|
||
|
}
|
||
|
|
||
|
// if request from current container node then return Target_System
|
||
|
cnr, err := t.cnrLister.ContainerNodesInfo(ctx, req.CID(), 0)
|
||
|
if err != nil {
|
||
|
t.log.Warn("can't get current container list", zap.String("err", err.Error()))
|
||
|
return acl.Target_Unknown
|
||
|
}
|
||
|
|
||
|
for i := range cnr {
|
||
|
if bytes.Equal(cnr[i].PubKey, ownerKeyBytes) {
|
||
|
return acl.Target_System
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if request from previous container node then return Target_System
|
||
|
cnr, err = t.cnrLister.ContainerNodesInfo(ctx, req.CID(), 1)
|
||
|
if err != nil {
|
||
|
t.log.Warn("can't get previous container list", zap.String("err", err.Error()))
|
||
|
return acl.Target_Unknown
|
||
|
}
|
||
|
|
||
|
for i := range cnr {
|
||
|
if bytes.Equal(cnr[i].PubKey, ownerKeyBytes) {
|
||
|
return acl.Target_System
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if none of the above return Target_Others
|
||
|
return acl.Target_Others
|
||
|
}
|
||
|
|
||
|
func checkObjectRequestOwnerMatch(req serviceRequest) bool {
|
||
|
rt := req.Type()
|
||
|
|
||
|
// ignore all request types except Put and Delete
|
||
|
if rt != object.RequestPut && rt != object.RequestDelete {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// get request owner
|
||
|
reqOwner, _, err := requestOwner(req)
|
||
|
if err != nil {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
var payloadOwner OwnerID
|
||
|
|
||
|
// get owner from request payload
|
||
|
if rt == object.RequestPut {
|
||
|
obj := req.(transport.PutInfo).GetHead()
|
||
|
if obj == nil {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
payloadOwner = obj.GetSystemHeader().OwnerID
|
||
|
} else {
|
||
|
payloadOwner = req.(*object.DeleteRequest).OwnerID
|
||
|
}
|
||
|
|
||
|
return reqOwner.Equal(payloadOwner)
|
||
|
}
|
||
|
|
||
|
// FIXME: this solution only works with healthy key-to-owner conversion.
|
||
|
func requestOwner(req serviceRequest) (OwnerID, *ecdsa.PublicKey, error) {
|
||
|
// if session token exists => return its owner
|
||
|
if token := req.GetSessionToken(); token != nil {
|
||
|
return token.GetOwnerID(), crypto.UnmarshalPublicKey(token.GetOwnerKey()), nil
|
||
|
}
|
||
|
|
||
|
signKeys := req.GetSignKeyPairs()
|
||
|
if len(signKeys) == 0 {
|
||
|
return OwnerID{}, nil, errMissingSignatures
|
||
|
}
|
||
|
|
||
|
firstKey := signKeys[0].GetPublicKey()
|
||
|
if firstKey == nil {
|
||
|
return OwnerID{}, nil, crypto.ErrEmptyPublicKey
|
||
|
}
|
||
|
|
||
|
owner, err := refs.NewOwnerID(firstKey)
|
||
|
|
||
|
return owner, firstKey, err
|
||
|
}
|
||
|
|
||
|
// HeadersOfType returns request or object headers.
|
||
|
func (s serviceRequestInfo) HeadersOfType(typ acl.HeaderType) ([]acl.Header, bool) {
|
||
|
switch typ {
|
||
|
default:
|
||
|
return nil, true
|
||
|
case acl.HdrTypeRequest:
|
||
|
return libacl.TypedHeaderSourceFromExtendedHeaders(s.req).HeadersOfType(typ)
|
||
|
case acl.HdrTypeObjSys, acl.HdrTypeObjUsr:
|
||
|
obj, ok := s.objHdrSrc.getHeaders()
|
||
|
if !ok {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
return libacl.TypedHeaderSourceFromObject(obj).HeadersOfType(typ)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Key returns a binary representation of sender public key.
|
||
|
func (s serviceRequestInfo) Key() []byte {
|
||
|
_, key, err := requestOwner(s.req)
|
||
|
if err != nil {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
return crypto.MarshalPublicKey(key)
|
||
|
}
|
||
|
|
||
|
// TypeOf returns true of object request type corresponds to passed OperationType.
|
||
|
func (s serviceRequestInfo) TypeOf(opType acl.OperationType) bool {
|
||
|
switch s.req.Type() {
|
||
|
case object.RequestGet:
|
||
|
return opType == acl.OpTypeGet
|
||
|
case object.RequestPut:
|
||
|
return opType == acl.OpTypePut
|
||
|
case object.RequestHead:
|
||
|
return opType == acl.OpTypeHead
|
||
|
case object.RequestSearch:
|
||
|
return opType == acl.OpTypeSearch
|
||
|
case object.RequestDelete:
|
||
|
return opType == acl.OpTypeDelete
|
||
|
case object.RequestRange:
|
||
|
return opType == acl.OpTypeRange
|
||
|
case object.RequestRangeHash:
|
||
|
return opType == acl.OpTypeRangeHash
|
||
|
default:
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TargetOf return true if target field is equal to passed ACL target.
|
||
|
func (s serviceRequestInfo) TargetOf(target acl.Target) bool {
|
||
|
return s.target == target
|
||
|
}
|
||
|
|
||
|
func (s requestObjHdrSrc) getHeaders() (*Object, bool) {
|
||
|
switch s.req.Type() {
|
||
|
case object.RequestSearch:
|
||
|
// object header filters is not supported in Search request now
|
||
|
return nil, true
|
||
|
case object.RequestPut:
|
||
|
// for Put we get object headers from request
|
||
|
return s.req.(transport.PutInfo).GetHead(), true
|
||
|
default:
|
||
|
tReq := &transportRequest{
|
||
|
serviceRequest: s.req,
|
||
|
}
|
||
|
|
||
|
// for other requests we get object headers from local storage
|
||
|
m, err := s.ls.Meta(tReq.GetAddress())
|
||
|
if err == nil {
|
||
|
return m.GetObject(), true
|
||
|
}
|
||
|
|
||
|
return nil, false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type requestActionParams struct {
|
||
|
eaclSrc libacl.ExtendedACLSource
|
||
|
|
||
|
request serviceRequest
|
||
|
|
||
|
objHdrSrc objectHeadersSource
|
||
|
|
||
|
target acl.Target
|
||
|
}
|
||
|
|
||
|
func (s reqActionCalc) calculateRequestAction(ctx context.Context, p requestActionParams) acl.ExtendedACLAction {
|
||
|
// get EACL table
|
||
|
table, err := p.eaclSrc.GetExtendedACLTable(ctx, p.request.CID())
|
||
|
if err != nil {
|
||
|
s.log.Warn("could not get extended acl of the container",
|
||
|
zap.Stringer("cid", p.request.CID()),
|
||
|
zap.String("error", err.Error()),
|
||
|
)
|
||
|
|
||
|
return acl.ActionUndefined
|
||
|
}
|
||
|
|
||
|
// create RequestInfo instance
|
||
|
reqInfo := &serviceRequestInfo{
|
||
|
target: p.target,
|
||
|
req: p.request,
|
||
|
objHdrSrc: p.objHdrSrc,
|
||
|
}
|
||
|
|
||
|
// calculate ACL action
|
||
|
return s.extACLChecker.Action(table, reqInfo)
|
||
|
}
|
||
|
|
||
|
func (s aclInfoReceiver) getACLInfo(ctx context.Context, req serviceRequest) (*aclInfo, error) {
|
||
|
rule, err := s.basicACLGetter.GetBasicACL(ctx, req.CID())
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
isBearer, err := s.basicChecker.Bearer(rule, req.Type())
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// fetch target from the request
|
||
|
target := s.targetFinder.Target(ctx, req)
|
||
|
|
||
|
return &aclInfo{
|
||
|
rule: rule,
|
||
|
|
||
|
checkExtended: target != acl.Target_System && s.basicChecker.Extended(rule),
|
||
|
|
||
|
target: target,
|
||
|
|
||
|
checkBearer: target != acl.Target_System && isBearer && req.GetBearerToken() != nil,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (s eaclFromBearer) GetExtendedACLTable(ctx context.Context, cid CID) (acl.ExtendedACLTable, error) {
|
||
|
table := acl.WrapEACLTable(nil)
|
||
|
|
||
|
if err := table.UnmarshalBinary(s.bearer.GetACLRules()); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return table, nil
|
||
|
}
|