frostfs-node/pkg/services/container/ape.go

671 lines
19 KiB
Go
Raw Normal View History

package container
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net"
"strings"
aperequest "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/ape/request"
containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
frostfsidcore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/frostfsid"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs"
session "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"
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"
commonschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"google.golang.org/grpc/peer"
)
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")
errOwnerIDIsNotSet = errors.New("owner id is not set")
errInvalidDomainZone = errors.New("invalid domain zone: no namespace is expected")
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
frostFSIDClient frostfsidcore.SubjectProvider
next Server
}
func NewAPEServer(router policyengine.ChainRouter, reader containers, ir ir, nm netmap.Source, frostFSIDClient frostfsidcore.SubjectProvider, srv Server) Server {
return &apeChecker{
router: router,
reader: reader,
ir: ir,
next: srv,
nm: nm,
frostFSIDClient: frostFSIDClient,
}
}
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(ctx, 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(ctx, req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(),
nativeschema.MethodGetContainer); err != nil {
return nil, err
}
return ac.next.Get(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: hex.EncodeToString(pk.Bytes()),
nativeschema.PropertyKeyActorRole: role,
}
reqProps, err = ac.fillWithUserClaimTags(reqProps, pk)
if err != nil {
return nil, err
}
if p, ok := peer.FromContext(ctx); ok {
if tcpAddr, ok := p.Addr.(*net.TCPAddr); ok {
reqProps[commonschema.PropertyKeyFrostFSSourceIP] = tcpAddr.IP.String()
}
}
namespace, err := ac.namespaceByOwner(req.GetBody().GetOwnerID())
if err != nil {
return nil, fmt.Errorf("could not get owner namespace: %w", err)
}
if err := ac.validateNamespaceByPublicKey(pk, namespace); err != nil {
return nil, err
}
request := aperequest.NewRequest(
nativeschema.MethodListContainers,
aperequest.NewResource(
resourceName(namespace, ""),
make(map[string]string),
),
reqProps,
)
groups, err := aperequest.Groups(ac.frostFSIDClient, pk)
if err != nil {
return nil, fmt.Errorf("failed to get group ids: %w", err)
}
// Policy contract keeps group related chains as namespace-group pair.
for i := range groups {
groups[i] = fmt.Sprintf("%s:%s", namespace, groups[i])
}
rt := policyengine.NewRequestTargetWithNamespace(namespace)
rt.User = &policyengine.Target{
Type: policyengine.User,
Name: fmt.Sprintf("%s:%s", namespace, pk.Address()),
}
rt.Groups = make([]policyengine.Target, len(groups))
for i := range groups {
rt.Groups[i] = policyengine.GroupTarget(groups[i])
}
s, found, err := ac.router.IsAllowed(apechain.Ingress, rt, 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: hex.EncodeToString(pk.Bytes()),
nativeschema.PropertyKeyActorRole: role,
}
reqProps, err = ac.fillWithUserClaimTags(reqProps, pk)
if err != nil {
return nil, err
}
if p, ok := peer.FromContext(ctx); ok {
if tcpAddr, ok := p.Addr.(*net.TCPAddr); ok {
reqProps[commonschema.PropertyKeyFrostFSSourceIP] = tcpAddr.IP.String()
}
}
namespace, err := ac.namespaceByKnownOwner(req.GetBody().GetContainer().GetOwnerID())
if err != nil {
return nil, fmt.Errorf("get namespace error: %w", err)
}
if err = validateNamespace(req.GetBody().GetContainer(), namespace); err != nil {
return nil, err
}
request := aperequest.NewRequest(
nativeschema.MethodPutContainer,
aperequest.NewResource(
resourceName(namespace, ""),
make(map[string]string),
),
reqProps,
)
groups, err := aperequest.Groups(ac.frostFSIDClient, pk)
if err != nil {
return nil, fmt.Errorf("failed to get group ids: %w", err)
}
// Policy contract keeps group related chains as namespace-group pair.
for i := range groups {
groups[i] = fmt.Sprintf("%s:%s", namespace, groups[i])
}
rt := policyengine.NewRequestTargetWithNamespace(namespace)
rt.User = &policyengine.Target{
Type: policyengine.User,
Name: fmt.Sprintf("%s:%s", namespace, pk.Address()),
}
rt.Groups = make([]policyengine.Target, len(groups))
for i := range groups {
rt.Groups[i] = policyengine.GroupTarget(groups[i])
}
s, found, err := ac.router.IsAllowed(apechain.Ingress, rt, 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) validateContainerBoundedOperation(ctx context.Context, 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, pk, err := ac.getRequestProps(ctx, mh, vh, cont, id)
if err != nil {
return err
}
namespace := ""
cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cont.Value).Zone(), ".ns")
if hasNamespace {
namespace = cntNamespace
}
groups, err := aperequest.Groups(ac.frostFSIDClient, pk)
if err != nil {
return fmt.Errorf("failed to get group ids: %w", err)
}
// Policy contract keeps group related chains as namespace-group pair.
for i := range groups {
groups[i] = fmt.Sprintf("%s:%s", namespace, groups[i])
}
request := aperequest.NewRequest(
op,
aperequest.NewResource(
resourceName(namespace, id.EncodeToString()),
ac.getContainerProps(cont),
),
reqProps,
)
s, found, err := ac.router.IsAllowed(apechain.Ingress,
policyengine.NewRequestTargetExtended(namespace, id.EncodeToString(), fmt.Sprintf("%s:%s", namespace, pk.Address()), groups),
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
}
func resourceName(namespace string, container string) string {
if namespace == "" && container == "" {
return nativeschema.ResourceFormatRootContainers
}
if namespace == "" && container != "" {
return fmt.Sprintf(nativeschema.ResourceFormatRootContainer, container)
}
if namespace != "" && container == "" {
return fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, namespace)
}
return fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainer, namespace, container)
}
func (ac *apeChecker) getContainerProps(c *containercore.Container) map[string]string {
return map[string]string{
nativeschema.PropertyKeyContainerOwnerID: c.Value.Owner().EncodeToString(),
}
}
func (ac *apeChecker) getRequestProps(ctx context.Context, mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader,
cont *containercore.Container, cnrID cid.ID,
) (map[string]string, *keys.PublicKey, error) {
actor, pk, err := ac.getActorAndPublicKey(mh, vh, cnrID)
if err != nil {
return nil, nil, err
}
role, err := ac.getRole(actor, pk, cont, cnrID)
if err != nil {
return nil, nil, err
}
reqProps := map[string]string{
nativeschema.PropertyKeyActorPublicKey: hex.EncodeToString(pk.Bytes()),
nativeschema.PropertyKeyActorRole: role,
}
reqProps, err = ac.fillWithUserClaimTags(reqProps, pk)
if err != nil {
return nil, nil, err
}
if p, ok := peer.FromContext(ctx); ok {
if tcpAddr, ok := p.Addr.(*net.TCPAddr); ok {
reqProps[commonschema.PropertyKeyFrostFSSourceIP] = tcpAddr.IP.String()
}
}
return reqProps, pk, 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
}
if isContainerNode(nm, pk, binCnrID, cont) {
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), nil
}
func isContainerNode(nm *netmapSDK.NetMap, pk, binCnrID []byte, cont *containercore.Container) bool {
// It could an error only if the network map doesn't have enough nodes to
// fulfil the policy. It's a logical error that doesn't affect an actor role
// determining, so we ignore it
cnrVectors, _ := nm.ContainerNodes(cont.Value.PlacementPolicy(), binCnrID)
for i := range cnrVectors {
for j := range cnrVectors[i] {
if bytes.Equal(cnrVectors[i][j].PublicKey(), pk) {
return true
}
}
}
return false
}
func (ac *apeChecker) namespaceByOwner(owner *refs.OwnerID) (string, error) {
var ownerSDK user.ID
if owner == nil {
return "", errOwnerIDIsNotSet
}
if err := ownerSDK.ReadFromV2(*owner); err != nil {
return "", err
}
addr, err := ownerSDK.ScriptHash()
if err != nil {
return "", err
}
namespace := ""
subject, err := ac.frostFSIDClient.GetSubject(addr)
if err == nil {
namespace = subject.Namespace
} else {
if !strings.Contains(err.Error(), frostfsidcore.SubjectNotFoundErrorMessage) {
return "", fmt.Errorf("get subject error: %w", err)
}
}
return namespace, nil
}
func (ac *apeChecker) namespaceByKnownOwner(owner *refs.OwnerID) (string, error) {
var ownerSDK user.ID
if owner == nil {
return "", errOwnerIDIsNotSet
}
if err := ownerSDK.ReadFromV2(*owner); err != nil {
return "", err
}
addr, err := ownerSDK.ScriptHash()
if err != nil {
return "", err
}
subject, err := ac.frostFSIDClient.GetSubject(addr)
if err != nil {
return "", fmt.Errorf("get subject error: %w", err)
}
return subject.Namespace, nil
}
// validateNamespace validates a namespace set in a container.
// If frostfs-id contract stores a namespace N1 for an owner ID and a container within a request
// is set with namespace N2 (via Zone() property), then N2 is invalid and the request is denied.
func validateNamespace(cnrV2 *container.Container, ownerIDNamespace string) error {
if cnrV2 == nil {
return nil
}
var cnr cnrSDK.Container
if err := cnr.ReadFromV2(*cnrV2); err != nil {
return err
}
cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cnr).Zone(), ".ns")
if hasNamespace {
if cntNamespace != ownerIDNamespace {
if ownerIDNamespace == "" {
return errInvalidDomainZone
}
return fmt.Errorf("invalid domain zone: expected namespace %s, but got %s", ownerIDNamespace, cntNamespace)
}
} else if ownerIDNamespace != "" {
return fmt.Errorf("invalid domain zone: expected namespace %s, but got invalid or empty", ownerIDNamespace)
}
return nil
}
// validateNamespace validates if a namespace of a request actor equals to owner's namespace.
// An actor's namespace is calculated by a public key.
func (ac *apeChecker) validateNamespaceByPublicKey(pk *keys.PublicKey, ownerIDNamespace string) error {
var actor user.ID
user.IDFromKey(&actor, (ecdsa.PublicKey)(*pk))
actorOwnerID := new(refs.OwnerID)
actor.WriteToV2(actorOwnerID)
actorNamespace, err := ac.namespaceByOwner(actorOwnerID)
if err != nil {
return fmt.Errorf("could not get actor namespace: %w", err)
}
if actorNamespace != ownerIDNamespace {
return fmt.Errorf("actor namespace %s differs from owner: %s", actorNamespace, ownerIDNamespace)
}
return nil
}
// fillWithUserClaimTags fills ape request properties with user claim tags getting them from frostfsid contract by actor public key.
func (ac *apeChecker) fillWithUserClaimTags(reqProps map[string]string, pk *keys.PublicKey) (map[string]string, error) {
if reqProps == nil {
reqProps = make(map[string]string)
}
props, err := aperequest.FormFrostfsIDRequestProperties(ac.frostFSIDClient, pk)
if err != nil {
return reqProps, err
}
for propertyName, properyValue := range props {
reqProps[propertyName] = properyValue
}
return reqProps, nil
}