From e47af4b11159a231127cd3ad13beb379a7c21ed7 Mon Sep 17 00:00:00 2001 From: aarifullin Date: Tue, 7 Nov 2023 21:29:51 +0300 Subject: [PATCH] [#7] engine: Revise CachedChainStorage interface * Nuke out CachedChainStorage interface * Introduce LocalOverrideStorage interface to manage local overrides * Introduce MorphRuleChainStorage interface to manage chains in the policy contract * Extend Engine interface Signed-off-by: Airat Arifullin --- pkg/engine/chain_router.go | 101 ++++++++++ pkg/engine/errors.go | 10 + pkg/engine/inmemory.go | 113 ----------- pkg/engine/inmemory/inmemory.go | 48 +++++ pkg/engine/{ => inmemory}/inmemory_test.go | 49 +++-- pkg/engine/inmemory/local_storage.go | 109 +++++++++++ pkg/engine/inmemory/local_storage_test.go | 217 +++++++++++++++++++++ pkg/engine/inmemory/morph_storage.go | 52 +++++ pkg/engine/interface.go | 84 ++++++-- 9 files changed, 633 insertions(+), 150 deletions(-) create mode 100644 pkg/engine/chain_router.go create mode 100644 pkg/engine/errors.go delete mode 100644 pkg/engine/inmemory.go create mode 100644 pkg/engine/inmemory/inmemory.go rename pkg/engine/{ => inmemory}/inmemory_test.go (74%) create mode 100644 pkg/engine/inmemory/local_storage.go create mode 100644 pkg/engine/inmemory/local_storage_test.go create mode 100644 pkg/engine/inmemory/morph_storage.go diff --git a/pkg/engine/chain_router.go b/pkg/engine/chain_router.go new file mode 100644 index 0000000..c798793 --- /dev/null +++ b/pkg/engine/chain_router.go @@ -0,0 +1,101 @@ +package engine + +import ( + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" +) + +type defaultChainRouter struct { + morph MorphRuleChainStorage + + local LocalOverrideStorage +} + +func NewDefaultChainRouter(morph MorphRuleChainStorage) ChainRouter { + return &defaultChainRouter{ + morph: morph, + } +} + +func NewDefaultChainRouterWithLocalOverrides(morph MorphRuleChainStorage, local LocalOverrideStorage) ChainRouter { + return &defaultChainRouter{ + morph: morph, + local: local, + } +} + +func (dr *defaultChainRouter) IsAllowed(name chain.Name, namespace string, r resource.Request) (status chain.Status, ruleFound bool, err error) { + if dr.local != nil { + var localRuleFound bool + status, localRuleFound, err = dr.checkLocalOverrides(name, r) + if err != nil { + return chain.NoRuleFound, false, err + } else if localRuleFound { + ruleFound = true + return + } + } + + var namespaceRuleFound bool + status, namespaceRuleFound, err = dr.checkNamespaceChains(name, namespace, r) + if err != nil { + return + } else if namespaceRuleFound && status != chain.Allow { + ruleFound = true + return + } + + var cnrRuleFound bool + status, cnrRuleFound, err = dr.checkContainerChains(name, r.Resource().Name(), r) + if err != nil { + return + } else if cnrRuleFound && status != chain.Allow { + ruleFound = true + return + } + + status = chain.NoRuleFound + if ruleFound = namespaceRuleFound || cnrRuleFound; ruleFound { + status = chain.Allow + } + return +} + +func (dr *defaultChainRouter) checkLocalOverrides(name chain.Name, r resource.Request) (status chain.Status, ruleFound bool, err error) { + localOverrides, err := dr.local.ListOverrides(name, r.Resource().Name()) + if err != nil { + return + } + for _, c := range localOverrides { + if status, ruleFound = c.Match(r); ruleFound && status != chain.Allow { + return + } + } + return +} + +func (dr *defaultChainRouter) checkNamespaceChains(name chain.Name, namespace string, r resource.Request) (status chain.Status, ruleFound bool, err error) { + namespaceChains, err := dr.morph.ListMorphRuleChains(name, NamespaceTarget(namespace)) + if err != nil { + return + } + for _, c := range namespaceChains { + if status, ruleFound = c.Match(r); ruleFound { + return + } + } + return +} + +func (dr *defaultChainRouter) checkContainerChains(name chain.Name, container string, r resource.Request) (status chain.Status, ruleFound bool, err error) { + containerChains, err := dr.morph.ListMorphRuleChains(name, ContainerTarget(container)) + if err != nil { + return + } + for _, c := range containerChains { + if status, ruleFound = c.Match(r); ruleFound { + return + } + } + return +} diff --git a/pkg/engine/errors.go b/pkg/engine/errors.go new file mode 100644 index 0000000..c08ec29 --- /dev/null +++ b/pkg/engine/errors.go @@ -0,0 +1,10 @@ +package engine + +import "errors" + +var ( + ErrUnknownTarget = errors.New("unknown target type") + ErrChainNotFound = errors.New("chain not found") + ErrChainNameNotFound = errors.New("chain name not found") + ErrResourceNotFound = errors.New("resource not found") +) diff --git a/pkg/engine/inmemory.go b/pkg/engine/inmemory.go deleted file mode 100644 index c37fdd9..0000000 --- a/pkg/engine/inmemory.go +++ /dev/null @@ -1,113 +0,0 @@ -package engine - -import ( - "git.frostfs.info/TrueCloudLab/policy-engine/util" - - "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" - "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" -) - -type inmemory struct { - namespace map[chain.Name][]chainWrapper - resource map[chain.Name][]chainWrapper - local map[chain.Name][]*chain.Chain -} - -type chainWrapper struct { - object string - chain *chain.Chain -} - -// NewInMemory returns new inmemory instance of chain storage. -func NewInMemory() CachedChainStorage { - return &inmemory{ - namespace: make(map[chain.Name][]chainWrapper), - resource: make(map[chain.Name][]chainWrapper), - local: make(map[chain.Name][]*chain.Chain), - } -} - -// IsAllowed implements the Engine interface. -func (s *inmemory) IsAllowed(name chain.Name, namespace string, r resource.Request) (chain.Status, bool) { - var ruleFound bool - if local, ok := s.local[name]; ok { - for _, c := range local { - if status, matched := c.Match(r); matched && status != chain.Allow { - return status, true - } - } - } - if cs, ok := s.namespace[name]; ok { - status, ok := matchArray(cs, namespace, r) - if ok && status != chain.Allow { - return status, true - } - ruleFound = ruleFound || ok - } - if cs, ok := s.resource[name]; ok { - status, ok := matchArray(cs, r.Resource().Name(), r) - if ok { - return status, true - } - ruleFound = ruleFound || ok - } - if ruleFound { - return chain.Allow, true - } - return chain.NoRuleFound, false -} - -func matchArray(cs []chainWrapper, object string, r resource.Request) (chain.Status, bool) { - for _, c := range cs { - if !util.GlobMatch(object, c.object) { - continue - } - if status, matched := c.chain.Match(r); matched { - return status, true - } - } - return chain.NoRuleFound, false -} - -func (s *inmemory) AddResourceChain(name chain.Name, resource string, c *chain.Chain) { - s.resource[name] = append(s.resource[name], chainWrapper{resource, c}) -} - -func (s *inmemory) AddNameSpaceChain(name chain.Name, namespace string, c *chain.Chain) { - s.namespace[name] = append(s.namespace[name], chainWrapper{namespace, c}) -} - -func (s *inmemory) AddOverride(name chain.Name, c *chain.Chain) { - s.local[name] = append(s.local[name], c) -} - -func (s *inmemory) GetOverride(name chain.Name, chainID chain.ID) (chain *chain.Chain, found bool) { - chains := s.local[name] - - for _, chain = range chains { - if chain.ID == chainID { - found = true - return - } - } - - return -} - -func (s *inmemory) RemoveOverride(name chain.Name, chainID chain.ID) (found bool) { - chains := s.local[name] - - for i, chain := range chains { - if chain.ID == chainID { - s.local[name] = append(chains[:i], chains[i+1:]...) - found = true - return - } - } - - return -} - -func (s *inmemory) ListOverrides(name chain.Name) []*chain.Chain { - return s.local[name] -} diff --git a/pkg/engine/inmemory/inmemory.go b/pkg/engine/inmemory/inmemory.go new file mode 100644 index 0000000..9e97d23 --- /dev/null +++ b/pkg/engine/inmemory/inmemory.go @@ -0,0 +1,48 @@ +package inmemory + +import ( + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" +) + +type inmemory struct { + router engine.ChainRouter + + morph engine.MorphRuleChainStorage + + local engine.LocalOverrideStorage +} + +// NewInMemoryLocalOverrides returns new inmemory instance of chain storage with +// local overrides manager. +func NewInMemoryLocalOverrides() engine.LocalOverrideEngine { + morph := NewInmemoryMorphRuleChainStorage() + local := NewInmemoryLocalStorage() + return &inmemory{ + router: engine.NewDefaultChainRouterWithLocalOverrides(morph, local), + morph: morph, + local: local, + } +} + +// NewInMemory returns new inmemory instance of chain storage. +func NewInMemory() engine.Engine { + morph := NewInmemoryMorphRuleChainStorage() + return &inmemory{ + router: engine.NewDefaultChainRouter(morph), + morph: morph, + } +} + +func (im *inmemory) LocalStorage() engine.LocalOverrideStorage { + return im.local +} + +func (im *inmemory) MorphRuleChainStorage() engine.MorphRuleChainStorage { + return im.morph +} + +func (im *inmemory) IsAllowed(name chain.Name, namespace string, r resource.Request) (status chain.Status, ruleFound bool, err error) { + return im.router.IsAllowed(name, namespace, r) +} diff --git a/pkg/engine/inmemory_test.go b/pkg/engine/inmemory/inmemory_test.go similarity index 74% rename from pkg/engine/inmemory_test.go rename to pkg/engine/inmemory/inmemory_test.go index 72e7fc0..bc47313 100644 --- a/pkg/engine/inmemory_test.go +++ b/pkg/engine/inmemory/inmemory_test.go @@ -1,9 +1,10 @@ -package engine +package inmemory import ( "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/stretchr/testify/require" ) @@ -18,7 +19,7 @@ func TestInmemory(t *testing.T) { actor2 = "owner2" ) - s := NewInMemory() + s := NewInMemoryLocalOverrides() // Object which was put via S3. res := resourcetest.NewResource(object, map[string]string{"FromS3": "true"}) @@ -28,11 +29,11 @@ func TestInmemory(t *testing.T) { "Actor": actor1, }) - status, ok := s.IsAllowed(chain.Ingress, namespace, reqGood) + status, ok, _ := s.IsAllowed(chain.Ingress, namespace, reqGood) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) - s.AddNameSpaceChain(chain.Ingress, namespace, &chain.Chain{ + s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace), &chain.Chain{ Rules: []chain.Rule{ { // Restrict to remove ANY object from the namespace. Status: chain.AccessDenied, @@ -62,7 +63,7 @@ func TestInmemory(t *testing.T) { }, }) - s.AddNameSpaceChain(chain.Ingress, namespace2, &chain.Chain{ + 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, @@ -72,7 +73,7 @@ func TestInmemory(t *testing.T) { }, }) - s.AddResourceChain(chain.Ingress, container, &chain.Chain{ + 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, @@ -102,7 +103,7 @@ func TestInmemory(t *testing.T) { "SourceIP": "10.122.1.20", "Actor": actor1, }) - status, ok := s.IsAllowed(chain.Ingress, namespace, reqBadIP) + status, ok, _ := s.IsAllowed(chain.Ingress, namespace, reqBadIP) require.Equal(t, chain.AccessDenied, status) require.True(t, ok) }) @@ -112,7 +113,7 @@ func TestInmemory(t *testing.T) { "SourceIP": "10.1.1.13", "Actor": actor2, }) - status, ok := s.IsAllowed(chain.Ingress, namespace, reqBadActor) + status, ok, _ := s.IsAllowed(chain.Ingress, namespace, reqBadActor) require.Equal(t, chain.AccessDenied, status) require.True(t, ok) }) @@ -120,14 +121,14 @@ func TestInmemory(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, namespace, resourcetest.NewRequest("native::object::get", objGood, map[string]string{ + status, ok, _ := s.IsAllowed(chain.Ingress, namespace, 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, namespace, resourcetest.NewRequest("native::object::get", objBadAttr, map[string]string{ + status, ok, _ = s.IsAllowed(chain.Ingress, namespace, resourcetest.NewRequest("native::object::get", objBadAttr, map[string]string{ "SourceIP": "10.1.1.14", "Actor": actor2, })) @@ -140,33 +141,33 @@ func TestInmemory(t *testing.T) { "SourceIP": "10.1.1.12", "Actor": actor1, }) - status, ok := s.IsAllowed(chain.Ingress, namespace, reqBadOperation) + status, ok, _ := s.IsAllowed(chain.Ingress, namespace, 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, namespace2, req) + status, ok, _ = s.IsAllowed(chain.Ingress, namespace2, 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, namespace2, req) + status, ok, _ = s.IsAllowed(chain.Ingress, namespace2, 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, namespace2, req) + status, ok, _ = s.IsAllowed(chain.Ingress, namespace2, req) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) }) t.Run("good", func(t *testing.T) { - status, ok = s.IsAllowed(chain.Ingress, namespace, reqGood) + status, ok, _ = s.IsAllowed(chain.Ingress, namespace, reqGood) require.Equal(t, chain.NoRuleFound, status) require.False(t, ok) t.Run("quota on a different container", func(t *testing.T) { - s.AddOverride(chain.Ingress, &chain.Chain{ + s.LocalStorage().AddOverride(chain.Ingress, container, &chain.Chain{ Rules: []chain.Rule{{ Status: chain.QuotaLimitReached, Actions: chain.Actions{Names: []string{"native::object::put"}}, @@ -174,12 +175,14 @@ func TestInmemory(t *testing.T) { }}, }) - status, ok = s.IsAllowed(chain.Ingress, namespace, reqGood) + status, ok, _ = s.IsAllowed(chain.Ingress, namespace, 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) { - s.AddOverride(chain.Ingress, &chain.Chain{ + quotaRuleChainID, _ = s.LocalStorage().AddOverride(chain.Ingress, container, &chain.Chain{ Rules: []chain.Rule{{ Status: chain.QuotaLimitReached, Actions: chain.Actions{Names: []string{"native::object::put"}}, @@ -187,9 +190,17 @@ func TestInmemory(t *testing.T) { }}, }) - status, ok = s.IsAllowed(chain.Ingress, namespace, reqGood) + status, ok, _ = s.IsAllowed(chain.Ingress, namespace, 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, container, quotaRuleChainID) + require.NoError(t, err) + + status, ok, _ = s.IsAllowed(chain.Ingress, namespace, reqGood) + require.Equal(t, chain.NoRuleFound, status) + require.False(t, ok) + }) }) } diff --git a/pkg/engine/inmemory/local_storage.go b/pkg/engine/inmemory/local_storage.go new file mode 100644 index 0000000..3c3f8ba --- /dev/null +++ b/pkg/engine/inmemory/local_storage.go @@ -0,0 +1,109 @@ +package inmemory + +import ( + "fmt" + "math/rand" + "strings" + + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" + "git.frostfs.info/TrueCloudLab/policy-engine/util" +) + +type targetToChain map[string][]*chain.Chain + +type inmemoryLocalStorage struct { + usedChainID map[chain.ID]struct{} + nameToResourceChains map[chain.Name]targetToChain +} + +func NewInmemoryLocalStorage() engine.LocalOverrideStorage { + return &inmemoryLocalStorage{ + usedChainID: map[chain.ID]struct{}{}, + nameToResourceChains: make(map[chain.Name]targetToChain), + } +} + +func (s *inmemoryLocalStorage) generateChainID(name chain.Name, resource string) chain.ID { + var id chain.ID + for { + suffix := rand.Uint32() % 100 + sid := fmt.Sprintf("%s:%s/%d", name, resource, suffix) + sid = strings.ReplaceAll(sid, "*", "") + sid = strings.ReplaceAll(sid, "/", ":") + sid = strings.ReplaceAll(sid, "::", ":") + id = chain.ID(sid) + _, ok := s.usedChainID[id] + if ok { + continue + } + s.usedChainID[id] = struct{}{} + break + } + return id +} + +func (s *inmemoryLocalStorage) AddOverride(name chain.Name, resource string, c *chain.Chain) (chain.ID, error) { + // AddOverride assigns generated chain ID if it has not been assigned. + if c.ID == "" { + c.ID = s.generateChainID(name, resource) + } + if s.nameToResourceChains[name] == nil { + s.nameToResourceChains[name] = make(targetToChain) + } + rc := s.nameToResourceChains[name] + rc[resource] = append(rc[resource], c) + return c.ID, nil +} + +func (s *inmemoryLocalStorage) GetOverride(name chain.Name, resource string, chainID chain.ID) (*chain.Chain, error) { + if _, ok := s.nameToResourceChains[name]; !ok { + return nil, engine.ErrChainNameNotFound + } + chains, ok := s.nameToResourceChains[name][resource] + if !ok { + return nil, engine.ErrResourceNotFound + } + for _, c := range chains { + if c.ID == chainID { + return c, nil + } + } + return nil, engine.ErrChainNotFound +} + +func (s *inmemoryLocalStorage) RemoveOverride(name chain.Name, resource string, chainID chain.ID) error { + if _, ok := s.nameToResourceChains[name]; !ok { + return engine.ErrChainNameNotFound + } + chains, ok := s.nameToResourceChains[name][resource] + if !ok { + return engine.ErrResourceNotFound + } + for i, c := range chains { + if c.ID == chainID { + s.nameToResourceChains[name][resource] = append(chains[:i], chains[i+1:]...) + return nil + } + } + return engine.ErrChainNotFound +} + +func (s *inmemoryLocalStorage) ListOverrides(name chain.Name, resource string) ([]*chain.Chain, error) { + rcs, ok := s.nameToResourceChains[name] + if !ok { + return []*chain.Chain{}, nil + } + for container, chains := range rcs { + if !util.GlobMatch(resource, container) { + continue + } + return chains, nil + } + return []*chain.Chain{}, nil +} + +func (s *inmemoryLocalStorage) DropAllOverrides(name chain.Name) error { + s.nameToResourceChains[name] = make(targetToChain) + return nil +} diff --git a/pkg/engine/inmemory/local_storage_test.go b/pkg/engine/inmemory/local_storage_test.go new file mode 100644 index 0000000..2fbe26d --- /dev/null +++ b/pkg/engine/inmemory/local_storage_test.go @@ -0,0 +1,217 @@ +package inmemory + +import ( + "testing" + + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" + "github.com/stretchr/testify/require" +) + +const ( + resrc = "native:::object/ExYw/*" + chainID = "ingress:ExYw" + nonExistChainId = "ingress:LxGyWyL" +) + +func testInmemLocalStorage() *inmemoryLocalStorage { + return NewInmemoryLocalStorage().(*inmemoryLocalStorage) +} + +func TestAddOverride(t *testing.T) { + inmem := testInmemLocalStorage() + + inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{Names: []string{"native::object::delete"}}, + Resources: chain.Resources{Names: []string{"native::object::*"}}, + }, + }, + }) + + ingressChains, ok := inmem.nameToResourceChains[chain.Ingress] + require.True(t, ok) + resourceChains, ok := ingressChains[resrc] + require.True(t, ok) + require.Len(t, resourceChains, 1) + require.Len(t, resourceChains[0].Rules, 1) + + inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.QuotaLimitReached, + Actions: chain.Actions{Names: []string{"native::object::put"}}, + Resources: chain.Resources{Names: []string{"native::object::*"}}, + }, + { + Status: chain.AccessDenied, + Actions: chain.Actions{Names: []string{"native::object::get"}}, + Resources: chain.Resources{Names: []string{"native::object::*"}}, + }, + }, + }) + + ingressChains, ok = inmem.nameToResourceChains[chain.Ingress] + require.True(t, ok) + resourceChains, ok = ingressChains[resrc] + require.True(t, ok) + require.Len(t, resourceChains, 2) + require.Len(t, resourceChains[1].Rules, 2) +} + +func TestRemoveOverride(t *testing.T) { + t.Run("remove from empty storage", func(t *testing.T) { + inmem := testInmemLocalStorage() + err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(chainID)) + require.ErrorIs(t, err, engine.ErrChainNameNotFound) + }) + + t.Run("remove not added chain id", func(t *testing.T) { + inmem := testInmemLocalStorage() + inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{ + ID: chain.ID(chainID), + 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::*"}}, + }, + }, + }) + + err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(nonExistChainId)) + require.ErrorIs(t, err, engine.ErrChainNotFound) + }) + + t.Run("remove existing chain id", func(t *testing.T) { + inmem := testInmemLocalStorage() + inmem.AddOverride(chain.Ingress, resrc, &chain.Chain{ + ID: chain.ID(chainID), + 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::*"}}, + }, + }, + }) + + err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(chainID)) + require.NoError(t, err) + + ingressChains, ok := inmem.nameToResourceChains[chain.Ingress] + require.True(t, ok) + require.Len(t, ingressChains, 1) + resourceChains, ok := ingressChains[resrc] + require.True(t, ok) + require.Len(t, resourceChains, 0) + }) +} + +func TestGetOverride(t *testing.T) { + addChain := &chain.Chain{ + ID: chain.ID(chainID), + 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::*"}}, + }, + }, + } + + t.Run("get from empty storage", func(t *testing.T) { + inmem := testInmemLocalStorage() + _, err := inmem.GetOverride(chain.Ingress, resrc, chain.ID(chainID)) + require.ErrorIs(t, err, engine.ErrChainNameNotFound) + }) + + t.Run("get not added chain id", func(t *testing.T) { + inmem := testInmemLocalStorage() + inmem.AddOverride(chain.Ingress, resrc, addChain) + + const nonExistingChainID = "ingress:LxGyWyL" + + _, err := inmem.GetOverride(chain.Ingress, resrc, chain.ID(nonExistingChainID)) + require.ErrorIs(t, err, engine.ErrChainNotFound) + }) + + t.Run("get existing chain id", func(t *testing.T) { + inmem := testInmemLocalStorage() + inmem.AddOverride(chain.Ingress, resrc, addChain) + + c, err := inmem.GetOverride(chain.Ingress, resrc, chain.ID(chainID)) + require.NoError(t, err) + require.EqualValues(t, *addChain, *c) + }) + + t.Run("get removed chain id", func(t *testing.T) { + inmem := testInmemLocalStorage() + inmem.AddOverride(chain.Ingress, resrc, addChain) + + err := inmem.RemoveOverride(chain.Ingress, resrc, chain.ID(chainID)) + require.NoError(t, err) + + _, err = inmem.GetOverride(chain.Ingress, resrc, chain.ID(chainID)) + require.ErrorIs(t, err, engine.ErrChainNotFound) + }) +} + +func TestListOverrides(t *testing.T) { + addChain := &chain.Chain{ + ID: chain.ID(chainID), + 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::*"}}, + }, + }, + } + + t.Run("list empty storage", func(t *testing.T) { + inmem := testInmemLocalStorage() + l, _ := inmem.ListOverrides(chain.Ingress, resrc) + require.Len(t, l, 0) + }) + + t.Run("list with one added resource", func(t *testing.T) { + inmem := testInmemLocalStorage() + inmem.AddOverride(chain.Ingress, resrc, addChain) + l, _ := inmem.ListOverrides(chain.Ingress, resrc) + require.Len(t, l, 1) + }) + + t.Run("list after drop", func(t *testing.T) { + inmem := testInmemLocalStorage() + inmem.AddOverride(chain.Ingress, resrc, addChain) + l, _ := inmem.ListOverrides(chain.Ingress, resrc) + require.Len(t, l, 1) + + _ = inmem.DropAllOverrides(chain.Ingress) + l, _ = inmem.ListOverrides(chain.Ingress, resrc) + require.Len(t, l, 0) + }) +} + +func TestGenerateID(t *testing.T) { + inmem := testInmemLocalStorage() + ids := make([]chain.ID, 0, 100) + for i := 0; i < 100; i++ { + ids = append(ids, inmem.generateChainID(chain.Ingress, resrc)) + } + require.False(t, hasDuplicates(ids)) +} + +func hasDuplicates(ids []chain.ID) bool { + seen := make(map[chain.ID]bool) + for _, id := range ids { + if seen[id] { + return true + } + seen[id] = true + } + return false +} diff --git a/pkg/engine/inmemory/morph_storage.go b/pkg/engine/inmemory/morph_storage.go new file mode 100644 index 0000000..2f802fb --- /dev/null +++ b/pkg/engine/inmemory/morph_storage.go @@ -0,0 +1,52 @@ +package inmemory + +import ( + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" +) + +type inmemoryMorphRuleChainStorage struct { + nameToNamespaceChains engine.LocalOverrideStorage + nameToContainerChains engine.LocalOverrideStorage +} + +func NewInmemoryMorphRuleChainStorage() engine.MorphRuleChainStorage { + return &inmemoryMorphRuleChainStorage{ + nameToNamespaceChains: NewInmemoryLocalStorage(), + nameToContainerChains: NewInmemoryLocalStorage(), + } +} + +func (s *inmemoryMorphRuleChainStorage) AddMorphRuleChain(name chain.Name, target engine.Target, c *chain.Chain) (err error) { + switch target.Type { + case engine.Namespace: + _, err = s.nameToNamespaceChains.AddOverride(name, target.Name, c) + case engine.Container: + _, err = s.nameToContainerChains.AddOverride(name, target.Name, c) + default: + err = engine.ErrUnknownTarget + } + return +} + +func (s *inmemoryMorphRuleChainStorage) RemoveMorphRuleChain(name chain.Name, target engine.Target, chainID chain.ID) error { + switch target.Type { + case engine.Namespace: + return s.nameToNamespaceChains.RemoveOverride(name, target.Name, chainID) + case engine.Container: + return s.nameToContainerChains.RemoveOverride(name, target.Name, chainID) + default: + return engine.ErrUnknownTarget + } +} + +func (s *inmemoryMorphRuleChainStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) { + switch target.Type { + case engine.Namespace: + return s.nameToNamespaceChains.ListOverrides(name, target.Name) + case engine.Container: + return s.nameToContainerChains.ListOverrides(name, target.Name) + default: + } + return nil, engine.ErrUnknownTarget +} diff --git a/pkg/engine/interface.go b/pkg/engine/interface.go index e753951..fe0b4a7 100644 --- a/pkg/engine/interface.go +++ b/pkg/engine/interface.go @@ -5,26 +5,74 @@ import ( "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" ) -// Engine ... -type Engine interface { +type ChainRouter interface { // IsAllowed returns status for the operation after all checks. // The second return value signifies whether a matching rule was found. - IsAllowed(name chain.Name, namespace string, r resource.Request) (chain.Status, bool) + IsAllowed(name chain.Name, target string, r resource.Request) (status chain.Status, found bool, err error) } -// CachedChainStorage ... -type CachedChainStorage interface { - Engine - // Adds a policy chain used for all operations with a specific resource. - AddResourceChain(name chain.Name, resource string, c *chain.Chain) - // Adds a policy chain used for all operations in the namespace. - AddNameSpaceChain(name chain.Name, namespace string, c *chain.Chain) - // Adds a local policy chain used for all operations with this service. - AddOverride(name chain.Name, c *chain.Chain) - // Gets the local override with given chain id. - GetOverride(name chain.Name, chainID chain.ID) (chain *chain.Chain, found bool) - // Remove the local override with given chain id. - RemoveOverride(name chain.Name, chainID chain.ID) (removed bool) - // ListOverrides returns the list of local overrides. - ListOverrides(name chain.Name) []*chain.Chain +// LocalOverrideStorage is the interface to manage local overrides defined +// for a node. Local overrides have a higher priority than chains got from morph storage. +type LocalOverrideStorage interface { + AddOverride(name chain.Name, resource string, c *chain.Chain) (chain.ID, error) + + GetOverride(name chain.Name, resource string, chainID chain.ID) (*chain.Chain, error) + + RemoveOverride(name chain.Name, resource string, chainID chain.ID) error + + ListOverrides(name chain.Name, resource string) ([]*chain.Chain, error) + + DropAllOverrides(name chain.Name) error +} + +type TargetType rune + +const ( + Namespace TargetType = 'n' + Container TargetType = 'c' +) + +type Target struct { + Type TargetType + Name string +} + +func NamespaceTarget(namespace string) Target { + return Target{ + Type: Namespace, + Name: namespace, + } +} + +func ContainerTarget(container string) Target { + return Target{ + Type: Container, + Name: container, + } +} + +// MorphRuleChainStorage is the interface to manage chains from the chain storage. +// Basically, this implies that the storage manages rules stored in policy contract. +type MorphRuleChainStorage interface { + AddMorphRuleChain(name chain.Name, target Target, c *chain.Chain) error + + RemoveMorphRuleChain(name chain.Name, target Target, chainID chain.ID) error + + ListMorphRuleChains(name chain.Name, target Target) ([]*chain.Chain, error) +} + +// Engine is the interface that provides methods to check request permissions checking +// chain rules from morph client - this implies using the policy contract. +type Engine interface { + ChainRouter + + MorphRuleChainStorage() MorphRuleChainStorage +} + +// LocalOverrideEngine is extended Engine that also provides methods to manage a local +// chain rule storage. Local overrides must have the highest priority during request checking. +type LocalOverrideEngine interface { + Engine + + LocalStorage() LocalOverrideStorage }