From c8baf76fae5e0bedd49d66fcd8fe003996766f49 Mon Sep 17 00:00:00 2001 From: Airat Arifullin Date: Wed, 27 Dec 2023 17:18:15 +0300 Subject: [PATCH] [#872] object: Introduce APE middlewar for object service Signed-off-by: Airat Arifullin --- cmd/frostfs-node/object.go | 23 +- pkg/services/object/acl/ape.go | 49 --- pkg/services/object/acl/ape_request.go | 100 ------ pkg/services/object/acl/v2/request.go | 7 + pkg/services/object/acl/v2/service.go | 104 +++++- pkg/services/object/acl/v2/types.go | 6 - pkg/services/object/ape/checker.go | 82 +++++ pkg/services/object/ape/checker_test.go | 348 ++++++++++++++++++++ pkg/services/object/ape/request.go | 159 ++++++++++ pkg/services/object/ape/request_test.go | 324 +++++++++++++++++++ pkg/services/object/ape/service.go | 404 ++++++++++++++++++++++++ pkg/services/object/ape/types.go | 9 + pkg/services/object/request_context.go | 16 + 13 files changed, 1456 insertions(+), 175 deletions(-) delete mode 100644 pkg/services/object/acl/ape.go delete mode 100644 pkg/services/object/acl/ape_request.go create mode 100644 pkg/services/object/ape/checker.go create mode 100644 pkg/services/object/ape/checker_test.go create mode 100644 pkg/services/object/ape/request.go create mode 100644 pkg/services/object/ape/request_test.go create mode 100644 pkg/services/object/ape/service.go create mode 100644 pkg/services/object/ape/types.go create mode 100644 pkg/services/object/request_context.go diff --git a/cmd/frostfs-node/object.go b/cmd/frostfs-node/object.go index 2d18f82a3..5c82f0dcc 100644 --- a/cmd/frostfs-node/object.go +++ b/cmd/frostfs-node/object.go @@ -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 | | signature | response | acl | split + // grpc | | 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) @@ -419,11 +422,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( @@ -431,12 +434,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 } diff --git a/pkg/services/object/acl/ape.go b/pkg/services/object/acl/ape.go deleted file mode 100644 index a431ad252..000000000 --- a/pkg/services/object/acl/ape.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/object/acl/ape_request.go b/pkg/services/object/acl/ape_request.go deleted file mode 100644 index accbdce5f..000000000 --- a/pkg/services/object/acl/ape_request.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/object/acl/v2/request.go b/pkg/services/object/acl/v2/request.go index 675768969..c58c00e0e 100644 --- a/pkg/services/object/acl/v2/request.go +++ b/pkg/services/object/acl/v2/request.go @@ -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 diff --git a/pkg/services/object/acl/v2/service.go b/pkg/services/object/acl/v2/service.go index ee167d0e4..ed077411c 100644 --- a/pkg/services/object/acl/v2/service.go +++ b/pkg/services/object/acl/v2/service.go @@ -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 + + 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 diff --git a/pkg/services/object/acl/v2/types.go b/pkg/services/object/acl/v2/types.go index a113c4693..061cd26b6 100644 --- a/pkg/services/object/acl/v2/types.go +++ b/pkg/services/object/acl/v2/types.go @@ -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 -} diff --git a/pkg/services/object/ape/checker.go b/pkg/services/object/ape/checker.go new file mode 100644 index 000000000..fdaa9df85 --- /dev/null +++ b/pkg/services/object/ape/checker.go @@ -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 +} diff --git a/pkg/services/object/ape/checker_test.go b/pkg/services/object/ape/checker_test.go new file mode 100644 index 000000000..9d548480b --- /dev/null +++ b/pkg/services/object/ape/checker_test.go @@ -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) + } + }) + } + }) + } +} diff --git a/pkg/services/object/ape/request.go b/pkg/services/object/ape/request.go new file mode 100644 index 000000000..caf52645c --- /dev/null +++ b/pkg/services/object/ape/request.go @@ -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 +} diff --git a/pkg/services/object/ape/request_test.go b/pkg/services/object/ape/request_test.go new file mode 100644 index 000000000..55aa691b4 --- /dev/null +++ b/pkg/services/object/ape/request_test.go @@ -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)) + }) + } +} diff --git a/pkg/services/object/ape/service.go b/pkg/services/object/ape/service.go new file mode 100644 index 000000000..df4dcc306 --- /dev/null +++ b/pkg/services/object/ape/service.go @@ -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 +} diff --git a/pkg/services/object/ape/types.go b/pkg/services/object/ape/types.go new file mode 100644 index 000000000..46e55360d --- /dev/null +++ b/pkg/services/object/ape/types.go @@ -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 +} diff --git a/pkg/services/object/request_context.go b/pkg/services/object/request_context.go new file mode 100644 index 000000000..4b9aa04d1 --- /dev/null +++ b/pkg/services/object/request_context.go @@ -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 +}