forked from TrueCloudLab/policy-engine
[#7] engine: Revise CachedChainStorage interface
* Nuke out CachedChainStorage interface * Introduce LocalOverrideStorage interface to manage local overrides * Introduce MorphRuleChainStorage interface to manage chains in the policy contract * Extend Engine interface Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
This commit is contained in:
parent
a08f600d97
commit
17453d3cda
9 changed files with 633 additions and 150 deletions
101
pkg/engine/chain_router.go
Normal file
101
pkg/engine/chain_router.go
Normal 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
10
pkg/engine/errors.go
Normal 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")
|
||||
)
|
|
@ -1,113 +0,0 @@
|
|||
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]
|
||||
}
|
48
pkg/engine/inmemory/inmemory.go
Normal file
48
pkg/engine/inmemory/inmemory.go
Normal 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)
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
package engine
|
||||
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"
|
||||
)
|
||||
|
@ -18,7 +19,7 @@ func TestInmemory(t *testing.T) {
|
|||
actor2 = "owner2"
|
||||
)
|
||||
|
||||
s := NewInMemory()
|
||||
s := NewInMemoryLocalOverrides()
|
||||
|
||||
// Object which was put via S3.
|
||||
res := resourcetest.NewResource(object, map[string]string{"FromS3": "true"})
|
||||
|
@ -28,11 +29,11 @@ func TestInmemory(t *testing.T) {
|
|||
"Actor": actor1,
|
||||
})
|
||||
|
||||
status, ok := s.IsAllowed(chain.Ingress, namespace, reqGood)
|
||||
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{
|
||||
s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(namespace), &chain.Chain{
|
||||
Rules: []chain.Rule{
|
||||
{ // Restrict to remove ANY object from the namespace.
|
||||
Status: chain.AccessDenied,
|
||||
|
@ -62,7 +63,7 @@ func TestInmemory(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
s.AddNameSpaceChain(chain.Ingress, namespace2, &chain.Chain{
|
||||
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,
|
||||
|
@ -72,7 +73,7 @@ func TestInmemory(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
s.AddResourceChain(chain.Ingress, container, &chain.Chain{
|
||||
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,
|
||||
|
@ -102,7 +103,7 @@ func TestInmemory(t *testing.T) {
|
|||
"SourceIP": "10.122.1.20",
|
||||
"Actor": actor1,
|
||||
})
|
||||
status, ok := s.IsAllowed(chain.Ingress, namespace, reqBadIP)
|
||||
status, ok, _ := s.IsAllowed(chain.Ingress, namespace, reqBadIP)
|
||||
require.Equal(t, chain.AccessDenied, status)
|
||||
require.True(t, ok)
|
||||
})
|
||||
|
@ -112,7 +113,7 @@ func TestInmemory(t *testing.T) {
|
|||
"SourceIP": "10.1.1.13",
|
||||
"Actor": actor2,
|
||||
})
|
||||
status, ok := s.IsAllowed(chain.Ingress, namespace, reqBadActor)
|
||||
status, ok, _ := s.IsAllowed(chain.Ingress, namespace, reqBadActor)
|
||||
require.Equal(t, chain.AccessDenied, status)
|
||||
require.True(t, ok)
|
||||
})
|
||||
|
@ -120,14 +121,14 @@ func TestInmemory(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{
|
||||
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{
|
||||
status, ok, _ = s.IsAllowed(chain.Ingress, namespace, resourcetest.NewRequest("native::object::get", objBadAttr, map[string]string{
|
||||
"SourceIP": "10.1.1.14",
|
||||
"Actor": actor2,
|
||||
}))
|
||||
|
@ -140,33 +141,33 @@ func TestInmemory(t *testing.T) {
|
|||
"SourceIP": "10.1.1.12",
|
||||
"Actor": actor1,
|
||||
})
|
||||
status, ok := s.IsAllowed(chain.Ingress, namespace, reqBadOperation)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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{
|
||||
s.LocalStorage().AddOverride(chain.Ingress, container, &chain.Chain{
|
||||
Rules: []chain.Rule{{
|
||||
Status: chain.QuotaLimitReached,
|
||||
Actions: chain.Actions{Names: []string{"native::object::put"}},
|
||||
|
@ -174,12 +175,14 @@ func TestInmemory(t *testing.T) {
|
|||
}},
|
||||
})
|
||||
|
||||
status, ok = s.IsAllowed(chain.Ingress, namespace, reqGood)
|
||||
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) {
|
||||
s.AddOverride(chain.Ingress, &chain.Chain{
|
||||
quotaRuleChainID, _ = s.LocalStorage().AddOverride(chain.Ingress, container, &chain.Chain{
|
||||
Rules: []chain.Rule{{
|
||||
Status: chain.QuotaLimitReached,
|
||||
Actions: chain.Actions{Names: []string{"native::object::put"}},
|
||||
|
@ -187,9 +190,17 @@ func TestInmemory(t *testing.T) {
|
|||
}},
|
||||
})
|
||||
|
||||
status, ok = s.IsAllowed(chain.Ingress, namespace, reqGood)
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
109
pkg/engine/inmemory/local_storage.go
Normal file
109
pkg/engine/inmemory/local_storage.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package inmemory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/util"
|
||||
)
|
||||
|
||||
type targetToChain map[string][]*chain.Chain
|
||||
|
||||
type inmemoryLocalStorage struct {
|
||||
usedChainID map[chain.ID]struct{}
|
||||
nameToResourceChains map[chain.Name]targetToChain
|
||||
}
|
||||
|
||||
func NewInmemoryLocalStorage() engine.LocalOverrideStorage {
|
||||
return &inmemoryLocalStorage{
|
||||
usedChainID: map[chain.ID]struct{}{},
|
||||
nameToResourceChains: make(map[chain.Name]targetToChain),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *inmemoryLocalStorage) generateChainID(name chain.Name, resource string) chain.ID {
|
||||
var id chain.ID
|
||||
for {
|
||||
suffix := rand.Uint32() % 100
|
||||
sid := fmt.Sprintf("%s:%s/%d", name, resource, suffix)
|
||||
sid = strings.ReplaceAll(sid, "*", "")
|
||||
sid = strings.ReplaceAll(sid, "/", ":")
|
||||
sid = strings.ReplaceAll(sid, "::", ":")
|
||||
id = chain.ID(sid)
|
||||
_, ok := s.usedChainID[id]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
s.usedChainID[id] = struct{}{}
|
||||
break
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *inmemoryLocalStorage) AddOverride(name chain.Name, resource string, c *chain.Chain) (chain.ID, error) {
|
||||
// AddOverride assigns generated chain ID if it has not been assigned.
|
||||
if c.ID == "" {
|
||||
c.ID = s.generateChainID(name, resource)
|
||||
}
|
||||
if s.nameToResourceChains[name] == nil {
|
||||
s.nameToResourceChains[name] = make(targetToChain)
|
||||
}
|
||||
rc := s.nameToResourceChains[name]
|
||||
rc[resource] = append(rc[resource], c)
|
||||
return c.ID, nil
|
||||
}
|
||||
|
||||
func (s *inmemoryLocalStorage) GetOverride(name chain.Name, resource string, chainID chain.ID) (*chain.Chain, error) {
|
||||
if _, ok := s.nameToResourceChains[name]; !ok {
|
||||
return nil, engine.ErrChainNameNotFound
|
||||
}
|
||||
chains, ok := s.nameToResourceChains[name][resource]
|
||||
if !ok {
|
||||
return nil, engine.ErrResourceNotFound
|
||||
}
|
||||
for _, c := range chains {
|
||||
if c.ID == chainID {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return nil, engine.ErrChainNotFound
|
||||
}
|
||||
|
||||
func (s *inmemoryLocalStorage) RemoveOverride(name chain.Name, resource string, chainID chain.ID) error {
|
||||
if _, ok := s.nameToResourceChains[name]; !ok {
|
||||
return engine.ErrChainNameNotFound
|
||||
}
|
||||
chains, ok := s.nameToResourceChains[name][resource]
|
||||
if !ok {
|
||||
return engine.ErrResourceNotFound
|
||||
}
|
||||
for i, c := range chains {
|
||||
if c.ID == chainID {
|
||||
s.nameToResourceChains[name][resource] = append(chains[:i], chains[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return engine.ErrChainNotFound
|
||||
}
|
||||
|
||||
func (s *inmemoryLocalStorage) ListOverrides(name chain.Name, resource string) ([]*chain.Chain, error) {
|
||||
rcs, ok := s.nameToResourceChains[name]
|
||||
if !ok {
|
||||
return []*chain.Chain{}, nil
|
||||
}
|
||||
for container, chains := range rcs {
|
||||
if !util.GlobMatch(resource, container) {
|
||||
continue
|
||||
}
|
||||
return chains, nil
|
||||
}
|
||||
return []*chain.Chain{}, nil
|
||||
}
|
||||
|
||||
func (s *inmemoryLocalStorage) DropAllOverrides(name chain.Name) error {
|
||||
s.nameToResourceChains[name] = make(targetToChain)
|
||||
return nil
|
||||
}
|
217
pkg/engine/inmemory/local_storage_test.go
Normal file
217
pkg/engine/inmemory/local_storage_test.go
Normal 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
|
||||
}
|
52
pkg/engine/inmemory/morph_storage.go
Normal file
52
pkg/engine/inmemory/morph_storage.go
Normal 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
|
||||
}
|
|
@ -5,26 +5,74 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
||||
)
|
||||
|
||||
// Engine ...
|
||||
type Engine interface {
|
||||
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, namespace string, r resource.Request) (chain.Status, bool)
|
||||
IsAllowed(name chain.Name, target string, r resource.Request) (status chain.Status, found bool, err error)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue