[#872] object: Use APE instead EACL for object service handlers #888
15 changed files with 1457 additions and 176 deletions
|
@ -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
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
BIN
go.sum
Binary file not shown.
|
@ -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
|
||||||
|
|
||||||
|
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
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
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
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
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
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)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
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
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{}
|
||||||
|
|
||||||
|
var RequestContextKey = RequestContextKeyT{}
|
||||||
|
|
||||||
|
// RequestContext is a context passed between middleware handlers.
|
||||||
|
type RequestContext struct {
|
||||||
|
Namespace string
|
||||||
|
|
||||||
|
SenderKey []byte
|
||||||
|
|
||||||
|
Role acl.Role
|
||||||
|
}
|
Loading…
Reference in a new issue