ape, adm, container: Form APE requests properly #940

Merged
fyrchik merged 3 commits from aarifullin/frostfs-node:fix/934-invalid_ape_request into master 2024-02-01 17:38:27 +00:00
13 changed files with 850 additions and 56 deletions

View file

@ -151,11 +151,13 @@ func initFrostfsIDCreateNamespaceCmd() {
frostfsidCmd.AddCommand(frostfsidCreateNamespaceCmd)
frostfsidCreateNamespaceCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc)
frostfsidCreateNamespaceCmd.Flags().String(namespaceFlag, "", "Namespace name to create")
frostfsidCreateNamespaceCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func initFrostfsIDListNamespacesCmd() {
frostfsidCmd.AddCommand(frostfsidListNamespacesCmd)
frostfsidListNamespacesCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc)
frostfsidListNamespacesCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func initFrostfsIDCreateSubjectCmd() {
@ -164,12 +166,14 @@ func initFrostfsIDCreateSubjectCmd() {
frostfsidCreateSubjectCmd.Flags().String(namespaceFlag, "", "Namespace where create subject")
frostfsidCreateSubjectCmd.Flags().String(subjectNameFlag, "", "Subject name, must be unique in namespace")
frostfsidCreateSubjectCmd.Flags().String(subjectKeyFlag, "", "Subject hex-encoded public key")
frostfsidCreateSubjectCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func initFrostfsIDDeleteSubjectCmd() {
frostfsidCmd.AddCommand(frostfsidDeleteSubjectCmd)
frostfsidDeleteSubjectCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc)
frostfsidDeleteSubjectCmd.Flags().String(subjectAddressFlag, "", "Subject address")
frostfsidDeleteSubjectCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func initFrostfsIDListSubjectsCmd() {
@ -177,6 +181,7 @@ func initFrostfsIDListSubjectsCmd() {
frostfsidListSubjectsCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc)
frostfsidListSubjectsCmd.Flags().String(namespaceFlag, "", "Namespace to list subjects")
frostfsidListSubjectsCmd.Flags().Bool(includeNamesFlag, false, "Whether include subject name (require additional requests)")
frostfsidListSubjectsCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func initFrostfsIDCreateGroupCmd() {
@ -184,6 +189,7 @@ func initFrostfsIDCreateGroupCmd() {
frostfsidCreateGroupCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc)
frostfsidCreateGroupCmd.Flags().String(namespaceFlag, "", "Namespace where create group")
frostfsidCreateGroupCmd.Flags().String(groupNameFlag, "", "Group name, must be unique in namespace")
frostfsidCreateGroupCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func initFrostfsIDDeleteGroupCmd() {
@ -191,12 +197,14 @@ func initFrostfsIDDeleteGroupCmd() {
frostfsidDeleteGroupCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc)
frostfsidDeleteGroupCmd.Flags().String(namespaceFlag, "", "Namespace to delete group")
frostfsidDeleteGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id")
frostfsidDeleteGroupCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func initFrostfsIDListGroupsCmd() {
frostfsidCmd.AddCommand(frostfsidListGroupsCmd)
frostfsidListGroupsCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc)
frostfsidListGroupsCmd.Flags().String(namespaceFlag, "", "Namespace to list groups")
frostfsidListGroupsCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func initFrostfsIDAddSubjectToGroupCmd() {
@ -204,6 +212,7 @@ func initFrostfsIDAddSubjectToGroupCmd() {
frostfsidAddSubjectToGroupCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc)
frostfsidAddSubjectToGroupCmd.Flags().String(subjectAddressFlag, "", "Subject address")
frostfsidAddSubjectToGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id")
frostfsidAddSubjectToGroupCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func initFrostfsIDRemoveSubjectFromGroupCmd() {
@ -211,6 +220,7 @@ func initFrostfsIDRemoveSubjectFromGroupCmd() {
frostfsidRemoveSubjectFromGroupCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc)
frostfsidRemoveSubjectFromGroupCmd.Flags().String(subjectAddressFlag, "", "Subject address")
frostfsidRemoveSubjectFromGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id")
frostfsidRemoveSubjectFromGroupCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func initFrostfsIDListGroupSubjectsCmd() {
@ -219,6 +229,7 @@ func initFrostfsIDListGroupSubjectsCmd() {
frostfsidListGroupSubjectsCmd.Flags().String(namespaceFlag, "", "Namespace name")
frostfsidListGroupSubjectsCmd.Flags().Int64(groupIDFlag, 0, "Group id")
frostfsidListGroupSubjectsCmd.Flags().Bool(includeNamesFlag, false, "Whether include subject name (require additional requests)")
frostfsidListGroupSubjectsCmd.Flags().String(alphabetWalletsFlag, "", alphabetWalletsFlagDesc)
}
func frostfsidCreateNamespace(cmd *cobra.Command, _ []string) {

View file

@ -449,6 +449,7 @@ type cfg struct {
cfgMorph cfgMorph
cfgAccounting cfgAccounting
cfgContainer cfgContainer
cfgFrostfsID cfgFrostfsID
cfgNodeInfo cfgNodeInfo
cfgNetmap cfgNetmap
cfgControlService cfgControlService
@ -569,6 +570,10 @@ type cfgContainer struct {
workerPool util.WorkerPool // pool for asynchronous handlers
}
type cfgFrostfsID struct {
scriptHash neogoutil.Uint160
}
type cfgNetmap struct {
scriptHash neogoutil.Uint160
wrapper *nmClient.Client
@ -681,6 +686,8 @@ func initCfg(appCfg *config.Config) *cfg {
}
c.cfgContainer = initContainer(appCfg)
c.cfgFrostfsID = initFrostfsID(appCfg)
c.cfgNetmap = initNetmap(appCfg, netState, relayOnly)
c.cfgGRPC = initCfgGRPC()
@ -779,6 +786,12 @@ func initContainer(appCfg *config.Config) cfgContainer {
}
}
func initFrostfsID(appCfg *config.Config) cfgFrostfsID {
return cfgFrostfsID{
scriptHash: contractsconfig.FrostfsID(appCfg),
}
}
func initCfgGRPC() cfgGRPC {
maxChunkSize := uint64(maxMsgSize) * 3 / 4 // 25% to meta, 75% to payload
maxAddrAmount := uint64(maxChunkSize) / addressSize // each address is about 72 bytes

View file

@ -38,6 +38,10 @@ func Container(c *config.Config) util.Uint160 {
return contractAddress(c, "container")
}
func FrostfsID(c *config.Config) util.Uint160 {

Please, write a comment similar to other functions.
I wonder why do we need this section -- it has been here since before-NNS times, now everything can be fetched.

Please, write a comment similar to other functions. I wonder why do we need this section -- it has been here since before-NNS times, now everything can be fetched.
return contractAddress(c, "frostfsid")
}
// Proxy returnsthe value of "proxy" config parameter
// from "contracts" section.
//

View file

@ -9,6 +9,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
containerCore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
cntClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/frostfsid"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/event"
containerEvent "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/event/container"
containerTransportGRPC "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network/transport/container/grpc"
@ -32,11 +33,14 @@ func initContainerService(_ context.Context, c *cfg) {
cnrRdr, cnrWrt := configureEACLAndContainerSources(c, wrap, cnrSrc)
frostFSIDClient, err := frostfsid.NewFromMorph(c.cfgMorph.client, c.cfgFrostfsID.scriptHash, 0)
fatalOnErr(err)
server := containerTransportGRPC.New(
containerService.NewSignService(
&c.key.PrivateKey,
containerService.NewAPEServer(c.cfgObject.cfgAccessPolicyEngine.accessPolicyEngine, cnrRdr,
newCachedIRFetcher(createInnerRingFetcher(c)), c.netMapSource,
newCachedIRFetcher(createInnerRingFetcher(c)), c.netMapSource, frostFSIDClient,
containerService.NewExecutionService(containerMorph.NewExecutor(cnrRdr, cnrWrt), c.respSvc),
),
),

View file

@ -288,6 +288,7 @@ func lookupScriptHashesInNNS(c *cfg) {
{&c.cfgNetmap.scriptHash, client.NNSNetmapContractName},
{&c.cfgAccounting.scriptHash, client.NNSBalanceContractName},
{&c.cfgContainer.scriptHash, client.NNSContainerContractName},
{&c.cfgFrostfsID.scriptHash, client.NNSFrostFSIDContractName},
{&c.cfgMorph.proxyScriptHash, client.NNSProxyContractName},
{&c.cfgObject.cfgAccessPolicyEngine.policyContractHash, client.NNSPolicyContractName},
}

2
go.mod
View file

@ -6,7 +6,7 @@ require (
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240112150928-72885aae835c
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20240115082915-f2a82aa635aa
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240117145620-110b7e41706e
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240126141009-65b4525b3bf0
git.frostfs.info/TrueCloudLab/hrw v1.2.1
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240122104724-06cbfe8691ad
git.frostfs.info/TrueCloudLab/tzhash v1.8.0

BIN
go.sum

Binary file not shown.

View file

@ -115,6 +115,10 @@ func getTargetBucket(tx *bbolt.Tx, name chain.Name, target policyengine.Target)
return nil, fmt.Errorf("%w: %w: %c", policyengine.ErrChainNotFound, ErrTargetTypeBucketNotFound, target.Type)
}
if target.Type == policyengine.Namespace && target.Name == "" {
target.Name = "root"
Review

Such renaming is actual only for local overrides or also for rules in contract?

Such renaming is actual only for local overrides or also for rules in contract?
Review

Only for local overrides (this is because boltdb does not allow to create buckets with empty names)

I also would like to highlight that listing with empty namespace does not behave the same way as in Policy contract

**Only** for local overrides (this is because boltdb does not allow to create buckets with empty names) I also would like to highlight that listing with empty namespace does not behave the same way as in Policy contract
}
rbucket := typeBucket.Bucket([]byte(target.Name))
if rbucket == nil {
return nil, fmt.Errorf("%w: %w: %s", policyengine.ErrChainNotFound, ErrTargetNameBucketNotFound, target.Name)
@ -146,6 +150,10 @@ func getTargetBucketCreateIfEmpty(tx *bbolt.Tx, name chain.Name, target policyen
}
}
if target.Type == policyengine.Namespace && target.Name == "" {
target.Name = "root"
}
rbucket := typeBucket.Bucket([]byte(target.Name))
if rbucket == nil {
var err error

View file

@ -33,16 +33,17 @@ func (c *Client) GetSubject(addr util.Uint160) (*frostfsidclient.Subject, error)
return subj, nil
}
// parseSubject from https://git.frostfs.info/TrueCloudLab/frostfs-contract/src/commit/dd5919348da9731f24504e7bc485516c2ba5f11c/frostfsid/client/client.go#L592
func parseSubject(structArr []stackitem.Item) (*frostfsidclient.Subject, error) {

The current implementation does not work - I was obliged to slightly fix it

The current implementation does not work - I was obliged to slightly fix it
if len(structArr) < 5 {
return nil, errors.New("invalid response subject struct")
func parseSubject(res []stackitem.Item) (*frostfsidclient.Subject, error) {
if ln := len(res); ln != 1 {
return nil, fmt.Errorf("unexpected stack item count (%s): %d", methodGetSubject, ln)
}
var (
err error
subj frostfsidclient.Subject
)
structArr, err := client.ArrayFromStackItem(res[0])
if err != nil {
return nil, fmt.Errorf("could not get item array of container (%s): %w", methodGetSubject, err)
}
var subj frostfsidclient.Subject
subj.PrimaryKey, err = unwrap.PublicKey(makeValidRes(structArr[0]))
if err != nil {

View file

@ -8,14 +8,17 @@ import (
"crypto/sha256"
"errors"
"fmt"
"strings"
"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"
"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
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"
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"
@ -25,6 +28,7 @@ import (
"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"
"github.com/nspcc-dev/neo-go/pkg/util"
)
var (
@ -35,6 +39,7 @@ var (
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")
errSubjectNotFound = errors.New("subject not found")
undefinedContainerID = cid.ID{}
)
@ -47,22 +52,29 @@ type containers interface {
Get(cid.ID) (*containercore.Container, error)
}
type frostfsidSubjectProvider interface {
GetSubject(util.Uint160) (*client.Subject, error)
}
type apeChecker struct {
router policyengine.ChainRouter
reader containers
ir ir
nm netmap.Source
frostFSIDClient frostfsidSubjectProvider
next Server
}
func NewAPEServer(router policyengine.ChainRouter, reader containers, ir ir, nm netmap.Source, srv Server) Server {
func NewAPEServer(router policyengine.ChainRouter, reader containers, ir ir, nm netmap.Source, frostFSIDClient frostfsidSubjectProvider, srv Server) Server {
return &apeChecker{
router: router,
reader: reader,
ir: ir,
next: srv,
nm: nm,
router: router,
reader: reader,
ir: ir,
next: srv,
nm: nm,
frostFSIDClient: frostFSIDClient,
}
}
@ -125,9 +137,17 @@ func (ac *apeChecker) List(ctx context.Context, req *container.ListRequest) (*co
nativeschema.PropertyKeyActorRole: role,
}
namespace, err := ac.namespaceByOwner(req.GetBody().GetOwnerID())

Shouldn't we get namespace from actor instead?

Shouldn't we get namespace from actor instead?

Although we discussed this privately with @dkirillov, I am going to describe the idea:

This may lead to a situation when resource name can be calculated incorrectly, because an actor can be out of owner's namespace

Although we discussed this privately with @dkirillov, I am going to describe the idea: This may lead to a situation when resource name can be calculated incorrectly, because an actor can be out of owner's namespace
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{
resource: &apeResource{
name: nativeschema.ResourceFormatRootContainers,
name: resourceName(namespace, ""),
props: make(map[string]string),
},
op: nativeschema.MethodListContainers,
@ -135,7 +155,7 @@ func (ac *apeChecker) List(ctx context.Context, req *container.ListRequest) (*co
}
s, found, err := ac.router.IsAllowed(apechain.Ingress,
policyengine.NewRequestTargetWithNamespace(""),
policyengine.NewRequestTargetWithNamespace(namespace),
request)
if err != nil {
return nil, err
@ -162,9 +182,17 @@ func (ac *apeChecker) Put(ctx context.Context, req *container.PutRequest) (*cont
nativeschema.PropertyKeyActorRole: role,

Should we check that the user is trying to create a container in the namespace it belongs to?
Otherwise it seems he can bypass rule that restrict any container creation in namespace1 if he belongs to namespace2

Should we check that the user is trying to create a container in the namespace it belongs to? Otherwise it seems he can bypass rule that restrict any container creation in `namespace1` if he belongs to `namespace2`

I was thinking about this scenario.
If we are putting a conatiner where a namespace cut from Zone() differs from a namespace got from FFsID contract (by OwnerID), then the check is performed on "from FFsID"-namespace anyway. When the creation is pushed to the sidechain to be processed by an inner-ring node, it performs this check and denies to create a conatiner because of different containers.

But now I think this check should be used here because using "from FFsID"-namespace is implicit and an error may seem not obvious

I was thinking about this scenario. If we are putting a conatiner where a namespace cut from `Zone()` differs from a namespace got from FFsID contract (by OwnerID), then the check is performed on "from FFsID"-namespace anyway. When the creation is pushed to the sidechain to be processed by an inner-ring node, it performs this check and denies to create a conatiner because of different containers. But now I think **this check should be used here** because using "from FFsID"-namespace is implicit and an error may seem not obvious

Actually, now I can create container in namespace1.ns zone if I use key from namespace2 and have rule that forbids any creation in namespace1. But you said that I couldn't do that. Am I missing something?

Actually, now I can create container in `namespace1.ns` zone if I use key from `namespace2` and have rule that forbids any creation in `namespace1`. But you said that I couldn't do that. Am I missing something?

Have you added a subject into frostfs-id contract?

Have you added a subject into frostfs-id contract?

BTW, I have added validation

BTW, I have added validation

Have you added a subject into frostfs-id contract

Yes

> Have you added a subject into frostfs-id contract Yes

Actually, now I can create container in namespace1.ns zone if I use key from namespace2 and have rule that forbids any creation in namespace1. But you said that I couldn't do that. Am I missing something?

Let's take this step by step.

First, we need to register ns, namespace2.ns in NNS contrace (frostfs-adm util does not support registring yet, I do it locally in mishmashed manner :)).

Then, when namespace2 exists, we need to bind an owner with this namespace.
Note "022bb4041c50d607ff871dec7e4cd7778388e0ea6849d84ccbd9aa8f32e16a8131" is got from owner in unit-test created above.

frostfs-adm morph frostfsid create-namespace --namespace namespace2 --rpc-endpoint='http://morph-chain.frostfs.devenv:30333' --alphabet-wallets='frostfs-dev-env/services/ir';

frostfs-adm morph frostfsid create-subject --namespace namespace2 --subject-key "022bb4041c50d607ff871dec7e4cd7778388e0ea6849d84ccbd9aa8f32e16a8131" --subject-name nodeowner --rpc-endpoint='http://morph-chain.frostfs.devenv:30333' --alphabet-wallets='frostfs-dev-env/services/ir';

So, let's go on:

t.Run("check restrict create container", func(t *testing.T) {
		namespaceForbid := "namespace1"

		chainRestrictObjectPut := &chain.Chain{ID: "chainRestrictContainerPut", Rules: []chain.Rule{{
			Status:    chain.AccessDenied,
			Actions:   chain.Actions{Names: []string{native.MethodPutContainer}},
			Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainers, namespaceForbid)}},
		}}}

		err = putPolicy(&controlCli, key, namespaceForbid, chainRestrictObjectPut)
		require.NoError(t, err)

		_, err := createContainer(ctx, p, ownerID, "test1", "namespace1.ns")
		require.Error(t, err)
	})

Therefore, you cannot create container in "namespace1.ns"

> Actually, now I can create container in namespace1.ns zone if I use key from namespace2 and have rule that forbids any creation in namespace1. But you said that I couldn't do that. Am I missing something? Let's take this step by step. First, we need to register `ns`, `namespace2.ns` in NNS contrace (`frostfs-adm` util does not support registring yet, I do it locally in mishmashed manner :)). Then, when `namespace2` exists, we need to bind an owner with this namespace. Note `"022bb4041c50d607ff871dec7e4cd7778388e0ea6849d84ccbd9aa8f32e16a8131"` is got from owner in unit-test created above. ```bash frostfs-adm morph frostfsid create-namespace --namespace namespace2 --rpc-endpoint='http://morph-chain.frostfs.devenv:30333' --alphabet-wallets='frostfs-dev-env/services/ir'; frostfs-adm morph frostfsid create-subject --namespace namespace2 --subject-key "022bb4041c50d607ff871dec7e4cd7778388e0ea6849d84ccbd9aa8f32e16a8131" --subject-name nodeowner --rpc-endpoint='http://morph-chain.frostfs.devenv:30333' --alphabet-wallets='frostfs-dev-env/services/ir'; ``` So, let's go on: ```go t.Run("check restrict create container", func(t *testing.T) { namespaceForbid := "namespace1" chainRestrictObjectPut := &chain.Chain{ID: "chainRestrictContainerPut", Rules: []chain.Rule{{ Status: chain.AccessDenied, Actions: chain.Actions{Names: []string{native.MethodPutContainer}}, Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainers, namespaceForbid)}}, }}} err = putPolicy(&controlCli, key, namespaceForbid, chainRestrictObjectPut) require.NoError(t, err) _, err := createContainer(ctx, p, ownerID, "test1", "namespace1.ns") require.Error(t, err) }) ``` Therefore, you cannot create container in `"namespace1.ns"`
}
namespace, err := ac.namespaceByOwner(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{
resource: &apeResource{
name: nativeschema.ResourceFormatRootContainers,
name: resourceName(namespace, ""),
props: make(map[string]string),
},
op: nativeschema.MethodPutContainer,
@ -172,7 +200,7 @@ func (ac *apeChecker) Put(ctx context.Context, req *container.PutRequest) (*cont
}
s, found, err := ac.router.IsAllowed(apechain.Ingress,
policyengine.NewRequestTargetWithNamespace(""),
policyengine.NewRequestTargetWithNamespace(namespace),
request)
if err != nil {
return nil, err
@ -251,9 +279,15 @@ func (ac *apeChecker) validateContainerBoundedOperation(containerID *refs.Contai
return err
}
namespace := ""
cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cont.Value).Zone(), ".ns")
if hasNamespace {
namespace = cntNamespace
}
request := &apeRequest{
resource: &apeResource{
name: fmt.Sprintf(nativeschema.ResourceFormatRootContainer, id.EncodeToString()),
name: resourceName(namespace, id.EncodeToString()),
props: ac.getContainerProps(cont),
},
op: op,
@ -261,7 +295,7 @@ func (ac *apeChecker) validateContainerBoundedOperation(containerID *refs.Contai
}
s, found, err := ac.router.IsAllowed(apechain.Ingress,
policyengine.NewRequestTargetWithContainer(id.EncodeToString()),
policyengine.NewRequestTarget(cntNamespace, id.EncodeToString()),
request)
if err != nil {
return err
@ -326,6 +360,19 @@ func (r *apeResource) Property(key string) string {
return r.props[key]
}
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(),
@ -514,3 +561,70 @@ func isContainerNode(nm *netmapSDK.NetMap, pk, binCnrID []byte, cont *containerc
return false, nil
}
func (ac *apeChecker) namespaceByOwner(owner *refs.OwnerID) (string, error) {
var ownerSDK user.ID
if owner == nil {
return "", fmt.Errorf("owner id is not set")
}
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(), errSubjectNotFound.Error()) {
return "", fmt.Errorf("get subject error: %w", err)
}
}
return 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

This doesn't work if we are trying to create container in container zone (aka root namespace)

This doesn't work if we are trying to create container in `container` zone (aka root namespace)

Let me check

Let me check

Sorry, I didn't get your point.
Let's consider all cases

  1. cnr.zone is empty, i.e. Zone() returns "container"; ownerIDNamespace is empty
  2. cnr.zone is "container", i.e. Zone() returns "container"; ownerIDNamespace is empty
  3. cnr.zone is namespace1.ns; ownerIDNamespace is empty
  4. cnr.zone is namespace1.ns; ownerIDNamespace is namespace1
  5. cnr.zone is namespace1.ns; ownerIDNamespace is namespace2

(1) - hasNamespace = false -> returns nil. Correct
(2) - hasNamespace = false -> returns nil. Correct
(3) - hasNamespace = true -> returns error. Correct, because frostfs-id has not bound the owner with the namespace
(4) - hasNamespace = true -> returns nil. Correct, because frostfs-id bound the owner with the namespace
(5) - hasNamespace = true -> returns error. Correct, because frostfs-id has not bound the owner with the namespace1, only namespace2

Sorry, I didn't get your point. Let's consider all cases 1. `cnr.zone` is empty, i.e. `Zone()` returns `"container"`; `ownerIDNamespace` is empty 2. `cnr.zone` is `"container"`, i.e. `Zone()` returns `"container"`; `ownerIDNamespace` is empty 3. `cnr.zone` is `namespace1.ns`; `ownerIDNamespace` is empty 4. `cnr.zone` is `namespace1.ns`; `ownerIDNamespace` is `namespace1` 5. `cnr.zone` is `namespace1.ns`; `ownerIDNamespace` is `namespace2` (1) - `hasNamespace = false` -> returns `nil`. Correct (2) - `hasNamespace = false` -> returns `nil`. Correct (3) - `hasNamespace = true` -> returns `error`. Correct, because frostfs-id has not bound the owner with the namespace (4) - `hasNamespace = true` -> returns `nil`. Correct, because frostfs-id bound the owner with the namespace (5) - `hasNamespace = true` -> returns `error`. Correct, because frostfs-id has not bound the owner with the `namespace1`, only `namespace2`

Why are the cases 1 and 2 correct? We will get no error here and fetch rules in ownerIDnamespace but if we can have denial rules in root namespace we still create container there (but shouldn't I suppose)

Why are the cases 1 and 2 correct? We will get no error [here](https://git.frostfs.info/aarifullin/frostfs-node/src/commit/7eb90e7c00fc7c2c1b6f164bffc86135fb4732cc/pkg/services/container/ape.go#L186) and fetch rules in `ownerIDnamespace` but if we can have denial rules in root namespace we still create container there (but shouldn't I suppose)

but if we can have denial rules in root namespace we still create container there

I still do not see any contradiction here. If container is being put without domain and an owner ID is not found in frostfs-id contract, then we suppose a container is created within root namespace

and fetch rules in ownerIDnamespace

And because it is empty - we fetch rules for root namespace

we can have denial rules in root namespace

If denial rules are defined for root namespace, they will work out.
This validation is rather needed to make sure a client sets correct zone (if he sets it for some reason - although, it is not required)

> but if we can have denial rules in root namespace we still create container there I still do not see any contradiction here. If container is being put without domain and an owner ID is not found in frostfs-id contract, then we suppose a container is created within root namespace > and fetch rules in ownerIDnamespace And because it is empty - we fetch rules for root namespace > we can have denial rules in root namespace If denial rules are defined for root namespace, they will work out. This validation is rather needed to make sure a client sets correct zone (if he sets it for some reason - although, it is not required)

Well, I want the following test to pass:

func TestDenyCreationContainerInRootIfActorInCustomNS(t *testing.T) {
	srv := &srvStub{
		calls: map[string]int{},
	}
	router := inmemory.NewInMemory()
	contRdr := &containerStub{
		c: map[cid.ID]*containercore.Container{},
	}
	ir := &irStub{
		keys: [][]byte{},
	}
	nm := &netmapStub{}

	cnrID, testContainer := initTestContainer(t, false)
	contRdr.c[cnrID] = &containercore.Container{Value: testContainer}

	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}},
		}},
	})
	require.NoError(t, err)

	req := initPutRequest(t, testContainer)
	ownerScriptHash := initOwnerIDScriptHash(t, testContainer)

	frostfsIDSubjectReader := &frostfsidStub{
		subjects: map[util.Uint160]*client.Subject{
			ownerScriptHash: {
				Namespace: testDomainName,
				Name:      testDomainName,
			},
		},
	}
	apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
	resp, err := apeSrv.Put(context.Background(), req)
	require.Nil(t, resp)
	var errAccessDenied *apistatus.ObjectAccessDenied
	require.ErrorAs(t, err, &errAccessDenied)
}
Well, I want the following test to pass: ```golang func TestDenyCreationContainerInRootIfActorInCustomNS(t *testing.T) { srv := &srvStub{ calls: map[string]int{}, } router := inmemory.NewInMemory() contRdr := &containerStub{ c: map[cid.ID]*containercore.Container{}, } ir := &irStub{ keys: [][]byte{}, } nm := &netmapStub{} cnrID, testContainer := initTestContainer(t, false) contRdr.c[cnrID] = &containercore.Container{Value: testContainer} 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}}, }}, }) require.NoError(t, err) req := initPutRequest(t, testContainer) ownerScriptHash := initOwnerIDScriptHash(t, testContainer) frostfsIDSubjectReader := &frostfsidStub{ subjects: map[util.Uint160]*client.Subject{ ownerScriptHash: { Namespace: testDomainName, Name: testDomainName, }, }, } apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv) resp, err := apeSrv.Put(context.Background(), req) require.Nil(t, resp) var errAccessDenied *apistatus.ObjectAccessDenied require.ErrorAs(t, err, &errAccessDenied) } ```

I have updated PR with more strict validation for namespaces for Put and List. Your test-case above is no longer actual. Please, check this out

If owner ID can be found within frostfs-id and no domain params are set in cnr before Put - container won't be created, because a client must set these params by SetDomain. Therefore, a resource name for such a container will define namespace (cannot be root so)

I have updated PR with more strict validation for namespaces for Put and List. Your test-case above is no longer actual. Please, check this out If owner ID can be found within frostfs-id and no domain params are set in cnr before Put - container won't be created, because a client must set these params by `SetDomain`. Therefore, a resource name for such a container will define namespace (cannot be root so)
}
cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cnr).Zone(), ".ns")
if hasNamespace {
if cntNamespace != ownerIDNamespace {
if ownerIDNamespace == "" {
return fmt.Errorf("invalid domain zone: no namespace is expected")
}
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
}

View file

@ -2,6 +2,7 @@ package container
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"testing"
@ -9,10 +10,12 @@ import (
"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"
session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
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"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
containertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/test"
@ -24,16 +27,25 @@ import (
"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/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require"
)
const (
testDomainName = "testdomainname"
testDomainZone = "testdomainname.ns"
)
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 put container, read namespace from frostfsID", testDenyPutContainerReadNamespaceFromFrostfsID)
t.Run("deny put container with invlaid namespace", testDenyPutContainerInvalidNamespace)
t.Run("deny list containers for owner with PK", testDenyListContainersForPK)
t.Run("deny list containers by namespace invalidation", testDenyListContainersValidationNamespaceError)
}
func testDenyGetContainerForOthers(t *testing.T) {
@ -49,7 +61,10 @@ func testDenyGetContainerForOthers(t *testing.T) {
keys: [][]byte{},
}
nm := &netmapStub{}
apeSrv := NewAPEServer(router, contRdr, ir, nm, srv)
frostfsIDSubjectReader := &frostfsidStub{
subjects: map[util.Uint160]*client.Subject{},
}
apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
contID := cidtest.ID()
testContainer := containertest.Container()
@ -122,7 +137,10 @@ func testDenySetContainerEACLForIR(t *testing.T) {
keys: [][]byte{},
}
nm := &netmapStub{}
apeSrv := NewAPEServer(router, contRdr, ir, nm, srv)
frostfsIDSubjectReader := &frostfsidStub{
subjects: map[util.Uint160]*client.Subject{},
}
apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
contID := cidtest.ID()
testContainer := containertest.Container()
@ -197,7 +215,10 @@ func testDenyGetContainerEACLForIRSessionToken(t *testing.T) {
keys: [][]byte{},
}
nm := &netmapStub{}
apeSrv := NewAPEServer(router, contRdr, ir, nm, srv)
frostfsIDSubjectReader := &frostfsidStub{
subjects: map[util.Uint160]*client.Subject{},
}
apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
contID := cidtest.ID()
testContainer := containertest.Container()
@ -283,7 +304,10 @@ func testDenyPutContainerForOthersSessionToken(t *testing.T) {
keys: [][]byte{},
}
nm := &netmapStub{}
apeSrv := NewAPEServer(router, contRdr, ir, nm, srv)
frostfsIDSubjectReader := &frostfsidStub{
subjects: map[util.Uint160]*client.Subject{},
}
apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
testContainer := containertest.Container()
@ -317,26 +341,7 @@ func testDenyPutContainerForOthersSessionToken(t *testing.T) {
})
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.ContainerSigned()
sToken.ApplyOnlyTo(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))
req := initPutRequest(t, testContainer)
resp, err := apeSrv.Put(context.Background(), req)
require.Nil(t, resp)
@ -344,6 +349,139 @@ func testDenyPutContainerForOthersSessionToken(t *testing.T) {
require.ErrorAs(t, err, &errAccessDenied)
}
func testDenyPutContainerReadNamespaceFromFrostfsID(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{}
cnrID, testContainer := initTestContainer(t, true)
contRdr.c[cnrID] = &containercore.Container{Value: testContainer}
nm.currentEpoch = 100
nm.netmaps = map[uint64]*netmap.NetMap{}
_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{
Rules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{
Names: []string{
nativeschema.MethodPutContainer,
},
},
Resources: chain.Resources{
Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName),
},
},
Condition: []chain.Condition{
{
Object: chain.ObjectRequest,
Key: nativeschema.PropertyKeyActorRole,
Value: nativeschema.PropertyValueContainerRoleOthers,
Op: chain.CondStringEquals,
},
},
},
},
})
require.NoError(t, err)
req := initPutRequest(t, testContainer)
ownerScriptHash := initOwnerIDScriptHash(t, testContainer)
frostfsIDSubjectReader := &frostfsidStub{
subjects: map[util.Uint160]*client.Subject{
ownerScriptHash: {
Namespace: testDomainName,
Name: testDomainName,
},
},
}
apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
resp, err := apeSrv.Put(context.Background(), req)
require.Nil(t, resp)
var errAccessDenied *apistatus.ObjectAccessDenied
require.ErrorAs(t, err, &errAccessDenied)
}
func testDenyPutContainerInvalidNamespace(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{}
cnrID, testContainer := initTestContainer(t, false)
var domain cnrSDK.Domain
domain.SetName("incorrect" + testDomainName)
domain.SetZone("incorrect" + testDomainZone)
cnrSDK.WriteDomain(&testContainer, domain)
contRdr.c[cnrID] = &containercore.Container{Value: testContainer}
nm.currentEpoch = 100
nm.netmaps = map[uint64]*netmap.NetMap{}
_, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{
Rules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{
Names: []string{
nativeschema.MethodPutContainer,
},
},
Resources: chain.Resources{
Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName),
},
},
Condition: []chain.Condition{
{
Object: chain.ObjectRequest,
Key: nativeschema.PropertyKeyActorRole,
Value: nativeschema.PropertyValueContainerRoleOthers,
Op: chain.CondStringEquals,
},
},
},
},
})
require.NoError(t, err)
req := initPutRequest(t, testContainer)
ownerScriptHash := initOwnerIDScriptHash(t, testContainer)
frostfsIDSubjectReader := &frostfsidStub{
subjects: map[util.Uint160]*client.Subject{
ownerScriptHash: {
Namespace: testDomainName,
Name: testDomainName,
},
},
}
apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
resp, err := apeSrv.Put(context.Background(), req)
require.Nil(t, resp)
require.ErrorContains(t, err, "invalid domain zone")
}
func testDenyListContainersForPK(t *testing.T) {
t.Parallel()
srv := &srvStub{
@ -357,7 +495,10 @@ func testDenyListContainersForPK(t *testing.T) {
keys: [][]byte{},
}
nm := &netmapStub{}
apeSrv := NewAPEServer(router, contRdr, ir, nm, srv)
frostfsIDSubjectReader := &frostfsidStub{
subjects: map[util.Uint160]*client.Subject{},
}
apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
nm.currentEpoch = 100
nm.netmaps = map[uint64]*netmap.NetMap{}
@ -409,6 +550,82 @@ func testDenyListContainersForPK(t *testing.T) {
require.ErrorAs(t, err, &errAccessDenied)
}
func testDenyListContainersValidationNamespaceError(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{}
actorPK, err := keys.NewPrivateKey()
require.NoError(t, err)
ownerPK, err := keys.NewPrivateKey()
require.NoError(t, err)
actorScriptHash, ownerScriptHash := initActorOwnerScriptHashes(t, actorPK, ownerPK)
const actorDomain = "actor" + testDomainName
frostfsIDSubjectReader := &frostfsidStub{
subjects: map[util.Uint160]*client.Subject{
actorScriptHash: {
Namespace: actorDomain,
Name: actorDomain,
},
ownerScriptHash: {
Namespace: testDomainName,
Name: testDomainName,
},
},
}
apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv)
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.MethodListContainers,
},
},
Resources: chain.Resources{
Names: []string{
nativeschema.ResourceFormatRootContainers,
},
},
Condition: []chain.Condition{
{
Object: chain.ObjectRequest,
Key: nativeschema.PropertyKeyActorPublicKey,
Value: actorPK.PublicKey().String(),
Op: chain.CondStringEquals,
},
},
},
},
})
require.NoError(t, err)
req := initListRequest(t, actorPK, ownerPK)
resp, err := apeSrv.List(context.Background(), req)
require.Nil(t, resp)
require.ErrorContains(t, err, "actor namespace "+actorDomain+" differs")
}
type srvStub struct {
calls map[string]int
}
@ -489,3 +706,424 @@ func (s *netmapStub) GetNetMapByEpoch(epoch uint64) (*netmap.NetMap, error) {
func (s *netmapStub) Epoch() (uint64, error) {
return s.currentEpoch, nil
}
type frostfsidStub struct {
subjects map[util.Uint160]*client.Subject
}
func (f *frostfsidStub) GetSubject(owner util.Uint160) (*client.Subject, error) {
s, ok := f.subjects[owner]
if !ok {
return nil, errSubjectNotFound
}
return s, nil
}
type testAPEServer struct {
engine engine.Engine
containerReader *containerStub
ir *irStub
netmap *netmapStub
frostfsIDSubjectReader *frostfsidStub
apeChecker *apeChecker
}
func newTestAPEServer() testAPEServer {
srv := &srvStub{
calls: map[string]int{},
}
engine := inmemory.NewInMemory()
containerReader := &containerStub{
c: map[cid.ID]*containercore.Container{},
}
ir := &irStub{
keys: [][]byte{},
}
netmap := &netmapStub{}
frostfsIDSubjectReader := &frostfsidStub{
subjects: map[util.Uint160]*client.Subject{},
}
apeChecker := &apeChecker{
router: engine,
reader: containerReader,
ir: ir,
nm: netmap,
frostFSIDClient: frostfsIDSubjectReader,
next: srv,
}
return testAPEServer{
engine: engine,
containerReader: containerReader,
ir: ir,
netmap: netmap,
frostfsIDSubjectReader: frostfsIDSubjectReader,
apeChecker: apeChecker,
}
}
func TestValidateContainerBoundedOperation(t *testing.T) {
t.Parallel()
t.Run("check root-defined container in root-defined container target rule", func(t *testing.T) {
t.Parallel()
components := newTestAPEServer()
contID, testContainer := initTestContainer(t, false)
components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
initTestNetmap(components.netmap)
_, _, err := components.engine.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 := initTestGetContainerRequest(t, contID)
err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
aErr := apeErr(nativeschema.MethodGetContainer, chain.AccessDenied)
require.ErrorContains(t, err, aErr.Error())
})
t.Run("check root-defined container in testdomain-defined container target rule", func(t *testing.T) {
t.Parallel()
components := newTestAPEServer()
contID, testContainer := initTestContainer(t, false)
components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
initTestNetmap(components.netmap)
_, _, err := components.engine.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.ResourceFormatNamespaceContainer, testDomainName, contID.EncodeToString()),
},
},
Condition: []chain.Condition{
{
Object: chain.ObjectRequest,
Key: nativeschema.PropertyKeyActorRole,
Value: nativeschema.PropertyValueContainerRoleOthers,
Op: chain.CondStringEquals,
},
},
},
},
})
require.NoError(t, err)
req := initTestGetContainerRequest(t, contID)
err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
require.NoError(t, err)
})
t.Run("check root-defined container in testdomain namespace target rule", func(t *testing.T) {
t.Parallel()
components := newTestAPEServer()
contID, testContainer := initTestContainer(t, false)
components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
initTestNetmap(components.netmap)
_, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{
Rules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{
Names: []string{
nativeschema.MethodGetContainer,
},
},
Resources: chain.Resources{
Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName),
},
},
Condition: []chain.Condition{
{
Object: chain.ObjectRequest,
Key: nativeschema.PropertyKeyActorRole,
Value: nativeschema.PropertyValueContainerRoleOthers,
Op: chain.CondStringEquals,
},
},
},
},
})
require.NoError(t, err)
req := initTestGetContainerRequest(t, contID)
err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
require.NoError(t, err)
})
t.Run("check testdomain-defined container in root-defined container target rule", func(t *testing.T) {
t.Parallel()
components := newTestAPEServer()
contID, testContainer := initTestContainer(t, true)
components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
initTestNetmap(components.netmap)
_, _, err := components.engine.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 := initTestGetContainerRequest(t, contID)
err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
require.NoError(t, err)
})
t.Run("check testdomain-defined container in testdomain-defined container target rule", func(t *testing.T) {
t.Parallel()
components := newTestAPEServer()
contID, testContainer := initTestContainer(t, true)
components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
initTestNetmap(components.netmap)
_, _, err := components.engine.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.ResourceFormatNamespaceContainer, testDomainName, contID.EncodeToString()),
},
},
Condition: []chain.Condition{
{
Object: chain.ObjectRequest,
Key: nativeschema.PropertyKeyActorRole,
Value: nativeschema.PropertyValueContainerRoleOthers,
Op: chain.CondStringEquals,
},
},
},
},
})
require.NoError(t, err)
req := initTestGetContainerRequest(t, contID)
err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
aErr := apeErr(nativeschema.MethodGetContainer, chain.AccessDenied)
require.ErrorContains(t, err, aErr.Error())
fyrchik marked this conversation as resolved Outdated

