[#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
21 changed files with 1002 additions and 498 deletions

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,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 (
"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
}
@ -136,7 +132,7 @@ func (c ConditionType) String() string {
}
}
func (c *Condition) Match(req Request) bool {
func (c *Condition) Match(req resource.Request) bool {
var val string
switch c.Object {
case ObjectResource:
@ -159,9 +155,9 @@ func (c *Condition) Match(req Request) bool {
case CondStringNotEqualsIgnoreCase:
return !strings.EqualFold(val, c.Value)
case CondStringLike:
return globMatch(val, c.Value)
return util.GlobMatch(val, c.Value)
case CondStringNotLike:
return !globMatch(val, c.Value)
return !util.GlobMatch(val, c.Value)
case CondStringLessThan:
return val < c.Value
case CondStringLessThanEquals:
@ -173,10 +169,10 @@ func (c *Condition) Match(req Request) bool {
}
}
func (r *Rule) Match(req Request) (status Status, matched bool) {
func (r *Rule) Match(req resource.Request) (status Status, matched bool) {
found := len(r.Resources.Names) == 0
for i := range r.Resources.Names {
if globMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted {
if util.GlobMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted {
found = true
break
}
@ -185,21 +181,21 @@ func (r *Rule) Match(req Request) (status Status, matched bool) {
return NoRuleFound, false
}
for i := range r.Actions.Names {
if globMatch(req.Operation(), r.Actions.Names[i]) != r.Actions.Inverted {
if util.GlobMatch(req.Operation(), r.Actions.Names[i]) != r.Actions.Inverted {
return r.matchCondition(req)
}
}
return NoRuleFound, false
}
func (r *Rule) matchCondition(obj Request) (status Status, matched bool) {
func (r *Rule) matchCondition(obj resource.Request) (status Status, matched bool) {
if r.Any {
return r.matchAny(obj)
}
return r.matchAll(obj)
}
func (r *Rule) matchAny(obj Request) (status Status, matched bool) {
func (r *Rule) matchAny(obj resource.Request) (status Status, matched bool) {
for i := range r.Condition {
if r.Condition[i].Match(obj) {
return r.Status, true
@ -208,7 +204,7 @@ func (r *Rule) matchAny(obj Request) (status Status, matched bool) {
return NoRuleFound, false
}
func (r *Rule) matchAll(obj Request) (status Status, matched bool) {
func (r *Rule) matchAll(obj resource.Request) (status Status, matched bool) {
for i := range r.Condition {
if !r.Condition[i].Match(obj) {
return NoRuleFound, false
@ -217,7 +213,7 @@ func (r *Rule) matchAll(obj Request) (status Status, matched bool) {
return r.Status, true
}
func (c *Chain) Match(req Request) (status Status, matched bool) {
func (c *Chain) Match(req resource.Request) (status Status, matched bool) {
for i := range c.Rules {
status, matched := c.Rules[i].Match(req)
if matched {

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"

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

@TrueCloudLab/storage-core-developers

Even I tried to move to this file with git mv it's showed as the new created file because I changed the file in next commits. But I want to emphasize I have not changed the logical structure of the unittest and changed only signatures

@TrueCloudLab/storage-core-developers Even I tried to move to this file with `git mv` it's showed as the new created file because I changed the file in next commits. But I want to **emphasize** I have **not** changed the logical structure of the unittest and changed only signatures
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 {

Does chain id have a specific structure?

Does chain id have a specific structure?

No, it does not.

I glimpsed at the chain names in policy contract unit-tests and found the idea to name a chain like ingress:resource:name more useful than a numeric ID. Anyway, inmemory implementation will only be used for a while and will be kept only for unit-tests. I think such names are fine and readable

No, it does not. I glimpsed at the chain names in policy contract unit-tests and found the idea to name a chain like `ingress:resource:name` more useful than a numeric ID. Anyway, `inmemory` implementation will only be used for a while and will be kept only for unit-tests. I think such names are fine and readable

Anyway, inmemory implementation will only be used for a while and will be kept only for unit-tests.

Will we have some in-memory implementation for LocalOverrideStorage that can be used in s3-gw/node? Or it's expected that s3-gw and node must implement their own LocalOverrideStorage ?

> Anyway, inmemory implementation will only be used for a while and will be kept only for unit-tests. Will we have some in-memory implementation for `LocalOverrideStorage` that can be used in s3-gw/node? Or it's expected that s3-gw and node must implement their own `LocalOverrideStorage` ?

Good question.
It assumed that LocalOverrideStorage should be implemented by each service separately. For example, frostfs-node will use DB to keep and retrieve local overrides that doesn't seem applicable for s3-gw.

But I am wondering was it implied to define local overrides for s3-gw? If it was not, then probably it makes sense to separate the interface:

type Engine interface {
   ChainRouter

   MorphRuleChainStorage() MorphRuleChainStorage
}

type EngineWithLocalOverrides interface {
    Engine
    
    LocalStorage() LocalOverrideStorage
}
Good question. It assumed that `LocalOverrideStorage` should be implemented by each service separately. For example, frostfs-node will use DB to keep and retrieve local overrides that doesn't seem applicable for s3-gw. But I am wondering was it implied to define local overrides for `s3-gw`? If it was not, then probably it makes sense to separate the interface: ```golang type Engine interface { ChainRouter MorphRuleChainStorage() MorphRuleChainStorage } type EngineWithLocalOverrides interface { Engine LocalStorage() LocalOverrideStorage } ```

But I am wondering was it implied to define local overrides for s3-gw?

I'm not sure. It seems we will have only some cache layer, but it be a part of MorphRuleChainStorage implementation I think

@alexvanin

> But I am wondering was it implied to define local overrides for s3-gw? I'm not sure. It seems we will have only some cache layer, but it be a part of `MorphRuleChainStorage` implementation I think @alexvanin

I have splitted the interface into two: LocalOverrideEngine and Engine (the first one contains the second). Please, check

I have splitted the interface into two: `LocalOverrideEngine` and `Engine` (the first one contains the second). Please, check
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.).
// 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,
}
}

View file

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