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 {
|
if a.config.Protocol > 0 {
|
||||||
base.ProtocolVersion = uint8(a.config.Protocol)
|
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
|
// Format the build string
|
||||||
revision := a.config.Revision
|
revision := a.config.Revision
|
||||||
|
|
|
@ -30,6 +30,7 @@ func nextConfig() *Config {
|
||||||
conf.Ports.SerfWan = 18300 + idx
|
conf.Ports.SerfWan = 18300 + idx
|
||||||
conf.Ports.Server = 18100 + idx
|
conf.Ports.Server = 18100 + idx
|
||||||
conf.Server = true
|
conf.Server = true
|
||||||
|
conf.ACLDatacenter = "dc1"
|
||||||
|
|
||||||
cons := consul.DefaultConfig()
|
cons := consul.DefaultConfig()
|
||||||
conf.ConsulConfig = cons
|
conf.ConsulConfig = cons
|
||||||
|
|
|
@ -194,6 +194,41 @@ type Config struct {
|
||||||
CheckUpdateInterval time.Duration `mapstructure:"-"`
|
CheckUpdateInterval time.Duration `mapstructure:"-"`
|
||||||
CheckUpdateIntervalRaw string `mapstructure:"check_update_interval" json:"-"`
|
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
|
// AEInterval controls the anti-entropy interval. This is how often
|
||||||
// the agent attempts to reconcile it's local state with the server'
|
// the agent attempts to reconcile it's local state with the server'
|
||||||
// representation of our state. Defaults to every 60s.
|
// representation of our state. Defaults to every 60s.
|
||||||
|
@ -246,6 +281,9 @@ func DefaultConfig() *Config {
|
||||||
Protocol: consul.ProtocolVersionMax,
|
Protocol: consul.ProtocolVersionMax,
|
||||||
CheckUpdateInterval: 5 * time.Minute,
|
CheckUpdateInterval: 5 * time.Minute,
|
||||||
AEInterval: 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
|
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
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -583,6 +629,25 @@ func MergeConfig(a, b *Config) *Config {
|
||||||
if b.SyslogFacility != "" {
|
if b.SyslogFacility != "" {
|
||||||
result.SyslogFacility = 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
|
// Copy the start join addresses
|
||||||
result.StartJoin = make([]string, 0, len(a.StartJoin)+len(b.StartJoin))
|
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 {
|
if config.CheckUpdateInterval != 10*time.Minute {
|
||||||
t.Fatalf("bad: %#v", config)
|
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) {
|
func TestDecodeConfig_Service(t *testing.T) {
|
||||||
|
@ -503,6 +531,13 @@ func TestMergeConfig(t *testing.T) {
|
||||||
RejoinAfterLeave: true,
|
RejoinAfterLeave: true,
|
||||||
CheckUpdateInterval: 8 * time.Minute,
|
CheckUpdateInterval: 8 * time.Minute,
|
||||||
CheckUpdateIntervalRaw: "8m",
|
CheckUpdateIntervalRaw: "8m",
|
||||||
|
ACLToken: "1234",
|
||||||
|
ACLMasterToken: "2345",
|
||||||
|
ACLDatacenter: "dc2",
|
||||||
|
ACLTTL: 15 * time.Second,
|
||||||
|
ACLTTLRaw: "15s",
|
||||||
|
ACLDownPolicy: "deny",
|
||||||
|
ACLDefaultPolicy: "deny",
|
||||||
}
|
}
|
||||||
|
|
||||||
c := MergeConfig(a, b)
|
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/node/", s.wrap(s.SessionsForNode))
|
||||||
s.mux.HandleFunc("/v1/session/list", s.wrap(s.SessionList))
|
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 {
|
if enableDebug {
|
||||||
s.mux.HandleFunc("/debug/pprof/", pprof.Index)
|
s.mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||||
s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
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
|
// parse is a convenience method for endpoints that need
|
||||||
// to use both parseWait and parseDC.
|
// to use both parseWait and parseDC.
|
||||||
func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, dc *string, b *structs.QueryOptions) bool {
|
func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, dc *string, b *structs.QueryOptions) bool {
|
||||||
s.parseDC(req, dc)
|
s.parseDC(req, dc)
|
||||||
|
s.parseToken(req, &b.Token)
|
||||||
if parseConsistency(resp, req, b) {
|
if parseConsistency(resp, req, b) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,6 +145,7 @@ func (s *HTTPServer) KVSPut(resp http.ResponseWriter, req *http.Request, args *s
|
||||||
Value: nil,
|
Value: nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
applyReq.Token = args.Token
|
||||||
|
|
||||||
// Check for flags
|
// Check for flags
|
||||||
params := req.URL.Query()
|
params := req.URL.Query()
|
||||||
|
@ -215,6 +216,7 @@ func (s *HTTPServer) KVSDelete(resp http.ResponseWriter, req *http.Request, args
|
||||||
Key: args.Key,
|
Key: args.Key,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
applyReq.Token = args.Token
|
||||||
|
|
||||||
// Check for recurse
|
// Check for recurse
|
||||||
params := req.URL.Query()
|
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")
|
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
|
// Ensure we have a log output
|
||||||
if config.LogOutput == nil {
|
if config.LogOutput == nil {
|
||||||
config.LogOutput = os.Stderr
|
config.LogOutput = os.Stderr
|
||||||
|
|
|
@ -128,6 +128,38 @@ type Config struct {
|
||||||
// operators track which versions are actively deployed
|
// operators track which versions are actively deployed
|
||||||
Build string
|
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
|
// ServerUp callback can be used to trigger a notification that
|
||||||
// a Consul server is now up and known about.
|
// a Consul server is now up and known about.
|
||||||
ServerUp func()
|
ServerUp func()
|
||||||
|
@ -145,6 +177,24 @@ func (c *Config) CheckVersion() error {
|
||||||
return nil
|
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
|
// AppendCA opens and parses the CA file and adds the certificates to
|
||||||
// the provided CertPool.
|
// the provided CertPool.
|
||||||
func (c *Config) AppendCA(pool *x509.CertPool) error {
|
func (c *Config) AppendCA(pool *x509.CertPool) error {
|
||||||
|
@ -324,6 +374,9 @@ func DefaultConfig() *Config {
|
||||||
SerfWANConfig: serf.DefaultConfig(),
|
SerfWANConfig: serf.DefaultConfig(),
|
||||||
ReconcileInterval: 60 * time.Second,
|
ReconcileInterval: 60 * time.Second,
|
||||||
ProtocolVersion: ProtocolVersionMax,
|
ProtocolVersion: ProtocolVersionMax,
|
||||||
|
ACLTTL: 30 * time.Second,
|
||||||
|
ACLDefaultPolicy: "allow",
|
||||||
|
ACLDownPolicy: "extend-cache",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increase our reap interval to 3 days instead of 24h.
|
// 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)
|
return c.applyKVSOperation(buf[1:], log.Index)
|
||||||
case structs.SessionRequestType:
|
case structs.SessionRequestType:
|
||||||
return c.applySessionOperation(buf[1:], log.Index)
|
return c.applySessionOperation(buf[1:], log.Index)
|
||||||
|
case structs.ACLRequestType:
|
||||||
|
return c.applyACLOperation(buf[1:], log.Index)
|
||||||
default:
|
default:
|
||||||
panic(fmt.Errorf("failed to apply request: %#v", buf))
|
panic(fmt.Errorf("failed to apply request: %#v", buf))
|
||||||
}
|
}
|
||||||
|
@ -196,6 +198,33 @@ func (c *consulFSM) applySessionOperation(buf []byte, index uint64) interface{}
|
||||||
return nil
|
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) {
|
func (c *consulFSM) Snapshot() (raft.FSMSnapshot, error) {
|
||||||
defer func(start time.Time) {
|
defer func(start time.Time) {
|
||||||
c.logger.Printf("[INFO] consul.fsm: snapshot created in %v", time.Now().Sub(start))
|
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
|
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:
|
default:
|
||||||
return fmt.Errorf("Unrecognized msg type: %v", msgType)
|
return fmt.Errorf("Unrecognized msg type: %v", msgType)
|
||||||
}
|
}
|
||||||
|
@ -298,6 +336,11 @@ func (s *consulSnapshot) Persist(sink raft.SnapshotSink) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.persistACLs(sink, encoder); err != nil {
|
||||||
|
sink.Cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.persistKV(sink, encoder); err != nil {
|
if err := s.persistKV(sink, encoder); err != nil {
|
||||||
sink.Cancel()
|
sink.Cancel()
|
||||||
return err
|
return err
|
||||||
|
@ -364,6 +407,22 @@ func (s *consulSnapshot) persistSessions(sink raft.SnapshotSink,
|
||||||
return nil
|
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,
|
func (s *consulSnapshot) persistKV(sink raft.SnapshotSink,
|
||||||
encoder *codec.Encoder) error {
|
encoder *codec.Encoder) error {
|
||||||
streamCh := make(chan interface{}, 256)
|
streamCh := make(chan interface{}, 256)
|
||||||
|
|
|
@ -328,6 +328,8 @@ func TestFSM_SnapshotRestore(t *testing.T) {
|
||||||
})
|
})
|
||||||
session := &structs.Session{Node: "foo"}
|
session := &structs.Session{Node: "foo"}
|
||||||
fsm.state.SessionCreate(9, session)
|
fsm.state.SessionCreate(9, session)
|
||||||
|
acl := &structs.ACL{Name: "User Token"}
|
||||||
|
fsm.state.ACLSet(10, acl, false)
|
||||||
|
|
||||||
// Snapshot
|
// Snapshot
|
||||||
snap, err := fsm.Snapshot()
|
snap, err := fsm.Snapshot()
|
||||||
|
@ -392,7 +394,16 @@ func TestFSM_SnapshotRestore(t *testing.T) {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
if s.Node != "foo" {
|
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)
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/armon/go-metrics"
|
"github.com/armon/go-metrics"
|
||||||
"github.com/hashicorp/consul/consul/structs"
|
"github.com/hashicorp/consul/consul/structs"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// KVS endpoint is used to manipulate the Key-Value store
|
// 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")
|
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
|
// 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
|
// 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
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
acl, err := k.srv.resolveToken(args.Token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Get the local state
|
// Get the local state
|
||||||
state := k.srv.fsm.State()
|
state := k.srv.fsm.State()
|
||||||
return k.srv.blockingRPC(&args.QueryOptions,
|
return k.srv.blockingRPC(&args.QueryOptions,
|
||||||
|
@ -75,6 +98,9 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if acl != nil && !acl.KeyRead(args.Key) {
|
||||||
|
ent = nil
|
||||||
|
}
|
||||||
if ent == nil {
|
if ent == nil {
|
||||||
// Must provide non-zero index to prevent blocking
|
// Must provide non-zero index to prevent blocking
|
||||||
// Index 1 is impossible anyways (due to Raft internals)
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
acl, err := k.srv.resolveToken(args.Token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Get the local state
|
// Get the local state
|
||||||
state := k.srv.fsm.State()
|
state := k.srv.fsm.State()
|
||||||
return k.srv.blockingRPC(&args.QueryOptions,
|
return k.srv.blockingRPC(&args.QueryOptions,
|
||||||
|
@ -108,6 +139,9 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if acl != nil {
|
||||||
|
ent = FilterDirEnt(acl, ent)
|
||||||
|
}
|
||||||
if len(ent) == 0 {
|
if len(ent) == 0 {
|
||||||
// Must provide non-zero index to prevent blocking
|
// Must provide non-zero index to prevent blocking
|
||||||
// Index 1 is impossible anyways (due to Raft internals)
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
acl, err := k.srv.resolveToken(args.Token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Get the local state
|
// Get the local state
|
||||||
state := k.srv.fsm.State()
|
state := k.srv.fsm.State()
|
||||||
return k.srv.blockingRPC(&args.QueryOptions,
|
return k.srv.blockingRPC(&args.QueryOptions,
|
||||||
&reply.QueryMeta,
|
&reply.QueryMeta,
|
||||||
state.QueryTables("KVSListKeys"),
|
state.QueryTables("KVSListKeys"),
|
||||||
func() error {
|
func() error {
|
||||||
var err error
|
index, keys, err := state.KVSListKeys(args.Prefix, args.Seperator)
|
||||||
reply.Index, reply.Keys, err = state.KVSListKeys(args.Prefix, args.Seperator)
|
reply.Index = index
|
||||||
|
if acl != nil {
|
||||||
|
keys = FilterKeys(acl, keys)
|
||||||
|
}
|
||||||
|
reply.Keys = keys
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package consul
|
package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/hashicorp/consul/consul/structs"
|
|
||||||
"github.com/hashicorp/consul/testutil"
|
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/consul/structs"
|
||||||
|
"github.com/hashicorp/consul/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestKVS_Apply(t *testing.T) {
|
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) {
|
func TestKVS_Get(t *testing.T) {
|
||||||
dir1, s1 := testServer(t)
|
dir1, s1 := testServer(t)
|
||||||
defer os.RemoveAll(dir1)
|
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) {
|
func TestKVSEndpoint_List(t *testing.T) {
|
||||||
dir1, s1 := testServer(t)
|
dir1, s1 := testServer(t)
|
||||||
defer os.RemoveAll(dir1)
|
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) {
|
func TestKVSEndpoint_ListKeys(t *testing.T) {
|
||||||
dir1, s1 := testServer(t)
|
dir1, s1 := testServer(t)
|
||||||
defer os.RemoveAll(dir1)
|
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) {
|
func TestKVS_Apply_LockDelay(t *testing.T) {
|
||||||
dir1, s1 := testServer(t)
|
dir1, s1 := testServer(t)
|
||||||
defer os.RemoveAll(dir1)
|
defer os.RemoveAll(dir1)
|
||||||
|
@ -294,3 +565,18 @@ func TestKVS_Apply_LockDelay(t *testing.T) {
|
||||||
t.Fatalf("should acquire")
|
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
|
package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"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)
|
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
|
// Reconcile channel is only used once initial reconcile
|
||||||
// has succeeded
|
// has succeeded
|
||||||
var reconcileCh chan serf.Member
|
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
|
// reconcile is used to reconcile the differences between Serf
|
||||||
// membership and what is reflected in our strongly consistent store.
|
// membership and what is reflected in our strongly consistent store.
|
||||||
// Mainly we need to ensure all live nodes are registered, all failed
|
// Mainly we need to ensure all live nodes are registered, all failed
|
||||||
|
|
|
@ -15,6 +15,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
|
"github.com/hashicorp/golang-lru"
|
||||||
"github.com/hashicorp/raft"
|
"github.com/hashicorp/raft"
|
||||||
"github.com/hashicorp/raft-mdb"
|
"github.com/hashicorp/raft-mdb"
|
||||||
"github.com/hashicorp/serf/serf"
|
"github.com/hashicorp/serf/serf"
|
||||||
|
@ -43,11 +45,24 @@ const (
|
||||||
// serverMaxStreams controsl how many idle streams we keep
|
// serverMaxStreams controsl how many idle streams we keep
|
||||||
// open to a server
|
// open to a server
|
||||||
serverMaxStreams = 64
|
serverMaxStreams = 64
|
||||||
|
|
||||||
|
// Maximum number of cached ACL entries
|
||||||
|
aclCacheSize = 256
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server is Consul server which manages the service discovery,
|
// Server is Consul server which manages the service discovery,
|
||||||
// health checking, DC forwarding, Raft, and multiple Serf pools.
|
// health checking, DC forwarding, Raft, and multiple Serf pools.
|
||||||
type Server struct {
|
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
|
config *Config
|
||||||
|
|
||||||
// Connection pool to other consul servers
|
// Connection pool to other consul servers
|
||||||
|
@ -125,6 +140,7 @@ type endpoints struct {
|
||||||
KVS *KVS
|
KVS *KVS
|
||||||
Session *Session
|
Session *Session
|
||||||
Internal *Internal
|
Internal *Internal
|
||||||
|
ACL *ACL
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer is used to construct a new Consul server from the
|
// 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")
|
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
|
// Ensure we have a log output
|
||||||
if config.LogOutput == nil {
|
if config.LogOutput == nil {
|
||||||
config.LogOutput = os.Stderr
|
config.LogOutput = os.Stderr
|
||||||
|
@ -175,6 +196,27 @@ func NewServer(config *Config) (*Server, error) {
|
||||||
shutdownCh: make(chan struct{}),
|
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
|
// Initialize the RPC layer
|
||||||
if err := s.setupRPC(tlsConfig); err != nil {
|
if err := s.setupRPC(tlsConfig); err != nil {
|
||||||
s.Shutdown()
|
s.Shutdown()
|
||||||
|
@ -336,6 +378,7 @@ func (s *Server) setupRPC(tlsConfig *tls.Config) error {
|
||||||
s.endpoints.KVS = &KVS{s}
|
s.endpoints.KVS = &KVS{s}
|
||||||
s.endpoints.Session = &Session{s}
|
s.endpoints.Session = &Session{s}
|
||||||
s.endpoints.Internal = &Internal{s}
|
s.endpoints.Internal = &Internal{s}
|
||||||
|
s.endpoints.ACL = &ACL{s}
|
||||||
|
|
||||||
// Register the handlers
|
// Register the handlers
|
||||||
s.rpcServer.Register(s.endpoints.Status)
|
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.KVS)
|
||||||
s.rpcServer.Register(s.endpoints.Session)
|
s.rpcServer.Register(s.endpoints.Session)
|
||||||
s.rpcServer.Register(s.endpoints.Internal)
|
s.rpcServer.Register(s.endpoints.Internal)
|
||||||
|
s.rpcServer.Register(s.endpoints.ACL)
|
||||||
|
|
||||||
list, err := net.ListenTCP("tcp", s.config.RPCAddr)
|
list, err := net.ListenTCP("tcp", s.config.RPCAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -101,6 +101,17 @@ func testServerDCExpect(t *testing.T, dc string, expect int) (string, *Server) {
|
||||||
return dir, 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) {
|
func TestServer_StartStop(t *testing.T) {
|
||||||
dir := tmpDir(t)
|
dir := tmpDir(t)
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
|
@ -21,6 +21,7 @@ const (
|
||||||
dbKVS = "kvs"
|
dbKVS = "kvs"
|
||||||
dbSessions = "sessions"
|
dbSessions = "sessions"
|
||||||
dbSessionChecks = "sessionChecks"
|
dbSessionChecks = "sessionChecks"
|
||||||
|
dbACLs = "acls"
|
||||||
dbMaxMapSize32bit uint64 = 512 * 1024 * 1024 // 512MB maximum size
|
dbMaxMapSize32bit uint64 = 512 * 1024 * 1024 // 512MB maximum size
|
||||||
dbMaxMapSize64bit uint64 = 32 * 1024 * 1024 * 1024 // 32GB maximum size
|
dbMaxMapSize64bit uint64 = 32 * 1024 * 1024 * 1024 // 32GB maximum size
|
||||||
)
|
)
|
||||||
|
@ -53,6 +54,7 @@ type StateStore struct {
|
||||||
kvsTable *MDBTable
|
kvsTable *MDBTable
|
||||||
sessionTable *MDBTable
|
sessionTable *MDBTable
|
||||||
sessionCheckTable *MDBTable
|
sessionCheckTable *MDBTable
|
||||||
|
aclTable *MDBTable
|
||||||
tables MDBTables
|
tables MDBTables
|
||||||
watch map[*MDBTable]*NotifyGroup
|
watch map[*MDBTable]*NotifyGroup
|
||||||
queryTables map[string]MDBTables
|
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
|
// Store the set of tables
|
||||||
s.tables = []*MDBTable{s.nodeTable, s.serviceTable, s.checkTable,
|
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 {
|
for _, table := range s.tables {
|
||||||
table.Env = s.env
|
table.Env = s.env
|
||||||
table.Encoder = encoder
|
table.Encoder = encoder
|
||||||
|
@ -338,6 +357,8 @@ func (s *StateStore) initialize() error {
|
||||||
"SessionGet": MDBTables{s.sessionTable},
|
"SessionGet": MDBTables{s.sessionTable},
|
||||||
"SessionList": MDBTables{s.sessionTable},
|
"SessionList": MDBTables{s.sessionTable},
|
||||||
"NodeSessions": MDBTables{s.sessionTable},
|
"NodeSessions": MDBTables{s.sessionTable},
|
||||||
|
"ACLGet": MDBTables{s.aclTable},
|
||||||
|
"ACLList": MDBTables{s.aclTable},
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1249,8 +1270,8 @@ func (s *StateStore) SessionCreate(index uint64, session *structs.Session) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new session ID, verify uniqueness
|
// Generate a new session ID, verify uniqueness
|
||||||
session.ID = generateUUID()
|
|
||||||
for {
|
for {
|
||||||
|
session.ID = generateUUID()
|
||||||
res, err = s.sessionTable.GetTxn(tx, "id", session.ID)
|
res, err = s.sessionTable.GetTxn(tx, "id", session.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1346,7 +1367,7 @@ func (s *StateStore) NodeSessions(node string) (uint64, []*structs.Session, erro
|
||||||
return idx, out, err
|
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 {
|
func (s *StateStore) SessionDestroy(index uint64, id string) error {
|
||||||
tx, err := s.tables.StartTxn(false)
|
tx, err := s.tables.StartTxn(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1482,6 +1503,124 @@ func (s *StateStore) invalidateLocks(index uint64, tx *MDBTxn,
|
||||||
return nil
|
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
|
// Snapshot is used to create a point in time snapshot
|
||||||
func (s *StateStore) Snapshot() (*StateSnapshot, error) {
|
func (s *StateStore) Snapshot() (*StateSnapshot, error) {
|
||||||
// Begin a new txn on all tables
|
// Begin a new txn on all tables
|
||||||
|
@ -1555,3 +1694,13 @@ func (s *StateSnapshot) SessionList() ([]*structs.Session, error) {
|
||||||
}
|
}
|
||||||
return out, err
|
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)
|
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
|
// Take a snapshot
|
||||||
snap, err := store.Snapshot()
|
snap, err := store.Snapshot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -660,7 +676,7 @@ func TestStoreSnapshot(t *testing.T) {
|
||||||
defer snap.Close()
|
defer snap.Close()
|
||||||
|
|
||||||
// Check the last nodes
|
// Check the last nodes
|
||||||
if idx := snap.LastIndex(); idx != 18 {
|
if idx := snap.LastIndex(); idx != 20 {
|
||||||
t.Fatalf("bad: %v", idx)
|
t.Fatalf("bad: %v", idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -724,14 +740,23 @@ func TestStoreSnapshot(t *testing.T) {
|
||||||
t.Fatalf("missing sessions")
|
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!
|
// 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)
|
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)
|
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)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
checkAfter := &structs.HealthCheck{
|
checkAfter := &structs.HealthCheck{
|
||||||
|
@ -741,11 +766,16 @@ func TestStoreSnapshot(t *testing.T) {
|
||||||
Status: structs.HealthCritical,
|
Status: structs.HealthCritical,
|
||||||
ServiceID: "db",
|
ServiceID: "db",
|
||||||
}
|
}
|
||||||
if err := store.EnsureCheck(22, checkAfter); err != nil {
|
if err := store.EnsureCheck(24, checkAfter); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
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)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -807,6 +837,15 @@ func TestStoreSnapshot(t *testing.T) {
|
||||||
if len(sessions) != 2 {
|
if len(sessions) != 2 {
|
||||||
t.Fatalf("missing sessions")
|
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) {
|
func TestEnsureCheck(t *testing.T) {
|
||||||
|
@ -2117,3 +2156,144 @@ func TestSessionInvalidate_KeyUnlock(t *testing.T) {
|
||||||
t.Fatalf("Bad: %v", expires)
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ugorji/go/codec"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
|
"github.com/ugorji/go/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -20,6 +22,7 @@ const (
|
||||||
DeregisterRequestType
|
DeregisterRequestType
|
||||||
KVSRequestType
|
KVSRequestType
|
||||||
SessionRequestType
|
SessionRequestType
|
||||||
|
ACLRequestType
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -32,6 +35,15 @@ const (
|
||||||
HealthCritical = "critical"
|
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 (
|
const (
|
||||||
// MaxLockDelay provides a maximum LockDelay value for
|
// MaxLockDelay provides a maximum LockDelay value for
|
||||||
// a session. Any value above this will not be respected.
|
// a session. Any value above this will not be respected.
|
||||||
|
@ -43,10 +55,15 @@ type RPCInfo interface {
|
||||||
RequestDatacenter() string
|
RequestDatacenter() string
|
||||||
IsRead() bool
|
IsRead() bool
|
||||||
AllowStaleRead() bool
|
AllowStaleRead() bool
|
||||||
|
ACLToken() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryOptions is used to specify various flags for read queries
|
// QueryOptions is used to specify various flags for read queries
|
||||||
type QueryOptions struct {
|
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
|
// If set, wait until query exceeds given index. Must be provided
|
||||||
// with MaxQueryTime.
|
// with MaxQueryTime.
|
||||||
MinQueryIndex uint64
|
MinQueryIndex uint64
|
||||||
|
@ -72,7 +89,15 @@ func (q QueryOptions) AllowStaleRead() bool {
|
||||||
return q.AllowStale
|
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
|
// WriteRequest only applies to writes, always false
|
||||||
func (w WriteRequest) IsRead() bool {
|
func (w WriteRequest) IsRead() bool {
|
||||||
|
@ -83,6 +108,10 @@ func (w WriteRequest) AllowStaleRead() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w WriteRequest) ACLToken() string {
|
||||||
|
return w.Token
|
||||||
|
}
|
||||||
|
|
||||||
// QueryMeta allows a query response to include potentially
|
// QueryMeta allows a query response to include potentially
|
||||||
// useful metadata about a query
|
// useful metadata about a query
|
||||||
type QueryMeta struct {
|
type QueryMeta struct {
|
||||||
|
@ -396,6 +425,74 @@ type IndexedSessions struct {
|
||||||
QueryMeta
|
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
|
// msgpackHandle is a shared handle for encoding/decoding of structs
|
||||||
var msgpackHandle = &codec.MsgpackHandle{}
|
var msgpackHandle = &codec.MsgpackHandle{}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ All endpoints fall into one of several categories:
|
||||||
* catalog - Manages nodes and services
|
* catalog - Manages nodes and services
|
||||||
* health - Manages health checks
|
* health - Manages health checks
|
||||||
* session - Session manipulation
|
* session - Session manipulation
|
||||||
|
* acl - ACL creations and management
|
||||||
* status - Consul system status
|
* status - Consul system status
|
||||||
* internal - Internal APIs. Purposely undocumented, subject to change.
|
* 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,
|
whitespace removed. By adding "?pretty" to the HTTP request URL,
|
||||||
formatted JSON will be returned.
|
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
|
## KV
|
||||||
|
|
||||||
The KV endpoint is used to expose a simple key/value store. This can be used
|
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.
|
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
|
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
|
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
|
### GET Method
|
||||||
|
|
||||||
|
@ -1039,6 +1049,145 @@ It returns a JSON body like this:
|
||||||
|
|
||||||
This endpoint supports blocking queries and all consistency modes.
|
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
|
## Status
|
||||||
|
|
||||||
The Status endpoints are used to get information about the 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,
|
will not make use of TLS for outgoing connections. This applies to clients and servers,
|
||||||
as both will make outgoing connections.
|
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
|
## Ports Used
|
||||||
|
|
||||||
Consul requires up to 5 different ports to work properly, some requiring
|
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>
|
<a href="/docs/internals/sessions.html">Sessions</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-internals-acl") %>>
|
||||||
|
<a href="/docs/internals/acl.html">ACLs</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-internals-security") %>>
|
<li<%= sidebar_current("docs-internals-security") %>>
|
||||||
<a href="/docs/internals/security.html">Security Model</a>
|
<a href="/docs/internals/security.html">Security Model</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in a new issue