package container import ( "context" "crypto/ecdsa" "encoding/hex" "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" 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" frostfsidcore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/frostfsid" 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" "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" commonschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/common" nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "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("allow then deny get container", testAllowThenDenyGetContainerRuleDefined) t.Run("deny get container no rule found", testDenyGetContainerNoRuleFound) t.Run("deny get container for others", testDenyGetContainerForOthers) t.Run("deny get container by user claim tag", testDenyGetContainerByUserClaimTag) 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 testAllowThenDenyGetContainerRuleDefined(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{} frostfsIDSubjectReader := &frostfsidStub{ subjects: map[util.Uint160]*client.Subject{}, } apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, 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 addDefaultAllowGetPolicy(t, router, contID) 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)) _, err = apeSrv.Get(context.Background(), req) require.NoError(t, err) _, _, 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()), }, }, }, }, }) require.NoError(t, err) resp, err := apeSrv.Get(context.Background(), req) require.Nil(t, resp) var errAccessDenied *apistatus.ObjectAccessDenied require.ErrorAs(t, err, &errAccessDenied) require.Contains(t, errAccessDenied.Reason(), chain.AccessDenied.String()) } func testDenyGetContainerNoRuleFound(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{} frostfsIDSubjectReader := &frostfsidStub{ subjects: map[util.Uint160]*client.Subject{}, } apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, 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 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) require.Contains(t, errAccessDenied.Reason(), chain.NoRuleFound.String()) } 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{} frostfsIDSubjectReader := &frostfsidStub{ subjects: map[util.Uint160]*client.Subject{}, } apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, 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 testDenyGetContainerByUserClaimTag(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{} pk, err := keys.NewPrivateKey() require.NoError(t, err) frostfsIDSubjectReader := &frostfsidStub{ subjects: map[util.Uint160]*client.Subject{ pk.PublicKey().GetScriptHash(): { KV: map[string]string{ "tag-attr1": "value1", "tag-attr2": "value2", }, }, }, } apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, 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: fmt.Sprintf(commonschema.PropertyKeyFormatFrostFSIDUserClaim, "tag-attr1"), Value: "value100", Op: chain.CondStringNotEquals, }, }, }, }, }) require.NoError(t, err) req := &container.GetRequest{} req.SetBody(&container.GetRequestBody{}) var refContID refs.ContainerID contID.WriteToV2(&refContID) req.GetBody().SetContainerID(&refContID) 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{} frostfsIDSubjectReader := &frostfsidStub{ subjects: map[util.Uint160]*client.Subject{}, } apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, 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{} frostfsIDSubjectReader := &frostfsidStub{ subjects: map[util.Uint160]*client.Subject{}, } apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, 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.ContainerSigned() sToken.ApplyOnlyTo(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{} frostfsIDSubjectReader := &frostfsidStub{ subjects: map[util.Uint160]*client.Subject{}, } apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, 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 := initPutRequest(t, testContainer) resp, err := apeSrv.Put(context.Background(), req) require.Nil(t, resp) var errAccessDenied *apistatus.ObjectAccessDenied 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{ calls: map[string]int{}, } router := inmemory.NewInMemory() contRdr := &containerStub{ c: map[cid.ID]*containercore.Container{}, } ir := &irStub{ keys: [][]byte{}, } nm := &netmapStub{} 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{} 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: hex.EncodeToString(pk.PublicKey().Bytes()), 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) } 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 } 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 } type frostfsidStub struct { subjects map[util.Uint160]*client.Subject subjectsExt map[util.Uint160]*client.SubjectExtended } func (f *frostfsidStub) GetSubject(owner util.Uint160) (*client.Subject, error) { s, ok := f.subjects[owner] if !ok { return nil, fmt.Errorf("%s", frostfsidcore.SubjectNotFoundErrorMessage) } return s, nil } func (f *frostfsidStub) GetSubjectExtended(owner util.Uint160) (*client.SubjectExtended, error) { s, ok := f.subjectsExt[owner] if !ok { return nil, fmt.Errorf("%s", frostfsidcore.SubjectNotFoundErrorMessage) } 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{}, subjectsExt: map[util.Uint160]*client.SubjectExtended{}, } 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) addDefaultAllowGetPolicy(t, components.engine, contID) 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) addDefaultAllowGetPolicy(t, components.engine, contID) 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) addDefaultAllowGetPolicy(t, components.engine, contID) 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) addDefaultAllowGetPolicy(t, components.engine, contID) 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 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 } func addDefaultAllowGetPolicy(t *testing.T, e engine.Engine, contID cid.ID) { _, _, err := e.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{ Rules: []chain.Rule{ { Status: chain.Allow, Actions: chain.Actions{ Names: []string{ nativeschema.MethodGetContainer, }, }, Resources: chain.Resources{ Names: []string{ nativeschema.ResourceFormatAllContainers, }, }, }, }, }) require.NoError(t, err) }