forked from TrueCloudLab/frostfs-node
d421022547
In previous implementation node returns "access denied" on Object.Put with object with unset owner. Although object owner must be set, its absence should not be considered as access error. The same applies to sender key. Check owner ID and public key emptiness only if sticky bit is set. Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
794 lines
20 KiB
Go
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 := request.GetMetaHeader().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 {
|
|
// 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
|
|
}
|
|
|
|
if owner == nil || len(info.senderKey) == 0 {
|
|
return false
|
|
}
|
|
|
|
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()
|
|
}
|