[#1052] object: Make ape middleware form request info

* Move some helpers from `acl/v2` package to `ape`. Also move errors;
* Introduce `Metadata`, `RequestInfo` types;
* Introduce `RequestInfoExtractor` interface and its implementation.
  The extractor's purpose is to extract request info based on request
  metadata. It also validates session token;
* Refactor ape service - each handler forms request info and pass
  necessary fields to checker.

Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
This commit is contained in:
Airat Arifullin 2025-03-19 13:57:44 +03:00 committed by Evgenii Stratonikov
parent eed0824590
commit 73e35bc885
8 changed files with 759 additions and 193 deletions

View file

@ -172,7 +172,7 @@ func initObjectService(c *cfg) {
splitSvc := createSplitService(c, sPutV2, sGetV2, sSearchV2, sDeleteV2, sPatch)
apeSvc := createAPEService(c, splitSvc)
apeSvc := createAPEService(c, &irFetcher, splitSvc)
aclSvc := createACLServiceV2(c, apeSvc, &irFetcher)
@ -439,7 +439,7 @@ func createACLServiceV2(c *cfg, apeSvc *objectAPE.Service, irFetcher *cachedIRFe
)
}
func createAPEService(c *cfg, splitSvc *objectService.TransportSplitter) *objectAPE.Service {
func createAPEService(c *cfg, irFetcher *cachedIRFetcher, splitSvc *objectService.TransportSplitter) *objectAPE.Service {
return objectAPE.NewService(
objectAPE.NewChecker(
c.cfgObject.cfgAccessPolicyEngine.accessPolicyEngine.LocalStorage(),
@ -451,6 +451,7 @@ func createAPEService(c *cfg, splitSvc *objectService.TransportSplitter) *object
c.cfgObject.cnrSource,
c.binPublicKey,
),
objectAPE.NewRequestInfoExtractor(c.log, c.cfgObject.cnrSource, irFetcher, c.netMapSource),
splitSvc,
)
}

View file

@ -7,6 +7,21 @@ import (
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
)
var (
errMissingContainerID = malformedRequestError("missing container ID")
errEmptyVerificationHeader = malformedRequestError("empty verification header")
errEmptyBodySig = malformedRequestError("empty at body signature")
errInvalidSessionSig = malformedRequestError("invalid session token signature")
errInvalidSessionOwner = malformedRequestError("invalid session token owner")
errInvalidVerb = malformedRequestError("session token verb is invalid")
)
func malformedRequestError(reason string) error {
invalidArgErr := &apistatus.InvalidArgument{}
invalidArgErr.SetMessage(reason)
return invalidArgErr
}
func toStatusErr(err error) error {
var chRouterErr *checkercore.ChainRouterError
if !errors.As(err, &chRouterErr) {

View file

@ -0,0 +1,172 @@
package ape
import (
"context"
"encoding/hex"
"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/util/logger"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/session"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
cnrSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
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"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)
type Metadata struct {
Container cid.ID
Object *oid.ID
MetaHeader *session.RequestMetaHeader
VerificationHeader *session.RequestVerificationHeader
SessionToken *sessionSDK.Object
BearerToken *bearer.Token
}
func (m Metadata) RequestOwner() (*user.ID, *keys.PublicKey, error) {
if m.VerificationHeader == nil {
return nil, nil, errEmptyVerificationHeader
}
if m.BearerToken != nil && m.BearerToken.Impersonate() {
return unmarshalPublicKeyWithOwner(m.BearerToken.SigningKeyBytes())
}
// if session token is presented, use it as truth source
if m.SessionToken != nil {
// verify signature of session token
return ownerFromToken(m.SessionToken)
}
// otherwise get original body signature
bodySignature := originalBodySignature(m.VerificationHeader)
if bodySignature == nil {
return nil, nil, errEmptyBodySig
}
return unmarshalPublicKeyWithOwner(bodySignature.GetKey())
}
// RequestInfo contains request information extracted by request metadata.
type RequestInfo struct {
// Role defines under which role this request is executed.
// It must be represented only as a constant represented in native schema.
Role string
ContainerOwner user.ID
// Namespace defines to which namespace a container is belonged.
Namespace string
// HEX-encoded sender key.
SenderKey string
}
type RequestInfoExtractor interface {
GetRequestInfo(context.Context, Metadata, string) (RequestInfo, error)
}
type extractor struct {
containers container.Source
nm netmap.Source
classifier objectCore.SenderClassifier
}
func NewRequestInfoExtractor(log *logger.Logger, containers container.Source, irFetcher InnerRingFetcher, nm netmap.Source) RequestInfoExtractor {
return &extractor{
containers: containers,
nm: nm,
classifier: objectCore.NewSenderClassifier(irFetcher, nm, log),
}
}
func (e *extractor) verifySessionToken(ctx context.Context, sessionToken *sessionSDK.Object, method string) error {
currentEpoch, err := e.nm.Epoch(ctx)
if err != nil {
return errors.New("can't fetch current epoch")
}
if sessionToken.ExpiredAt(currentEpoch) {
return new(apistatus.SessionTokenExpired)
}
if sessionToken.InvalidAt(currentEpoch) {
return fmt.Errorf("malformed request: token is invalid at %d epoch)", currentEpoch)
}
if !assertVerb(*sessionToken, method) {
return errInvalidVerb
}
return nil
}
func (e *extractor) GetRequestInfo(ctx context.Context, m Metadata, method string) (ri RequestInfo, err error) {
cnr, err := e.containers.Get(ctx, m.Container)
if err != nil {
return ri, err
}
if m.SessionToken != nil {
if err = e.verifySessionToken(ctx, m.SessionToken, method); err != nil {
return ri, err
}
}
ownerID, ownerKey, err := m.RequestOwner()
if err != nil {
return ri, err
}
res, err := e.classifier.Classify(ctx, ownerID, ownerKey, m.Container, cnr.Value)
if err != nil {
return ri, err
}
ri.Role = nativeSchemaRole(res.Role)
ri.ContainerOwner = cnr.Value.Owner()
cnrNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cnr.Value).Zone(), ".ns")
if hasNamespace {
ri.Namespace = cnrNamespace
}
// it is assumed that at the moment the key will be valid,
// otherwise the request would not pass validation
ri.SenderKey = hex.EncodeToString(res.Key)
return ri, nil
}
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
}

View file

@ -0,0 +1,164 @@
package ape
import (
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs"
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/session"
sigutilV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/util/signature"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
sessionSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require"
)
func TestRequestOwner(t *testing.T) {
containerOwner, err := keys.NewPrivateKey()
require.NoError(t, err)
userPk, err := keys.NewPrivateKey()
require.NoError(t, err)
var userID user.ID
user.IDFromKey(&userID, userPk.PrivateKey.PublicKey)
var userSignature refs.Signature
userSignature.SetKey(userPk.PublicKey().Bytes())
vh := new(sessionV2.RequestVerificationHeader)
vh.SetBodySignature(&userSignature)
t.Run("empty verification header", func(t *testing.T) {
req := Metadata{}
checkOwner(t, req, nil, errEmptyVerificationHeader)
})
t.Run("empty verification header signature", func(t *testing.T) {
req := Metadata{
VerificationHeader: new(sessionV2.RequestVerificationHeader),
}
checkOwner(t, req, nil, errEmptyBodySig)
})
t.Run("no tokens", func(t *testing.T) {
req := Metadata{
VerificationHeader: vh,
}
checkOwner(t, req, userPk.PublicKey(), nil)
})
t.Run("bearer without impersonate, no session", func(t *testing.T) {
req := Metadata{
VerificationHeader: vh,
BearerToken: newBearer(t, containerOwner, userID, false),
}
checkOwner(t, req, userPk.PublicKey(), nil)
})
t.Run("bearer with impersonate, no session", func(t *testing.T) {
req := Metadata{
VerificationHeader: vh,
BearerToken: newBearer(t, containerOwner, userID, true),
}
checkOwner(t, req, containerOwner.PublicKey(), nil)
})
t.Run("bearer with impersonate, with session", func(t *testing.T) {
// To check that bearer token takes priority, use different key to sign session token.
pk, err := keys.NewPrivateKey()
require.NoError(t, err)
req := Metadata{
VerificationHeader: vh,
BearerToken: newBearer(t, containerOwner, userID, true),
SessionToken: newSession(t, pk),
}
checkOwner(t, req, containerOwner.PublicKey(), nil)
})
t.Run("with session", func(t *testing.T) {
req := Metadata{
VerificationHeader: vh,
SessionToken: newSession(t, containerOwner),
}
checkOwner(t, req, containerOwner.PublicKey(), nil)
})
t.Run("malformed session token", func(t *testing.T) {
// This test is tricky: session token has issuer field and signature, which must correspond to each other.
// SDK prevents constructing such token in the first place, but it is still possible via API.
// Thus, construct v2 token, convert it to SDK one and pass to our function.
pk, err := keys.NewPrivateKey()
require.NoError(t, err)
var user1 user.ID
user.IDFromKey(&user1, pk.PrivateKey.PublicKey)
var id refs.OwnerID
id.SetValue(user1.WalletBytes())
raw, err := uuid.New().MarshalBinary()
require.NoError(t, err)
var cidV2 refs.ContainerID
cidtest.ID().WriteToV2(&cidV2)
sessionCtx := new(sessionV2.ObjectSessionContext)
sessionCtx.SetTarget(&cidV2)
var body sessionV2.TokenBody
body.SetOwnerID(&id)
body.SetID(raw)
body.SetLifetime(new(sessionV2.TokenLifetime))
body.SetSessionKey(pk.PublicKey().Bytes())
body.SetContext(sessionCtx)
var tokV2 sessionV2.Token
tokV2.SetBody(&body)
require.NoError(t, sigutilV2.SignData(&containerOwner.PrivateKey, smWrapper{Token: &tokV2}))
require.NoError(t, sigutilV2.VerifyData(smWrapper{Token: &tokV2}))
var tok sessionSDK.Object
require.NoError(t, tok.ReadFromV2(tokV2))
req := Metadata{
VerificationHeader: vh,
SessionToken: &tok,
}
checkOwner(t, req, nil, errInvalidSessionOwner)
})
}
type smWrapper struct {
*sessionV2.Token
}
func (s smWrapper) ReadSignedData(data []byte) ([]byte, error) {
return s.Token.GetBody().StableMarshal(data), nil
}
func (s smWrapper) SignedDataSize() int {
return s.Token.GetBody().StableSize()
}
func newSession(t *testing.T, pk *keys.PrivateKey) *sessionSDK.Object {
var tok sessionSDK.Object
require.NoError(t, tok.Sign(pk.PrivateKey))
return &tok
}
func newBearer(t *testing.T, pk *keys.PrivateKey, user user.ID, impersonate bool) *bearer.Token {
var tok bearer.Token
tok.SetImpersonate(impersonate)
tok.ForUser(user)
require.NoError(t, tok.Sign(pk.PrivateKey))
return &tok
}
func checkOwner(t *testing.T, req Metadata, expected *keys.PublicKey, expectedErr error) {
_, actual, err := req.RequestOwner()
if expectedErr != nil {
require.ErrorIs(t, err, expectedErr)
return
}
require.NoError(t, err)
require.Equal(t, expected, actual)
}

View file

@ -2,9 +2,6 @@ package ape
import (
"context"
"encoding/hex"
"errors"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/engine"
objectSvc "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object"
@ -12,19 +9,18 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/util"
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/session"
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"
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
)
var errFailedToCastToRequestContext = errors.New("failed cast to RequestContext")
type Service struct {
apeChecker Checker
extractor RequestInfoExtractor
next objectSvc.ServiceServer
}
@ -64,9 +60,10 @@ func NewStorageEngineHeaderProvider(e *engine.StorageEngine, s *getsvc.Service)
}
}
func NewService(apeChecker Checker, next objectSvc.ServiceServer) *Service {
func NewService(apeChecker Checker, extractor RequestInfoExtractor, next objectSvc.ServiceServer) *Service {
return &Service{
apeChecker: apeChecker,
extractor: extractor,
next: next,
}
}
@ -76,15 +73,9 @@ type getStreamBasicChecker struct {
apeChecker Checker
namespace string
metadata Metadata
senderKey []byte
containerOwner user.ID
role string
bearerToken *bearer.Token
reqInfo RequestInfo
}
func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error {
@ -95,15 +86,15 @@ func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error {
}
prm := Prm{
Namespace: g.namespace,
Namespace: g.reqInfo.Namespace,
Container: cnrID,
Object: objID,
Header: partInit.GetHeader(),
Method: nativeschema.MethodGetObject,
SenderKey: hex.EncodeToString(g.senderKey),
ContainerOwner: g.containerOwner,
Role: g.role,
BearerToken: g.bearerToken,
SenderKey: g.reqInfo.SenderKey,
ContainerOwner: g.reqInfo.ContainerOwner,
Role: g.reqInfo.Role,
BearerToken: g.metadata.BearerToken,
XHeaders: resp.GetMetaHeader().GetXHeaders(),
}
@ -114,69 +105,53 @@ func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error {
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, errFailedToCastToRequestContext
}
return rc, nil
}
func (c *Service) Get(request *objectV2.GetRequest, stream objectSvc.GetObjectStream) error {
reqCtx, err := requestContext(stream.Context())
md, err := newMetadata(request, request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
if err != nil {
return toStatusErr(err)
return err
}
reqInfo, err := c.extractor.GetRequestInfo(stream.Context(), md, nativeschema.MethodGetObject)
if err != nil {
return err
}
return c.next.Get(request, &getStreamBasicChecker{
GetObjectStream: stream,
apeChecker: c.apeChecker,
namespace: reqCtx.Namespace,
senderKey: reqCtx.SenderKey,
containerOwner: reqCtx.ContainerOwner,
role: nativeSchemaRole(reqCtx.Role),
bearerToken: reqCtx.BearerToken,
metadata: md,
reqInfo: reqInfo,
})
}
type putStreamBasicChecker struct {
apeChecker Checker
extractor RequestInfoExtractor
next objectSvc.PutObjectStream
}
func (p *putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRequest) error {
meta := request.GetMetaHeader()
for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() {
meta = origin
}
if partInit, ok := request.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit); ok {
reqCtx, err := requestContext(ctx)
md, err := newMetadata(request, partInit.GetHeader().GetContainerID(), partInit.GetObjectID())
if err != nil {
return toStatusErr(err)
return err
}
cnrID, objID, err := getAddressParamsSDK(partInit.GetHeader().GetContainerID(), partInit.GetObjectID())
reqInfo, err := p.extractor.GetRequestInfo(ctx, md, nativeschema.MethodPutObject)
if err != nil {
return toStatusErr(err)
return err
}
prm := Prm{
Namespace: reqCtx.Namespace,
Container: cnrID,
Object: objID,
Namespace: reqInfo.Namespace,
Container: md.Container,
Object: md.Object,
Header: partInit.GetHeader(),
Method: nativeschema.MethodPutObject,
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
ContainerOwner: reqCtx.ContainerOwner,
Role: nativeSchemaRole(reqCtx.Role),
BearerToken: reqCtx.BearerToken,
XHeaders: meta.GetXHeaders(),
SenderKey: reqInfo.SenderKey,
ContainerOwner: reqInfo.ContainerOwner,
Role: reqInfo.Role,
BearerToken: md.BearerToken,
XHeaders: md.MetaHeader.GetXHeaders(),
}
if err := p.apeChecker.CheckAPE(ctx, prm); err != nil {
@ -196,6 +171,7 @@ func (c *Service) Put(ctx context.Context) (objectSvc.PutObjectStream, error) {
return &putStreamBasicChecker{
apeChecker: c.apeChecker,
extractor: c.extractor,
next: streamer,
}, err
}
@ -203,40 +179,36 @@ func (c *Service) Put(ctx context.Context) (objectSvc.PutObjectStream, error) {
type patchStreamBasicChecker struct {
apeChecker Checker
extractor RequestInfoExtractor
next objectSvc.PatchObjectStream
nonFirstSend bool
}
func (p *patchStreamBasicChecker) Send(ctx context.Context, request *objectV2.PatchRequest) error {
meta := request.GetMetaHeader()
for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() {
meta = origin
}
if !p.nonFirstSend {
p.nonFirstSend = true
reqCtx, err := requestContext(ctx)
md, err := newMetadata(request, request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
if err != nil {
return toStatusErr(err)
return err
}
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
reqInfo, err := p.extractor.GetRequestInfo(ctx, md, nativeschema.MethodPatchObject)
if err != nil {
return toStatusErr(err)
return err
}
prm := Prm{
Namespace: reqCtx.Namespace,
Container: cnrID,
Object: objID,
Namespace: reqInfo.Namespace,
Container: md.Container,
Object: md.Object,
Method: nativeschema.MethodPatchObject,
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
ContainerOwner: reqCtx.ContainerOwner,
Role: nativeSchemaRole(reqCtx.Role),
BearerToken: reqCtx.BearerToken,
XHeaders: meta.GetXHeaders(),
SenderKey: reqInfo.SenderKey,
ContainerOwner: reqInfo.ContainerOwner,
Role: reqInfo.Role,
BearerToken: md.BearerToken,
XHeaders: md.MetaHeader.GetXHeaders(),
}
if err := p.apeChecker.CheckAPE(ctx, prm); err != nil {
@ -256,22 +228,17 @@ func (c *Service) Patch(ctx context.Context) (objectSvc.PatchObjectStream, error
return &patchStreamBasicChecker{
apeChecker: c.apeChecker,
extractor: c.extractor,
next: streamer,
}, err
}
func (c *Service) Head(ctx context.Context, request *objectV2.HeadRequest) (*objectV2.HeadResponse, error) {
meta := request.GetMetaHeader()
for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() {
meta = origin
}
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
md, err := newMetadata(request, request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
if err != nil {
return nil, err
}
reqCtx, err := requestContext(ctx)
reqInfo, err := c.extractor.GetRequestInfo(ctx, md, nativeschema.MethodHeadObject)
if err != nil {
return nil, err
}
@ -285,7 +252,7 @@ func (c *Service) Head(ctx context.Context, request *objectV2.HeadRequest) (*obj
switch headerPart := resp.GetBody().GetHeaderPart().(type) {
case *objectV2.ShortHeader:
cidV2 := new(refs.ContainerID)
cnrID.WriteToV2(cidV2)
md.Container.WriteToV2(cidV2)
header.SetContainerID(cidV2)
header.SetVersion(headerPart.GetVersion())
header.SetCreationEpoch(headerPart.GetCreationEpoch())
@ -301,16 +268,16 @@ func (c *Service) Head(ctx context.Context, request *objectV2.HeadRequest) (*obj
}
err = c.apeChecker.CheckAPE(ctx, Prm{
Namespace: reqCtx.Namespace,
Container: cnrID,
Object: objID,
Namespace: reqInfo.Namespace,
Container: md.Container,
Object: md.Object,
Header: header,
Method: nativeschema.MethodHeadObject,
Role: nativeSchemaRole(reqCtx.Role),
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
ContainerOwner: reqCtx.ContainerOwner,
BearerToken: reqCtx.BearerToken,
XHeaders: meta.GetXHeaders(),
Role: reqInfo.Role,
SenderKey: reqInfo.SenderKey,
ContainerOwner: reqInfo.ContainerOwner,
BearerToken: md.BearerToken,
XHeaders: md.MetaHeader.GetXHeaders(),
})
if err != nil {
return nil, toStatusErr(err)
@ -319,32 +286,24 @@ func (c *Service) Head(ctx context.Context, request *objectV2.HeadRequest) (*obj
}
func (c *Service) Search(request *objectV2.SearchRequest, stream objectSvc.SearchStream) error {
meta := request.GetMetaHeader()
for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() {
meta = origin
}
var cnrID cid.ID
if cnrV2 := request.GetBody().GetContainerID(); cnrV2 != nil {
if err := cnrID.ReadFromV2(*cnrV2); err != nil {
return toStatusErr(err)
}
}
reqCtx, err := requestContext(stream.Context())
md, err := newMetadata(request, request.GetBody().GetContainerID(), nil)
if err != nil {
return toStatusErr(err)
return err
}
reqInfo, err := c.extractor.GetRequestInfo(stream.Context(), md, nativeschema.MethodSearchObject)
if err != nil {
return err
}
err = c.apeChecker.CheckAPE(stream.Context(), Prm{
Namespace: reqCtx.Namespace,
Container: cnrID,
Namespace: reqInfo.Namespace,
Container: md.Container,
Method: nativeschema.MethodSearchObject,
Role: nativeSchemaRole(reqCtx.Role),
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
ContainerOwner: reqCtx.ContainerOwner,
BearerToken: reqCtx.BearerToken,
XHeaders: meta.GetXHeaders(),
Role: reqInfo.Role,
SenderKey: reqInfo.SenderKey,
ContainerOwner: reqInfo.ContainerOwner,
BearerToken: md.BearerToken,
XHeaders: md.MetaHeader.GetXHeaders(),
})
if err != nil {
return toStatusErr(err)
@ -354,31 +313,25 @@ func (c *Service) Search(request *objectV2.SearchRequest, stream objectSvc.Searc
}
func (c *Service) Delete(ctx context.Context, request *objectV2.DeleteRequest) (*objectV2.DeleteResponse, error) {
meta := request.GetMetaHeader()
for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() {
meta = origin
}
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
md, err := newMetadata(request, request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
if err != nil {
return nil, err
}
reqCtx, err := requestContext(ctx)
reqInfo, err := c.extractor.GetRequestInfo(ctx, md, nativeschema.MethodDeleteObject)
if err != nil {
return nil, err
}
err = c.apeChecker.CheckAPE(ctx, Prm{
Namespace: reqCtx.Namespace,
Container: cnrID,
Object: objID,
Namespace: reqInfo.Namespace,
Container: md.Container,
Object: md.Object,
Method: nativeschema.MethodDeleteObject,
Role: nativeSchemaRole(reqCtx.Role),
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
ContainerOwner: reqCtx.ContainerOwner,
BearerToken: reqCtx.BearerToken,
XHeaders: meta.GetXHeaders(),
Role: reqInfo.Role,
SenderKey: reqInfo.SenderKey,
ContainerOwner: reqInfo.ContainerOwner,
BearerToken: md.BearerToken,
XHeaders: md.MetaHeader.GetXHeaders(),
})
if err != nil {
return nil, toStatusErr(err)
@ -393,31 +346,25 @@ func (c *Service) Delete(ctx context.Context, request *objectV2.DeleteRequest) (
}
func (c *Service) GetRange(request *objectV2.GetRangeRequest, stream objectSvc.GetObjectRangeStream) error {
meta := request.GetMetaHeader()
for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() {
meta = origin
}
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
md, err := newMetadata(request, request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
if err != nil {
return toStatusErr(err)
return err
}
reqCtx, err := requestContext(stream.Context())
reqInfo, err := c.extractor.GetRequestInfo(stream.Context(), md, nativeschema.MethodRangeObject)
if err != nil {
return toStatusErr(err)
return err
}
err = c.apeChecker.CheckAPE(stream.Context(), Prm{
Namespace: reqCtx.Namespace,
Container: cnrID,
Object: objID,
Namespace: reqInfo.Namespace,
Container: md.Container,
Object: md.Object,
Method: nativeschema.MethodRangeObject,
Role: nativeSchemaRole(reqCtx.Role),
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
ContainerOwner: reqCtx.ContainerOwner,
BearerToken: reqCtx.BearerToken,
XHeaders: meta.GetXHeaders(),
Role: reqInfo.Role,
SenderKey: reqInfo.SenderKey,
ContainerOwner: reqInfo.ContainerOwner,
BearerToken: md.BearerToken,
XHeaders: md.MetaHeader.GetXHeaders(),
})
if err != nil {
return toStatusErr(err)
@ -427,31 +374,25 @@ func (c *Service) GetRange(request *objectV2.GetRangeRequest, stream objectSvc.G
}
func (c *Service) GetRangeHash(ctx context.Context, request *objectV2.GetRangeHashRequest) (*objectV2.GetRangeHashResponse, error) {
meta := request.GetMetaHeader()
for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() {
meta = origin
}
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
md, err := newMetadata(request, request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
if err != nil {
return nil, err
}
reqCtx, err := requestContext(ctx)
reqInfo, err := c.extractor.GetRequestInfo(ctx, md, nativeschema.MethodHashObject)
if err != nil {
return nil, err
}
prm := Prm{
Namespace: reqCtx.Namespace,
Container: cnrID,
Object: objID,
Namespace: reqInfo.Namespace,
Container: md.Container,
Object: md.Object,
Method: nativeschema.MethodHashObject,
Role: nativeSchemaRole(reqCtx.Role),
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
ContainerOwner: reqCtx.ContainerOwner,
BearerToken: reqCtx.BearerToken,
XHeaders: meta.GetXHeaders(),
Role: reqInfo.Role,
SenderKey: reqInfo.SenderKey,
ContainerOwner: reqInfo.ContainerOwner,
BearerToken: md.BearerToken,
XHeaders: md.MetaHeader.GetXHeaders(),
}
resp, err := c.next.GetRangeHash(ctx, request)
@ -466,32 +407,26 @@ func (c *Service) GetRangeHash(ctx context.Context, request *objectV2.GetRangeHa
}
func (c *Service) PutSingle(ctx context.Context, request *objectV2.PutSingleRequest) (*objectV2.PutSingleResponse, error) {
meta := request.GetMetaHeader()
for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() {
meta = origin
}
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetObject().GetHeader().GetContainerID(), request.GetBody().GetObject().GetObjectID())
md, err := newMetadata(request, request.GetBody().GetObject().GetHeader().GetContainerID(), request.GetBody().GetObject().GetObjectID())
if err != nil {
return nil, err
}
reqCtx, err := requestContext(ctx)
reqInfo, err := c.extractor.GetRequestInfo(ctx, md, nativeschema.MethodPutObject)
if err != nil {
return nil, err
}
prm := Prm{
Namespace: reqCtx.Namespace,
Container: cnrID,
Object: objID,
Namespace: reqInfo.Namespace,
Container: md.Container,
Object: md.Object,
Header: request.GetBody().GetObject().GetHeader(),
Method: nativeschema.MethodPutObject,
Role: nativeSchemaRole(reqCtx.Role),
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
ContainerOwner: reqCtx.ContainerOwner,
BearerToken: reqCtx.BearerToken,
XHeaders: meta.GetXHeaders(),
Role: reqInfo.Role,
SenderKey: reqInfo.SenderKey,
ContainerOwner: reqInfo.ContainerOwner,
BearerToken: md.BearerToken,
XHeaders: md.MetaHeader.GetXHeaders(),
}
if err = c.apeChecker.CheckAPE(ctx, prm); err != nil {
@ -501,18 +436,36 @@ func (c *Service) PutSingle(ctx context.Context, request *objectV2.PutSingleRequ
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
}
type request interface {
GetMetaHeader() *session.RequestMetaHeader
GetVerificationHeader() *session.RequestVerificationHeader
}
func newMetadata(request request, cnrV2 *refs.ContainerID, objV2 *refs.ObjectID) (md Metadata, err error) {
meta := request.GetMetaHeader()
for origin := meta.GetOrigin(); origin != nil; origin = meta.GetOrigin() {
meta = origin
}
if objV2 != nil {
objID = new(oid.ID)
if err = objID.ReadFromV2(*objV2); err != nil {
return
}
cnrID, objID, err := getAddressParamsSDK(cnrV2, objV2)
if err != nil {
return
}
session, err := readSessionToken(cnrID, objID, meta.GetSessionToken())
if err != nil {
return
}
bearer, err := originalBearerToken(request.GetMetaHeader())
if err != nil {
return
}
md = Metadata{
Container: cnrID,
Object: objID,
VerificationHeader: request.GetVerificationHeader(),
SessionToken: session,
BearerToken: bearer,
}
return
}

View file

@ -7,3 +7,11 @@ import "context"
type Checker interface {
CheckAPE(context.Context, Prm) error
}
// InnerRingFetcher is an interface that must provide
// Inner Ring information.
type InnerRingFetcher interface {
// InnerRingKeys must return list of public keys of
// the actual inner ring.
InnerRingKeys(ctx context.Context) ([][]byte, error)
}

View file

@ -0,0 +1,169 @@
package ape
import (
"crypto/ecdsa"
"crypto/elliptic"
"errors"
"fmt"
refsV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs"
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/session"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
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"
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)
func getAddressParamsSDK(cidV2 *refsV2.ContainerID, objV2 *refsV2.ObjectID) (cnrID cid.ID, objID *oid.ID, err error) {
if cidV2 != nil {
if err = cnrID.ReadFromV2(*cidV2); err != nil {
return
}
} else {
err = errMissingContainerID
return
}
if objV2 != nil {
objID = new(oid.ID)
if err = objID.ReadFromV2(*objV2); err != nil {
return
}
}
return
}
// originalBearerToken goes down to original request meta header and fetches
// bearer token from there.
func originalBearerToken(header *sessionV2.RequestMetaHeader) (*bearer.Token, error) {
for header.GetOrigin() != nil {
header = header.GetOrigin()
}
tokV2 := header.GetBearerToken()
if tokV2 == nil {
return nil, nil
}
var tok bearer.Token
return &tok, tok.ReadFromV2(*tokV2)
}
func ownerFromToken(token *sessionSDK.Object) (*user.ID, *keys.PublicKey, error) {
// 1. First check signature of session token.
if !token.VerifySignature() {
return nil, nil, errInvalidSessionSig
}
// 2. Then check if session token owner issued the session token
// TODO(@cthulhu-rider): #468 implement and use another approach to avoid conversion
var tokV2 sessionV2.Token
token.WriteToV2(&tokV2)
tokenIssuerKey, err := unmarshalPublicKey(tokV2.GetSignature().GetKey())
if err != nil {
return nil, nil, fmt.Errorf("invalid key in session token signature: %w", err)
}
tokenIssuer := token.Issuer()
if !isOwnerFromKey(tokenIssuer, tokenIssuerKey) {
// TODO: #767 in this case we can issue all owner keys from frostfs.id and check once again
return nil, nil, errInvalidSessionOwner
}
return &tokenIssuer, tokenIssuerKey, nil
}
func originalBodySignature(v *sessionV2.RequestVerificationHeader) *refsV2.Signature {
if v == nil {
return nil
}
for v.GetOrigin() != nil {
v = v.GetOrigin()
}
return v.GetBodySignature()
}
func unmarshalPublicKey(bs []byte) (*keys.PublicKey, error) {
return keys.NewPublicKeyFromBytes(bs, elliptic.P256())
}
func isOwnerFromKey(id user.ID, key *keys.PublicKey) bool {
if key == nil {
return false
}
var id2 user.ID
user.IDFromKey(&id2, (ecdsa.PublicKey)(*key))
return id2.Equals(id)
}
// assertVerb checks that token verb corresponds to the method.
func assertVerb(tok sessionSDK.Object, method string) bool {
switch method {
case nativeschema.MethodPutObject:
return tok.AssertVerb(sessionSDK.VerbObjectPut, sessionSDK.VerbObjectDelete, sessionSDK.VerbObjectPatch)
case nativeschema.MethodDeleteObject:
return tok.AssertVerb(sessionSDK.VerbObjectDelete)
case nativeschema.MethodGetObject:
return tok.AssertVerb(sessionSDK.VerbObjectGet)
case nativeschema.MethodHeadObject:
return tok.AssertVerb(
sessionSDK.VerbObjectHead,
sessionSDK.VerbObjectGet,
sessionSDK.VerbObjectDelete,
sessionSDK.VerbObjectRange,
sessionSDK.VerbObjectRangeHash,
sessionSDK.VerbObjectPatch,
)
case nativeschema.MethodSearchObject:
return tok.AssertVerb(sessionSDK.VerbObjectSearch, sessionSDK.VerbObjectDelete)
case nativeschema.MethodRangeObject:
return tok.AssertVerb(sessionSDK.VerbObjectRange, sessionSDK.VerbObjectRangeHash, sessionSDK.VerbObjectPatch)
case nativeschema.MethodHashObject:
return tok.AssertVerb(sessionSDK.VerbObjectRangeHash)
case nativeschema.MethodPatchObject:
return tok.AssertVerb(sessionSDK.VerbObjectPatch)
}
return false
}
// assertSessionRelation checks if given token describing the FrostFS session
// relates to the given container and optional object. Missing object
// means that the context isn't bound to any FrostFS object in the container.
// Returns no error iff relation is correct. Criteria:
//
// session is bound to the given container
// object is not specified or session is bound to this object
//
// Session MUST be bound to the particular container, otherwise behavior is undefined.
func assertSessionRelation(tok sessionSDK.Object, cnr cid.ID, obj *oid.ID) error {
if !tok.AssertContainer(cnr) {
return errors.New("requested container is not related to the session")
}
if obj != nil && !tok.AssertObject(*obj) {
return errors.New("requested object is not related to the session")
}
return nil
}
func unmarshalPublicKeyWithOwner(rawKey []byte) (*user.ID, *keys.PublicKey, error) {
key, err := unmarshalPublicKey(rawKey)
if err != nil {
return nil, nil, fmt.Errorf("invalid signature key: %w", err)
}
var idSender user.ID
user.IDFromKey(&idSender, (ecdsa.PublicKey)(*key))
return &idSender, key, nil
}

View file

@ -0,0 +1,84 @@
package ape
import (
"slices"
"testing"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
sessionSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"github.com/stretchr/testify/require"
)
func TestIsVerbCompatible(t *testing.T) {
table := map[string][]sessionSDK.ObjectVerb{
nativeschema.MethodPutObject: {sessionSDK.VerbObjectPut, sessionSDK.VerbObjectDelete, sessionSDK.VerbObjectPatch},
nativeschema.MethodDeleteObject: {sessionSDK.VerbObjectDelete},
nativeschema.MethodGetObject: {sessionSDK.VerbObjectGet},
nativeschema.MethodHeadObject: {
sessionSDK.VerbObjectHead,
sessionSDK.VerbObjectGet,
sessionSDK.VerbObjectDelete,
sessionSDK.VerbObjectRange,
sessionSDK.VerbObjectRangeHash,
sessionSDK.VerbObjectPatch,
},
nativeschema.MethodRangeObject: {sessionSDK.VerbObjectRange, sessionSDK.VerbObjectRangeHash, sessionSDK.VerbObjectPatch},
nativeschema.MethodHashObject: {sessionSDK.VerbObjectRangeHash},
nativeschema.MethodSearchObject: {sessionSDK.VerbObjectSearch, sessionSDK.VerbObjectDelete},
nativeschema.MethodPatchObject: {sessionSDK.VerbObjectPatch},
}
verbs := []sessionSDK.ObjectVerb{
sessionSDK.VerbObjectPut,
sessionSDK.VerbObjectDelete,
sessionSDK.VerbObjectHead,
sessionSDK.VerbObjectRange,
sessionSDK.VerbObjectRangeHash,
sessionSDK.VerbObjectGet,
sessionSDK.VerbObjectSearch,
sessionSDK.VerbObjectPatch,
}
var tok sessionSDK.Object
for op, list := range table {
for _, verb := range verbs {
contains := slices.Contains(list, verb)
tok.ForVerb(verb)
require.Equal(t, contains, assertVerb(tok, op),
"%v in token, %s executing", verb, op)
}
}
}
func TestAssertSessionRelation(t *testing.T) {
var tok sessionSDK.Object
cnr := cidtest.ID()
cnrOther := cidtest.ID()
obj := oidtest.ID()
objOther := oidtest.ID()
// make sure ids differ, otherwise test won't work correctly
require.False(t, cnrOther.Equals(cnr))
require.False(t, objOther.Equals(obj))
// bind session to the container (required)
tok.BindContainer(cnr)
// test container-global session
require.NoError(t, assertSessionRelation(tok, cnr, nil))
require.NoError(t, assertSessionRelation(tok, cnr, &obj))
require.Error(t, assertSessionRelation(tok, cnrOther, nil))
require.Error(t, assertSessionRelation(tok, cnrOther, &obj))
// limit the session to the particular object
tok.LimitByObjects(obj)
// test fixed object session (here obj arg must be non-nil everywhere)
require.NoError(t, assertSessionRelation(tok, cnr, &obj))
require.Error(t, assertSessionRelation(tok, cnr, &objOther))
}