From 05a73045d0ba201f4d6d8db2c8a1d014b2d6b0df Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 6 Aug 2014 15:08:17 -0700 Subject: [PATCH] acl: First pass --- acl/acl.go | 118 +++++++++++++++++++++++++++++++++++++ acl/acl_test.go | 142 +++++++++++++++++++++++++++++++++++++++++++++ acl/policy.go | 50 ++++++++++++++++ acl/policy_test.go | 52 +++++++++++++++++ 4 files changed, 362 insertions(+) create mode 100644 acl/acl.go create mode 100644 acl/acl_test.go create mode 100644 acl/policy.go create mode 100644 acl/policy_test.go diff --git a/acl/acl.go b/acl/acl.go new file mode 100644 index 000000000..51395948c --- /dev/null +++ b/acl/acl.go @@ -0,0 +1,118 @@ +package acl + +import ( + "fmt" + + "github.com/armon/go-radix" +) + +var ( + // allowAll is a singleton policy which allows all actions + allowAll ACL + + // denyAll is a singleton policy which denies all actions + denyAll ACL +) + +func init() { + // Setup the singletons + allowAll = &StaticACL{defaultAllow: true} + denyAll = &StaticACL{defaultAllow: false} +} + +// ACL is the interface for policy enforcement. +type ACL interface { + KeyRead(string) bool + KeyWrite(string) bool +} + +// StaticACL is used to implement a base ACL policy. It either +// allows or denies all requests. This can be used as a parent +// ACL to act in a blacklist or whitelist mode. +type StaticACL struct { + defaultAllow bool +} + +func (s *StaticACL) KeyRead(string) bool { + return s.defaultAllow +} + +func (s *StaticACL) KeyWrite(string) bool { + return s.defaultAllow +} + +// AllowAll returns an ACL rule that allows all operations +func AllowAll() ACL { + return allowAll +} + +// DenyAll returns an ACL rule that denies all operations +func DenyAll() ACL { + return denyAll +} + +// PolicyACL is used to wrap a set of ACL policies to provide +// the ACL interface. +type PolicyACL struct { + // parent is used to resolve policy if we have + // no matching rule. + parent ACL + + // keyRead contains the read policies + keyRead *radix.Tree + + // keyWrite contains the write policies + keyWrite *radix.Tree +} + +// New is used to construct a policy based ACL from a set of policies +// and a parent policy to resolve missing cases. +func New(parent ACL, policy *Policy) (*PolicyACL, error) { + p := &PolicyACL{ + parent: parent, + keyRead: radix.New(), + keyWrite: radix.New(), + } + + // Load the key policy + for _, kp := range policy.Keys { + switch kp.Policy { + case KeyPolicyDeny: + p.keyRead.Insert(kp.Prefix, false) + p.keyWrite.Insert(kp.Prefix, false) + case KeyPolicyRead: + p.keyRead.Insert(kp.Prefix, true) + p.keyWrite.Insert(kp.Prefix, false) + case KeyPolicyWrite: + p.keyRead.Insert(kp.Prefix, true) + p.keyWrite.Insert(kp.Prefix, true) + default: + return nil, fmt.Errorf("Invalid key policy: %#v", kp) + } + } + return p, nil +} + +// KeyRead returns if a key is allowed to be read +func (p *PolicyACL) KeyRead(key string) bool { + // Look for a matching rule + _, rule, ok := p.keyRead.LongestPrefix(key) + if ok { + return rule.(bool) + } + + // No matching rule, use the parent. + return p.parent.KeyRead(key) +} + +// KeyWrite returns if a key is allowed to be written +func (p *PolicyACL) KeyWrite(key string) bool { + // Look for a matching rule + _, rule, ok := p.keyWrite.LongestPrefix(key) + if ok { + return rule.(bool) + } + + // No matching rule, use the parent. + return p.parent.KeyWrite(key) +} diff --git a/acl/acl_test.go b/acl/acl_test.go new file mode 100644 index 000000000..baf327e09 --- /dev/null +++ b/acl/acl_test.go @@ -0,0 +1,142 @@ +package acl + +import ( + "testing" +) + +func TestStaticACL(t *testing.T) { + all := AllowAll() + if _, ok := all.(*StaticACL); !ok { + t.Fatalf("expected static") + } + + none := DenyAll() + if _, ok := none.(*StaticACL); !ok { + t.Fatalf("expected static") + } + + if !all.KeyRead("foobar") { + t.Fatalf("should allow") + } + if !all.KeyWrite("foobar") { + t.Fatalf("should allow") + } + + if none.KeyRead("foobar") { + t.Fatalf("should not allow") + } + if none.KeyWrite("foobar") { + t.Fatalf("should not allow") + } +} + +func TestPolicyACL(t *testing.T) { + all := AllowAll() + policy := &Policy{ + Keys: []*KeyPolicy{ + &KeyPolicy{ + Prefix: "foo/", + Policy: KeyPolicyWrite, + }, + &KeyPolicy{ + Prefix: "foo/priv/", + Policy: KeyPolicyDeny, + }, + &KeyPolicy{ + Prefix: "bar/", + Policy: KeyPolicyDeny, + }, + &KeyPolicy{ + Prefix: "zip/", + Policy: KeyPolicyRead, + }, + }, + } + acl, err := New(all, policy) + if err != nil { + t.Fatalf("err: %v", err) + } + + type tcase struct { + inp string + read bool + write bool + } + cases := []tcase{ + {"other", true, true}, + {"foo/test", true, true}, + {"foo/priv/test", false, false}, + {"bar/any", false, false}, + {"zip/test", true, false}, + } + for _, c := range cases { + if c.read != acl.KeyRead(c.inp) { + t.Fatalf("Read fail: %#v", c) + } + if c.write != acl.KeyWrite(c.inp) { + t.Fatalf("Write fail: %#v", c) + } + } +} + +func TestPolicyACL_Parent(t *testing.T) { + deny := DenyAll() + policyRoot := &Policy{ + Keys: []*KeyPolicy{ + &KeyPolicy{ + Prefix: "foo/", + Policy: KeyPolicyWrite, + }, + &KeyPolicy{ + Prefix: "bar/", + Policy: KeyPolicyRead, + }, + }, + } + root, err := New(deny, policyRoot) + if err != nil { + t.Fatalf("err: %v", err) + } + + policy := &Policy{ + Keys: []*KeyPolicy{ + &KeyPolicy{ + Prefix: "foo/priv/", + Policy: KeyPolicyRead, + }, + &KeyPolicy{ + Prefix: "bar/", + Policy: KeyPolicyDeny, + }, + &KeyPolicy{ + Prefix: "zip/", + Policy: KeyPolicyRead, + }, + }, + } + acl, err := New(root, policy) + if err != nil { + t.Fatalf("err: %v", err) + } + + type tcase struct { + inp string + read bool + write bool + } + cases := []tcase{ + {"other", false, false}, + {"foo/test", true, true}, + {"foo/priv/test", true, false}, + {"bar/any", false, false}, + {"zip/test", true, false}, + } + for _, c := range cases { + if c.read != acl.KeyRead(c.inp) { + t.Fatalf("Read fail: %#v", c) + } + if c.write != acl.KeyWrite(c.inp) { + t.Fatalf("Write fail: %#v", c) + } + } +} diff --git a/acl/policy.go b/acl/policy.go new file mode 100644 index 000000000..2abdc9812 --- /dev/null +++ b/acl/policy.go @@ -0,0 +1,50 @@ +package acl + +import ( + "fmt" + "github.com/hashicorp/hcl" +) + +// KeyPolicyType controls the various access levels for keys +type KeyPolicyType string + +const ( + KeyPolicyDeny = "deny" + KeyPolicyRead = "read" + KeyPolicyWrite = "write" +) + +// Policy is used to represent the policy specified by +// an ACL configuration. +type Policy struct { + Keys []*KeyPolicy `hcl:"key"` +} + +// KeyPolicy represents a policy for a key +type KeyPolicy struct { + Prefix string `hcl:",key"` + Policy KeyPolicyType +} + +// Parse is used to parse the specified ACL rules into an +// intermediary set of policies, before being compiled into +// the ACL +func Parse(rules string) (*Policy, error) { + // Decode the rules + p := &Policy{} + if err := hcl.Decode(p, rules); err != nil { + return nil, fmt.Errorf("Failed to parse ACL rules: %v", err) + } + + // Validate the key policy + for _, kp := range p.Keys { + switch kp.Policy { + case KeyPolicyDeny: + case KeyPolicyRead: + case KeyPolicyWrite: + default: + return nil, fmt.Errorf("Invalid key policy: %#v", kp) + } + } + return p, nil +} diff --git a/acl/policy_test.go b/acl/policy_test.go new file mode 100644 index 000000000..c389f3b25 --- /dev/null +++ b/acl/policy_test.go @@ -0,0 +1,52 @@ +package acl + +import ( + "reflect" + "testing" +) + +func TestParse(t *testing.T) { + inp := ` +key "" { + policy = "read" +} +key "foo/" { + policy = "write" +} +key "foo/bar/" { + policy = "read" +} +key "foo/bar/baz" { + polizy = "deny" +} + ` + exp := &Policy{ + Keys: []*KeyPolicy{ + &KeyPolicy{ + Prefix: "", + Policy: KeyPolicyRead, + }, + &KeyPolicy{ + Prefix: "foo/", + Policy: KeyPolicyWrite, + }, + &KeyPolicy{ + Prefix: "foo/bar/", + Policy: KeyPolicyRead, + }, + &KeyPolicy{ + Prefix: "foo/bar/baz", + Policy: KeyPolicyDeny, + }, + }, + } + + out, err := Parse(inp) + if err != nil { + t.Fatalf("err: %v", err) + } + + if reflect.DeepEqual(out, exp) { + t.Fatalf("bad: %#v", out) + } +}