package inmemory import ( "bytes" "testing" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" resourcetest "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) func TestAddRootOverrides(t *testing.T) { s := NewInMemoryLocalOverrides() target := engine.NamespaceTarget("") id, err := s.LocalStorage().AddOverride(chain.S3, target, &chain.Chain{ Rules: []chain.Rule{{ Status: chain.Allow, Actions: chain.Actions{Names: []string{"s3:PutObject"}}, Resources: chain.Resources{Names: []string{"*"}}, }}, }) require.NoError(t, err) res, err := s.LocalStorage().ListOverrides(chain.S3, target) require.NoError(t, err) require.Len(t, res, 1) require.Equal(t, string(id), string(res[0].ID)) } func TestInmemory(t *testing.T) { const ( object = "native::object::abc/xyz" container = "native::object::abc/*" namespace = "Tenant1" namespace2 = "Tenant2" actor1 = "owner1" actor2 = "owner2" ) s := NewInMemoryLocalOverrides() // Object which was put via S3. res := resourcetest.NewResource(object, map[string]string{"FromS3": "true"}) // Request initiating from the trusted subnet and actor. reqGood := resourcetest.NewRequest("native::object::put", res, map[string]string{ "SourceIP": "10.1.1.12", "Actor": actor1, }) status, ok, _ := s.IsAllowed(chain.Ingress, engine.NewRequestTargetWithNamespace(namespace), reqGood) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace), &chain.Chain{ Rules: []chain.Rule{ { // Restrict to remove ANY object from the namespace. Status: chain.AccessDenied, Actions: chain.Actions{Names: []string{"native::object::delete"}}, Resources: chain.Resources{Names: []string{"native::object::*"}}, }, { // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise. Status: chain.AccessDenied, Actions: chain.Actions{Names: []string{"native::object::put"}}, Resources: chain.Resources{Names: []string{"native::object::*"}}, Any: true, Condition: []chain.Condition{ { Op: chain.CondStringNotLike, Object: chain.ObjectRequest, Key: "SourceIP", Value: "10.1.1.*", }, { Op: chain.CondStringNotEquals, Object: chain.ObjectRequest, Key: "Actor", Value: actor1, }, }, }, }, }) _, it, err := s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) require.NoError(t, err) itemStacksEqual(t, it.Values, toStackItems(namespace)) s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{ Rules: []chain.Rule{ { // Deny all expect "native::object::get" for all objects expect "native::object::abc/xyz". Status: chain.AccessDenied, Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}}, Resources: chain.Resources{Inverted: true, Names: []string{object}}, }, }, }) _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) require.NoError(t, err) itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2)) s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(container), &chain.Chain{ Rules: []chain.Rule{ { // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute. Status: chain.Allow, Actions: chain.Actions{Names: []string{"native::object::get"}}, Resources: chain.Resources{Names: []string{"native::object::abc/*"}}, Condition: []chain.Condition{ { Op: chain.CondStringEquals, Object: chain.ObjectResource, Key: "Department", Value: "HR", }, { Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: "Actor", Value: actor2, }, }, }, }, }) _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) require.NoError(t, err) itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2)) _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container) require.NoError(t, err) itemStacksEqual(t, it.Values, toStackItems(container)) t.Run("bad subnet, namespace deny", func(t *testing.T) { // Request initiating from the untrusted subnet. reqBadIP := resourcetest.NewRequest("native::object::put", res, map[string]string{ "SourceIP": "10.122.1.20", "Actor": actor1, }) status, ok, _ := s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqBadIP) require.Equal(t, chain.AccessDenied, status) require.True(t, ok) }) t.Run("bad actor, namespace deny", func(t *testing.T) { // Request initiating from the untrusted actor. reqBadActor := resourcetest.NewRequest("native::object::put", res, map[string]string{ "SourceIP": "10.1.1.13", "Actor": actor2, }) status, ok, _ := s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqBadActor) require.Equal(t, chain.AccessDenied, status) require.True(t, ok) }) t.Run("bad object, container deny", func(t *testing.T) { objGood := resourcetest.NewResource("native::object::abc/id1", map[string]string{"Department": "HR"}) objBadAttr := resourcetest.NewResource("native::object::abc/id2", map[string]string{"Department": "Support"}) status, ok, _ := s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), resourcetest.NewRequest("native::object::get", objGood, map[string]string{ "SourceIP": "10.1.1.14", "Actor": actor2, })) require.Equal(t, chain.Allow, status) require.True(t, ok) status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), resourcetest.NewRequest("native::object::get", objBadAttr, map[string]string{ "SourceIP": "10.1.1.14", "Actor": actor2, })) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) }) t.Run("bad operation, namespace deny", func(t *testing.T) { // Request with the forbidden operation. reqBadOperation := resourcetest.NewRequest("native::object::delete", res, map[string]string{ "SourceIP": "10.1.1.12", "Actor": actor1, }) status, ok, _ := s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqBadOperation) require.Equal(t, chain.AccessDenied, status) require.True(t, ok) }) t.Run("inverted rules", func(t *testing.T) { req := resourcetest.NewRequest("native::object::put", resourcetest.NewResource(object, nil), nil) status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace2, container), req) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) req = resourcetest.NewRequest("native::object::put", resourcetest.NewResource("native::object::cba/def", nil), nil) status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace2, container), req) require.Equal(t, chain.AccessDenied, status) require.True(t, ok) req = resourcetest.NewRequest("native::object::get", resourcetest.NewResource("native::object::cba/def", nil), nil) status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace2, container), req) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) }) t.Run("good", func(t *testing.T) { status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) t.Run("quota on a different container", func(t *testing.T) { s.LocalStorage().AddOverride(chain.Ingress, engine.ContainerTarget(container), &chain.Chain{ Rules: []chain.Rule{{ Status: chain.QuotaLimitReached, Actions: chain.Actions{Names: []string{"native::object::put"}}, Resources: chain.Resources{Names: []string{"native::object::cba/*"}}, }}, }) _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) require.NoError(t, err) itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2)) _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container) require.NoError(t, err) itemStacksEqual(t, it.Values, toStackItems(container)) status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) }) var quotaRuleChainID chain.ID t.Run("quota on the request container", func(t *testing.T) { quotaRuleChainID, _ = s.LocalStorage().AddOverride(chain.Ingress, engine.ContainerTarget(container), &chain.Chain{ Rules: []chain.Rule{{ Status: chain.QuotaLimitReached, Actions: chain.Actions{Names: []string{"native::object::put"}}, Resources: chain.Resources{Names: []string{"native::object::abc/*"}}, }}, }) _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) require.NoError(t, err) itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2)) _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container) require.NoError(t, err) itemStacksEqual(t, it.Values, toStackItems(container)) status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood) require.Equal(t, chain.QuotaLimitReached, status) require.True(t, ok) }) t.Run("removed quota on the request container", func(t *testing.T) { err := s.LocalStorage().RemoveOverride(chain.Ingress, engine.ContainerTarget(container), quotaRuleChainID) require.NoError(t, err) _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Namespace) require.NoError(t, err) itemStacksEqual(t, it.Values, toStackItems(namespace, namespace2)) _, it, err = s.MorphRuleChainStorage().ListTargetsIterator(engine.Container) require.NoError(t, err) itemStacksEqual(t, it.Values, toStackItems(container)) status, ok, _ = s.IsAllowed(chain.Ingress, engine.NewRequestTarget(namespace, container), reqGood) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) }) }) t.Run("remove all", func(t *testing.T) { s := NewInMemoryLocalOverrides() _, _, err := s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace), &chain.Chain{ Rules: []chain.Rule{ { Status: chain.AccessDenied, Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}}, Resources: chain.Resources{Inverted: true, Names: []string{object}}, }, }, }) require.NoError(t, err) _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{ Rules: []chain.Rule{ { Status: chain.Allow, Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}}, Resources: chain.Resources{Inverted: true, Names: []string{object}}, }, }, }) require.NoError(t, err) _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{ Rules: []chain.Rule{ { Status: chain.AccessDenied, Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}}, Resources: chain.Resources{Inverted: true, Names: []string{object}}, }, }, }) require.NoError(t, err) _, _, err = s.MorphRuleChainStorage().RemoveMorphRuleChainsByTarget(chain.Ingress, engine.NamespaceTarget(namespace2)) require.NoError(t, err) chains, err := s.MorphRuleChainStorage().ListMorphRuleChains(chain.Ingress, engine.NamespaceTarget(namespace2)) require.NoError(t, err) require.Equal(t, 0, len(chains)) chains, err = s.MorphRuleChainStorage().ListMorphRuleChains(chain.Ingress, engine.NamespaceTarget(namespace)) require.NoError(t, err) require.Equal(t, 1, len(chains)) }) } func itemStacksEqual(t *testing.T, got []stackitem.Item, expected []stackitem.Item) { next: for _, exp := range expected { expBytes, err := exp.TryBytes() require.NoError(t, err) for _, v := range got { vBytes, err := v.TryBytes() require.NoError(t, err) if bytes.Equal(vBytes, expBytes) { continue next } } t.Fatalf("not found %s", exp) } } func toStackItems(names ...string) []stackitem.Item { var items []stackitem.Item for _, name := range names { items = append(items, stackitem.NewByteArray([]byte(name))) } return items }