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, - } -}