[#872] object: Use APE instead EACL for object service handlers #888
|
@ -24,6 +24,7 @@ import (
|
||||||
objectService "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object"
|
objectService "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl"
|
||||||
v2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/v2"
|
v2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/v2"
|
||||||
|
objectAPE "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/ape"
|
||||||
deletesvc "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/delete"
|
deletesvc "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/delete"
|
||||||
deletesvcV2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/delete/v2"
|
deletesvcV2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/delete/v2"
|
||||||
getsvc "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/get"
|
getsvc "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/get"
|
||||||
|
@ -183,11 +184,13 @@ func initObjectService(c *cfg) {
|
||||||
sDeleteV2 := createDeleteServiceV2(sDelete)
|
sDeleteV2 := createDeleteServiceV2(sDelete)
|
||||||
|
|
||||||
// build service pipeline
|
// build service pipeline
|
||||||
// grpc | <metrics> | signature | response | acl | split
|
// grpc | <metrics> | signature | response | acl | ape | split
|
||||||
|
|
||||||
splitSvc := createSplitService(c, sPutV2, sGetV2, sSearchV2, sDeleteV2)
|
splitSvc := createSplitService(c, sPutV2, sGetV2, sSearchV2, sDeleteV2)
|
||||||
|
|
||||||
aclSvc := createACLServiceV2(c, splitSvc, &irFetcher)
|
apeSvc := createAPEService(c, splitSvc)
|
||||||
|
|
||||||
|
aclSvc := createACLServiceV2(c, apeSvc, &irFetcher)
|
||||||
|
|
||||||
var commonSvc objectService.Common
|
var commonSvc objectService.Common
|
||||||
commonSvc.Init(&c.internals, aclSvc)
|
commonSvc.Init(&c.internals, aclSvc)
|
||||||
|
@ -414,11 +417,11 @@ func createSplitService(c *cfg, sPutV2 *putsvcV2.Service, sGetV2 *getsvcV2.Servi
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createACLServiceV2(c *cfg, splitSvc *objectService.TransportSplitter, irFetcher *cachedIRFetcher) v2.Service {
|
func createACLServiceV2(c *cfg, apeSvc *objectAPE.Service, irFetcher *cachedIRFetcher) v2.Service {
|
||||||
ls := c.cfgObject.cfgLocalStorage.localStorage
|
ls := c.cfgObject.cfgLocalStorage.localStorage
|
||||||
|
|
||||||
return v2.New(
|
return v2.New(
|
||||||
splitSvc,
|
apeSvc,
|
||||||
c.netMapSource,
|
c.netMapSource,
|
||||||
irFetcher,
|
irFetcher,
|
||||||
acl.NewChecker(
|
acl.NewChecker(
|
||||||
|
@ -426,12 +429,22 @@ func createACLServiceV2(c *cfg, splitSvc *objectService.TransportSplitter, irFet
|
||||||
c.cfgObject.eaclSource,
|
c.cfgObject.eaclSource,
|
||||||
eaclSDK.NewValidator(),
|
eaclSDK.NewValidator(),
|
||||||
ls),
|
ls),
|
||||||
acl.NewAPEChecker(c.log, c.cfgObject.cfgAccessPolicyEngine.accessPolicyEngine.chainRouter),
|
|
||||||
c.cfgObject.cnrSource,
|
c.cfgObject.cnrSource,
|
||||||
v2.WithLogger(c.log),
|
v2.WithLogger(c.log),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createAPEService(c *cfg, splitSvc *objectService.TransportSplitter) *objectAPE.Service {
|
||||||
|
return objectAPE.NewService(
|
||||||
|
c.log,
|
||||||
|
objectAPE.NewChecker(
|
||||||
|
c.cfgObject.cfgAccessPolicyEngine.accessPolicyEngine.chainRouter,
|
||||||
|
objectAPE.NewStorageEngineHeaderProvider(c.cfgObject.cfgLocalStorage.localStorage),
|
||||||
|
),
|
||||||
|
splitSvc,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type morphEACLFetcher struct {
|
type morphEACLFetcher struct {
|
||||||
w *cntClient.Client
|
w *cntClient.Client
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
|
@ -8,7 +8,7 @@ require (
|
||||||
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65
|
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65
|
||||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231122162120-56debcfa569e
|
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231122162120-56debcfa569e
|
||||||
git.frostfs.info/TrueCloudLab/hrw v1.2.1
|
git.frostfs.info/TrueCloudLab/hrw v1.2.1
|
||||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20231211080303-8c673ee4f4af
|
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20231221121354-ed93bb5cc574
|
||||||
git.frostfs.info/TrueCloudLab/tzhash v1.8.0
|
git.frostfs.info/TrueCloudLab/tzhash v1.8.0
|
||||||
github.com/cheggaaa/pb v1.0.29
|
github.com/cheggaaa/pb v1.0.29
|
||||||
github.com/chzyer/readline v1.5.1
|
github.com/chzyer/readline v1.5.1
|
||||||
|
|
BIN
go.sum
|
@ -1,49 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/v2"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
|
||||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
|
||||||
)
|
|
||||||
|
|
||||||
type apeCheckerImpl struct {
|
|
||||||
log *logger.Logger
|
|
||||||
chainRouter policyengine.ChainRouter
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAPEChecker(log *logger.Logger, chainRouter policyengine.ChainRouter) v2.APEChainChecker {
|
|
||||||
return &apeCheckerImpl{
|
|
||||||
log: log,
|
|
||||||
chainRouter: chainRouter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *apeCheckerImpl) CheckIfRequestPermitted(reqInfo v2.RequestInfo) error {
|
|
||||||
request := new(Request)
|
|
||||||
request.FromRequestInfo(reqInfo)
|
|
||||||
|
|
||||||
cnrTarget := reqInfo.ContainerID().EncodeToString()
|
|
||||||
|
|
||||||
status, ruleFound, err := c.chainRouter.IsAllowed(apechain.Ingress, policyengine.NewRequestTargetWithContainer(cnrTarget), request)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ruleFound || status == apechain.Allow {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return apeErr(reqInfo, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessDeniedAPEReasonFmt = "access to operation %s is denied by access policy engine: %s"
|
|
||||||
|
|
||||||
func apeErr(req v2.RequestInfo, status apechain.Status) error {
|
|
||||||
errAccessDenied := &apistatus.ObjectAccessDenied{}
|
|
||||||
errAccessDenied.WriteReason(fmt.Sprintf(accessDeniedAPEReasonFmt, req.Operation(), status.String()))
|
|
||||||
return errAccessDenied
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/v2"
|
|
||||||
aclSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
|
||||||
aperesource "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
|
||||||
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
operation string
|
|
||||||
resource *resource
|
|
||||||
properties map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ aperesource.Request = (*Request)(nil)
|
|
||||||
|
|
||||||
type resource struct {
|
|
||||||
name string
|
|
||||||
properties map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ aperesource.Resource = (*resource)(nil)
|
|
||||||
|
|
||||||
func (r *resource) Name() string {
|
|
||||||
return r.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) Property(key string) string {
|
|
||||||
return r.properties[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func getResource(reqInfo v2.RequestInfo) *resource {
|
|
||||||
var name string
|
|
||||||
cid := reqInfo.ContainerID()
|
|
||||||
if oid := reqInfo.ObjectID(); oid != nil {
|
|
||||||
name = fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, cid.EncodeToString(), oid.EncodeToString())
|
|
||||||
} else {
|
|
||||||
name = fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cid.EncodeToString())
|
|
||||||
}
|
|
||||||
return &resource{
|
|
||||||
name: name,
|
|
||||||
properties: make(map[string]string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getProperties(_ v2.RequestInfo) map[string]string {
|
|
||||||
return map[string]string{
|
|
||||||
nativeschema.PropertyKeyActorPublicKey: "",
|
|
||||||
nativeschema.PropertyKeyActorRole: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOperation(reqInfo v2.RequestInfo) string {
|
|
||||||
switch op := reqInfo.Operation(); op {
|
|
||||||
case aclSDK.OpObjectGet:
|
|
||||||
return nativeschema.MethodGetObject
|
|
||||||
case aclSDK.OpObjectHead:
|
|
||||||
return nativeschema.MethodHeadObject
|
|
||||||
case aclSDK.OpObjectPut:
|
|
||||||
return nativeschema.MethodPutObject
|
|
||||||
case aclSDK.OpObjectDelete:
|
|
||||||
return nativeschema.MethodDeleteObject
|
|
||||||
case aclSDK.OpObjectSearch:
|
|
||||||
return nativeschema.MethodSearchObject
|
|
||||||
case aclSDK.OpObjectRange:
|
|
||||||
return nativeschema.MethodRangeObject
|
|
||||||
case aclSDK.OpObjectHash:
|
|
||||||
return nativeschema.MethodHashObject
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRequest() *Request {
|
|
||||||
return &Request{
|
|
||||||
resource: new(resource),
|
|
||||||
properties: map[string]string{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) FromRequestInfo(ri v2.RequestInfo) {
|
|
||||||
r.operation = getOperation(ri)
|
|
||||||
r.resource = getResource(ri)
|
|
||||||
r.properties = getProperties(ri)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) Operation() string {
|
|
||||||
return r.operation
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) Property(key string) string {
|
|
||||||
return r.properties[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) Resource() aperesource.Resource {
|
|
||||||
return r.resource
|
|
||||||
}
|
|
|
@ -22,6 +22,9 @@ type RequestInfo struct {
|
||||||
operation acl.Op // put, get, head, etc.
|
operation acl.Op // put, get, head, etc.
|
||||||
cnrOwner user.ID // container owner
|
cnrOwner user.ID // container owner
|
||||||
|
|
||||||
|
// cnrNamespace defined to which namespace a container is belonged.
|
||||||
|
cnrNamespace string
|
||||||
|
|
||||||
idCnr cid.ID
|
idCnr cid.ID
|
||||||
|
|
||||||
// optional for some request
|
// optional for some request
|
||||||
|
@ -57,6 +60,10 @@ func (r RequestInfo) ContainerOwner() user.ID {
|
||||||
return r.cnrOwner
|
return r.cnrOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r RequestInfo) ContainerNamespace() string {
|
||||||
|
return r.cnrNamespace
|
||||||
|
}
|
||||||
|
|
||||||
// ObjectID return object ID.
|
// ObjectID return object ID.
|
||||||
func (r RequestInfo) ObjectID() *oid.ID {
|
func (r RequestInfo) ObjectID() *oid.ID {
|
||||||
return r.obj
|
return r.obj
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
containerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
||||||
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
||||||
|
@ -67,10 +68,6 @@ type cfg struct {
|
||||||
|
|
||||||
checker ACLChecker
|
checker ACLChecker
|
||||||
|
|
||||||
// TODO(aarifullin): apeCheck is temporarily the part of
|
|
||||||
// acl service and must be standalone.
|
|
||||||
apeChecker APEChainChecker
|
|
||||||
|
|
||||||
irFetcher InnerRingFetcher
|
irFetcher InnerRingFetcher
|
||||||
|
|
||||||
nm netmap.Source
|
nm netmap.Source
|
||||||
|
@ -83,7 +80,6 @@ func New(next object.ServiceServer,
|
||||||
nm netmap.Source,
|
nm netmap.Source,
|
||||||
irf InnerRingFetcher,
|
irf InnerRingFetcher,
|
||||||
acl ACLChecker,
|
acl ACLChecker,
|
||||||
apeChecker APEChainChecker,
|
|
||||||
cs container.Source,
|
cs container.Source,
|
||||||
opts ...Option,
|
opts ...Option,
|
||||||
) Service {
|
) Service {
|
||||||
|
@ -93,7 +89,6 @@ func New(next object.ServiceServer,
|
||||||
nm: nm,
|
nm: nm,
|
||||||
irFetcher: irf,
|
irFetcher: irf,
|
||||||
checker: acl,
|
checker: acl,
|
||||||
apeChecker: apeChecker,
|
|
||||||
containers: cs,
|
containers: cs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +102,75 @@ func New(next object.ServiceServer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrappedGetObjectStream propagates RequestContext into GetObjectStream's context.
|
||||||
|
// This allows to retrieve already calculated immutable request-specific values in next handler invocation.
|
||||||
|
type wrappedGetObjectStream struct {
|
||||||
|
object.GetObjectStream
|
||||||
|
|
||||||
|
requestInfo RequestInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedGetObjectStream) Context() context.Context {
|
||||||
|
return context.WithValue(w.GetObjectStream.Context(), object.RequestContextKey, &object.RequestContext{
|
||||||
|
Namespace: w.requestInfo.ContainerNamespace(),
|
||||||
|
SenderKey: w.requestInfo.SenderKey(),
|
||||||
|
Role: w.requestInfo.RequestRole(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWrappedGetObjectStreamStream(getObjectStream object.GetObjectStream, reqInfo RequestInfo) object.GetObjectStream {
|
||||||
|
return &wrappedGetObjectStream{
|
||||||
|
GetObjectStream: getObjectStream,
|
||||||
|
requestInfo: reqInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrappedRangeStream propagates RequestContext into GetObjectRangeStream's context.
|
||||||
|
// This allows to retrieve already calculated immutable request-specific values in next handler invocation.
|
||||||
|
type wrappedRangeStream struct {
|
||||||
|
object.GetObjectRangeStream
|
||||||
|
|
||||||
|
requestInfo RequestInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedRangeStream) Context() context.Context {
|
||||||
|
return context.WithValue(w.GetObjectRangeStream.Context(), object.RequestContextKey, &object.RequestContext{
|
||||||
|
Namespace: w.requestInfo.ContainerNamespace(),
|
||||||
|
SenderKey: w.requestInfo.SenderKey(),
|
||||||
|
Role: w.requestInfo.RequestRole(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWrappedRangeStream(rangeStream object.GetObjectRangeStream, reqInfo RequestInfo) object.GetObjectRangeStream {
|
||||||
|
return &wrappedRangeStream{
|
||||||
|
GetObjectRangeStream: rangeStream,
|
||||||
|
requestInfo: reqInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrappedSearchStream propagates RequestContext into SearchStream's context.
|
||||||
|
// This allows to retrieve already calculated immutable request-specific values in next handler invocation.
|
||||||
|
type wrappedSearchStream struct {
|
||||||
|
object.SearchStream
|
||||||
aarifullin marked this conversation as resolved
Outdated
|
|||||||
|
|
||||||
|
requestInfo RequestInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedSearchStream) Context() context.Context {
|
||||||
|
return context.WithValue(w.SearchStream.Context(), object.RequestContextKey, &object.RequestContext{
|
||||||
|
Namespace: w.requestInfo.ContainerNamespace(),
|
||||||
|
SenderKey: w.requestInfo.SenderKey(),
|
||||||
|
Role: w.requestInfo.RequestRole(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWrappedSearchStream(searchStream object.SearchStream, reqInfo RequestInfo) object.SearchStream {
|
||||||
|
return &wrappedSearchStream{
|
||||||
|
SearchStream: searchStream,
|
||||||
|
requestInfo: reqInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get implements ServiceServer interface, makes ACL checks and calls
|
// Get implements ServiceServer interface, makes ACL checks and calls
|
||||||
// next Get method in the ServiceServer pipeline.
|
// next Get method in the ServiceServer pipeline.
|
||||||
func (b Service) Get(request *objectV2.GetRequest, stream object.GetObjectStream) error {
|
func (b Service) Get(request *objectV2.GetRequest, stream object.GetObjectStream) error {
|
||||||
|
@ -158,7 +222,7 @@ func (b Service) Get(request *objectV2.GetRequest, stream object.GetObjectStream
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.next.Get(request, &getStreamBasicChecker{
|
return b.next.Get(request, &getStreamBasicChecker{
|
||||||
GetObjectStream: stream,
|
GetObjectStream: newWrappedGetObjectStreamStream(stream, reqInfo),
|
||||||
info: reqInfo,
|
info: reqInfo,
|
||||||
checker: b.checker,
|
checker: b.checker,
|
||||||
})
|
})
|
||||||
|
@ -224,7 +288,7 @@ func (b Service) Head(
|
||||||
return nil, eACLErr(reqInfo, err)
|
return nil, eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.next.Head(ctx, request)
|
resp, err := b.next.Head(requestContext(ctx, reqInfo), request)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err = b.checker.CheckEACL(resp, reqInfo); err != nil {
|
if err = b.checker.CheckEACL(resp, reqInfo); err != nil {
|
||||||
err = eACLErr(reqInfo, err)
|
err = eACLErr(reqInfo, err)
|
||||||
|
@ -277,7 +341,7 @@ func (b Service) Search(request *objectV2.SearchRequest, stream object.SearchStr
|
||||||
|
|
||||||
return b.next.Search(request, &searchStreamBasicChecker{
|
return b.next.Search(request, &searchStreamBasicChecker{
|
||||||
checker: b.checker,
|
checker: b.checker,
|
||||||
SearchStream: stream,
|
SearchStream: newWrappedSearchStream(stream, reqInfo),
|
||||||
info: reqInfo,
|
info: reqInfo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -333,7 +397,7 @@ func (b Service) Delete(
|
||||||
return nil, eACLErr(reqInfo, err)
|
return nil, eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.next.Delete(ctx, request)
|
return b.next.Delete(requestContext(ctx, reqInfo), request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetObjectRangeStream) error {
|
func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetObjectRangeStream) error {
|
||||||
|
@ -386,11 +450,18 @@ func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetOb
|
||||||
|
|
||||||
return b.next.GetRange(request, &rangeStreamBasicChecker{
|
return b.next.GetRange(request, &rangeStreamBasicChecker{
|
||||||
checker: b.checker,
|
checker: b.checker,
|
||||||
GetObjectRangeStream: stream,
|
GetObjectRangeStream: newWrappedRangeStream(stream, reqInfo),
|
||||||
info: reqInfo,
|
info: reqInfo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requestContext(ctx context.Context, reqInfo RequestInfo) context.Context {
|
||||||
|
return context.WithValue(ctx, object.RequestContextKey, &object.RequestContext{
|
||||||
|
SenderKey: reqInfo.SenderKey(),
|
||||||
|
Role: reqInfo.RequestRole(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (b Service) GetRangeHash(
|
func (b Service) GetRangeHash(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request *objectV2.GetRangeHashRequest,
|
request *objectV2.GetRangeHashRequest,
|
||||||
|
@ -442,7 +513,7 @@ func (b Service) GetRangeHash(
|
||||||
return nil, eACLErr(reqInfo, err)
|
return nil, eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.next.GetRangeHash(ctx, request)
|
return b.next.GetRangeHash(requestContext(ctx, reqInfo), request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Service) PutSingle(ctx context.Context, request *objectV2.PutSingleRequest) (*objectV2.PutSingleResponse, error) {
|
func (b Service) PutSingle(ctx context.Context, request *objectV2.PutSingleRequest) (*objectV2.PutSingleResponse, error) {
|
||||||
|
@ -501,7 +572,7 @@ func (b Service) PutSingle(ctx context.Context, request *objectV2.PutSingleReque
|
||||||
return nil, eACLErr(reqInfo, err)
|
return nil, eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.next.PutSingle(ctx, request)
|
return b.next.PutSingle(requestContext(ctx, reqInfo), request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRequest) error {
|
func (p putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRequest) error {
|
||||||
|
@ -566,9 +637,11 @@ func (p putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRe
|
||||||
|
|
||||||
reqInfo.obj = obj
|
reqInfo.obj = obj
|
||||||
|
|
||||||
if err := p.source.apeChecker.CheckIfRequestPermitted(reqInfo); err != nil {
|
if !p.source.checker.CheckBasicACL(reqInfo) || !p.source.checker.StickyBitCheck(reqInfo, idOwner) {
|
||||||
return err
|
return basicACLErr(reqInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx = requestContext(ctx, reqInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.next.Send(ctx, request)
|
return p.next.Send(ctx, request)
|
||||||
|
@ -671,6 +744,7 @@ func (b Service) findRequestInfo(req MetaWithToken, idCnr cid.ID, op acl.Op) (in
|
||||||
info.operation = op
|
info.operation = op
|
||||||
info.cnrOwner = cnr.Value.Owner()
|
info.cnrOwner = cnr.Value.Owner()
|
||||||
info.idCnr = idCnr
|
info.idCnr = idCnr
|
||||||
|
info.cnrNamespace = cnr.Value.Attribute(containerV2.SysAttributeZone)
|
||||||
|
|
||||||
// it is assumed that at the moment the key will be valid,
|
// it is assumed that at the moment the key will be valid,
|
||||||
// otherwise the request would not pass validation
|
// otherwise the request would not pass validation
|
||||||
|
|
|
@ -26,9 +26,3 @@ type InnerRingFetcher interface {
|
||||||
// the actual inner ring.
|
// the actual inner ring.
|
||||||
InnerRingKeys() ([][]byte, error)
|
InnerRingKeys() ([][]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// APEChainChecker is the interface that provides methods to
|
|
||||||
// check if the access policy engine permits to perform the request.
|
|
||||||
type APEChainChecker interface {
|
|
||||||
CheckIfRequestPermitted(RequestInfo) error
|
|
||||||
}
|
|
||||||
|
|
82
pkg/services/object/ape/checker.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package ape
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||||
|
policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||||
|
)
|
||||||
|
|
||||||
|
type checkerImpl struct {
|
||||||
|
chainRouter policyengine.ChainRouter
|
||||||
|
|
||||||
|
headerProvider HeaderProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChecker(chainRouter policyengine.ChainRouter, headerProvider HeaderProvider) Checker {
|
||||||
|
return &checkerImpl{
|
||||||
|
chainRouter: chainRouter,
|
||||||
|
|
||||||
|
headerProvider: headerProvider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Prm struct {
|
||||||
|
Namespace string
|
||||||
|
|
||||||
|
Container cid.ID
|
||||||
|
|
||||||
|
// Object ID is omitted for some methods.
|
||||||
|
Object *oid.ID
|
||||||
|
|
||||||
|
// If Header is set, then object attributes and properties will be parsed from
|
||||||
|
// a request/response's header.
|
||||||
|
Header *objectV2.Header
|
||||||
|
|
||||||
|
// Method must be represented only as a constant represented in native schema.
|
||||||
|
Method string
|
||||||
|
|
||||||
|
// Role must be representedonly as a constant represented in native schema.
|
||||||
|
Role string
|
||||||
|
|
||||||
|
// An encoded sender's public key string.
|
||||||
|
SenderKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errMissingOID = fmt.Errorf("object ID is not set")
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckAPE checks if a request or a response is permitted creating an ape request and passing
|
||||||
|
// it to chain router.
|
||||||
|
func (c *checkerImpl) CheckAPE(ctx context.Context, prm Prm) error {
|
||||||
|
r, err := c.newAPERequest(ctx, prm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create ape request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, ruleFound, err := c.chainRouter.IsAllowed(apechain.Ingress,
|
||||||
|
policyengine.NewRequestTarget(prm.Namespace, prm.Container.EncodeToString()), r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ruleFound || status == apechain.Allow {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return apeErr(prm.Method, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessDeniedAPEReasonFmt = "access to operation %s is denied by access policy engine: %s"
|
||||||
|
|
||||||
|
func apeErr(op string, status apechain.Status) error {
|
||||||
|
errAccessDenied := &apistatus.ObjectAccessDenied{}
|
||||||
|
errAccessDenied.WriteReason(fmt.Sprintf(accessDeniedAPEReasonFmt, op, status.String()))
|
||||||
|
return errAccessDenied
|
||||||
|
}
|
348
pkg/services/object/ape/checker_test.go
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
package ape
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
|
||||||
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||||
|
policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||||
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
|
||||||
|
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type headerProviderMock struct {
|
||||||
|
m map[oid.Address]*objectSDK.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ HeaderProvider = (*headerProviderMock)(nil)
|
||||||
|
|
||||||
|
func (h *headerProviderMock) addHeader(c cid.ID, o oid.ID, header *objectSDK.Object) {
|
||||||
|
var addr oid.Address
|
||||||
|
addr.SetContainer(c)
|
||||||
|
addr.SetObject(o)
|
||||||
|
h.m[addr] = header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *headerProviderMock) GetHeader(_ context.Context, c cid.ID, o oid.ID) (*objectSDK.Object, error) {
|
||||||
|
var addr oid.Address
|
||||||
|
addr.SetContainer(c)
|
||||||
|
addr.SetObject(o)
|
||||||
|
obj, ok := h.m[addr]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("address not found")
|
||||||
|
}
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHeaderProviderMock() *headerProviderMock {
|
||||||
|
return &headerProviderMock{
|
||||||
|
m: make(map[oid.Address]*objectSDK.Object),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContainerIDSDK(t *testing.T, encodedCID string) cid.ID {
|
||||||
|
var cnr cid.ID
|
||||||
|
require.NoError(t, cnr.DecodeString(encodedCID))
|
||||||
|
return cnr
|
||||||
|
}
|
||||||
|
|
||||||
|
func newObjectIDSDK(t *testing.T, encodedOID *string) *oid.ID {
|
||||||
|
if encodedOID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
obj := new(oid.ID)
|
||||||
|
require.NoError(t, obj.DecodeString(*encodedOID))
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
type headerObjectSDKParams struct {
|
||||||
|
majorVersion, minorVersion uint32
|
||||||
|
owner user.ID
|
||||||
|
epoch uint64
|
||||||
|
payloadSize uint64
|
||||||
|
typ objectSDK.Type
|
||||||
|
payloadChecksum checksum.Checksum
|
||||||
|
payloadHomomorphicHash checksum.Checksum
|
||||||
|
attributes []struct {
|
||||||
|
key string
|
||||||
|
val string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringPtr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHeaderObjectSDK(t *testing.T, cnr cid.ID, oid *oid.ID, headerObjSDK *headerObjectSDKParams) *objectSDK.Object {
|
||||||
|
objSDK := objectSDK.New()
|
||||||
|
objSDK.SetContainerID(cnr)
|
||||||
|
if oid != nil {
|
||||||
|
objSDK.SetID(*oid)
|
||||||
|
}
|
||||||
|
if headerObjSDK == nil {
|
||||||
|
return objSDK
|
||||||
|
}
|
||||||
|
ver := new(version.Version)
|
||||||
|
ver.SetMajor(headerObjSDK.majorVersion)
|
||||||
|
ver.SetMinor(headerObjSDK.minorVersion)
|
||||||
|
objSDK.SetVersion(ver)
|
||||||
|
objSDK.SetCreationEpoch(headerObjSDK.epoch)
|
||||||
|
objSDK.SetOwnerID(headerObjSDK.owner)
|
||||||
|
objSDK.SetPayloadSize(headerObjSDK.payloadSize)
|
||||||
|
objSDK.SetType(headerObjSDK.typ)
|
||||||
|
objSDK.SetPayloadChecksum(headerObjSDK.payloadChecksum)
|
||||||
|
objSDK.SetPayloadHomomorphicHash(headerObjSDK.payloadHomomorphicHash)
|
||||||
|
|
||||||
|
var attrs []objectSDK.Attribute
|
||||||
|
for _, attr := range headerObjSDK.attributes {
|
||||||
|
attrSDK := objectSDK.NewAttribute()
|
||||||
|
attrSDK.SetKey(attr.key)
|
||||||
|
attrSDK.SetValue(attr.val)
|
||||||
|
attrs = append(attrs, *attrSDK)
|
||||||
|
}
|
||||||
|
objSDK.SetAttributes(attrs...)
|
||||||
|
|
||||||
|
return objSDK
|
||||||
|
}
|
||||||
|
|
||||||
|
type testHeader struct {
|
||||||
|
headerObjSDK *headerObjectSDKParams
|
||||||
|
|
||||||
|
// If fromHeaderProvider is set, then running test should
|
||||||
|
// consider that a header is recieved from a header provider.
|
||||||
|
fromHeaderProvider bool
|
||||||
|
|
||||||
|
// If fromHeaderProvider is set, then running test should
|
||||||
|
// consider that a header is recieved from a message header.
|
||||||
|
fromRequestResponseHeader bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
methodsRequiredOID = []string{
|
||||||
|
nativeschema.MethodGetObject,
|
||||||
|
nativeschema.MethodHeadObject,
|
||||||
|
nativeschema.MethodRangeObject,
|
||||||
|
nativeschema.MethodHashObject,
|
||||||
|
nativeschema.MethodDeleteObject,
|
||||||
|
}
|
||||||
|
|
||||||
|
methodsOptionalOID = []string{
|
||||||
|
nativeschema.MethodSearchObject, nativeschema.MethodPutObject,
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace = "test_namespace"
|
||||||
|
|
||||||
|
containerID = "73tQMTYyUkTgmvPR1HWib6pndbhSoBovbnMF7Pws8Rcy"
|
||||||
|
|
||||||
|
objectID = "BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R"
|
||||||
|
|
||||||
|
role = "Container"
|
||||||
|
|
||||||
|
senderKey = hex.EncodeToString([]byte{1, 0, 0, 1})
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPECheck(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
container string
|
||||||
|
object *string
|
||||||
|
methods []string
|
||||||
|
header testHeader
|
||||||
|
containerRules []chain.Rule
|
||||||
|
expectAPEErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "oid required requests are allowed",
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
methods: methodsRequiredOID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oid optional requests are allowed",
|
||||||
|
container: containerID,
|
||||||
|
methods: methodsOptionalOID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oid required requests are denied",
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
methods: methodsRequiredOID,
|
||||||
|
containerRules: []chain.Rule{
|
||||||
|
{
|
||||||
|
Status: chain.AccessDenied,
|
||||||
|
Actions: chain.Actions{Names: methodsRequiredOID},
|
||||||
|
Resources: chain.Resources{
|
||||||
|
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, containerID, objectID)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectAPEErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oid required requests are denied by an attribute",
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
methods: methodsRequiredOID,
|
||||||
|
header: testHeader{
|
||||||
|
headerObjSDK: &headerObjectSDKParams{
|
||||||
|
attributes: []struct {
|
||||||
|
key string
|
||||||
|
val string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
key: "attr1",
|
||||||
|
val: "attribute_value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fromHeaderProvider: true,
|
||||||
|
},
|
||||||
|
containerRules: []chain.Rule{
|
||||||
|
{
|
||||||
|
Status: chain.AccessDenied,
|
||||||
|
Actions: chain.Actions{Names: methodsRequiredOID},
|
||||||
|
Resources: chain.Resources{
|
||||||
|
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, containerID, objectID)},
|
||||||
|
},
|
||||||
|
Any: true,
|
||||||
|
Condition: []chain.Condition{
|
||||||
|
{
|
||||||
|
Op: chain.CondStringLike,
|
||||||
|
Object: chain.ObjectResource,
|
||||||
|
Key: "attr1",
|
||||||
|
Value: "attribute*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectAPEErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oid required requests are denied by sender",
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
methods: methodsRequiredOID,
|
||||||
|
header: testHeader{
|
||||||
|
headerObjSDK: &headerObjectSDKParams{
|
||||||
|
attributes: []struct {
|
||||||
|
key string
|
||||||
|
val string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
key: "attr1",
|
||||||
|
val: "attribute_value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fromHeaderProvider: true,
|
||||||
|
},
|
||||||
|
containerRules: []chain.Rule{
|
||||||
|
{
|
||||||
|
Status: chain.AccessDenied,
|
||||||
|
Actions: chain.Actions{Names: methodsRequiredOID},
|
||||||
|
Resources: chain.Resources{
|
||||||
|
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, containerID, objectID)},
|
||||||
|
},
|
||||||
|
Any: true,
|
||||||
|
Condition: []chain.Condition{
|
||||||
|
{
|
||||||
|
Op: chain.CondStringLike,
|
||||||
|
Object: chain.ObjectRequest,
|
||||||
|
Key: nativeschema.PropertyKeyActorPublicKey,
|
||||||
|
Value: senderKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectAPEErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "optional oid requests reached quota limit by an attribute",
|
||||||
|
container: containerID,
|
||||||
|
methods: methodsOptionalOID,
|
||||||
|
header: testHeader{
|
||||||
|
headerObjSDK: &headerObjectSDKParams{
|
||||||
|
payloadSize: 1000,
|
||||||
|
},
|
||||||
|
fromRequestResponseHeader: true,
|
||||||
|
},
|
||||||
|
containerRules: []chain.Rule{
|
||||||
|
{
|
||||||
|
Status: chain.QuotaLimitReached,
|
||||||
|
Actions: chain.Actions{Names: methodsOptionalOID},
|
||||||
|
Resources: chain.Resources{
|
||||||
|
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, containerID)},
|
||||||
|
},
|
||||||
|
Any: true,
|
||||||
|
Condition: []chain.Condition{
|
||||||
|
{
|
||||||
|
Op: chain.CondStringEquals,
|
||||||
|
Object: chain.ObjectResource,
|
||||||
|
Key: nativeschema.PropertyKeyObjectPayloadLength,
|
||||||
|
Value: "1000",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectAPEErr: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
for _, method := range test.methods {
|
||||||
|
t.Run(method, func(t *testing.T) {
|
||||||
|
headerProvider := newHeaderProviderMock()
|
||||||
|
|
||||||
|
cnr := newContainerIDSDK(t, test.container)
|
||||||
|
obj := newObjectIDSDK(t, test.object)
|
||||||
|
|
||||||
|
ls := inmemory.NewInmemoryLocalStorage()
|
||||||
|
ms := inmemory.NewInmemoryMorphRuleChainStorage()
|
||||||
|
|
||||||
|
ls.AddOverride(chain.Ingress, policyengine.ContainerTarget(test.container), &chain.Chain{
|
||||||
|
Rules: test.containerRules,
|
||||||
|
})
|
||||||
|
|
||||||
|
router := policyengine.NewDefaultChainRouterWithLocalOverrides(ms, ls)
|
||||||
|
|
||||||
|
checker := NewChecker(router, headerProvider)
|
||||||
|
|
||||||
|
prm := Prm{
|
||||||
|
Method: method,
|
||||||
|
Container: cnr,
|
||||||
|
Object: obj,
|
||||||
|
Role: role,
|
||||||
|
SenderKey: senderKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerObjSDK *objectSDK.Object
|
||||||
|
if test.header.headerObjSDK != nil {
|
||||||
|
headerObjSDK = newHeaderObjectSDK(t, cnr, obj, test.header.headerObjSDK)
|
||||||
|
if test.header.fromHeaderProvider {
|
||||||
|
require.NotNil(t, obj, "oid is required if a header is expected to be found in header provider")
|
||||||
|
headerProvider.addHeader(cnr, *obj, headerObjSDK)
|
||||||
|
} else if test.header.fromRequestResponseHeader {
|
||||||
|
prm.Header = headerObjSDK.ToV2().GetHeader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := checker.CheckAPE(context.Background(), prm)
|
||||||
|
if test.expectAPEErr {
|
||||||
|
aErr := apeErr(method, chain.AccessDenied)
|
||||||
|
require.ErrorAs(t, err, &aErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
159
pkg/services/object/ape/request.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
package ape
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||||
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
aperesource "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
||||||
|
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||||
|
)
|
||||||
|
|
||||||
|
type request struct {
|
||||||
|
operation string
|
||||||
|
resource *resource
|
||||||
|
properties map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ aperesource.Request = (*request)(nil)
|
||||||
|
|
||||||
|
type resource struct {
|
||||||
|
name string
|
||||||
|
properties map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ aperesource.Resource = (*resource)(nil)
|
||||||
|
|
||||||
|
func (r *resource) Name() string {
|
||||||
|
return r.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) Property(key string) string {
|
||||||
|
return r.properties[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *request) Operation() string {
|
||||||
|
return r.operation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *request) Property(key string) string {
|
||||||
|
return r.properties[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *request) Resource() aperesource.Resource {
|
||||||
|
return r.resource
|
||||||
|
}
|
||||||
|
|
||||||
|
func nativeSchemaRole(role acl.Role) string {
|
||||||
|
switch role {
|
||||||
|
case acl.RoleOwner:
|
||||||
|
return nativeschema.PropertyValueContainerRoleOwner
|
||||||
|
case acl.RoleContainer:
|
||||||
|
return nativeschema.PropertyValueContainerRoleContainer
|
||||||
|
case acl.RoleInnerRing:
|
||||||
|
return nativeschema.PropertyValueContainerRoleIR
|
||||||
|
case acl.RoleOthers:
|
||||||
|
return nativeschema.PropertyValueContainerRoleOthers
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceName(cid cid.ID, oid *oid.ID, namespace string) string {
|
||||||
|
if namespace == "root" || namespace == "" {
|
||||||
|
if oid != nil {
|
||||||
|
return fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, cid.EncodeToString(), oid.EncodeToString())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cid.EncodeToString())
|
||||||
|
}
|
||||||
|
if oid != nil {
|
||||||
|
return fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainerObject, namespace, cid.EncodeToString(), oid.EncodeToString())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainerObjects, namespace, cid.EncodeToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// objectProperties collects object properties from address parameters and a header if it is passed.
|
||||||
|
func objectProperties(cnr cid.ID, oid *oid.ID, header *objectV2.Header) map[string]string {
|
||||||
|
objectProps := map[string]string{
|
||||||
|
nativeschema.PropertyKeyObjectContainerID: cnr.EncodeToString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if oid != nil {
|
||||||
|
objectProps[nativeschema.PropertyKeyObjectID] = oid.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if header == nil {
|
||||||
|
return objectProps
|
||||||
|
}
|
||||||
|
|
||||||
|
objV2 := new(objectV2.Object)
|
||||||
|
objV2.SetHeader(header)
|
||||||
|
objSDK := objectSDK.NewFromV2(objV2)
|
||||||
|
|
||||||
|
objectProps[nativeschema.PropertyKeyObjectVersion] = objSDK.Version().String()
|
||||||
|
objectProps[nativeschema.PropertyKeyObjectOwnerID] = objSDK.OwnerID().EncodeToString()
|
||||||
|
objectProps[nativeschema.PropertyKeyObjectCreationEpoch] = strconv.Itoa(int(objSDK.CreationEpoch()))
|
||||||
|
objectProps[nativeschema.PropertyKeyObjectPayloadLength] = strconv.Itoa(int(objSDK.PayloadSize()))
|
||||||
|
objectProps[nativeschema.PropertyKeyObjectType] = objSDK.Type().String()
|
||||||
|
|
||||||
|
pcs, isSet := objSDK.PayloadChecksum()
|
||||||
|
if isSet {
|
||||||
|
objectProps[nativeschema.PropertyKeyObjectPayloadHash] = pcs.String()
|
||||||
|
}
|
||||||
|
hcs, isSet := objSDK.PayloadHomomorphicHash()
|
||||||
|
if isSet {
|
||||||
|
objectProps[nativeschema.PropertyKeyObjectHomomorphicHash] = hcs.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, attr := range header.GetAttributes() {
|
||||||
|
objectProps[attr.GetKey()] = attr.GetValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectProps
|
||||||
|
}
|
||||||
|
|
||||||
|
// newAPERequest creates an APE request to be passed to a chain router. It collects resource properties from
|
||||||
|
// header provided by headerProvider. If it cannot be found in headerProvider, then properties are
|
||||||
|
// initialized from header given in prm (if it is set). Otherwise, just CID and OID are set to properties.
|
||||||
|
func (c *checkerImpl) newAPERequest(ctx context.Context, prm Prm) (*request, error) {
|
||||||
|
switch prm.Method {
|
||||||
|
case nativeschema.MethodGetObject,
|
||||||
|
nativeschema.MethodHeadObject,
|
||||||
|
nativeschema.MethodRangeObject,
|
||||||
|
nativeschema.MethodHashObject,
|
||||||
|
nativeschema.MethodDeleteObject:
|
||||||
|
if prm.Object == nil {
|
||||||
|
return nil, fmt.Errorf("method %s: %w", prm.Method, errMissingOID)
|
||||||
|
}
|
||||||
|
case nativeschema.MethodSearchObject, nativeschema.MethodPutObject:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown method: %s", prm.Method)
|
||||||
|
}
|
||||||
|
|
||||||
dstepanov-yadro marked this conversation as resolved
Outdated
dstepanov-yadro
commented
Is it ok to skip getHeader error? Is it ok to skip getHeader error?
aarifullin
commented
Yes. If Yes. If `err != nil`, then it means the object is not found locally (check [this](https://git.frostfs.info/TrueCloudLab/frostfs-node/pulls/888#issuecomment-30310) out).
Do you suggest to check it for `NotFoundError`?
|
|||||||
|
var header *objectV2.Header
|
||||||
|
if prm.Header != nil {
|
||||||
|
header = prm.Header
|
||||||
|
} else if prm.Object != nil {
|
||||||
|
headerObjSDK, err := c.headerProvider.GetHeader(ctx, prm.Container, *prm.Object)
|
||||||
|
if err == nil {
|
||||||
|
header = headerObjSDK.ToV2().GetHeader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &request{
|
||||||
|
operation: prm.Method,
|
||||||
|
resource: &resource{
|
||||||
|
name: resourceName(prm.Container, prm.Object, prm.Namespace),
|
||||||
|
properties: objectProperties(prm.Container, prm.Object, header),
|
||||||
|
},
|
||||||
|
properties: map[string]string{
|
||||||
|
nativeschema.PropertyKeyActorPublicKey: prm.SenderKey,
|
||||||
|
nativeschema.PropertyKeyActorRole: prm.Role,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
324
pkg/services/object/ape/request_test.go
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
package ape
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
|
checksumtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum/test"
|
||||||
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
|
||||||
|
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestObjectProperties(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
container string
|
||||||
|
object *string
|
||||||
|
header *headerObjectSDKParams
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fully filled header",
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
header: &headerObjectSDKParams{
|
||||||
|
majorVersion: 1,
|
||||||
|
minorVersion: 1,
|
||||||
|
owner: usertest.ID(),
|
||||||
|
epoch: 3,
|
||||||
|
payloadSize: 1000,
|
||||||
|
typ: objectSDK.TypeRegular,
|
||||||
|
payloadChecksum: checksumtest.Checksum(),
|
||||||
|
payloadHomomorphicHash: checksumtest.Checksum(),
|
||||||
|
attributes: []struct {
|
||||||
|
key string
|
||||||
|
val string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
key: "attr1",
|
||||||
|
val: "val1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "attr2",
|
||||||
|
val: "val2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partially filled header",
|
||||||
|
container: containerID,
|
||||||
|
header: &headerObjectSDKParams{
|
||||||
|
majorVersion: 1,
|
||||||
|
minorVersion: 1,
|
||||||
|
owner: usertest.ID(),
|
||||||
|
epoch: 3,
|
||||||
|
attributes: []struct {
|
||||||
|
key string
|
||||||
|
val string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
key: "attr1",
|
||||||
|
val: "val1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only address paramaters set in header",
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only container set in header",
|
||||||
|
container: containerID,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
cnr := newContainerIDSDK(t, test.container)
|
||||||
|
obj := newObjectIDSDK(t, test.object)
|
||||||
|
header := newHeaderObjectSDK(t, cnr, obj, test.header)
|
||||||
|
|
||||||
|
props := objectProperties(cnr, obj, header.ToV2().GetHeader())
|
||||||
|
require.Equal(t, test.container, props[nativeschema.PropertyKeyObjectContainerID])
|
||||||
|
|
||||||
|
if obj != nil {
|
||||||
|
require.Equal(t, *test.object, props[nativeschema.PropertyKeyObjectID])
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.header != nil {
|
||||||
|
require.Equal(t,
|
||||||
|
fmt.Sprintf("v%d.%d", test.header.majorVersion, test.header.minorVersion),
|
||||||
|
props[nativeschema.PropertyKeyObjectVersion],
|
||||||
|
)
|
||||||
|
require.Equal(t, test.header.owner.EncodeToString(), props[nativeschema.PropertyKeyObjectOwnerID])
|
||||||
|
require.Equal(t, fmt.Sprintf("%d", test.header.epoch), props[nativeschema.PropertyKeyObjectCreationEpoch])
|
||||||
|
require.Equal(t, fmt.Sprintf("%d", test.header.payloadSize), props[nativeschema.PropertyKeyObjectPayloadLength])
|
||||||
|
require.Equal(t, test.header.typ.String(), props[nativeschema.PropertyKeyObjectType])
|
||||||
|
require.Equal(t, test.header.payloadChecksum.String(), props[nativeschema.PropertyKeyObjectPayloadHash])
|
||||||
|
require.Equal(t, test.header.payloadHomomorphicHash.String(), props[nativeschema.PropertyKeyObjectHomomorphicHash])
|
||||||
|
|
||||||
|
for _, attr := range test.header.attributes {
|
||||||
|
require.Equal(t, attr.val, props[attr.key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAPERequest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
methods []string
|
||||||
|
namespace string
|
||||||
|
container string
|
||||||
|
object *string
|
||||||
|
header testHeader
|
||||||
|
expectErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "oid required requests",
|
||||||
|
methods: methodsRequiredOID,
|
||||||
|
namespace: namespace,
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
header: testHeader{
|
||||||
|
headerObjSDK: &headerObjectSDKParams{
|
||||||
|
majorVersion: 1,
|
||||||
|
minorVersion: 1,
|
||||||
|
owner: usertest.ID(),
|
||||||
|
epoch: 3,
|
||||||
|
payloadSize: 1000,
|
||||||
|
typ: objectSDK.TypeRegular,
|
||||||
|
payloadChecksum: checksumtest.Checksum(),
|
||||||
|
payloadHomomorphicHash: checksumtest.Checksum(),
|
||||||
|
},
|
||||||
|
fromHeaderProvider: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oid required requests but header cannot be found locally",
|
||||||
|
methods: methodsRequiredOID,
|
||||||
|
namespace: namespace,
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
header: testHeader{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oid required requests missed oid",
|
||||||
|
methods: methodsRequiredOID,
|
||||||
|
namespace: namespace,
|
||||||
|
container: containerID,
|
||||||
|
object: nil,
|
||||||
|
header: testHeader{},
|
||||||
|
expectErr: errMissingOID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "response for oid required requests",
|
||||||
|
methods: methodsRequiredOID,
|
||||||
|
namespace: namespace,
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
header: testHeader{
|
||||||
|
headerObjSDK: &headerObjectSDKParams{
|
||||||
|
majorVersion: 1,
|
||||||
|
minorVersion: 1,
|
||||||
|
owner: usertest.ID(),
|
||||||
|
epoch: 3,
|
||||||
|
payloadSize: 1000,
|
||||||
|
typ: objectSDK.TypeRegular,
|
||||||
|
payloadChecksum: checksumtest.Checksum(),
|
||||||
|
payloadHomomorphicHash: checksumtest.Checksum(),
|
||||||
|
},
|
||||||
|
fromRequestResponseHeader: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oid not required methods request",
|
||||||
|
methods: methodsOptionalOID,
|
||||||
|
namespace: namespace,
|
||||||
|
container: containerID,
|
||||||
|
object: nil,
|
||||||
|
header: testHeader{
|
||||||
|
headerObjSDK: &headerObjectSDKParams{
|
||||||
|
majorVersion: 6,
|
||||||
|
minorVersion: 66,
|
||||||
|
owner: usertest.ID(),
|
||||||
|
epoch: 3,
|
||||||
|
typ: objectSDK.TypeLock,
|
||||||
|
},
|
||||||
|
fromRequestResponseHeader: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oid not required methods request but no header",
|
||||||
|
methods: methodsOptionalOID,
|
||||||
|
namespace: namespace,
|
||||||
|
container: containerID,
|
||||||
|
object: nil,
|
||||||
|
header: testHeader{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
for _, method := range test.methods {
|
||||||
|
t.Run(method, func(t *testing.T) {
|
||||||
|
cnr := newContainerIDSDK(t, test.container)
|
||||||
|
obj := newObjectIDSDK(t, test.object)
|
||||||
|
|
||||||
|
prm := Prm{
|
||||||
|
Namespace: test.namespace,
|
||||||
|
Method: method,
|
||||||
|
Container: cnr,
|
||||||
|
Object: obj,
|
||||||
|
Role: role,
|
||||||
|
SenderKey: senderKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
headerSource := newHeaderProviderMock()
|
||||||
|
|
||||||
|
var headerObjSDK *objectSDK.Object
|
||||||
|
if test.header.headerObjSDK != nil {
|
||||||
|
headerObjSDK = newHeaderObjectSDK(t, cnr, obj, test.header.headerObjSDK)
|
||||||
|
if test.header.fromHeaderProvider {
|
||||||
|
require.NotNil(t, obj, "oid is required if a header is expected to be found in header provider")
|
||||||
|
headerSource.addHeader(cnr, *obj, headerObjSDK)
|
||||||
|
} else if test.header.fromRequestResponseHeader {
|
||||||
|
prm.Header = headerObjSDK.ToV2().GetHeader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := checkerImpl{
|
||||||
|
headerProvider: headerSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := c.newAPERequest(context.TODO(), prm)
|
||||||
|
if test.expectErr != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, test.expectErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedRequest := request{
|
||||||
|
operation: method,
|
||||||
|
resource: &resource{
|
||||||
|
name: resourceName(cnr, obj, prm.Namespace),
|
||||||
|
properties: objectProperties(cnr, obj, func() *objectV2.Header {
|
||||||
|
if headerObjSDK != nil {
|
||||||
|
return headerObjSDK.ToV2().GetHeader()
|
||||||
|
}
|
||||||
|
return prm.Header
|
||||||
|
}()),
|
||||||
|
},
|
||||||
|
properties: map[string]string{
|
||||||
|
nativeschema.PropertyKeyActorPublicKey: prm.SenderKey,
|
||||||
|
nativeschema.PropertyKeyActorRole: prm.Role,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, expectedRequest, *r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceName(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
container string
|
||||||
|
object *string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "non-root namespace, CID",
|
||||||
|
namespace: namespace,
|
||||||
|
container: containerID,
|
||||||
|
expected: fmt.Sprintf("native:object/%s/%s/*", namespace, containerID),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-root namespace, CID, OID",
|
||||||
|
namespace: namespace,
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
expected: fmt.Sprintf("native:object/%s/%s/%s", namespace, containerID, objectID),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty namespace, CID",
|
||||||
|
namespace: "",
|
||||||
|
container: containerID,
|
||||||
|
expected: fmt.Sprintf("native:object//%s/*", containerID),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty namespace, CID, OID",
|
||||||
|
namespace: "",
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
expected: fmt.Sprintf("native:object//%s/%s", containerID, objectID),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "root namespace, CID",
|
||||||
|
namespace: "root",
|
||||||
|
container: containerID,
|
||||||
|
expected: fmt.Sprintf("native:object//%s/*", containerID),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "root namespace, CID, OID",
|
||||||
|
namespace: "root",
|
||||||
|
container: containerID,
|
||||||
|
object: stringPtr(objectID),
|
||||||
|
expected: fmt.Sprintf("native:object//%s/%s", containerID, objectID),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
cnr := newContainerIDSDK(t, test.container)
|
||||||
|
obj := newObjectIDSDK(t, test.object)
|
||||||
|
require.Equal(t, test.expected, resourceName(cnr, obj, test.namespace))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
404
pkg/services/object/ape/service.go
Normal file
|
@ -0,0 +1,404 @@
|
||||||
|
package ape
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/engine"
|
||||||
|
objectSvc "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
|
||||||
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
log *logger.Logger
|
||||||
|
|
||||||
|
apeChecker Checker
|
||||||
|
|
||||||
|
next objectSvc.ServiceServer
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ objectSvc.ServiceServer = (*Service)(nil)
|
||||||
|
|
||||||
|
type HeaderProvider interface {
|
||||||
|
GetHeader(ctx context.Context, cnr cid.ID, oid oid.ID) (*objectSDK.Object, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type storageEngineHeaderProvider struct {
|
||||||
|
storageEngine *engine.StorageEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p storageEngineHeaderProvider) GetHeader(ctx context.Context, cnr cid.ID, objID oid.ID) (*objectSDK.Object, error) {
|
||||||
|
var addr oid.Address
|
||||||
|
addr.SetContainer(cnr)
|
||||||
|
addr.SetObject(objID)
|
||||||
|
return engine.Head(ctx, p.storageEngine, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorageEngineHeaderProvider(e *engine.StorageEngine) HeaderProvider {
|
||||||
|
return storageEngineHeaderProvider{
|
||||||
|
storageEngine: e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(log *logger.Logger, apeChecker Checker, next objectSvc.ServiceServer) *Service {
|
||||||
|
return &Service{
|
||||||
|
log: log,
|
||||||
|
apeChecker: apeChecker,
|
||||||
|
next: next,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type getStreamBasicChecker struct {
|
||||||
|
objectSvc.GetObjectStream
|
||||||
|
|
||||||
|
apeChecker Checker
|
||||||
|
|
||||||
|
senderKey []byte
|
||||||
|
|
||||||
|
role string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error {
|
||||||
|
if partInit, ok := resp.GetBody().GetObjectPart().(*objectV2.GetObjectPartInit); ok {
|
||||||
|
cnrID, objID, err := getAddressParamsSDK(partInit.GetHeader().GetContainerID(), partInit.GetObjectID())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
prm := Prm{
|
||||||
|
Container: cnrID,
|
||||||
|
Object: objID,
|
||||||
|
Header: partInit.GetHeader(),
|
||||||
|
Method: nativeschema.MethodGetContainer,
|
||||||
|
SenderKey: hex.EncodeToString(g.senderKey),
|
||||||
|
Role: g.role,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.apeChecker.CheckAPE(g.Context(), prm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return g.GetObjectStream.Send(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestContext(ctx context.Context) (*objectSvc.RequestContext, error) {
|
||||||
|
untyped := ctx.Value(objectSvc.RequestContextKey)
|
||||||
|
if untyped == nil {
|
||||||
|
return nil, fmt.Errorf("no key %s in context", objectSvc.RequestContextKey)
|
||||||
acid-ant marked this conversation as resolved
Outdated
acid-ant
commented
Maybe just Maybe just `no key %s in context`?
aarifullin
commented
Fixed Fixed
|
|||||||
|
}
|
||||||
|
rc, ok := untyped.(*objectSvc.RequestContext)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed cast to RequestContext")
|
||||||
|
}
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Service) Get(request *objectV2.GetRequest, stream objectSvc.GetObjectStream) error {
|
||||||
|
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCtx, err := requestContext(stream.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.apeChecker.CheckAPE(stream.Context(), Prm{
|
||||||
|
Namespace: reqCtx.Namespace,
|
||||||
|
Container: cnrID,
|
||||||
|
Object: objID,
|
||||||
|
Method: nativeschema.MethodGetObject,
|
||||||
|
Role: nativeSchemaRole(reqCtx.Role),
|
||||||
|
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.next.Get(request, &getStreamBasicChecker{
|
||||||
|
GetObjectStream: stream,
|
||||||
|
apeChecker: c.apeChecker,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type putStreamBasicChecker struct {
|
||||||
|
apeChecker Checker
|
||||||
|
|
||||||
|
next objectSvc.PutObjectStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRequest) error {
|
||||||
|
if partInit, ok := request.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit); ok {
|
||||||
|
reqCtx, err := requestContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cnrID, objID, err := getAddressParamsSDK(partInit.GetHeader().GetContainerID(), partInit.GetObjectID())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
prm := Prm{
|
||||||
|
Namespace: reqCtx.Namespace,
|
||||||
|
Container: cnrID,
|
||||||
|
Object: objID,
|
||||||
|
Header: partInit.GetHeader(),
|
||||||
|
Method: nativeschema.MethodPutObject,
|
||||||
|
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
||||||
|
Role: nativeSchemaRole(reqCtx.Role),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.apeChecker.CheckAPE(ctx, prm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.next.Send(ctx, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p putStreamBasicChecker) CloseAndRecv(ctx context.Context) (*objectV2.PutResponse, error) {
|
||||||
|
return p.next.CloseAndRecv(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Service) Put() (objectSvc.PutObjectStream, error) {
|
||||||
|
streamer, err := c.next.Put()
|
||||||
|
|
||||||
|
return &putStreamBasicChecker{
|
||||||
|
apeChecker: c.apeChecker,
|
||||||
|
next: streamer,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Service) Head(ctx context.Context, request *objectV2.HeadRequest) (*objectV2.HeadResponse, error) {
|
||||||
|
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCtx, err := requestContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.apeChecker.CheckAPE(ctx, Prm{
|
||||||
|
Namespace: reqCtx.Namespace,
|
||||||
|
Container: cnrID,
|
||||||
|
Object: objID,
|
||||||
|
Method: nativeschema.MethodHeadObject,
|
||||||
|
Role: nativeSchemaRole(reqCtx.Role),
|
||||||
|
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.next.Head(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
header := new(objectV2.Header)
|
||||||
|
switch headerPart := resp.GetBody().GetHeaderPart().(type) {
|
||||||
|
case *objectV2.ShortHeader:
|
||||||
|
cidV2 := new(refs.ContainerID)
|
||||||
|
cnrID.WriteToV2(cidV2)
|
||||||
|
header.SetContainerID(cidV2)
|
||||||
|
header.SetVersion(headerPart.GetVersion())
|
||||||
|
header.SetCreationEpoch(headerPart.GetCreationEpoch())
|
||||||
|
header.SetOwnerID(headerPart.GetOwnerID())
|
||||||
|
header.SetObjectType(headerPart.GetObjectType())
|
||||||
|
header.SetHomomorphicHash(header.GetHomomorphicHash())
|
||||||
|
header.SetPayloadLength(headerPart.GetPayloadLength())
|
||||||
|
header.SetPayloadHash(headerPart.GetPayloadHash())
|
||||||
|
case *objectV2.HeaderWithSignature:
|
||||||
|
header = headerPart.GetHeader()
|
||||||
|
default:
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.apeChecker.CheckAPE(ctx, Prm{
|
||||||
|
Namespace: reqCtx.Namespace,
|
||||||
|
Container: cnrID,
|
||||||
|
Object: objID,
|
||||||
|
Header: header,
|
||||||
|
Method: nativeschema.MethodHeadObject,
|
||||||
|
Role: nativeSchemaRole(reqCtx.Role),
|
||||||
|
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Service) Search(request *objectV2.SearchRequest, stream objectSvc.SearchStream) error {
|
||||||
|
var cnrID cid.ID
|
||||||
|
if cnrV2 := request.GetBody().GetContainerID(); cnrV2 != nil {
|
||||||
|
if err := cnrID.ReadFromV2(*cnrV2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCtx, err := requestContext(stream.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.apeChecker.CheckAPE(stream.Context(), Prm{
|
||||||
|
Namespace: reqCtx.Namespace,
|
||||||
|
Container: cnrID,
|
||||||
|
Method: nativeschema.MethodSearchObject,
|
||||||
|
Role: nativeSchemaRole(reqCtx.Role),
|
||||||
|
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.next.Search(request, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Service) Delete(ctx context.Context, request *objectV2.DeleteRequest) (*objectV2.DeleteResponse, error) {
|
||||||
|
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCtx, err := requestContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.apeChecker.CheckAPE(ctx, Prm{
|
||||||
|
Namespace: reqCtx.Namespace,
|
||||||
|
Container: cnrID,
|
||||||
|
Object: objID,
|
||||||
|
Method: nativeschema.MethodDeleteObject,
|
||||||
|
Role: nativeSchemaRole(reqCtx.Role),
|
||||||
|
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
acid-ant marked this conversation as resolved
Outdated
acid-ant
commented
Looks like redundant check. Looks like redundant check.
aarifullin
commented
I think you are right. I will remove this check I think you are right. I will remove this check
aarifullin
commented
Nah, let's keep it like that. Please, check this comment out Nah, let's keep it like that. Please, check this [comment](https://git.frostfs.info/TrueCloudLab/frostfs-node/pulls/888/files#issuecomment-30310) out
acid-ant
commented
I agree for I agree for `get` like requests, now it is clear, but for delete it looks strange. Even if we reject the whole `delete` request, we perform physical deletion, isn't it?
aarifullin
commented
This sounds really reasonable, okay. I'll remove it > Even if we reject the whole delete request, we perform physical deletion, isn't it?
This sounds really reasonable, okay. I'll remove it
aarifullin
commented
Fixed Fixed
acid-ant
commented
Just for my information, in this case when we will get attributes of the object to do some checks via Just for my information, in this case when we will get attributes of the object to do some checks via `ape`?
aarifullin
commented
> we will get attributes
https://git.frostfs.info/TrueCloudLab/frostfs-node/pulls/888/files#diff-1e1d20897061c8991e8e64e87a89cd65aef8d86f
` objectProperties` method
|
|||||||
|
resp, err := c.next.Delete(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Service) GetRange(request *objectV2.GetRangeRequest, stream objectSvc.GetObjectRangeStream) error {
|
||||||
|
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCtx, err := requestContext(stream.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.apeChecker.CheckAPE(stream.Context(), Prm{
|
||||||
|
Namespace: reqCtx.Namespace,
|
||||||
|
Container: cnrID,
|
||||||
|
Object: objID,
|
||||||
|
Method: nativeschema.MethodRangeObject,
|
||||||
|
Role: nativeSchemaRole(reqCtx.Role),
|
||||||
|
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.next.GetRange(request, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Service) GetRangeHash(ctx context.Context, request *objectV2.GetRangeHashRequest) (*objectV2.GetRangeHashResponse, error) {
|
||||||
|
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCtx, err := requestContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prm := Prm{
|
||||||
|
Namespace: reqCtx.Namespace,
|
||||||
|
Container: cnrID,
|
||||||
|
Object: objID,
|
||||||
|
Method: nativeschema.MethodHashObject,
|
||||||
|
Role: nativeSchemaRole(reqCtx.Role),
|
||||||
|
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.apeChecker.CheckAPE(ctx, prm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.next.GetRangeHash(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.apeChecker.CheckAPE(ctx, prm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
acid-ant marked this conversation as resolved
Outdated
acid-ant
commented
Why we need to perform this check twice? Why we need to perform this check twice?
aarifullin
commented
This idea is not mine, it is already used in object service. That doesn't look obvious but first check If it is passed after first check, we get the requested object, we get it with filled header (-> properties) and we need to perform the check second time to these cases when APE check can be performed only on object attributes like This idea is not mine, it is already used in object service.
That doesn't look obvious but first check `c.apeChecker.CheckAPE` is kind of _trial_ invocation: if an object is placed _locally_, then an object's header can be recieved immediately and thus we are able to collect all object properties like [system](https://git.frostfs.info/TrueCloudLab/policy-engine/src/branch/master/schema/native/consts.go#L40-L48) and [user](https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/branch/master/object/wellknown_attributes.go) attributes . If it cannot be found locally, then only CID, OID attributes passed to APE: at least this can be used for checks like "does APE _allow_ to put more object to the container?" - we can reduce response time because we don't need to get entire object to check if the request is denined by <CID, OID> in APE.
If it is passed after first check, we get the requested object, we get it with filled header (-> properties) and we need to perform the check second time to these cases when APE check can be performed only on object attributes like `payloadHash < 1000?` or `objectType != regular`. That is fair for all get-like methods but you was right `delete` barely needs this second check (_however, we still want to check if an object with some attributes is allowed to get deleted_)
acid-ant
commented
Now it is pretty much clear, thanks. Now it is pretty much clear, thanks.
|
|||||||
|
return resp, nil
|
||||||
|
}
|
||||||
dstepanov-yadro marked this conversation as resolved
Outdated
dstepanov-yadro
commented
Don't understand: line 356 is APE check. Don't understand: line 356 is APE check.
aarifullin
commented
Sorry, that was left after previous workaround :). It'll be removed! Sorry, that was left after previous workaround :). It'll be removed!
|
|||||||
|
|
||||||
|
func (c *Service) PutSingle(ctx context.Context, request *objectV2.PutSingleRequest) (*objectV2.PutSingleResponse, error) {
|
||||||
|
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetObject().GetHeader().GetContainerID(), request.GetBody().GetObject().GetObjectID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCtx, err := requestContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prm := Prm{
|
||||||
|
Namespace: reqCtx.Namespace,
|
||||||
|
Container: cnrID,
|
||||||
|
Object: objID,
|
||||||
|
Header: request.GetBody().GetObject().GetHeader(),
|
||||||
|
Method: nativeschema.MethodPutObject,
|
||||||
|
Role: nativeSchemaRole(reqCtx.Role),
|
||||||
|
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.apeChecker.CheckAPE(ctx, prm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.next.PutSingle(ctx, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAddressParamsSDK(cidV2 *refs.ContainerID, objV2 *refs.ObjectID) (cnrID cid.ID, objID *oid.ID, err error) {
|
||||||
|
if cidV2 != nil {
|
||||||
|
if err = cnrID.ReadFromV2(*cidV2); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if objV2 != nil {
|
||||||
|
objID = new(oid.ID)
|
||||||
|
if err = objID.ReadFromV2(*objV2); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
9
pkg/services/object/ape/types.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package ape
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Checker provides methods to check requests and responses
|
||||||
|
// with access policy engine.
|
||||||
|
type Checker interface {
|
||||||
|
CheckAPE(context.Context, Prm) error
|
||||||
|
}
|
16
pkg/services/object/request_context.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package object
|
||||||
|
|
||||||
|
import "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||||
|
|
||||||
|
type RequestContextKeyT struct{}
|
||||||
fyrchik
commented
It is usually better to use It is usually better to use `RequestContextKey struct{}` and empty value as a key: this is the approach used in golang `net` stdlib.
aarifullin
commented
Thank you, fixed Thank you, fixed
|
|||||||
|
|
||||||
|
var RequestContextKey = RequestContextKeyT{}
|
||||||
|
|
||||||
|
// RequestContext is a context passed between middleware handlers.
|
||||||
|
type RequestContext struct {
|
||||||
|
Namespace string
|
||||||
|
|
||||||
|
SenderKey []byte
|
||||||
|
|
||||||
|
Role acl.Role
|
||||||
|
}
|
stream
has context.