acl: First pass

This commit is contained in:
Armon Dadgar 2014-08-06 15:08:17 -07:00
parent 6f7bf36ee9
commit 05a73045d0
4 changed files with 362 additions and 0 deletions

118
acl/acl.go Normal file
View File

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

142
acl/acl_test.go Normal file
View File

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

50
acl/policy.go Normal file
View File

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

52
acl/policy_test.go Normal file
View File

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