Add APE checks for container operations #881
3 changed files with 1009 additions and 1 deletions
|
@ -35,7 +35,10 @@ func initContainerService(_ context.Context, c *cfg) {
|
|||
server := containerTransportGRPC.New(
|
||||
containerService.NewSignService(
|
||||
&c.key.PrivateKey,
|
||||
containerService.NewExecutionService(containerMorph.NewExecutor(cnrRdr, cnrWrt), c.respSvc),
|
||||
containerService.NewAPEServer(c.cfgObject.cfgAccessPolicyEngine.accessPolicyEngine, cnrRdr,
|
||||
newCachedIRFetcher(createInnerRingFetcher(c)), c.netMapSource,
|
||||
containerService.NewExecutionService(containerMorph.NewExecutor(cnrRdr, cnrWrt), c.respSvc),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
514
pkg/services/container/ape.go
Normal file
514
pkg/services/container/ape.go
Normal file
|
@ -0,0 +1,514 @@
|
|||
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
|
||||
}
|
491
pkg/services/container/ape_test.go
Normal file
491
pkg/services/container/ape_test.go
Normal file
|
@ -0,0 +1,491 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
||||
containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
||||
containertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/test"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
sessiontest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session/test"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
|
||||
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPE(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("deny get container for others", testDenyGetContainerForOthers)
|
||||
t.Run("deny set container eACL for IR", testDenySetContainerEACLForIR)
|
||||
t.Run("deny get container eACL for IR with session token", testDenyGetContainerEACLForIRSessionToken)
|
||||
t.Run("deny put container for others with session token", testDenyPutContainerForOthersSessionToken)
|
||||
t.Run("deny list containers for owner with PK", testDenyListContainersForPK)
|
||||
}
|
||||
|
||||
func testDenyGetContainerForOthers(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := &srvStub{
|
||||
calls: map[string]int{},
|
||||
}
|
||||
router := inmemory.NewInMemory()
|
||||
contRdr := &containerStub{
|
||||
c: map[cid.ID]*containercore.Container{},
|
||||
}
|
||||
ir := &irStub{
|
||||
keys: [][]byte{},
|
||||
}
|
||||
nm := &netmapStub{}
|
||||
apeSrv := NewAPEServer(router, contRdr, ir, nm, srv)
|
||||
|
||||
contID := cidtest.ID()
|
||||
testContainer := containertest.Container()
|
||||
pp := netmap.PlacementPolicy{}
|
||||
require.NoError(t, pp.DecodeString("REP 1"))
|
||||
testContainer.SetPlacementPolicy(pp)
|
||||
contRdr.c[contID] = &containercore.Container{Value: testContainer}
|
||||
|
||||
nm.currentEpoch = 100
|
||||
nm.netmaps = map[uint64]*netmap.NetMap{}
|
||||
var testNetmap netmap.NetMap
|
||||
testNetmap.SetEpoch(nm.currentEpoch)
|
||||
testNetmap.SetNodes([]netmap.NodeInfo{{}})
|
||||
nm.netmaps[nm.currentEpoch] = &testNetmap
|
||||
nm.netmaps[nm.currentEpoch-1] = &testNetmap
|
||||
|
||||
_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
|
||||
Rules: []chain.Rule{
|
||||
{
|
||||
Status: chain.AccessDenied,
|
||||
Actions: chain.Actions{
|
||||
Names: []string{
|
||||
nativeschema.MethodGetContainer,
|
||||
},
|
||||
},
|
||||
Resources: chain.Resources{
|
||||
Names: []string{
|
||||
fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()),
|
||||
},
|
||||
},
|
||||
Condition: []chain.Condition{
|
||||
{
|
||||
Object: chain.ObjectRequest,
|
||||
Key: nativeschema.PropertyKeyActorRole,
|
||||
Value: nativeschema.PropertyValueContainerRoleOthers,
|
||||
Op: chain.CondStringEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &container.GetRequest{}
|
||||
req.SetBody(&container.GetRequestBody{})
|
||||
var refContID refs.ContainerID
|
||||
contID.WriteToV2(&refContID)
|
||||
req.GetBody().SetContainerID(&refContID)
|
||||
|
||||
pk, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))
|
||||
|
||||
resp, err := apeSrv.Get(context.Background(), req)
|
||||
require.Nil(t, resp)
|
||||
var errAccessDenied *apistatus.ObjectAccessDenied
|
||||
require.ErrorAs(t, err, &errAccessDenied)
|
||||
}
|
||||
|
||||
func testDenySetContainerEACLForIR(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := &srvStub{
|
||||
calls: map[string]int{},
|
||||
}
|
||||
router := inmemory.NewInMemory()
|
||||
contRdr := &containerStub{
|
||||
c: map[cid.ID]*containercore.Container{},
|
||||
}
|
||||
ir := &irStub{
|
||||
keys: [][]byte{},
|
||||
}
|
||||
nm := &netmapStub{}
|
||||
apeSrv := NewAPEServer(router, contRdr, ir, nm, srv)
|
||||
|
||||
contID := cidtest.ID()
|
||||
testContainer := containertest.Container()
|
||||
pp := netmap.PlacementPolicy{}
|
||||
require.NoError(t, pp.DecodeString("REP 1"))
|
||||
testContainer.SetPlacementPolicy(pp)
|
||||
contRdr.c[contID] = &containercore.Container{Value: testContainer}
|
||||
|
||||
nm.currentEpoch = 100
|
||||
nm.netmaps = map[uint64]*netmap.NetMap{}
|
||||
var testNetmap netmap.NetMap
|
||||
testNetmap.SetEpoch(nm.currentEpoch)
|
||||
testNetmap.SetNodes([]netmap.NodeInfo{{}})
|
||||
nm.netmaps[nm.currentEpoch] = &testNetmap
|
||||
nm.netmaps[nm.currentEpoch-1] = &testNetmap
|
||||
|
||||
_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
|
||||
Rules: []chain.Rule{
|
||||
{
|
||||
Status: chain.AccessDenied,
|
||||
Actions: chain.Actions{
|
||||
Names: []string{
|
||||
nativeschema.MethodSetContainerEACL,
|
||||
},
|
||||
},
|
||||
Resources: chain.Resources{
|
||||
Names: []string{
|
||||
fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()),
|
||||
},
|
||||
},
|
||||
Condition: []chain.Condition{
|
||||
{
|
||||
Object: chain.ObjectRequest,
|
||||
Key: nativeschema.PropertyKeyActorRole,
|
||||
Value: nativeschema.PropertyValueContainerRoleIR,
|
||||
Op: chain.CondStringEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &container.SetExtendedACLRequest{}
|
||||
req.SetBody(&container.SetExtendedACLRequestBody{})
|
||||
var refContID refs.ContainerID
|
||||
contID.WriteToV2(&refContID)
|
||||
req.GetBody().SetEACL(&acl.Table{})
|
||||
req.GetBody().GetEACL().SetContainerID(&refContID)
|
||||
|
||||
pk, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))
|
||||
ir.keys = append(ir.keys, pk.PublicKey().Bytes())
|
||||
|
||||
resp, err := apeSrv.SetExtendedACL(context.Background(), req)
|
||||
require.Nil(t, resp)
|
||||
var errAccessDenied *apistatus.ObjectAccessDenied
|
||||
require.ErrorAs(t, err, &errAccessDenied)
|
||||
}
|
||||
|
||||
func testDenyGetContainerEACLForIRSessionToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := &srvStub{
|
||||
calls: map[string]int{},
|
||||
}
|
||||
router := inmemory.NewInMemory()
|
||||
contRdr := &containerStub{
|
||||
c: map[cid.ID]*containercore.Container{},
|
||||
}
|
||||
ir := &irStub{
|
||||
keys: [][]byte{},
|
||||
}
|
||||
nm := &netmapStub{}
|
||||
apeSrv := NewAPEServer(router, contRdr, ir, nm, srv)
|
||||
|
||||
contID := cidtest.ID()
|
||||
testContainer := containertest.Container()
|
||||
pp := netmap.PlacementPolicy{}
|
||||
require.NoError(t, pp.DecodeString("REP 1"))
|
||||
testContainer.SetPlacementPolicy(pp)
|
||||
contRdr.c[contID] = &containercore.Container{Value: testContainer}
|
||||
|
||||
nm.currentEpoch = 100
|
||||
nm.netmaps = map[uint64]*netmap.NetMap{}
|
||||
var testNetmap netmap.NetMap
|
||||
testNetmap.SetEpoch(nm.currentEpoch)
|
||||
testNetmap.SetNodes([]netmap.NodeInfo{{}})
|
||||
nm.netmaps[nm.currentEpoch] = &testNetmap
|
||||
nm.netmaps[nm.currentEpoch-1] = &testNetmap
|
||||
|
||||
_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{
|
||||
Rules: []chain.Rule{
|
||||
{
|
||||
Status: chain.AccessDenied,
|
||||
Actions: chain.Actions{
|
||||
Names: []string{
|
||||
nativeschema.MethodGetContainerEACL,
|
||||
},
|
||||
},
|
||||
Resources: chain.Resources{
|
||||
Names: []string{
|
||||
fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()),
|
||||
},
|
||||
},
|
||||
Condition: []chain.Condition{
|
||||
{
|
||||
Object: chain.ObjectRequest,
|
||||
Key: nativeschema.PropertyKeyActorRole,
|
||||
Value: nativeschema.PropertyValueContainerRoleIR,
|
||||
Op: chain.CondStringEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &container.GetExtendedACLRequest{}
|
||||
req.SetBody(&container.GetExtendedACLRequestBody{})
|
||||
var refContID refs.ContainerID
|
||||
contID.WriteToV2(&refContID)
|
||||
req.GetBody().SetContainerID(&refContID)
|
||||
|
||||
pk, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))
|
||||
|
||||
sessionPK, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
sToken := sessiontest.ObjectSigned()
|
||||
sToken.BindContainer(contID)
|
||||
require.NoError(t, sToken.Sign(sessionPK.PrivateKey))
|
||||
var sTokenV2 session.Token
|
||||
sToken.WriteToV2(&sTokenV2)
|
||||
metaHeader := new(session.RequestMetaHeader)
|
||||
metaHeader.SetSessionToken(&sTokenV2)
|
||||
req.SetMetaHeader(metaHeader)
|
||||
|
||||
ir.keys = append(ir.keys, sessionPK.PublicKey().Bytes())
|
||||
|
||||
resp, err := apeSrv.GetExtendedACL(context.Background(), req)
|
||||
require.Nil(t, resp)
|
||||
var errAccessDenied *apistatus.ObjectAccessDenied
|
||||
require.ErrorAs(t, err, &errAccessDenied)
|
||||
}
|
||||
|
||||
func testDenyPutContainerForOthersSessionToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := &srvStub{
|
||||
calls: map[string]int{},
|
||||
}
|
||||
router := inmemory.NewInMemory()
|
||||
contRdr := &containerStub{
|
||||
c: map[cid.ID]*containercore.Container{},
|
||||
}
|
||||
ir := &irStub{
|
||||
keys: [][]byte{},
|
||||
}
|
||||
nm := &netmapStub{}
|
||||
apeSrv := NewAPEServer(router, contRdr, ir, nm, srv)
|
||||
|
||||
testContainer := containertest.Container()
|
||||
|
||||
nm.currentEpoch = 100
|
||||
nm.netmaps = map[uint64]*netmap.NetMap{}
|
||||
|
||||
_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(""), &chain.Chain{
|
||||
Rules: []chain.Rule{
|
||||
{
|
||||
Status: chain.AccessDenied,
|
||||
Actions: chain.Actions{
|
||||
Names: []string{
|
||||
nativeschema.MethodPutContainer,
|
||||
},
|
||||
},
|
||||
Resources: chain.Resources{
|
||||
Names: []string{
|
||||
nativeschema.ResourceFormatRootContainers,
|
||||
},
|
||||
},
|
||||
Condition: []chain.Condition{
|
||||
{
|
||||
Object: chain.ObjectRequest,
|
||||
Key: nativeschema.PropertyKeyActorRole,
|
||||
Value: nativeschema.PropertyValueContainerRoleOthers,
|
||||
Op: chain.CondStringEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &container.PutRequest{}
|
||||
req.SetBody(&container.PutRequestBody{})
|
||||
var reqCont container.Container
|
||||
testContainer.WriteToV2(&reqCont)
|
||||
req.GetBody().SetContainer(&reqCont)
|
||||
|
||||
sessionPK, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
sToken := sessiontest.ObjectSigned()
|
||||
sToken.BindContainer(cid.ID{})
|
||||
require.NoError(t, sToken.Sign(sessionPK.PrivateKey))
|
||||
var sTokenV2 session.Token
|
||||
sToken.WriteToV2(&sTokenV2)
|
||||
metaHeader := new(session.RequestMetaHeader)
|
||||
metaHeader.SetSessionToken(&sTokenV2)
|
||||
req.SetMetaHeader(metaHeader)
|
||||
|
||||
pk, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))
|
||||
|
||||
resp, err := apeSrv.Put(context.Background(), req)
|
||||
require.Nil(t, resp)
|
||||
var errAccessDenied *apistatus.ObjectAccessDenied
|
||||
require.ErrorAs(t, err, &errAccessDenied)
|
||||
}
|
||||
|
||||
func testDenyListContainersForPK(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := &srvStub{
|
||||
calls: map[string]int{},
|
||||
}
|
||||
router := inmemory.NewInMemory()
|
||||
contRdr := &containerStub{
|
||||
c: map[cid.ID]*containercore.Container{},
|
||||
}
|
||||
ir := &irStub{
|
||||
keys: [][]byte{},
|
||||
}
|
||||
nm := &netmapStub{}
|
||||
apeSrv := NewAPEServer(router, contRdr, ir, nm, srv)
|
||||
|
||||
nm.currentEpoch = 100
|
||||
nm.netmaps = map[uint64]*netmap.NetMap{}
|
||||
|
||||
pk, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(""), &chain.Chain{
|
||||
Rules: []chain.Rule{
|
||||
{
|
||||
Status: chain.AccessDenied,
|
||||
Actions: chain.Actions{
|
||||
Names: []string{
|
||||
nativeschema.MethodListContainers,
|
||||
},
|
||||
},
|
||||
Resources: chain.Resources{
|
||||
Names: []string{
|
||||
nativeschema.ResourceFormatRootContainers,
|
||||
},
|
||||
},
|
||||
Condition: []chain.Condition{
|
||||
{
|
||||
Object: chain.ObjectRequest,
|
||||
Key: nativeschema.PropertyKeyActorPublicKey,
|
||||
Value: pk.PublicKey().String(),
|
||||
Op: chain.CondStringEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var userID user.ID
|
||||
user.IDFromKey(&userID, pk.PrivateKey.PublicKey)
|
||||
|
||||
req := &container.ListRequest{}
|
||||
req.SetBody(&container.ListRequestBody{})
|
||||
var ownerID refs.OwnerID
|
||||
userID.WriteToV2(&ownerID)
|
||||
req.GetBody().SetOwnerID(&ownerID)
|
||||
|
||||
require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req))
|
||||
|
||||
resp, err := apeSrv.List(context.Background(), req)
|
||||
require.Nil(t, resp)
|
||||
var errAccessDenied *apistatus.ObjectAccessDenied
|
||||
require.ErrorAs(t, err, &errAccessDenied)
|
||||
}
|
||||
|
||||
type srvStub struct {
|
||||
calls map[string]int
|
||||
}
|
||||
|
||||
func (s *srvStub) AnnounceUsedSpace(context.Context, *container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) {
|
||||
s.calls["AnnounceUsedSpace"]++
|
||||
return &container.AnnounceUsedSpaceResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *srvStub) Delete(context.Context, *container.DeleteRequest) (*container.DeleteResponse, error) {
|
||||
s.calls["Delete"]++
|
||||
return &container.DeleteResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *srvStub) Get(context.Context, *container.GetRequest) (*container.GetResponse, error) {
|
||||
s.calls["Get"]++
|
||||
return &container.GetResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *srvStub) GetExtendedACL(context.Context, *container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) {
|
||||
s.calls["GetExtendedACL"]++
|
||||
return &container.GetExtendedACLResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *srvStub) List(context.Context, *container.ListRequest) (*container.ListResponse, error) {
|
||||
s.calls["List"]++
|
||||
return &container.ListResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *srvStub) Put(context.Context, *container.PutRequest) (*container.PutResponse, error) {
|
||||
s.calls["Put"]++
|
||||
return &container.PutResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *srvStub) SetExtendedACL(context.Context, *container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) {
|
||||
s.calls["SetExtendedACL"]++
|
||||
return &container.SetExtendedACLResponse{}, nil
|
||||
}
|
||||
|
||||
type irStub struct {
|
||||
keys [][]byte
|
||||
}
|
||||
|
||||
func (s *irStub) InnerRingKeys() ([][]byte, error) {
|
||||
return s.keys, nil
|
||||
}
|
||||
|
||||
type containerStub struct {
|
||||
c map[cid.ID]*containercore.Container
|
||||
}
|
||||
|
||||
func (s *containerStub) Get(id cid.ID) (*containercore.Container, error) {
|
||||
if v, ok := s.c[id]; ok {
|
||||
return v, nil
|
||||
}
|
||||
return nil, errors.New("container not found")
|
||||
}
|
||||
|
||||
type netmapStub struct {
|
||||
netmaps map[uint64]*netmap.NetMap
|
||||
currentEpoch uint64
|
||||
}
|
||||
|
||||
func (s *netmapStub) GetNetMap(diff uint64) (*netmap.NetMap, error) {
|
||||
if diff >= s.currentEpoch {
|
||||
return nil, errors.New("invalid diff")
|
||||
}
|
||||
return s.GetNetMapByEpoch(s.currentEpoch - diff)
|
||||
}
|
||||
|
||||
func (s *netmapStub) GetNetMapByEpoch(epoch uint64) (*netmap.NetMap, error) {
|
||||
if nm, found := s.netmaps[epoch]; found {
|
||||
return nm, nil
|
||||
}
|
||||
return nil, errors.New("netmap not found")
|
||||
}
|
||||
|
||||
func (s *netmapStub) Epoch() (uint64, error) {
|
||||
return s.currentEpoch, nil
|
||||
}
|
Loading…
Reference in a new issue