package v2 import ( "context" "errors" "fmt" "strings" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap" objectCore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/object" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger" objectV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/session" apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" cnrSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" sessionSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "go.uber.org/zap" ) // Service checks basic ACL rules. type Service struct { *cfg c objectCore.SenderClassifier } type putStreamBasicChecker struct { source *Service next object.PutObjectStream } type patchStreamBasicChecker struct { source *Service next object.PatchObjectStream nonFirstSend bool } type getStreamBasicChecker struct { checker ACLChecker object.GetObjectStream info RequestInfo } type rangeStreamBasicChecker struct { checker ACLChecker object.GetObjectRangeStream info RequestInfo } type searchStreamBasicChecker struct { checker ACLChecker object.SearchStream info RequestInfo } // Option represents Service constructor option. type Option func(*cfg) type cfg struct { log *logger.Logger containers container.Source checker ACLChecker irFetcher InnerRingFetcher nm netmap.Source next object.ServiceServer } // New is a constructor for object ACL checking service. func New(next object.ServiceServer, nm netmap.Source, irf InnerRingFetcher, acl ACLChecker, cs container.Source, opts ...Option, ) Service { cfg := &cfg{ log: &logger.Logger{Logger: zap.L()}, next: next, nm: nm, irFetcher: irf, checker: acl, containers: cs, } for i := range opts { opts[i](cfg) } return Service{ cfg: cfg, c: objectCore.NewSenderClassifier(cfg.irFetcher, cfg.nm, cfg.log), } } // 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(), ContainerOwner: w.requestInfo.ContainerOwner(), SenderKey: w.requestInfo.SenderKey(), Role: w.requestInfo.RequestRole(), SoftAPECheck: w.requestInfo.IsSoftAPECheck(), BearerToken: w.requestInfo.Bearer(), }) } 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(), ContainerOwner: w.requestInfo.ContainerOwner(), SenderKey: w.requestInfo.SenderKey(), Role: w.requestInfo.RequestRole(), SoftAPECheck: w.requestInfo.IsSoftAPECheck(), BearerToken: w.requestInfo.Bearer(), }) } 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(), ContainerOwner: w.requestInfo.ContainerOwner(), SenderKey: w.requestInfo.SenderKey(), Role: w.requestInfo.RequestRole(), SoftAPECheck: w.requestInfo.IsSoftAPECheck(), BearerToken: w.requestInfo.Bearer(), }) } 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 { cnr, err := getContainerIDFromRequest(request) if err != nil { return err } obj, err := getObjectIDFromRequestBody(request.GetBody()) if err != nil { return err } sTok, err := originalSessionToken(request.GetMetaHeader()) if err != nil { return err } if sTok != nil { err = assertSessionRelation(*sTok, cnr, obj) 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 := b.findRequestInfo(req, cnr, acl.OpObjectGet) if err != nil { return err } reqInfo.obj = obj if reqInfo.IsSoftAPECheck() { if !b.checker.CheckBasicACL(reqInfo) { return basicACLErr(reqInfo) } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { return eACLErr(reqInfo, err) } } return b.next.Get(request, &getStreamBasicChecker{ GetObjectStream: newWrappedGetObjectStreamStream(stream, reqInfo), info: reqInfo, checker: b.checker, }) } func (b Service) Put() (object.PutObjectStream, error) { streamer, err := b.next.Put() return putStreamBasicChecker{ source: &b, next: streamer, }, err } func (b Service) Patch() (object.PatchObjectStream, error) { streamer, err := b.next.Patch() return &patchStreamBasicChecker{ source: &b, next: streamer, }, err } func (b Service) Head( ctx context.Context, request *objectV2.HeadRequest, ) (*objectV2.HeadResponse, error) { cnr, err := getContainerIDFromRequest(request) if err != nil { return nil, err } obj, err := getObjectIDFromRequestBody(request.GetBody()) if err != nil { return nil, err } sTok, err := originalSessionToken(request.GetMetaHeader()) if err != nil { return nil, err } if sTok != nil { err = assertSessionRelation(*sTok, cnr, obj) if err != nil { return nil, err } } bTok, err := originalBearerToken(request.GetMetaHeader()) if err != nil { return nil, err } req := MetaWithToken{ vheader: request.GetVerificationHeader(), token: sTok, bearer: bTok, src: request, } reqInfo, err := b.findRequestInfo(req, cnr, acl.OpObjectHead) if err != nil { return nil, err } reqInfo.obj = obj if reqInfo.IsSoftAPECheck() { if !b.checker.CheckBasicACL(reqInfo) { return nil, basicACLErr(reqInfo) } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { return nil, eACLErr(reqInfo, err) } } 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) } } return resp, err } func (b Service) Search(request *objectV2.SearchRequest, stream object.SearchStream) error { id, err := getContainerIDFromRequest(request) if err != nil { return err } sTok, err := originalSessionToken(request.GetMetaHeader()) if err != nil { return err } if sTok != nil { err = assertSessionRelation(*sTok, id, nil) 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 := b.findRequestInfo(req, id, acl.OpObjectSearch) if err != nil { return err } if reqInfo.IsSoftAPECheck() { if !b.checker.CheckBasicACL(reqInfo) { return basicACLErr(reqInfo) } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { return eACLErr(reqInfo, err) } } return b.next.Search(request, &searchStreamBasicChecker{ checker: b.checker, SearchStream: newWrappedSearchStream(stream, reqInfo), info: reqInfo, }) } func (b Service) Delete( ctx context.Context, request *objectV2.DeleteRequest, ) (*objectV2.DeleteResponse, error) { cnr, err := getContainerIDFromRequest(request) if err != nil { return nil, err } obj, err := getObjectIDFromRequestBody(request.GetBody()) if err != nil { return nil, err } sTok, err := originalSessionToken(request.GetMetaHeader()) if err != nil { return nil, err } if sTok != nil { err = assertSessionRelation(*sTok, cnr, obj) if err != nil { return nil, err } } bTok, err := originalBearerToken(request.GetMetaHeader()) if err != nil { return nil, err } req := MetaWithToken{ vheader: request.GetVerificationHeader(), token: sTok, bearer: bTok, src: request, } reqInfo, err := b.findRequestInfo(req, cnr, acl.OpObjectDelete) if err != nil { return nil, err } reqInfo.obj = obj if reqInfo.IsSoftAPECheck() { if !b.checker.CheckBasicACL(reqInfo) { return nil, basicACLErr(reqInfo) } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { return nil, eACLErr(reqInfo, err) } } return b.next.Delete(requestContext(ctx, reqInfo), request) } func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetObjectRangeStream) error { cnr, err := getContainerIDFromRequest(request) if err != nil { return err } obj, err := getObjectIDFromRequestBody(request.GetBody()) if err != nil { return err } sTok, err := originalSessionToken(request.GetMetaHeader()) if err != nil { return err } if sTok != nil { err = assertSessionRelation(*sTok, cnr, obj) 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 := b.findRequestInfo(req, cnr, acl.OpObjectRange) if err != nil { return err } reqInfo.obj = obj if reqInfo.IsSoftAPECheck() { if !b.checker.CheckBasicACL(reqInfo) { return basicACLErr(reqInfo) } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { return eACLErr(reqInfo, err) } } return b.next.GetRange(request, &rangeStreamBasicChecker{ checker: b.checker, GetObjectRangeStream: newWrappedRangeStream(stream, reqInfo), info: reqInfo, }) } func requestContext(ctx context.Context, reqInfo RequestInfo) context.Context { return context.WithValue(ctx, object.RequestContextKey, &object.RequestContext{ Namespace: reqInfo.ContainerNamespace(), ContainerOwner: reqInfo.ContainerOwner(), SenderKey: reqInfo.SenderKey(), Role: reqInfo.RequestRole(), SoftAPECheck: reqInfo.IsSoftAPECheck(), BearerToken: reqInfo.Bearer(), }) } func (b Service) GetRangeHash( ctx context.Context, request *objectV2.GetRangeHashRequest, ) (*objectV2.GetRangeHashResponse, error) { cnr, err := getContainerIDFromRequest(request) if err != nil { return nil, err } obj, err := getObjectIDFromRequestBody(request.GetBody()) if err != nil { return nil, err } sTok, err := originalSessionToken(request.GetMetaHeader()) if err != nil { return nil, err } if sTok != nil { err = assertSessionRelation(*sTok, cnr, obj) if err != nil { return nil, err } } bTok, err := originalBearerToken(request.GetMetaHeader()) if err != nil { return nil, err } req := MetaWithToken{ vheader: request.GetVerificationHeader(), token: sTok, bearer: bTok, src: request, } reqInfo, err := b.findRequestInfo(req, cnr, acl.OpObjectHash) if err != nil { return nil, err } reqInfo.obj = obj if reqInfo.IsSoftAPECheck() { if !b.checker.CheckBasicACL(reqInfo) { return nil, basicACLErr(reqInfo) } else if err := b.checker.CheckEACL(request, reqInfo); err != nil { return nil, eACLErr(reqInfo, err) } } return b.next.GetRangeHash(requestContext(ctx, reqInfo), request) } func (b Service) PutSingle(ctx context.Context, request *objectV2.PutSingleRequest) (*objectV2.PutSingleResponse, error) { cnr, err := getContainerIDFromRequest(request) if err != nil { return nil, err } idV2 := request.GetBody().GetObject().GetHeader().GetOwnerID() if idV2 == nil { return nil, errors.New("missing object owner") } var idOwner user.ID err = idOwner.ReadFromV2(*idV2) if err != nil { return nil, fmt.Errorf("invalid object owner: %w", err) } obj, err := getObjectIDFromRefObjectID(request.GetBody().GetObject().GetObjectID()) if err != nil { return nil, err } var sTok *sessionSDK.Object sTok, err = readSessionToken(cnr, obj, request.GetMetaHeader().GetSessionToken()) if err != nil { return nil, err } bTok, err := originalBearerToken(request.GetMetaHeader()) if err != nil { return nil, err } req := MetaWithToken{ vheader: request.GetVerificationHeader(), token: sTok, bearer: bTok, src: request, } reqInfo, err := b.findRequestInfo(req, cnr, acl.OpObjectPut) if err != nil { return nil, err } reqInfo.obj = obj if reqInfo.IsSoftAPECheck() { if !b.checker.CheckBasicACL(reqInfo) || !b.checker.StickyBitCheck(reqInfo, idOwner) { return nil, basicACLErr(reqInfo) } if err := b.checker.CheckEACL(request, reqInfo); err != nil { return nil, eACLErr(reqInfo, err) } } return b.next.PutSingle(requestContext(ctx, reqInfo), request) } func (p putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRequest) error { body := request.GetBody() if body == nil { return errEmptyBody } part := body.GetObjectPart() if part, ok := part.(*objectV2.PutObjectPartInit); ok { cnr, err := getContainerIDFromRequest(request) if err != nil { return err } idV2 := part.GetHeader().GetOwnerID() if idV2 == nil { return errors.New("missing object owner") } var idOwner user.ID err = idOwner.ReadFromV2(*idV2) if err != nil { return fmt.Errorf("invalid object owner: %w", err) } objV2 := part.GetObjectID() var obj *oid.ID if objV2 != nil { 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.findRequestInfo(req, cnr, acl.OpObjectPut) if err != nil { return err } reqInfo.obj = obj if reqInfo.IsSoftAPECheck() { 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) } func readSessionToken(cnr cid.ID, obj *oid.ID, tokV2 *session.Token) (*sessionSDK.Object, error) { var sTok *sessionSDK.Object if tokV2 != nil { sTok = new(sessionSDK.Object) err := sTok.ReadFromV2(*tokV2) if err != nil { return nil, fmt.Errorf("invalid session token: %w", err) } if sTok.AssertVerb(sessionSDK.VerbObjectDelete) { // if session relates to object's removal, we don't check // relation of the tombstone to the session here since user // can't predict tomb's ID. err = assertSessionRelation(*sTok, cnr, nil) } else { err = assertSessionRelation(*sTok, cnr, obj) } if err != nil { return nil, err } } return sTok, nil } func (p putStreamBasicChecker) CloseAndRecv(ctx context.Context) (*objectV2.PutResponse, error) { return p.next.CloseAndRecv(ctx) } func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error { if _, ok := resp.GetBody().GetObjectPart().(*objectV2.GetObjectPartInit); ok { if err := g.checker.CheckEACL(resp, g.info); err != nil { return eACLErr(g.info, err) } } return g.GetObjectStream.Send(resp) } func (g *rangeStreamBasicChecker) Send(resp *objectV2.GetRangeResponse) error { if err := g.checker.CheckEACL(resp, g.info); err != nil { return eACLErr(g.info, err) } return g.GetObjectRangeStream.Send(resp) } func (g *searchStreamBasicChecker) Send(resp *objectV2.SearchResponse) error { if err := g.checker.CheckEACL(resp, g.info); err != nil { return eACLErr(g.info, err) } 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 { 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) } if !assertVerb(*req.token, op) { return info, errInvalidVerb } } // 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.operation = op 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 } // 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 }