mirror of https://github.com/hashicorp/consul
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