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
Collaborator
  • Transform empty namespace within chainbase, not in control service like it was before.
  • Slightly fix adm util to read alphabet wallets in frostfs-id commands
  • Make container APE middleware read namespaces, because they have been passed as empty (-> root):
    -- Those methods that can access already existing containers and thus can get container properties should read namespace from Zone property. If Zone is not set, take a namespace for root.
    -- Otherwise, define namespaces by owner ID via frostfs-id contract.
    -- Improve unit-tests, consider more cases.
* Transform empty namespace within chainbase, not in control service like it was before. * Slightly fix adm util to read alphabet wallets in frostfs-id commands * Make container APE middleware read namespaces, because they have been passed as empty (-> root): -- Those methods that can access already existing containers and thus can get container properties should read namespace from Zone property. If Zone is not set, take a namespace for root. -- Otherwise, define namespaces by owner ID via frostfs-id contract. -- Improve unit-tests, consider more cases.
aarifullin requested review from dstepanov-yadro 2024-01-29 21:12:35 +00:00
aarifullin requested review from dkirillov 2024-01-29 21:12:50 +00:00
aarifullin requested review from storage-core-committers 2024-01-29 21:12:51 +00:00
aarifullin requested review from storage-core-developers 2024-01-29 21:13:02 +00:00
aarifullin reviewed 2024-01-29 21:18:15 +00:00
@ -34,4 +34,2 @@
}
// 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) {
Poster
Collaborator

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
acid-ant approved these changes 2024-01-30 07:12:50 +00:00
fyrchik approved these changes 2024-01-30 08:05:13 +00:00
@ -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.
aarifullin changed title from ape, adm, container: Form APE requests properly to WIP: ape, adm, container: Form APE requests properly 2024-01-30 08:07:02 +00:00
fyrchik reviewed 2024-01-30 08:08:41 +00:00
@ -492,0 +980,4 @@
})
require.NoError(t, err)
req := &container.GetRequest{}

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
fyrchik reviewed 2024-01-30 08:08:46 +00:00
@ -492,0 +980,4 @@
})
require.NoError(t, err)
req := &container.GetRequest{}

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?
fyrchik marked this conversation as resolved
fyrchik reviewed 2024-01-30 08:08:48 +00:00
@ -492,0 +980,4 @@
})
require.NoError(t, err)
req := &container.GetRequest{}

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?
Poster
Collaborator

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?
Poster
Collaborator

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

Sorry, I forgot to push yesterday. Now it is fine
aarifullin marked this conversation as resolved
dstepanov-yadro approved these changes 2024-01-30 10:02:14 +00:00
aarifullin force-pushed fix/934-invalid_ape_request from 2d2ab42e83 to 6b342c34ec 2024-01-30 15:24:23 +00:00 Compare
aarifullin changed title from WIP: ape, adm, container: Form APE requests properly to ape, adm, container: Form APE requests properly 2024-01-30 15:24:39 +00:00
dstepanov-yadro approved these changes 2024-01-30 15:42:05 +00:00
dkirillov reviewed 2024-01-31 09:05:08 +00:00
@ -162,9 +179,14 @@ func (ac *apeChecker) Put(ctx context.Context, req *container.PutRequest) (*cont
nativeschema.PropertyKeyActorRole: role,
}
namespace, err := ac.namespaceByOwner(req.GetBody().GetContainer().GetOwnerID())

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`
Poster
Collaborator

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?
Poster
Collaborator

Have you added a subject into frostfs-id contract?

Have you added a subject into frostfs-id contract?
Poster
Collaborator

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
Poster
Collaborator

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"`
aarifullin force-pushed fix/934-invalid_ape_request from 6b342c34ec to dd86e88a57 2024-01-31 09:06:21 +00:00 Compare
dkirillov reviewed 2024-01-31 09:26:00 +00:00
@ -125,9 +137,14 @@ 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?
Poster
Collaborator

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
dkirillov reviewed 2024-01-31 09:43:33 +00:00
@ -116,2 +116,4 @@
}
if target.Type == policyengine.Namespace && target.Name == "" {
target.Name = "root"

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?
Poster
Collaborator

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
aarifullin force-pushed fix/934-invalid_ape_request from dd86e88a57 to 7eb90e7c00 2024-01-31 10:32:45 +00:00 Compare
fyrchik approved these changes 2024-01-31 10:56:04 +00:00
dkirillov reviewed 2024-01-31 11:26:39 +00:00
@ -517,0 +596,4 @@
return err
}
cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cnr).Zone(), ".ns")
if hasNamespace && cntNamespace != ownerIDNamespace {

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)
Poster
Collaborator

Let me check

Let me check
Poster
Collaborator

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)
Poster
Collaborator

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) } ```
Poster
Collaborator

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)
aarifullin force-pushed fix/934-invalid_ape_request from 7eb90e7c00 to 8fd5d3b8cc 2024-01-31 13:21:55 +00:00 Compare
aarifullin requested review from dstepanov-yadro 2024-01-31 13:22:52 +00:00
aarifullin requested review from acid-ant 2024-01-31 13:22:53 +00:00
aarifullin requested review from fyrchik 2024-01-31 13:22:54 +00:00
aarifullin force-pushed fix/934-invalid_ape_request from 8fd5d3b8cc to 0587fce97a 2024-02-01 12:22:35 +00:00 Compare
dkirillov approved these changes 2024-02-01 14:32:56 +00:00
fyrchik approved these changes 2024-02-01 17:38:16 +00:00
fyrchik merged commit 5be2af881a into master 2024-02-01 17:38:27 +00:00
Sign in to join this conversation.
No reviewers
TrueCloudLab/storage-core-developers
No Milestone
No Assignees
5 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: TrueCloudLab/frostfs-node#940
There is no content yet.