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 5bd4f4c..0000000 --- a/inmemory.go +++ /dev/null @@ -1,106 +0,0 @@ -package policyengine - -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 !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 81% rename from chain.go rename to pkg/chain/chain.go index 9be3cde..a98ef43 100644 --- a/chain.go +++ b/pkg/chain/chain.go @@ -1,23 +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 } @@ -136,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: @@ -159,9 +155,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: @@ -173,10 +169,10 @@ 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 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,21 +181,21 @@ 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) } } 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 @@ -208,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 @@ -217,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/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/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/inmemory_test.go b/pkg/engine/inmemory/inmemory_test.go new file mode 100644 index 0000000..bc47313 --- /dev/null +++ b/pkg/engine/inmemory/inmemory_test.go @@ -0,0 +1,206 @@ +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" +) + +func TestInmemory(t *testing.T) { + const ( + object = "native::object::abc/xyz" + container = "native::object::abc/*" + namespace = "Tenant1" + namespace2 = "Tenant2" + actor1 = "owner1" + actor2 = "owner2" + ) + + s := NewInMemoryLocalOverrides() + + // Object which was put via S3. + res := resourcetest.NewResource(object, map[string]string{"FromS3": "true"}) + // Request initiating from the trusted subnet and actor. + reqGood := resourcetest.NewRequest("native::object::put", res, map[string]string{ + "SourceIP": "10.1.1.12", + "Actor": actor1, + }) + + status, ok, _ := s.IsAllowed(chain.Ingress, namespace, reqGood) + require.Equal(t, chain.NoRuleFound, status) + require.False(t, ok) + + s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace), &chain.Chain{ + Rules: []chain.Rule{ + { // Restrict to remove ANY object from the namespace. + Status: chain.AccessDenied, + Actions: chain.Actions{Names: []string{"native::object::delete"}}, + Resources: chain.Resources{Names: []string{"native::object::*"}}, + }, + { // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise. + Status: chain.AccessDenied, + Actions: chain.Actions{Names: []string{"native::object::put"}}, + Resources: chain.Resources{Names: []string{"native::object::*"}}, + Any: true, + Condition: []chain.Condition{ + { + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, + Key: "SourceIP", + Value: "10.1.1.*", + }, + { + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, + Key: "Actor", + Value: actor1, + }, + }, + }, + }, + }) + + s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace2), &chain.Chain{ + Rules: []chain.Rule{ + { // Deny all expect "native::object::get" for all objects expect "native::object::abc/xyz". + Status: chain.AccessDenied, + Actions: chain.Actions{Inverted: true, Names: []string{"native::object::get"}}, + Resources: chain.Resources{Inverted: true, Names: []string{object}}, + }, + }, + }) + + s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(container), &chain.Chain{ + Rules: []chain.Rule{ + { // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute. + Status: chain.Allow, + Actions: chain.Actions{Names: []string{"native::object::get"}}, + Resources: chain.Resources{Names: []string{"native::object::abc/*"}}, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Object: chain.ObjectResource, + Key: "Department", + Value: "HR", + }, + { + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "Actor", + Value: actor2, + }, + }, + }, + }, + }) + + 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.LocalStorage().AddOverride(chain.Ingress, container, &chain.Chain{ + Rules: []chain.Rule{{ + Status: chain.QuotaLimitReached, + Actions: chain.Actions{Names: []string{"native::object::put"}}, + Resources: chain.Resources{Names: []string{"native::object::cba/*"}}, + }}, + }) + + 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) { + quotaRuleChainID, _ = s.LocalStorage().AddOverride(chain.Ingress, container, &chain.Chain{ + Rules: []chain.Rule{{ + Status: chain.QuotaLimitReached, + Actions: chain.Actions{Names: []string{"native::object::put"}}, + Resources: chain.Resources{Names: []string{"native::object::abc/*"}}, + }}, + }) + + 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 new file mode 100644 index 0000000..fe0b4a7 --- /dev/null +++ b/pkg/engine/interface.go @@ -0,0 +1,78 @@ +package engine + +import ( + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" +) + +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, target string, r resource.Request) (status chain.Status, found bool, err error) +} + +// 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 +} 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, - } -} 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: