frostfs-node/pkg/services/object/acl/acl.go
Leonard Lyubich 7a13053fab [#818] object/acl: Fit sticky bit specification
In previous implementation sticky bit could disrupt access of container
nodes to replication. According to NeoFS specification sticky bit should not
affect the requests sent by nodes from SYSTEM group.

Add role check to `stickyBitCheck`.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2021-09-10 15:14:59 +03:00

794 lines
20 KiB
Go

package acl
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
acl "github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
cid "github.com/nspcc-dev/neofs-api-go/pkg/container/id"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-api-go/pkg/owner"
"github.com/nspcc-dev/neofs-api-go/util/signature"
bearer "github.com/nspcc-dev/neofs-api-go/v2/acl"
"github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-api-go/v2/session"
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
core "github.com/nspcc-dev/neofs-node/pkg/core/container"
"github.com/nspcc-dev/neofs-node/pkg/core/netmap"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine"
objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object"
"github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl"
eaclV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl/v2"
)
type (
// Service checks basic ACL rules.
Service struct {
*cfg
}
putStreamBasicChecker struct {
source *Service
next objectSvc.PutObjectStream
*eACLCfg
}
getStreamBasicChecker struct {
objectSvc.GetObjectStream
info requestInfo
*eACLCfg
}
rangeStreamBasicChecker struct {
objectSvc.GetObjectRangeStream
info requestInfo
*eACLCfg
}
searchStreamBasicChecker struct {
objectSvc.SearchStream
info requestInfo
*eACLCfg
}
requestInfo struct {
basicACL basicACLHelper
requestRole acl.Role
isInnerRing bool
operation acl.Operation // put, get, head, etc.
cnrOwner *owner.ID // container owner
cid *cid.ID
oid *objectSDK.ID
senderKey []byte
bearer *bearer.BearerToken // bearer token of request
srcRequest interface{}
}
)
// Option represents Service constructor option.
type Option func(*cfg)
type cfg struct {
containers core.Source
sender SenderClassifier
next objectSvc.ServiceServer
*eACLCfg
}
type eACLCfg struct {
eACLOpts []eacl.Option
eACL *eacl.Validator
localStorage *engine.StorageEngine
state netmap.State
}
type accessErr struct {
requestInfo
failedCheckTyp string
}
var (
ErrMalformedRequest = errors.New("malformed request")
ErrUnknownRole = errors.New("can't classify request sender")
ErrUnknownContainer = errors.New("can't fetch container info")
)
func defaultCfg() *cfg {
return &cfg{
eACLCfg: new(eACLCfg),
}
}
// New is a constructor for object ACL checking service.
func New(opts ...Option) Service {
cfg := defaultCfg()
for i := range opts {
opts[i](cfg)
}
cfg.eACL = eacl.NewValidator(cfg.eACLOpts...)
return Service{
cfg: cfg,
}
}
func (b Service) Get(request *object.GetRequest, stream objectSvc.GetObjectStream) error {
cid, err := getContainerIDFromRequest(request)
if err != nil {
return err
}
sTok := originalSessionToken(request.GetMetaHeader())
req := metaWithToken{
vheader: request.GetVerificationHeader(),
token: sTok,
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := b.findRequestInfo(req, cid, acl.OperationGet)
if err != nil {
return err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
useObjectIDFromSession(&reqInfo, sTok)
if !basicACLCheck(reqInfo) {
return basicACLErr(reqInfo)
} else if !eACLCheck(request, reqInfo, b.eACLCfg) {
return eACLErr(reqInfo)
}
return b.next.Get(request, &getStreamBasicChecker{
GetObjectStream: stream,
info: reqInfo,
eACLCfg: b.eACLCfg,
})
}
func (b Service) Put(ctx context.Context) (objectSvc.PutObjectStream, error) {
streamer, err := b.next.Put(ctx)
return putStreamBasicChecker{
source: &b,
next: streamer,
eACLCfg: b.eACLCfg,
}, err
}
func (b Service) Head(
ctx context.Context,
request *object.HeadRequest) (*object.HeadResponse, error) {
cid, err := getContainerIDFromRequest(request)
if err != nil {
return nil, err
}
sTok := originalSessionToken(request.GetMetaHeader())
req := metaWithToken{
vheader: request.GetVerificationHeader(),
token: sTok,
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := b.findRequestInfo(req, cid, acl.OperationHead)
if err != nil {
return nil, err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
useObjectIDFromSession(&reqInfo, sTok)
if !basicACLCheck(reqInfo) {
return nil, basicACLErr(reqInfo)
} else if !eACLCheck(request, reqInfo, b.eACLCfg) {
return nil, eACLErr(reqInfo)
}
resp, err := b.next.Head(ctx, request)
if err == nil {
if !eACLCheck(resp, reqInfo, b.eACLCfg) {
err = eACLErr(reqInfo)
}
}
return resp, err
}
func (b Service) Search(request *object.SearchRequest, stream objectSvc.SearchStream) error {
var id *cid.ID
id, err := getContainerIDFromRequest(request)
if err != nil {
return err
}
req := metaWithToken{
vheader: request.GetVerificationHeader(),
token: originalSessionToken(request.GetMetaHeader()),
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := b.findRequestInfo(req, id, acl.OperationSearch)
if err != nil {
return err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
if !basicACLCheck(reqInfo) {
return basicACLErr(reqInfo)
} else if !eACLCheck(request, reqInfo, b.eACLCfg) {
return eACLErr(reqInfo)
}
return b.next.Search(request, &searchStreamBasicChecker{
SearchStream: stream,
info: reqInfo,
eACLCfg: b.eACLCfg,
})
}
func (b Service) Delete(
ctx context.Context,
request *object.DeleteRequest) (*object.DeleteResponse, error) {
cid, err := getContainerIDFromRequest(request)
if err != nil {
return nil, err
}
sTok := originalSessionToken(request.GetMetaHeader())
req := metaWithToken{
vheader: request.GetVerificationHeader(),
token: sTok,
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := b.findRequestInfo(req, cid, acl.OperationDelete)
if err != nil {
return nil, err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
useObjectIDFromSession(&reqInfo, sTok)
if !basicACLCheck(reqInfo) {
return nil, basicACLErr(reqInfo)
} else if !eACLCheck(request, reqInfo, b.eACLCfg) {
return nil, eACLErr(reqInfo)
}
return b.next.Delete(ctx, request)
}
func (b Service) GetRange(request *object.GetRangeRequest, stream objectSvc.GetObjectRangeStream) error {
cid, err := getContainerIDFromRequest(request)
if err != nil {
return err
}
sTok := originalSessionToken(request.GetMetaHeader())
req := metaWithToken{
vheader: request.GetVerificationHeader(),
token: sTok,
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := b.findRequestInfo(req, cid, acl.OperationRange)
if err != nil {
return err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
useObjectIDFromSession(&reqInfo, sTok)
if !basicACLCheck(reqInfo) {
return basicACLErr(reqInfo)
} else if !eACLCheck(request, reqInfo, b.eACLCfg) {
return eACLErr(reqInfo)
}
return b.next.GetRange(request, &rangeStreamBasicChecker{
GetObjectRangeStream: stream,
info: reqInfo,
eACLCfg: b.eACLCfg,
})
}
func (b Service) GetRangeHash(
ctx context.Context,
request *object.GetRangeHashRequest) (*object.GetRangeHashResponse, error) {
cid, err := getContainerIDFromRequest(request)
if err != nil {
return nil, err
}
sTok := originalSessionToken(request.GetMetaHeader())
req := metaWithToken{
vheader: request.GetVerificationHeader(),
token: sTok,
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := b.findRequestInfo(req, cid, acl.OperationRangeHash)
if err != nil {
return nil, err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
useObjectIDFromSession(&reqInfo, sTok)
if !basicACLCheck(reqInfo) {
return nil, basicACLErr(reqInfo)
} else if !eACLCheck(request, reqInfo, b.eACLCfg) {
return nil, eACLErr(reqInfo)
}
return b.next.GetRangeHash(ctx, request)
}
func (p putStreamBasicChecker) Send(request *object.PutRequest) error {
body := request.GetBody()
if body == nil {
return ErrMalformedRequest
}
part := body.GetObjectPart()
if part, ok := part.(*object.PutObjectPartInit); ok {
cid, err := getContainerIDFromRequest(request)
if err != nil {
return err
}
ownerID, err := getObjectOwnerFromMessage(request)
if err != nil {
return err
}
sTok := part.GetHeader().GetSessionToken()
req := metaWithToken{
vheader: request.GetVerificationHeader(),
token: sTok,
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := p.source.findRequestInfo(req, cid, acl.OperationPut)
if err != nil {
return err
}
reqInfo.oid = getObjectIDFromRequestBody(part)
useObjectIDFromSession(&reqInfo, sTok)
if !basicACLCheck(reqInfo) || !stickyBitCheck(reqInfo, ownerID) {
return basicACLErr(reqInfo)
} else if !eACLCheck(request, reqInfo, p.eACLCfg) {
return eACLErr(reqInfo)
}
}
return p.next.Send(request)
}
func (p putStreamBasicChecker) CloseAndRecv() (*object.PutResponse, error) {
return p.next.CloseAndRecv()
}
func (g *getStreamBasicChecker) Send(resp *object.GetResponse) error {
if _, ok := resp.GetBody().GetObjectPart().(*object.GetObjectPartInit); ok {
if !eACLCheck(resp, g.info, g.eACLCfg) {
return eACLErr(g.info)
}
}
return g.GetObjectStream.Send(resp)
}
func (g *rangeStreamBasicChecker) Send(resp *object.GetRangeResponse) error {
if !eACLCheck(resp, g.info, g.eACLCfg) {
return eACLErr(g.info)
}
return g.GetObjectRangeStream.Send(resp)
}
func (g *searchStreamBasicChecker) Send(resp *object.SearchResponse) error {
if !eACLCheck(resp, g.info, g.eACLCfg) {
return eACLErr(g.info)
}
return g.SearchStream.Send(resp)
}
func (b Service) findRequestInfo(
req metaWithToken,
cid *cid.ID,
op acl.Operation) (info requestInfo, err error) {
cnr, err := b.containers.Get(cid) // fetch actual container
if err != nil || cnr.OwnerID() == nil {
return info, ErrUnknownContainer
}
// find request role and key
role, isIR, key, err := b.sender.Classify(req, cid, cnr)
if err != nil {
return info, err
}
if role == acl.RoleUnknown {
return info, ErrUnknownRole
}
// find verb from token if it is present
verb := sourceVerbOfRequest(req, op)
// todo: check verb sanity, if it was generated correctly. Do we need it ?
info.basicACL = basicACLHelper(cnr.BasicACL())
info.requestRole = role
info.isInnerRing = isIR
info.operation = verb
info.cnrOwner = cnr.OwnerID()
info.cid = cid
// it is assumed that at the moment the key will be valid,
// otherwise the request would not pass validation
info.senderKey = key
// add bearer token if it is present in request
info.bearer = req.bearer
info.srcRequest = req.src
return info, nil
}
func getContainerIDFromRequest(req interface{}) (id *cid.ID, err error) {
switch v := req.(type) {
case *object.GetRequest:
return cid.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil
case *object.PutRequest:
objPart := v.GetBody().GetObjectPart()
if part, ok := objPart.(*object.PutObjectPartInit); ok {
return cid.NewFromV2(part.GetHeader().GetContainerID()), nil
}
return nil, errors.New("can't get cid in chunk")
case *object.HeadRequest:
return cid.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil
case *object.SearchRequest:
return cid.NewFromV2(v.GetBody().GetContainerID()), nil
case *object.DeleteRequest:
return cid.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil
case *object.GetRangeRequest:
return cid.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil
case *object.GetRangeHashRequest:
return cid.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil
default:
return nil, errors.New("unknown request type")
}
}
func useObjectIDFromSession(req *requestInfo, token *session.SessionToken) {
if token == nil {
return
}
objCtx, ok := token.GetBody().GetContext().(*session.ObjectSessionContext)
if !ok {
return
}
req.oid = objectSDK.NewIDFromV2(
objCtx.GetAddress().GetObjectID(),
)
}
func getObjectIDFromRequestBody(body interface{}) *objectSDK.ID {
switch v := body.(type) {
default:
return nil
case interface {
GetObjectID() *refs.ObjectID
}:
return objectSDK.NewIDFromV2(v.GetObjectID())
case interface {
GetAddress() *refs.Address
}:
return objectSDK.NewIDFromV2(v.GetAddress().GetObjectID())
}
}
func getObjectOwnerFromMessage(req interface{}) (id *owner.ID, err error) {
switch v := req.(type) {
case *object.PutRequest:
objPart := v.GetBody().GetObjectPart()
if part, ok := objPart.(*object.PutObjectPartInit); ok {
return owner.NewIDFromV2(part.GetHeader().GetOwnerID()), nil
}
return nil, errors.New("can't get cid in chunk")
case *object.GetResponse:
objPart := v.GetBody().GetObjectPart()
if part, ok := objPart.(*object.GetObjectPartInit); ok {
return owner.NewIDFromV2(part.GetHeader().GetOwnerID()), nil
}
return nil, errors.New("can't get cid in chunk")
default:
return nil, errors.New("unsupported request type")
}
}
// main check function for basic ACL
func basicACLCheck(info requestInfo) bool {
// check basic ACL permissions
var checkFn func(acl.Operation) bool
switch info.requestRole {
case acl.RoleUser:
checkFn = info.basicACL.UserAllowed
case acl.RoleSystem:
checkFn = info.basicACL.SystemAllowed
if info.isInnerRing {
checkFn = info.basicACL.InnerRingAllowed
}
case acl.RoleOthers:
checkFn = info.basicACL.OthersAllowed
default:
// log there
return false
}
return checkFn(info.operation)
}
func stickyBitCheck(info requestInfo, owner *owner.ID) bool {
if owner == nil || len(info.senderKey) == 0 {
return false
}
// According to NeoFS specification sticky bit has no effect on system nodes
// for correct intra-container work with objects (in particular, replication).
if info.requestRole == acl.RoleSystem {
return true
}
if !info.basicACL.Sticky() {
return true
}
requestSenderKey := unmarshalPublicKey(info.senderKey)
return isOwnerFromKey(owner, requestSenderKey)
}
func eACLCheck(msg interface{}, reqInfo requestInfo, cfg *eACLCfg) bool {
if reqInfo.basicACL.Final() {
return true
}
// if bearer token is not allowed, then ignore it
if !reqInfo.basicACL.BearerAllowed(reqInfo.operation) {
reqInfo.bearer = nil
}
// if bearer token is not present, isValidBearer returns true
if !isValidBearer(reqInfo, cfg.state) {
return false
}
hdrSrcOpts := make([]eaclV2.Option, 0, 3)
addr := objectSDK.NewAddress()
addr.SetContainerID(reqInfo.cid)
addr.SetObjectID(reqInfo.oid)
hdrSrcOpts = append(hdrSrcOpts,
eaclV2.WithLocalObjectStorage(cfg.localStorage),
eaclV2.WithAddress(addr.ToV2()),
)
if req, ok := msg.(eaclV2.Request); ok {
hdrSrcOpts = append(hdrSrcOpts, eaclV2.WithServiceRequest(req))
} else {
hdrSrcOpts = append(hdrSrcOpts,
eaclV2.WithServiceResponse(
msg.(eaclV2.Response),
reqInfo.srcRequest.(eaclV2.Request),
),
)
}
action := cfg.eACL.CalculateAction(new(eacl.ValidationUnit).
WithRole(reqInfo.requestRole).
WithOperation(reqInfo.operation).
WithContainerID(reqInfo.cid).
WithSenderKey(reqInfo.senderKey).
WithHeaderSource(
eaclV2.NewMessageHeaderSource(hdrSrcOpts...),
).
WithBearerToken(reqInfo.bearer),
)
return action == acl.ActionAllow
}
// sourceVerbOfRequest looks for verb in session token and if it is not found,
// returns reqVerb.
func sourceVerbOfRequest(req metaWithToken, reqVerb acl.Operation) acl.Operation {
if req.token != nil {
switch v := req.token.GetBody().GetContext().(type) {
case *session.ObjectSessionContext:
return tokenVerbToOperation(v.GetVerb())
default:
// do nothing, return request verb
}
}
return reqVerb
}
func tokenVerbToOperation(verb session.ObjectSessionVerb) acl.Operation {
switch verb {
case session.ObjectVerbGet:
return acl.OperationGet
case session.ObjectVerbPut:
return acl.OperationPut
case session.ObjectVerbHead:
return acl.OperationHead
case session.ObjectVerbSearch:
return acl.OperationSearch
case session.ObjectVerbDelete:
return acl.OperationDelete
case session.ObjectVerbRange:
return acl.OperationRange
case session.ObjectVerbRangeHash:
return acl.OperationRangeHash
default:
return acl.OperationUnknown
}
}
func (a *accessErr) Error() string {
return fmt.Sprintf("access to operation %v is denied by %s check", a.operation, a.failedCheckTyp)
}
func basicACLErr(info requestInfo) error {
return &accessErr{
requestInfo: info,
failedCheckTyp: "basic ACL",
}
}
func eACLErr(info requestInfo) error {
return &accessErr{
requestInfo: info,
failedCheckTyp: "extended ACL",
}
}
// isValidBearer returns true if bearer token correctly signed by authorized
// entity. This method might be define on whole ACL service because it will
// require to fetch current epoch to check lifetime.
func isValidBearer(reqInfo requestInfo, st netmap.State) bool {
token := reqInfo.bearer
// 0. Check if bearer token is present in reqInfo. It might be non nil
// empty structure.
if token == nil || (token.GetBody() == nil && token.GetSignature() == nil) {
return true
}
// 1. First check token lifetime. Simplest verification.
if !isValidLifetime(token.GetBody().GetLifetime(), st.CurrentEpoch()) {
return false
}
// 2. Then check if bearer token is signed correctly.
signWrapper := v2signature.StableMarshalerWrapper{SM: token.GetBody()}
if err := signature.VerifyDataWithSource(signWrapper, func() (key, sig []byte) {
tokenSignature := token.GetSignature()
return tokenSignature.GetKey(), tokenSignature.GetSign()
}); err != nil {
return false // invalid signature
}
// 3. Then check if container owner signed this token.
tokenIssuerKey := unmarshalPublicKey(token.GetSignature().GetKey())
if !isOwnerFromKey(reqInfo.cnrOwner, tokenIssuerKey) {
// todo: in this case we can issue all owner keys from neofs.id and check once again
return false
}
// 4. Then check if request sender has rights to use this token.
tokenOwnerField := owner.NewIDFromV2(token.GetBody().GetOwnerID())
if tokenOwnerField != nil { // see bearer token owner field description
requestSenderKey := unmarshalPublicKey(reqInfo.senderKey)
if !isOwnerFromKey(tokenOwnerField, requestSenderKey) {
// todo: in this case we can issue all owner keys from neofs.id and check once again
return false
}
}
return true
}
func isValidLifetime(lifetime *bearer.TokenLifetime, epoch uint64) bool {
// The "exp" (expiration time) claim identifies the expiration time on
// or after which the JWT MUST NOT be accepted for processing.
// The "nbf" (not before) claim identifies the time before which the JWT
// MUST NOT be accepted for processing
// RFC 7519 sections 4.1.4, 4.1.5
return epoch >= lifetime.GetNbf() && epoch <= lifetime.GetExp()
}
func isOwnerFromKey(id *owner.ID, key *keys.PublicKey) bool {
if id == nil || key == nil {
return false
}
wallet, err := owner.NEO3WalletFromPublicKey((*ecdsa.PublicKey)(key))
if err != nil {
return false
}
return id.Equal(owner.NewIDFromNeo3Wallet(wallet))
}
// originalBearerToken goes down to original request meta header and fetches
// bearer token from there.
func originalBearerToken(header *session.RequestMetaHeader) *bearer.BearerToken {
for header.GetOrigin() != nil {
header = header.GetOrigin()
}
return header.GetBearerToken()
}
// originalSessionToken goes down to original request meta header and fetches
// session token from there.
func originalSessionToken(header *session.RequestMetaHeader) *session.SessionToken {
for header.GetOrigin() != nil {
header = header.GetOrigin()
}
return header.GetSessionToken()
}