policy: added sparse policy tree abstraction for quickly retrieving effective policies at runtime when walking a tree

This commit is contained in:
Jarek Kowalski
2019-12-07 12:21:21 -08:00
parent b68ef7d781
commit ece99073d4
2 changed files with 246 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
package policy
import "strings"
// DefaultPolicy is a default policy returned by policy tree in absence of other policies.
var DefaultPolicy = &Policy{
FilesPolicy: defaultFilesPolicy,
}
// Tree represents a node in the policy tree, where a policy can be
// defined. A nil tree is a valid tree with default policy.
type Tree struct {
effective *Policy
inherited bool
children map[string]*Tree
}
// DefinedPolicy returns policy that's been explicitly defined for tree node or nil if no policy was defined.
func (t *Tree) DefinedPolicy() *Policy {
if t == nil || t.inherited {
return nil
}
return t.effective
}
// EffectivePolicy returns policy that's been defined for this tree node or inherited from its parent.
func (t *Tree) EffectivePolicy() *Policy {
if t == nil {
return DefaultPolicy
}
return t.effective
}
func (t *Tree) IsInherited() bool {
if t == nil {
return true
}
return t.inherited
}
func (t *Tree) Child(name string) *Tree {
if t == nil {
return nil
}
parts := strings.Split(name, "/")
switch len(parts) {
case 1:
if name == "." || name == "" {
return t
}
ch := t.children[name]
if ch != nil {
return ch
}
// tree with no children, we can just reuse current node
if len(t.children) == 0 && t.inherited {
return t
}
return &Tree{effective: t.effective, inherited: true}
default:
ch := t
for _, p := range parts {
ch = ch.Child(p)
}
return ch
}
}
// BuildTree builds a policy tree from the given map of paths to policies.
// Each path must be relative and start with "." and be separated by slashes.
func BuildTree(defined map[string]*Policy, defaultPolicy *Policy) *Tree {
return buildTreeNode(defined, ".", defaultPolicy)
}
func buildTreeNode(defined map[string]*Policy, path string, defaultPolicy *Policy) *Tree {
n := &Tree{
effective: defined[path],
}
if n.effective == nil {
n.effective = defaultPolicy
n.inherited = true
}
children := childrenWithPrefix(defined, path+"/")
if len(children) > 0 {
n.children = map[string]*Tree{}
for childName, descendants := range children {
n.children[childName] = buildTreeNode(descendants, path+"/"+childName, n.effective)
}
}
return n
}
func childrenWithPrefix(m map[string]*Policy, path string) map[string]map[string]*Policy {
result := map[string]map[string]*Policy{}
for k, v := range m {
if !strings.HasPrefix(k, path) {
continue
}
childName := strings.Split(k[len(path):], "/")[0]
if result[childName] == nil {
result[childName] = map[string]*Policy{}
}
result[childName][k] = v
}
return result
}

View File

@@ -0,0 +1,124 @@
package policy
import (
"fmt"
"reflect"
"testing"
)
var (
defaultPolicy = &Policy{
FilesPolicy: FilesPolicy{
IgnoreRules: []string{"default"},
},
}
policyA = &Policy{
FilesPolicy: FilesPolicy{
IgnoreRules: []string{"a"},
},
}
policyB = &Policy{
FilesPolicy: FilesPolicy{
IgnoreRules: []string{"b"},
},
}
policyC = &Policy{
FilesPolicy: FilesPolicy{
IgnoreRules: []string{"c"},
},
}
)
func TestTreeChild(t *testing.T) {
complexTree := &Tree{
effective: policyA,
children: map[string]*Tree{
"foo": {
effective: policyB,
children: map[string]*Tree{
"xxx": {
effective: policyC,
},
"yyy": {
effective: policyB,
},
"zzz": {
effective: policyA,
},
},
},
"bar": {
effective: policyC,
},
},
}
cases := []struct {
n *Tree
path string
wantPolicy *Policy
wantInherited bool
}{
{nil, "blah", DefaultPolicy, true},
{&Tree{effective: policyA}, "blah", policyA, true},
{complexTree, "", policyA, false},
{complexTree, "foo", policyB, false},
{complexTree, "foo/anything", policyB, true},
{complexTree, "foo/xxx", policyC, false},
{complexTree, "foo/xxx/child/grand/child", policyC, true},
{complexTree, "foo/yyy", policyB, false},
{complexTree, "foo/yyy/child", policyB, true},
{complexTree, "foo/zzz", policyA, false},
{complexTree, "foo/zzz/child", policyA, true},
{complexTree, "bar", policyC, false},
{complexTree, "bar1", policyA, true},
}
for _, tc := range cases {
verifyTreePolicy(t, tc.n, tc.path, tc.wantPolicy, tc.wantInherited)
}
}
func TestBuildTree(t *testing.T) {
n := BuildTree(map[string]*Policy{
".": policyA,
"./foo": policyB,
"./bar/baz/bleh": policyC,
}, defaultPolicy)
dumpTree(n, "root")
verifyTreePolicy(t, n, "", policyA, false)
verifyTreePolicy(t, n, ".", policyA, false)
verifyTreePolicy(t, n, "./foo", policyB, false)
verifyTreePolicy(t, n, "foo/.", policyB, false)
verifyTreePolicy(t, n, "foo/bar", policyB, true)
verifyTreePolicy(t, n, "./foo/./././bar", policyB, true)
verifyTreePolicy(t, n, "not-foo", policyA, true)
verifyTreePolicy(t, n, "bar", policyA, true)
verifyTreePolicy(t, n, "bar/./baz", policyA, true)
verifyTreePolicy(t, n, "bar/baz/bleh/././.", policyC, false)
verifyTreePolicy(t, n, "bar/baz/bleh/./././x", policyC, true)
}
func verifyTreePolicy(t *testing.T, n *Tree, path string, wantPolicy *Policy, wantInherited bool) {
t.Helper()
c := n.Child(path)
if got, want := c.EffectivePolicy(), wantPolicy; !reflect.DeepEqual(got, want) {
t.Errorf("invalid policy for %q: %v, want %v", path, got, want)
}
if got, want := c.IsInherited(), wantInherited; got != want {
t.Errorf("invalid child 'inherited' result for %q, got %v want %v", path, got, want)
}
}
func dumpTree(n *Tree, prefix string) {
fmt.Println(prefix + ".policy: " + n.effective.FilesPolicy.IgnoreRules[0])
for cname, cnode := range n.children {
dumpTree(cnode, prefix+"."+cname)
}
}