frostfs-node/pkg/services/object/ape/metadata.go
Airat Arifullin 73e35bc885 [#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>
2025-03-23 06:39:32 +00:00

172 lines
4.8 KiB
Go

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
}