frostfs-node/pkg/services/object/ape/util.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

169 lines
5 KiB
Go

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
}