mirror of https://github.com/status-im/consul.git
acl: First pass
This commit is contained in:
parent
78049ad240
commit
7a1d778474
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue