Merge pull request #291 from hashicorp/f-acl

Adding support for ACL system
This commit is contained in:
Armon Dadgar 2014-08-18 15:47:23 -07:00
commit 00611a7e61
36 changed files with 4526 additions and 18 deletions

216
acl/acl.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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)
}
}

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

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

View file

@ -181,6 +181,24 @@ func (a *Agent) consulConfig() *consul.Config {
if a.config.Protocol > 0 {
base.ProtocolVersion = uint8(a.config.Protocol)
}
if a.config.ACLToken != "" {
base.ACLToken = a.config.ACLToken
}
if a.config.ACLMasterToken != "" {
base.ACLMasterToken = a.config.ACLMasterToken
}
if a.config.ACLDatacenter != "" {
base.ACLDatacenter = a.config.ACLDatacenter
}
if a.config.ACLTTLRaw != "" {
base.ACLTTL = a.config.ACLTTL
}
if a.config.ACLDefaultPolicy != "" {
base.ACLDefaultPolicy = a.config.ACLDefaultPolicy
}
if a.config.ACLDownPolicy != "" {
base.ACLDownPolicy = a.config.ACLDownPolicy
}
// Format the build string
revision := a.config.Revision

View file

@ -30,6 +30,7 @@ func nextConfig() *Config {
conf.Ports.SerfWan = 18300 + idx
conf.Ports.Server = 18100 + idx
conf.Server = true
conf.ACLDatacenter = "dc1"
cons := consul.DefaultConfig()
conf.ConsulConfig = cons

View file

@ -194,6 +194,41 @@ type Config struct {
CheckUpdateInterval time.Duration `mapstructure:"-"`
CheckUpdateIntervalRaw string `mapstructure:"check_update_interval" json:"-"`
// ACLToken is the default token used to make requests if a per-request
// token is not provided. If not configured the 'anonymous' token is used.
ACLToken string `mapstructure:"acl_token" json:"-"`
// ACLMasterToken is used to bootstrap the ACL system. It should be specified
// on the servers in the ACLDatacenter. When the leader comes online, it ensures
// that the Master token is available. This provides the initial token.
ACLMasterToken string `mapstructure:"acl_master_token" json:"-"`
// ACLDatacenter is the central datacenter that holds authoritative
// ACL records. This must be the same for the entire cluster.
// If this is not set, ACLs are not enabled. Off by default.
ACLDatacenter string `mapstructure:"acl_datacenter"`
// ACLTTL is used to control the time-to-live of cached ACLs . This has
// a major impact on performance. By default, it is set to 30 seconds.
ACLTTL time.Duration `mapstructure:"-"`
ACLTTLRaw string `mapstructure:"acl_ttl"`
// ACLDefaultPolicy is used to control the ACL interaction when
// there is no defined policy. This can be "allow" which means
// ACLs are used to black-list, or "deny" which means ACLs are
// white-lists.
ACLDefaultPolicy string `mapstructure:"acl_default_policy"`
// ACLDownPolicy is used to control the ACL interaction when we cannot
// reach the ACLDatacenter and the token is not in the cache.
// There are two modes:
// * deny - Deny all requests
// * extend-cache - Ignore the cache expiration, and allow cached
// ACL's to be used to service requests. This
// is the default. If the ACL is not in the cache,
// this acts like deny.
ACLDownPolicy string `mapstructure:"acl_down_policy"`
// AEInterval controls the anti-entropy interval. This is how often
// the agent attempts to reconcile it's local state with the server'
// representation of our state. Defaults to every 60s.
@ -246,6 +281,9 @@ func DefaultConfig() *Config {
Protocol: consul.ProtocolVersionMax,
CheckUpdateInterval: 5 * time.Minute,
AEInterval: time.Minute,
ACLTTL: 30 * time.Second,
ACLDownPolicy: "extend-cache",
ACLDefaultPolicy: "allow",
}
}
@ -341,6 +379,14 @@ func DecodeConfig(r io.Reader) (*Config, error) {
result.CheckUpdateInterval = dur
}
if raw := result.ACLTTLRaw; raw != "" {
dur, err := time.ParseDuration(raw)
if err != nil {
return nil, fmt.Errorf("ACL TTL invalid: %v", err)
}
result.ACLTTL = dur
}
return &result, nil
}
@ -583,6 +629,25 @@ func MergeConfig(a, b *Config) *Config {
if b.SyslogFacility != "" {
result.SyslogFacility = b.SyslogFacility
}
if b.ACLToken != "" {
result.ACLToken = b.ACLToken
}
if b.ACLMasterToken != "" {
result.ACLMasterToken = b.ACLMasterToken
}
if b.ACLDatacenter != "" {
result.ACLDatacenter = b.ACLDatacenter
}
if b.ACLTTLRaw != "" {
result.ACLTTL = b.ACLTTL
result.ACLTTLRaw = b.ACLTTLRaw
}
if b.ACLDownPolicy != "" {
result.ACLDownPolicy = b.ACLDownPolicy
}
if b.ACLDefaultPolicy != "" {
result.ACLDefaultPolicy = b.ACLDefaultPolicy
}
// Copy the start join addresses
result.StartJoin = make([]string, 0, len(a.StartJoin)+len(b.StartJoin))

View file

@ -356,6 +356,34 @@ func TestDecodeConfig(t *testing.T) {
if config.CheckUpdateInterval != 10*time.Minute {
t.Fatalf("bad: %#v", config)
}
// ACLs
input = `{"acl_token": "1234", "acl_datacenter": "dc2",
"acl_ttl": "60s", "acl_down_policy": "deny",
"acl_default_policy": "deny", "acl_master_token": "2345"}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}
if config.ACLToken != "1234" {
t.Fatalf("bad: %#v", config)
}
if config.ACLMasterToken != "2345" {
t.Fatalf("bad: %#v", config)
}
if config.ACLDatacenter != "dc2" {
t.Fatalf("bad: %#v", config)
}
if config.ACLTTL != 60*time.Second {
t.Fatalf("bad: %#v", config)
}
if config.ACLDownPolicy != "deny" {
t.Fatalf("bad: %#v", config)
}
if config.ACLDefaultPolicy != "deny" {
t.Fatalf("bad: %#v", config)
}
}
func TestDecodeConfig_Service(t *testing.T) {
@ -503,6 +531,13 @@ func TestMergeConfig(t *testing.T) {
RejoinAfterLeave: true,
CheckUpdateInterval: 8 * time.Minute,
CheckUpdateIntervalRaw: "8m",
ACLToken: "1234",
ACLMasterToken: "2345",
ACLDatacenter: "dc2",
ACLTTL: 15 * time.Second,
ACLTTLRaw: "15s",
ACLDownPolicy: "deny",
ACLDefaultPolicy: "deny",
}
c := MergeConfig(a, b)

View file

@ -99,6 +99,22 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.mux.HandleFunc("/v1/session/node/", s.wrap(s.SessionsForNode))
s.mux.HandleFunc("/v1/session/list", s.wrap(s.SessionList))
if s.agent.config.ACLDatacenter != "" {
s.mux.HandleFunc("/v1/acl/create", s.wrap(s.ACLCreate))
s.mux.HandleFunc("/v1/acl/update", s.wrap(s.ACLUpdate))
s.mux.HandleFunc("/v1/acl/destroy/", s.wrap(s.ACLDestroy))
s.mux.HandleFunc("/v1/acl/info/", s.wrap(s.ACLGet))
s.mux.HandleFunc("/v1/acl/clone/", s.wrap(s.ACLClone))
s.mux.HandleFunc("/v1/acl/list", s.wrap(s.ACLList))
} else {
s.mux.HandleFunc("/v1/acl/create", s.wrap(aclDisabled))
s.mux.HandleFunc("/v1/acl/update", s.wrap(aclDisabled))
s.mux.HandleFunc("/v1/acl/destroy/", s.wrap(aclDisabled))
s.mux.HandleFunc("/v1/acl/info/", s.wrap(aclDisabled))
s.mux.HandleFunc("/v1/acl/clone/", s.wrap(aclDisabled))
s.mux.HandleFunc("/v1/acl/list", s.wrap(aclDisabled))
}
if enableDebug {
s.mux.HandleFunc("/debug/pprof/", pprof.Index)
s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
@ -273,10 +289,20 @@ func (s *HTTPServer) parseDC(req *http.Request, dc *string) {
}
}
// parseToken is used to parse the ?token query param
func (s *HTTPServer) parseToken(req *http.Request, token *string) {
if other := req.URL.Query().Get("token"); other != "" {
*token = other
} else if *token == "" {
*token = s.agent.config.ACLToken
}
}
// parse is a convenience method for endpoints that need
// to use both parseWait and parseDC.
func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, dc *string, b *structs.QueryOptions) bool {
s.parseDC(req, dc)
s.parseToken(req, &b.Token)
if parseConsistency(resp, req, b) {
return true
}

View file

@ -145,6 +145,7 @@ func (s *HTTPServer) KVSPut(resp http.ResponseWriter, req *http.Request, args *s
Value: nil,
},
}
applyReq.Token = args.Token
// Check for flags
params := req.URL.Query()
@ -215,6 +216,7 @@ func (s *HTTPServer) KVSDelete(resp http.ResponseWriter, req *http.Request, args
Key: args.Key,
},
}
applyReq.Token = args.Token
// Check for recurse
params := req.URL.Query()

194
consul/acl.go Normal file
View 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
View 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
View 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
View 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"
}
`

View file

@ -80,6 +80,11 @@ func NewClient(config *Config) (*Client, error) {
return nil, fmt.Errorf("Config must provide a DataDir")
}
// Sanity check the ACLs
if err := config.CheckACL(); err != nil {
return nil, err
}
// Ensure we have a log output
if config.LogOutput == nil {
config.LogOutput = os.Stderr

View file

@ -128,6 +128,38 @@ type Config struct {
// operators track which versions are actively deployed
Build string
// ACLToken is the default token to use when making a request.
// If not provided, the anonymous token is used. This enables
// backwards compatibility as well.
ACLToken string
// ACLMasterToken is used to bootstrap the ACL system. It should be specified
// on the servers in the ACLDatacenter. When the leader comes online, it ensures
// that the Master token is available. This provides the initial token.
ACLMasterToken string
// ACLDatacenter provides the authoritative datacenter for ACL
// tokens. If not provided, ACL verification is disabled.
ACLDatacenter string
// ACLTTL controls the time-to-live of cached ACL policies.
// It can be set to zero to disable caching, but this adds
// a substantial cost.
ACLTTL time.Duration
// ACLDefaultPolicy is used to control the ACL interaction when
// there is no defined policy. This can be "allow" which means
// ACLs are used to black-list, or "deny" which means ACLs are
// white-lists.
ACLDefaultPolicy string
// ACLDownPolicy controls the behavior of ACLs if the ACLDatacenter
// cannot be contacted. It can be either "deny" to deny all requests,
// or "extend-cache" which ignores the ACLCacheInterval and uses
// cached policies. If a policy is not in the cache, it acts like deny.
// "allow" can be used to allow all requests. This is not recommended.
ACLDownPolicy string
// ServerUp callback can be used to trigger a notification that
// a Consul server is now up and known about.
ServerUp func()
@ -145,6 +177,24 @@ func (c *Config) CheckVersion() error {
return nil
}
// CheckACL is used to sanity check the ACL configuration
func (c *Config) CheckACL() error {
switch c.ACLDefaultPolicy {
case "allow":
case "deny":
default:
return fmt.Errorf("Unsupported default ACL policy: %s", c.ACLDefaultPolicy)
}
switch c.ACLDownPolicy {
case "allow":
case "deny":
case "extend-cache":
default:
return fmt.Errorf("Unsupported down ACL policy: %s", c.ACLDownPolicy)
}
return nil
}
// AppendCA opens and parses the CA file and adds the certificates to
// the provided CertPool.
func (c *Config) AppendCA(pool *x509.CertPool) error {
@ -324,6 +374,9 @@ func DefaultConfig() *Config {
SerfWANConfig: serf.DefaultConfig(),
ReconcileInterval: 60 * time.Second,
ProtocolVersion: ProtocolVersionMax,
ACLTTL: 30 * time.Second,
ACLDefaultPolicy: "allow",
ACLDownPolicy: "extend-cache",
}
// Increase our reap interval to 3 days instead of 24h.

89
consul/filter.go Normal file
View 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
View 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"
}
`

View file

@ -69,6 +69,8 @@ func (c *consulFSM) Apply(log *raft.Log) interface{} {
return c.applyKVSOperation(buf[1:], log.Index)
case structs.SessionRequestType:
return c.applySessionOperation(buf[1:], log.Index)
case structs.ACLRequestType:
return c.applyACLOperation(buf[1:], log.Index)
default:
panic(fmt.Errorf("failed to apply request: %#v", buf))
}
@ -196,6 +198,33 @@ func (c *consulFSM) applySessionOperation(buf []byte, index uint64) interface{}
return nil
}
func (c *consulFSM) applyACLOperation(buf []byte, index uint64) interface{} {
var req structs.ACLRequest
if err := structs.Decode(buf, &req); err != nil {
panic(fmt.Errorf("failed to decode request: %v", err))
}
switch req.Op {
case structs.ACLSet:
if err := c.state.ACLSet(index, &req.ACL, false); err != nil {
return err
} else {
return req.ACL.ID
}
case structs.ACLForceSet:
if err := c.state.ACLSet(index, &req.ACL, true); err != nil {
return err
} else {
return req.ACL.ID
}
case structs.ACLDelete:
return c.state.ACLDelete(index, req.ACL.ID)
default:
c.logger.Printf("[WARN] consul.fsm: Invalid ACL operation '%s'", req.Op)
return fmt.Errorf("Invalid ACL operation '%s'", req.Op)
}
return nil
}
func (c *consulFSM) Snapshot() (raft.FSMSnapshot, error) {
defer func(start time.Time) {
c.logger.Printf("[INFO] consul.fsm: snapshot created in %v", time.Now().Sub(start))
@ -267,6 +296,15 @@ func (c *consulFSM) Restore(old io.ReadCloser) error {
return err
}
case structs.ACLRequestType:
var req structs.ACL
if err := dec.Decode(&req); err != nil {
return err
}
if err := c.state.ACLRestore(&req); err != nil {
return err
}
default:
return fmt.Errorf("Unrecognized msg type: %v", msgType)
}
@ -298,6 +336,11 @@ func (s *consulSnapshot) Persist(sink raft.SnapshotSink) error {
return err
}
if err := s.persistACLs(sink, encoder); err != nil {
sink.Cancel()
return err
}
if err := s.persistKV(sink, encoder); err != nil {
sink.Cancel()
return err
@ -364,6 +407,22 @@ func (s *consulSnapshot) persistSessions(sink raft.SnapshotSink,
return nil
}
func (s *consulSnapshot) persistACLs(sink raft.SnapshotSink,
encoder *codec.Encoder) error {
acls, err := s.state.ACLList()
if err != nil {
return err
}
for _, s := range acls {
sink.Write([]byte{byte(structs.ACLRequestType)})
if err := encoder.Encode(s); err != nil {
return err
}
}
return nil
}
func (s *consulSnapshot) persistKV(sink raft.SnapshotSink,
encoder *codec.Encoder) error {
streamCh := make(chan interface{}, 256)

View file

@ -328,6 +328,8 @@ func TestFSM_SnapshotRestore(t *testing.T) {
})
session := &structs.Session{Node: "foo"}
fsm.state.SessionCreate(9, session)
acl := &structs.ACL{Name: "User Token"}
fsm.state.ACLSet(10, acl, false)
// Snapshot
snap, err := fsm.Snapshot()
@ -392,7 +394,16 @@ func TestFSM_SnapshotRestore(t *testing.T) {
t.Fatalf("err: %v", err)
}
if s.Node != "foo" {
t.Fatalf("bad: %v", d)
t.Fatalf("bad: %v", s)
}
// Verify ACL is restored
_, a, err := fsm.state.ACLGet(acl.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if a.Name != "User Token" {
t.Fatalf("bad: %v", a)
}
}
@ -767,3 +778,75 @@ func TestFSM_KVSUnlock(t *testing.T) {
t.Fatalf("bad: %v", *d)
}
}
func TestFSM_ACL_Set_Delete(t *testing.T) {
fsm, err := NewFSM(os.Stderr)
if err != nil {
t.Fatalf("err: %v", err)
}
defer fsm.Close()
// Create a new ACL
req := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLSet,
ACL: structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
},
}
buf, err := structs.Encode(structs.ACLRequestType, req)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := fsm.Apply(makeLog(buf))
if err, ok := resp.(error); ok {
t.Fatalf("resp: %v", err)
}
// Get the ACL
id := resp.(string)
_, acl, err := fsm.state.ACLGet(id)
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("missing")
}
// Verify the ACL
if acl.ID != id {
t.Fatalf("bad: %v", *acl)
}
if acl.Name != "User token" {
t.Fatalf("bad: %v", *acl)
}
if acl.Type != structs.ACLTypeClient {
t.Fatalf("bad: %v", *acl)
}
// Try to destroy
destroy := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLDelete,
ACL: structs.ACL{
ID: id,
},
}
buf, err = structs.Encode(structs.ACLRequestType, destroy)
if err != nil {
t.Fatalf("err: %v", err)
}
resp = fsm.Apply(makeLog(buf))
if resp != nil {
t.Fatalf("resp: %v", resp)
}
_, acl, err = fsm.state.ACLGet(id)
if err != nil {
t.Fatalf("err: %v", err)
}
if acl != nil {
t.Fatalf("should be destroyed")
}
}

View file

@ -2,9 +2,10 @@ package consul
import (
"fmt"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/consul/structs"
"time"
)
// KVS endpoint is used to manipulate the Key-Value store
@ -25,6 +26,23 @@ func (k *KVS) Apply(args *structs.KVSRequest, reply *bool) error {
return fmt.Errorf("Must provide key")
}
// Apply the ACL policy if any
acl, err := k.srv.resolveToken(args.Token)
if err != nil {
return err
} else if acl != nil {
switch args.Op {
case structs.KVSDeleteTree:
if !acl.KeyWritePrefix(args.DirEnt.Key) {
return permissionDeniedErr
}
default:
if !acl.KeyWrite(args.DirEnt.Key) {
return permissionDeniedErr
}
}
}
// If this is a lock, we must check for a lock-delay. Since lock-delay
// is based on wall-time, each peer expire the lock-delay at a slightly
// different time. This means the enforcement of lock-delay cannot be done
@ -65,6 +83,11 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er
return err
}
acl, err := k.srv.resolveToken(args.Token)
if err != nil {
return err
}
// Get the local state
state := k.srv.fsm.State()
return k.srv.blockingRPC(&args.QueryOptions,
@ -75,6 +98,9 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er
if err != nil {
return err
}
if acl != nil && !acl.KeyRead(args.Key) {
ent = nil
}
if ent == nil {
// Must provide non-zero index to prevent blocking
// Index 1 is impossible anyways (due to Raft internals)
@ -98,6 +124,11 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e
return err
}
acl, err := k.srv.resolveToken(args.Token)
if err != nil {
return err
}
// Get the local state
state := k.srv.fsm.State()
return k.srv.blockingRPC(&args.QueryOptions,
@ -108,6 +139,9 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e
if err != nil {
return err
}
if acl != nil {
ent = FilterDirEnt(acl, ent)
}
if len(ent) == 0 {
// Must provide non-zero index to prevent blocking
// Index 1 is impossible anyways (due to Raft internals)
@ -139,14 +173,23 @@ func (k *KVS) ListKeys(args *structs.KeyListRequest, reply *structs.IndexedKeyLi
return err
}
acl, err := k.srv.resolveToken(args.Token)
if err != nil {
return err
}
// Get the local state
state := k.srv.fsm.State()
return k.srv.blockingRPC(&args.QueryOptions,
&reply.QueryMeta,
state.QueryTables("KVSListKeys"),
func() error {
var err error
reply.Index, reply.Keys, err = state.KVSListKeys(args.Prefix, args.Seperator)
index, keys, err := state.KVSListKeys(args.Prefix, args.Seperator)
reply.Index = index
if acl != nil {
keys = FilterKeys(acl, keys)
}
reply.Keys = keys
return err
})
}

View file

@ -1,11 +1,13 @@
package consul
import (
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/testutil"
"os"
"strings"
"testing"
"time"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/testutil"
)
func TestKVS_Apply(t *testing.T) {
@ -64,6 +66,68 @@ func TestKVS_Apply(t *testing.T) {
}
}
func TestKVS_Apply_ACLDeny(t *testing.T) {
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "deny"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
client := rpcClient(t, s1)
defer client.Close()
testutil.WaitForLeader(t, client.Call, "dc1")
// Create the ACL
arg := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLSet,
ACL: structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
Rules: testListRules,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out string
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
id := out
// Try a write
argR := structs.KVSRequest{
Datacenter: "dc1",
Op: structs.KVSSet,
DirEnt: structs.DirEntry{
Key: "foo/bar",
Flags: 42,
Value: []byte("test"),
},
WriteRequest: structs.WriteRequest{Token: id},
}
var outR bool
err := client.Call("KVS.Apply", &argR, &outR)
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
// Try a recursive delete
argR = structs.KVSRequest{
Datacenter: "dc1",
Op: structs.KVSDeleteTree,
DirEnt: structs.DirEntry{
Key: "test",
},
WriteRequest: structs.WriteRequest{Token: id},
}
err = client.Call("KVS.Apply", &argR, &outR)
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
}
func TestKVS_Get(t *testing.T) {
dir1, s1 := testServer(t)
defer os.RemoveAll(dir1)
@ -111,6 +175,51 @@ func TestKVS_Get(t *testing.T) {
}
}
func TestKVS_Get_ACLDeny(t *testing.T) {
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "deny"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
client := rpcClient(t, s1)
defer client.Close()
testutil.WaitForLeader(t, client.Call, "dc1")
arg := structs.KVSRequest{
Datacenter: "dc1",
Op: structs.KVSSet,
DirEnt: structs.DirEntry{
Key: "zip",
Flags: 42,
Value: []byte("test"),
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out bool
if err := client.Call("KVS.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
getR := structs.KeyRequest{
Datacenter: "dc1",
Key: "zip",
}
var dirent structs.IndexedDirEntries
if err := client.Call("KVS.Get", &getR, &dirent); err != nil {
t.Fatalf("err: %v", err)
}
if dirent.Index == 0 {
t.Fatalf("Bad: %v", dirent)
}
if len(dirent.Entries) != 0 {
t.Fatalf("Bad: %v", dirent)
}
}
func TestKVSEndpoint_List(t *testing.T) {
dir1, s1 := testServer(t)
defer os.RemoveAll(dir1)
@ -170,6 +279,90 @@ func TestKVSEndpoint_List(t *testing.T) {
}
}
func TestKVSEndpoint_List_ACLDeny(t *testing.T) {
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "deny"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
client := rpcClient(t, s1)
defer client.Close()
testutil.WaitForLeader(t, client.Call, "dc1")
keys := []string{
"abe",
"bar",
"foo",
"test",
"zip",
}
for _, key := range keys {
arg := structs.KVSRequest{
Datacenter: "dc1",
Op: structs.KVSSet,
DirEnt: structs.DirEntry{
Key: key,
Flags: 1,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out bool
if err := client.Call("KVS.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
}
arg := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLSet,
ACL: structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
Rules: testListRules,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out string
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
id := out
getR := structs.KeyRequest{
Datacenter: "dc1",
Key: "",
QueryOptions: structs.QueryOptions{Token: id},
}
var dirent structs.IndexedDirEntries
if err := client.Call("KVS.List", &getR, &dirent); err != nil {
t.Fatalf("err: %v", err)
}
if dirent.Index == 0 {
t.Fatalf("Bad: %v", dirent)
}
if len(dirent.Entries) != 2 {
t.Fatalf("Bad: %v", dirent.Entries)
}
for i := 0; i < len(dirent.Entries); i++ {
d := dirent.Entries[i]
switch i {
case 0:
if d.Key != "foo" {
t.Fatalf("bad key")
}
case 1:
if d.Key != "test" {
t.Fatalf("bad key")
}
}
}
}
func TestKVSEndpoint_ListKeys(t *testing.T) {
dir1, s1 := testServer(t)
defer os.RemoveAll(dir1)
@ -227,6 +420,84 @@ func TestKVSEndpoint_ListKeys(t *testing.T) {
}
}
func TestKVSEndpoint_ListKeys_ACLDeny(t *testing.T) {
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "deny"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
client := rpcClient(t, s1)
defer client.Close()
testutil.WaitForLeader(t, client.Call, "dc1")
keys := []string{
"abe",
"bar",
"foo",
"test",
"zip",
}
for _, key := range keys {
arg := structs.KVSRequest{
Datacenter: "dc1",
Op: structs.KVSSet,
DirEnt: structs.DirEntry{
Key: key,
Flags: 1,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out bool
if err := client.Call("KVS.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
}
arg := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLSet,
ACL: structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
Rules: testListRules,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out string
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
id := out
getR := structs.KeyListRequest{
Datacenter: "dc1",
Prefix: "",
Seperator: "/",
QueryOptions: structs.QueryOptions{Token: id},
}
var dirent structs.IndexedKeyList
if err := client.Call("KVS.ListKeys", &getR, &dirent); err != nil {
t.Fatalf("err: %v", err)
}
if dirent.Index == 0 {
t.Fatalf("Bad: %v", dirent)
}
if len(dirent.Keys) != 2 {
t.Fatalf("Bad: %v", dirent.Keys)
}
if dirent.Keys[0] != "foo" {
t.Fatalf("Bad: %v", dirent.Keys)
}
if dirent.Keys[1] != "test" {
t.Fatalf("Bad: %v", dirent.Keys)
}
}
func TestKVS_Apply_LockDelay(t *testing.T) {
dir1, s1 := testServer(t)
defer os.RemoveAll(dir1)
@ -294,3 +565,18 @@ func TestKVS_Apply_LockDelay(t *testing.T) {
t.Fatalf("should acquire")
}
}
var testListRules = `
key "" {
policy = "deny"
}
key "foo" {
policy = "read"
}
key "test" {
policy = "write"
}
key "test/priv" {
policy = "read"
}
`

View file

@ -1,6 +1,7 @@
package consul
import (
"fmt"
"net"
"strconv"
"time"
@ -54,6 +55,11 @@ func (s *Server) leaderLoop(stopCh chan struct{}) {
s.logger.Printf("[WARN] consul: failed to broadcast new leader event: %v", err)
}
// Setup ACLs if we are the leader and need to
if err := s.initializeACL(); err != nil {
s.logger.Printf("[ERR] consul: ACL initialization failed: %v", err)
}
// Reconcile channel is only used once initial reconcile
// has succeeded
var reconcileCh chan serf.Member
@ -99,6 +105,73 @@ WAIT:
}
}
// initializeACL is used to setup the ACLs if we are the leader
// and need to do this.
func (s *Server) initializeACL() error {
// Bail if not configured or we are not authoritative
authDC := s.config.ACLDatacenter
if len(authDC) == 0 || authDC != s.config.Datacenter {
return nil
}
// Purge the cache, since it could've changed while we
// were not the leader
s.aclAuthCache.Purge()
// Look for the anonymous token
state := s.fsm.State()
_, acl, err := state.ACLGet(anonymousToken)
if err != nil {
return fmt.Errorf("failed to get anonymous token: %v", err)
}
// Create anonymous token if missing
if acl == nil {
req := structs.ACLRequest{
Datacenter: authDC,
Op: structs.ACLForceSet,
ACL: structs.ACL{
ID: anonymousToken,
Name: "Anonymous Token",
Type: structs.ACLTypeClient,
},
}
_, err := s.raftApply(structs.ACLRequestType, &req)
if err != nil {
return fmt.Errorf("failed to create anonymous token: %v", err)
}
}
// Check for configured master token
master := s.config.ACLMasterToken
if len(master) == 0 {
return nil
}
// Look for the master token
_, acl, err = state.ACLGet(master)
if err != nil {
return fmt.Errorf("failed to get master token: %v", err)
}
if acl == nil {
req := structs.ACLRequest{
Datacenter: authDC,
Op: structs.ACLForceSet,
ACL: structs.ACL{
ID: master,
Name: "Master Token",
Type: structs.ACLTypeManagement,
},
}
_, err := s.raftApply(structs.ACLRequestType, &req)
if err != nil {
return fmt.Errorf("failed to create master token: %v", err)
}
}
return nil
}
// reconcile is used to reconcile the differences between Serf
// membership and what is reflected in our strongly consistent store.
// Mainly we need to ensure all live nodes are registered, all failed

View file

@ -15,6 +15,8 @@ import (
"sync"
"time"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/golang-lru"
"github.com/hashicorp/raft"
"github.com/hashicorp/raft-mdb"
"github.com/hashicorp/serf/serf"
@ -43,11 +45,24 @@ const (
// serverMaxStreams controsl how many idle streams we keep
// open to a server
serverMaxStreams = 64
// Maximum number of cached ACL entries
aclCacheSize = 256
)
// Server is Consul server which manages the service discovery,
// health checking, DC forwarding, Raft, and multiple Serf pools.
type Server struct {
// aclAuthCache is the authoritative ACL cache
aclAuthCache *acl.Cache
// aclCache is a non-authoritative ACL cache
aclCache *lru.Cache
// aclPolicyCache is a policy cache
aclPolicyCache *lru.Cache
// Consul configuration
config *Config
// Connection pool to other consul servers
@ -125,6 +140,7 @@ type endpoints struct {
KVS *KVS
Session *Session
Internal *Internal
ACL *ACL
}
// NewServer is used to construct a new Consul server from the
@ -140,6 +156,11 @@ func NewServer(config *Config) (*Server, error) {
return nil, fmt.Errorf("Config must provide a DataDir")
}
// Sanity check the ACLs
if err := config.CheckACL(); err != nil {
return nil, err
}
// Ensure we have a log output
if config.LogOutput == nil {
config.LogOutput = os.Stderr
@ -175,6 +196,27 @@ func NewServer(config *Config) (*Server, error) {
shutdownCh: make(chan struct{}),
}
// Initialize the authoritative ACL cache
s.aclAuthCache, err = acl.NewCache(aclCacheSize, s.aclFault)
if err != nil {
s.Shutdown()
return nil, fmt.Errorf("Failed to create ACL cache: %v", err)
}
// Initialize the non-authoritative ACL cache
s.aclCache, err = lru.New(aclCacheSize)
if err != nil {
s.Shutdown()
return nil, fmt.Errorf("Failed to create ACL cache: %v", err)
}
// Initialize the ACL policy cache
s.aclPolicyCache, err = lru.New(aclCacheSize)
if err != nil {
s.Shutdown()
return nil, fmt.Errorf("Failed to create ACL policy cache: %v", err)
}
// Initialize the RPC layer
if err := s.setupRPC(tlsConfig); err != nil {
s.Shutdown()
@ -336,6 +378,7 @@ func (s *Server) setupRPC(tlsConfig *tls.Config) error {
s.endpoints.KVS = &KVS{s}
s.endpoints.Session = &Session{s}
s.endpoints.Internal = &Internal{s}
s.endpoints.ACL = &ACL{s}
// Register the handlers
s.rpcServer.Register(s.endpoints.Status)
@ -344,6 +387,7 @@ func (s *Server) setupRPC(tlsConfig *tls.Config) error {
s.rpcServer.Register(s.endpoints.KVS)
s.rpcServer.Register(s.endpoints.Session)
s.rpcServer.Register(s.endpoints.Internal)
s.rpcServer.Register(s.endpoints.ACL)
list, err := net.ListenTCP("tcp", s.config.RPCAddr)
if err != nil {

View file

@ -101,6 +101,17 @@ func testServerDCExpect(t *testing.T, dc string, expect int) (string, *Server) {
return dir, server
}
func testServerWithConfig(t *testing.T, cb func(c *Config)) (string, *Server) {
name := fmt.Sprintf("Node %d", getPort())
dir, config := testServerConfig(t, name)
cb(config)
server, err := NewServer(config)
if err != nil {
t.Fatalf("err: %v", err)
}
return dir, server
}
func TestServer_StartStop(t *testing.T) {
dir := tmpDir(t)
defer os.RemoveAll(dir)

View file

@ -21,6 +21,7 @@ const (
dbKVS = "kvs"
dbSessions = "sessions"
dbSessionChecks = "sessionChecks"
dbACLs = "acls"
dbMaxMapSize32bit uint64 = 512 * 1024 * 1024 // 512MB maximum size
dbMaxMapSize64bit uint64 = 32 * 1024 * 1024 * 1024 // 32GB maximum size
)
@ -53,6 +54,7 @@ type StateStore struct {
kvsTable *MDBTable
sessionTable *MDBTable
sessionCheckTable *MDBTable
aclTable *MDBTable
tables MDBTables
watch map[*MDBTable]*NotifyGroup
queryTables map[string]MDBTables
@ -306,9 +308,26 @@ func (s *StateStore) initialize() error {
},
}
s.aclTable = &MDBTable{
Name: dbACLs,
Indexes: map[string]*MDBIndex{
"id": &MDBIndex{
Unique: true,
Fields: []string{"ID"},
},
},
Decoder: func(buf []byte) interface{} {
out := new(structs.ACL)
if err := structs.Decode(buf, out); err != nil {
panic(err)
}
return out
},
}
// Store the set of tables
s.tables = []*MDBTable{s.nodeTable, s.serviceTable, s.checkTable,
s.kvsTable, s.sessionTable, s.sessionCheckTable}
s.kvsTable, s.sessionTable, s.sessionCheckTable, s.aclTable}
for _, table := range s.tables {
table.Env = s.env
table.Encoder = encoder
@ -338,6 +357,8 @@ func (s *StateStore) initialize() error {
"SessionGet": MDBTables{s.sessionTable},
"SessionList": MDBTables{s.sessionTable},
"NodeSessions": MDBTables{s.sessionTable},
"ACLGet": MDBTables{s.aclTable},
"ACLList": MDBTables{s.aclTable},
}
return nil
}
@ -1249,8 +1270,8 @@ func (s *StateStore) SessionCreate(index uint64, session *structs.Session) error
}
// Generate a new session ID, verify uniqueness
session.ID = generateUUID()
for {
session.ID = generateUUID()
res, err = s.sessionTable.GetTxn(tx, "id", session.ID)
if err != nil {
return err
@ -1346,7 +1367,7 @@ func (s *StateStore) NodeSessions(node string) (uint64, []*structs.Session, erro
return idx, out, err
}
// SessionDelete is used to destroy a session.
// SessionDestroy is used to destroy a session.
func (s *StateStore) SessionDestroy(index uint64, id string) error {
tx, err := s.tables.StartTxn(false)
if err != nil {
@ -1482,6 +1503,124 @@ func (s *StateStore) invalidateLocks(index uint64, tx *MDBTxn,
return nil
}
// ACLSet is used to create or update an ACL entry
// allowCreate is used for initialization of the anonymous and master tokens,
// since it permits them to be created with a specified ID that does not exist.
func (s *StateStore) ACLSet(index uint64, acl *structs.ACL, allowCreate bool) error {
// Start a new txn
tx, err := s.tables.StartTxn(false)
if err != nil {
return err
}
defer tx.Abort()
// Generate a new session ID
if acl.ID == "" {
for {
acl.ID = generateUUID()
res, err := s.aclTable.GetTxn(tx, "id", acl.ID)
if err != nil {
return err
}
// Quit if this ID is unique
if len(res) == 0 {
break
}
}
acl.CreateIndex = index
acl.ModifyIndex = index
} else {
// Look for the existing node
res, err := s.aclTable.GetTxn(tx, "id", acl.ID)
if err != nil {
return err
}
switch len(res) {
case 0:
if !allowCreate {
return fmt.Errorf("Invalid ACL")
}
acl.CreateIndex = index
acl.ModifyIndex = index
case 1:
exist := res[0].(*structs.ACL)
acl.CreateIndex = exist.CreateIndex
acl.ModifyIndex = index
default:
panic(fmt.Errorf("Duplicate ACL definition. Internal error"))
}
}
// Insert the ACL
if err := s.aclTable.InsertTxn(tx, acl); err != nil {
return err
}
// Trigger the update notifications
if err := s.aclTable.SetLastIndexTxn(tx, index); err != nil {
return err
}
tx.Defer(func() { s.watch[s.aclTable].Notify() })
return tx.Commit()
}
// ACLRestore is used to restore an ACL. It should only be used when
// doing a restore, otherwise ACLSet should be used.
func (s *StateStore) ACLRestore(acl *structs.ACL) error {
// Start a new txn
tx, err := s.aclTable.StartTxn(false, nil)
if err != nil {
return err
}
defer tx.Abort()
if err := s.aclTable.InsertTxn(tx, acl); err != nil {
return err
}
return tx.Commit()
}
// ACLGet is used to get an ACL by ID
func (s *StateStore) ACLGet(id string) (uint64, *structs.ACL, error) {
idx, res, err := s.aclTable.Get("id", id)
var d *structs.ACL
if len(res) > 0 {
d = res[0].(*structs.ACL)
}
return idx, d, err
}
// ACLList is used to list all the acls
func (s *StateStore) ACLList() (uint64, []*structs.ACL, error) {
idx, res, err := s.aclTable.Get("id")
out := make([]*structs.ACL, len(res))
for i, raw := range res {
out[i] = raw.(*structs.ACL)
}
return idx, out, err
}
// ACLDelete is used to remove an ACL
func (s *StateStore) ACLDelete(index uint64, id string) error {
tx, err := s.tables.StartTxn(false)
if err != nil {
panic(fmt.Errorf("Failed to start txn: %v", err))
}
defer tx.Abort()
if n, err := s.aclTable.DeleteTxn(tx, "id", id); err != nil {
return err
} else if n > 0 {
if err := s.aclTable.SetLastIndexTxn(tx, index); err != nil {
return err
}
tx.Defer(func() { s.watch[s.aclTable].Notify() })
}
return tx.Commit()
}
// Snapshot is used to create a point in time snapshot
func (s *StateStore) Snapshot() (*StateSnapshot, error) {
// Begin a new txn on all tables
@ -1555,3 +1694,13 @@ func (s *StateSnapshot) SessionList() ([]*structs.Session, error) {
}
return out, err
}
// ACLList is used to list all of the ACLs
func (s *StateSnapshot) ACLList() ([]*structs.ACL, error) {
res, err := s.store.aclTable.GetTxn(s.tx, "id")
out := make([]*structs.ACL, len(res))
for i, raw := range res {
out[i] = raw.(*structs.ACL)
}
return out, err
}

View file

@ -652,6 +652,22 @@ func TestStoreSnapshot(t *testing.T) {
t.Fatalf("err: %v", err)
}
a1 := &structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
}
if err := store.ACLSet(19, a1, false); err != nil {
t.Fatalf("err: %v", err)
}
a2 := &structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
}
if err := store.ACLSet(20, a2, false); err != nil {
t.Fatalf("err: %v", err)
}
// Take a snapshot
snap, err := store.Snapshot()
if err != nil {
@ -660,7 +676,7 @@ func TestStoreSnapshot(t *testing.T) {
defer snap.Close()
// Check the last nodes
if idx := snap.LastIndex(); idx != 18 {
if idx := snap.LastIndex(); idx != 20 {
t.Fatalf("bad: %v", idx)
}
@ -724,14 +740,23 @@ func TestStoreSnapshot(t *testing.T) {
t.Fatalf("missing sessions")
}
// Check for an acl
acls, err := snap.ACLList()
if err != nil {
t.Fatalf("err: %v", err)
}
if len(acls) != 2 {
t.Fatalf("missing acls")
}
// Make some changes!
if err := store.EnsureService(19, "foo", &structs.NodeService{"db", "db", []string{"slave"}, 8000}); err != nil {
if err := store.EnsureService(21, "foo", &structs.NodeService{"db", "db", []string{"slave"}, 8000}); err != nil {
t.Fatalf("err: %v", err)
}
if err := store.EnsureService(20, "bar", &structs.NodeService{"db", "db", []string{"master"}, 8000}); err != nil {
if err := store.EnsureService(22, "bar", &structs.NodeService{"db", "db", []string{"master"}, 8000}); err != nil {
t.Fatalf("err: %v", err)
}
if err := store.EnsureNode(21, structs.Node{"baz", "127.0.0.3"}); err != nil {
if err := store.EnsureNode(23, structs.Node{"baz", "127.0.0.3"}); err != nil {
t.Fatalf("err: %v", err)
}
checkAfter := &structs.HealthCheck{
@ -741,11 +766,16 @@ func TestStoreSnapshot(t *testing.T) {
Status: structs.HealthCritical,
ServiceID: "db",
}
if err := store.EnsureCheck(22, checkAfter); err != nil {
if err := store.EnsureCheck(24, checkAfter); err != nil {
t.Fatalf("err: %v", err)
}
if err := store.KVSDelete(23, "/web/b"); err != nil {
if err := store.KVSDelete(25, "/web/b"); err != nil {
t.Fatalf("err: %v", err)
}
// Nuke an ACL
if err := store.ACLDelete(26, a1.ID); err != nil {
t.Fatalf("err: %v", err)
}
@ -807,6 +837,15 @@ func TestStoreSnapshot(t *testing.T) {
if len(sessions) != 2 {
t.Fatalf("missing sessions")
}
// Check for an acl
acls, err = snap.ACLList()
if err != nil {
t.Fatalf("err: %v", err)
}
if len(acls) != 2 {
t.Fatalf("missing acls")
}
}
func TestEnsureCheck(t *testing.T) {
@ -2117,3 +2156,144 @@ func TestSessionInvalidate_KeyUnlock(t *testing.T) {
t.Fatalf("Bad: %v", expires)
}
}
func TestACLSet_Get(t *testing.T) {
store, err := testStateStore()
if err != nil {
t.Fatalf("err: %v", err)
}
defer store.Close()
idx, out, err := store.ACLGet("1234")
if err != nil {
t.Fatalf("err: %v", err)
}
if idx != 0 {
t.Fatalf("bad: %v", idx)
}
if out != nil {
t.Fatalf("bad: %v", out)
}
a := &structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
Rules: "",
}
if err := store.ACLSet(50, a, false); err != nil {
t.Fatalf("err: %v", err)
}
if a.CreateIndex != 50 {
t.Fatalf("Bad: %v", a)
}
if a.ModifyIndex != 50 {
t.Fatalf("Bad: %v", a)
}
if a.ID == "" {
t.Fatalf("Bad: %v", a)
}
idx, out, err = store.ACLGet(a.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if idx != 50 {
t.Fatalf("bad: %v", idx)
}
if !reflect.DeepEqual(out, a) {
t.Fatalf("bad: %v", out)
}
// Update
a.Rules = "foo bar baz"
if err := store.ACLSet(52, a, false); err != nil {
t.Fatalf("err: %v", err)
}
if a.CreateIndex != 50 {
t.Fatalf("Bad: %v", a)
}
if a.ModifyIndex != 52 {
t.Fatalf("Bad: %v", a)
}
idx, out, err = store.ACLGet(a.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if idx != 52 {
t.Fatalf("bad: %v", idx)
}
if !reflect.DeepEqual(out, a) {
t.Fatalf("bad: %v", out)
}
}
func TestACLDelete(t *testing.T) {
store, err := testStateStore()
if err != nil {
t.Fatalf("err: %v", err)
}
defer store.Close()
a := &structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
Rules: "",
}
if err := store.ACLSet(50, a, false); err != nil {
t.Fatalf("err: %v", err)
}
if err := store.ACLDelete(52, a.ID); err != nil {
t.Fatalf("err: %v", err)
}
if err := store.ACLDelete(53, a.ID); err != nil {
t.Fatalf("err: %v", err)
}
idx, out, err := store.ACLGet(a.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if idx != 52 {
t.Fatalf("bad: %v", idx)
}
if out != nil {
t.Fatalf("bad: %v", out)
}
}
func TestACLList(t *testing.T) {
store, err := testStateStore()
if err != nil {
t.Fatalf("err: %v", err)
}
defer store.Close()
a1 := &structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
}
if err := store.ACLSet(50, a1, false); err != nil {
t.Fatalf("err: %v", err)
}
a2 := &structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
}
if err := store.ACLSet(51, a2, false); err != nil {
t.Fatalf("err: %v", err)
}
idx, out, err := store.ACLList()
if err != nil {
t.Fatalf("err: %v", err)
}
if idx != 51 {
t.Fatalf("bad: %v", idx)
}
if len(out) != 2 {
t.Fatalf("bad: %v", out)
}
}

View file

@ -3,8 +3,10 @@ package structs
import (
"bytes"
"fmt"
"github.com/ugorji/go/codec"
"time"
"github.com/hashicorp/consul/acl"
"github.com/ugorji/go/codec"
)
var (
@ -20,6 +22,7 @@ const (
DeregisterRequestType
KVSRequestType
SessionRequestType
ACLRequestType
)
const (
@ -32,6 +35,15 @@ const (
HealthCritical = "critical"
)
const (
// Client tokens have rules applied
ACLTypeClient = "client"
// Management tokens have an always allow policy.
// They are used for token management.
ACLTypeManagement = "management"
)
const (
// MaxLockDelay provides a maximum LockDelay value for
// a session. Any value above this will not be respected.
@ -43,10 +55,15 @@ type RPCInfo interface {
RequestDatacenter() string
IsRead() bool
AllowStaleRead() bool
ACLToken() string
}
// QueryOptions is used to specify various flags for read queries
type QueryOptions struct {
// Token is the ACL token ID. If not provided, the 'anonymous'
// token is assumed for backwards compatibility.
Token string
// If set, wait until query exceeds given index. Must be provided
// with MaxQueryTime.
MinQueryIndex uint64
@ -72,7 +89,15 @@ func (q QueryOptions) AllowStaleRead() bool {
return q.AllowStale
}
type WriteRequest struct{}
func (q QueryOptions) ACLToken() string {
return q.Token
}
type WriteRequest struct {
// Token is the ACL token ID. If not provided, the 'anonymous'
// token is assumed for backwards compatibility.
Token string
}
// WriteRequest only applies to writes, always false
func (w WriteRequest) IsRead() bool {
@ -83,6 +108,10 @@ func (w WriteRequest) AllowStaleRead() bool {
return false
}
func (w WriteRequest) ACLToken() string {
return w.Token
}
// QueryMeta allows a query response to include potentially
// useful metadata about a query
type QueryMeta struct {
@ -396,6 +425,74 @@ type IndexedSessions struct {
QueryMeta
}
// ACL is used to represent a token and it's rules
type ACL struct {
CreateIndex uint64
ModifyIndex uint64
ID string
Name string
Type string
Rules string
}
type ACLs []*ACL
type ACLOp string
const (
ACLSet ACLOp = "set"
ACLForceSet = "force-set"
ACLDelete = "delete"
)
// ACLRequest is used to create, update or delete an ACL
type ACLRequest struct {
Datacenter string
Op ACLOp
ACL ACL
WriteRequest
}
func (r *ACLRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLSpecificRequest is used to request an ACL by ID
type ACLSpecificRequest struct {
Datacenter string
ACL string
QueryOptions
}
func (r *ACLSpecificRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLPolicyRequest is used to request an ACL by ID, conditionally
// filtering on an ID
type ACLPolicyRequest struct {
Datacenter string
ACL string
ETag string
QueryOptions
}
func (r *ACLPolicyRequest) RequestDatacenter() string {
return r.Datacenter
}
type IndexedACLs struct {
ACLs ACLs
QueryMeta
}
type ACLPolicy struct {
ETag string
Parent string
Policy *acl.Policy
TTL time.Duration
QueryMeta
}
// msgpackHandle is a shared handle for encoding/decoding of structs
var msgpackHandle = &codec.MsgpackHandle{}

View file

@ -17,6 +17,7 @@ All endpoints fall into one of several categories:
* catalog - Manages nodes and services
* health - Manages health checks
* session - Session manipulation
* acl - ACL creations and management
* status - Consul system status
* internal - Internal APIs. Purposely undocumented, subject to change.
@ -85,6 +86,14 @@ By default, the output of all HTTP API requests return minimized JSON with all
whitespace removed. By adding "?pretty" to the HTTP request URL,
formatted JSON will be returned.
## ACLs
Several endpoints in Consul use or require ACL tokens to operate. An agent
can be configured to use a default token in requests using the `acl_token`
configuration option. However, the token can also be specified per-request
by using the "?token=" query parameter. This will take precedence over the
default token.
## KV
The KV endpoint is used to expose a simple key/value store. This can be used
@ -99,7 +108,8 @@ are all supported. It is important to note that each datacenter has its
own K/V store, and that there is no replication between datacenters.
By default the datacenter of the agent is queried, however the dc can
be provided using the "?dc=" query parameter. If a client wants to write
to all Datacenters, one request per datacenter must be made.
to all Datacenters, one request per datacenter must be made. The KV endpoint
supports the use of ACL tokens.
### GET Method
@ -1039,6 +1049,145 @@ It returns a JSON body like this:
This endpoint supports blocking queries and all consistency modes.
## ACL
The ACL endpoints are used to create, update, destroy and query ACL tokens.
The following endpoints are supported:
* /v1/acl/create: Creates a new token with policy
* /v1/acl/update: Update the policy of a token
* /v1/acl/destroy/\<id\>: Destroys a given token
* /v1/acl/info/\<id\>: Queries the policy of a given token
* /v1/acl/clone/\<id\>: Creates a new token by cloning an existing token
* /v1/acl/list: Lists all the active tokens
### /v1/acl/create
The create endpoint is used to make a new token. A token has a name,
type, and a set of ACL rules. The name is opaque to Consul, and type
is either "client" or "management". A management token is effectively
like a root user, and has the ability to perform any action including
creating, modifying, and deleting ACLs. A client token can only perform
actions as permitted by the rules associated, and may never manage ACLs.
This means the request to this endpoint must be made with a management
token.
In any Consul cluster, only a single datacenter is authoritative for ACLs, so
all requests are automatically routed to that datacenter regardless
of the agent that the request is made to.
The create endpoint expects a JSON request body to be PUT. The request
body must look like:
{
"Name": "my-app-token",
"Type": "client",
"Rules": "",
}
None of the fields are mandatory, and in fact no body needs to be PUT
if the defaults are to be used. The `Name` and `Rules` default to being
blank, and the `Type` defaults to "client". The format of `Rules` is
[documented here](/docs/internals/acl.html).
The return code is 200 on success, along with a body like:
{"ID":"adf4238a-882b-9ddc-4a9d-5b6758e4159e"}
This is used to provide the ID of the newly created ACL token.
### /v1/acl/update
The update endpoint is used to modify the policy for a given
ACL token. It is very similar to the create endpoint, however
instead of generating a new token ID, the `ID` field must be
provided. Requests to this endpoint must be made with a management
token.
In any Consul cluster, only a single datacenter is authoritative for ACLs, so
all requests are automatically routed to that datacenter regardless
of the agent that the request is made to.
The update endpoint expects a JSON request body to be PUT. The request
body must look like:
{
"ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e"
"Name": "my-app-token-updated",
"Type": "client",
"Rules": "# New Rules",
}
Only the `ID` field is mandatory, the other fields provide defaults.
The `Name` and `Rules` default to being blank, and the `Type` defaults to "client".
The format of `Rules` is [documented here](/docs/internals/acl.html).
The return code is 200 on success.
### /v1/acl/destroy/\<id\>
The destroy endpoint is hit with a PUT and destroys the given ACL token.
The request is automatically routed to the authoritative ACL datacenter.
The token being destroyed must be provided after the slash, and requests
to the endpoint must be made with a management token.
The return code is 200 on success.
### /v1/acl/info/\<id\>
This endpoint is hit with a GET and returns the token information
by ID. All requests are routed to the authoritative ACL datacenter
The token being queried must be provided after the slash.
It returns a JSON body like this:
[
{
"CreateIndex":3,
"ModifyIndex":3,
"ID":"8f246b77-f3e1-ff88-5b48-8ec93abf3e05",
"Name":"Client Token",
"Type":"client",
"Rules":"..."
}
]
If the session is not found, null is returned instead of a JSON list.
### /v1/acl/clone/\<id\>
The clone endpoint is hit with a PUT and returns a token ID that
is cloned from an existing token. This allows a token to serve
as a template for others, making it simple to generate new tokens
without complex rule management. The source token must be provided
after the slash. Requests to this endpoint require a management token.
The return code is 200 on success, along with a body like:
{"ID":"adf4238a-882b-9ddc-4a9d-5b6758e4159e"}
This is used to provide the ID of the newly created ACL token.
### /v1/acl/list
The list endpoint is hit with a GET and lists all the active
ACL tokens. This is a privileged endpoint, and requires a
management token.
It returns a JSON body like this:
[
{
"CreateIndex":3,
"ModifyIndex":3,
"ID":"8f246b77-f3e1-ff88-5b48-8ec93abf3e05",
"Name":"Client Token",
"Type":"client",
"Rules":"..."
},
...
]
## Status
The Status endpoints are used to get information about the status

View file

@ -284,6 +284,38 @@ definitions support being updated during a reload.
will not make use of TLS for outgoing connections. This applies to clients and servers,
as both will make outgoing connections.
* `acl_datacenter` - Only used by servers. This designates the datacenter which
is authoritative for ACL information. It must be provided to enable ACLs.
All servers and datacenters must agree on the ACL datacenter.
* `acl_token` - When provided, the agent will use this token when making requests
to the Consul servers. Clients can override this token on a per-request basis
by providing the ?token parameter. When not provided, the empty token is used
which maps to the 'anonymous' ACL policy.
* `acl_master_token` - Only used for servers in the `acl_datacenter`. This token
will be created if it does not exist with management level permissions. It allows
operators to bootstrap the ACL system with a token ID that is well-known.
* `acl_default_policy` - Either "allow" or "deny", defaults to "allow". The
default policy controls the behavior of a token when there is no matching
rule. In "allow" mode, ACLs are a blacklist: any operation not specifically
prohibited is allowed. In "deny" mode, ACLs are a whilelist: any operation not
specifically allowed is blocked.
* `acl_down_policy` - Either "allow", "deny" or "extend-cache" which is the
default. In the case that the policy for a token cannot be read from the
`acl_datacenter` or leader node, the down policy is applied. In "allow" mode,
all actions are permitted, "deny" restricts all operations, and "extend-cache"
allows any cached ACLs to be used, ignoring their TTL values. If a non-cached
ACL is used, "extend-cache" acts like "deny".
* `acl_ttl` - Used to control Time-To-Live caching of ACLs. By default this
is 30 seconds. This setting has a major performance impact: reducing it will
cause more frequent refreshes, while increasing it reduces the number of caches.
However, because the caches are not actively invalidated, ACL policy may be stale
up to the TTL value.
## Ports Used
Consul requires up to 5 different ports to work properly, some requiring

View 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.

View file

@ -38,6 +38,10 @@
<a href="/docs/internals/sessions.html">Sessions</a>
</li>
<li<%= sidebar_current("docs-internals-acl") %>>
<a href="/docs/internals/acl.html">ACLs</a>
</li>
<li<%= sidebar_current("docs-internals-security") %>>
<a href="/docs/internals/security.html">Security Model</a>
</li>