[#7] engine: Revise storage interface #15

Merged
fyrchik merged 3 commits from aarifullin/policy-engine:feature/7-revise_iface into master 2023-11-15 09:22:43 +00:00
15 changed files with 511 additions and 496 deletions
Showing only changes of commit 6eb4a649c3 - Show all commits

View file

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

View file

@ -3,7 +3,7 @@ package iam
import (
"testing"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
chain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
fyrchik marked this conversation as resolved Outdated

Why do we use alias here? In the previous version it was because of the hyphen.

Why do we use alias here? In the previous version it was because of the hyphen.

Because tests use

var chain chain.Chain

So, either we need to make either an alias for the package or rename chain. Would we prefer the second option?

Because tests use ```golang var chain chain.Chain ``` So, either we need to make either an alias for the package or rename `chain`. Would we prefer the second option?

I think the second option is better. I will fix

I think the second option is better. I will fix
"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",
},

View file

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

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,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
fyrchik marked this conversation as resolved Outdated

Why is it a pointer? Empty value can equal to missing ID is is not a valid name.

Why is it a pointer? Empty value can equal to missing ID is is not a valid name.

Fixed

Fixed
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 {

View file

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

View file

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

View file

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

113
pkg/engine/inmemory.go Normal file
View file

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

195
pkg/engine/inmemory_test.go Normal file
View file

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

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

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

View file

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

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 {
fyrchik marked this conversation as resolved Outdated

Why do both request and resource accept a map now?

Why do both request and resource accept a map now?

I don't know. I just moved the file and made constructors importable :) (new.. -> New...). What is your concern about these maps?

Also, this structs are usable only for unit-tests witihn policy-engine

I don't know. I just moved the file and made constructors importable :) (`new..` -> `New...`). What is your concern about these maps? Also, this structs are usable only for unit-tests witihn policy-engine

Oh, I see, missed testutil package

Oh, I see, missed testutil package
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,
}
}