515 lines
14 KiB
Go
515 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")
|
||
|
)
|
||
|
|
||
|
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, cid.ID{})
|
||
|
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.Object, error) {
|
||
|
for mh.GetOrigin() != nil {
|
||
|
mh = mh.GetOrigin()
|
||
|
}
|
||
|
st := mh.GetSessionToken()
|
||
|
if st == nil {
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
var tok sessionSDK.Object
|
||
|
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.Object, cnrID cid.ID) (*user.ID, *keys.PublicKey, error) {
|
||
|
if !st.AssertContainer(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
|
||
|
}
|