frostfs-node/pkg/services/container/ape.go
Dmitrii Stepanov 5c0a736a25
Some checks failed
DCO action / DCO (pull_request) Successful in 1m23s
Vulncheck / Vulncheck (pull_request) Successful in 3m29s
Tests and linters / Tests (1.21) (pull_request) Failing after 3m58s
Build / Build Components (1.21) (pull_request) Successful in 3m46s
Build / Build Components (1.20) (pull_request) Successful in 3m52s
Tests and linters / Lint (pull_request) Successful in 4m48s
Tests and linters / Staticcheck (pull_request) Successful in 5m5s
Tests and linters / Tests (1.20) (pull_request) Successful in 7m4s
Tests and linters / Tests with -race (pull_request) Successful in 8m36s
[#899] containerSvc: Fix invalid session token type
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2024-01-10 18:37:54 +03:00

516 lines
14 KiB
Go

package container
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"errors"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
sessionSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)
var (
errMissingContainerID = errors.New("missing container ID")
errSessionContainerMissmatch = errors.New("requested container is not related to the session")
errMissingVerificationHeader = errors.New("malformed request: empty verification header")
errInvalidSessionTokenSignature = errors.New("malformed request: invalid session token signature")
errInvalidSessionTokenOwner = errors.New("malformed request: invalid session token owner")
errEmptyBodySignature = errors.New("malformed request: empty body signature")
errMissingOwnerID = errors.New("malformed request: missing owner ID")
undefinedContainerID = cid.ID{}
)
type ir interface {
InnerRingKeys() ([][]byte, error)
}
type containers interface {
Get(cid.ID) (*containercore.Container, error)
}
type apeChecker struct {
router policyengine.ChainRouter
reader containers
ir ir
nm netmap.Source
next Server
}
func NewAPEServer(router policyengine.ChainRouter, reader containers, ir ir, nm netmap.Source, srv Server) Server {
return &apeChecker{
router: router,
reader: reader,
ir: ir,
next: srv,
nm: nm,
}
}
func (ac *apeChecker) AnnounceUsedSpace(ctx context.Context, req *container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.AnnounceUsedSpace")
defer span.End()
// this method is not used, so not checked
return ac.next.AnnounceUsedSpace(ctx, req)
}
func (ac *apeChecker) Delete(ctx context.Context, req *container.DeleteRequest) (*container.DeleteResponse, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.Delete")
defer span.End()
if err := ac.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(),
nativeschema.MethodDeleteContainer); err != nil {
return nil, err
}
return ac.next.Delete(ctx, req)
}
func (ac *apeChecker) Get(ctx context.Context, req *container.GetRequest) (*container.GetResponse, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.Get")
defer span.End()
if err := ac.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(),
nativeschema.MethodGetContainer); err != nil {
return nil, err
}
return ac.next.Get(ctx, req)
}
func (ac *apeChecker) GetExtendedACL(ctx context.Context, req *container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.GetExtendedACL")
defer span.End()
if err := ac.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(),
nativeschema.MethodGetContainerEACL); err != nil {
return nil, err
}
return ac.next.GetExtendedACL(ctx, req)
}
func (ac *apeChecker) List(ctx context.Context, req *container.ListRequest) (*container.ListResponse, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.List")
defer span.End()
role, pk, err := ac.getRoleWithoutContainerID(req.GetBody().GetOwnerID(), req.GetMetaHeader(), req.GetVerificationHeader())
if err != nil {
return nil, err
}
reqProps := map[string]string{
nativeschema.PropertyKeyActorPublicKey: pk.String(),
nativeschema.PropertyKeyActorRole: role,
}
request := &apeRequest{
resource: &apeResource{
name: nativeschema.ResourceFormatRootContainers,
props: make(map[string]string),
},
op: nativeschema.MethodListContainers,
props: reqProps,
}
s, found, err := ac.router.IsAllowed(apechain.Ingress,
policyengine.NewRequestTargetWithNamespace(""),
request)
if err != nil {
return nil, err
}
if !found || s == apechain.Allow {
return ac.next.List(ctx, req)
}
return nil, apeErr(nativeschema.MethodListContainers, s)
}
func (ac *apeChecker) Put(ctx context.Context, req *container.PutRequest) (*container.PutResponse, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.Put")
defer span.End()
role, pk, err := ac.getRoleWithoutContainerID(req.GetBody().GetContainer().GetOwnerID(), req.GetMetaHeader(), req.GetVerificationHeader())
if err != nil {
return nil, err
}
reqProps := map[string]string{
nativeschema.PropertyKeyActorPublicKey: pk.String(),
nativeschema.PropertyKeyActorRole: role,
}
request := &apeRequest{
resource: &apeResource{
name: nativeschema.ResourceFormatRootContainers,
props: make(map[string]string),
},
op: nativeschema.MethodPutContainer,
props: reqProps,
}
s, found, err := ac.router.IsAllowed(apechain.Ingress,
policyengine.NewRequestTargetWithNamespace(""),
request)
if err != nil {
return nil, err
}
if !found || s == apechain.Allow {
return ac.next.Put(ctx, req)
}
return nil, apeErr(nativeschema.MethodPutContainer, s)
}
func (ac *apeChecker) getRoleWithoutContainerID(oID *refs.OwnerID, mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader) (string, *keys.PublicKey, error) {
if vh == nil {
return "", nil, errMissingVerificationHeader
}
if oID == nil {
return "", nil, errMissingOwnerID
}
var ownerID user.ID
if err := ownerID.ReadFromV2(*oID); err != nil {
return "", nil, err
}
actor, pk, err := ac.getActorAndPublicKey(mh, vh, undefinedContainerID)
if err != nil {
return "", nil, err
}
if actor.Equals(ownerID) {
return nativeschema.PropertyValueContainerRoleOwner, pk, nil
}
pkBytes := pk.Bytes()
isIR, err := ac.isInnerRingKey(pkBytes)
if err != nil {
return "", nil, err
}
if isIR {
return nativeschema.PropertyValueContainerRoleIR, pk, nil
}
return nativeschema.PropertyValueContainerRoleOthers, pk, nil
}
func (ac *apeChecker) SetExtendedACL(ctx context.Context, req *container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.SetExtendedACL")
defer span.End()
if err := ac.validateContainerBoundedOperation(req.GetBody().GetEACL().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(),
nativeschema.MethodSetContainerEACL); err != nil {
return nil, err
}
return ac.next.SetExtendedACL(ctx, req)
}
func (ac *apeChecker) validateContainerBoundedOperation(containerID *refs.ContainerID, mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader, op string) error {
if vh == nil {
return errMissingVerificationHeader
}
id, err := getContainerID(containerID)
if err != nil {
return err
}
cont, err := ac.reader.Get(id)
if err != nil {
return err
}
reqProps, err := ac.getRequestProps(mh, vh, cont, id)
if err != nil {
return err
}
request := &apeRequest{
resource: &apeResource{
name: fmt.Sprintf(nativeschema.ResourceFormatRootContainer, id.EncodeToString()),
props: ac.getContainerProps(cont),
},
op: op,
props: reqProps,
}
s, found, err := ac.router.IsAllowed(apechain.Ingress,
policyengine.NewRequestTargetWithContainer(id.EncodeToString()),
request)
if err != nil {
return err
}
if !found || s == apechain.Allow {
return nil
}
return apeErr(op, s)
}
func apeErr(operation string, status apechain.Status) error {
errAccessDenied := &apistatus.ObjectAccessDenied{}
errAccessDenied.WriteReason(fmt.Sprintf("access to container operation %s is denied by access policy engine: %s", operation, status.String()))
return errAccessDenied
}
func getContainerID(reqContID *refs.ContainerID) (cid.ID, error) {
if reqContID == nil {
return cid.ID{}, errMissingContainerID
}
var id cid.ID
err := id.ReadFromV2(*reqContID)
if err != nil {
return cid.ID{}, fmt.Errorf("invalid container ID: %w", err)
}
return id, nil
}
type apeRequest struct {
resource *apeResource
op string
props map[string]string
}
// Operation implements resource.Request.
func (r *apeRequest) Operation() string {
return r.op
}
// Property implements resource.Request.
func (r *apeRequest) Property(key string) string {
return r.props[key]
}
// Resource implements resource.Request.
func (r *apeRequest) Resource() resource.Resource {
return r.resource
}
type apeResource struct {
name string
props map[string]string
}
func (r *apeResource) Name() string {
return r.name
}
func (r *apeResource) Property(key string) string {
return r.props[key]
}
func (ac *apeChecker) getContainerProps(c *containercore.Container) map[string]string {
return map[string]string{
nativeschema.PropertyKeyContainerOwnerID: c.Value.Owner().EncodeToString(),
}
}
func (ac *apeChecker) getRequestProps(mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader,
cont *containercore.Container, cnrID cid.ID,
) (map[string]string, error) {
actor, pk, err := ac.getActorAndPublicKey(mh, vh, cnrID)
if err != nil {
return nil, err
}
role, err := ac.getRole(actor, pk, cont, cnrID)
if err != nil {
return nil, err
}
return map[string]string{
nativeschema.PropertyKeyActorPublicKey: pk.String(),
nativeschema.PropertyKeyActorRole: role,
}, nil
}
func (ac *apeChecker) getRole(actor *user.ID, pk *keys.PublicKey, cont *containercore.Container, cnrID cid.ID) (string, error) {
if cont.Value.Owner().Equals(*actor) {
return nativeschema.PropertyValueContainerRoleOwner, nil
}
pkBytes := pk.Bytes()
isIR, err := ac.isInnerRingKey(pkBytes)
if err != nil {
return "", err
}
if isIR {
return nativeschema.PropertyValueContainerRoleIR, nil
}
isContainer, err := ac.isContainerKey(pkBytes, cnrID, cont)
if err != nil {
return "", err
}
if isContainer {
return nativeschema.PropertyValueContainerRoleContainer, nil
}
return nativeschema.PropertyValueContainerRoleOthers, nil
}
func (ac *apeChecker) getActorAndPublicKey(mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader, cnrID cid.ID) (*user.ID, *keys.PublicKey, error) {
st, err := ac.getSessionToken(mh)
if err != nil {
return nil, nil, err
}
if st != nil {
return ac.getActorAndPKFromSessionToken(st, cnrID)
}
return ac.getActorAndPKFromSignature(vh)
}
func (ac *apeChecker) getActorAndPKFromSignature(vh *session.RequestVerificationHeader) (*user.ID, *keys.PublicKey, error) {
for vh.GetOrigin() != nil {
vh = vh.GetOrigin()
}
sig := vh.GetBodySignature()
if sig == nil {
return nil, nil, errEmptyBodySignature
}
key, err := keys.NewPublicKeyFromBytes(sig.GetKey(), elliptic.P256())
if err != nil {
return nil, nil, fmt.Errorf("invalid signature key: %w", err)
}
var userID user.ID
user.IDFromKey(&userID, (ecdsa.PublicKey)(*key))
return &userID, key, nil
}
func (ac *apeChecker) getSessionToken(mh *session.RequestMetaHeader) (*sessionSDK.Container, error) {
for mh.GetOrigin() != nil {
mh = mh.GetOrigin()
}
st := mh.GetSessionToken()
if st == nil {
return nil, nil
}
var tok sessionSDK.Container
err := tok.ReadFromV2(*st)
if err != nil {
return nil, fmt.Errorf("invalid session token: %w", err)
}
return &tok, nil
}
func (ac *apeChecker) getActorAndPKFromSessionToken(st *sessionSDK.Container, cnrID cid.ID) (*user.ID, *keys.PublicKey, error) {
if cnrID != undefinedContainerID && !st.AppliedTo(cnrID) {
return nil, nil, errSessionContainerMissmatch
}
if !st.VerifySignature() {
return nil, nil, errInvalidSessionTokenSignature
}
var tok session.Token
st.WriteToV2(&tok)
signaturePublicKey, err := keys.NewPublicKeyFromBytes(tok.GetSignature().GetKey(), elliptic.P256())
if err != nil {
return nil, nil, fmt.Errorf("invalid key in session token signature: %w", err)
}
tokenIssuer := st.Issuer()
if !isOwnerFromKey(tokenIssuer, signaturePublicKey) {
return nil, nil, errInvalidSessionTokenOwner
}
return &tokenIssuer, signaturePublicKey, nil
}
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)
}
func (ac *apeChecker) isInnerRingKey(pk []byte) (bool, error) {
innerRingKeys, err := ac.ir.InnerRingKeys()
if err != nil {
return false, err
}
for i := range innerRingKeys {
if bytes.Equal(innerRingKeys[i], pk) {
return true, nil
}
}
return false, nil
}
func (ac *apeChecker) isContainerKey(pk []byte, cnrID cid.ID, cont *containercore.Container) (bool, error) {
binCnrID := make([]byte, sha256.Size)
cnrID.Encode(binCnrID)
nm, err := netmap.GetLatestNetworkMap(ac.nm)
if err != nil {
return false, err
}
in, err := isContainerNode(nm, pk, binCnrID, cont)
if err != nil {
return false, err
} else if in {
return true, nil
}
// then check previous netmap, this can happen in-between epoch change
// when node migrates data from last epoch container
nm, err = netmap.GetPreviousNetworkMap(ac.nm)
if err != nil {
return false, err
}
return isContainerNode(nm, pk, binCnrID, cont)
}
func isContainerNode(nm *netmapSDK.NetMap, pk, binCnrID []byte, cont *containercore.Container) (bool, error) {
cnrVectors, err := nm.ContainerNodes(cont.Value.PlacementPolicy(), binCnrID)
if err != nil {
return false, err
}
for i := range cnrVectors {
for j := range cnrVectors[i] {
if bytes.Equal(cnrVectors[i][j].PublicKey(), pk) {
return true, nil
}
}
}
return false, nil
}