These 5 lines create dummy request and repeat in every test, can we move them to a function to make tests a bit more readable?

These 5 lines create dummy request and repeat in every test, can we move them to a function to make tests a bit more readable?

It seems private key is used only to sign the request, probably a good idea to return already signed request from this function.

It seems private key is used only to sign the request, probably a good idea to return already signed request from this function.
fyrchik marked this conversation as resolved Outdated

These 5 lines create dummy request and repeat in every test, can we move them to a function to make tests a bit more readable?

These 5 lines create dummy request and repeat in every test, can we move them to a function to make tests a bit more readable?
aarifullin marked this conversation as resolved Outdated

These 5 lines create dummy request and repeat in every test, can we move them to a function to make tests a bit more readable?

These 5 lines create dummy request and repeat in every test, can we move them to a function to make tests a bit more readable?

Have fixed for all cases :)

Have fixed for all cases :)

Hm, either I don't see it or you haven't pushed the update?

Hm, either I don't see it or you haven't pushed the update?

Sorry, I forgot to push yesterday. Now it is fine

Sorry, I forgot to push yesterday. Now it is fine
})
t.Run("check testdomain-defined container in testdomain namespace target rule", func(t *testing.T) {
t.Parallel()
components := newTestAPEServer()
contID, testContainer := initTestContainer(t, true)
components.containerReader.c[contID] = &containercore.Container{Value: testContainer}
initTestNetmap(components.netmap)
_, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{
Rules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{
Names: []string{
nativeschema.MethodGetContainer,
},
},
Resources: chain.Resources{
Names: []string{
fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName),
},
},
Condition: []chain.Condition{
{
Object: chain.ObjectRequest,
Key: nativeschema.PropertyKeyActorRole,
Value: nativeschema.PropertyValueContainerRoleOthers,
Op: chain.CondStringEquals,
},
},
},
},
})
require.NoError(t, err)
req := initTestGetContainerRequest(t, contID)
err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer)
aErr := apeErr(nativeschema.MethodGetContainer, chain.AccessDenied)
require.ErrorContains(t, err, aErr.Error())
})
}
func initTestGetContainerRequest(t *testing.T, contID cid.ID) *container.GetRequest {
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))
return req
}
func initTestNetmap(netmapStub *netmapStub) {
netmapStub.currentEpoch = 100
netmapStub.netmaps = map[uint64]*netmap.NetMap{}
var testNetmap netmap.NetMap
testNetmap.SetEpoch(netmapStub.currentEpoch)
testNetmap.SetNodes([]netmap.NodeInfo{{}})
netmapStub.netmaps[netmapStub.currentEpoch] = &testNetmap
netmapStub.netmaps[netmapStub.currentEpoch-1] = &testNetmap
}
func initTestContainer(t *testing.T, isDomainSet bool) (cid.ID, cnrSDK.Container) {
contID := cidtest.ID()
testContainer := containertest.Container()
pp := netmap.PlacementPolicy{}
require.NoError(t, pp.DecodeString("REP 1"))
testContainer.SetPlacementPolicy(pp)
if isDomainSet {
// no domain defined -> container is defined in root namespace
var domain cnrSDK.Domain
domain.SetName(testDomainName)
domain.SetZone(testDomainZone)
cnrSDK.WriteDomain(&testContainer, domain)
}
return contID, testContainer
}
func initPutRequest(t *testing.T, testContainer cnrSDK.Container) *container.PutRequest {
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.ContainerSigned()
sToken.ApplyOnlyTo(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))
return req
}
func initOwnerIDScriptHash(t *testing.T, testContainer cnrSDK.Container) util.Uint160 {
var ownerSDK *user.ID
owner := testContainer.Owner()
ownerSDK = &owner
sc, err := ownerSDK.ScriptHash()
require.NoError(t, err)
return sc
}
func initActorOwnerScriptHashes(t *testing.T, actorPK *keys.PrivateKey, ownerPK *keys.PrivateKey) (actorScriptHash util.Uint160, ownerScriptHash util.Uint160) {
var actorUserID user.ID
user.IDFromKey(&actorUserID, ecdsa.PublicKey(*actorPK.PublicKey()))
var err error
actorScriptHash, err = actorUserID.ScriptHash()
require.NoError(t, err)
var ownerUserID user.ID
user.IDFromKey(&ownerUserID, ecdsa.PublicKey(*ownerPK.PublicKey()))
ownerScriptHash, err = ownerUserID.ScriptHash()
require.NoError(t, err)
require.NotEqual(t, ownerScriptHash.String(), actorScriptHash.String())
return
}
func initListRequest(t *testing.T, actorPK *keys.PrivateKey, ownerPK *keys.PrivateKey) *container.ListRequest {
var ownerUserID user.ID
user.IDFromKey(&ownerUserID, ownerPK.PrivateKey.PublicKey)
req := &container.ListRequest{}
req.SetBody(&container.ListRequestBody{})
var ownerID refs.OwnerID
ownerUserID.WriteToV2(&ownerID)
req.GetBody().SetOwnerID(&ownerID)
require.NoError(t, signature.SignServiceMessage(&actorPK.PrivateKey, req))
return req
}

