[#872] object: Use APE instead EACL for object service handlers #888

Closed
aarifullin wants to merge 1 commit from feature/872-object_svc_ape into master
15 changed files with 1457 additions and 176 deletions

View file

@ -24,6 +24,7 @@ import (
objectService "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl"
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"
deletesvcV2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/delete/v2"
getsvc "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/get"
@ -183,11 +184,13 @@ func initObjectService(c *cfg) {
sDeleteV2 := createDeleteServiceV2(sDelete)
// build service pipeline
// grpc | <metrics> | signature | response | acl | split
// grpc | <metrics> | signature | response | acl | ape | split
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
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
return v2.New(
splitSvc,
apeSvc,
c.netMapSource,
irFetcher,
acl.NewChecker(
@ -426,12 +429,22 @@ func createACLServiceV2(c *cfg, splitSvc *objectService.TransportSplitter, irFet
c.cfgObject.eaclSource,
eaclSDK.NewValidator(),
ls),
acl.NewAPEChecker(c.log, c.cfgObject.cfgAccessPolicyEngine.accessPolicyEngine.chainRouter),
c.cfgObject.cnrSource,
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 {
w *cntClient.Client
}

2
go.mod
View file

@ -8,7 +8,7 @@ require (
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/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
github.com/cheggaaa/pb v1.0.29
github.com/chzyer/readline v1.5.1

BIN
go.sum

Binary file not shown.

View file

@ -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
}

View file

@ -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
}

View file

@ -22,6 +22,9 @@ type RequestInfo struct {
operation acl.Op // put, get, head, etc.
cnrOwner user.ID // container owner
// cnrNamespace defined to which namespace a container is belonged.
cnrNamespace string
idCnr cid.ID
// optional for some request
@ -57,6 +60,10 @@ func (r RequestInfo) ContainerOwner() user.ID {
return r.cnrOwner
}
func (r RequestInfo) ContainerNamespace() string {
return r.cnrNamespace
}
// ObjectID return object ID.
func (r RequestInfo) ObjectID() *oid.ID {
return r.obj

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
containerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
@ -67,10 +68,6 @@ type cfg struct {
checker ACLChecker
// TODO(aarifullin): apeCheck is temporarily the part of
// acl service and must be standalone.
apeChecker APEChainChecker
irFetcher InnerRingFetcher
nm netmap.Source
@ -83,7 +80,6 @@ func New(next object.ServiceServer,
nm netmap.Source,
irf InnerRingFetcher,
acl ACLChecker,
apeChecker APEChainChecker,
cs container.Source,
opts ...Option,
) Service {
@ -93,7 +89,6 @@ func New(next object.ServiceServer,
nm: nm,
irFetcher: irf,
checker: acl,
apeChecker: apeChecker,
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

stream has context.

`stream` has context.
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
// next Get method in the ServiceServer pipeline.
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{
GetObjectStream: stream,
GetObjectStream: newWrappedGetObjectStreamStream(stream, reqInfo),
info: reqInfo,
checker: b.checker,
})
@ -224,7 +288,7 @@ func (b Service) Head(
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 = b.checker.CheckEACL(resp, reqInfo); err != nil {
err = eACLErr(reqInfo, err)
@ -277,7 +341,7 @@ func (b Service) Search(request *objectV2.SearchRequest, stream object.SearchStr
return b.next.Search(request, &searchStreamBasicChecker{
checker: b.checker,
SearchStream: stream,
SearchStream: newWrappedSearchStream(stream, reqInfo),
info: reqInfo,
})
}
@ -333,7 +397,7 @@ func (b Service) Delete(
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 {
@ -386,11 +450,18 @@ func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetOb
return b.next.GetRange(request, &rangeStreamBasicChecker{
checker: b.checker,
GetObjectRangeStream: stream,
GetObjectRangeStream: newWrappedRangeStream(stream, 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(
ctx context.Context,
request *objectV2.GetRangeHashRequest,
@ -442,7 +513,7 @@ func (b Service) GetRangeHash(
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) {
@ -501,7 +572,7 @@ func (b Service) PutSingle(ctx context.Context, request *objectV2.PutSingleReque
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 {
@ -566,9 +637,11 @@ func (p putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRe
reqInfo.obj = obj
if err := p.source.apeChecker.CheckIfRequestPermitted(reqInfo); err != nil {
return err
if !p.source.checker.CheckBasicACL(reqInfo) || !p.source.checker.StickyBitCheck(reqInfo, idOwner) {
return basicACLErr(reqInfo)
}
ctx = requestContext(ctx, reqInfo)
}
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.cnrOwner = cnr.Value.Owner()
info.idCnr = idCnr
info.cnrNamespace = cnr.Value.Attribute(containerV2.SysAttributeZone)
// it is assumed that at the moment the key will be valid,
// otherwise the request would not pass validation

View file

@ -26,9 +26,3 @@ type InnerRingFetcher interface {
// the actual inner ring.
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
}

View 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
}

View 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)
}
})
}
})
}
}

View 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

Is it ok to skip getHeader error?

Is it ok to skip getHeader error?

Yes. If err != nil, then it means the object is not found locally (check this out).
Do you suggest to check it for NotFoundError?

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
}

View 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))
})
}
}

View 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

Maybe just no key %s in context?

Maybe just `no key %s in context`?

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

Looks like redundant check.

Looks like redundant check.

I think you are right. I will remove this check

I think you are right. I will remove this check

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

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?

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?

Even if we reject the whole delete request, we perform physical deletion, isn't it?

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

Fixed

Fixed

Just for my information, in this case when we will get attributes of the object to do some checks via ape?

Just for my information, in this case when we will get attributes of the object to do some checks via `ape`?

we will get attributes

#888/files

objectProperties method

> 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

Why we need to perform this check twice?

Why we need to perform this check twice?

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 and user 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)

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_)

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

Don't understand: line 356 is APE check.

Don't understand: line 356 is APE check.

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
}

View 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
}

View file

@ -0,0 +1,16 @@
package object
import "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
type RequestContextKeyT struct{}

It is usually better to use RequestContextKey struct{} and empty value as a key: this is the approach used in golang net stdlib.

It is usually better to use `RequestContextKey struct{}` and empty value as a key: this is the approach used in golang `net` stdlib.

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
}