diff --git a/pkg/services/object/acl/v2/service.go b/pkg/services/object/acl/v2/service.go index a9ddad7ca..5a8e8b065 100644 --- a/pkg/services/object/acl/v2/service.go +++ b/pkg/services/object/acl/v2/service.go @@ -35,6 +35,12 @@ type putStreamBasicChecker struct { next object.PutObjectStream } +type patchStreamBasicChecker struct { + source *Service + next object.PatchObjectStream + nonFirstSend bool +} + type getStreamBasicChecker struct { checker ACLChecker @@ -250,7 +256,12 @@ func (b Service) Put() (object.PutObjectStream, error) { } func (b Service) Patch() (object.PatchObjectStream, error) { - return b.next.Patch() + streamer, err := b.next.Patch() + + return &patchStreamBasicChecker{ + source: &b, + next: streamer, + }, err } func (b Service) Head( @@ -738,6 +749,65 @@ func (g *searchStreamBasicChecker) Send(resp *objectV2.SearchResponse) error { return g.SearchStream.Send(resp) } +func (p *patchStreamBasicChecker) Send(ctx context.Context, request *objectV2.PatchRequest) error { + body := request.GetBody() + if body == nil { + return errEmptyBody + } + + if !p.nonFirstSend { + p.nonFirstSend = true + + cnr, err := getContainerIDFromRequest(request) + if err != nil { + return err + } + + objV2 := request.GetBody().GetAddress().GetObjectID() + if objV2 == nil { + return errors.New("missing oid") + } + obj := new(oid.ID) + err = obj.ReadFromV2(*objV2) + if err != nil { + return err + } + + var sTok *sessionSDK.Object + sTok, err = readSessionToken(cnr, obj, request.GetMetaHeader().GetSessionToken()) + if err != nil { + return err + } + + bTok, err := originalBearerToken(request.GetMetaHeader()) + if err != nil { + return err + } + + req := MetaWithToken{ + vheader: request.GetVerificationHeader(), + token: sTok, + bearer: bTok, + src: request, + } + + reqInfo, err := p.source.findRequestInfoWithoutACLOperationAssert(req, cnr) + if err != nil { + return err + } + + reqInfo.obj = obj + + ctx = requestContext(ctx, reqInfo) + } + + return p.next.Send(ctx, request) +} + +func (p patchStreamBasicChecker) CloseAndRecv(ctx context.Context) (*objectV2.PatchResponse, error) { + return p.next.CloseAndRecv(ctx) +} + func (b Service) findRequestInfo(req MetaWithToken, idCnr cid.ID, op acl.Op) (info RequestInfo, err error) { cnr, err := b.containers.Get(idCnr) // fetch actual container if err != nil { @@ -794,3 +864,56 @@ func (b Service) findRequestInfo(req MetaWithToken, idCnr cid.ID, op acl.Op) (in return info, nil } + +// findRequestInfoWithoutACLOperationAssert is findRequestInfo without session token verb assert. +func (b Service) findRequestInfoWithoutACLOperationAssert(req MetaWithToken, idCnr cid.ID) (info RequestInfo, err error) { + cnr, err := b.containers.Get(idCnr) // fetch actual container + if err != nil { + return info, err + } + + if req.token != nil { + currentEpoch, err := b.nm.Epoch() + if err != nil { + return info, errors.New("can't fetch current epoch") + } + if req.token.ExpiredAt(currentEpoch) { + return info, new(apistatus.SessionTokenExpired) + } + if req.token.InvalidAt(currentEpoch) { + return info, fmt.Errorf("%s: token is invalid at %d epoch)", + invalidRequestMessage, currentEpoch) + } + } + + // find request role and key + ownerID, ownerKey, err := req.RequestOwner() + if err != nil { + return info, err + } + res, err := b.c.Classify(ownerID, ownerKey, idCnr, cnr.Value) + if err != nil { + return info, err + } + + info.basicACL = cnr.Value.BasicACL() + info.requestRole = res.Role + info.cnrOwner = cnr.Value.Owner() + info.idCnr = idCnr + + cnrNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cnr.Value).Zone(), ".ns") + if hasNamespace { + info.cnrNamespace = cnrNamespace + } + + // it is assumed that at the moment the key will be valid, + // otherwise the request would not pass validation + info.senderKey = res.Key + + // add bearer token if it is present in request + info.bearer = req.bearer + + info.srcRequest = req.src + + return info, nil +} diff --git a/pkg/services/object/acl/v2/util.go b/pkg/services/object/acl/v2/util.go index 76fd9651d..c5225e8c4 100644 --- a/pkg/services/object/acl/v2/util.go +++ b/pkg/services/object/acl/v2/util.go @@ -46,6 +46,8 @@ func getContainerIDFromRequest(req any) (cid.ID, error) { idV2 = v.GetBody().GetAddress().GetContainerID() case *objectV2.PutSingleRequest: idV2 = v.GetBody().GetObject().GetHeader().GetContainerID() + case *objectV2.PatchRequest: + idV2 = v.GetBody().GetAddress().GetContainerID() default: return cid.ID{}, errors.New("unknown request type") } diff --git a/pkg/services/object/ape/checker_test.go b/pkg/services/object/ape/checker_test.go index afe19fc51..090f6a83c 100644 --- a/pkg/services/object/ape/checker_test.go +++ b/pkg/services/object/ape/checker_test.go @@ -518,22 +518,7 @@ func TestAPECheck_BearerTokenOverrides(t *testing.T) { ls := inmemory.NewInmemoryLocalStorage() ms := inmemory.NewInmemoryMorphRuleChainStorage() - node1Key, err := keys.NewPrivateKey() - require.NoError(t, err) - node1 := netmapSDK.NodeInfo{} - node1.SetPublicKey(node1Key.PublicKey().Bytes()) - netmap := &netmapSDK.NetMap{} - netmap.SetEpoch(100) - netmap.SetNodes([]netmapSDK.NodeInfo{node1}) - - nm := &netmapStub{ - currentEpoch: 100, - netmaps: map[uint64]*netmapSDK.NetMap{ - 100: netmap, - }, - } - - checker := NewChecker(ls, ms, headerProvider, frostfsidProvider, nm, &stMock{}, nil, nil) + checker := NewChecker(ls, ms, headerProvider, frostfsidProvider, nil, &stMock{}, nil, nil) prm := Prm{ Method: method, @@ -556,7 +541,7 @@ func TestAPECheck_BearerTokenOverrides(t *testing.T) { } } - err = checker.CheckAPE(context.Background(), prm) + err := checker.CheckAPE(context.Background(), prm) if test.expectAPEErr { require.Error(t, err) } else { diff --git a/pkg/services/object/ape/request.go b/pkg/services/object/ape/request.go index 1c129f65f..da5307ca7 100644 --- a/pkg/services/object/ape/request.go +++ b/pkg/services/object/ape/request.go @@ -103,7 +103,8 @@ func (c *checkerImpl) newAPERequest(ctx context.Context, prm Prm) (aperequest.Re nativeschema.MethodHeadObject, nativeschema.MethodRangeObject, nativeschema.MethodHashObject, - nativeschema.MethodDeleteObject: + nativeschema.MethodDeleteObject, + nativeschema.MethodPatchObject: if prm.Object == nil { return defaultRequest, fmt.Errorf("method %s: %w", prm.Method, errMissingOID) } diff --git a/pkg/services/object/ape/service.go b/pkg/services/object/ape/service.go index 64dd19c24..a1634e7c5 100644 --- a/pkg/services/object/ape/service.go +++ b/pkg/services/object/ape/service.go @@ -204,8 +204,60 @@ func (c *Service) Put() (objectSvc.PutObjectStream, error) { }, err } +type patchStreamBasicChecker struct { + apeChecker Checker + + next objectSvc.PatchObjectStream + + nonFirstSend bool +} + +func (p *patchStreamBasicChecker) Send(ctx context.Context, request *objectV2.PatchRequest) error { + if !p.nonFirstSend { + p.nonFirstSend = true + + reqCtx, err := requestContext(ctx) + if err != nil { + return toStatusErr(err) + } + + cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID()) + if err != nil { + return toStatusErr(err) + } + + prm := Prm{ + Namespace: reqCtx.Namespace, + Container: cnrID, + Object: objID, + Method: nativeschema.MethodPatchObject, + SenderKey: hex.EncodeToString(reqCtx.SenderKey), + ContainerOwner: reqCtx.ContainerOwner, + Role: nativeSchemaRole(reqCtx.Role), + SoftAPECheck: reqCtx.SoftAPECheck, + BearerToken: reqCtx.BearerToken, + XHeaders: request.GetMetaHeader().GetXHeaders(), + } + + if err := p.apeChecker.CheckAPE(ctx, prm); err != nil { + return toStatusErr(err) + } + } + + return p.next.Send(ctx, request) +} + +func (p patchStreamBasicChecker) CloseAndRecv(ctx context.Context) (*objectV2.PatchResponse, error) { + return p.next.CloseAndRecv(ctx) +} + func (c *Service) Patch() (objectSvc.PatchObjectStream, error) { - return c.next.Patch() + streamer, err := c.next.Patch() + + return &patchStreamBasicChecker{ + apeChecker: c.apeChecker, + next: streamer, + }, err } func (c *Service) Head(ctx context.Context, request *objectV2.HeadRequest) (*objectV2.HeadResponse, error) {