From a064a01fdbe63c947e1d451fd42e5d67afe987d2 Mon Sep 17 00:00:00 2001 From: aarifullin Date: Tue, 7 Nov 2023 19:53:23 +0300 Subject: [PATCH 1/3] [#7] engine: Move globMatch to common util package Signed-off-by: Airat Arifullin --- chain.go | 10 ++++++---- inmemory.go | 6 +++++- glob.go => util/glob.go | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) rename glob.go => util/glob.go (87%) diff --git a/chain.go b/chain.go index 9be3cde..cb17f70 100644 --- a/chain.go +++ b/chain.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "strings" + + "git.frostfs.info/TrueCloudLab/policy-engine/util" ) // Engine ... @@ -159,9 +161,9 @@ func (c *Condition) Match(req Request) bool { case CondStringNotEqualsIgnoreCase: return !strings.EqualFold(val, c.Value) case CondStringLike: - return globMatch(val, c.Value) + return util.GlobMatch(val, c.Value) case CondStringNotLike: - return !globMatch(val, c.Value) + return !util.GlobMatch(val, c.Value) case CondStringLessThan: return val < c.Value case CondStringLessThanEquals: @@ -176,7 +178,7 @@ func (c *Condition) Match(req Request) bool { func (r *Rule) Match(req Request) (status Status, matched bool) { found := len(r.Resources.Names) == 0 for i := range r.Resources.Names { - if globMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted { + if util.GlobMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted { found = true break } @@ -185,7 +187,7 @@ func (r *Rule) Match(req Request) (status Status, matched bool) { return NoRuleFound, false } for i := range r.Actions.Names { - if globMatch(req.Operation(), r.Actions.Names[i]) != r.Actions.Inverted { + if util.GlobMatch(req.Operation(), r.Actions.Names[i]) != r.Actions.Inverted { return r.matchCondition(req) } } diff --git a/inmemory.go b/inmemory.go index 5bd4f4c..50c146d 100644 --- a/inmemory.go +++ b/inmemory.go @@ -1,5 +1,9 @@ package policyengine +import ( + "git.frostfs.info/TrueCloudLab/policy-engine/util" +) + type inmemory struct { namespace map[Name][]chain resource map[Name][]chain @@ -52,7 +56,7 @@ func (s *inmemory) IsAllowed(name Name, namespace string, r Request) (Status, bo func matchArray(cs []chain, object string, r Request) (Status, bool) { for _, c := range cs { - if !globMatch(object, c.object) { + if !util.GlobMatch(object, c.object) { continue } if status, matched := c.chain.Match(r); matched { diff --git a/glob.go b/util/glob.go similarity index 87% rename from glob.go rename to util/glob.go index 41a28d3..9e8ed36 100644 --- a/glob.go +++ b/util/glob.go @@ -1,4 +1,4 @@ -package policyengine +package util import ( "strings" @@ -9,7 +9,7 @@ import ( // ? in pattern correspond to any symbol. // * in pattern correspond to any sequence of symbols. // Currently only '*' in the suffix is supported. -func globMatch(s, pattern string) bool { +func GlobMatch(s, pattern string) bool { index := strings.IndexByte(pattern, '*') switch index { default: -- 2.45.2 From 6eb4a649c3a34289e41649c676141a8b48debe98 Mon Sep 17 00:00:00 2001 From: aarifullin Date: Tue, 7 Nov 2023 20:20:54 +0300 Subject: [PATCH 2/3] [#7] engine: Set project structure pattern for files * Create pkg package * Move chain-relates structures to pkg/chain package * Move inmemory and interface files to pkg/engine package * Move resource structures to pkg/resource package * Move GlobMatch to util package Signed-off-by: Airat Arifullin --- iam/converter.go | 82 ++++----- iam/converter_test.go | 128 +++++++------- inmemory.go | 110 ------------ inmemory_test.go | 193 -------------------- interface.go | 18 -- chain.go => pkg/chain/chain.go | 28 ++- chain_names.go => pkg/chain/chain_names.go | 2 +- chain_test.go => pkg/chain/chain_test.go | 2 +- error.go => pkg/chain/error.go | 2 +- pkg/engine/inmemory.go | 113 ++++++++++++ pkg/engine/inmemory_test.go | 195 +++++++++++++++++++++ pkg/engine/interface.go | 30 ++++ resource.go => pkg/resource/resource.go | 2 +- pkg/resource/testutil/resource.go | 53 ++++++ resource_test.go | 49 ------ 15 files changed, 511 insertions(+), 496 deletions(-) delete mode 100644 inmemory.go delete mode 100644 inmemory_test.go delete mode 100644 interface.go rename chain.go => pkg/chain/chain.go (86%) rename chain_names.go => pkg/chain/chain_names.go (92%) rename chain_test.go => pkg/chain/chain_test.go (96%) rename error.go => pkg/chain/error.go (96%) create mode 100644 pkg/engine/inmemory.go create mode 100644 pkg/engine/inmemory_test.go create mode 100644 pkg/engine/interface.go rename resource.go => pkg/resource/resource.go (96%) create mode 100644 pkg/resource/testutil/resource.go delete mode 100644 resource_test.go diff --git a/iam/converter.go b/iam/converter.go index d6642f3..b781823 100644 --- a/iam/converter.go +++ b/iam/converter.go @@ -6,7 +6,7 @@ import ( "strings" "time" - policyengine "git.frostfs.info/TrueCloudLab/policy-engine" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" ) const ( @@ -52,41 +52,41 @@ const ( CondArnNotLike string = "ArnNotLike" ) -func (p Policy) ToChain() (*policyengine.Chain, error) { +func (p Policy) ToChain() (*chain.Chain, error) { if err := p.Validate(GeneralPolicyType); err != nil { return nil, err } - var chain policyengine.Chain + var ch chain.Chain for _, statement := range p.Statement { - status := policyengine.AccessDenied + status := chain.AccessDenied if statement.Effect == AllowEffect { - status = policyengine.Allow + status = chain.Allow } var principals []string - var op policyengine.ConditionType + var op chain.ConditionType statementPrincipal, inverted := statement.principal() if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false principals = []string{Wildcard} - op = policyengine.CondStringLike + op = chain.CondStringLike } else { for _, principal := range statementPrincipal { principals = append(principals, principal...) } - op = policyengine.CondStringEquals + op = chain.CondStringEquals if inverted { - op = policyengine.CondStringNotEquals + op = chain.CondStringNotEquals } } - var conditions []policyengine.Condition + var conditions []chain.Condition for _, principal := range principals { - conditions = append(conditions, policyengine.Condition{ + conditions = append(conditions, chain.Condition{ Op: op, - Object: policyengine.ObjectRequest, + Object: chain.ObjectRequest, Key: RequestOwnerProperty, Value: principal, }) @@ -99,49 +99,49 @@ func (p Policy) ToChain() (*policyengine.Chain, error) { conditions = append(conditions, conds...) action, actionInverted := statement.action() - ruleAction := policyengine.Actions{Inverted: actionInverted, Names: action} + ruleAction := chain.Actions{Inverted: actionInverted, Names: action} resource, resourceInverted := statement.resource() - ruleResource := policyengine.Resources{Inverted: resourceInverted, Names: resource} + ruleResource := chain.Resources{Inverted: resourceInverted, Names: resource} - r := policyengine.Rule{ + r := chain.Rule{ Status: status, Actions: ruleAction, Resources: ruleResource, Any: true, Condition: conditions, } - chain.Rules = append(chain.Rules, r) + ch.Rules = append(ch.Rules, r) } - return &chain, nil + return &ch, nil } //nolint:funlen -func (c Conditions) ToChainCondition() ([]policyengine.Condition, error) { - var conditions []policyengine.Condition +func (c Conditions) ToChainCondition() ([]chain.Condition, error) { + var conditions []chain.Condition var convertValue convertFunction for op, KVs := range c { - var condType policyengine.ConditionType + var condType chain.ConditionType switch { case strings.HasPrefix(op, "String"): convertValue = noConvertFunction switch op { case CondStringEquals: - condType = policyengine.CondStringEquals + condType = chain.CondStringEquals case CondStringNotEquals: - condType = policyengine.CondStringNotEquals + condType = chain.CondStringNotEquals case CondStringEqualsIgnoreCase: - condType = policyengine.CondStringEqualsIgnoreCase + condType = chain.CondStringEqualsIgnoreCase case CondStringNotEqualsIgnoreCase: - condType = policyengine.CondStringNotEqualsIgnoreCase + condType = chain.CondStringNotEqualsIgnoreCase case CondStringLike: - condType = policyengine.CondStringLike + condType = chain.CondStringLike case CondStringNotLike: - condType = policyengine.CondStringNotLike + condType = chain.CondStringNotLike default: return nil, fmt.Errorf("unsupported condition operator: '%s'", op) } @@ -149,13 +149,13 @@ func (c Conditions) ToChainCondition() ([]policyengine.Condition, error) { convertValue = noConvertFunction switch op { case CondArnEquals: - condType = policyengine.CondStringEquals + condType = chain.CondStringEquals case CondArnNotEquals: - condType = policyengine.CondStringNotEquals + condType = chain.CondStringNotEquals case CondArnLike: - condType = policyengine.CondStringLike + condType = chain.CondStringLike case CondArnNotLike: - condType = policyengine.CondStringNotLike + condType = chain.CondStringNotLike default: return nil, fmt.Errorf("unsupported condition operator: '%s'", op) } @@ -165,33 +165,33 @@ func (c Conditions) ToChainCondition() ([]policyengine.Condition, error) { convertValue = dateConvertFunction switch op { case CondDateEquals: - condType = policyengine.CondStringEquals + condType = chain.CondStringEquals case CondDateNotEquals: - condType = policyengine.CondStringNotEquals + condType = chain.CondStringNotEquals case CondDateLessThan: - condType = policyengine.CondStringLessThan + condType = chain.CondStringLessThan case CondDateLessThanEquals: - condType = policyengine.CondStringLessThanEquals + condType = chain.CondStringLessThanEquals case CondDateGreaterThan: - condType = policyengine.CondStringGreaterThan + condType = chain.CondStringGreaterThan case CondDateGreaterThanEquals: - condType = policyengine.CondStringGreaterThanEquals + condType = chain.CondStringGreaterThanEquals default: return nil, fmt.Errorf("unsupported condition operator: '%s'", op) } case op == CondBool: convertValue = noConvertFunction - condType = policyengine.CondStringEqualsIgnoreCase + condType = chain.CondStringEqualsIgnoreCase case op == CondIPAddress: // todo consider using converters // "203.0.113.0/24" -> "203.0.113.*", // "2001:DB8:1234:5678::/64" -> "2001:DB8:1234:5678:*" // or having specific condition type for IP convertValue = noConvertFunction - condType = policyengine.CondStringLike + condType = chain.CondStringLike case op == CondNotIPAddress: convertValue = noConvertFunction - condType = policyengine.CondStringNotLike + condType = chain.CondStringNotLike default: return nil, fmt.Errorf("unsupported condition operator: '%s'", op) } @@ -203,9 +203,9 @@ func (c Conditions) ToChainCondition() ([]policyengine.Condition, error) { return nil, err } - conditions = append(conditions, policyengine.Condition{ + conditions = append(conditions, chain.Condition{ Op: condType, - Object: policyengine.ObjectRequest, + Object: chain.ObjectRequest, Key: key, Value: converted, }) diff --git a/iam/converter_test.go b/iam/converter_test.go index 3de9cb5..27fcc04 100644 --- a/iam/converter_test.go +++ b/iam/converter_test.go @@ -3,7 +3,7 @@ package iam import ( "testing" - policyengine "git.frostfs.info/TrueCloudLab/policy-engine" + chain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "github.com/stretchr/testify/require" ) @@ -26,22 +26,22 @@ func TestConverters(t *testing.T) { }}, } - expected := &policyengine.Chain{Rules: []policyengine.Rule{ + expected := &chain.Chain{Rules: []chain.Rule{ { - Status: policyengine.Allow, - Actions: policyengine.Actions{Names: p.Statement[0].Action}, - Resources: policyengine.Resources{Names: p.Statement[0].Resource}, + Status: chain.Allow, + Actions: chain.Actions{Names: p.Statement[0].Action}, + Resources: chain.Resources{Names: p.Statement[0].Resource}, Any: true, - Condition: []policyengine.Condition{ + Condition: []chain.Condition{ { - Op: policyengine.CondStringEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, Key: RequestOwnerProperty, Value: "arn:aws:iam::111122223333:user/JohnDoe", }, { - Op: policyengine.CondStringEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, Key: "s3:RequestObjectTag/Department", Value: "Finance", }, @@ -67,16 +67,16 @@ func TestConverters(t *testing.T) { }}, } - expected := &policyengine.Chain{Rules: []policyengine.Rule{ + expected := &chain.Chain{Rules: []chain.Rule{ { - Status: policyengine.AccessDenied, - Actions: policyengine.Actions{Inverted: true, Names: p.Statement[0].NotAction}, - Resources: policyengine.Resources{Inverted: true, Names: p.Statement[0].NotResource}, + Status: chain.AccessDenied, + Actions: chain.Actions{Inverted: true, Names: p.Statement[0].NotAction}, + Resources: chain.Resources{Inverted: true, Names: p.Statement[0].NotResource}, Any: true, - Condition: []policyengine.Condition{ + Condition: []chain.Condition{ { - Op: policyengine.CondStringNotEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, Key: RequestOwnerProperty, Value: "arn:aws:iam::111122223333:user/JohnDoe", }, @@ -154,136 +154,136 @@ func TestConverters(t *testing.T) { }}, } - expected := &policyengine.Chain{Rules: []policyengine.Rule{ + expected := &chain.Chain{Rules: []chain.Rule{ { - Status: policyengine.Allow, - Actions: policyengine.Actions{Names: p.Statement[0].Action}, - Resources: policyengine.Resources{Names: p.Statement[0].Resource}, + Status: chain.Allow, + Actions: chain.Actions{Names: p.Statement[0].Action}, + Resources: chain.Resources{Names: p.Statement[0].Resource}, Any: true, - Condition: []policyengine.Condition{ + Condition: []chain.Condition{ { - Op: policyengine.CondStringLike, - Object: policyengine.ObjectRequest, + Op: chain.CondStringLike, + Object: chain.ObjectRequest, Key: RequestOwnerProperty, Value: "*", }, { - Op: policyengine.CondStringEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, Key: "key1", Value: "val0", }, { - Op: policyengine.CondStringEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, Key: "key1", Value: "val1", }, { - Op: policyengine.CondStringNotEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, Key: "key2", Value: "val2", }, { - Op: policyengine.CondStringEqualsIgnoreCase, - Object: policyengine.ObjectRequest, + Op: chain.CondStringEqualsIgnoreCase, + Object: chain.ObjectRequest, Key: "key3", Value: "val3", }, { - Op: policyengine.CondStringNotEqualsIgnoreCase, - Object: policyengine.ObjectRequest, + Op: chain.CondStringNotEqualsIgnoreCase, + Object: chain.ObjectRequest, Key: "key4", Value: "val4", }, { - Op: policyengine.CondStringLike, - Object: policyengine.ObjectRequest, + Op: chain.CondStringLike, + Object: chain.ObjectRequest, Key: "key5", Value: "val5", }, { - Op: policyengine.CondStringNotLike, - Object: policyengine.ObjectRequest, + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, Key: "key6", Value: "val6", }, { - Op: policyengine.CondStringEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, Key: "key7", Value: "1136189045", }, { - Op: policyengine.CondStringNotEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, Key: "key8", Value: "1136214245", }, { - Op: policyengine.CondStringLessThan, - Object: policyengine.ObjectRequest, + Op: chain.CondStringLessThan, + Object: chain.ObjectRequest, Key: "key9", Value: "1136192645", }, { - Op: policyengine.CondStringLessThanEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringLessThanEquals, + Object: chain.ObjectRequest, Key: "key10", Value: "1136203445", }, { - Op: policyengine.CondStringGreaterThan, - Object: policyengine.ObjectRequest, + Op: chain.CondStringGreaterThan, + Object: chain.ObjectRequest, Key: "key11", Value: "1136217845", }, { - Op: policyengine.CondStringGreaterThanEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringGreaterThanEquals, + Object: chain.ObjectRequest, Key: "key12", Value: "1136225045", }, { - Op: policyengine.CondStringEqualsIgnoreCase, - Object: policyengine.ObjectRequest, + Op: chain.CondStringEqualsIgnoreCase, + Object: chain.ObjectRequest, Key: "key13", Value: "True", }, { - Op: policyengine.CondStringLike, - Object: policyengine.ObjectRequest, + Op: chain.CondStringLike, + Object: chain.ObjectRequest, Key: "key14", Value: "val14", }, { - Op: policyengine.CondStringNotLike, - Object: policyengine.ObjectRequest, + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, Key: "key15", Value: "val15", }, { - Op: policyengine.CondStringEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, Key: "key16", Value: "val16", }, { - Op: policyengine.CondStringLike, - Object: policyengine.ObjectRequest, + Op: chain.CondStringLike, + Object: chain.ObjectRequest, Key: "key17", Value: "val17", }, { - Op: policyengine.CondStringNotEquals, - Object: policyengine.ObjectRequest, + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, Key: "key18", Value: "val18", }, { - Op: policyengine.CondStringNotLike, - Object: policyengine.ObjectRequest, + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, Key: "key19", Value: "val19", }, diff --git a/inmemory.go b/inmemory.go deleted file mode 100644 index 50c146d..0000000 --- a/inmemory.go +++ /dev/null @@ -1,110 +0,0 @@ -package policyengine - -import ( - "git.frostfs.info/TrueCloudLab/policy-engine/util" -) - -type inmemory struct { - namespace map[Name][]chain - resource map[Name][]chain - local map[Name][]*Chain -} - -type chain struct { - object string - chain *Chain -} - -// NewInMemory returns new inmemory instance of chain storage. -func NewInMemory() CachedChainStorage { - return &inmemory{ - namespace: make(map[Name][]chain), - resource: make(map[Name][]chain), - local: make(map[Name][]*Chain), - } -} - -// IsAllowed implements the Engine interface. -func (s *inmemory) IsAllowed(name Name, namespace string, r Request) (Status, bool) { - var ruleFound bool - if local, ok := s.local[name]; ok { - for _, c := range local { - if status, matched := c.Match(r); matched && status != Allow { - return status, true - } - } - } - if cs, ok := s.namespace[name]; ok { - status, ok := matchArray(cs, namespace, r) - if ok && status != 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 Allow, true - } - return NoRuleFound, false -} - -func matchArray(cs []chain, object string, r Request) (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 NoRuleFound, false -} - -func (s *inmemory) AddResourceChain(name Name, resource string, c *Chain) { - s.resource[name] = append(s.resource[name], chain{resource, c}) -} - -func (s *inmemory) AddNameSpaceChain(name Name, namespace string, c *Chain) { - s.namespace[name] = append(s.namespace[name], chain{namespace, c}) -} - -func (s *inmemory) AddOverride(name Name, c *Chain) { - s.local[name] = append(s.local[name], c) -} - -func (s *inmemory) GetOverride(name Name, chainID ChainID) (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 Name, chainID ChainID) (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 Name) []*Chain { - return s.local[name] -} diff --git a/inmemory_test.go b/inmemory_test.go deleted file mode 100644 index b5be336..0000000 --- a/inmemory_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package policyengine - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestInmemory(t *testing.T) { - const ( - object = "native::object::abc/xyz" - container = "native::object::abc/*" - namespace = "Tenant1" - namespace2 = "Tenant2" - actor1 = "owner1" - actor2 = "owner2" - ) - - s := NewInMemory() - - // Object which was put via S3. - res := newResource(object, map[string]string{"FromS3": "true"}) - // Request initiating from the trusted subnet and actor. - reqGood := newRequest("native::object::put", res, map[string]string{ - "SourceIP": "10.1.1.12", - "Actor": actor1, - }) - - status, ok := s.IsAllowed(Ingress, namespace, reqGood) - require.Equal(t, NoRuleFound, status) - require.False(t, ok) - - s.AddNameSpaceChain(Ingress, namespace, &Chain{ - Rules: []Rule{ - { // Restrict to remove ANY object from the namespace. - Status: AccessDenied, - Actions: Actions{Names: []string{"native::object::delete"}}, - Resources: Resources{Names: []string{"native::object::*"}}, - }, - { // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise. - Status: AccessDenied, - Actions: Actions{Names: []string{"native::object::put"}}, - Resources: Resources{Names: []string{"native::object::*"}}, - Any: true, - Condition: []Condition{ - { - Op: CondStringNotLike, - Object: ObjectRequest, - Key: "SourceIP", - Value: "10.1.1.*", - }, - { - Op: CondStringNotEquals, - Object: ObjectRequest, - Key: "Actor", - Value: actor1, - }, - }, - }, - }, - }) - - s.AddNameSpaceChain(Ingress, namespace2, &Chain{ - Rules: []Rule{ - { // Deny all expect "native::object::get" for all objects expect "native::object::abc/xyz". - Status: AccessDenied, - Actions: Actions{Inverted: true, Names: []string{"native::object::get"}}, - Resources: Resources{Inverted: true, Names: []string{object}}, - }, - }, - }) - - s.AddResourceChain(Ingress, container, &Chain{ - Rules: []Rule{ - { // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute. - Status: Allow, - Actions: Actions{Names: []string{"native::object::get"}}, - Resources: Resources{Names: []string{"native::object::abc/*"}}, - Condition: []Condition{ - { - Op: CondStringEquals, - Object: ObjectResource, - Key: "Department", - Value: "HR", - }, - { - Op: CondStringEquals, - Object: ObjectRequest, - Key: "Actor", - Value: actor2, - }, - }, - }, - }, - }) - - t.Run("bad subnet, namespace deny", func(t *testing.T) { - // Request initiating from the untrusted subnet. - reqBadIP := newRequest("native::object::put", res, map[string]string{ - "SourceIP": "10.122.1.20", - "Actor": actor1, - }) - status, ok := s.IsAllowed(Ingress, namespace, reqBadIP) - require.Equal(t, AccessDenied, status) - require.True(t, ok) - }) - t.Run("bad actor, namespace deny", func(t *testing.T) { - // Request initiating from the untrusted actor. - reqBadActor := newRequest("native::object::put", res, map[string]string{ - "SourceIP": "10.1.1.13", - "Actor": actor2, - }) - status, ok := s.IsAllowed(Ingress, namespace, reqBadActor) - require.Equal(t, AccessDenied, status) - require.True(t, ok) - }) - t.Run("bad object, container deny", func(t *testing.T) { - objGood := newResource("native::object::abc/id1", map[string]string{"Department": "HR"}) - objBadAttr := newResource("native::object::abc/id2", map[string]string{"Department": "Support"}) - - status, ok := s.IsAllowed(Ingress, namespace, newRequest("native::object::get", objGood, map[string]string{ - "SourceIP": "10.1.1.14", - "Actor": actor2, - })) - require.Equal(t, Allow, status) - require.True(t, ok) - - status, ok = s.IsAllowed(Ingress, namespace, newRequest("native::object::get", objBadAttr, map[string]string{ - "SourceIP": "10.1.1.14", - "Actor": actor2, - })) - require.Equal(t, NoRuleFound, status) - require.False(t, ok) - }) - t.Run("bad operation, namespace deny", func(t *testing.T) { - // Request with the forbidden operation. - reqBadOperation := newRequest("native::object::delete", res, map[string]string{ - "SourceIP": "10.1.1.12", - "Actor": actor1, - }) - status, ok := s.IsAllowed(Ingress, namespace, reqBadOperation) - require.Equal(t, AccessDenied, status) - require.True(t, ok) - }) - t.Run("inverted rules", func(t *testing.T) { - req := newRequest("native::object::put", newResource(object, nil), nil) - status, ok = s.IsAllowed(Ingress, namespace2, req) - require.Equal(t, NoRuleFound, status) - require.False(t, ok) - - req = newRequest("native::object::put", newResource("native::object::cba/def", nil), nil) - status, ok = s.IsAllowed(Ingress, namespace2, req) - require.Equal(t, AccessDenied, status) - require.True(t, ok) - - req = newRequest("native::object::get", newResource("native::object::cba/def", nil), nil) - status, ok = s.IsAllowed(Ingress, namespace2, req) - require.Equal(t, NoRuleFound, status) - require.False(t, ok) - }) - t.Run("good", func(t *testing.T) { - status, ok = s.IsAllowed(Ingress, namespace, reqGood) - require.Equal(t, NoRuleFound, status) - require.False(t, ok) - - t.Run("quota on a different container", func(t *testing.T) { - s.AddOverride(Ingress, &Chain{ - Rules: []Rule{{ - Status: QuotaLimitReached, - Actions: Actions{Names: []string{"native::object::put"}}, - Resources: Resources{Names: []string{"native::object::cba/*"}}, - }}, - }) - - status, ok = s.IsAllowed(Ingress, namespace, reqGood) - require.Equal(t, NoRuleFound, status) - require.False(t, ok) - }) - t.Run("quota on the request container", func(t *testing.T) { - s.AddOverride(Ingress, &Chain{ - Rules: []Rule{{ - Status: QuotaLimitReached, - Actions: Actions{Names: []string{"native::object::put"}}, - Resources: Resources{Names: []string{"native::object::abc/*"}}, - }}, - }) - - status, ok = s.IsAllowed(Ingress, namespace, reqGood) - require.Equal(t, QuotaLimitReached, status) - require.True(t, ok) - }) - }) -} diff --git a/interface.go b/interface.go deleted file mode 100644 index d75a701..0000000 --- a/interface.go +++ /dev/null @@ -1,18 +0,0 @@ -package policyengine - -// CachedChainStorage ... -type CachedChainStorage interface { - Engine - // Adds a policy chain used for all operations with a specific resource. - AddResourceChain(name Name, resource string, c *Chain) - // Adds a policy chain used for all operations in the namespace. - AddNameSpaceChain(name Name, namespace string, c *Chain) - // Adds a local policy chain used for all operations with this service. - AddOverride(name Name, c *Chain) - // Gets the local override with given chain id. - GetOverride(name Name, chainID ChainID) (chain *Chain, found bool) - // Remove the local override with given chain id. - RemoveOverride(name Name, chainID ChainID) (removed bool) - // ListOverrides returns the list of local overrides. - ListOverrides(name Name) []*Chain -} diff --git a/chain.go b/pkg/chain/chain.go similarity index 86% rename from chain.go rename to pkg/chain/chain.go index cb17f70..a98ef43 100644 --- a/chain.go +++ b/pkg/chain/chain.go @@ -1,25 +1,19 @@ -package policyengine +package chain import ( "encoding/json" "fmt" "strings" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" "git.frostfs.info/TrueCloudLab/policy-engine/util" ) -// Engine ... -type Engine interface { - // IsAllowed returns status for the operation after all checks. - // The second return value signifies whether a matching rule was found. - IsAllowed(name Name, namespace string, r Request) (Status, bool) -} - -// ChainID is the ID of rule chain. -type ChainID string +// ID is the ID of rule chain. +type ID string type Chain struct { - ID ChainID + ID ID Rules []Rule } @@ -138,7 +132,7 @@ func (c ConditionType) String() string { } } -func (c *Condition) Match(req Request) bool { +func (c *Condition) Match(req resource.Request) bool { var val string switch c.Object { case ObjectResource: @@ -175,7 +169,7 @@ func (c *Condition) Match(req Request) bool { } } -func (r *Rule) Match(req Request) (status Status, matched bool) { +func (r *Rule) Match(req resource.Request) (status Status, matched bool) { found := len(r.Resources.Names) == 0 for i := range r.Resources.Names { if util.GlobMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted { @@ -194,14 +188,14 @@ func (r *Rule) Match(req Request) (status Status, matched bool) { return NoRuleFound, false } -func (r *Rule) matchCondition(obj Request) (status Status, matched bool) { +func (r *Rule) matchCondition(obj resource.Request) (status Status, matched bool) { if r.Any { return r.matchAny(obj) } return r.matchAll(obj) } -func (r *Rule) matchAny(obj Request) (status Status, matched bool) { +func (r *Rule) matchAny(obj resource.Request) (status Status, matched bool) { for i := range r.Condition { if r.Condition[i].Match(obj) { return r.Status, true @@ -210,7 +204,7 @@ func (r *Rule) matchAny(obj Request) (status Status, matched bool) { return NoRuleFound, false } -func (r *Rule) matchAll(obj Request) (status Status, matched bool) { +func (r *Rule) matchAll(obj resource.Request) (status Status, matched bool) { for i := range r.Condition { if !r.Condition[i].Match(obj) { return NoRuleFound, false @@ -219,7 +213,7 @@ func (r *Rule) matchAll(obj Request) (status Status, matched bool) { return r.Status, true } -func (c *Chain) Match(req Request) (status Status, matched bool) { +func (c *Chain) Match(req resource.Request) (status Status, matched bool) { for i := range c.Rules { status, matched := c.Rules[i].Match(req) if matched { diff --git a/chain_names.go b/pkg/chain/chain_names.go similarity index 92% rename from chain_names.go rename to pkg/chain/chain_names.go index 637b967..8bb88c0 100644 --- a/chain_names.go +++ b/pkg/chain/chain_names.go @@ -1,4 +1,4 @@ -package policyengine +package chain // Name represents the place in the request lifecycle where policy is applied. type Name string diff --git a/chain_test.go b/pkg/chain/chain_test.go similarity index 96% rename from chain_test.go rename to pkg/chain/chain_test.go index 690ec37..2c5ae9c 100644 --- a/chain_test.go +++ b/pkg/chain/chain_test.go @@ -1,4 +1,4 @@ -package policyengine +package chain import ( "testing" diff --git a/error.go b/pkg/chain/error.go similarity index 96% rename from error.go rename to pkg/chain/error.go index 3fc1c59..218e372 100644 --- a/error.go +++ b/pkg/chain/error.go @@ -1,4 +1,4 @@ -package policyengine +package chain import "fmt" diff --git a/pkg/engine/inmemory.go b/pkg/engine/inmemory.go new file mode 100644 index 0000000..c37fdd9 --- /dev/null +++ b/pkg/engine/inmemory.go @@ -0,0 +1,113 @@ +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_test.go b/pkg/engine/inmemory_test.go new file mode 100644 index 0000000..72e7fc0 --- /dev/null +++ b/pkg/engine/inmemory_test.go @@ -0,0 +1,195 @@ +package engine + +import ( + "testing" + + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + resourcetest "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" + "github.com/stretchr/testify/require" +) + +func TestInmemory(t *testing.T) { + const ( + object = "native::object::abc/xyz" + container = "native::object::abc/*" + namespace = "Tenant1" + namespace2 = "Tenant2" + actor1 = "owner1" + actor2 = "owner2" + ) + + s := NewInMemory() + + // 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, namespace, reqGood) + require.Equal(t, chain.NoRuleFound, status) + require.False(t, ok) + + s.AddNameSpaceChain(chain.Ingress, 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, + }, + }, + }, + }, + }) + + s.AddNameSpaceChain(chain.Ingress, 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}}, + }, + }, + }) + + s.AddResourceChain(chain.Ingress, 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, + }, + }, + }, + }, + }) + + 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, namespace, 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, namespace, 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, 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{ + "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, 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) + 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) + 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) + 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) + 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{ + Rules: []chain.Rule{{ + Status: chain.QuotaLimitReached, + Actions: chain.Actions{Names: []string{"native::object::put"}}, + Resources: chain.Resources{Names: []string{"native::object::cba/*"}}, + }}, + }) + + status, ok = s.IsAllowed(chain.Ingress, namespace, reqGood) + require.Equal(t, chain.NoRuleFound, status) + require.False(t, ok) + }) + t.Run("quota on the request container", func(t *testing.T) { + s.AddOverride(chain.Ingress, &chain.Chain{ + Rules: []chain.Rule{{ + Status: chain.QuotaLimitReached, + Actions: chain.Actions{Names: []string{"native::object::put"}}, + Resources: chain.Resources{Names: []string{"native::object::abc/*"}}, + }}, + }) + + status, ok = s.IsAllowed(chain.Ingress, namespace, reqGood) + require.Equal(t, chain.QuotaLimitReached, status) + require.True(t, ok) + }) + }) +} diff --git a/pkg/engine/interface.go b/pkg/engine/interface.go new file mode 100644 index 0000000..e753951 --- /dev/null +++ b/pkg/engine/interface.go @@ -0,0 +1,30 @@ +package engine + +import ( + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" +) + +// Engine ... +type Engine 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) +} + +// 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 +} diff --git a/resource.go b/pkg/resource/resource.go similarity index 96% rename from resource.go rename to pkg/resource/resource.go index 94187cd..7180f20 100644 --- a/resource.go +++ b/pkg/resource/resource.go @@ -1,4 +1,4 @@ -package policyengine +package resource // Request represents generic named resource (bucket, container etc.). // Name is resource depenent but should be globally unique for any given diff --git a/pkg/resource/testutil/resource.go b/pkg/resource/testutil/resource.go new file mode 100644 index 0000000..02e6394 --- /dev/null +++ b/pkg/resource/testutil/resource.go @@ -0,0 +1,53 @@ +package testutil + +import ( + resourcepkg "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" +) + +type Resource struct { + name string + properties map[string]string +} + +func (r *Resource) Name() string { + return r.name +} + +func (r *Resource) Property(name string) string { + return r.properties[name] +} + +func NewResource(name string, properties map[string]string) *Resource { + if properties == nil { + properties = make(map[string]string) + } + return &Resource{name: name, properties: properties} +} + +type Request struct { + operation string + properties map[string]string + resource *Resource +} + +var _ resourcepkg.Request = (*Request)(nil) + +func (r *Request) Operation() string { + return r.operation +} + +func (r *Request) Resource() resourcepkg.Resource { + return r.resource +} + +func (r *Request) Property(name string) string { + return r.properties[name] +} + +func NewRequest(op string, r *Resource, properties map[string]string) *Request { + return &Request{ + operation: op, + properties: properties, + resource: r, + } +} diff --git a/resource_test.go b/resource_test.go deleted file mode 100644 index ed68e3a..0000000 --- a/resource_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package policyengine - -type resource struct { - name string - properties map[string]string -} - -func (r *resource) Name() string { - return r.name -} - -func (r *resource) Property(name string) string { - return r.properties[name] -} - -func newResource(name string, properties map[string]string) *resource { - if properties == nil { - properties = make(map[string]string) - } - return &resource{name: name, properties: properties} -} - -type request struct { - operation string - properties map[string]string - resource *resource -} - -var _ Request = (*request)(nil) - -func (r *request) Operation() string { - return r.operation -} - -func (r *request) Resource() Resource { - return r.resource -} - -func (r *request) Property(name string) string { - return r.properties[name] -} - -func newRequest(op string, r *resource, properties map[string]string) *request { - return &request{ - operation: op, - properties: properties, - resource: r, - } -} -- 2.45.2 From e47af4b11159a231127cd3ad13beb379a7c21ed7 Mon Sep 17 00:00:00 2001 From: aarifullin Date: Tue, 7 Nov 2023 21:29:51 +0300 Subject: [PATCH 3/3] [#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 } -- 2.45.2