Compare commits

...

3 commits

Author SHA1 Message Date
aarifullin
e47af4b111 [#7] engine: Revise CachedChainStorage interface
All checks were successful
DCO action / DCO (pull_request) Successful in 1m3s
Tests and linters / Tests (1.21) (pull_request) Successful in 1m8s
Tests and linters / Tests (1.20) (pull_request) Successful in 1m27s
Tests and linters / Tests with -race (pull_request) Successful in 1m21s
Tests and linters / Staticcheck (pull_request) Successful in 1m28s
Tests and linters / Lint (pull_request) Successful in 2m9s
* Nuke out CachedChainStorage interface
* Introduce LocalOverrideStorage interface to manage
  local overrides
* Introduce MorphRuleChainStorage interface to manage
  chains in the policy contract
* Extend Engine interface

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-11-14 13:39:31 +03:00
aarifullin
6eb4a649c3 [#7] engine: Set project structure pattern for files
* Create pkg package
* Move chain-relates structures to pkg/chain package
* Move inmemory and interface files to pkg/engine package
* Move resource structures to pkg/resource package
* Move GlobMatch to util package

Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-11-14 10:55:20 +03:00
aarifullin
a064a01fdb [#7] engine: Move globMatch to common util package
Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
2023-11-14 10:55:20 +03:00
21 changed files with 1002 additions and 498 deletions

View file

@ -6,7 +6,7 @@ import (
"strings" "strings"
"time" "time"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
) )
const ( const (
@ -52,41 +52,41 @@ const (
CondArnNotLike string = "ArnNotLike" CondArnNotLike string = "ArnNotLike"
) )
func (p Policy) ToChain() (*policyengine.Chain, error) { func (p Policy) ToChain() (*chain.Chain, error) {
if err := p.Validate(GeneralPolicyType); err != nil { if err := p.Validate(GeneralPolicyType); err != nil {
return nil, err return nil, err
} }
var chain policyengine.Chain var ch chain.Chain
for _, statement := range p.Statement { for _, statement := range p.Statement {
status := policyengine.AccessDenied status := chain.AccessDenied
if statement.Effect == AllowEffect { if statement.Effect == AllowEffect {
status = policyengine.Allow status = chain.Allow
} }
var principals []string var principals []string
var op policyengine.ConditionType var op chain.ConditionType
statementPrincipal, inverted := statement.principal() statementPrincipal, inverted := statement.principal()
if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false
principals = []string{Wildcard} principals = []string{Wildcard}
op = policyengine.CondStringLike op = chain.CondStringLike
} else { } else {
for _, principal := range statementPrincipal { for _, principal := range statementPrincipal {
principals = append(principals, principal...) principals = append(principals, principal...)
} }
op = policyengine.CondStringEquals op = chain.CondStringEquals
if inverted { if inverted {
op = policyengine.CondStringNotEquals op = chain.CondStringNotEquals
} }
} }
var conditions []policyengine.Condition var conditions []chain.Condition
for _, principal := range principals { for _, principal := range principals {
conditions = append(conditions, policyengine.Condition{ conditions = append(conditions, chain.Condition{
Op: op, Op: op,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: RequestOwnerProperty, Key: RequestOwnerProperty,
Value: principal, Value: principal,
}) })
@ -99,49 +99,49 @@ func (p Policy) ToChain() (*policyengine.Chain, error) {
conditions = append(conditions, conds...) conditions = append(conditions, conds...)
action, actionInverted := statement.action() action, actionInverted := statement.action()
ruleAction := policyengine.Actions{Inverted: actionInverted, Names: action} ruleAction := chain.Actions{Inverted: actionInverted, Names: action}
resource, resourceInverted := statement.resource() 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, Status: status,
Actions: ruleAction, Actions: ruleAction,
Resources: ruleResource, Resources: ruleResource,
Any: true, Any: true,
Condition: conditions, Condition: conditions,
} }
chain.Rules = append(chain.Rules, r) ch.Rules = append(ch.Rules, r)
} }
return &chain, nil return &ch, nil
} }
//nolint:funlen //nolint:funlen
func (c Conditions) ToChainCondition() ([]policyengine.Condition, error) { func (c Conditions) ToChainCondition() ([]chain.Condition, error) {
var conditions []policyengine.Condition var conditions []chain.Condition
var convertValue convertFunction var convertValue convertFunction
for op, KVs := range c { for op, KVs := range c {
var condType policyengine.ConditionType var condType chain.ConditionType
switch { switch {
case strings.HasPrefix(op, "String"): case strings.HasPrefix(op, "String"):
convertValue = noConvertFunction convertValue = noConvertFunction
switch op { switch op {
case CondStringEquals: case CondStringEquals:
condType = policyengine.CondStringEquals condType = chain.CondStringEquals
case CondStringNotEquals: case CondStringNotEquals:
condType = policyengine.CondStringNotEquals condType = chain.CondStringNotEquals
case CondStringEqualsIgnoreCase: case CondStringEqualsIgnoreCase:
condType = policyengine.CondStringEqualsIgnoreCase condType = chain.CondStringEqualsIgnoreCase
case CondStringNotEqualsIgnoreCase: case CondStringNotEqualsIgnoreCase:
condType = policyengine.CondStringNotEqualsIgnoreCase condType = chain.CondStringNotEqualsIgnoreCase
case CondStringLike: case CondStringLike:
condType = policyengine.CondStringLike condType = chain.CondStringLike
case CondStringNotLike: case CondStringNotLike:
condType = policyengine.CondStringNotLike condType = chain.CondStringNotLike
default: default:
return nil, fmt.Errorf("unsupported condition operator: '%s'", op) return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
} }
@ -149,13 +149,13 @@ func (c Conditions) ToChainCondition() ([]policyengine.Condition, error) {
convertValue = noConvertFunction convertValue = noConvertFunction
switch op { switch op {
case CondArnEquals: case CondArnEquals:
condType = policyengine.CondStringEquals condType = chain.CondStringEquals
case CondArnNotEquals: case CondArnNotEquals:
condType = policyengine.CondStringNotEquals condType = chain.CondStringNotEquals
case CondArnLike: case CondArnLike:
condType = policyengine.CondStringLike condType = chain.CondStringLike
case CondArnNotLike: case CondArnNotLike:
condType = policyengine.CondStringNotLike condType = chain.CondStringNotLike
default: default:
return nil, fmt.Errorf("unsupported condition operator: '%s'", op) return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
} }
@ -165,33 +165,33 @@ func (c Conditions) ToChainCondition() ([]policyengine.Condition, error) {
convertValue = dateConvertFunction convertValue = dateConvertFunction
switch op { switch op {
case CondDateEquals: case CondDateEquals:
condType = policyengine.CondStringEquals condType = chain.CondStringEquals
case CondDateNotEquals: case CondDateNotEquals:
condType = policyengine.CondStringNotEquals condType = chain.CondStringNotEquals
case CondDateLessThan: case CondDateLessThan:
condType = policyengine.CondStringLessThan condType = chain.CondStringLessThan
case CondDateLessThanEquals: case CondDateLessThanEquals:
condType = policyengine.CondStringLessThanEquals condType = chain.CondStringLessThanEquals
case CondDateGreaterThan: case CondDateGreaterThan:
condType = policyengine.CondStringGreaterThan condType = chain.CondStringGreaterThan
case CondDateGreaterThanEquals: case CondDateGreaterThanEquals:
condType = policyengine.CondStringGreaterThanEquals condType = chain.CondStringGreaterThanEquals
default: default:
return nil, fmt.Errorf("unsupported condition operator: '%s'", op) return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
} }
case op == CondBool: case op == CondBool:
convertValue = noConvertFunction convertValue = noConvertFunction
condType = policyengine.CondStringEqualsIgnoreCase condType = chain.CondStringEqualsIgnoreCase
case op == CondIPAddress: case op == CondIPAddress:
// todo consider using converters // todo consider using converters
// "203.0.113.0/24" -> "203.0.113.*", // "203.0.113.0/24" -> "203.0.113.*",
// "2001:DB8:1234:5678::/64" -> "2001:DB8:1234:5678:*" // "2001:DB8:1234:5678::/64" -> "2001:DB8:1234:5678:*"
// or having specific condition type for IP // or having specific condition type for IP
convertValue = noConvertFunction convertValue = noConvertFunction
condType = policyengine.CondStringLike condType = chain.CondStringLike
case op == CondNotIPAddress: case op == CondNotIPAddress:
convertValue = noConvertFunction convertValue = noConvertFunction
condType = policyengine.CondStringNotLike condType = chain.CondStringNotLike
default: default:
return nil, fmt.Errorf("unsupported condition operator: '%s'", op) return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
} }
@ -203,9 +203,9 @@ func (c Conditions) ToChainCondition() ([]policyengine.Condition, error) {
return nil, err return nil, err
} }
conditions = append(conditions, policyengine.Condition{ conditions = append(conditions, chain.Condition{
Op: condType, Op: condType,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: key, Key: key,
Value: converted, Value: converted,
}) })

View file

@ -3,7 +3,7 @@ package iam
import ( import (
"testing" "testing"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine" chain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"github.com/stretchr/testify/require" "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, Status: chain.Allow,
Actions: policyengine.Actions{Names: p.Statement[0].Action}, Actions: chain.Actions{Names: p.Statement[0].Action},
Resources: policyengine.Resources{Names: p.Statement[0].Resource}, Resources: chain.Resources{Names: p.Statement[0].Resource},
Any: true, Any: true,
Condition: []policyengine.Condition{ Condition: []chain.Condition{
{ {
Op: policyengine.CondStringEquals, Op: chain.CondStringEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: RequestOwnerProperty, Key: RequestOwnerProperty,
Value: "arn:aws:iam::111122223333:user/JohnDoe", Value: "arn:aws:iam::111122223333:user/JohnDoe",
}, },
{ {
Op: policyengine.CondStringEquals, Op: chain.CondStringEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "s3:RequestObjectTag/Department", Key: "s3:RequestObjectTag/Department",
Value: "Finance", 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, Status: chain.AccessDenied,
Actions: policyengine.Actions{Inverted: true, Names: p.Statement[0].NotAction}, Actions: chain.Actions{Inverted: true, Names: p.Statement[0].NotAction},
Resources: policyengine.Resources{Inverted: true, Names: p.Statement[0].NotResource}, Resources: chain.Resources{Inverted: true, Names: p.Statement[0].NotResource},
Any: true, Any: true,
Condition: []policyengine.Condition{ Condition: []chain.Condition{
{ {
Op: policyengine.CondStringNotEquals, Op: chain.CondStringNotEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: RequestOwnerProperty, Key: RequestOwnerProperty,
Value: "arn:aws:iam::111122223333:user/JohnDoe", 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, Status: chain.Allow,
Actions: policyengine.Actions{Names: p.Statement[0].Action}, Actions: chain.Actions{Names: p.Statement[0].Action},
Resources: policyengine.Resources{Names: p.Statement[0].Resource}, Resources: chain.Resources{Names: p.Statement[0].Resource},
Any: true, Any: true,
Condition: []policyengine.Condition{ Condition: []chain.Condition{
{ {
Op: policyengine.CondStringLike, Op: chain.CondStringLike,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: RequestOwnerProperty, Key: RequestOwnerProperty,
Value: "*", Value: "*",
}, },
{ {
Op: policyengine.CondStringEquals, Op: chain.CondStringEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key1", Key: "key1",
Value: "val0", Value: "val0",
}, },
{ {
Op: policyengine.CondStringEquals, Op: chain.CondStringEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key1", Key: "key1",
Value: "val1", Value: "val1",
}, },
{ {
Op: policyengine.CondStringNotEquals, Op: chain.CondStringNotEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key2", Key: "key2",
Value: "val2", Value: "val2",
}, },
{ {
Op: policyengine.CondStringEqualsIgnoreCase, Op: chain.CondStringEqualsIgnoreCase,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key3", Key: "key3",
Value: "val3", Value: "val3",
}, },
{ {
Op: policyengine.CondStringNotEqualsIgnoreCase, Op: chain.CondStringNotEqualsIgnoreCase,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key4", Key: "key4",
Value: "val4", Value: "val4",
}, },
{ {
Op: policyengine.CondStringLike, Op: chain.CondStringLike,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key5", Key: "key5",
Value: "val5", Value: "val5",
}, },
{ {
Op: policyengine.CondStringNotLike, Op: chain.CondStringNotLike,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key6", Key: "key6",
Value: "val6", Value: "val6",
}, },
{ {
Op: policyengine.CondStringEquals, Op: chain.CondStringEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key7", Key: "key7",
Value: "1136189045", Value: "1136189045",
}, },
{ {
Op: policyengine.CondStringNotEquals, Op: chain.CondStringNotEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key8", Key: "key8",
Value: "1136214245", Value: "1136214245",
}, },
{ {
Op: policyengine.CondStringLessThan, Op: chain.CondStringLessThan,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key9", Key: "key9",
Value: "1136192645", Value: "1136192645",
}, },
{ {
Op: policyengine.CondStringLessThanEquals, Op: chain.CondStringLessThanEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key10", Key: "key10",
Value: "1136203445", Value: "1136203445",
}, },
{ {
Op: policyengine.CondStringGreaterThan, Op: chain.CondStringGreaterThan,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key11", Key: "key11",
Value: "1136217845", Value: "1136217845",
}, },
{ {
Op: policyengine.CondStringGreaterThanEquals, Op: chain.CondStringGreaterThanEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key12", Key: "key12",
Value: "1136225045", Value: "1136225045",
}, },
{ {
Op: policyengine.CondStringEqualsIgnoreCase, Op: chain.CondStringEqualsIgnoreCase,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key13", Key: "key13",
Value: "True", Value: "True",
}, },
{ {
Op: policyengine.CondStringLike, Op: chain.CondStringLike,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key14", Key: "key14",
Value: "val14", Value: "val14",
}, },
{ {
Op: policyengine.CondStringNotLike, Op: chain.CondStringNotLike,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key15", Key: "key15",
Value: "val15", Value: "val15",
}, },
{ {
Op: policyengine.CondStringEquals, Op: chain.CondStringEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key16", Key: "key16",
Value: "val16", Value: "val16",
}, },
{ {
Op: policyengine.CondStringLike, Op: chain.CondStringLike,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key17", Key: "key17",
Value: "val17", Value: "val17",
}, },
{ {
Op: policyengine.CondStringNotEquals, Op: chain.CondStringNotEquals,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key18", Key: "key18",
Value: "val18", Value: "val18",
}, },
{ {
Op: policyengine.CondStringNotLike, Op: chain.CondStringNotLike,
Object: policyengine.ObjectRequest, Object: chain.ObjectRequest,
Key: "key19", Key: "key19",
Value: "val19", Value: "val19",
}, },

View file

@ -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]
}

View file

@ -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)
})
})
}

View file

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

View file

@ -1,23 +1,19 @@
package policyengine package chain
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
"git.frostfs.info/TrueCloudLab/policy-engine/util"
) )
// Engine ... // ID is the ID of rule chain.
type Engine interface { type ID string
// 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
type Chain struct { type Chain struct {
ID ChainID ID ID
Rules []Rule 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 var val string
switch c.Object { switch c.Object {
case ObjectResource: case ObjectResource:
@ -159,9 +155,9 @@ func (c *Condition) Match(req Request) bool {
case CondStringNotEqualsIgnoreCase: case CondStringNotEqualsIgnoreCase:
return !strings.EqualFold(val, c.Value) return !strings.EqualFold(val, c.Value)
case CondStringLike: case CondStringLike:
return globMatch(val, c.Value) return util.GlobMatch(val, c.Value)
case CondStringNotLike: case CondStringNotLike:
return !globMatch(val, c.Value) return !util.GlobMatch(val, c.Value)
case CondStringLessThan: case CondStringLessThan:
return val < c.Value return val < c.Value
case CondStringLessThanEquals: 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 found := len(r.Resources.Names) == 0
for i := range r.Resources.Names { 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 found = true
break break
} }
@ -185,21 +181,21 @@ func (r *Rule) Match(req Request) (status Status, matched bool) {
return NoRuleFound, false return NoRuleFound, false
} }
for i := range r.Actions.Names { 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 r.matchCondition(req)
} }
} }
return NoRuleFound, false 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 { if r.Any {
return r.matchAny(obj) return r.matchAny(obj)
} }
return r.matchAll(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 { for i := range r.Condition {
if r.Condition[i].Match(obj) { if r.Condition[i].Match(obj) {
return r.Status, true return r.Status, true
@ -208,7 +204,7 @@ func (r *Rule) matchAny(obj Request) (status Status, matched bool) {
return NoRuleFound, false 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 { for i := range r.Condition {
if !r.Condition[i].Match(obj) { if !r.Condition[i].Match(obj) {
return NoRuleFound, false return NoRuleFound, false
@ -217,7 +213,7 @@ func (r *Rule) matchAll(obj Request) (status Status, matched bool) {
return r.Status, true 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 { for i := range c.Rules {
status, matched := c.Rules[i].Match(req) status, matched := c.Rules[i].Match(req)
if matched { if matched {

View file

@ -1,4 +1,4 @@
package policyengine package chain
// Name represents the place in the request lifecycle where policy is applied. // Name represents the place in the request lifecycle where policy is applied.
type Name string type Name string

View file

@ -1,4 +1,4 @@
package policyengine package chain
import ( import (
"testing" "testing"

View file

@ -1,4 +1,4 @@
package policyengine package chain
import "fmt" import "fmt"

101
pkg/engine/chain_router.go Normal file
View file

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

10
pkg/engine/errors.go Normal file
View file

@ -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")
)

View file

@ -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)
}

View file

@ -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)
})
})
}

View file

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

View file

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

View file

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

78
pkg/engine/interface.go Normal file
View file

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

View file

@ -1,4 +1,4 @@
package policyengine package resource
// Request represents generic named resource (bucket, container etc.). // Request represents generic named resource (bucket, container etc.).
// Name is resource depenent but should be globally unique for any given // Name is resource depenent but should be globally unique for any given

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package policyengine package util
import ( import (
"strings" "strings"
@ -9,7 +9,7 @@ import (
// ? in pattern correspond to any symbol. // ? in pattern correspond to any symbol.
// * in pattern correspond to any sequence of symbols. // * in pattern correspond to any sequence of symbols.
// Currently only '*' in the suffix is supported. // Currently only '*' in the suffix is supported.
func globMatch(s, pattern string) bool { func GlobMatch(s, pattern string) bool {
index := strings.IndexByte(pattern, '*') index := strings.IndexByte(pattern, '*')
switch index { switch index {
default: default: