[#1111] object/acl: Refactor service

Make all operations that related to `neofs-api-go` library be placed in `v2`
packages. They parse all v2-versioned structs info `neofs-sdk-go`
abstractions and pass them to the corresponding `acl`/`eacl` packages. `v2`
packages are the only packages that do import `neofs-api-go` library. `eacl`
and `acl` provide public functions that only accepts `sdk` structures.

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
This commit is contained in:
Pavel Karpy 2022-02-11 15:25:05 +03:00 committed by Alex Vanin
parent 7ccd1625af
commit 99b31e3235
17 changed files with 1266 additions and 1000 deletions

View file

@ -19,6 +19,7 @@ import (
objectTransportGRPC "github.com/nspcc-dev/neofs-node/pkg/network/transport/object/grpc" objectTransportGRPC "github.com/nspcc-dev/neofs-node/pkg/network/transport/object/grpc"
objectService "github.com/nspcc-dev/neofs-node/pkg/services/object" objectService "github.com/nspcc-dev/neofs-node/pkg/services/object"
"github.com/nspcc-dev/neofs-node/pkg/services/object/acl" "github.com/nspcc-dev/neofs-node/pkg/services/object/acl"
v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2"
deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete"
deletesvcV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/delete/v2" deletesvcV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/delete/v2"
getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get"
@ -194,7 +195,7 @@ func initObjectService(c *cfg) {
coreConstructor := (*coreClientConstructor)(clientConstructor) coreConstructor := (*coreClientConstructor)(clientConstructor)
var irFetcher acl.InnerRingFetcher var irFetcher v2.InnerRingFetcher
if c.cfgMorph.client.ProbeNotary() { if c.cfgMorph.client.ProbeNotary() {
irFetcher = &innerRingFetcherWithNotary{ irFetcher = &innerRingFetcherWithNotary{
@ -345,21 +346,22 @@ func initObjectService(c *cfg) {
}, },
) )
aclSvc := acl.New( aclSvc := v2.New(
acl.WithSenderClassifier( v2.WithLogger(c.log),
acl.NewSenderClassifier( v2.WithIRFetcher(irFetcher),
c.log, v2.WithNetmapClient(c.cfgNetmap.wrapper),
irFetcher, v2.WithContainerSource(
c.cfgNetmap.wrapper,
),
),
acl.WithContainerSource(
c.cfgObject.cnrSource, c.cfgObject.cnrSource,
), ),
acl.WithNextService(splitSvc), v2.WithNextService(splitSvc),
acl.WithLocalStorage(ls), v2.WithEACLChecker(
acl.WithEACLSource(c.cfgObject.eaclSource), acl.NewChecker(new(acl.CheckerPrm).
acl.WithNetmapState(c.cfgNetmap.state), SetNetmapState(c.cfgNetmap.state).
SetEACLSource(c.cfgObject.eaclSource).
SetValidator(eaclSDK.NewValidator()).
SetLocalStorage(ls),
),
),
) )
respSvc := objectService.NewResponseService( respSvc := objectService.NewResponseService(

View file

@ -1,614 +1,137 @@
package acl package acl
import ( import (
"context"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic"
"errors" "errors"
"fmt" "fmt"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
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"
"github.com/nspcc-dev/neofs-node/pkg/core/container" "github.com/nspcc-dev/neofs-node/pkg/core/container"
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/core/netmap"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" "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" "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl"
eaclV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl/v2" eaclV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl/v2"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id" v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2"
eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl"
objectSDKAddress "github.com/nspcc-dev/neofs-sdk-go/object/address" addressSDK "github.com/nspcc-dev/neofs-sdk-go/object/address"
objectSDKID "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/owner" "github.com/nspcc-dev/neofs-sdk-go/owner"
"github.com/nspcc-dev/neofs-sdk-go/util/signature" bearerSDK "github.com/nspcc-dev/neofs-sdk-go/token"
) )
type ( // CheckerPrm groups parameters for Checker
// Service checks basic ACL rules. // constructor.
Service struct { type CheckerPrm struct {
*cfg eaclSrc eacl.Source
} validator *eaclSDK.Validator
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 eaclSDK.Role
isInnerRing bool
operation eaclSDK.Operation // put, get, head, etc.
cnrOwner *owner.ID // container owner
cid *cid.ID
oid *objectSDKID.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 {
eaclSource eacl.Source
eACL *eaclSDK.Validator
localStorage *engine.StorageEngine localStorage *engine.StorageEngine
state netmap.State
state netmap.State
} }
type accessErr struct { func (c *CheckerPrm) SetEACLSource(v eacl.Source) *CheckerPrm {
requestInfo c.eaclSrc = v
return c
failedCheckTyp string
} }
var ( func (c *CheckerPrm) SetValidator(v *eaclSDK.Validator) *CheckerPrm {
ErrMalformedRequest = errors.New("malformed request") c.validator = v
ErrUnknownRole = errors.New("can't classify request sender") return c
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 (c *CheckerPrm) SetLocalStorage(v *engine.StorageEngine) *CheckerPrm {
func New(opts ...Option) Service { c.localStorage = v
cfg := defaultCfg() return c
for i := range opts {
opts[i](cfg)
}
cfg.eACL = eaclSDK.NewValidator()
return Service{
cfg: cfg,
}
} }
func (b Service) Get(request *object.GetRequest, stream objectSvc.GetObjectStream) error { func (c *CheckerPrm) SetNetmapState(v netmap.State) *CheckerPrm {
idCnr, err := getContainerIDFromRequest(request) c.state = v
if err != nil { return c
return err
}
sTok := originalSessionToken(request.GetMetaHeader())
req := metaWithToken{
vheader: request.GetVerificationHeader(),
token: sTok,
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := b.findRequestInfo(req, idCnr, eaclSDK.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) { // Checker implements v2.ACLChecker interfaces and provides
streamer, err := b.next.Put(ctx) // ACL/eACL validation functionality.
type Checker struct {
return putStreamBasicChecker{ eaclSrc eacl.Source
source: &b, validator *eaclSDK.Validator
next: streamer, localStorage *engine.StorageEngine
eACLCfg: b.eACLCfg, state netmap.State
}, err
} }
func (b Service) Head( // NewChecker creates Checker.
ctx context.Context, // Panics if at least one of the parameter is nil.
request *object.HeadRequest) (*object.HeadResponse, error) { func NewChecker(prm *CheckerPrm) *Checker {
idCnr, err := getContainerIDFromRequest(request) panicOnNil := func(fieldName string, field interface{}) {
if err != nil { if field == nil {
return nil, err panic(fmt.Sprintf("incorrect field %s (%T): %v", fieldName, field, field))
}
sTok := originalSessionToken(request.GetMetaHeader())
req := metaWithToken{
vheader: request.GetVerificationHeader(),
token: sTok,
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := b.findRequestInfo(req, idCnr, eaclSDK.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 panicOnNil("EACLSource", prm.eaclSrc)
} panicOnNil("EACLValidator", prm.validator)
panicOnNil("LocalStorageEngine", prm.localStorage)
panicOnNil("NetmapState", prm.state)
func (b Service) Search(request *object.SearchRequest, stream objectSvc.SearchStream) error { return &Checker{
var id *cid.ID eaclSrc: prm.eaclSrc,
validator: prm.validator,
id, err := getContainerIDFromRequest(request) localStorage: prm.localStorage,
if err != nil { state: prm.state,
return err
}
req := metaWithToken{
vheader: request.GetVerificationHeader(),
token: originalSessionToken(request.GetMetaHeader()),
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := b.findRequestInfo(req, id, eaclSDK.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) {
idCnr, 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, idCnr, eaclSDK.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 {
idCnr, 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, idCnr, eaclSDK.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) {
idCnr, 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, idCnr, eaclSDK.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 {
idCnr, 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, idCnr, eaclSDK.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 eaclSDK.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 == eaclSDK.RoleUnknown {
return info, ErrUnknownRole
}
// find verb from token if it is present
verb := sourceVerbOfRequest(req, op)
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.Token) { // CheckBasicACL is a main check function for basic ACL.
if token == nil { func (c *Checker) CheckBasicACL(info v2.RequestInfo) bool {
return
}
objCtx, ok := token.GetBody().GetContext().(*session.ObjectSessionContext)
if !ok {
return
}
req.oid = objectSDKID.NewIDFromV2(
objCtx.GetAddress().GetObjectID(),
)
}
func getObjectIDFromRequestBody(body interface{}) *objectSDKID.ID {
switch v := body.(type) {
default:
return nil
case interface {
GetObjectID() *refs.ObjectID
}:
return objectSDKID.NewIDFromV2(v.GetObjectID())
case interface {
GetAddress() *refs.Address
}:
return objectSDKID.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 // check basic ACL permissions
var checkFn func(eaclSDK.Operation) bool var checkFn func(eaclSDK.Operation) bool
switch info.requestRole { switch info.RequestRole() {
case eaclSDK.RoleUser: case eaclSDK.RoleUser:
checkFn = info.basicACL.UserAllowed checkFn = basicACLHelper(info.BasicACL()).UserAllowed
case eaclSDK.RoleSystem: case eaclSDK.RoleSystem:
checkFn = info.basicACL.SystemAllowed checkFn = basicACLHelper(info.BasicACL()).SystemAllowed
if info.isInnerRing { if info.IsInnerRing() {
checkFn = info.basicACL.InnerRingAllowed checkFn = basicACLHelper(info.BasicACL()).InnerRingAllowed
} }
case eaclSDK.RoleOthers: case eaclSDK.RoleOthers:
checkFn = info.basicACL.OthersAllowed checkFn = basicACLHelper(info.BasicACL()).OthersAllowed
default: default:
// log there // log there
return false return false
} }
return checkFn(info.operation) return checkFn(info.Operation())
} }
func stickyBitCheck(info requestInfo, owner *owner.ID) bool { // StickyBitCheck validates owner field in the request if sticky bit is enabled.
func (c *Checker) StickyBitCheck(info v2.RequestInfo, owner *owner.ID) bool {
// According to NeoFS specification sticky bit has no effect on system nodes // According to NeoFS specification sticky bit has no effect on system nodes
// for correct intra-container work with objects (in particular, replication). // for correct intra-container work with objects (in particular, replication).
if info.requestRole == eaclSDK.RoleSystem { if info.RequestRole() == eaclSDK.RoleSystem {
return true return true
} }
if !info.basicACL.Sticky() { if !basicACLHelper(info.BasicACL()).Sticky() {
return true return true
} }
if owner == nil || len(info.senderKey) == 0 { if owner == nil || len(info.SenderKey()) == 0 {
return false return false
} }
requestSenderKey := unmarshalPublicKey(info.senderKey) requestSenderKey := unmarshalPublicKey(info.SenderKey())
return isOwnerFromKey(owner, requestSenderKey) return isOwnerFromKey(owner, requestSenderKey)
} }
func eACLCheck(msg interface{}, reqInfo requestInfo, cfg *eACLCfg) bool { // CheckEACL is a main check function for extended ACL.
if reqInfo.basicACL.Final() { func (c *Checker) CheckEACL(msg interface{}, reqInfo v2.RequestInfo) bool {
if basicACLHelper(reqInfo.BasicACL()).Final() {
return true return true
} }
// if bearer token is not allowed, then ignore it // if bearer token is not allowed, then ignore it
if !reqInfo.basicACL.BearerAllowed(reqInfo.operation) { if !basicACLHelper(reqInfo.BasicACL()).BearerAllowed(reqInfo.Operation()) {
reqInfo.bearer = nil reqInfo.CleanBearer()
} }
var ( var (
@ -616,29 +139,29 @@ func eACLCheck(msg interface{}, reqInfo requestInfo, cfg *eACLCfg) bool {
err error err error
) )
if reqInfo.bearer == nil { if reqInfo.Bearer().Empty() {
table, err = cfg.eaclSource.GetEACL(reqInfo.cid) table, err = c.eaclSrc.GetEACL(reqInfo.ContainerID())
if err != nil { if err != nil {
return errors.Is(err, container.ErrEACLNotFound) return errors.Is(err, container.ErrEACLNotFound)
} }
} else { } else {
table = eaclSDK.NewTableFromV2(reqInfo.bearer.GetBody().GetEACL()) table = reqInfo.Bearer().EACLTable()
} }
// if bearer token is not present, isValidBearer returns true // if bearer token is not present, isValidBearer returns true
if !isValidBearer(reqInfo, cfg.state) { if !isValidBearer(reqInfo, c.state) {
return false return false
} }
hdrSrcOpts := make([]eaclV2.Option, 0, 3) hdrSrcOpts := make([]eaclV2.Option, 0, 3)
addr := objectSDKAddress.NewAddress() addr := addressSDK.NewAddress()
addr.SetContainerID(reqInfo.cid) addr.SetContainerID(reqInfo.ContainerID())
addr.SetObjectID(reqInfo.oid) addr.SetObjectID(reqInfo.ObjectID())
hdrSrcOpts = append(hdrSrcOpts, hdrSrcOpts = append(hdrSrcOpts,
eaclV2.WithLocalObjectStorage(cfg.localStorage), eaclV2.WithLocalObjectStorage(c.localStorage),
eaclV2.WithAddress(addr.ToV2()), eaclV2.WithAddress(addr),
) )
if req, ok := msg.(eaclV2.Request); ok { if req, ok := msg.(eaclV2.Request); ok {
@ -647,16 +170,16 @@ func eACLCheck(msg interface{}, reqInfo requestInfo, cfg *eACLCfg) bool {
hdrSrcOpts = append(hdrSrcOpts, hdrSrcOpts = append(hdrSrcOpts,
eaclV2.WithServiceResponse( eaclV2.WithServiceResponse(
msg.(eaclV2.Response), msg.(eaclV2.Response),
reqInfo.srcRequest.(eaclV2.Request), reqInfo.Request().(eaclV2.Request),
), ),
) )
} }
action := cfg.eACL.CalculateAction(new(eaclSDK.ValidationUnit). action := c.validator.CalculateAction(new(eaclSDK.ValidationUnit).
WithRole(reqInfo.requestRole). WithRole(reqInfo.RequestRole()).
WithOperation(reqInfo.operation). WithOperation(reqInfo.Operation()).
WithContainerID(reqInfo.cid). WithContainerID(reqInfo.ContainerID()).
WithSenderKey(reqInfo.senderKey). WithSenderKey(reqInfo.SenderKey()).
WithHeaderSource( WithHeaderSource(
eaclV2.NewMessageHeaderSource(hdrSrcOpts...), eaclV2.NewMessageHeaderSource(hdrSrcOpts...),
). ).
@ -666,97 +189,39 @@ func eACLCheck(msg interface{}, reqInfo requestInfo, cfg *eACLCfg) bool {
return action == eaclSDK.ActionAllow return action == eaclSDK.ActionAllow
} }
// sourceVerbOfRequest looks for verb in session token and if it is not found,
// returns reqVerb.
func sourceVerbOfRequest(req metaWithToken, reqVerb eaclSDK.Operation) eaclSDK.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) eaclSDK.Operation {
switch verb {
case session.ObjectVerbGet:
return eaclSDK.OperationGet
case session.ObjectVerbPut:
return eaclSDK.OperationPut
case session.ObjectVerbHead:
return eaclSDK.OperationHead
case session.ObjectVerbSearch:
return eaclSDK.OperationSearch
case session.ObjectVerbDelete:
return eaclSDK.OperationDelete
case session.ObjectVerbRange:
return eaclSDK.OperationRange
case session.ObjectVerbRangeHash:
return eaclSDK.OperationRangeHash
default:
return eaclSDK.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 // isValidBearer returns true if bearer token correctly signed by authorized
// entity. This method might be define on whole ACL service because it will // entity. This method might be defined on whole ACL service because it will
// require to fetch current epoch to check lifetime. // require fetching current epoch to check lifetime.
func isValidBearer(reqInfo requestInfo, st netmap.State) bool { func isValidBearer(reqInfo v2.RequestInfo, st netmap.State) bool {
token := reqInfo.bearer token := reqInfo.Bearer()
// 0. Check if bearer token is present in reqInfo. It might be non nil // 0. Check if bearer token is present in reqInfo. It might be non nil
// empty structure. // empty structure.
if token == nil || (token.GetBody() == nil && token.GetSignature() == nil) { if token == nil || token.Empty() {
return true return true
} }
// 1. First check token lifetime. Simplest verification. // 1. First check token lifetime. Simplest verification.
if !isValidLifetime(token.GetBody().GetLifetime(), st.CurrentEpoch()) { if !isValidLifetime(token, st.CurrentEpoch()) {
return false return false
} }
// 2. Then check if bearer token is signed correctly. // 2. Then check if bearer token is signed correctly.
signWrapper := v2signature.StableMarshalerWrapper{SM: token.GetBody()} if err := token.VerifySignature(); err != nil {
if err := signature.VerifyDataWithSource(signWrapper, func() (key, sig []byte) {
tokenSignature := token.GetSignature()
return tokenSignature.GetKey(), tokenSignature.GetSign()
}); err != nil {
return false // invalid signature return false // invalid signature
} }
// 3. Then check if container owner signed this token. // 3. Then check if container owner signed this token.
tokenIssuerKey := unmarshalPublicKey(token.GetSignature().GetKey()) tokenIssuerKey := unmarshalPublicKey(token.Signature().Key())
if !isOwnerFromKey(reqInfo.cnrOwner, tokenIssuerKey) { if !isOwnerFromKey(reqInfo.ContainerOwner(), tokenIssuerKey) {
// TODO: #767 in this case we can issue all owner keys from neofs.id and check once again // TODO: #767 in this case we can issue all owner keys from neofs.id and check once again
return false return false
} }
// 4. Then check if request sender has rights to use this token. // 4. Then check if request sender has rights to use this token.
tokenOwnerField := owner.NewIDFromV2(token.GetBody().GetOwnerID()) tokenOwnerField := token.OwnerID()
if tokenOwnerField != nil { // see bearer token owner field description if tokenOwnerField != nil { // see bearer token owner field description
requestSenderKey := unmarshalPublicKey(reqInfo.senderKey) requestSenderKey := unmarshalPublicKey(reqInfo.SenderKey())
if !isOwnerFromKey(tokenOwnerField, requestSenderKey) { if !isOwnerFromKey(tokenOwnerField, requestSenderKey) {
// TODO: #767 in this case we can issue all owner keys from neofs.id and check once again // TODO: #767 in this case we can issue all owner keys from neofs.id and check once again
return false return false
@ -766,13 +231,13 @@ func isValidBearer(reqInfo requestInfo, st netmap.State) bool {
return true return true
} }
func isValidLifetime(lifetime *bearer.TokenLifetime, epoch uint64) bool { func isValidLifetime(t *bearerSDK.BearerToken, epoch uint64) bool {
// The "exp" (expiration time) claim identifies the expiration time on // The "exp" (expiration time) claim identifies the expiration time on
// or after which the JWT MUST NOT be accepted for processing. // or after which the JWT MUST NOT be accepted for processing.
// The "nbf" (not before) claim identifies the time before which the JWT // The "nbf" (not before) claim identifies the time before which the JWT
// MUST NOT be accepted for processing // MUST NOT be accepted for processing
// RFC 7519 sections 4.1.4, 4.1.5 // RFC 7519 sections 4.1.4, 4.1.5
return epoch >= lifetime.GetNbf() && epoch <= lifetime.GetExp() return epoch >= t.NotBeforeTime() && epoch <= t.Expiration()
} }
func isOwnerFromKey(id *owner.ID, key *keys.PublicKey) bool { func isOwnerFromKey(id *owner.ID, key *keys.PublicKey) bool {
@ -783,22 +248,10 @@ func isOwnerFromKey(id *owner.ID, key *keys.PublicKey) bool {
return id.Equal(owner.NewIDFromPublicKey((*ecdsa.PublicKey)(key))) return id.Equal(owner.NewIDFromPublicKey((*ecdsa.PublicKey)(key)))
} }
// originalBearerToken goes down to original request meta header and fetches func unmarshalPublicKey(bs []byte) *keys.PublicKey {
// bearer token from there. pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256())
func originalBearerToken(header *session.RequestMetaHeader) *bearer.BearerToken { if err != nil {
for header.GetOrigin() != nil { return nil
header = header.GetOrigin()
} }
return pub
return header.GetBearerToken()
}
// originalSessionToken goes down to original request meta header and fetches
// session token from there.
func originalSessionToken(header *session.RequestMetaHeader) *session.Token {
for header.GetOrigin() != nil {
header = header.GetOrigin()
}
return header.GetSessionToken()
} }

View file

@ -3,71 +3,66 @@ package acl
import ( import (
"testing" "testing"
"github.com/nspcc-dev/neofs-api-go/v2/acl" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine"
acltest "github.com/nspcc-dev/neofs-api-go/v2/acl/test" v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2"
"github.com/nspcc-dev/neofs-api-go/v2/session" cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id"
sessiontest "github.com/nspcc-dev/neofs-api-go/v2/session/test" eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
"github.com/nspcc-dev/neofs-sdk-go/owner" "github.com/nspcc-dev/neofs-sdk-go/owner"
ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test" ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestOriginalTokens(t *testing.T) { type emptyEACLSource struct{}
sToken := sessiontest.GenerateSessionToken(false)
bToken := acltest.GenerateBearerToken(false)
for i := 0; i < 10; i++ { func (e emptyEACLSource) GetEACL(_ *cidSDK.ID) (*eaclSDK.Table, error) {
metaHeaders := testGenerateMetaHeader(uint32(i), bToken, sToken) return nil, nil
require.Equal(t, sToken, originalSessionToken(metaHeaders), i)
require.Equal(t, bToken, originalBearerToken(metaHeaders), i)
}
} }
func testGenerateMetaHeader(depth uint32, b *acl.BearerToken, s *session.Token) *session.RequestMetaHeader { type emptyNetmapState struct{}
metaHeader := new(session.RequestMetaHeader)
metaHeader.SetBearerToken(b)
metaHeader.SetSessionToken(s)
for i := uint32(0); i < depth; i++ { func (e emptyNetmapState) CurrentEpoch() uint64 {
link := metaHeader return 0
metaHeader = new(session.RequestMetaHeader)
metaHeader.SetOrigin(link)
}
return metaHeader
} }
func TestStickyCheck(t *testing.T) { func TestStickyCheck(t *testing.T) {
checker := NewChecker(new(CheckerPrm).
SetLocalStorage(&engine.StorageEngine{}).
SetValidator(eaclSDK.NewValidator()).
SetEACLSource(emptyEACLSource{}).
SetNetmapState(emptyNetmapState{}),
)
t.Run("system role", func(t *testing.T) { t.Run("system role", func(t *testing.T) {
var info requestInfo var info v2.RequestInfo
info.senderKey = make([]byte, 33) // any non-empty key info.SetSenderKey(make([]byte, 33)) // any non-empty key
info.requestRole = eacl.RoleSystem info.SetRequestRole(eaclSDK.RoleSystem)
info.basicACL.SetSticky() setSticky(&info, true)
require.True(t, stickyBitCheck(info, ownertest.ID()))
info.basicACL.ResetSticky() require.True(t, checker.StickyBitCheck(info, ownertest.ID()))
require.True(t, stickyBitCheck(info, ownertest.ID()))
setSticky(&info, false)
require.True(t, checker.StickyBitCheck(info, ownertest.ID()))
}) })
t.Run("owner ID and/or public key emptiness", func(t *testing.T) { t.Run("owner ID and/or public key emptiness", func(t *testing.T) {
var info requestInfo var info v2.RequestInfo
info.requestRole = eacl.RoleOthers // should be non-system role info.SetRequestRole(eaclSDK.RoleOthers) // should be non-system role
assertFn := func(isSticky, withKey, withOwner, expected bool) { assertFn := func(isSticky, withKey, withOwner, expected bool) {
if isSticky { if isSticky {
info.basicACL.SetSticky() setSticky(&info, true)
} else { } else {
info.basicACL.ResetSticky() setSticky(&info, false)
} }
if withKey { if withKey {
info.senderKey = make([]byte, 33) info.SetSenderKey(make([]byte, 33))
} else { } else {
info.senderKey = nil info.SetSenderKey(nil)
} }
var ownerID *owner.ID var ownerID *owner.ID
@ -76,7 +71,7 @@ func TestStickyCheck(t *testing.T) {
ownerID = ownertest.ID() ownerID = ownertest.ID()
} }
require.Equal(t, expected, stickyBitCheck(info, ownerID)) require.Equal(t, expected, checker.StickyBitCheck(info, ownerID))
} }
assertFn(true, false, false, false) assertFn(true, false, false, false)
@ -88,3 +83,15 @@ func TestStickyCheck(t *testing.T) {
assertFn(false, true, true, true) assertFn(false, true, true, true)
}) })
} }
func setSticky(req *v2.RequestInfo, enabled bool) {
bh := basicACLHelper(req.BasicACL())
if enabled {
bh.SetSticky()
} else {
bh.ResetSticky()
}
req.SetBasicACL(uint32(bh))
}

View file

@ -28,17 +28,15 @@ const (
const leftACLBitPos = opOffset + bitsPerOp*opNumber - 1 const leftACLBitPos = opOffset + bitsPerOp*opNumber - 1
var ( var order = map[eacl.Operation]uint8{
order = map[eacl.Operation]uint8{ eacl.OperationRangeHash: 0,
eacl.OperationRangeHash: 0, eacl.OperationRange: 1,
eacl.OperationRange: 1, eacl.OperationSearch: 2,
eacl.OperationSearch: 2, eacl.OperationDelete: 3,
eacl.OperationDelete: 3, eacl.OperationPut: 4,
eacl.OperationPut: 4, eacl.OperationHead: 5,
eacl.OperationHead: 5, eacl.OperationGet: 6,
eacl.OperationGet: 6, }
}
)
// returns true if n-th left bit is set (starting at 0). // returns true if n-th left bit is set (starting at 0).
func isLeftBitSet(value basicACLHelper, n uint8) bool { func isLeftBitSet(value basicACLHelper, n uint8) bool {

View file

@ -1,219 +0,0 @@
package acl
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
bearer "github.com/nspcc-dev/neofs-api-go/v2/acl"
"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/netmap"
"github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/nspcc-dev/neofs-sdk-go/owner"
"github.com/nspcc-dev/neofs-sdk-go/signature"
sigutil "github.com/nspcc-dev/neofs-sdk-go/util/signature"
"go.uber.org/zap"
)
type (
InnerRingFetcher interface {
InnerRingKeys() ([][]byte, error)
}
metaWithToken struct {
vheader *session.RequestVerificationHeader
token *session.Token
bearer *bearer.BearerToken
src interface{}
}
SenderClassifier struct {
log *zap.Logger
innerRing InnerRingFetcher
netmap core.Source
}
)
func NewSenderClassifier(l *zap.Logger, ir InnerRingFetcher, nm core.Source) SenderClassifier {
return SenderClassifier{
log: l,
innerRing: ir,
netmap: nm,
}
}
func (c SenderClassifier) Classify(
req metaWithToken,
cid *cid.ID,
cnr *container.Container) (role eaclSDK.Role, isIR bool, key []byte, err error) {
if cid == nil {
return 0, false, nil, fmt.Errorf("%w: container id is not set", ErrMalformedRequest)
}
ownerID, ownerKey, err := requestOwner(req)
if err != nil {
return 0, false, nil, err
}
ownerKeyInBytes := ownerKey.Bytes()
// TODO: #767 get owner from neofs.id if present
// if request owner is the same as container owner, return RoleUser
if ownerID.Equal(cnr.OwnerID()) {
return eaclSDK.RoleUser, false, ownerKeyInBytes, nil
}
isInnerRingNode, err := c.isInnerRingKey(ownerKeyInBytes)
if err != nil {
// do not throw error, try best case matching
c.log.Debug("can't check if request from inner ring",
zap.String("error", err.Error()))
} else if isInnerRingNode {
return eaclSDK.RoleSystem, true, ownerKeyInBytes, nil
}
isContainerNode, err := c.isContainerKey(ownerKeyInBytes, cid.ToV2().GetValue(), cnr)
if err != nil {
// error might happen if request has `RoleOther` key and placement
// is not possible for previous epoch, so
// do not throw error, try best case matching
c.log.Debug("can't check if request from container node",
zap.String("error", err.Error()))
} else if isContainerNode {
return eaclSDK.RoleSystem, false, ownerKeyInBytes, nil
}
// if none of above, return RoleOthers
return eaclSDK.RoleOthers, false, ownerKeyInBytes, nil
}
func requestOwner(req metaWithToken) (*owner.ID, *keys.PublicKey, error) {
if req.vheader == nil {
return nil, nil, fmt.Errorf("%w: nil verification header", ErrMalformedRequest)
}
// if session token is presented, use it as truth source
if req.token.GetBody() != nil {
// verify signature of session token
return ownerFromToken(req.token)
}
// otherwise get original body signature
bodySignature := originalBodySignature(req.vheader)
if bodySignature == nil {
return nil, nil, fmt.Errorf("%w: nil at body signature", ErrMalformedRequest)
}
key := unmarshalPublicKey(bodySignature.Key())
return owner.NewIDFromPublicKey((*ecdsa.PublicKey)(key)), key, nil
}
func originalBodySignature(v *session.RequestVerificationHeader) *signature.Signature {
if v == nil {
return nil
}
for v.GetOrigin() != nil {
v = v.GetOrigin()
}
return signature.NewFromV2(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) {
nm, err := core.GetLatestNetworkMap(c.netmap) // first check current netmap
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 = core.GetPreviousNetworkMap(c.netmap)
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.PlacementPolicy(), 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].PublicKey(), owner) {
return true, nil
}
}
return false, nil
}
func ownerFromToken(token *session.Token) (*owner.ID, *keys.PublicKey, error) {
// 1. First check signature of session token.
signWrapper := v2signature.StableMarshalerWrapper{SM: token.GetBody()}
if err := sigutil.VerifyDataWithSource(signWrapper, func() (key, sig []byte) {
tokenSignature := token.GetSignature()
return tokenSignature.GetKey(), tokenSignature.GetSign()
}); err != nil {
return nil, nil, fmt.Errorf("%w: invalid session token signature", ErrMalformedRequest)
}
// 2. Then check if session token owner issued the session token
tokenIssuerKey := unmarshalPublicKey(token.GetSignature().GetKey())
tokenOwner := owner.NewIDFromV2(token.GetBody().GetOwnerID())
if !isOwnerFromKey(tokenOwner, tokenIssuerKey) {
// TODO: #767 in this case we can issue all owner keys from neofs.id and check once again
return nil, nil, fmt.Errorf("%w: invalid session token owner", ErrMalformedRequest)
}
return tokenOwner, tokenIssuerKey, nil
}
func unmarshalPublicKey(bs []byte) *keys.PublicKey {
pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256())
if err != nil {
return nil
}
return pub
}

View file

@ -5,7 +5,6 @@ import (
"github.com/nspcc-dev/neofs-api-go/v2/acl" "github.com/nspcc-dev/neofs-api-go/v2/acl"
objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" objectV2 "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" "github.com/nspcc-dev/neofs-api-go/v2/session"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl"
@ -22,7 +21,7 @@ type cfg struct {
msg xHeaderSource msg xHeaderSource
addr *refs.Address addr *objectSDKAddress.Address
} }
type ObjectStorage interface { type ObjectStorage interface {
@ -83,11 +82,6 @@ func requestHeaders(msg xHeaderSource) []eaclSDK.Header {
} }
func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) { func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) {
var addr *objectSDKAddress.Address
if h.addr != nil {
addr = objectSDKAddress.NewAddressFromV2(h.addr)
}
switch m := h.msg.(type) { switch m := h.msg.(type) {
default: default:
panic(fmt.Sprintf("unexpected message type %T", h.msg)) panic(fmt.Sprintf("unexpected message type %T", h.msg))
@ -101,20 +95,20 @@ func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) {
*objectV2.GetRangeRequest, *objectV2.GetRangeRequest,
*objectV2.GetRangeHashRequest, *objectV2.GetRangeHashRequest,
*objectV2.DeleteRequest: *objectV2.DeleteRequest:
return addressHeaders(objectSDKAddress.NewAddressFromV2(h.addr)), true return addressHeaders(h.addr), true
case *objectV2.PutRequest: case *objectV2.PutRequest:
if v, ok := req.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit); ok { if v, ok := req.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit); ok {
oV2 := new(objectV2.Object) oV2 := new(objectV2.Object)
oV2.SetObjectID(v.GetObjectID()) oV2.SetObjectID(v.GetObjectID())
oV2.SetHeader(v.GetHeader()) oV2.SetHeader(v.GetHeader())
if addr == nil { if h.addr == nil {
addr = objectSDKAddress.NewAddress() addr := objectSDKAddress.NewAddress()
addr.SetContainerID(cid.NewFromV2(v.GetHeader().GetContainerID())) addr.SetContainerID(cid.NewFromV2(v.GetHeader().GetContainerID()))
addr.SetObjectID(objectSDKID.NewIDFromV2(v.GetObjectID())) addr.SetObjectID(objectSDKID.NewIDFromV2(v.GetObjectID()))
} }
hs := headersFromObject(object.NewFromV2(oV2), addr) hs := headersFromObject(object.NewFromV2(oV2), h.addr)
return hs, true return hs, true
} }
@ -135,7 +129,7 @@ func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) {
oV2.SetObjectID(v.GetObjectID()) oV2.SetObjectID(v.GetObjectID())
oV2.SetHeader(v.GetHeader()) oV2.SetHeader(v.GetHeader())
return headersFromObject(object.NewFromV2(oV2), addr), true return headersFromObject(object.NewFromV2(oV2), h.addr), true
} }
case *objectV2.HeadResponse: case *objectV2.HeadResponse:
oV2 := new(objectV2.Object) oV2 := new(objectV2.Object)
@ -146,7 +140,7 @@ func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) {
case *objectV2.ShortHeader: case *objectV2.ShortHeader:
hdr = new(objectV2.Header) hdr = new(objectV2.Header)
hdr.SetContainerID(h.addr.GetContainerID()) hdr.SetContainerID(h.addr.ContainerID().ToV2())
hdr.SetVersion(v.GetVersion()) hdr.SetVersion(v.GetVersion())
hdr.SetCreationEpoch(v.GetCreationEpoch()) hdr.SetCreationEpoch(v.GetCreationEpoch())
hdr.SetOwnerID(v.GetOwnerID()) hdr.SetOwnerID(v.GetOwnerID())
@ -158,16 +152,14 @@ func (h *headerSource) objectHeaders() ([]eaclSDK.Header, bool) {
oV2.SetHeader(hdr) oV2.SetHeader(hdr)
return headersFromObject(object.NewFromV2(oV2), addr), true return headersFromObject(object.NewFromV2(oV2), h.addr), true
} }
} }
return nil, true return nil, true
} }
func (h *headerSource) localObjectHeaders(addrV2 *refs.Address) ([]eaclSDK.Header, bool) { func (h *headerSource) localObjectHeaders(addr *objectSDKAddress.Address) ([]eaclSDK.Header, bool) {
addr := objectSDKAddress.NewAddressFromV2(addrV2)
obj, err := h.storage.Head(addr) obj, err := h.storage.Head(addr)
if err == nil { if err == nil {
return headersFromObject(obj, addr), true return headersFromObject(obj, addr), true
@ -176,10 +168,10 @@ func (h *headerSource) localObjectHeaders(addrV2 *refs.Address) ([]eaclSDK.Heade
return addressHeaders(addr), false return addressHeaders(addr), false
} }
func cidHeader(cid *cid.ID) eaclSDK.Header { func cidHeader(idCnr *cid.ID) eaclSDK.Header {
return &sysObjHdr{ return &sysObjHdr{
k: acl.FilterObjectContainerID, k: acl.FilterObjectContainerID,
v: cidValue(cid), v: cidValue(idCnr),
} }
} }

View file

@ -4,7 +4,7 @@ import (
"io" "io"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine"
"github.com/nspcc-dev/neofs-sdk-go/object" objectSDK "github.com/nspcc-dev/neofs-sdk-go/object"
objectSDKAddress "github.com/nspcc-dev/neofs-sdk-go/object/address" objectSDKAddress "github.com/nspcc-dev/neofs-sdk-go/object/address"
) )
@ -12,7 +12,7 @@ type localStorage struct {
ls *engine.StorageEngine ls *engine.StorageEngine
} }
func (s *localStorage) Head(addr *objectSDKAddress.Address) (*object.Object, error) { func (s *localStorage) Head(addr *objectSDKAddress.Address) (*objectSDK.Object, error) {
if s.ls == nil { if s.ls == nil {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }

View file

@ -1,8 +1,8 @@
package v2 package v2
import ( import (
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine"
addressSDK "github.com/nspcc-dev/neofs-sdk-go/object/address"
) )
func WithObjectStorage(v ObjectStorage) Option { func WithObjectStorage(v ObjectStorage) Option {
@ -36,7 +36,7 @@ func WithServiceResponse(resp Response, req Request) Option {
} }
} }
func WithAddress(v *refs.Address) Option { func WithAddress(v *addressSDK.Address) Option {
return func(c *cfg) { return func(c *cfg) {
c.addr = v c.addr = v
} }

View file

@ -1,51 +0,0 @@
package acl
import (
"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"
)
// WithContainerSource returns option to set container source.
func WithContainerSource(v container.Source) Option {
return func(c *cfg) {
c.containers = v
}
}
// WithSenderClassifier returns option to set sender classifier.
func WithSenderClassifier(v SenderClassifier) Option {
return func(c *cfg) {
c.sender = v
}
}
// WithNextService returns option to set next object service.
func WithNextService(v objectSvc.ServiceServer) Option {
return func(c *cfg) {
c.next = v
}
}
// WithEACLSource returns options to set eACL table source.
func WithEACLSource(v eacl.Source) Option {
return func(c *cfg) {
c.eACLCfg.eaclSource = v
}
}
// WithLocalStorage returns options to set local object storage.
func WithLocalStorage(v *engine.StorageEngine) Option {
return func(c *cfg) {
c.localStorage = v
}
}
// WithNetmapState returns options to set global netmap state.
func WithNetmapState(v netmap.State) Option {
return func(c *cfg) {
c.state = v
}
}

View file

@ -0,0 +1,148 @@
package v2
import (
"bytes"
"errors"
core "github.com/nspcc-dev/neofs-node/pkg/core/netmap"
"github.com/nspcc-dev/neofs-sdk-go/container"
cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id"
eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"go.uber.org/zap"
)
type senderClassifier struct {
log *zap.Logger
innerRing InnerRingFetcher
netmap core.Source
}
var errContainerIDNotSet = errors.New("container id is not set")
type classifyResult struct {
role eaclSDK.Role
isIR bool
key []byte
}
func (c senderClassifier) classify(
req MetaWithToken,
idCnr *cidSDK.ID,
cnr *container.Container) (res *classifyResult, err error) {
if idCnr == nil {
return nil, errContainerIDNotSet
}
ownerID, ownerKey, err := req.RequestOwner()
if err != nil {
return nil, err
}
ownerKeyInBytes := ownerKey.Bytes()
// TODO: #767 get owner from neofs.id if present
// if request owner is the same as container owner, return RoleUser
if ownerID.Equal(cnr.OwnerID()) {
return &classifyResult{
role: eaclSDK.RoleUser,
isIR: false,
key: ownerKeyInBytes,
}, nil
}
isInnerRingNode, err := c.isInnerRingKey(ownerKeyInBytes)
if err != nil {
// do not throw error, try best case matching
c.log.Debug("can't check if request from inner ring",
zap.String("error", err.Error()))
} else if isInnerRingNode {
return &classifyResult{
role: eaclSDK.RoleSystem,
isIR: true,
key: ownerKeyInBytes,
}, nil
}
isContainerNode, err := c.isContainerKey(ownerKeyInBytes, idCnr.ToV2().GetValue(), cnr)
if err != nil {
// error might happen if request has `RoleOther` key and placement
// is not possible for previous epoch, so
// do not throw error, try best case matching
c.log.Debug("can't check if request from container node",
zap.String("error", err.Error()))
} else if isContainerNode {
return &classifyResult{
role: eaclSDK.RoleSystem,
isIR: false,
key: ownerKeyInBytes,
}, nil
}
// if none of above, return RoleOthers
return &classifyResult{
role: eaclSDK.RoleOthers,
key: ownerKeyInBytes,
}, nil
}
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, idCnr []byte,
cnr *container.Container) (bool, error) {
nm, err := core.GetLatestNetworkMap(c.netmap) // first check current netmap
if err != nil {
return false, err
}
in, err := lookupKeyInContainer(nm, owner, idCnr, 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 = core.GetPreviousNetworkMap(c.netmap)
if err != nil {
return false, err
}
return lookupKeyInContainer(nm, owner, idCnr, cnr)
}
func lookupKeyInContainer(
nm *netmap.Netmap,
owner, idCnr []byte,
cnr *container.Container) (bool, error) {
cnrNodes, err := nm.GetContainerNodes(cnr.PlacementPolicy(), idCnr)
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].PublicKey(), owner) {
return true, nil
}
}
return false, nil
}

View file

@ -0,0 +1,40 @@
package v2
import (
"errors"
"fmt"
)
var (
// ErrMalformedRequest is returned when request contains
// invalid data.
ErrMalformedRequest = errors.New("malformed request")
// ErrUnknownRole is returned when role of the sender is unknown.
ErrUnknownRole = errors.New("can't classify request sender")
// ErrUnknownContainer is returned when container fetching errors appeared.
ErrUnknownContainer = errors.New("can't fetch container info")
)
type accessErr struct {
RequestInfo
failedCheckTyp string
}
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",
}
}

View file

@ -0,0 +1,51 @@
package v2
import (
"github.com/nspcc-dev/neofs-node/pkg/core/container"
netmapClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap"
objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object"
"go.uber.org/zap"
)
// WithLogger returns option to set logger.
func WithLogger(v *zap.Logger) Option {
return func(c *cfg) {
c.log = v
}
}
// WithNetmapClient return option to set
// netmap client.
func WithNetmapClient(v *netmapClient.Client) Option {
return func(c *cfg) {
c.nm = v
}
}
// WithContainerSource returns option to set container source.
func WithContainerSource(v container.Source) Option {
return func(c *cfg) {
c.containers = v
}
}
// WithNextService returns option to set next object service.
func WithNextService(v objectSvc.ServiceServer) Option {
return func(c *cfg) {
c.next = v
}
}
// WithEACLChecker returns option to set eACL checker.
func WithEACLChecker(v ACLChecker) Option {
return func(c *cfg) {
c.checker = v
}
}
// WithIRFetcher returns option to set inner ring fetcher.
func WithIRFetcher(v InnerRingFetcher) Option {
return func(c *cfg) {
c.irFetcher = v
}
}

View file

@ -0,0 +1,135 @@
package v2
import (
"crypto/ecdsa"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
sessionV2 "github.com/nspcc-dev/neofs-api-go/v2/session"
containerIDSDK "github.com/nspcc-dev/neofs-sdk-go/container/id"
eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl"
oidSDK "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/owner"
sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session"
bearerSDK "github.com/nspcc-dev/neofs-sdk-go/token"
)
// RequestInfo groups parsed version-independent (from SDK library)
// request information and raw API request.
type RequestInfo struct {
basicACL uint32
requestRole eaclSDK.Role
isInnerRing bool
operation eaclSDK.Operation // put, get, head, etc.
cnrOwner *owner.ID // container owner
idCnr *containerIDSDK.ID
oid *oidSDK.ID
senderKey []byte
bearer *bearerSDK.BearerToken // bearer token of request
srcRequest interface{}
}
func (r *RequestInfo) SetBasicACL(basicACL uint32) {
r.basicACL = basicACL
}
func (r *RequestInfo) SetRequestRole(requestRole eaclSDK.Role) {
r.requestRole = requestRole
}
func (r *RequestInfo) SetSenderKey(senderKey []byte) {
r.senderKey = senderKey
}
// Request returns raw API request.
func (r RequestInfo) Request() interface{} {
return r.srcRequest
}
// ContainerOwner returns owner if the container.
func (r RequestInfo) ContainerOwner() *owner.ID {
return r.cnrOwner
}
// ObjectID return object ID.
func (r RequestInfo) ObjectID() *oidSDK.ID {
return r.oid
}
// ContainerID return container ID.
func (r RequestInfo) ContainerID() *containerIDSDK.ID {
return r.idCnr
}
// CleanBearer forces cleaning bearer token information.
func (r *RequestInfo) CleanBearer() {
r.bearer = nil
}
// Bearer returns bearer token of the request.
func (r RequestInfo) Bearer() *bearerSDK.BearerToken {
return r.bearer
}
// IsInnerRing specifies if request was made by inner ring.
func (r RequestInfo) IsInnerRing() bool {
return r.isInnerRing
}
// BasicACL returns basic ACL of the container.
func (r RequestInfo) BasicACL() uint32 {
return r.basicACL
}
// SenderKey returns public key of the request's sender.
func (r RequestInfo) SenderKey() []byte {
return r.senderKey
}
// Operation returns request's operation.
func (r RequestInfo) Operation() eaclSDK.Operation {
return r.operation
}
// RequestRole returns request sender's role.
func (r RequestInfo) RequestRole() eaclSDK.Role {
return r.requestRole
}
// MetaWithToken groups session and bearer tokens,
// verification header and raw API request.
type MetaWithToken struct {
vheader *sessionV2.RequestVerificationHeader
token *sessionSDK.Token
bearer *bearerSDK.BearerToken
src interface{}
}
// RequestOwner returns ownerID and its public key
// according to internal meta information.
func (r MetaWithToken) RequestOwner() (*owner.ID, *keys.PublicKey, error) {
if r.vheader == nil {
return nil, nil, fmt.Errorf("%w: nil verification header", ErrMalformedRequest)
}
// if session token is presented, use it as truth source
if r.token != nil {
// verify signature of session token
return ownerFromToken(r.token)
}
// otherwise get original body signature
bodySignature := originalBodySignature(r.vheader)
if bodySignature == nil {
return nil, nil, fmt.Errorf("%w: nil at body signature", ErrMalformedRequest)
}
key := unmarshalPublicKey(bodySignature.Key())
return owner.NewIDFromPublicKey((*ecdsa.PublicKey)(key)), key, nil
}

View file

@ -0,0 +1,447 @@
package v2
import (
"context"
"fmt"
objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-node/pkg/core/container"
netmapClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap"
"github.com/nspcc-dev/neofs-node/pkg/services/object"
cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id"
eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl"
sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session"
"go.uber.org/zap"
)
// Service checks basic ACL rules.
type Service struct {
*cfg
c senderClassifier
}
type putStreamBasicChecker struct {
source *Service
next object.PutObjectStream
}
type getStreamBasicChecker struct {
checker ACLChecker
object.GetObjectStream
info RequestInfo
}
type rangeStreamBasicChecker struct {
checker ACLChecker
object.GetObjectRangeStream
info RequestInfo
}
type searchStreamBasicChecker struct {
checker ACLChecker
object.SearchStream
info RequestInfo
}
// Option represents Service constructor option.
type Option func(*cfg)
type cfg struct {
log *zap.Logger
containers container.Source
checker ACLChecker
irFetcher InnerRingFetcher
nm *netmapClient.Client
next object.ServiceServer
}
func defaultCfg() *cfg {
return &cfg{
log: zap.L(),
}
}
// New is a constructor for object ACL checking service.
func New(opts ...Option) Service {
cfg := defaultCfg()
for i := range opts {
opts[i](cfg)
}
panicOnNil := func(v interface{}, name string) {
if v == nil {
panic(fmt.Sprintf("ACL service: %s is nil", name))
}
}
panicOnNil(cfg.next, "next Service")
panicOnNil(cfg.nm, "netmap client")
panicOnNil(cfg.irFetcher, "inner Ring fetcher")
panicOnNil(cfg.checker, "acl checker")
panicOnNil(cfg.containers, "container source")
return Service{
cfg: cfg,
c: senderClassifier{
log: cfg.log,
innerRing: cfg.irFetcher,
netmap: cfg.nm,
},
}
}
// Get implements ServiceServer interface, makes ACL checks and calls
// next Get method in the ServiceServer pipeline.
func (b Service) Get(request *objectV2.GetRequest, stream object.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, eaclSDK.OperationGet)
if err != nil {
return err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
useObjectIDFromSession(&reqInfo, sTok)
if !b.checker.CheckBasicACL(reqInfo) {
return basicACLErr(reqInfo)
} else if !b.checker.CheckEACL(request, reqInfo) {
return eACLErr(reqInfo)
}
return b.next.Get(request, &getStreamBasicChecker{
GetObjectStream: stream,
info: reqInfo,
checker: b.checker,
})
}
func (b Service) Put(ctx context.Context) (object.PutObjectStream, error) {
streamer, err := b.next.Put(ctx)
return putStreamBasicChecker{
source: &b,
next: streamer,
}, err
}
func (b Service) Head(
ctx context.Context,
request *objectV2.HeadRequest) (*objectV2.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, eaclSDK.OperationHead)
if err != nil {
return nil, err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
useObjectIDFromSession(&reqInfo, sTok)
if !b.checker.CheckBasicACL(reqInfo) {
return nil, basicACLErr(reqInfo)
} else if !b.checker.CheckEACL(request, reqInfo) {
return nil, eACLErr(reqInfo)
}
resp, err := b.next.Head(ctx, request)
if err == nil {
if !b.checker.CheckEACL(resp, reqInfo) {
err = eACLErr(reqInfo)
}
}
return resp, err
}
func (b Service) Search(request *objectV2.SearchRequest, stream object.SearchStream) error {
var id *cidSDK.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, eaclSDK.OperationSearch)
if err != nil {
return err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
if !b.checker.CheckBasicACL(reqInfo) {
return basicACLErr(reqInfo)
} else if !b.checker.CheckEACL(request, reqInfo) {
return eACLErr(reqInfo)
}
return b.next.Search(request, &searchStreamBasicChecker{
checker: b.checker,
SearchStream: stream,
info: reqInfo,
})
}
func (b Service) Delete(
ctx context.Context,
request *objectV2.DeleteRequest) (*objectV2.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, eaclSDK.OperationDelete)
if err != nil {
return nil, err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
useObjectIDFromSession(&reqInfo, sTok)
if !b.checker.CheckBasicACL(reqInfo) {
return nil, basicACLErr(reqInfo)
} else if !b.checker.CheckEACL(request, reqInfo) {
return nil, eACLErr(reqInfo)
}
return b.next.Delete(ctx, request)
}
func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.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, eaclSDK.OperationRange)
if err != nil {
return err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
useObjectIDFromSession(&reqInfo, sTok)
if !b.checker.CheckBasicACL(reqInfo) {
return basicACLErr(reqInfo)
} else if !b.checker.CheckEACL(request, reqInfo) {
return eACLErr(reqInfo)
}
return b.next.GetRange(request, &rangeStreamBasicChecker{
checker: b.checker,
GetObjectRangeStream: stream,
info: reqInfo,
})
}
func (b Service) GetRangeHash(
ctx context.Context,
request *objectV2.GetRangeHashRequest) (*objectV2.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, eaclSDK.OperationRangeHash)
if err != nil {
return nil, err
}
reqInfo.oid = getObjectIDFromRequestBody(request.GetBody())
useObjectIDFromSession(&reqInfo, sTok)
if !b.checker.CheckBasicACL(reqInfo) {
return nil, basicACLErr(reqInfo)
} else if !b.checker.CheckEACL(request, reqInfo) {
return nil, eACLErr(reqInfo)
}
return b.next.GetRangeHash(ctx, request)
}
func (p putStreamBasicChecker) Send(request *objectV2.PutRequest) error {
body := request.GetBody()
if body == nil {
return ErrMalformedRequest
}
part := body.GetObjectPart()
if part, ok := part.(*objectV2.PutObjectPartInit); ok {
cid, err := getContainerIDFromRequest(request)
if err != nil {
return err
}
ownerID, err := getObjectOwnerFromMessage(request)
if err != nil {
return err
}
sTok := sessionSDK.NewTokenFromV2(request.GetMetaHeader().GetSessionToken())
req := MetaWithToken{
vheader: request.GetVerificationHeader(),
token: sTok,
bearer: originalBearerToken(request.GetMetaHeader()),
src: request,
}
reqInfo, err := p.source.findRequestInfo(req, cid, eaclSDK.OperationPut)
if err != nil {
return err
}
reqInfo.oid = getObjectIDFromRequestBody(part)
useObjectIDFromSession(&reqInfo, sTok)
if !p.source.checker.CheckBasicACL(reqInfo) || !p.source.checker.StickyBitCheck(reqInfo, ownerID) {
return basicACLErr(reqInfo)
} else if !p.source.checker.CheckEACL(request, reqInfo) {
return eACLErr(reqInfo)
}
}
return p.next.Send(request)
}
func (p putStreamBasicChecker) CloseAndRecv() (*objectV2.PutResponse, error) {
return p.next.CloseAndRecv()
}
func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error {
if _, ok := resp.GetBody().GetObjectPart().(*objectV2.GetObjectPartInit); ok {
if !g.checker.CheckEACL(resp, g.info) {
return eACLErr(g.info)
}
}
return g.GetObjectStream.Send(resp)
}
func (g *rangeStreamBasicChecker) Send(resp *objectV2.GetRangeResponse) error {
if !g.checker.CheckEACL(resp, g.info) {
return eACLErr(g.info)
}
return g.GetObjectRangeStream.Send(resp)
}
func (g *searchStreamBasicChecker) Send(resp *objectV2.SearchResponse) error {
if !g.checker.CheckEACL(resp, g.info) {
return eACLErr(g.info)
}
return g.SearchStream.Send(resp)
}
func (b Service) findRequestInfo(
req MetaWithToken,
cid *cidSDK.ID,
op eaclSDK.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
res, err := b.c.classify(req, cid, cnr)
if err != nil {
return info, err
}
if res.role == eaclSDK.RoleUnknown {
return info, ErrUnknownRole
}
// find verb from token if it is present
verb := sourceVerbOfRequest(req, op)
info.basicACL = cnr.BasicACL()
info.requestRole = res.role
info.isInnerRing = res.isIR
info.operation = verb
info.cnrOwner = cnr.OwnerID()
info.idCnr = cid
// it is assumed that at the moment the key will be valid,
// otherwise the request would not pass validation
info.senderKey = res.key
// add bearer token if it is present in request
info.bearer = req.bearer
info.srcRequest = req.src
return info, nil
}

View file

@ -0,0 +1,28 @@
package v2
import (
"github.com/nspcc-dev/neofs-sdk-go/owner"
)
// ACLChecker is an interface that must provide
// ACL related checks.
type ACLChecker interface {
// CheckBasicACL must return true only if request
// passes basic ACL validation.
CheckBasicACL(RequestInfo) bool
// CheckEACL must return true only if request
// passes extended ACL validation.
CheckEACL(interface{}, RequestInfo) bool
// StickyBitCheck must return true only if sticky bit
// is disabled or enabled but request contains correct
// owner field.
StickyBitCheck(RequestInfo, *owner.ID) bool
}
// InnerRingFetcher is an interface that must provide
// Inner Ring information.
type InnerRingFetcher interface {
// InnerRingKeys must return list of public keys of
// the actual inner ring.
InnerRingKeys() ([][]byte, error)
}

View file

@ -0,0 +1,197 @@
package v2
import (
"crypto/ecdsa"
"crypto/elliptic"
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object"
refsV2 "github.com/nspcc-dev/neofs-api-go/v2/refs"
sessionV2 "github.com/nspcc-dev/neofs-api-go/v2/session"
containerIDSDK "github.com/nspcc-dev/neofs-sdk-go/container/id"
eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl"
oidSDK "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/owner"
sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/signature"
bearerSDK "github.com/nspcc-dev/neofs-sdk-go/token"
)
func getContainerIDFromRequest(req interface{}) (id *containerIDSDK.ID, err error) {
switch v := req.(type) {
case *objectV2.GetRequest:
return containerIDSDK.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil
case *objectV2.PutRequest:
objPart := v.GetBody().GetObjectPart()
if part, ok := objPart.(*objectV2.PutObjectPartInit); ok {
return containerIDSDK.NewFromV2(part.GetHeader().GetContainerID()), nil
}
return nil, errors.New("can't get container ID in chunk")
case *objectV2.HeadRequest:
return containerIDSDK.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil
case *objectV2.SearchRequest:
return containerIDSDK.NewFromV2(v.GetBody().GetContainerID()), nil
case *objectV2.DeleteRequest:
return containerIDSDK.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil
case *objectV2.GetRangeRequest:
return containerIDSDK.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil
case *objectV2.GetRangeHashRequest:
return containerIDSDK.NewFromV2(v.GetBody().GetAddress().GetContainerID()), nil
default:
return nil, errors.New("unknown request type")
}
}
// originalBearerToken goes down to original request meta header and fetches
// bearer token from there.
func originalBearerToken(header *sessionV2.RequestMetaHeader) *bearerSDK.BearerToken {
for header.GetOrigin() != nil {
header = header.GetOrigin()
}
return bearerSDK.NewBearerTokenFromV2(header.GetBearerToken())
}
// originalSessionToken goes down to original request meta header and fetches
// session token from there.
func originalSessionToken(header *sessionV2.RequestMetaHeader) *sessionSDK.Token {
for header.GetOrigin() != nil {
header = header.GetOrigin()
}
return sessionSDK.NewTokenFromV2(header.GetSessionToken())
}
func getObjectIDFromRequestBody(body interface{}) *oidSDK.ID {
switch v := body.(type) {
default:
return nil
case interface {
GetObjectID() *refsV2.ObjectID
}:
return oidSDK.NewIDFromV2(v.GetObjectID())
case interface {
GetAddress() *refsV2.Address
}:
return oidSDK.NewIDFromV2(v.GetAddress().GetObjectID())
}
}
func getObjectOwnerFromMessage(req interface{}) (id *owner.ID, err error) {
switch v := req.(type) {
case *objectV2.PutRequest:
objPart := v.GetBody().GetObjectPart()
if part, ok := objPart.(*objectV2.PutObjectPartInit); ok {
return owner.NewIDFromV2(part.GetHeader().GetOwnerID()), nil
}
return nil, errors.New("can't get container ID in chunk")
case *objectV2.GetResponse:
objPart := v.GetBody().GetObjectPart()
if part, ok := objPart.(*objectV2.GetObjectPartInit); ok {
return owner.NewIDFromV2(part.GetHeader().GetOwnerID()), nil
}
return nil, errors.New("can't get container ID in chunk")
default:
return nil, errors.New("unsupported request type")
}
}
// sourceVerbOfRequest looks for verb in session token and if it is not found,
// returns reqVerb.
func sourceVerbOfRequest(req MetaWithToken, reqVerb eaclSDK.Operation) eaclSDK.Operation {
if req.token != nil {
switch v := req.token.Context().(type) {
case *sessionSDK.ObjectContext:
return tokenVerbToOperation(v)
default:
// do nothing, return request verb
}
}
return reqVerb
}
func useObjectIDFromSession(req *RequestInfo, token *sessionSDK.Token) {
if token == nil {
return
}
objCtx, ok := token.Context().(*sessionSDK.ObjectContext)
if !ok {
return
}
req.oid = objCtx.Address().ObjectID()
}
func tokenVerbToOperation(ctx *sessionSDK.ObjectContext) eaclSDK.Operation {
switch {
case ctx.IsForGet():
return eaclSDK.OperationGet
case ctx.IsForPut():
return eaclSDK.OperationPut
case ctx.IsForHead():
return eaclSDK.OperationHead
case ctx.IsForSearch():
return eaclSDK.OperationSearch
case ctx.IsForDelete():
return eaclSDK.OperationDelete
case ctx.IsForRange():
return eaclSDK.OperationRange
case ctx.IsForRangeHash():
return eaclSDK.OperationRangeHash
default:
return eaclSDK.OperationUnknown
}
}
func ownerFromToken(token *sessionSDK.Token) (*owner.ID, *keys.PublicKey, error) {
// 1. First check signature of session token.
if !token.VerifySignature() {
return nil, nil, fmt.Errorf("%w: invalid session token signature", ErrMalformedRequest)
}
// 2. Then check if session token owner issued the session token
tokenIssuerKey := unmarshalPublicKey(token.Signature().Key())
tokenOwner := token.OwnerID()
if !isOwnerFromKey(tokenOwner, tokenIssuerKey) {
// TODO: #767 in this case we can issue all owner keys from neofs.id and check once again
return nil, nil, fmt.Errorf("%w: invalid session token owner", ErrMalformedRequest)
}
return tokenOwner, tokenIssuerKey, nil
}
func originalBodySignature(v *sessionV2.RequestVerificationHeader) *signature.Signature {
if v == nil {
return nil
}
for v.GetOrigin() != nil {
v = v.GetOrigin()
}
return signature.NewFromV2(v.GetBodySignature())
}
func unmarshalPublicKey(bs []byte) *keys.PublicKey {
pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256())
if err != nil {
return nil
}
return pub
}
func isOwnerFromKey(id *owner.ID, key *keys.PublicKey) bool {
if id == nil || key == nil {
return false
}
return id.Equal(owner.NewIDFromPublicKey((*ecdsa.PublicKey)(key)))
}

View file

@ -0,0 +1,38 @@
package v2
import (
"testing"
"github.com/nspcc-dev/neofs-api-go/v2/acl"
acltest "github.com/nspcc-dev/neofs-api-go/v2/acl/test"
"github.com/nspcc-dev/neofs-api-go/v2/session"
sessiontest "github.com/nspcc-dev/neofs-api-go/v2/session/test"
sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session"
bearerSDK "github.com/nspcc-dev/neofs-sdk-go/token"
"github.com/stretchr/testify/require"
)
func TestOriginalTokens(t *testing.T) {
sToken := sessiontest.GenerateSessionToken(false)
bToken := acltest.GenerateBearerToken(false)
for i := 0; i < 10; i++ {
metaHeaders := testGenerateMetaHeader(uint32(i), bToken, sToken)
require.Equal(t, sessionSDK.NewTokenFromV2(sToken), originalSessionToken(metaHeaders), i)
require.Equal(t, bearerSDK.NewBearerTokenFromV2(bToken), originalBearerToken(metaHeaders), i)
}
}
func testGenerateMetaHeader(depth uint32, b *acl.BearerToken, s *session.SessionToken) *session.RequestMetaHeader {
metaHeader := new(session.RequestMetaHeader)
metaHeader.SetBearerToken(b)
metaHeader.SetSessionToken(s)
for i := uint32(0); i < depth; i++ {
link := metaHeader
metaHeader = new(session.RequestMetaHeader)
metaHeader.SetOrigin(link)
}
return metaHeader
}