package container import ( "context" "crypto/ecdsa" "encoding/hex" "errors" "fmt" "net" "testing" "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" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs" session "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/session" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/signature" 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" "google.golang.org/grpc/peer" ) const ( testDomainName = "testdomainname" testDomainZone = "testdomainname.ns" ) func TestAPE(t *testing.T) { t.Parallel() t.Run("allow then deny get container", testAllowThenDenyGetContainerRuleDefined) t.Run("allow by group id", TestAllowByGroupIDs) 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 get container by IP", testDenyGetContainerByIP) t.Run("deny get container by group id", testDenyGetContainerByGroupID) 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) } const ( incomingIP = "192.92.33.1" ) func ctxWithPeerInfo() context.Context { return peer.NewContext(context.Background(), &peer.Peer{ Addr: &net.TCPAddr{ IP: net.ParseIP(incomingIP), Port: 41111, }, }) } 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 TestAllowByGroupIDs(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", }, }, }, subjectsExt: map[util.Uint160]*client.SubjectExtended{ pk.PublicKey().GetScriptHash(): { KV: map[string]string{ "tag-attr1": "value1", "tag-attr2": "value2", }, Groups: []*client.Group{ { ID: 1, Name: "Group#1", }, }, }, }, } 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) require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req)) _, _, err = router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.GroupTarget(":1"), &chain.Chain{ Rules: []chain.Rule{ { Status: chain.Allow, Actions: chain.Actions{ Names: []string{ nativeschema.MethodGetContainer, }, }, Resources: chain.Resources{ Names: []string{ fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()), }, }, Condition: []chain.Condition{ { Kind: chain.KindRequest, Key: commonschema.PropertyKeyFrostFSIDGroupID, Value: "1", Op: chain.CondStringEquals, }, }, }, }, }) require.NoError(t, err) resp, err := apeSrv.Get(context.Background(), req) require.NotNil(t, resp) require.NoError(t, err) } 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{ { Kind: chain.KindRequest, 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", }, }, }, subjectsExt: map[util.Uint160]*client.SubjectExtended{ pk.PublicKey().GetScriptHash(): { KV: map[string]string{ "tag-attr1": "value1", "tag-attr2": "value2", }, Groups: []*client.Group{ { ID: 19888, }, }, }, }, } 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{ { Kind: chain.KindRequest, 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 testDenyGetContainerByIP(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", }, }, }, subjectsExt: map[util.Uint160]*client.SubjectExtended{ pk.PublicKey().GetScriptHash(): { KV: map[string]string{ "tag-attr1": "value1", "tag-attr2": "value2", }, Groups: []*client.Group{ { ID: 19888, }, }, }, }, } 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{ { Kind: chain.KindRequest, Key: commonschema.PropertyKeyFrostFSSourceIP, Value: incomingIP + "/16", Op: chain.CondIPAddress, }, }, }, }, }) 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(ctxWithPeerInfo(), req) require.Nil(t, resp) var errAccessDenied *apistatus.ObjectAccessDenied require.ErrorAs(t, err, &errAccessDenied) require.Contains(t, errAccessDenied.Reason(), chain.AccessDenied.String()) } func testDenyGetContainerByGroupID(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", }, }, }, subjectsExt: map[util.Uint160]*client.SubjectExtended{ pk.PublicKey().GetScriptHash(): { KV: map[string]string{ "tag-attr1": "value1", "tag-attr2": "value2", }, Groups: []*client.Group{ { ID: 19888, }, }, }, }, } 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{ { Kind: chain.KindRequest, Key: commonschema.PropertyKeyFrostFSIDGroupID, Value: "19888", 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) 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 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{} testContainer := containertest.Container() owner := testContainer.Owner() ownerAddr, err := owner.ScriptHash() require.NoError(t, err) frostfsIDSubjectReader := &frostfsidStub{ subjects: map[util.Uint160]*client.Subject{ ownerAddr: {}, }, } 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.MethodPutContainer, }, }, Resources: chain.Resources{ Names: []string{ nativeschema.ResourceFormatRootContainers, }, }, Condition: []chain.Condition{ { Kind: chain.KindRequest, 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{ { Kind: chain.KindRequest, 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, }, }, subjectsExt: map[util.Uint160]*client.SubjectExtended{ ownerScriptHash: { Namespace: testDomainName, Name: testDomainName, KV: map[string]string{ "tag-attr1": "value1", "tag-attr2": "value2", }, Groups: []*client.Group{ { ID: 19888, }, }, }, }, } 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{ { Kind: chain.KindRequest, 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, }, }, subjectsExt: map[util.Uint160]*client.SubjectExtended{ ownerScriptHash: { Namespace: testDomainName, Name: testDomainName, KV: map[string]string{ "tag-attr1": "value1", "tag-attr2": "value2", }, Groups: []*client.Group{ { ID: 19888, }, }, }, }, } 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{ { Kind: chain.KindRequest, 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, }, }, subjectsExt: map[util.Uint160]*client.SubjectExtended{ actorScriptHash: { Namespace: actorDomain, Name: actorDomain, KV: map[string]string{ "tag-attr1": "value1", "tag-attr2": "value2", }, Groups: []*client.Group{ { ID: 19777, }, }, }, ownerScriptHash: { Namespace: testDomainName, Name: testDomainName, KV: map[string]string{ "tag-attr1": "value1", "tag-attr2": "value2", }, Groups: []*client.Group{ { ID: 19888, }, }, }, }, } 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{ { Kind: chain.KindRequest, 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) 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) 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 } 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{ { Kind: chain.KindRequest, Key: nativeschema.PropertyKeyActorRole, Value: nativeschema.PropertyValueContainerRoleOthers, Op: chain.CondStringEquals, }, }, }, }, }) require.NoError(t, err) req := initTestGetContainerRequest(t, contID) err = components.apeChecker.validateContainerBoundedOperation(ctxWithPeerInfo(), 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{ { Kind: chain.KindRequest, 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(ctxWithPeerInfo(), 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{ { Kind: chain.KindRequest, 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(ctxWithPeerInfo(), 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{ { Kind: chain.KindRequest, 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(ctxWithPeerInfo(), 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{ { Kind: chain.KindRequest, 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(ctxWithPeerInfo(), 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{ { Kind: chain.KindRequest, Key: nativeschema.PropertyKeyActorRole, Value: nativeschema.PropertyValueContainerRoleOthers, Op: chain.CondStringEquals, }, }, }, }, }) require.NoError(t, err) req := initTestGetContainerRequest(t, contID) err = components.apeChecker.validateContainerBoundedOperation(ctxWithPeerInfo(), 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) }