Merge pull request #291 from hashicorp/f-acl
Adding support for ACL system
This commit is contained in:
commit
00611a7e61
216
acl/acl.go
Normal file
216
acl/acl.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"github.com/armon/go-radix"
|
||||
)
|
||||
|
||||
var (
|
||||
// allowAll is a singleton policy which allows all
|
||||
// non-management actions
|
||||
allowAll ACL
|
||||
|
||||
// denyAll is a singleton policy which denies all actions
|
||||
denyAll ACL
|
||||
|
||||
// manageAll is a singleton policy which allows all
|
||||
// actions, including management
|
||||
manageAll ACL
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Setup the singletons
|
||||
allowAll = &StaticACL{
|
||||
allowManage: false,
|
||||
defaultAllow: true,
|
||||
}
|
||||
denyAll = &StaticACL{
|
||||
allowManage: false,
|
||||
defaultAllow: false,
|
||||
}
|
||||
manageAll = &StaticACL{
|
||||
allowManage: true,
|
||||
defaultAllow: true,
|
||||
}
|
||||
}
|
||||
|
||||
// ACL is the interface for policy enforcement.
|
||||
type ACL interface {
|
||||
// KeyRead checks for permission to read a given key
|
||||
KeyRead(string) bool
|
||||
|
||||
// KeyWrite checks for permission to write a given key
|
||||
KeyWrite(string) bool
|
||||
|
||||
// KeyWritePrefix checks for permission to write to an
|
||||
// entire key prefix. This means there must be no sub-policies
|
||||
// that deny a write.
|
||||
KeyWritePrefix(string) bool
|
||||
|
||||
// ACLList checks for permission to list all the ACLs
|
||||
ACLList() bool
|
||||
|
||||
// ACLModify checks for permission to manipulate ACLs
|
||||
ACLModify() 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 {
|
||||
allowManage bool
|
||||
defaultAllow bool
|
||||
}
|
||||
|
||||
func (s *StaticACL) KeyRead(string) bool {
|
||||
return s.defaultAllow
|
||||
}
|
||||
|
||||
func (s *StaticACL) KeyWrite(string) bool {
|
||||
return s.defaultAllow
|
||||
}
|
||||
|
||||
func (s *StaticACL) KeyWritePrefix(string) bool {
|
||||
return s.defaultAllow
|
||||
}
|
||||
|
||||
func (s *StaticACL) ACLList() bool {
|
||||
return s.allowManage
|
||||
}
|
||||
|
||||
func (s *StaticACL) ACLModify() bool {
|
||||
return s.allowManage
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ManageAll returns an ACL rule that can manage all resources
|
||||
func ManageAll() ACL {
|
||||
return manageAll
|
||||
}
|
||||
|
||||
// RootACL returns a possible ACL if the ID matches a root policy
|
||||
func RootACL(id string) ACL {
|
||||
switch id {
|
||||
case "allow":
|
||||
return allowAll
|
||||
case "deny":
|
||||
return denyAll
|
||||
case "manage":
|
||||
return manageAll
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// keyRules contains the key policies
|
||||
keyRules *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,
|
||||
keyRules: radix.New(),
|
||||
}
|
||||
|
||||
// Load the key policy
|
||||
for _, kp := range policy.Keys {
|
||||
p.keyRules.Insert(kp.Prefix, kp.Policy)
|
||||
}
|
||||
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.keyRules.LongestPrefix(key)
|
||||
if ok {
|
||||
switch rule.(string) {
|
||||
case KeyPolicyRead:
|
||||
return true
|
||||
case KeyPolicyWrite:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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.keyRules.LongestPrefix(key)
|
||||
if ok {
|
||||
switch rule.(string) {
|
||||
case KeyPolicyWrite:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// No matching rule, use the parent.
|
||||
return p.parent.KeyWrite(key)
|
||||
}
|
||||
|
||||
// KeyWritePrefix returns if a prefix is allowed to be written
|
||||
func (p *PolicyACL) KeyWritePrefix(prefix string) bool {
|
||||
// Look for a matching rule that denies
|
||||
_, rule, ok := p.keyRules.LongestPrefix(prefix)
|
||||
if ok && rule.(string) != KeyPolicyWrite {
|
||||
return false
|
||||
}
|
||||
|
||||
// Look if any of our children have a deny policy
|
||||
deny := false
|
||||
p.keyRules.WalkPrefix(prefix, func(path string, rule interface{}) bool {
|
||||
// We have a rule to prevent a write in a sub-directory!
|
||||
if rule.(string) != KeyPolicyWrite {
|
||||
deny = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Deny the write if any sub-rules may be violated
|
||||
if deny {
|
||||
return false
|
||||
}
|
||||
|
||||
// If we had a matching rule, done
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// No matching rule, use the parent.
|
||||
return p.parent.KeyWritePrefix(prefix)
|
||||
}
|
||||
|
||||
// ACLList checks if listing of ACLs is allowed
|
||||
func (p *PolicyACL) ACLList() bool {
|
||||
return p.parent.ACLList()
|
||||
}
|
||||
|
||||
// ACLModify checks if modification of ACLs is allowed
|
||||
func (p *PolicyACL) ACLModify() bool {
|
||||
return p.parent.ACLModify()
|
||||
}
|
197
acl/acl_test.go
Normal file
197
acl/acl_test.go
Normal file
|
@ -0,0 +1,197 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRootACL(t *testing.T) {
|
||||
if RootACL("allow") != AllowAll() {
|
||||
t.Fatalf("Bad root")
|
||||
}
|
||||
if RootACL("deny") != DenyAll() {
|
||||
t.Fatalf("Bad root")
|
||||
}
|
||||
if RootACL("manage") != ManageAll() {
|
||||
t.Fatalf("Bad root")
|
||||
}
|
||||
if RootACL("foo") != nil {
|
||||
t.Fatalf("bad root")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
manage := ManageAll()
|
||||
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 all.ACLList() {
|
||||
t.Fatalf("should not allow")
|
||||
}
|
||||
if all.ACLModify() {
|
||||
t.Fatalf("should not allow")
|
||||
}
|
||||
|
||||
if none.KeyRead("foobar") {
|
||||
t.Fatalf("should not allow")
|
||||
}
|
||||
if none.KeyWrite("foobar") {
|
||||
t.Fatalf("should not allow")
|
||||
}
|
||||
if none.ACLList() {
|
||||
t.Fatalf("should not noneow")
|
||||
}
|
||||
if none.ACLModify() {
|
||||
t.Fatalf("should not noneow")
|
||||
}
|
||||
|
||||
if !manage.KeyRead("foobar") {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if !manage.KeyWrite("foobar") {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if !manage.ACLList() {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if !manage.ACLModify() {
|
||||
t.Fatalf("should 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
|
||||
writePrefix bool
|
||||
}
|
||||
cases := []tcase{
|
||||
{"other", true, true, true},
|
||||
{"foo/test", true, true, true},
|
||||
{"foo/priv/test", false, false, false},
|
||||
{"bar/any", false, false, false},
|
||||
{"zip/test", true, false, false},
|
||||
{"foo/", true, true, false},
|
||||
{"", true, 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)
|
||||
}
|
||||
if c.writePrefix != acl.KeyWritePrefix(c.inp) {
|
||||
t.Fatalf("Write prefix 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
|
||||
writePrefix bool
|
||||
}
|
||||
cases := []tcase{
|
||||
{"other", false, false, false},
|
||||
{"foo/test", true, true, true},
|
||||
{"foo/priv/test", true, false, false},
|
||||
{"bar/any", false, false, false},
|
||||
{"zip/test", true, false, 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)
|
||||
}
|
||||
if c.writePrefix != acl.KeyWritePrefix(c.inp) {
|
||||
t.Fatalf("Write prefix fail: %#v", c)
|
||||
}
|
||||
}
|
||||
}
|
164
acl/cache.go
Normal file
164
acl/cache.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
// FaultFunc is a function used to fault in the parent,
|
||||
// rules for an ACL given it's ID
|
||||
type FaultFunc func(id string) (string, string, error)
|
||||
|
||||
// aclEntry allows us to store the ACL with it's policy ID
|
||||
type aclEntry struct {
|
||||
ACL ACL
|
||||
Parent string
|
||||
RuleID string
|
||||
}
|
||||
|
||||
// Cache is used to implement policy and ACL caching
|
||||
type Cache struct {
|
||||
faultfn FaultFunc
|
||||
aclCache *lru.Cache // Cache id -> acl
|
||||
policyCache *lru.Cache // Cache policy -> acl
|
||||
ruleCache *lru.Cache // Cache rules -> policy
|
||||
}
|
||||
|
||||
// NewCache contructs a new policy and ACL cache of a given size
|
||||
func NewCache(size int, faultfn FaultFunc) (*Cache, error) {
|
||||
if size <= 0 {
|
||||
return nil, fmt.Errorf("Must provide positive cache size")
|
||||
}
|
||||
rc, _ := lru.New(size)
|
||||
pc, _ := lru.New(size)
|
||||
ac, _ := lru.New(size)
|
||||
c := &Cache{
|
||||
faultfn: faultfn,
|
||||
aclCache: ac,
|
||||
policyCache: pc,
|
||||
ruleCache: rc,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetPolicy is used to get a potentially cached policy set.
|
||||
// If not cached, it will be parsed, and then cached.
|
||||
func (c *Cache) GetPolicy(rules string) (*Policy, error) {
|
||||
return c.getPolicy(c.ruleID(rules), rules)
|
||||
}
|
||||
|
||||
// getPolicy is an internal method to get a cached policy,
|
||||
// but it assumes a pre-computed ID
|
||||
func (c *Cache) getPolicy(id, rules string) (*Policy, error) {
|
||||
raw, ok := c.ruleCache.Get(id)
|
||||
if ok {
|
||||
return raw.(*Policy), nil
|
||||
}
|
||||
policy, err := Parse(rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy.ID = id
|
||||
c.ruleCache.Add(id, policy)
|
||||
return policy, nil
|
||||
|
||||
}
|
||||
|
||||
// ruleID is used to generate an ID for a rule
|
||||
func (c *Cache) ruleID(rules string) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(rules)))
|
||||
}
|
||||
|
||||
// policyID returns the cache ID for a policy
|
||||
func (c *Cache) policyID(parent, ruleID string) string {
|
||||
return parent + ":" + ruleID
|
||||
}
|
||||
|
||||
// GetACLPolicy is used to get the potentially cached ACL
|
||||
// policy. If not cached, it will be generated and then cached.
|
||||
func (c *Cache) GetACLPolicy(id string) (string, *Policy, error) {
|
||||
// Check for a cached acl
|
||||
if raw, ok := c.aclCache.Get(id); ok {
|
||||
cached := raw.(aclEntry)
|
||||
if raw, ok := c.ruleCache.Get(cached.RuleID); ok {
|
||||
return cached.Parent, raw.(*Policy), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fault in the rules
|
||||
parent, rules, err := c.faultfn(id)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Get cached
|
||||
policy, err := c.GetPolicy(rules)
|
||||
return parent, policy, err
|
||||
}
|
||||
|
||||
// GetACL is used to get a potentially cached ACL policy.
|
||||
// If not cached, it will be generated and then cached.
|
||||
func (c *Cache) GetACL(id string) (ACL, error) {
|
||||
// Look for the ACL directly
|
||||
raw, ok := c.aclCache.Get(id)
|
||||
if ok {
|
||||
return raw.(aclEntry).ACL, nil
|
||||
}
|
||||
|
||||
// Get the rules
|
||||
parentID, rules, err := c.faultfn(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ruleID := c.ruleID(rules)
|
||||
|
||||
// Check for a compiled ACL
|
||||
policyID := c.policyID(parentID, ruleID)
|
||||
var compiled ACL
|
||||
if raw, ok := c.policyCache.Get(policyID); ok {
|
||||
compiled = raw.(ACL)
|
||||
} else {
|
||||
// Get the policy
|
||||
policy, err := c.getPolicy(ruleID, rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the parent ACL
|
||||
parent := RootACL(parentID)
|
||||
if parent == nil {
|
||||
parent, err = c.GetACL(parentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Compile the ACL
|
||||
acl, err := New(parent, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache the compiled ACL
|
||||
c.policyCache.Add(policyID, acl)
|
||||
compiled = acl
|
||||
}
|
||||
|
||||
// Cache and return the ACL
|
||||
c.aclCache.Add(id, aclEntry{compiled, parentID, ruleID})
|
||||
return compiled, nil
|
||||
}
|
||||
|
||||
// ClearACL is used to clear the ACL cache if any
|
||||
func (c *Cache) ClearACL(id string) {
|
||||
c.aclCache.Remove(id)
|
||||
}
|
||||
|
||||
// Purge is used to clear all the ACL caches. The
|
||||
// rule and policy caches are not purged, since they
|
||||
// are content-hashed anyways.
|
||||
func (c *Cache) Purge() {
|
||||
c.aclCache.Purge()
|
||||
}
|
298
acl/cache_test.go
Normal file
298
acl/cache_test.go
Normal file
|
@ -0,0 +1,298 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCache_GetPolicy(t *testing.T) {
|
||||
c, err := NewCache(1, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
p, err := c.GetPolicy("")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Should get the same policy
|
||||
p1, err := c.GetPolicy("")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if p != p1 {
|
||||
t.Fatalf("should be cached")
|
||||
}
|
||||
|
||||
// Cache a new policy
|
||||
_, err = c.GetPolicy(testSimplePolicy)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Test invalidation of p
|
||||
p3, err := c.GetPolicy("")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if p == p3 {
|
||||
t.Fatalf("should be not cached")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_GetACL(t *testing.T) {
|
||||
policies := map[string]string{
|
||||
"foo": testSimplePolicy,
|
||||
"bar": testSimplePolicy2,
|
||||
}
|
||||
faultfn := func(id string) (string, string, error) {
|
||||
return "deny", policies[id], nil
|
||||
}
|
||||
|
||||
c, err := NewCache(1, faultfn)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
acl, err := c.GetACL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if acl.KeyRead("bar/test") {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
|
||||
acl2, err := c.GetACL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if acl != acl2 {
|
||||
t.Fatalf("should be cached")
|
||||
}
|
||||
|
||||
// Invalidate cache
|
||||
_, err = c.GetACL("bar")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
acl3, err := c.GetACL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if acl == acl3 {
|
||||
t.Fatalf("should not be cached")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_ClearACL(t *testing.T) {
|
||||
policies := map[string]string{
|
||||
"foo": testSimplePolicy,
|
||||
"bar": testSimplePolicy,
|
||||
}
|
||||
faultfn := func(id string) (string, string, error) {
|
||||
return "deny", policies[id], nil
|
||||
}
|
||||
|
||||
c, err := NewCache(1, faultfn)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
acl, err := c.GetACL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Nuke the cache
|
||||
c.ClearACL("foo")
|
||||
|
||||
// Clear the policy cache
|
||||
c.policyCache.Purge()
|
||||
|
||||
acl2, err := c.GetACL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if acl == acl2 {
|
||||
t.Fatalf("should not be cached")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_Purge(t *testing.T) {
|
||||
policies := map[string]string{
|
||||
"foo": testSimplePolicy,
|
||||
"bar": testSimplePolicy,
|
||||
}
|
||||
faultfn := func(id string) (string, string, error) {
|
||||
return "deny", policies[id], nil
|
||||
}
|
||||
|
||||
c, err := NewCache(1, faultfn)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
acl, err := c.GetACL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Nuke the cache
|
||||
c.Purge()
|
||||
c.policyCache.Purge()
|
||||
|
||||
acl2, err := c.GetACL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if acl == acl2 {
|
||||
t.Fatalf("should not be cached")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_GetACLPolicy(t *testing.T) {
|
||||
policies := map[string]string{
|
||||
"foo": testSimplePolicy,
|
||||
"bar": testSimplePolicy,
|
||||
}
|
||||
faultfn := func(id string) (string, string, error) {
|
||||
return "deny", policies[id], nil
|
||||
}
|
||||
c, err := NewCache(1, faultfn)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
p, err := c.GetPolicy(testSimplePolicy)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
_, err = c.GetACL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
parent, p2, err := c.GetACLPolicy("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if parent != "deny" {
|
||||
t.Fatalf("bad: %v", parent)
|
||||
}
|
||||
|
||||
if p2 != p {
|
||||
t.Fatalf("expected cached policy")
|
||||
}
|
||||
|
||||
parent, p3, err := c.GetACLPolicy("bar")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if parent != "deny" {
|
||||
t.Fatalf("bad: %v", parent)
|
||||
}
|
||||
|
||||
if p3 != p {
|
||||
t.Fatalf("expected cached policy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_GetACL_Parent(t *testing.T) {
|
||||
faultfn := func(id string) (string, string, error) {
|
||||
switch id {
|
||||
case "foo":
|
||||
// Foo inherits from bar
|
||||
return "bar", testSimplePolicy, nil
|
||||
case "bar":
|
||||
return "deny", testSimplePolicy2, nil
|
||||
}
|
||||
t.Fatalf("bad case")
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
c, err := NewCache(1, faultfn)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
acl, err := c.GetACL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if !acl.KeyRead("bar/test") {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_GetACL_ParentCache(t *testing.T) {
|
||||
// Same rules, different parent
|
||||
faultfn := func(id string) (string, string, error) {
|
||||
switch id {
|
||||
case "foo":
|
||||
return "allow", testSimplePolicy, nil
|
||||
case "bar":
|
||||
return "deny", testSimplePolicy, nil
|
||||
}
|
||||
t.Fatalf("bad case")
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
c, err := NewCache(16, faultfn)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
acl, err := c.GetACL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if !acl.KeyRead("bar/test") {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
|
||||
acl2, err := c.GetACL("bar")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if acl == acl2 {
|
||||
t.Fatalf("should not match")
|
||||
}
|
||||
|
||||
if acl2.KeyRead("bar/test") {
|
||||
t.Fatalf("should not allow")
|
||||
}
|
||||
if !acl2.KeyRead("foo/test") {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
}
|
||||
|
||||
var testSimplePolicy = `
|
||||
key "foo/" {
|
||||
policy = "read"
|
||||
}
|
||||
`
|
||||
|
||||
var testSimplePolicy2 = `
|
||||
key "bar/" {
|
||||
policy = "read"
|
||||
}
|
||||
`
|
57
acl/policy.go
Normal file
57
acl/policy.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/hcl"
|
||||
)
|
||||
|
||||
const (
|
||||
KeyPolicyDeny = "deny"
|
||||
KeyPolicyRead = "read"
|
||||
KeyPolicyWrite = "write"
|
||||
)
|
||||
|
||||
// Policy is used to represent the policy specified by
|
||||
// an ACL configuration.
|
||||
type Policy struct {
|
||||
ID string `hcl:"-"`
|
||||
Keys []*KeyPolicy `hcl:"key,expand"`
|
||||
}
|
||||
|
||||
// KeyPolicy represents a policy for a key
|
||||
type KeyPolicy struct {
|
||||
Prefix string `hcl:",key"`
|
||||
Policy string
|
||||
}
|
||||
|
||||
func (k *KeyPolicy) GoString() string {
|
||||
return fmt.Sprintf("%#v", *k)
|
||||
}
|
||||
|
||||
// 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 rules == "" {
|
||||
// Hot path for empty rules
|
||||
return p, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
100
acl/policy_test.go
Normal file
100
acl/policy_test.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
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" {
|
||||
policy = "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 %#v", out, exp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_JSON(t *testing.T) {
|
||||
inp := `{
|
||||
"key": {
|
||||
"": {
|
||||
"policy": "read"
|
||||
},
|
||||
"foo/": {
|
||||
"policy": "write"
|
||||
},
|
||||
"foo/bar/": {
|
||||
"policy": "read"
|
||||
},
|
||||
"foo/bar/baz": {
|
||||
"policy": "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 %#v", out, exp)
|
||||
}
|
||||
}
|
190
command/agent/acl_endpoint.go
Normal file
190
command/agent/acl_endpoint.go
Normal file
|
@ -0,0 +1,190 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// aclCreateResponse is used to wrap the ACL ID
|
||||
type aclCreateResponse struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
// aclDisabled handles if ACL datacenter is not configured
|
||||
func aclDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte("ACL support disabled"))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) ACLDestroy(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
args := structs.ACLRequest{
|
||||
Datacenter: s.agent.config.ACLDatacenter,
|
||||
Op: structs.ACLDelete,
|
||||
}
|
||||
s.parseToken(req, &args.Token)
|
||||
|
||||
// Pull out the acl id
|
||||
args.ACL.ID = strings.TrimPrefix(req.URL.Path, "/v1/acl/destroy/")
|
||||
if args.ACL.ID == "" {
|
||||
resp.WriteHeader(400)
|
||||
resp.Write([]byte("Missing ACL"))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var out string
|
||||
if err := s.agent.RPC("ACL.Apply", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) ACLCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
return s.aclSet(resp, req, false)
|
||||
}
|
||||
|
||||
func (s *HTTPServer) ACLUpdate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
return s.aclSet(resp, req, true)
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclSet(resp http.ResponseWriter, req *http.Request, update bool) (interface{}, error) {
|
||||
// Mandate a PUT request
|
||||
if req.Method != "PUT" {
|
||||
resp.WriteHeader(405)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
args := structs.ACLRequest{
|
||||
Datacenter: s.agent.config.ACLDatacenter,
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Type: structs.ACLTypeClient,
|
||||
},
|
||||
}
|
||||
s.parseToken(req, &args.Token)
|
||||
|
||||
// Handle optional request body
|
||||
if req.ContentLength > 0 {
|
||||
if err := decodeBody(req, &args.ACL, nil); err != nil {
|
||||
resp.WriteHeader(400)
|
||||
resp.Write([]byte(fmt.Sprintf("Request decode failed: %v", err)))
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure there is no ID set for create
|
||||
if !update && args.ACL.ID != "" {
|
||||
resp.WriteHeader(400)
|
||||
resp.Write([]byte(fmt.Sprintf("ACL ID cannot be set")))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Ensure there is an ID set for update
|
||||
if update && args.ACL.ID == "" {
|
||||
resp.WriteHeader(400)
|
||||
resp.Write([]byte(fmt.Sprintf("ACL ID must be set")))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create the acl, get the ID
|
||||
var out string
|
||||
if err := s.agent.RPC("ACL.Apply", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Format the response as a JSON object
|
||||
return aclCreateResponse{out}, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) ACLClone(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
args := structs.ACLSpecificRequest{
|
||||
Datacenter: s.agent.config.ACLDatacenter,
|
||||
}
|
||||
var dc string
|
||||
if done := s.parse(resp, req, &dc, &args.QueryOptions); done {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Pull out the acl id
|
||||
args.ACL = strings.TrimPrefix(req.URL.Path, "/v1/acl/clone/")
|
||||
if args.ACL == "" {
|
||||
resp.WriteHeader(400)
|
||||
resp.Write([]byte("Missing ACL"))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var out structs.IndexedACLs
|
||||
defer setMeta(resp, &out.QueryMeta)
|
||||
if err := s.agent.RPC("ACL.Get", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Bail if the ACL is not found
|
||||
if len(out.ACLs) == 0 {
|
||||
resp.WriteHeader(404)
|
||||
resp.Write([]byte(fmt.Sprintf("Target ACL not found")))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create a new ACL
|
||||
createArgs := structs.ACLRequest{
|
||||
Datacenter: args.Datacenter,
|
||||
Op: structs.ACLSet,
|
||||
ACL: *out.ACLs[0],
|
||||
}
|
||||
createArgs.ACL.ID = ""
|
||||
createArgs.Token = args.Token
|
||||
|
||||
// Create the acl, get the ID
|
||||
var outID string
|
||||
if err := s.agent.RPC("ACL.Apply", &createArgs, &outID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Format the response as a JSON object
|
||||
return aclCreateResponse{outID}, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) ACLGet(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
args := structs.ACLSpecificRequest{
|
||||
Datacenter: s.agent.config.ACLDatacenter,
|
||||
}
|
||||
var dc string
|
||||
if done := s.parse(resp, req, &dc, &args.QueryOptions); done {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Pull out the acl id
|
||||
args.ACL = strings.TrimPrefix(req.URL.Path, "/v1/acl/info/")
|
||||
if args.ACL == "" {
|
||||
resp.WriteHeader(400)
|
||||
resp.Write([]byte("Missing ACL"))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var out structs.IndexedACLs
|
||||
defer setMeta(resp, &out.QueryMeta)
|
||||
if err := s.agent.RPC("ACL.Get", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.ACLs, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) ACLList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
args := structs.DCSpecificRequest{
|
||||
Datacenter: s.agent.config.ACLDatacenter,
|
||||
}
|
||||
var dc string
|
||||
if done := s.parse(resp, req, &dc, &args.QueryOptions); done {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var out structs.IndexedACLs
|
||||
defer setMeta(resp, &out.QueryMeta)
|
||||
if err := s.agent.RPC("ACL.List", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.ACLs, nil
|
||||
}
|
160
command/agent/acl_endpoint_test.go
Normal file
160
command/agent/acl_endpoint_test.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func makeTestACL(t *testing.T, srv *HTTPServer) string {
|
||||
body := bytes.NewBuffer(nil)
|
||||
enc := json.NewEncoder(body)
|
||||
raw := map[string]interface{}{
|
||||
"Name": "User Token",
|
||||
"Type": "client",
|
||||
"Rules": "",
|
||||
}
|
||||
enc.Encode(raw)
|
||||
|
||||
req, err := http.NewRequest("PUT", "/v1/acl/create", body)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := srv.ACLCreate(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
aclResp := obj.(aclCreateResponse)
|
||||
return aclResp.ID
|
||||
}
|
||||
|
||||
func TestACLUpdate(t *testing.T) {
|
||||
httpTest(t, func(srv *HTTPServer) {
|
||||
id := makeTestACL(t, srv)
|
||||
|
||||
body := bytes.NewBuffer(nil)
|
||||
enc := json.NewEncoder(body)
|
||||
raw := map[string]interface{}{
|
||||
"ID": id,
|
||||
"Name": "User Token 2",
|
||||
"Type": "client",
|
||||
"Rules": "",
|
||||
}
|
||||
enc.Encode(raw)
|
||||
|
||||
req, err := http.NewRequest("PUT", "/v1/acl/update", body)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := srv.ACLUpdate(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
aclResp := obj.(aclCreateResponse)
|
||||
if aclResp.ID != id {
|
||||
t.Fatalf("bad: %v", aclResp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestACLDestroy(t *testing.T) {
|
||||
httpTest(t, func(srv *HTTPServer) {
|
||||
id := makeTestACL(t, srv)
|
||||
req, err := http.NewRequest("PUT", "/v1/session/destroy/"+id, nil)
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := srv.ACLDestroy(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if resp := obj.(bool); !resp {
|
||||
t.Fatalf("should work")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestACLClone(t *testing.T) {
|
||||
httpTest(t, func(srv *HTTPServer) {
|
||||
id := makeTestACL(t, srv)
|
||||
|
||||
req, err := http.NewRequest("GET",
|
||||
"/v1/acl/clone/"+id, nil)
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := srv.ACLClone(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
aclResp, ok := obj.(aclCreateResponse)
|
||||
if !ok {
|
||||
t.Fatalf("should work: %#v %#v", obj, resp)
|
||||
}
|
||||
if aclResp.ID == id {
|
||||
t.Fatalf("bad id")
|
||||
}
|
||||
|
||||
req, err = http.NewRequest("GET",
|
||||
"/v1/acl/info/"+aclResp.ID, nil)
|
||||
resp = httptest.NewRecorder()
|
||||
obj, err = srv.ACLGet(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
respObj, ok := obj.(structs.ACLs)
|
||||
if !ok {
|
||||
t.Fatalf("should work")
|
||||
}
|
||||
if len(respObj) != 1 {
|
||||
t.Fatalf("bad: %v", respObj)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestACLGet(t *testing.T) {
|
||||
httpTest(t, func(srv *HTTPServer) {
|
||||
id := makeTestACL(t, srv)
|
||||
|
||||
req, err := http.NewRequest("GET",
|
||||
"/v1/acl/info/"+id, nil)
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := srv.ACLGet(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
respObj, ok := obj.(structs.ACLs)
|
||||
if !ok {
|
||||
t.Fatalf("should work")
|
||||
}
|
||||
if len(respObj) != 1 {
|
||||
t.Fatalf("bad: %v", respObj)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestACLList(t *testing.T) {
|
||||
httpTest(t, func(srv *HTTPServer) {
|
||||
var ids []string
|
||||
for i := 0; i < 10; i++ {
|
||||
ids = append(ids, makeTestACL(t, srv))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/v1/acl/list", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := srv.ACLList(resp, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
respObj, ok := obj.(structs.ACLs)
|
||||
if !ok {
|
||||
t.Fatalf("should work")
|
||||
}
|
||||
|
||||
// 10 + anonymous
|
||||
if len(respObj) != 11 {
|
||||
t.Fatalf("bad: %v", respObj)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -181,6 +181,24 @@ func (a *Agent) consulConfig() *consul.Config {
|
|||
if a.config.Protocol > 0 {
|
||||
base.ProtocolVersion = uint8(a.config.Protocol)
|
||||
}
|
||||
if a.config.ACLToken != "" {
|
||||
base.ACLToken = a.config.ACLToken
|
||||
}
|
||||
if a.config.ACLMasterToken != "" {
|
||||
base.ACLMasterToken = a.config.ACLMasterToken
|
||||
}
|
||||
if a.config.ACLDatacenter != "" {
|
||||
base.ACLDatacenter = a.config.ACLDatacenter
|
||||
}
|
||||
if a.config.ACLTTLRaw != "" {
|
||||
base.ACLTTL = a.config.ACLTTL
|
||||
}
|
||||
if a.config.ACLDefaultPolicy != "" {
|
||||
base.ACLDefaultPolicy = a.config.ACLDefaultPolicy
|
||||
}
|
||||
if a.config.ACLDownPolicy != "" {
|
||||
base.ACLDownPolicy = a.config.ACLDownPolicy
|
||||
}
|
||||
|
||||
// Format the build string
|
||||
revision := a.config.Revision
|
||||
|
|
|
@ -30,6 +30,7 @@ func nextConfig() *Config {
|
|||
conf.Ports.SerfWan = 18300 + idx
|
||||
conf.Ports.Server = 18100 + idx
|
||||
conf.Server = true
|
||||
conf.ACLDatacenter = "dc1"
|
||||
|
||||
cons := consul.DefaultConfig()
|
||||
conf.ConsulConfig = cons
|
||||
|
|
|
@ -194,6 +194,41 @@ type Config struct {
|
|||
CheckUpdateInterval time.Duration `mapstructure:"-"`
|
||||
CheckUpdateIntervalRaw string `mapstructure:"check_update_interval" json:"-"`
|
||||
|
||||
// ACLToken is the default token used to make requests if a per-request
|
||||
// token is not provided. If not configured the 'anonymous' token is used.
|
||||
ACLToken string `mapstructure:"acl_token" json:"-"`
|
||||
|
||||
// ACLMasterToken is used to bootstrap the ACL system. It should be specified
|
||||
// on the servers in the ACLDatacenter. When the leader comes online, it ensures
|
||||
// that the Master token is available. This provides the initial token.
|
||||
ACLMasterToken string `mapstructure:"acl_master_token" json:"-"`
|
||||
|
||||
// ACLDatacenter is the central datacenter that holds authoritative
|
||||
// ACL records. This must be the same for the entire cluster.
|
||||
// If this is not set, ACLs are not enabled. Off by default.
|
||||
ACLDatacenter string `mapstructure:"acl_datacenter"`
|
||||
|
||||
// ACLTTL is used to control the time-to-live of cached ACLs . This has
|
||||
// a major impact on performance. By default, it is set to 30 seconds.
|
||||
ACLTTL time.Duration `mapstructure:"-"`
|
||||
ACLTTLRaw string `mapstructure:"acl_ttl"`
|
||||
|
||||
// ACLDefaultPolicy is used to control the ACL interaction when
|
||||
// there is no defined policy. This can be "allow" which means
|
||||
// ACLs are used to black-list, or "deny" which means ACLs are
|
||||
// white-lists.
|
||||
ACLDefaultPolicy string `mapstructure:"acl_default_policy"`
|
||||
|
||||
// ACLDownPolicy is used to control the ACL interaction when we cannot
|
||||
// reach the ACLDatacenter and the token is not in the cache.
|
||||
// There are two modes:
|
||||
// * deny - Deny all requests
|
||||
// * extend-cache - Ignore the cache expiration, and allow cached
|
||||
// ACL's to be used to service requests. This
|
||||
// is the default. If the ACL is not in the cache,
|
||||
// this acts like deny.
|
||||
ACLDownPolicy string `mapstructure:"acl_down_policy"`
|
||||
|
||||
// AEInterval controls the anti-entropy interval. This is how often
|
||||
// the agent attempts to reconcile it's local state with the server'
|
||||
// representation of our state. Defaults to every 60s.
|
||||
|
@ -246,6 +281,9 @@ func DefaultConfig() *Config {
|
|||
Protocol: consul.ProtocolVersionMax,
|
||||
CheckUpdateInterval: 5 * time.Minute,
|
||||
AEInterval: time.Minute,
|
||||
ACLTTL: 30 * time.Second,
|
||||
ACLDownPolicy: "extend-cache",
|
||||
ACLDefaultPolicy: "allow",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,6 +379,14 @@ func DecodeConfig(r io.Reader) (*Config, error) {
|
|||
result.CheckUpdateInterval = dur
|
||||
}
|
||||
|
||||
if raw := result.ACLTTLRaw; raw != "" {
|
||||
dur, err := time.ParseDuration(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ACL TTL invalid: %v", err)
|
||||
}
|
||||
result.ACLTTL = dur
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
|
@ -583,6 +629,25 @@ func MergeConfig(a, b *Config) *Config {
|
|||
if b.SyslogFacility != "" {
|
||||
result.SyslogFacility = b.SyslogFacility
|
||||
}
|
||||
if b.ACLToken != "" {
|
||||
result.ACLToken = b.ACLToken
|
||||
}
|
||||
if b.ACLMasterToken != "" {
|
||||
result.ACLMasterToken = b.ACLMasterToken
|
||||
}
|
||||
if b.ACLDatacenter != "" {
|
||||
result.ACLDatacenter = b.ACLDatacenter
|
||||
}
|
||||
if b.ACLTTLRaw != "" {
|
||||
result.ACLTTL = b.ACLTTL
|
||||
result.ACLTTLRaw = b.ACLTTLRaw
|
||||
}
|
||||
if b.ACLDownPolicy != "" {
|
||||
result.ACLDownPolicy = b.ACLDownPolicy
|
||||
}
|
||||
if b.ACLDefaultPolicy != "" {
|
||||
result.ACLDefaultPolicy = b.ACLDefaultPolicy
|
||||
}
|
||||
|
||||
// Copy the start join addresses
|
||||
result.StartJoin = make([]string, 0, len(a.StartJoin)+len(b.StartJoin))
|
||||
|
|
|
@ -356,6 +356,34 @@ func TestDecodeConfig(t *testing.T) {
|
|||
if config.CheckUpdateInterval != 10*time.Minute {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
|
||||
// ACLs
|
||||
input = `{"acl_token": "1234", "acl_datacenter": "dc2",
|
||||
"acl_ttl": "60s", "acl_down_policy": "deny",
|
||||
"acl_default_policy": "deny", "acl_master_token": "2345"}`
|
||||
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if config.ACLToken != "1234" {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
if config.ACLMasterToken != "2345" {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
if config.ACLDatacenter != "dc2" {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
if config.ACLTTL != 60*time.Second {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
if config.ACLDownPolicy != "deny" {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
if config.ACLDefaultPolicy != "deny" {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeConfig_Service(t *testing.T) {
|
||||
|
@ -503,6 +531,13 @@ func TestMergeConfig(t *testing.T) {
|
|||
RejoinAfterLeave: true,
|
||||
CheckUpdateInterval: 8 * time.Minute,
|
||||
CheckUpdateIntervalRaw: "8m",
|
||||
ACLToken: "1234",
|
||||
ACLMasterToken: "2345",
|
||||
ACLDatacenter: "dc2",
|
||||
ACLTTL: 15 * time.Second,
|
||||
ACLTTLRaw: "15s",
|
||||
ACLDownPolicy: "deny",
|
||||
ACLDefaultPolicy: "deny",
|
||||
}
|
||||
|
||||
c := MergeConfig(a, b)
|
||||
|
|
|
@ -99,6 +99,22 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
|
|||
s.mux.HandleFunc("/v1/session/node/", s.wrap(s.SessionsForNode))
|
||||
s.mux.HandleFunc("/v1/session/list", s.wrap(s.SessionList))
|
||||
|
||||
if s.agent.config.ACLDatacenter != "" {
|
||||
s.mux.HandleFunc("/v1/acl/create", s.wrap(s.ACLCreate))
|
||||
s.mux.HandleFunc("/v1/acl/update", s.wrap(s.ACLUpdate))
|
||||
s.mux.HandleFunc("/v1/acl/destroy/", s.wrap(s.ACLDestroy))
|
||||
s.mux.HandleFunc("/v1/acl/info/", s.wrap(s.ACLGet))
|
||||
s.mux.HandleFunc("/v1/acl/clone/", s.wrap(s.ACLClone))
|
||||
s.mux.HandleFunc("/v1/acl/list", s.wrap(s.ACLList))
|
||||
} else {
|
||||
s.mux.HandleFunc("/v1/acl/create", s.wrap(aclDisabled))
|
||||
s.mux.HandleFunc("/v1/acl/update", s.wrap(aclDisabled))
|
||||
s.mux.HandleFunc("/v1/acl/destroy/", s.wrap(aclDisabled))
|
||||
s.mux.HandleFunc("/v1/acl/info/", s.wrap(aclDisabled))
|
||||
s.mux.HandleFunc("/v1/acl/clone/", s.wrap(aclDisabled))
|
||||
s.mux.HandleFunc("/v1/acl/list", s.wrap(aclDisabled))
|
||||
}
|
||||
|
||||
if enableDebug {
|
||||
s.mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
|
@ -273,10 +289,20 @@ func (s *HTTPServer) parseDC(req *http.Request, dc *string) {
|
|||
}
|
||||
}
|
||||
|
||||
// parseToken is used to parse the ?token query param
|
||||
func (s *HTTPServer) parseToken(req *http.Request, token *string) {
|
||||
if other := req.URL.Query().Get("token"); other != "" {
|
||||
*token = other
|
||||
} else if *token == "" {
|
||||
*token = s.agent.config.ACLToken
|
||||
}
|
||||
}
|
||||
|
||||
// parse is a convenience method for endpoints that need
|
||||
// to use both parseWait and parseDC.
|
||||
func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, dc *string, b *structs.QueryOptions) bool {
|
||||
s.parseDC(req, dc)
|
||||
s.parseToken(req, &b.Token)
|
||||
if parseConsistency(resp, req, b) {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -145,6 +145,7 @@ func (s *HTTPServer) KVSPut(resp http.ResponseWriter, req *http.Request, args *s
|
|||
Value: nil,
|
||||
},
|
||||
}
|
||||
applyReq.Token = args.Token
|
||||
|
||||
// Check for flags
|
||||
params := req.URL.Query()
|
||||
|
@ -215,6 +216,7 @@ func (s *HTTPServer) KVSDelete(resp http.ResponseWriter, req *http.Request, args
|
|||
Key: args.Key,
|
||||
},
|
||||
}
|
||||
applyReq.Token = args.Token
|
||||
|
||||
// Check for recurse
|
||||
params := req.URL.Query()
|
||||
|
|
194
consul/acl.go
Normal file
194
consul/acl.go
Normal file
|
@ -0,0 +1,194 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
)
|
||||
|
||||
const (
|
||||
// aclNotFound indicates there is no matching ACL
|
||||
aclNotFound = "ACL not found"
|
||||
|
||||
// rootDenied is returned when attempting to resolve a root ACL
|
||||
rootDenied = "Cannot resolve root ACL"
|
||||
|
||||
// permissionDenied is returned when an ACL based rejection happens
|
||||
permissionDenied = "Permission denied"
|
||||
|
||||
// aclDisabled is returned when ACL changes are not permitted
|
||||
// since they are disabled.
|
||||
aclDisabled = "ACL support disabled"
|
||||
|
||||
// anonymousToken is the token ID we re-write to if there
|
||||
// is no token ID provided
|
||||
anonymousToken = "anonymous"
|
||||
)
|
||||
|
||||
var (
|
||||
permissionDeniedErr = errors.New(permissionDenied)
|
||||
)
|
||||
|
||||
// aclCacheEntry is used to cache non-authoritative ACL's
|
||||
// If non-authoritative, then we must respect a TTL
|
||||
type aclCacheEntry struct {
|
||||
ACL acl.ACL
|
||||
Expires time.Time
|
||||
ETag string
|
||||
}
|
||||
|
||||
// aclFault is used to fault in the rules for an ACL if we take a miss
|
||||
func (s *Server) aclFault(id string) (string, string, error) {
|
||||
defer metrics.MeasureSince([]string{"consul", "acl", "fault"}, time.Now())
|
||||
state := s.fsm.State()
|
||||
_, acl, err := state.ACLGet(id)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if acl == nil {
|
||||
return "", "", errors.New(aclNotFound)
|
||||
}
|
||||
|
||||
// Management tokens have no policy and inherit from the
|
||||
// 'manage' root policy
|
||||
if acl.Type == structs.ACLTypeManagement {
|
||||
return "manage", "", nil
|
||||
}
|
||||
|
||||
// Otherwise use the base policy
|
||||
return s.config.ACLDefaultPolicy, acl.Rules, nil
|
||||
}
|
||||
|
||||
// resolveToken is used to resolve an ACL is any is appropriate
|
||||
func (s *Server) resolveToken(id string) (acl.ACL, error) {
|
||||
// Check if there is no ACL datacenter (ACL's disabled)
|
||||
authDC := s.config.ACLDatacenter
|
||||
if len(authDC) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"consul", "acl", "resolveToken"}, time.Now())
|
||||
|
||||
// Handle the anonymous token
|
||||
if len(id) == 0 {
|
||||
id = anonymousToken
|
||||
} else if acl.RootACL(id) != nil {
|
||||
return nil, errors.New(rootDenied)
|
||||
}
|
||||
|
||||
// Check if we are the ACL datacenter and the leader, use the
|
||||
// authoritative cache
|
||||
if s.config.Datacenter == authDC && s.IsLeader() {
|
||||
return s.aclAuthCache.GetACL(id)
|
||||
}
|
||||
|
||||
// Use our non-authoritative cache
|
||||
return s.lookupACL(id, authDC)
|
||||
}
|
||||
|
||||
// lookupACL is used when we are non-authoritative, and need
|
||||
// to resolve an ACL
|
||||
func (s *Server) lookupACL(id, authDC string) (acl.ACL, error) {
|
||||
// Check the cache for the ACL
|
||||
var cached *aclCacheEntry
|
||||
raw, ok := s.aclCache.Get(id)
|
||||
if ok {
|
||||
cached = raw.(*aclCacheEntry)
|
||||
}
|
||||
|
||||
// Check for live cache
|
||||
if cached != nil && time.Now().Before(cached.Expires) {
|
||||
metrics.IncrCounter([]string{"consul", "acl", "cache_hit"}, 1)
|
||||
return cached.ACL, nil
|
||||
} else {
|
||||
metrics.IncrCounter([]string{"consul", "acl", "cache_miss"}, 1)
|
||||
}
|
||||
|
||||
// Attempt to refresh the policy
|
||||
args := structs.ACLPolicyRequest{
|
||||
Datacenter: authDC,
|
||||
ACL: id,
|
||||
}
|
||||
if cached != nil {
|
||||
args.ETag = cached.ETag
|
||||
}
|
||||
var out structs.ACLPolicy
|
||||
err := s.RPC("ACL.GetPolicy", &args, &out)
|
||||
|
||||
// Handle the happy path
|
||||
if err == nil {
|
||||
return s.useACLPolicy(id, authDC, cached, &out)
|
||||
}
|
||||
|
||||
// Check for not-found
|
||||
if strings.Contains(err.Error(), aclNotFound) {
|
||||
return nil, errors.New(aclNotFound)
|
||||
} else {
|
||||
s.logger.Printf("[ERR] consul.acl: Failed to get policy for '%s': %v", id, err)
|
||||
}
|
||||
|
||||
// Unable to refresh, apply the down policy
|
||||
switch s.config.ACLDownPolicy {
|
||||
case "allow":
|
||||
return acl.AllowAll(), nil
|
||||
case "extend-cache":
|
||||
if cached != nil {
|
||||
return cached.ACL, nil
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return acl.DenyAll(), nil
|
||||
}
|
||||
}
|
||||
|
||||
// useACLPolicy handles an ACLPolicy response
|
||||
func (s *Server) useACLPolicy(id, authDC string, cached *aclCacheEntry, p *structs.ACLPolicy) (acl.ACL, error) {
|
||||
// Check if we can used the cached policy
|
||||
if cached != nil && cached.ETag == p.ETag {
|
||||
if p.TTL > 0 {
|
||||
cached.Expires = time.Now().Add(p.TTL)
|
||||
}
|
||||
return cached.ACL, nil
|
||||
}
|
||||
|
||||
// Check for a cached compiled policy
|
||||
var compiled acl.ACL
|
||||
raw, ok := s.aclPolicyCache.Get(p.ETag)
|
||||
if ok {
|
||||
compiled = raw.(acl.ACL)
|
||||
} else {
|
||||
// Resolve the parent policy
|
||||
parent := acl.RootACL(p.Parent)
|
||||
if parent == nil {
|
||||
var err error
|
||||
parent, err = s.lookupACL(p.Parent, authDC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Compile the ACL
|
||||
acl, err := acl.New(parent, p.Policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache the policy
|
||||
s.aclPolicyCache.Add(p.ETag, acl)
|
||||
compiled = acl
|
||||
}
|
||||
|
||||
// Cache the ACL
|
||||
cached = &aclCacheEntry{
|
||||
ACL: compiled,
|
||||
ETag: p.ETag,
|
||||
}
|
||||
if p.TTL > 0 {
|
||||
cached.Expires = time.Now().Add(p.TTL)
|
||||
}
|
||||
s.aclCache.Add(id, cached)
|
||||
return compiled, nil
|
||||
}
|
175
consul/acl_endpoint.go
Normal file
175
consul/acl_endpoint.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
)
|
||||
|
||||
// ACL endpoint is used to manipulate ACLs
|
||||
type ACL struct {
|
||||
srv *Server
|
||||
}
|
||||
|
||||
// Apply is used to apply a modifying request to the data store. This should
|
||||
// only be used for operations that modify the data
|
||||
func (a *ACL) Apply(args *structs.ACLRequest, reply *string) error {
|
||||
if done, err := a.srv.forward("ACL.Apply", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"consul", "acl", "apply"}, time.Now())
|
||||
|
||||
// Verify we are allowed to serve this request
|
||||
if a.srv.config.ACLDatacenter != a.srv.config.Datacenter {
|
||||
return fmt.Errorf(aclDisabled)
|
||||
}
|
||||
|
||||
// Verify token is permitted to list ACLs
|
||||
if acl, err := a.srv.resolveToken(args.Token); err != nil {
|
||||
return err
|
||||
} else if acl == nil || !acl.ACLModify() {
|
||||
return permissionDeniedErr
|
||||
}
|
||||
|
||||
switch args.Op {
|
||||
case structs.ACLSet:
|
||||
// Verify the ACL type
|
||||
switch args.ACL.Type {
|
||||
case structs.ACLTypeClient:
|
||||
case structs.ACLTypeManagement:
|
||||
default:
|
||||
return fmt.Errorf("Invalid ACL Type")
|
||||
}
|
||||
|
||||
// Validate the rules compile
|
||||
_, err := acl.Parse(args.ACL.Rules)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ACL rule compilation failed: %v", err)
|
||||
}
|
||||
|
||||
case structs.ACLDelete:
|
||||
if args.ACL.ID == "" {
|
||||
return fmt.Errorf("Missing ACL ID")
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Invalid ACL Operation")
|
||||
}
|
||||
|
||||
// Apply the update
|
||||
resp, err := a.srv.raftApply(structs.ACLRequestType, args)
|
||||
if err != nil {
|
||||
a.srv.logger.Printf("[ERR] consul.acl: Apply failed: %v", err)
|
||||
return err
|
||||
}
|
||||
if respErr, ok := resp.(error); ok {
|
||||
return respErr
|
||||
}
|
||||
|
||||
// Clear the cache if applicable
|
||||
if args.ACL.ID != "" {
|
||||
a.srv.aclAuthCache.ClearACL(args.ACL.ID)
|
||||
}
|
||||
|
||||
// Check if the return type is a string
|
||||
if respString, ok := resp.(string); ok {
|
||||
*reply = respString
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get is used to retrieve a single ACL
|
||||
func (a *ACL) Get(args *structs.ACLSpecificRequest,
|
||||
reply *structs.IndexedACLs) error {
|
||||
if done, err := a.srv.forward("ACL.Get", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify we are allowed to serve this request
|
||||
if a.srv.config.ACLDatacenter != a.srv.config.Datacenter {
|
||||
return fmt.Errorf(aclDisabled)
|
||||
}
|
||||
|
||||
// Get the local state
|
||||
state := a.srv.fsm.State()
|
||||
return a.srv.blockingRPC(&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
state.QueryTables("ACLGet"),
|
||||
func() error {
|
||||
index, acl, err := state.ACLGet(args.ACL)
|
||||
reply.Index = index
|
||||
if acl != nil {
|
||||
reply.ACLs = structs.ACLs{acl}
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// GetPolicy is used to retrieve a compiled policy object with a TTL. Does not
|
||||
// support a blocking query.
|
||||
func (a *ACL) GetPolicy(args *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
|
||||
if done, err := a.srv.forward("ACL.GetPolicy", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify we are allowed to serve this request
|
||||
if a.srv.config.ACLDatacenter != a.srv.config.Datacenter {
|
||||
return fmt.Errorf(aclDisabled)
|
||||
}
|
||||
|
||||
// Get the policy via the cache
|
||||
parent, policy, err := a.srv.aclAuthCache.GetACLPolicy(args.ACL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate an ETag
|
||||
conf := a.srv.config
|
||||
etag := fmt.Sprintf("%s:%s", parent, policy.ID)
|
||||
|
||||
// Setup the response
|
||||
reply.ETag = etag
|
||||
reply.TTL = conf.ACLTTL
|
||||
a.srv.setQueryMeta(&reply.QueryMeta)
|
||||
|
||||
// Only send the policy on an Etag mis-match
|
||||
if args.ETag != etag {
|
||||
reply.Parent = parent
|
||||
reply.Policy = policy
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List is used to list all the ACLs
|
||||
func (a *ACL) List(args *structs.DCSpecificRequest,
|
||||
reply *structs.IndexedACLs) error {
|
||||
if done, err := a.srv.forward("ACL.List", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify we are allowed to serve this request
|
||||
if a.srv.config.ACLDatacenter != a.srv.config.Datacenter {
|
||||
return fmt.Errorf(aclDisabled)
|
||||
}
|
||||
|
||||
// Verify token is permitted to list ACLs
|
||||
if acl, err := a.srv.resolveToken(args.Token); err != nil {
|
||||
return err
|
||||
} else if acl == nil || !acl.ACLList() {
|
||||
return permissionDeniedErr
|
||||
}
|
||||
|
||||
// Get the local state
|
||||
state := a.srv.fsm.State()
|
||||
return a.srv.blockingRPC(&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
state.QueryTables("ACLList"),
|
||||
func() error {
|
||||
var err error
|
||||
reply.Index, reply.ACLs, err = state.ACLList()
|
||||
return err
|
||||
})
|
||||
}
|
361
consul/acl_endpoint_test.go
Normal file
361
consul/acl_endpoint_test.go
Normal file
|
@ -0,0 +1,361 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"github.com/hashicorp/consul/testutil"
|
||||
)
|
||||
|
||||
func TestACLEndpoint_Apply(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out string
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
id := out
|
||||
|
||||
// Verify
|
||||
state := s1.fsm.State()
|
||||
_, s, err := state.ACLGet(out)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if s == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if s.ID != out {
|
||||
t.Fatalf("bad: %v", s)
|
||||
}
|
||||
if s.Name != "User token" {
|
||||
t.Fatalf("bad: %v", s)
|
||||
}
|
||||
|
||||
// Do a delete
|
||||
arg.Op = structs.ACLDelete
|
||||
arg.ACL.ID = out
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Verify
|
||||
_, s, err = state.ACLGet(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if s != nil {
|
||||
t.Fatalf("bad: %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLEndpoint_Update_PurgeCache(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out string
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
id := out
|
||||
|
||||
// Resolve
|
||||
acl1, err := s1.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl1 == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl1.KeyRead("foo") {
|
||||
t.Fatalf("should be allowed")
|
||||
}
|
||||
|
||||
// Do an update
|
||||
arg.ACL.ID = out
|
||||
arg.ACL.Rules = `{"key": {"": {"policy": "deny"}}}`
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Resolve again
|
||||
acl2, err := s1.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl2 == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if acl2 == acl1 {
|
||||
t.Fatalf("should not be cached")
|
||||
}
|
||||
if acl2.KeyRead("foo") {
|
||||
t.Fatalf("should not be allowed")
|
||||
}
|
||||
|
||||
// Do a delete
|
||||
arg.Op = structs.ACLDelete
|
||||
arg.ACL.Rules = ""
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Resolve again
|
||||
acl3, err := s1.resolveToken(id)
|
||||
if err == nil || err.Error() != aclNotFound {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl3 != nil {
|
||||
t.Fatalf("should be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLEndpoint_Apply_Denied(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
},
|
||||
}
|
||||
var out string
|
||||
err := client.Call("ACL.Apply", &arg, &out)
|
||||
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLEndpoint_Get(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out string
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
getR := structs.ACLSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
ACL: out,
|
||||
}
|
||||
var acls structs.IndexedACLs
|
||||
if err := client.Call("ACL.Get", &getR, &acls); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if acls.Index == 0 {
|
||||
t.Fatalf("Bad: %v", acls)
|
||||
}
|
||||
if len(acls.ACLs) != 1 {
|
||||
t.Fatalf("Bad: %v", acls)
|
||||
}
|
||||
s := acls.ACLs[0]
|
||||
if s.ID != out {
|
||||
t.Fatalf("bad: %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLEndpoint_GetPolicy(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out string
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
getR := structs.ACLPolicyRequest{
|
||||
Datacenter: "dc1",
|
||||
ACL: out,
|
||||
}
|
||||
var acls structs.ACLPolicy
|
||||
if err := client.Call("ACL.GetPolicy", &getR, &acls); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if acls.Policy == nil {
|
||||
t.Fatalf("Bad: %v", acls)
|
||||
}
|
||||
if acls.TTL != 30*time.Second {
|
||||
t.Fatalf("bad: %v", acls)
|
||||
}
|
||||
|
||||
// Do a conditional lookup with etag
|
||||
getR.ETag = acls.ETag
|
||||
var out2 structs.ACLPolicy
|
||||
if err := client.Call("ACL.GetPolicy", &getR, &out2); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if out2.Policy != nil {
|
||||
t.Fatalf("Bad: %v", out2)
|
||||
}
|
||||
if out2.TTL != 30*time.Second {
|
||||
t.Fatalf("bad: %v", out2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLEndpoint_List(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
ids := []string{}
|
||||
for i := 0; i < 5; i++ {
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out string
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
ids = append(ids, out)
|
||||
}
|
||||
|
||||
getR := structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
QueryOptions: structs.QueryOptions{Token: "root"},
|
||||
}
|
||||
var acls structs.IndexedACLs
|
||||
if err := client.Call("ACL.List", &getR, &acls); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if acls.Index == 0 {
|
||||
t.Fatalf("Bad: %v", acls)
|
||||
}
|
||||
|
||||
// 5 + anonymous + master
|
||||
if len(acls.ACLs) != 7 {
|
||||
t.Fatalf("Bad: %v", acls.ACLs)
|
||||
}
|
||||
for i := 0; i < len(acls.ACLs); i++ {
|
||||
s := acls.ACLs[i]
|
||||
if s.ID == anonymousToken || s.ID == "root" {
|
||||
continue
|
||||
}
|
||||
if !strContains(ids, s.ID) {
|
||||
t.Fatalf("bad: %v", s)
|
||||
}
|
||||
if s.Name != "User token" {
|
||||
t.Fatalf("bad: %v", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLEndpoint_List_Denied(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
getR := structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
var acls structs.IndexedACLs
|
||||
err := client.Call("ACL.List", &getR, &acls)
|
||||
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
684
consul/acl_test.go
Normal file
684
consul/acl_test.go
Normal file
|
@ -0,0 +1,684 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"github.com/hashicorp/consul/testutil"
|
||||
)
|
||||
|
||||
func TestACL_Disabled(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
acl, err := s1.resolveToken("does not exist")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl != nil {
|
||||
t.Fatalf("got acl")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_ResolveRootACL(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
|
||||
acl, err := s1.resolveToken("allow")
|
||||
if err == nil || err.Error() != rootDenied {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl != nil {
|
||||
t.Fatalf("bad: %v", acl)
|
||||
}
|
||||
|
||||
acl, err = s1.resolveToken("deny")
|
||||
if err == nil || err.Error() != rootDenied {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl != nil {
|
||||
t.Fatalf("bad: %v", acl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Authority_NotFound(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
acl, err := s1.resolveToken("does not exist")
|
||||
if err == nil || err.Error() != aclNotFound {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl != nil {
|
||||
t.Fatalf("got acl")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Authority_Found(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// Create a new token
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testACLPolicy,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var id string
|
||||
if err := client.Call("ACL.Apply", &arg, &id); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Resolve the token
|
||||
acl, err := s1.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
|
||||
// Check the policy
|
||||
if acl.KeyRead("bar") {
|
||||
t.Fatalf("unexpected read")
|
||||
}
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected failed read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Authority_Anonymous_Found(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// Resolve the token
|
||||
acl, err := s1.resolveToken("")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
|
||||
// Check the policy, should allow all
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected failed read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Authority_Master_Found(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.ACLMasterToken = "foobar"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// Resolve the token
|
||||
acl, err := s1.resolveToken("foobar")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
|
||||
// Check the policy, should allow all
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected failed read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Authority_Management(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.ACLMasterToken = "foobar"
|
||||
c.ACLDefaultPolicy = "deny"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// Resolve the token
|
||||
acl, err := s1.resolveToken("foobar")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
|
||||
// Check the policy, should allow all
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected failed read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_NonAuthority_NotFound(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.Bootstrap = false // Disable bootstrap
|
||||
})
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
|
||||
// Try to join
|
||||
addr := fmt.Sprintf("127.0.0.1:%d",
|
||||
s1.config.SerfLANConfig.MemberlistConfig.BindPort)
|
||||
if _, err := s2.JoinLAN([]string{addr}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
testutil.WaitForResult(func() (bool, error) {
|
||||
p1, _ := s1.raftPeers.Peers()
|
||||
return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1))
|
||||
}, func(err error) {
|
||||
t.Fatalf("should have 2 peers: %v", err)
|
||||
})
|
||||
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// find the non-authoritative server
|
||||
var nonAuth *Server
|
||||
if !s1.IsLeader() {
|
||||
nonAuth = s1
|
||||
} else {
|
||||
nonAuth = s2
|
||||
}
|
||||
|
||||
acl, err := nonAuth.resolveToken("does not exist")
|
||||
if err == nil || err.Error() != aclNotFound {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl != nil {
|
||||
t.Fatalf("got acl")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_NonAuthority_Found(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.Bootstrap = false // Disable bootstrap
|
||||
})
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
|
||||
// Try to join
|
||||
addr := fmt.Sprintf("127.0.0.1:%d",
|
||||
s1.config.SerfLANConfig.MemberlistConfig.BindPort)
|
||||
if _, err := s2.JoinLAN([]string{addr}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
testutil.WaitForResult(func() (bool, error) {
|
||||
p1, _ := s1.raftPeers.Peers()
|
||||
return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1))
|
||||
}, func(err error) {
|
||||
t.Fatalf("should have 2 peers: %v", err)
|
||||
})
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// Create a new token
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testACLPolicy,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var id string
|
||||
if err := client.Call("ACL.Apply", &arg, &id); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// find the non-authoritative server
|
||||
var nonAuth *Server
|
||||
if !s1.IsLeader() {
|
||||
nonAuth = s1
|
||||
} else {
|
||||
nonAuth = s2
|
||||
}
|
||||
|
||||
// Token should resolve
|
||||
acl, err := nonAuth.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
|
||||
// Check the policy
|
||||
if acl.KeyRead("bar") {
|
||||
t.Fatalf("unexpected read")
|
||||
}
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected failed read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_NonAuthority_Management(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.ACLMasterToken = "foobar"
|
||||
c.ACLDefaultPolicy = "deny"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.ACLDefaultPolicy = "deny"
|
||||
c.Bootstrap = false // Disable bootstrap
|
||||
})
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
|
||||
// Try to join
|
||||
addr := fmt.Sprintf("127.0.0.1:%d",
|
||||
s1.config.SerfLANConfig.MemberlistConfig.BindPort)
|
||||
if _, err := s2.JoinLAN([]string{addr}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
testutil.WaitForResult(func() (bool, error) {
|
||||
p1, _ := s1.raftPeers.Peers()
|
||||
return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1))
|
||||
}, func(err error) {
|
||||
t.Fatalf("should have 2 peers: %v", err)
|
||||
})
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// find the non-authoritative server
|
||||
var nonAuth *Server
|
||||
if !s1.IsLeader() {
|
||||
nonAuth = s1
|
||||
} else {
|
||||
nonAuth = s2
|
||||
}
|
||||
|
||||
// Resolve the token
|
||||
acl, err := nonAuth.resolveToken("foobar")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
|
||||
// Check the policy, should allow all
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected failed read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_DownPolicy_Deny(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLDownPolicy = "deny"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.ACLDownPolicy = "deny"
|
||||
c.Bootstrap = false // Disable bootstrap
|
||||
})
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
|
||||
// Try to join
|
||||
addr := fmt.Sprintf("127.0.0.1:%d",
|
||||
s1.config.SerfLANConfig.MemberlistConfig.BindPort)
|
||||
if _, err := s2.JoinLAN([]string{addr}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
testutil.WaitForResult(func() (bool, error) {
|
||||
p1, _ := s1.raftPeers.Peers()
|
||||
return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1))
|
||||
}, func(err error) {
|
||||
t.Fatalf("should have 2 peers: %v", err)
|
||||
})
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// Create a new token
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testACLPolicy,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var id string
|
||||
if err := client.Call("ACL.Apply", &arg, &id); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// find the non-authoritative server
|
||||
var nonAuth *Server
|
||||
var auth *Server
|
||||
if !s1.IsLeader() {
|
||||
nonAuth = s1
|
||||
auth = s2
|
||||
} else {
|
||||
nonAuth = s2
|
||||
auth = s1
|
||||
}
|
||||
|
||||
// Kill the authoritative server
|
||||
auth.Shutdown()
|
||||
|
||||
// Token should resolve into a DenyAll
|
||||
aclR, err := nonAuth.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if aclR != acl.DenyAll() {
|
||||
t.Fatalf("bad acl: %#v", aclR)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_DownPolicy_Allow(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLDownPolicy = "allow"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.ACLDownPolicy = "allow"
|
||||
c.Bootstrap = false // Disable bootstrap
|
||||
})
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
|
||||
// Try to join
|
||||
addr := fmt.Sprintf("127.0.0.1:%d",
|
||||
s1.config.SerfLANConfig.MemberlistConfig.BindPort)
|
||||
if _, err := s2.JoinLAN([]string{addr}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
testutil.WaitForResult(func() (bool, error) {
|
||||
p1, _ := s1.raftPeers.Peers()
|
||||
return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1))
|
||||
}, func(err error) {
|
||||
t.Fatalf("should have 2 peers: %v", err)
|
||||
})
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// Create a new token
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testACLPolicy,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var id string
|
||||
if err := client.Call("ACL.Apply", &arg, &id); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// find the non-authoritative server
|
||||
var nonAuth *Server
|
||||
var auth *Server
|
||||
if !s1.IsLeader() {
|
||||
nonAuth = s1
|
||||
auth = s2
|
||||
} else {
|
||||
nonAuth = s2
|
||||
auth = s1
|
||||
}
|
||||
|
||||
// Kill the authoritative server
|
||||
auth.Shutdown()
|
||||
|
||||
// Token should resolve into a AllowAll
|
||||
aclR, err := nonAuth.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if aclR != acl.AllowAll() {
|
||||
t.Fatalf("bad acl: %#v", aclR)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_DownPolicy_ExtendCache(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLTTL = 0
|
||||
c.ACLDownPolicy = "extend-cache"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.ACLTTL = 0
|
||||
c.ACLDownPolicy = "extend-cache"
|
||||
c.Bootstrap = false // Disable bootstrap
|
||||
})
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
|
||||
// Try to join
|
||||
addr := fmt.Sprintf("127.0.0.1:%d",
|
||||
s1.config.SerfLANConfig.MemberlistConfig.BindPort)
|
||||
if _, err := s2.JoinLAN([]string{addr}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
testutil.WaitForResult(func() (bool, error) {
|
||||
p1, _ := s1.raftPeers.Peers()
|
||||
return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1))
|
||||
}, func(err error) {
|
||||
t.Fatalf("should have 2 peers: %v", err)
|
||||
})
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// Create a new token
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testACLPolicy,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var id string
|
||||
if err := client.Call("ACL.Apply", &arg, &id); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// find the non-authoritative server
|
||||
var nonAuth *Server
|
||||
var auth *Server
|
||||
if !s1.IsLeader() {
|
||||
nonAuth = s1
|
||||
auth = s2
|
||||
} else {
|
||||
nonAuth = s2
|
||||
auth = s1
|
||||
}
|
||||
|
||||
// Warm the caches
|
||||
aclR, err := nonAuth.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if aclR == nil {
|
||||
t.Fatalf("bad acl: %#v", aclR)
|
||||
}
|
||||
|
||||
// Kill the authoritative server
|
||||
auth.Shutdown()
|
||||
|
||||
// Token should resolve into cached copy
|
||||
aclR2, err := nonAuth.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if aclR2 != aclR {
|
||||
t.Fatalf("bad acl: %#v", aclR)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_MultiDC_Found(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.Datacenter = "dc2"
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
})
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
|
||||
// Try to join
|
||||
addr := fmt.Sprintf("127.0.0.1:%d",
|
||||
s1.config.SerfWANConfig.MemberlistConfig.BindPort)
|
||||
if _, err := s2.JoinWAN([]string{addr}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
testutil.WaitForLeader(t, client.Call, "dc2")
|
||||
|
||||
// Create a new token
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testACLPolicy,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var id string
|
||||
if err := client.Call("ACL.Apply", &arg, &id); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Token should resolve
|
||||
acl, err := s2.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
|
||||
// Check the policy
|
||||
if acl.KeyRead("bar") {
|
||||
t.Fatalf("unexpected read")
|
||||
}
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected failed read")
|
||||
}
|
||||
}
|
||||
|
||||
var testACLPolicy = `
|
||||
key "" {
|
||||
policy = "deny"
|
||||
}
|
||||
key "foo/" {
|
||||
policy = "write"
|
||||
}
|
||||
`
|
|
@ -80,6 +80,11 @@ func NewClient(config *Config) (*Client, error) {
|
|||
return nil, fmt.Errorf("Config must provide a DataDir")
|
||||
}
|
||||
|
||||
// Sanity check the ACLs
|
||||
if err := config.CheckACL(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure we have a log output
|
||||
if config.LogOutput == nil {
|
||||
config.LogOutput = os.Stderr
|
||||
|
|
|
@ -128,6 +128,38 @@ type Config struct {
|
|||
// operators track which versions are actively deployed
|
||||
Build string
|
||||
|
||||
// ACLToken is the default token to use when making a request.
|
||||
// If not provided, the anonymous token is used. This enables
|
||||
// backwards compatibility as well.
|
||||
ACLToken string
|
||||
|
||||
// ACLMasterToken is used to bootstrap the ACL system. It should be specified
|
||||
// on the servers in the ACLDatacenter. When the leader comes online, it ensures
|
||||
// that the Master token is available. This provides the initial token.
|
||||
ACLMasterToken string
|
||||
|
||||
// ACLDatacenter provides the authoritative datacenter for ACL
|
||||
// tokens. If not provided, ACL verification is disabled.
|
||||
ACLDatacenter string
|
||||
|
||||
// ACLTTL controls the time-to-live of cached ACL policies.
|
||||
// It can be set to zero to disable caching, but this adds
|
||||
// a substantial cost.
|
||||
ACLTTL time.Duration
|
||||
|
||||
// ACLDefaultPolicy is used to control the ACL interaction when
|
||||
// there is no defined policy. This can be "allow" which means
|
||||
// ACLs are used to black-list, or "deny" which means ACLs are
|
||||
// white-lists.
|
||||
ACLDefaultPolicy string
|
||||
|
||||
// ACLDownPolicy controls the behavior of ACLs if the ACLDatacenter
|
||||
// cannot be contacted. It can be either "deny" to deny all requests,
|
||||
// or "extend-cache" which ignores the ACLCacheInterval and uses
|
||||
// cached policies. If a policy is not in the cache, it acts like deny.
|
||||
// "allow" can be used to allow all requests. This is not recommended.
|
||||
ACLDownPolicy string
|
||||
|
||||
// ServerUp callback can be used to trigger a notification that
|
||||
// a Consul server is now up and known about.
|
||||
ServerUp func()
|
||||
|
@ -145,6 +177,24 @@ func (c *Config) CheckVersion() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CheckACL is used to sanity check the ACL configuration
|
||||
func (c *Config) CheckACL() error {
|
||||
switch c.ACLDefaultPolicy {
|
||||
case "allow":
|
||||
case "deny":
|
||||
default:
|
||||
return fmt.Errorf("Unsupported default ACL policy: %s", c.ACLDefaultPolicy)
|
||||
}
|
||||
switch c.ACLDownPolicy {
|
||||
case "allow":
|
||||
case "deny":
|
||||
case "extend-cache":
|
||||
default:
|
||||
return fmt.Errorf("Unsupported down ACL policy: %s", c.ACLDownPolicy)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppendCA opens and parses the CA file and adds the certificates to
|
||||
// the provided CertPool.
|
||||
func (c *Config) AppendCA(pool *x509.CertPool) error {
|
||||
|
@ -324,6 +374,9 @@ func DefaultConfig() *Config {
|
|||
SerfWANConfig: serf.DefaultConfig(),
|
||||
ReconcileInterval: 60 * time.Second,
|
||||
ProtocolVersion: ProtocolVersionMax,
|
||||
ACLTTL: 30 * time.Second,
|
||||
ACLDefaultPolicy: "allow",
|
||||
ACLDownPolicy: "extend-cache",
|
||||
}
|
||||
|
||||
// Increase our reap interval to 3 days instead of 24h.
|
||||
|
|
89
consul/filter.go
Normal file
89
consul/filter.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
)
|
||||
|
||||
type dirEntFilter struct {
|
||||
acl acl.ACL
|
||||
ent structs.DirEntries
|
||||
}
|
||||
|
||||
func (d *dirEntFilter) Len() int {
|
||||
return len(d.ent)
|
||||
}
|
||||
func (d *dirEntFilter) Filter(i int) bool {
|
||||
return !d.acl.KeyRead(d.ent[i].Key)
|
||||
}
|
||||
func (d *dirEntFilter) Move(dst, src, span int) {
|
||||
copy(d.ent[dst:dst+span], d.ent[src:src+span])
|
||||
}
|
||||
|
||||
// FilterDirEnt is used to filter a list of directory entries
|
||||
// by applying an ACL policy
|
||||
func FilterDirEnt(acl acl.ACL, ent structs.DirEntries) structs.DirEntries {
|
||||
df := dirEntFilter{acl: acl, ent: ent}
|
||||
return ent[:FilterEntries(&df)]
|
||||
}
|
||||
|
||||
type keyFilter struct {
|
||||
acl acl.ACL
|
||||
keys []string
|
||||
}
|
||||
|
||||
func (k *keyFilter) Len() int {
|
||||
return len(k.keys)
|
||||
}
|
||||
func (k *keyFilter) Filter(i int) bool {
|
||||
return !k.acl.KeyRead(k.keys[i])
|
||||
}
|
||||
|
||||
func (k *keyFilter) Move(dst, src, span int) {
|
||||
copy(k.keys[dst:dst+span], k.keys[src:src+span])
|
||||
}
|
||||
|
||||
// FilterKeys is used to filter a list of keys by
|
||||
// applying an ACL policy
|
||||
func FilterKeys(acl acl.ACL, keys []string) []string {
|
||||
kf := keyFilter{acl: acl, keys: keys}
|
||||
return keys[:FilterEntries(&kf)]
|
||||
}
|
||||
|
||||
// Filter interfae is used with FilterEntries to do an
|
||||
// in-place filter of a slice.
|
||||
type Filter interface {
|
||||
Len() int
|
||||
Filter(int) bool
|
||||
Move(dst, src, span int)
|
||||
}
|
||||
|
||||
// FilterEntries is used to do an inplace filter of
|
||||
// a slice. This has cost proportional to the list length.
|
||||
func FilterEntries(f Filter) int {
|
||||
// Compact the list
|
||||
dst := 0
|
||||
src := 0
|
||||
n := f.Len()
|
||||
for dst < n {
|
||||
for src < n && f.Filter(src) {
|
||||
src++
|
||||
}
|
||||
if src == n {
|
||||
break
|
||||
}
|
||||
end := src + 1
|
||||
for end < n && !f.Filter(end) {
|
||||
end++
|
||||
}
|
||||
span := end - src
|
||||
if span > 0 {
|
||||
f.Move(dst, src, span)
|
||||
dst += span
|
||||
src += span
|
||||
}
|
||||
}
|
||||
|
||||
// Return the size of the slice
|
||||
return dst
|
||||
}
|
96
consul/filter_test.go
Normal file
96
consul/filter_test.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
)
|
||||
|
||||
func TestFilterDirEnt(t *testing.T) {
|
||||
policy, _ := acl.Parse(testFilterRules)
|
||||
aclR, _ := acl.New(acl.DenyAll(), policy)
|
||||
|
||||
type tcase struct {
|
||||
in []string
|
||||
out []string
|
||||
}
|
||||
cases := []tcase{
|
||||
tcase{
|
||||
in: []string{"foo/test", "foo/priv/nope", "foo/other", "zoo"},
|
||||
out: []string{"foo/test", "foo/other"},
|
||||
},
|
||||
tcase{
|
||||
in: []string{"abe", "lincoln"},
|
||||
out: nil,
|
||||
},
|
||||
tcase{
|
||||
in: []string{"abe", "foo/1", "foo/2", "foo/3", "nope"},
|
||||
out: []string{"foo/1", "foo/2", "foo/3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
ents := structs.DirEntries{}
|
||||
for _, in := range tc.in {
|
||||
ents = append(ents, &structs.DirEntry{Key: in})
|
||||
}
|
||||
|
||||
ents = FilterDirEnt(aclR, ents)
|
||||
var outL []string
|
||||
for _, e := range ents {
|
||||
outL = append(outL, e.Key)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(outL, tc.out) {
|
||||
t.Fatalf("bad: %#v %#v", outL, tc.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeys(t *testing.T) {
|
||||
policy, _ := acl.Parse(testFilterRules)
|
||||
aclR, _ := acl.New(acl.DenyAll(), policy)
|
||||
|
||||
type tcase struct {
|
||||
in []string
|
||||
out []string
|
||||
}
|
||||
cases := []tcase{
|
||||
tcase{
|
||||
in: []string{"foo/test", "foo/priv/nope", "foo/other", "zoo"},
|
||||
out: []string{"foo/test", "foo/other"},
|
||||
},
|
||||
tcase{
|
||||
in: []string{"abe", "lincoln"},
|
||||
out: nil,
|
||||
},
|
||||
tcase{
|
||||
in: []string{"abe", "foo/1", "foo/2", "foo/3", "nope"},
|
||||
out: []string{"foo/1", "foo/2", "foo/3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
out := FilterKeys(aclR, tc.in)
|
||||
if !reflect.DeepEqual(out, tc.out) {
|
||||
t.Fatalf("bad: %#v %#v", out, tc.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testFilterRules = `
|
||||
key "" {
|
||||
policy = "deny"
|
||||
}
|
||||
key "foo/" {
|
||||
policy = "read"
|
||||
}
|
||||
key "foo/priv/" {
|
||||
policy = "deny"
|
||||
}
|
||||
key "zip/" {
|
||||
policy = "read"
|
||||
}
|
||||
`
|
|
@ -69,6 +69,8 @@ func (c *consulFSM) Apply(log *raft.Log) interface{} {
|
|||
return c.applyKVSOperation(buf[1:], log.Index)
|
||||
case structs.SessionRequestType:
|
||||
return c.applySessionOperation(buf[1:], log.Index)
|
||||
case structs.ACLRequestType:
|
||||
return c.applyACLOperation(buf[1:], log.Index)
|
||||
default:
|
||||
panic(fmt.Errorf("failed to apply request: %#v", buf))
|
||||
}
|
||||
|
@ -196,6 +198,33 @@ func (c *consulFSM) applySessionOperation(buf []byte, index uint64) interface{}
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *consulFSM) applyACLOperation(buf []byte, index uint64) interface{} {
|
||||
var req structs.ACLRequest
|
||||
if err := structs.Decode(buf, &req); err != nil {
|
||||
panic(fmt.Errorf("failed to decode request: %v", err))
|
||||
}
|
||||
switch req.Op {
|
||||
case structs.ACLSet:
|
||||
if err := c.state.ACLSet(index, &req.ACL, false); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return req.ACL.ID
|
||||
}
|
||||
case structs.ACLForceSet:
|
||||
if err := c.state.ACLSet(index, &req.ACL, true); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return req.ACL.ID
|
||||
}
|
||||
case structs.ACLDelete:
|
||||
return c.state.ACLDelete(index, req.ACL.ID)
|
||||
default:
|
||||
c.logger.Printf("[WARN] consul.fsm: Invalid ACL operation '%s'", req.Op)
|
||||
return fmt.Errorf("Invalid ACL operation '%s'", req.Op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *consulFSM) Snapshot() (raft.FSMSnapshot, error) {
|
||||
defer func(start time.Time) {
|
||||
c.logger.Printf("[INFO] consul.fsm: snapshot created in %v", time.Now().Sub(start))
|
||||
|
@ -267,6 +296,15 @@ func (c *consulFSM) Restore(old io.ReadCloser) error {
|
|||
return err
|
||||
}
|
||||
|
||||
case structs.ACLRequestType:
|
||||
var req structs.ACL
|
||||
if err := dec.Decode(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.state.ACLRestore(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Unrecognized msg type: %v", msgType)
|
||||
}
|
||||
|
@ -298,6 +336,11 @@ func (s *consulSnapshot) Persist(sink raft.SnapshotSink) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := s.persistACLs(sink, encoder); err != nil {
|
||||
sink.Cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.persistKV(sink, encoder); err != nil {
|
||||
sink.Cancel()
|
||||
return err
|
||||
|
@ -364,6 +407,22 @@ func (s *consulSnapshot) persistSessions(sink raft.SnapshotSink,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *consulSnapshot) persistACLs(sink raft.SnapshotSink,
|
||||
encoder *codec.Encoder) error {
|
||||
acls, err := s.state.ACLList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range acls {
|
||||
sink.Write([]byte{byte(structs.ACLRequestType)})
|
||||
if err := encoder.Encode(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *consulSnapshot) persistKV(sink raft.SnapshotSink,
|
||||
encoder *codec.Encoder) error {
|
||||
streamCh := make(chan interface{}, 256)
|
||||
|
|
|
@ -328,6 +328,8 @@ func TestFSM_SnapshotRestore(t *testing.T) {
|
|||
})
|
||||
session := &structs.Session{Node: "foo"}
|
||||
fsm.state.SessionCreate(9, session)
|
||||
acl := &structs.ACL{Name: "User Token"}
|
||||
fsm.state.ACLSet(10, acl, false)
|
||||
|
||||
// Snapshot
|
||||
snap, err := fsm.Snapshot()
|
||||
|
@ -392,7 +394,16 @@ func TestFSM_SnapshotRestore(t *testing.T) {
|
|||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if s.Node != "foo" {
|
||||
t.Fatalf("bad: %v", d)
|
||||
t.Fatalf("bad: %v", s)
|
||||
}
|
||||
|
||||
// Verify ACL is restored
|
||||
_, a, err := fsm.state.ACLGet(acl.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if a.Name != "User Token" {
|
||||
t.Fatalf("bad: %v", a)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -767,3 +778,75 @@ func TestFSM_KVSUnlock(t *testing.T) {
|
|||
t.Fatalf("bad: %v", *d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSM_ACL_Set_Delete(t *testing.T) {
|
||||
fsm, err := NewFSM(os.Stderr)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer fsm.Close()
|
||||
|
||||
// Create a new ACL
|
||||
req := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
},
|
||||
}
|
||||
buf, err := structs.Encode(structs.ACLRequestType, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
resp := fsm.Apply(makeLog(buf))
|
||||
if err, ok := resp.(error); ok {
|
||||
t.Fatalf("resp: %v", err)
|
||||
}
|
||||
|
||||
// Get the ACL
|
||||
id := resp.(string)
|
||||
_, acl, err := fsm.state.ACLGet(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("missing")
|
||||
}
|
||||
|
||||
// Verify the ACL
|
||||
if acl.ID != id {
|
||||
t.Fatalf("bad: %v", *acl)
|
||||
}
|
||||
if acl.Name != "User token" {
|
||||
t.Fatalf("bad: %v", *acl)
|
||||
}
|
||||
if acl.Type != structs.ACLTypeClient {
|
||||
t.Fatalf("bad: %v", *acl)
|
||||
}
|
||||
|
||||
// Try to destroy
|
||||
destroy := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLDelete,
|
||||
ACL: structs.ACL{
|
||||
ID: id,
|
||||
},
|
||||
}
|
||||
buf, err = structs.Encode(structs.ACLRequestType, destroy)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
resp = fsm.Apply(makeLog(buf))
|
||||
if resp != nil {
|
||||
t.Fatalf("resp: %v", resp)
|
||||
}
|
||||
|
||||
_, acl, err = fsm.state.ACLGet(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl != nil {
|
||||
t.Fatalf("should be destroyed")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@ package consul
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"time"
|
||||
)
|
||||
|
||||
// KVS endpoint is used to manipulate the Key-Value store
|
||||
|
@ -25,6 +26,23 @@ func (k *KVS) Apply(args *structs.KVSRequest, reply *bool) error {
|
|||
return fmt.Errorf("Must provide key")
|
||||
}
|
||||
|
||||
// Apply the ACL policy if any
|
||||
acl, err := k.srv.resolveToken(args.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if acl != nil {
|
||||
switch args.Op {
|
||||
case structs.KVSDeleteTree:
|
||||
if !acl.KeyWritePrefix(args.DirEnt.Key) {
|
||||
return permissionDeniedErr
|
||||
}
|
||||
default:
|
||||
if !acl.KeyWrite(args.DirEnt.Key) {
|
||||
return permissionDeniedErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a lock, we must check for a lock-delay. Since lock-delay
|
||||
// is based on wall-time, each peer expire the lock-delay at a slightly
|
||||
// different time. This means the enforcement of lock-delay cannot be done
|
||||
|
@ -65,6 +83,11 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er
|
|||
return err
|
||||
}
|
||||
|
||||
acl, err := k.srv.resolveToken(args.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the local state
|
||||
state := k.srv.fsm.State()
|
||||
return k.srv.blockingRPC(&args.QueryOptions,
|
||||
|
@ -75,6 +98,9 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if acl != nil && !acl.KeyRead(args.Key) {
|
||||
ent = nil
|
||||
}
|
||||
if ent == nil {
|
||||
// Must provide non-zero index to prevent blocking
|
||||
// Index 1 is impossible anyways (due to Raft internals)
|
||||
|
@ -98,6 +124,11 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e
|
|||
return err
|
||||
}
|
||||
|
||||
acl, err := k.srv.resolveToken(args.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the local state
|
||||
state := k.srv.fsm.State()
|
||||
return k.srv.blockingRPC(&args.QueryOptions,
|
||||
|
@ -108,6 +139,9 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if acl != nil {
|
||||
ent = FilterDirEnt(acl, ent)
|
||||
}
|
||||
if len(ent) == 0 {
|
||||
// Must provide non-zero index to prevent blocking
|
||||
// Index 1 is impossible anyways (due to Raft internals)
|
||||
|
@ -139,14 +173,23 @@ func (k *KVS) ListKeys(args *structs.KeyListRequest, reply *structs.IndexedKeyLi
|
|||
return err
|
||||
}
|
||||
|
||||
acl, err := k.srv.resolveToken(args.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the local state
|
||||
state := k.srv.fsm.State()
|
||||
return k.srv.blockingRPC(&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
state.QueryTables("KVSListKeys"),
|
||||
func() error {
|
||||
var err error
|
||||
reply.Index, reply.Keys, err = state.KVSListKeys(args.Prefix, args.Seperator)
|
||||
index, keys, err := state.KVSListKeys(args.Prefix, args.Seperator)
|
||||
reply.Index = index
|
||||
if acl != nil {
|
||||
keys = FilterKeys(acl, keys)
|
||||
}
|
||||
reply.Keys = keys
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"github.com/hashicorp/consul/testutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/consul/structs"
|
||||
"github.com/hashicorp/consul/testutil"
|
||||
)
|
||||
|
||||
func TestKVS_Apply(t *testing.T) {
|
||||
|
@ -64,6 +66,68 @@ func TestKVS_Apply(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestKVS_Apply_ACLDeny(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
c.ACLDefaultPolicy = "deny"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
// Create the ACL
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testListRules,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out string
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
id := out
|
||||
|
||||
// Try a write
|
||||
argR := structs.KVSRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.KVSSet,
|
||||
DirEnt: structs.DirEntry{
|
||||
Key: "foo/bar",
|
||||
Flags: 42,
|
||||
Value: []byte("test"),
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: id},
|
||||
}
|
||||
var outR bool
|
||||
err := client.Call("KVS.Apply", &argR, &outR)
|
||||
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Try a recursive delete
|
||||
argR = structs.KVSRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.KVSDeleteTree,
|
||||
DirEnt: structs.DirEntry{
|
||||
Key: "test",
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: id},
|
||||
}
|
||||
err = client.Call("KVS.Apply", &argR, &outR)
|
||||
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVS_Get(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
|
@ -111,6 +175,51 @@ func TestKVS_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestKVS_Get_ACLDeny(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
c.ACLDefaultPolicy = "deny"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
arg := structs.KVSRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.KVSSet,
|
||||
DirEnt: structs.DirEntry{
|
||||
Key: "zip",
|
||||
Flags: 42,
|
||||
Value: []byte("test"),
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out bool
|
||||
if err := client.Call("KVS.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
getR := structs.KeyRequest{
|
||||
Datacenter: "dc1",
|
||||
Key: "zip",
|
||||
}
|
||||
var dirent structs.IndexedDirEntries
|
||||
if err := client.Call("KVS.Get", &getR, &dirent); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if dirent.Index == 0 {
|
||||
t.Fatalf("Bad: %v", dirent)
|
||||
}
|
||||
if len(dirent.Entries) != 0 {
|
||||
t.Fatalf("Bad: %v", dirent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVSEndpoint_List(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
|
@ -170,6 +279,90 @@ func TestKVSEndpoint_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestKVSEndpoint_List_ACLDeny(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
c.ACLDefaultPolicy = "deny"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
keys := []string{
|
||||
"abe",
|
||||
"bar",
|
||||
"foo",
|
||||
"test",
|
||||
"zip",
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
arg := structs.KVSRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.KVSSet,
|
||||
DirEnt: structs.DirEntry{
|
||||
Key: key,
|
||||
Flags: 1,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out bool
|
||||
if err := client.Call("KVS.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testListRules,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out string
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
id := out
|
||||
|
||||
getR := structs.KeyRequest{
|
||||
Datacenter: "dc1",
|
||||
Key: "",
|
||||
QueryOptions: structs.QueryOptions{Token: id},
|
||||
}
|
||||
var dirent structs.IndexedDirEntries
|
||||
if err := client.Call("KVS.List", &getR, &dirent); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if dirent.Index == 0 {
|
||||
t.Fatalf("Bad: %v", dirent)
|
||||
}
|
||||
if len(dirent.Entries) != 2 {
|
||||
t.Fatalf("Bad: %v", dirent.Entries)
|
||||
}
|
||||
for i := 0; i < len(dirent.Entries); i++ {
|
||||
d := dirent.Entries[i]
|
||||
switch i {
|
||||
case 0:
|
||||
if d.Key != "foo" {
|
||||
t.Fatalf("bad key")
|
||||
}
|
||||
case 1:
|
||||
if d.Key != "test" {
|
||||
t.Fatalf("bad key")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVSEndpoint_ListKeys(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
|
@ -227,6 +420,84 @@ func TestKVSEndpoint_ListKeys(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestKVSEndpoint_ListKeys_ACLDeny(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
c.ACLDefaultPolicy = "deny"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
testutil.WaitForLeader(t, client.Call, "dc1")
|
||||
|
||||
keys := []string{
|
||||
"abe",
|
||||
"bar",
|
||||
"foo",
|
||||
"test",
|
||||
"zip",
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
arg := structs.KVSRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.KVSSet,
|
||||
DirEnt: structs.DirEntry{
|
||||
Key: key,
|
||||
Flags: 1,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out bool
|
||||
if err := client.Call("KVS.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testListRules,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var out string
|
||||
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
id := out
|
||||
|
||||
getR := structs.KeyListRequest{
|
||||
Datacenter: "dc1",
|
||||
Prefix: "",
|
||||
Seperator: "/",
|
||||
QueryOptions: structs.QueryOptions{Token: id},
|
||||
}
|
||||
var dirent structs.IndexedKeyList
|
||||
if err := client.Call("KVS.ListKeys", &getR, &dirent); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if dirent.Index == 0 {
|
||||
t.Fatalf("Bad: %v", dirent)
|
||||
}
|
||||
if len(dirent.Keys) != 2 {
|
||||
t.Fatalf("Bad: %v", dirent.Keys)
|
||||
}
|
||||
if dirent.Keys[0] != "foo" {
|
||||
t.Fatalf("Bad: %v", dirent.Keys)
|
||||
}
|
||||
if dirent.Keys[1] != "test" {
|
||||
t.Fatalf("Bad: %v", dirent.Keys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVS_Apply_LockDelay(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
|
@ -294,3 +565,18 @@ func TestKVS_Apply_LockDelay(t *testing.T) {
|
|||
t.Fatalf("should acquire")
|
||||
}
|
||||
}
|
||||
|
||||
var testListRules = `
|
||||
key "" {
|
||||
policy = "deny"
|
||||
}
|
||||
key "foo" {
|
||||
policy = "read"
|
||||
}
|
||||
key "test" {
|
||||
policy = "write"
|
||||
}
|
||||
key "test/priv" {
|
||||
policy = "read"
|
||||
}
|
||||
`
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
@ -54,6 +55,11 @@ func (s *Server) leaderLoop(stopCh chan struct{}) {
|
|||
s.logger.Printf("[WARN] consul: failed to broadcast new leader event: %v", err)
|
||||
}
|
||||
|
||||
// Setup ACLs if we are the leader and need to
|
||||
if err := s.initializeACL(); err != nil {
|
||||
s.logger.Printf("[ERR] consul: ACL initialization failed: %v", err)
|
||||
}
|
||||
|
||||
// Reconcile channel is only used once initial reconcile
|
||||
// has succeeded
|
||||
var reconcileCh chan serf.Member
|
||||
|
@ -99,6 +105,73 @@ WAIT:
|
|||
}
|
||||
}
|
||||
|
||||
// initializeACL is used to setup the ACLs if we are the leader
|
||||
// and need to do this.
|
||||
func (s *Server) initializeACL() error {
|
||||
// Bail if not configured or we are not authoritative
|
||||
authDC := s.config.ACLDatacenter
|
||||
if len(authDC) == 0 || authDC != s.config.Datacenter {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Purge the cache, since it could've changed while we
|
||||
// were not the leader
|
||||
s.aclAuthCache.Purge()
|
||||
|
||||
// Look for the anonymous token
|
||||
state := s.fsm.State()
|
||||
_, acl, err := state.ACLGet(anonymousToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get anonymous token: %v", err)
|
||||
}
|
||||
|
||||
// Create anonymous token if missing
|
||||
if acl == nil {
|
||||
req := structs.ACLRequest{
|
||||
Datacenter: authDC,
|
||||
Op: structs.ACLForceSet,
|
||||
ACL: structs.ACL{
|
||||
ID: anonymousToken,
|
||||
Name: "Anonymous Token",
|
||||
Type: structs.ACLTypeClient,
|
||||
},
|
||||
}
|
||||
_, err := s.raftApply(structs.ACLRequestType, &req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create anonymous token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for configured master token
|
||||
master := s.config.ACLMasterToken
|
||||
if len(master) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Look for the master token
|
||||
_, acl, err = state.ACLGet(master)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get master token: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
req := structs.ACLRequest{
|
||||
Datacenter: authDC,
|
||||
Op: structs.ACLForceSet,
|
||||
ACL: structs.ACL{
|
||||
ID: master,
|
||||
Name: "Master Token",
|
||||
Type: structs.ACLTypeManagement,
|
||||
},
|
||||
}
|
||||
_, err := s.raftApply(structs.ACLRequestType, &req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create master token: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconcile is used to reconcile the differences between Serf
|
||||
// membership and what is reflected in our strongly consistent store.
|
||||
// Mainly we need to ensure all live nodes are registered, all failed
|
||||
|
|
|
@ -15,6 +15,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/golang-lru"
|
||||
"github.com/hashicorp/raft"
|
||||
"github.com/hashicorp/raft-mdb"
|
||||
"github.com/hashicorp/serf/serf"
|
||||
|
@ -43,11 +45,24 @@ const (
|
|||
// serverMaxStreams controsl how many idle streams we keep
|
||||
// open to a server
|
||||
serverMaxStreams = 64
|
||||
|
||||
// Maximum number of cached ACL entries
|
||||
aclCacheSize = 256
|
||||
)
|
||||
|
||||
// Server is Consul server which manages the service discovery,
|
||||
// health checking, DC forwarding, Raft, and multiple Serf pools.
|
||||
type Server struct {
|
||||
// aclAuthCache is the authoritative ACL cache
|
||||
aclAuthCache *acl.Cache
|
||||
|
||||
// aclCache is a non-authoritative ACL cache
|
||||
aclCache *lru.Cache
|
||||
|
||||
// aclPolicyCache is a policy cache
|
||||
aclPolicyCache *lru.Cache
|
||||
|
||||
// Consul configuration
|
||||
config *Config
|
||||
|
||||
// Connection pool to other consul servers
|
||||
|
@ -125,6 +140,7 @@ type endpoints struct {
|
|||
KVS *KVS
|
||||
Session *Session
|
||||
Internal *Internal
|
||||
ACL *ACL
|
||||
}
|
||||
|
||||
// NewServer is used to construct a new Consul server from the
|
||||
|
@ -140,6 +156,11 @@ func NewServer(config *Config) (*Server, error) {
|
|||
return nil, fmt.Errorf("Config must provide a DataDir")
|
||||
}
|
||||
|
||||
// Sanity check the ACLs
|
||||
if err := config.CheckACL(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure we have a log output
|
||||
if config.LogOutput == nil {
|
||||
config.LogOutput = os.Stderr
|
||||
|
@ -175,6 +196,27 @@ func NewServer(config *Config) (*Server, error) {
|
|||
shutdownCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Initialize the authoritative ACL cache
|
||||
s.aclAuthCache, err = acl.NewCache(aclCacheSize, s.aclFault)
|
||||
if err != nil {
|
||||
s.Shutdown()
|
||||
return nil, fmt.Errorf("Failed to create ACL cache: %v", err)
|
||||
}
|
||||
|
||||
// Initialize the non-authoritative ACL cache
|
||||
s.aclCache, err = lru.New(aclCacheSize)
|
||||
if err != nil {
|
||||
s.Shutdown()
|
||||
return nil, fmt.Errorf("Failed to create ACL cache: %v", err)
|
||||
}
|
||||
|
||||
// Initialize the ACL policy cache
|
||||
s.aclPolicyCache, err = lru.New(aclCacheSize)
|
||||
if err != nil {
|
||||
s.Shutdown()
|
||||
return nil, fmt.Errorf("Failed to create ACL policy cache: %v", err)
|
||||
}
|
||||
|
||||
// Initialize the RPC layer
|
||||
if err := s.setupRPC(tlsConfig); err != nil {
|
||||
s.Shutdown()
|
||||
|
@ -336,6 +378,7 @@ func (s *Server) setupRPC(tlsConfig *tls.Config) error {
|
|||
s.endpoints.KVS = &KVS{s}
|
||||
s.endpoints.Session = &Session{s}
|
||||
s.endpoints.Internal = &Internal{s}
|
||||
s.endpoints.ACL = &ACL{s}
|
||||
|
||||
// Register the handlers
|
||||
s.rpcServer.Register(s.endpoints.Status)
|
||||
|
@ -344,6 +387,7 @@ func (s *Server) setupRPC(tlsConfig *tls.Config) error {
|
|||
s.rpcServer.Register(s.endpoints.KVS)
|
||||
s.rpcServer.Register(s.endpoints.Session)
|
||||
s.rpcServer.Register(s.endpoints.Internal)
|
||||
s.rpcServer.Register(s.endpoints.ACL)
|
||||
|
||||
list, err := net.ListenTCP("tcp", s.config.RPCAddr)
|
||||
if err != nil {
|
||||
|
|
|
@ -101,6 +101,17 @@ func testServerDCExpect(t *testing.T, dc string, expect int) (string, *Server) {
|
|||
return dir, server
|
||||
}
|
||||
|
||||
func testServerWithConfig(t *testing.T, cb func(c *Config)) (string, *Server) {
|
||||
name := fmt.Sprintf("Node %d", getPort())
|
||||
dir, config := testServerConfig(t, name)
|
||||
cb(config)
|
||||
server, err := NewServer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
return dir, server
|
||||
}
|
||||
|
||||
func TestServer_StartStop(t *testing.T) {
|
||||
dir := tmpDir(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
|
|
@ -21,6 +21,7 @@ const (
|
|||
dbKVS = "kvs"
|
||||
dbSessions = "sessions"
|
||||
dbSessionChecks = "sessionChecks"
|
||||
dbACLs = "acls"
|
||||
dbMaxMapSize32bit uint64 = 512 * 1024 * 1024 // 512MB maximum size
|
||||
dbMaxMapSize64bit uint64 = 32 * 1024 * 1024 * 1024 // 32GB maximum size
|
||||
)
|
||||
|
@ -53,6 +54,7 @@ type StateStore struct {
|
|||
kvsTable *MDBTable
|
||||
sessionTable *MDBTable
|
||||
sessionCheckTable *MDBTable
|
||||
aclTable *MDBTable
|
||||
tables MDBTables
|
||||
watch map[*MDBTable]*NotifyGroup
|
||||
queryTables map[string]MDBTables
|
||||
|
@ -306,9 +308,26 @@ func (s *StateStore) initialize() error {
|
|||
},
|
||||
}
|
||||
|
||||
s.aclTable = &MDBTable{
|
||||
Name: dbACLs,
|
||||
Indexes: map[string]*MDBIndex{
|
||||
"id": &MDBIndex{
|
||||
Unique: true,
|
||||
Fields: []string{"ID"},
|
||||
},
|
||||
},
|
||||
Decoder: func(buf []byte) interface{} {
|
||||
out := new(structs.ACL)
|
||||
if err := structs.Decode(buf, out); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
},
|
||||
}
|
||||
|
||||
// Store the set of tables
|
||||
s.tables = []*MDBTable{s.nodeTable, s.serviceTable, s.checkTable,
|
||||
s.kvsTable, s.sessionTable, s.sessionCheckTable}
|
||||
s.kvsTable, s.sessionTable, s.sessionCheckTable, s.aclTable}
|
||||
for _, table := range s.tables {
|
||||
table.Env = s.env
|
||||
table.Encoder = encoder
|
||||
|
@ -338,6 +357,8 @@ func (s *StateStore) initialize() error {
|
|||
"SessionGet": MDBTables{s.sessionTable},
|
||||
"SessionList": MDBTables{s.sessionTable},
|
||||
"NodeSessions": MDBTables{s.sessionTable},
|
||||
"ACLGet": MDBTables{s.aclTable},
|
||||
"ACLList": MDBTables{s.aclTable},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1249,8 +1270,8 @@ func (s *StateStore) SessionCreate(index uint64, session *structs.Session) error
|
|||
}
|
||||
|
||||
// Generate a new session ID, verify uniqueness
|
||||
session.ID = generateUUID()
|
||||
for {
|
||||
session.ID = generateUUID()
|
||||
res, err = s.sessionTable.GetTxn(tx, "id", session.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1346,7 +1367,7 @@ func (s *StateStore) NodeSessions(node string) (uint64, []*structs.Session, erro
|
|||
return idx, out, err
|
||||
}
|
||||
|
||||
// SessionDelete is used to destroy a session.
|
||||
// SessionDestroy is used to destroy a session.
|
||||
func (s *StateStore) SessionDestroy(index uint64, id string) error {
|
||||
tx, err := s.tables.StartTxn(false)
|
||||
if err != nil {
|
||||
|
@ -1482,6 +1503,124 @@ func (s *StateStore) invalidateLocks(index uint64, tx *MDBTxn,
|
|||
return nil
|
||||
}
|
||||
|
||||
// ACLSet is used to create or update an ACL entry
|
||||
// allowCreate is used for initialization of the anonymous and master tokens,
|
||||
// since it permits them to be created with a specified ID that does not exist.
|
||||
func (s *StateStore) ACLSet(index uint64, acl *structs.ACL, allowCreate bool) error {
|
||||
// Start a new txn
|
||||
tx, err := s.tables.StartTxn(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Abort()
|
||||
|
||||
// Generate a new session ID
|
||||
if acl.ID == "" {
|
||||
for {
|
||||
acl.ID = generateUUID()
|
||||
res, err := s.aclTable.GetTxn(tx, "id", acl.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Quit if this ID is unique
|
||||
if len(res) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
acl.CreateIndex = index
|
||||
acl.ModifyIndex = index
|
||||
|
||||
} else {
|
||||
// Look for the existing node
|
||||
res, err := s.aclTable.GetTxn(tx, "id", acl.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch len(res) {
|
||||
case 0:
|
||||
if !allowCreate {
|
||||
return fmt.Errorf("Invalid ACL")
|
||||
}
|
||||
acl.CreateIndex = index
|
||||
acl.ModifyIndex = index
|
||||
case 1:
|
||||
exist := res[0].(*structs.ACL)
|
||||
acl.CreateIndex = exist.CreateIndex
|
||||
acl.ModifyIndex = index
|
||||
default:
|
||||
panic(fmt.Errorf("Duplicate ACL definition. Internal error"))
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the ACL
|
||||
if err := s.aclTable.InsertTxn(tx, acl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Trigger the update notifications
|
||||
if err := s.aclTable.SetLastIndexTxn(tx, index); err != nil {
|
||||
return err
|
||||
}
|
||||
tx.Defer(func() { s.watch[s.aclTable].Notify() })
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// ACLRestore is used to restore an ACL. It should only be used when
|
||||
// doing a restore, otherwise ACLSet should be used.
|
||||
func (s *StateStore) ACLRestore(acl *structs.ACL) error {
|
||||
// Start a new txn
|
||||
tx, err := s.aclTable.StartTxn(false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Abort()
|
||||
|
||||
if err := s.aclTable.InsertTxn(tx, acl); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// ACLGet is used to get an ACL by ID
|
||||
func (s *StateStore) ACLGet(id string) (uint64, *structs.ACL, error) {
|
||||
idx, res, err := s.aclTable.Get("id", id)
|
||||
var d *structs.ACL
|
||||
if len(res) > 0 {
|
||||
d = res[0].(*structs.ACL)
|
||||
}
|
||||
return idx, d, err
|
||||
}
|
||||
|
||||
// ACLList is used to list all the acls
|
||||
func (s *StateStore) ACLList() (uint64, []*structs.ACL, error) {
|
||||
idx, res, err := s.aclTable.Get("id")
|
||||
out := make([]*structs.ACL, len(res))
|
||||
for i, raw := range res {
|
||||
out[i] = raw.(*structs.ACL)
|
||||
}
|
||||
return idx, out, err
|
||||
}
|
||||
|
||||
// ACLDelete is used to remove an ACL
|
||||
func (s *StateStore) ACLDelete(index uint64, id string) error {
|
||||
tx, err := s.tables.StartTxn(false)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Failed to start txn: %v", err))
|
||||
}
|
||||
defer tx.Abort()
|
||||
|
||||
if n, err := s.aclTable.DeleteTxn(tx, "id", id); err != nil {
|
||||
return err
|
||||
} else if n > 0 {
|
||||
if err := s.aclTable.SetLastIndexTxn(tx, index); err != nil {
|
||||
return err
|
||||
}
|
||||
tx.Defer(func() { s.watch[s.aclTable].Notify() })
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Snapshot is used to create a point in time snapshot
|
||||
func (s *StateStore) Snapshot() (*StateSnapshot, error) {
|
||||
// Begin a new txn on all tables
|
||||
|
@ -1555,3 +1694,13 @@ func (s *StateSnapshot) SessionList() ([]*structs.Session, error) {
|
|||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
// ACLList is used to list all of the ACLs
|
||||
func (s *StateSnapshot) ACLList() ([]*structs.ACL, error) {
|
||||
res, err := s.store.aclTable.GetTxn(s.tx, "id")
|
||||
out := make([]*structs.ACL, len(res))
|
||||
for i, raw := range res {
|
||||
out[i] = raw.(*structs.ACL)
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
|
|
@ -652,6 +652,22 @@ func TestStoreSnapshot(t *testing.T) {
|
|||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
a1 := &structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
}
|
||||
if err := store.ACLSet(19, a1, false); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
a2 := &structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
}
|
||||
if err := store.ACLSet(20, a2, false); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Take a snapshot
|
||||
snap, err := store.Snapshot()
|
||||
if err != nil {
|
||||
|
@ -660,7 +676,7 @@ func TestStoreSnapshot(t *testing.T) {
|
|||
defer snap.Close()
|
||||
|
||||
// Check the last nodes
|
||||
if idx := snap.LastIndex(); idx != 18 {
|
||||
if idx := snap.LastIndex(); idx != 20 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
|
||||
|
@ -724,14 +740,23 @@ func TestStoreSnapshot(t *testing.T) {
|
|||
t.Fatalf("missing sessions")
|
||||
}
|
||||
|
||||
// Check for an acl
|
||||
acls, err := snap.ACLList()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(acls) != 2 {
|
||||
t.Fatalf("missing acls")
|
||||
}
|
||||
|
||||
// Make some changes!
|
||||
if err := store.EnsureService(19, "foo", &structs.NodeService{"db", "db", []string{"slave"}, 8000}); err != nil {
|
||||
if err := store.EnsureService(21, "foo", &structs.NodeService{"db", "db", []string{"slave"}, 8000}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if err := store.EnsureService(20, "bar", &structs.NodeService{"db", "db", []string{"master"}, 8000}); err != nil {
|
||||
if err := store.EnsureService(22, "bar", &structs.NodeService{"db", "db", []string{"master"}, 8000}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if err := store.EnsureNode(21, structs.Node{"baz", "127.0.0.3"}); err != nil {
|
||||
if err := store.EnsureNode(23, structs.Node{"baz", "127.0.0.3"}); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
checkAfter := &structs.HealthCheck{
|
||||
|
@ -741,11 +766,16 @@ func TestStoreSnapshot(t *testing.T) {
|
|||
Status: structs.HealthCritical,
|
||||
ServiceID: "db",
|
||||
}
|
||||
if err := store.EnsureCheck(22, checkAfter); err != nil {
|
||||
if err := store.EnsureCheck(24, checkAfter); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if err := store.KVSDelete(23, "/web/b"); err != nil {
|
||||
if err := store.KVSDelete(25, "/web/b"); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Nuke an ACL
|
||||
if err := store.ACLDelete(26, a1.ID); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
|
@ -807,6 +837,15 @@ func TestStoreSnapshot(t *testing.T) {
|
|||
if len(sessions) != 2 {
|
||||
t.Fatalf("missing sessions")
|
||||
}
|
||||
|
||||
// Check for an acl
|
||||
acls, err = snap.ACLList()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(acls) != 2 {
|
||||
t.Fatalf("missing acls")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureCheck(t *testing.T) {
|
||||
|
@ -2117,3 +2156,144 @@ func TestSessionInvalidate_KeyUnlock(t *testing.T) {
|
|||
t.Fatalf("Bad: %v", expires)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLSet_Get(t *testing.T) {
|
||||
store, err := testStateStore()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
idx, out, err := store.ACLGet("1234")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 0 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatalf("bad: %v", out)
|
||||
}
|
||||
|
||||
a := &structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: "",
|
||||
}
|
||||
if err := store.ACLSet(50, a, false); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if a.CreateIndex != 50 {
|
||||
t.Fatalf("Bad: %v", a)
|
||||
}
|
||||
if a.ModifyIndex != 50 {
|
||||
t.Fatalf("Bad: %v", a)
|
||||
}
|
||||
if a.ID == "" {
|
||||
t.Fatalf("Bad: %v", a)
|
||||
}
|
||||
|
||||
idx, out, err = store.ACLGet(a.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 50 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if !reflect.DeepEqual(out, a) {
|
||||
t.Fatalf("bad: %v", out)
|
||||
}
|
||||
|
||||
// Update
|
||||
a.Rules = "foo bar baz"
|
||||
if err := store.ACLSet(52, a, false); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if a.CreateIndex != 50 {
|
||||
t.Fatalf("Bad: %v", a)
|
||||
}
|
||||
if a.ModifyIndex != 52 {
|
||||
t.Fatalf("Bad: %v", a)
|
||||
}
|
||||
|
||||
idx, out, err = store.ACLGet(a.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 52 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if !reflect.DeepEqual(out, a) {
|
||||
t.Fatalf("bad: %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLDelete(t *testing.T) {
|
||||
store, err := testStateStore()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
a := &structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: "",
|
||||
}
|
||||
if err := store.ACLSet(50, a, false); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if err := store.ACLDelete(52, a.ID); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if err := store.ACLDelete(53, a.ID); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
idx, out, err := store.ACLGet(a.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 52 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatalf("bad: %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLList(t *testing.T) {
|
||||
store, err := testStateStore()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
a1 := &structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
}
|
||||
if err := store.ACLSet(50, a1, false); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
a2 := &structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
}
|
||||
if err := store.ACLSet(51, a2, false); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
idx, out, err := store.ACLList()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if idx != 51 {
|
||||
t.Fatalf("bad: %v", idx)
|
||||
}
|
||||
if len(out) != 2 {
|
||||
t.Fatalf("bad: %v", out)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,10 @@ package structs
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/ugorji/go/codec"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/ugorji/go/codec"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -20,6 +22,7 @@ const (
|
|||
DeregisterRequestType
|
||||
KVSRequestType
|
||||
SessionRequestType
|
||||
ACLRequestType
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -32,6 +35,15 @@ const (
|
|||
HealthCritical = "critical"
|
||||
)
|
||||
|
||||
const (
|
||||
// Client tokens have rules applied
|
||||
ACLTypeClient = "client"
|
||||
|
||||
// Management tokens have an always allow policy.
|
||||
// They are used for token management.
|
||||
ACLTypeManagement = "management"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxLockDelay provides a maximum LockDelay value for
|
||||
// a session. Any value above this will not be respected.
|
||||
|
@ -43,10 +55,15 @@ type RPCInfo interface {
|
|||
RequestDatacenter() string
|
||||
IsRead() bool
|
||||
AllowStaleRead() bool
|
||||
ACLToken() string
|
||||
}
|
||||
|
||||
// QueryOptions is used to specify various flags for read queries
|
||||
type QueryOptions struct {
|
||||
// Token is the ACL token ID. If not provided, the 'anonymous'
|
||||
// token is assumed for backwards compatibility.
|
||||
Token string
|
||||
|
||||
// If set, wait until query exceeds given index. Must be provided
|
||||
// with MaxQueryTime.
|
||||
MinQueryIndex uint64
|
||||
|
@ -72,7 +89,15 @@ func (q QueryOptions) AllowStaleRead() bool {
|
|||
return q.AllowStale
|
||||
}
|
||||
|
||||
type WriteRequest struct{}
|
||||
func (q QueryOptions) ACLToken() string {
|
||||
return q.Token
|
||||
}
|
||||
|
||||
type WriteRequest struct {
|
||||
// Token is the ACL token ID. If not provided, the 'anonymous'
|
||||
// token is assumed for backwards compatibility.
|
||||
Token string
|
||||
}
|
||||
|
||||
// WriteRequest only applies to writes, always false
|
||||
func (w WriteRequest) IsRead() bool {
|
||||
|
@ -83,6 +108,10 @@ func (w WriteRequest) AllowStaleRead() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (w WriteRequest) ACLToken() string {
|
||||
return w.Token
|
||||
}
|
||||
|
||||
// QueryMeta allows a query response to include potentially
|
||||
// useful metadata about a query
|
||||
type QueryMeta struct {
|
||||
|
@ -396,6 +425,74 @@ type IndexedSessions struct {
|
|||
QueryMeta
|
||||
}
|
||||
|
||||
// ACL is used to represent a token and it's rules
|
||||
type ACL struct {
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
Rules string
|
||||
}
|
||||
type ACLs []*ACL
|
||||
|
||||
type ACLOp string
|
||||
|
||||
const (
|
||||
ACLSet ACLOp = "set"
|
||||
ACLForceSet = "force-set"
|
||||
ACLDelete = "delete"
|
||||
)
|
||||
|
||||
// ACLRequest is used to create, update or delete an ACL
|
||||
type ACLRequest struct {
|
||||
Datacenter string
|
||||
Op ACLOp
|
||||
ACL ACL
|
||||
WriteRequest
|
||||
}
|
||||
|
||||
func (r *ACLRequest) RequestDatacenter() string {
|
||||
return r.Datacenter
|
||||
}
|
||||
|
||||
// ACLSpecificRequest is used to request an ACL by ID
|
||||
type ACLSpecificRequest struct {
|
||||
Datacenter string
|
||||
ACL string
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
func (r *ACLSpecificRequest) RequestDatacenter() string {
|
||||
return r.Datacenter
|
||||
}
|
||||
|
||||
// ACLPolicyRequest is used to request an ACL by ID, conditionally
|
||||
// filtering on an ID
|
||||
type ACLPolicyRequest struct {
|
||||
Datacenter string
|
||||
ACL string
|
||||
ETag string
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
func (r *ACLPolicyRequest) RequestDatacenter() string {
|
||||
return r.Datacenter
|
||||
}
|
||||
|
||||
type IndexedACLs struct {
|
||||
ACLs ACLs
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
type ACLPolicy struct {
|
||||
ETag string
|
||||
Parent string
|
||||
Policy *acl.Policy
|
||||
TTL time.Duration
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
// msgpackHandle is a shared handle for encoding/decoding of structs
|
||||
var msgpackHandle = &codec.MsgpackHandle{}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ All endpoints fall into one of several categories:
|
|||
* catalog - Manages nodes and services
|
||||
* health - Manages health checks
|
||||
* session - Session manipulation
|
||||
* acl - ACL creations and management
|
||||
* status - Consul system status
|
||||
* internal - Internal APIs. Purposely undocumented, subject to change.
|
||||
|
||||
|
@ -85,6 +86,14 @@ By default, the output of all HTTP API requests return minimized JSON with all
|
|||
whitespace removed. By adding "?pretty" to the HTTP request URL,
|
||||
formatted JSON will be returned.
|
||||
|
||||
## ACLs
|
||||
|
||||
Several endpoints in Consul use or require ACL tokens to operate. An agent
|
||||
can be configured to use a default token in requests using the `acl_token`
|
||||
configuration option. However, the token can also be specified per-request
|
||||
by using the "?token=" query parameter. This will take precedence over the
|
||||
default token.
|
||||
|
||||
## KV
|
||||
|
||||
The KV endpoint is used to expose a simple key/value store. This can be used
|
||||
|
@ -99,7 +108,8 @@ are all supported. It is important to note that each datacenter has its
|
|||
own K/V store, and that there is no replication between datacenters.
|
||||
By default the datacenter of the agent is queried, however the dc can
|
||||
be provided using the "?dc=" query parameter. If a client wants to write
|
||||
to all Datacenters, one request per datacenter must be made.
|
||||
to all Datacenters, one request per datacenter must be made. The KV endpoint
|
||||
supports the use of ACL tokens.
|
||||
|
||||
### GET Method
|
||||
|
||||
|
@ -1039,6 +1049,145 @@ It returns a JSON body like this:
|
|||
|
||||
This endpoint supports blocking queries and all consistency modes.
|
||||
|
||||
## ACL
|
||||
|
||||
The ACL endpoints are used to create, update, destroy and query ACL tokens.
|
||||
The following endpoints are supported:
|
||||
|
||||
* /v1/acl/create: Creates a new token with policy
|
||||
* /v1/acl/update: Update the policy of a token
|
||||
* /v1/acl/destroy/\<id\>: Destroys a given token
|
||||
* /v1/acl/info/\<id\>: Queries the policy of a given token
|
||||
* /v1/acl/clone/\<id\>: Creates a new token by cloning an existing token
|
||||
* /v1/acl/list: Lists all the active tokens
|
||||
|
||||
### /v1/acl/create
|
||||
|
||||
The create endpoint is used to make a new token. A token has a name,
|
||||
type, and a set of ACL rules. The name is opaque to Consul, and type
|
||||
is either "client" or "management". A management token is effectively
|
||||
like a root user, and has the ability to perform any action including
|
||||
creating, modifying, and deleting ACLs. A client token can only perform
|
||||
actions as permitted by the rules associated, and may never manage ACLs.
|
||||
This means the request to this endpoint must be made with a management
|
||||
token.
|
||||
|
||||
In any Consul cluster, only a single datacenter is authoritative for ACLs, so
|
||||
all requests are automatically routed to that datacenter regardless
|
||||
of the agent that the request is made to.
|
||||
|
||||
The create endpoint expects a JSON request body to be PUT. The request
|
||||
body must look like:
|
||||
|
||||
{
|
||||
"Name": "my-app-token",
|
||||
"Type": "client",
|
||||
"Rules": "",
|
||||
}
|
||||
|
||||
None of the fields are mandatory, and in fact no body needs to be PUT
|
||||
if the defaults are to be used. The `Name` and `Rules` default to being
|
||||
blank, and the `Type` defaults to "client". The format of `Rules` is
|
||||
[documented here](/docs/internals/acl.html).
|
||||
|
||||
The return code is 200 on success, along with a body like:
|
||||
|
||||
{"ID":"adf4238a-882b-9ddc-4a9d-5b6758e4159e"}
|
||||
|
||||
This is used to provide the ID of the newly created ACL token.
|
||||
|
||||
### /v1/acl/update
|
||||
|
||||
The update endpoint is used to modify the policy for a given
|
||||
ACL token. It is very similar to the create endpoint, however
|
||||
instead of generating a new token ID, the `ID` field must be
|
||||
provided. Requests to this endpoint must be made with a management
|
||||
token.
|
||||
|
||||
In any Consul cluster, only a single datacenter is authoritative for ACLs, so
|
||||
all requests are automatically routed to that datacenter regardless
|
||||
of the agent that the request is made to.
|
||||
|
||||
The update endpoint expects a JSON request body to be PUT. The request
|
||||
body must look like:
|
||||
|
||||
{
|
||||
"ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e"
|
||||
"Name": "my-app-token-updated",
|
||||
"Type": "client",
|
||||
"Rules": "# New Rules",
|
||||
}
|
||||
|
||||
Only the `ID` field is mandatory, the other fields provide defaults.
|
||||
The `Name` and `Rules` default to being blank, and the `Type` defaults to "client".
|
||||
The format of `Rules` is [documented here](/docs/internals/acl.html).
|
||||
|
||||
The return code is 200 on success.
|
||||
|
||||
### /v1/acl/destroy/\<id\>
|
||||
|
||||
The destroy endpoint is hit with a PUT and destroys the given ACL token.
|
||||
The request is automatically routed to the authoritative ACL datacenter.
|
||||
The token being destroyed must be provided after the slash, and requests
|
||||
to the endpoint must be made with a management token.
|
||||
|
||||
The return code is 200 on success.
|
||||
|
||||
### /v1/acl/info/\<id\>
|
||||
|
||||
This endpoint is hit with a GET and returns the token information
|
||||
by ID. All requests are routed to the authoritative ACL datacenter
|
||||
The token being queried must be provided after the slash.
|
||||
|
||||
It returns a JSON body like this:
|
||||
|
||||
[
|
||||
{
|
||||
"CreateIndex":3,
|
||||
"ModifyIndex":3,
|
||||
"ID":"8f246b77-f3e1-ff88-5b48-8ec93abf3e05",
|
||||
"Name":"Client Token",
|
||||
"Type":"client",
|
||||
"Rules":"..."
|
||||
}
|
||||
]
|
||||
|
||||
If the session is not found, null is returned instead of a JSON list.
|
||||
|
||||
### /v1/acl/clone/\<id\>
|
||||
|
||||
The clone endpoint is hit with a PUT and returns a token ID that
|
||||
is cloned from an existing token. This allows a token to serve
|
||||
as a template for others, making it simple to generate new tokens
|
||||
without complex rule management. The source token must be provided
|
||||
after the slash. Requests to this endpoint require a management token.
|
||||
|
||||
The return code is 200 on success, along with a body like:
|
||||
|
||||
{"ID":"adf4238a-882b-9ddc-4a9d-5b6758e4159e"}
|
||||
|
||||
This is used to provide the ID of the newly created ACL token.
|
||||
|
||||
### /v1/acl/list
|
||||
|
||||
The list endpoint is hit with a GET and lists all the active
|
||||
ACL tokens. This is a privileged endpoint, and requires a
|
||||
management token.
|
||||
|
||||
It returns a JSON body like this:
|
||||
|
||||
[
|
||||
{
|
||||
"CreateIndex":3,
|
||||
"ModifyIndex":3,
|
||||
"ID":"8f246b77-f3e1-ff88-5b48-8ec93abf3e05",
|
||||
"Name":"Client Token",
|
||||
"Type":"client",
|
||||
"Rules":"..."
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
## Status
|
||||
|
||||
The Status endpoints are used to get information about the status
|
||||
|
|
|
@ -284,6 +284,38 @@ definitions support being updated during a reload.
|
|||
will not make use of TLS for outgoing connections. This applies to clients and servers,
|
||||
as both will make outgoing connections.
|
||||
|
||||
* `acl_datacenter` - Only used by servers. This designates the datacenter which
|
||||
is authoritative for ACL information. It must be provided to enable ACLs.
|
||||
All servers and datacenters must agree on the ACL datacenter.
|
||||
|
||||
* `acl_token` - When provided, the agent will use this token when making requests
|
||||
to the Consul servers. Clients can override this token on a per-request basis
|
||||
by providing the ?token parameter. When not provided, the empty token is used
|
||||
which maps to the 'anonymous' ACL policy.
|
||||
|
||||
* `acl_master_token` - Only used for servers in the `acl_datacenter`. This token
|
||||
will be created if it does not exist with management level permissions. It allows
|
||||
operators to bootstrap the ACL system with a token ID that is well-known.
|
||||
|
||||
* `acl_default_policy` - Either "allow" or "deny", defaults to "allow". The
|
||||
default policy controls the behavior of a token when there is no matching
|
||||
rule. In "allow" mode, ACLs are a blacklist: any operation not specifically
|
||||
prohibited is allowed. In "deny" mode, ACLs are a whilelist: any operation not
|
||||
specifically allowed is blocked.
|
||||
|
||||
* `acl_down_policy` - Either "allow", "deny" or "extend-cache" which is the
|
||||
default. In the case that the policy for a token cannot be read from the
|
||||
`acl_datacenter` or leader node, the down policy is applied. In "allow" mode,
|
||||
all actions are permitted, "deny" restricts all operations, and "extend-cache"
|
||||
allows any cached ACLs to be used, ignoring their TTL values. If a non-cached
|
||||
ACL is used, "extend-cache" acts like "deny".
|
||||
|
||||
* `acl_ttl` - Used to control Time-To-Live caching of ACLs. By default this
|
||||
is 30 seconds. This setting has a major performance impact: reducing it will
|
||||
cause more frequent refreshes, while increasing it reduces the number of caches.
|
||||
However, because the caches are not actively invalidated, ACL policy may be stale
|
||||
up to the TTL value.
|
||||
|
||||
## Ports Used
|
||||
|
||||
Consul requires up to 5 different ports to work properly, some requiring
|
||||
|
|
112
website/source/docs/internals/acl.html.markdown
Normal file
112
website/source/docs/internals/acl.html.markdown
Normal file
|
@ -0,0 +1,112 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "ACL System"
|
||||
sidebar_current: "docs-internals-acl"
|
||||
---
|
||||
|
||||
# ACL System
|
||||
|
||||
Consul provides an optional Access Control List (ACL) system which can be used to control
|
||||
access to data and APIs. The ACL system is a
|
||||
[Capability-based system](http://en.wikipedia.org/wiki/Capability-based_security) that relies
|
||||
on tokens which can have fine grained rules applied to them. It is very similar to
|
||||
[AWS IAM](http://aws.amazon.com/iam/) in many ways.
|
||||
|
||||
## ACL Design
|
||||
|
||||
The ACL system is designed to be easy to use, fast to enforce, flexible to new
|
||||
policies, all while providing administrative insight. It has been modeled on
|
||||
the AWS IAM system, as well as the more general object-capability model. The system
|
||||
is modeled around "tokens".
|
||||
|
||||
Every token has an ID, name, type and rule set. The ID is a randomly generated
|
||||
UUID, making it unfeasible to guess. The name is opaque and human readable.
|
||||
Lastly the type is either "client" meaning it cannot modify ACL rules, and
|
||||
is restricted by the provided rules, or is "management" and is allowed to
|
||||
perform all actions.
|
||||
|
||||
The token ID is passed along with each RPC request to the servers. Agents
|
||||
[can be configured](/docs/agent/options.html) with `acl_token` to provide a default token,
|
||||
but the token can also be specified by a client on a [per-request basis](/docs/agent/http.html).
|
||||
ACLs are new as of Consul 0.4, meaning versions prior do not provide a token.
|
||||
This is handled by the special "anonymous" token. Anytime there is no token provided,
|
||||
the rules defined by that token are automatically applied. This lets policy be enforced
|
||||
on legacy clients.
|
||||
|
||||
Enforcement is always done by the server nodes. All servers must be [configured
|
||||
to provide](/docs/agent/options.html) an `acl_datacenter`, which enables
|
||||
ACL enforcement but also specified the authoritative datacenter. Consul does not
|
||||
replicate data cross-WAN, and instead relies on [RPC forwarding](/docs/internal/architecture.html)
|
||||
to support Multi-Datacenter configurations. However, because requests can be
|
||||
made across datacenter boundaries, ACL tokens must be valid globally. To avoid
|
||||
replication issues, a single datacenter is considered authoritative and stores
|
||||
all the tokens.
|
||||
|
||||
When a request is made to any non-authoritative server with a token, it must
|
||||
be resolved into the appropriate policy. This is done by reading the token
|
||||
from the authoritative server and caching a configurable `acl_ttl`. The implication
|
||||
of caching is that the cache TTL is an upper-bound on the staleness of policy
|
||||
that is enforced. It is possible to set a zero TTL, but this has adverse
|
||||
performance impacts, as every request requires refreshing the policy.
|
||||
|
||||
Another possible issue is an outage of the `acl_datacenter` or networking
|
||||
issues preventing access. In this case, it may be impossible for non-authoritative
|
||||
servers to resolve tokens. Consul provides a number of configurable `acl_down_policy`
|
||||
choices to tune behavior. It is possible to deny or permit all actions, or to ignore
|
||||
cache TTLs and enter a fail-safe mode.
|
||||
|
||||
ACLs can also act in either a whilelist or blacklist mode depending
|
||||
on the configuration of `acl_default_policy`. If the default policy is
|
||||
to deny all actions, then token rules can be set to allow or whitelist
|
||||
actions. In the inverse, the allow all default behavior is a blacklist,
|
||||
where rules are used to prohibit actions.
|
||||
|
||||
Bootstrapping the ACL system is done by providing an initial `acl_master_token`
|
||||
[configuration](/docs/agent/options.html), which will be created as a
|
||||
"management" type token if it does not exist.
|
||||
|
||||
## Rule Specification
|
||||
|
||||
A core part of the ACL system is a rule language which is used
|
||||
to describe the policy that must be enforced. We make use of
|
||||
the [HashiCorp Configuration Language (HCL)](https://github.com/hashicorp/hcl/)
|
||||
to specify policy. This language is human readable and interoperable
|
||||
with JSON making it easy to machine generate.
|
||||
|
||||
As of Consul 0.4, it is only possible to specify policies for the
|
||||
KV store. Specification in the HCL format looks like:
|
||||
|
||||
# Default all keys to read-only
|
||||
key "" {
|
||||
policy = "read"
|
||||
}
|
||||
key "foo/" {
|
||||
policy = "write"
|
||||
}
|
||||
key "foo/private/" {
|
||||
# Deny access to the private dir
|
||||
policy = "deny"
|
||||
}
|
||||
|
||||
This is equivalent to the following JSON input:
|
||||
|
||||
{
|
||||
"key": {
|
||||
"": {
|
||||
"policy": "read",
|
||||
},
|
||||
"foo/": {
|
||||
"policy": "write",
|
||||
},
|
||||
"foo/private": {
|
||||
"policy": "deny",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Key policies provide both a prefix and a policy. The rules are enforced
|
||||
using a longest-prefix match policy. This means we pick the most specific
|
||||
policy possible. The policy is either "read", "write" or "deny". A "write"
|
||||
policy implies "read", and there is no way to specify write-only. If there
|
||||
is no applicable rule, the `acl_default_policy` is applied.
|
||||
|
|
@ -38,6 +38,10 @@
|
|||
<a href="/docs/internals/sessions.html">Sessions</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-internals-acl") %>>
|
||||
<a href="/docs/internals/acl.html">ACLs</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-internals-security") %>>
|
||||
<a href="/docs/internals/security.html">Security Model</a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in a new issue