View file

@ -17,12 +17,7 @@ func apeTarget(chainTarget *control.ChainTarget) (engine.Target, error) {
case control.ChainTarget_CONTAINER:
return engine.ContainerTarget(chainTarget.GetName()), nil
case control.ChainTarget_NAMESPACE:
namespace := chainTarget.GetName()
// If namespace is empty, we take it for root namespace.
if namespace == "" {
namespace = "root"
}
return engine.NamespaceTarget(namespace), nil
return engine.NamespaceTarget(chainTarget.GetName()), nil
default:
}
return engine.Target{}, status.Error(codes.InvalidArgument,

View file

@ -4,8 +4,8 @@ import (
"context"
"errors"
"fmt"
"strings"
containerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
@ -14,6 +14,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
cnrSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
@ -744,7 +745,11 @@ func (b Service) findRequestInfo(req MetaWithToken, idCnr cid.ID, op acl.Op) (in
info.operation = op
info.cnrOwner = cnr.Value.Owner()
info.idCnr = idCnr
info.cnrNamespace = cnr.Value.Attribute(containerV2.SysAttributeZone)
cnrNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cnr.Value).Zone(), ".ns")
if hasNamespace {
info.cnrNamespace = cnrNamespace
}
// it is assumed that at the moment the key will be valid,
// otherwise the request would not pass validation