Introduce Code Policy validation via sentinel, with a noop implementation

This commit is contained in:
Preetha Appan 2017-09-14 14:31:01 -05:00
parent 69a088ca85
commit 8394ad08db
20 changed files with 373 additions and 155 deletions

View File

@ -2,6 +2,7 @@ package acl
import ( import (
"github.com/armon/go-radix" "github.com/armon/go-radix"
"github.com/hashicorp/consul/sentinel"
) )
var ( var (
@ -17,6 +18,10 @@ var (
manageAll ACL manageAll ACL
) )
// DefaultPolicyEnforcementLevel will be used if the user leaves the level
// blank when configuring an ACL.
const DefaultPolicyEnforcementLevel = "hard-mandatory"
func init() { func init() {
// Setup the singletons // Setup the singletons
allowAll = &StaticACL{ allowAll = &StaticACL{
@ -59,7 +64,7 @@ type ACL interface {
KeyRead(string) bool KeyRead(string) bool
// KeyWrite checks for permission to write a given key // KeyWrite checks for permission to write a given key
KeyWrite(string) bool KeyWrite(string, sentinel.ScopeFn) bool
// KeyWritePrefix checks for permission to write to an // KeyWritePrefix checks for permission to write to an
// entire key prefix. This means there must be no sub-policies // entire key prefix. This means there must be no sub-policies
@ -78,7 +83,7 @@ type ACL interface {
// NodeWrite checks for permission to create or update (register) a // NodeWrite checks for permission to create or update (register) a
// given node. // given node.
NodeWrite(string) bool NodeWrite(string, sentinel.ScopeFn) bool
// OperatorRead determines if the read-only Consul operator functions // OperatorRead determines if the read-only Consul operator functions
// can be used. // can be used.
@ -101,7 +106,7 @@ type ACL interface {
// ServiceWrite checks for permission to create or update a given // ServiceWrite checks for permission to create or update a given
// service // service
ServiceWrite(string) bool ServiceWrite(string, sentinel.ScopeFn) bool
// SessionRead checks for permission to read sessions for a given node. // SessionRead checks for permission to read sessions for a given node.
SessionRead(string) bool SessionRead(string) bool
@ -150,7 +155,7 @@ func (s *StaticACL) KeyRead(string) bool {
return s.defaultAllow return s.defaultAllow
} }
func (s *StaticACL) KeyWrite(string) bool { func (s *StaticACL) KeyWrite(string, sentinel.ScopeFn) bool {
return s.defaultAllow return s.defaultAllow
} }
@ -170,7 +175,7 @@ func (s *StaticACL) NodeRead(string) bool {
return s.defaultAllow return s.defaultAllow
} }
func (s *StaticACL) NodeWrite(string) bool { func (s *StaticACL) NodeWrite(string, sentinel.ScopeFn) bool {
return s.defaultAllow return s.defaultAllow
} }
@ -194,7 +199,7 @@ func (s *StaticACL) ServiceRead(string) bool {
return s.defaultAllow return s.defaultAllow
} }
func (s *StaticACL) ServiceWrite(string) bool { func (s *StaticACL) ServiceWrite(string, sentinel.ScopeFn) bool {
return s.defaultAllow return s.defaultAllow
} }
@ -239,6 +244,16 @@ func RootACL(id string) ACL {
} }
} }
// PolicyRule binds a regular ACL policy along with an optional piece of
// code to execute.
type PolicyRule struct {
// aclPolicy is used for simple acl rules(allow/deny/manage)
aclPolicy string
// sentinelPolicy has the code part of a policy
sentinelPolicy Sentinel
}
// PolicyACL is used to wrap a set of ACL policies to provide // PolicyACL is used to wrap a set of ACL policies to provide
// the ACL interface. // the ACL interface.
type PolicyACL struct { type PolicyACL struct {
@ -246,6 +261,10 @@ type PolicyACL struct {
// no matching rule. // no matching rule.
parent ACL parent ACL
// sentinel is an interface for validating and executing sentinel code
// policies.
sentinel sentinel.Evaluator
// agentRules contains the agent policies // agentRules contains the agent policies
agentRules *radix.Tree agentRules *radix.Tree
@ -278,7 +297,7 @@ type PolicyACL struct {
// New is used to construct a policy based ACL from a set of policies // New is used to construct a policy based ACL from a set of policies
// and a parent policy to resolve missing cases. // and a parent policy to resolve missing cases.
func New(parent ACL, policy *Policy) (*PolicyACL, error) { func New(parent ACL, policy *Policy, sentinel sentinel.Evaluator) (*PolicyACL, error) {
p := &PolicyACL{ p := &PolicyACL{
parent: parent, parent: parent,
agentRules: radix.New(), agentRules: radix.New(),
@ -288,6 +307,7 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) {
sessionRules: radix.New(), sessionRules: radix.New(),
eventRules: radix.New(), eventRules: radix.New(),
preparedQueryRules: radix.New(), preparedQueryRules: radix.New(),
sentinel: sentinel,
} }
// Load the agent policy // Load the agent policy
@ -297,17 +317,29 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) {
// Load the key policy // Load the key policy
for _, kp := range policy.Keys { for _, kp := range policy.Keys {
p.keyRules.Insert(kp.Prefix, kp.Policy) policyRule := PolicyRule{
aclPolicy: kp.Policy,
sentinelPolicy: kp.Sentinel,
}
p.keyRules.Insert(kp.Prefix, policyRule)
} }
// Load the node policy // Load the node policy
for _, np := range policy.Nodes { for _, np := range policy.Nodes {
p.nodeRules.Insert(np.Name, np.Policy) policyRule := PolicyRule{
aclPolicy: np.Policy,
sentinelPolicy: np.Sentinel,
}
p.nodeRules.Insert(np.Name, policyRule)
} }
// Load the service policy // Load the service policy
for _, sp := range policy.Services { for _, sp := range policy.Services {
p.serviceRules.Insert(sp.Name, sp.Policy) policyRule := PolicyRule{
aclPolicy: sp.Policy,
sentinelPolicy: sp.Sentinel,
}
p.serviceRules.Insert(sp.Name, policyRule)
} }
// Load the session policy // Load the session policy
@ -421,7 +453,8 @@ func (p *PolicyACL) KeyRead(key string) bool {
// Look for a matching rule // Look for a matching rule
_, rule, ok := p.keyRules.LongestPrefix(key) _, rule, ok := p.keyRules.LongestPrefix(key)
if ok { if ok {
switch rule.(string) { pr := rule.(PolicyRule)
switch pr.aclPolicy {
case PolicyRead, PolicyWrite: case PolicyRead, PolicyWrite:
return true return true
default: default:
@ -434,27 +467,28 @@ func (p *PolicyACL) KeyRead(key string) bool {
} }
// KeyWrite returns if a key is allowed to be written // KeyWrite returns if a key is allowed to be written
func (p *PolicyACL) KeyWrite(key string) bool { func (p *PolicyACL) KeyWrite(key string, scope sentinel.ScopeFn) bool {
// Look for a matching rule // Look for a matching rule
_, rule, ok := p.keyRules.LongestPrefix(key) _, rule, ok := p.keyRules.LongestPrefix(key)
if ok { if ok {
switch rule.(string) { pr := rule.(PolicyRule)
switch pr.aclPolicy {
case PolicyWrite: case PolicyWrite:
return true return p.executeCodePolicy(&pr.sentinelPolicy, scope)
default: default:
return false return false
} }
} }
// No matching rule, use the parent. // No matching rule, use the parent.
return p.parent.KeyWrite(key) return p.parent.KeyWrite(key, scope)
} }
// KeyWritePrefix returns if a prefix is allowed to be written // KeyWritePrefix returns if a prefix is allowed to be written
func (p *PolicyACL) KeyWritePrefix(prefix string) bool { func (p *PolicyACL) KeyWritePrefix(prefix string) bool {
// Look for a matching rule that denies // Look for a matching rule that denies
_, rule, ok := p.keyRules.LongestPrefix(prefix) _, rule, ok := p.keyRules.LongestPrefix(prefix)
if ok && rule.(string) != PolicyWrite { if ok && rule.(PolicyRule).aclPolicy != PolicyWrite {
return false return false
} }
@ -462,7 +496,7 @@ func (p *PolicyACL) KeyWritePrefix(prefix string) bool {
deny := false deny := false
p.keyRules.WalkPrefix(prefix, func(path string, rule interface{}) bool { p.keyRules.WalkPrefix(prefix, func(path string, rule interface{}) bool {
// We have a rule to prevent a write in a sub-directory! // We have a rule to prevent a write in a sub-directory!
if rule.(string) != PolicyWrite { if rule.(PolicyRule).aclPolicy != PolicyWrite {
deny = true deny = true
return true return true
} }
@ -522,7 +556,8 @@ func (p *PolicyACL) NodeRead(name string) bool {
_, rule, ok := p.nodeRules.LongestPrefix(name) _, rule, ok := p.nodeRules.LongestPrefix(name)
if ok { if ok {
switch rule { pr := rule.(PolicyRule)
switch pr.aclPolicy {
case PolicyRead, PolicyWrite: case PolicyRead, PolicyWrite:
return true return true
default: default:
@ -535,21 +570,22 @@ func (p *PolicyACL) NodeRead(name string) bool {
} }
// NodeWrite checks if writing (registering) a node is allowed // NodeWrite checks if writing (registering) a node is allowed
func (p *PolicyACL) NodeWrite(name string) bool { func (p *PolicyACL) NodeWrite(name string, scope sentinel.ScopeFn) bool {
// Check for an exact rule or catch-all // Check for an exact rule or catch-all
_, rule, ok := p.nodeRules.LongestPrefix(name) _, rule, ok := p.nodeRules.LongestPrefix(name)
if ok { if ok {
switch rule { pr := rule.(PolicyRule)
switch pr.aclPolicy {
case PolicyWrite: case PolicyWrite:
return true return p.executeCodePolicy(&pr.sentinelPolicy, scope)
default: default:
return false return false
} }
} }
// No matching rule, use the parent. // No matching rule, use the parent.
return p.parent.NodeWrite(name) return p.parent.NodeWrite(name, scope)
} }
// OperatorWrite determines if the state-changing operator functions are // OperatorWrite determines if the state-changing operator functions are
@ -603,9 +639,9 @@ func (p *PolicyACL) PreparedQueryWrite(prefix string) bool {
func (p *PolicyACL) ServiceRead(name string) bool { func (p *PolicyACL) ServiceRead(name string) bool {
// Check for an exact rule or catch-all // Check for an exact rule or catch-all
_, rule, ok := p.serviceRules.LongestPrefix(name) _, rule, ok := p.serviceRules.LongestPrefix(name)
if ok { if ok {
switch rule { pr := rule.(PolicyRule)
switch pr.aclPolicy {
case PolicyRead, PolicyWrite: case PolicyRead, PolicyWrite:
return true return true
default: default:
@ -618,21 +654,21 @@ func (p *PolicyACL) ServiceRead(name string) bool {
} }
// ServiceWrite checks if writing (registering) a service is allowed // ServiceWrite checks if writing (registering) a service is allowed
func (p *PolicyACL) ServiceWrite(name string) bool { func (p *PolicyACL) ServiceWrite(name string, scope sentinel.ScopeFn) bool {
// Check for an exact rule or catch-all // Check for an exact rule or catch-all
_, rule, ok := p.serviceRules.LongestPrefix(name) _, rule, ok := p.serviceRules.LongestPrefix(name)
if ok { if ok {
switch rule { pr := rule.(PolicyRule)
switch pr.aclPolicy {
case PolicyWrite: case PolicyWrite:
return true return p.executeCodePolicy(&pr.sentinelPolicy, scope)
default: default:
return false return false
} }
} }
// No matching rule, use the parent. // No matching rule, use the parent.
return p.parent.ServiceWrite(name) return p.parent.ServiceWrite(name, scope)
} }
// SessionRead checks for permission to read sessions for a given node. // SessionRead checks for permission to read sessions for a given node.
@ -670,3 +706,22 @@ func (p *PolicyACL) SessionWrite(node string) bool {
// No matching rule, use the parent. // No matching rule, use the parent.
return p.parent.SessionWrite(node) return p.parent.SessionWrite(node)
} }
// executeCodePolicy will run the associated code policy if code policies are
// enabled.
func (p *PolicyACL) executeCodePolicy(policy *Sentinel, scope sentinel.ScopeFn) bool {
if p.sentinel == nil {
return true
}
if policy.Code == "" || scope == nil {
return true
}
enforcement := policy.EnforcementLevel
if enforcement == "" {
enforcement = DefaultPolicyEnforcementLevel
}
return p.sentinel.Execute(policy.Code, enforcement, scope())
}

View File

@ -56,7 +56,7 @@ func TestStaticACL(t *testing.T) {
if !all.KeyRead("foobar") { if !all.KeyRead("foobar") {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !all.KeyWrite("foobar") { if !all.KeyWrite("foobar", nil) {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !all.KeyringRead() { if !all.KeyringRead() {
@ -68,7 +68,7 @@ func TestStaticACL(t *testing.T) {
if !all.NodeRead("foobar") { if !all.NodeRead("foobar") {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !all.NodeWrite("foobar") { if !all.NodeWrite("foobar", nil) {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !all.OperatorRead() { if !all.OperatorRead() {
@ -86,7 +86,7 @@ func TestStaticACL(t *testing.T) {
if !all.ServiceRead("foobar") { if !all.ServiceRead("foobar") {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !all.ServiceWrite("foobar") { if !all.ServiceWrite("foobar", nil) {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !all.SessionRead("foobar") { if !all.SessionRead("foobar") {
@ -126,7 +126,7 @@ func TestStaticACL(t *testing.T) {
if none.KeyRead("foobar") { if none.KeyRead("foobar") {
t.Fatalf("should not allow") t.Fatalf("should not allow")
} }
if none.KeyWrite("foobar") { if none.KeyWrite("foobar", nil) {
t.Fatalf("should not allow") t.Fatalf("should not allow")
} }
if none.KeyringRead() { if none.KeyringRead() {
@ -138,7 +138,7 @@ func TestStaticACL(t *testing.T) {
if none.NodeRead("foobar") { if none.NodeRead("foobar") {
t.Fatalf("should not allow") t.Fatalf("should not allow")
} }
if none.NodeWrite("foobar") { if none.NodeWrite("foobar", nil) {
t.Fatalf("should not allow") t.Fatalf("should not allow")
} }
if none.OperatorRead() { if none.OperatorRead() {
@ -156,7 +156,7 @@ func TestStaticACL(t *testing.T) {
if none.ServiceRead("foobar") { if none.ServiceRead("foobar") {
t.Fatalf("should not allow") t.Fatalf("should not allow")
} }
if none.ServiceWrite("foobar") { if none.ServiceWrite("foobar", nil) {
t.Fatalf("should not allow") t.Fatalf("should not allow")
} }
if none.SessionRead("foobar") { if none.SessionRead("foobar") {
@ -190,7 +190,7 @@ func TestStaticACL(t *testing.T) {
if !manage.KeyRead("foobar") { if !manage.KeyRead("foobar") {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !manage.KeyWrite("foobar") { if !manage.KeyWrite("foobar", nil) {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !manage.KeyringRead() { if !manage.KeyringRead() {
@ -202,7 +202,7 @@ func TestStaticACL(t *testing.T) {
if !manage.NodeRead("foobar") { if !manage.NodeRead("foobar") {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !manage.NodeWrite("foobar") { if !manage.NodeWrite("foobar", nil) {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !manage.OperatorRead() { if !manage.OperatorRead() {
@ -220,7 +220,7 @@ func TestStaticACL(t *testing.T) {
if !manage.ServiceRead("foobar") { if !manage.ServiceRead("foobar") {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !manage.ServiceWrite("foobar") { if !manage.ServiceWrite("foobar", nil) {
t.Fatalf("should allow") t.Fatalf("should allow")
} }
if !manage.SessionRead("foobar") { if !manage.SessionRead("foobar") {
@ -306,7 +306,7 @@ func TestPolicyACL(t *testing.T) {
}, },
}, },
} }
acl, err := New(all, policy) acl, err := New(all, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -330,7 +330,7 @@ func TestPolicyACL(t *testing.T) {
if c.read != acl.KeyRead(c.inp) { if c.read != acl.KeyRead(c.inp) {
t.Fatalf("Read fail: %#v", c) t.Fatalf("Read fail: %#v", c)
} }
if c.write != acl.KeyWrite(c.inp) { if c.write != acl.KeyWrite(c.inp, nil) {
t.Fatalf("Write fail: %#v", c) t.Fatalf("Write fail: %#v", c)
} }
if c.writePrefix != acl.KeyWritePrefix(c.inp) { if c.writePrefix != acl.KeyWritePrefix(c.inp) {
@ -357,7 +357,7 @@ func TestPolicyACL(t *testing.T) {
if c.read != acl.ServiceRead(c.inp) { if c.read != acl.ServiceRead(c.inp) {
t.Fatalf("Read fail: %#v", c) t.Fatalf("Read fail: %#v", c)
} }
if c.write != acl.ServiceWrite(c.inp) { if c.write != acl.ServiceWrite(c.inp, nil) {
t.Fatalf("Write fail: %#v", c) t.Fatalf("Write fail: %#v", c)
} }
} }
@ -444,7 +444,7 @@ func TestPolicyACL_Parent(t *testing.T) {
}, },
}, },
} }
root, err := New(deny, policyRoot) root, err := New(deny, policyRoot, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -477,7 +477,7 @@ func TestPolicyACL_Parent(t *testing.T) {
}, },
}, },
} }
acl, err := New(root, policy) acl, err := New(root, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -499,7 +499,7 @@ func TestPolicyACL_Parent(t *testing.T) {
if c.read != acl.KeyRead(c.inp) { if c.read != acl.KeyRead(c.inp) {
t.Fatalf("Read fail: %#v", c) t.Fatalf("Read fail: %#v", c)
} }
if c.write != acl.KeyWrite(c.inp) { if c.write != acl.KeyWrite(c.inp, nil) {
t.Fatalf("Write fail: %#v", c) t.Fatalf("Write fail: %#v", c)
} }
if c.writePrefix != acl.KeyWritePrefix(c.inp) { if c.writePrefix != acl.KeyWritePrefix(c.inp) {
@ -523,7 +523,7 @@ func TestPolicyACL_Parent(t *testing.T) {
if c.read != acl.ServiceRead(c.inp) { if c.read != acl.ServiceRead(c.inp) {
t.Fatalf("Read fail: %#v", c) t.Fatalf("Read fail: %#v", c)
} }
if c.write != acl.ServiceWrite(c.inp) { if c.write != acl.ServiceWrite(c.inp, nil) {
t.Fatalf("Write fail: %#v", c) t.Fatalf("Write fail: %#v", c)
} }
} }
@ -585,7 +585,7 @@ func TestPolicyACL_Agent(t *testing.T) {
}, },
}, },
} }
root, err := New(deny, policyRoot) root, err := New(deny, policyRoot, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -610,7 +610,7 @@ func TestPolicyACL_Agent(t *testing.T) {
}, },
}, },
} }
acl, err := New(root, policy) acl, err := New(root, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -659,7 +659,7 @@ func TestPolicyACL_Keyring(t *testing.T) {
{PolicyDeny, false, false}, {PolicyDeny, false, false},
} }
for _, c := range cases { for _, c := range cases {
acl, err := New(DenyAll(), &Policy{Keyring: c.inp}) acl, err := New(DenyAll(), &Policy{Keyring: c.inp}, nil)
if err != nil { if err != nil {
t.Fatalf("bad: %s", err) t.Fatalf("bad: %s", err)
} }
@ -685,7 +685,7 @@ func TestPolicyACL_Operator(t *testing.T) {
{PolicyDeny, false, false}, {PolicyDeny, false, false},
} }
for _, c := range cases { for _, c := range cases {
acl, err := New(DenyAll(), &Policy{Operator: c.inp}) acl, err := New(DenyAll(), &Policy{Operator: c.inp}, nil)
if err != nil { if err != nil {
t.Fatalf("bad: %s", err) t.Fatalf("bad: %s", err)
} }
@ -720,7 +720,7 @@ func TestPolicyACL_Node(t *testing.T) {
}, },
}, },
} }
root, err := New(deny, policyRoot) root, err := New(deny, policyRoot, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -745,7 +745,7 @@ func TestPolicyACL_Node(t *testing.T) {
}, },
}, },
} }
acl, err := New(root, policy) acl, err := New(root, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -775,7 +775,7 @@ func TestPolicyACL_Node(t *testing.T) {
if c.read != acl.NodeRead(c.inp) { if c.read != acl.NodeRead(c.inp) {
t.Fatalf("Read fail: %#v", c) t.Fatalf("Read fail: %#v", c)
} }
if c.write != acl.NodeWrite(c.inp) { if c.write != acl.NodeWrite(c.inp, nil) {
t.Fatalf("Write fail: %#v", c) t.Fatalf("Write fail: %#v", c)
} }
} }
@ -803,7 +803,7 @@ func TestPolicyACL_Session(t *testing.T) {
}, },
}, },
} }
root, err := New(deny, policyRoot) root, err := New(deny, policyRoot, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -828,7 +828,7 @@ func TestPolicyACL_Session(t *testing.T) {
}, },
}, },
} }
acl, err := New(root, policy) acl, err := New(root, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }

View File

@ -4,6 +4,7 @@ import (
"crypto/md5" "crypto/md5"
"fmt" "fmt"
"github.com/hashicorp/consul/sentinel"
"github.com/hashicorp/golang-lru" "github.com/hashicorp/golang-lru"
) )
@ -24,10 +25,11 @@ type Cache struct {
aclCache *lru.TwoQueueCache // Cache id -> acl aclCache *lru.TwoQueueCache // Cache id -> acl
policyCache *lru.TwoQueueCache // Cache policy -> acl policyCache *lru.TwoQueueCache // Cache policy -> acl
ruleCache *lru.TwoQueueCache // Cache rules -> policy ruleCache *lru.TwoQueueCache // Cache rules -> policy
sentinel sentinel.Evaluator
} }
// NewCache constructs a new policy and ACL cache of a given size // NewCache constructs a new policy and ACL cache of a given size
func NewCache(size int, faultfn FaultFunc) (*Cache, error) { func NewCache(size int, faultfn FaultFunc, sentinel sentinel.Evaluator) (*Cache, error) {
if size <= 0 { if size <= 0 {
return nil, fmt.Errorf("Must provide positive cache size") return nil, fmt.Errorf("Must provide positive cache size")
} }
@ -52,6 +54,7 @@ func NewCache(size int, faultfn FaultFunc) (*Cache, error) {
aclCache: ac, aclCache: ac,
policyCache: pc, policyCache: pc,
ruleCache: rc, ruleCache: rc,
sentinel: sentinel,
} }
return c, nil return c, nil
} }
@ -69,7 +72,7 @@ func (c *Cache) getPolicy(id, rules string) (*Policy, error) {
if ok { if ok {
return raw.(*Policy), nil return raw.(*Policy), nil
} }
policy, err := Parse(rules) policy, err := Parse(rules, c.sentinel)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -149,7 +152,7 @@ func (c *Cache) GetACL(id string) (ACL, error) {
} }
// Compile the ACL // Compile the ACL
acl, err := New(parent, policy) acl, err := New(parent, policy, c.sentinel)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -5,7 +5,7 @@ import (
) )
func TestCache_GetPolicy(t *testing.T) { func TestCache_GetPolicy(t *testing.T) {
c, err := NewCache(2, nil) c, err := NewCache(2, nil, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -62,7 +62,7 @@ func TestCache_GetACL(t *testing.T) {
return "deny", policies[id], nil return "deny", policies[id], nil
} }
c, err := NewCache(2, faultfn) c, err := NewCache(2, faultfn, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -125,7 +125,7 @@ func TestCache_ClearACL(t *testing.T) {
return "deny", policies[id], nil return "deny", policies[id], nil
} }
c, err := NewCache(16, faultfn) c, err := NewCache(16, faultfn, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -160,7 +160,7 @@ func TestCache_Purge(t *testing.T) {
return "deny", policies[id], nil return "deny", policies[id], nil
} }
c, err := NewCache(16, faultfn) c, err := NewCache(16, faultfn, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -192,7 +192,7 @@ func TestCache_GetACLPolicy(t *testing.T) {
faultfn := func(id string) (string, string, error) { faultfn := func(id string) (string, string, error) {
return "deny", policies[id], nil return "deny", policies[id], nil
} }
c, err := NewCache(16, faultfn) c, err := NewCache(16, faultfn, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -245,7 +245,7 @@ func TestCache_GetACL_Parent(t *testing.T) {
return "", "", nil return "", "", nil
} }
c, err := NewCache(16, faultfn) c, err := NewCache(16, faultfn, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -276,7 +276,7 @@ func TestCache_GetACL_ParentCache(t *testing.T) {
return "", "", nil return "", "", nil
} }
c, err := NewCache(16, faultfn) c, err := NewCache(16, faultfn, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }

View File

@ -3,6 +3,7 @@ package acl
import ( import (
"fmt" "fmt"
"github.com/hashicorp/consul/sentinel"
"github.com/hashicorp/hcl" "github.com/hashicorp/hcl"
) )
@ -27,6 +28,12 @@ type Policy struct {
Operator string `hcl:"operator"` Operator string `hcl:"operator"`
} }
// Sentinel defines a snippet of Sentinel code that can be attached to a policy.
type Sentinel struct {
Code string
EnforcementLevel string
}
// AgentPolicy represents a policy for working with agent endpoints on nodes // AgentPolicy represents a policy for working with agent endpoints on nodes
// with specific name prefixes. // with specific name prefixes.
type AgentPolicy struct { type AgentPolicy struct {
@ -42,6 +49,7 @@ func (a *AgentPolicy) GoString() string {
type KeyPolicy struct { type KeyPolicy struct {
Prefix string `hcl:",key"` Prefix string `hcl:",key"`
Policy string Policy string
Sentinel Sentinel
} }
func (k *KeyPolicy) GoString() string { func (k *KeyPolicy) GoString() string {
@ -52,6 +60,7 @@ func (k *KeyPolicy) GoString() string {
type NodePolicy struct { type NodePolicy struct {
Name string `hcl:",key"` Name string `hcl:",key"`
Policy string Policy string
Sentinel Sentinel
} }
func (n *NodePolicy) GoString() string { func (n *NodePolicy) GoString() string {
@ -62,6 +71,7 @@ func (n *NodePolicy) GoString() string {
type ServicePolicy struct { type ServicePolicy struct {
Name string `hcl:",key"` Name string `hcl:",key"`
Policy string Policy string
Sentinel Sentinel
} }
func (s *ServicePolicy) GoString() string { func (s *ServicePolicy) GoString() string {
@ -113,10 +123,38 @@ func isPolicyValid(policy string) bool {
} }
} }
// isSentinelValid makes sure the given sentinel block is valid, and will skip
// out if the evaluator is nil.
func isSentinelValid(sentinel sentinel.Evaluator, basicPolicy string, sp Sentinel) error {
// TODO (slackpad) - Need a unit test for this.
// Sentinel not enabled at all, or for this policy.
if sentinel == nil {
return nil
}
if sp.Code == "" {
return nil
}
// We only allow sentinel code on write policies at this time.
if basicPolicy != PolicyWrite {
return fmt.Errorf("code is only allowed for write policies")
}
// Validate the sentinel parts.
switch sp.EnforcementLevel {
case "", "soft-mandatory", "hard-mandatory":
// OK
default:
return fmt.Errorf("unsupported enforcement level %q", sp.EnforcementLevel)
}
return sentinel.Compile(sp.Code)
}
// Parse is used to parse the specified ACL rules into an // Parse is used to parse the specified ACL rules into an
// intermediary set of policies, before being compiled into // intermediary set of policies, before being compiled into
// the ACL // the ACL
func Parse(rules string) (*Policy, error) { func Parse(rules string, sentinel sentinel.Evaluator) (*Policy, error) {
// Decode the rules // Decode the rules
p := &Policy{} p := &Policy{}
if rules == "" { if rules == "" {
@ -140,6 +178,9 @@ func Parse(rules string) (*Policy, error) {
if !isPolicyValid(kp.Policy) { if !isPolicyValid(kp.Policy) {
return nil, fmt.Errorf("Invalid key policy: %#v", kp) return nil, fmt.Errorf("Invalid key policy: %#v", kp)
} }
if err := isSentinelValid(sentinel, kp.Policy, kp.Sentinel); err != nil {
return nil, fmt.Errorf("Invalid key Sentinel policy: %#v, got error:%v", kp, err)
}
} }
// Validate the node policies // Validate the node policies
@ -147,6 +188,9 @@ func Parse(rules string) (*Policy, error) {
if !isPolicyValid(np.Policy) { if !isPolicyValid(np.Policy) {
return nil, fmt.Errorf("Invalid node policy: %#v", np) return nil, fmt.Errorf("Invalid node policy: %#v", np)
} }
if err := isSentinelValid(sentinel, np.Policy, np.Sentinel); err != nil {
return nil, fmt.Errorf("Invalid node Sentinel policy: %#v, got error:%v", np, err)
}
} }
// Validate the service policies // Validate the service policies
@ -154,6 +198,9 @@ func Parse(rules string) (*Policy, error) {
if !isPolicyValid(sp.Policy) { if !isPolicyValid(sp.Policy) {
return nil, fmt.Errorf("Invalid service policy: %#v", sp) return nil, fmt.Errorf("Invalid service policy: %#v", sp)
} }
if err := isSentinelValid(sentinel, sp.Policy, sp.Sentinel); err != nil {
return nil, fmt.Errorf("Invalid service Sentinel policy: %#v, got error:%v", sp, err)
}
} }
// Validate the session policies // Validate the session policies

View File

@ -163,7 +163,7 @@ query "bar" {
}, },
} }
out, err := Parse(inp) out, err := Parse(inp, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -344,7 +344,7 @@ func TestACLPolicy_Parse_JSON(t *testing.T) {
}, },
} }
out, err := Parse(inp) out, err := Parse(inp, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -362,7 +362,7 @@ keyring = ""
Keyring: "", Keyring: "",
} }
out, err := Parse(inp) out, err := Parse(inp, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -380,7 +380,7 @@ operator = ""
Operator: "", Operator: "",
} }
out, err := Parse(inp) out, err := Parse(inp, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -403,7 +403,7 @@ func TestACLPolicy_Bad_Policy(t *testing.T) {
`session "" { policy = "nope" }`, `session "" { policy = "nope" }`,
} }
for _, c := range cases { for _, c := range cases {
_, err := Parse(c) _, err := Parse(c, nil)
if err == nil || !strings.Contains(err.Error(), "Invalid") { if err == nil || !strings.Contains(err.Error(), "Invalid") {
t.Fatalf("expected policy error, got: %#v", err) t.Fatalf("expected policy error, got: %#v", err)
} }

View File

@ -91,7 +91,7 @@ func newACLManager(config *config.RuntimeConfig) (*aclManager, error) {
}, },
}, },
} }
master, err := acl.New(acl.DenyAll(), policy) master, err := acl.New(acl.DenyAll(), policy, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -194,7 +194,7 @@ func (m *aclManager) lookupACL(a *Agent, id string) (acl.ACL, error) {
} }
} }
acl, err := acl.New(parent, reply.Policy) acl, err := acl.New(parent, reply.Policy, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -252,14 +252,14 @@ func (a *Agent) vetServiceRegister(token string, service *structs.NodeService) e
} }
// Vet the service itself. // Vet the service itself.
if !rule.ServiceWrite(service.Service) { if !rule.ServiceWrite(service.Service, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
// Vet any service that might be getting overwritten. // Vet any service that might be getting overwritten.
services := a.state.Services() services := a.state.Services()
if existing, ok := services[service.ID]; ok { if existing, ok := services[service.ID]; ok {
if !rule.ServiceWrite(existing.Service) { if !rule.ServiceWrite(existing.Service, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }
@ -282,7 +282,7 @@ func (a *Agent) vetServiceUpdate(token string, serviceID string) error {
// Vet any changes based on the existing services's info. // Vet any changes based on the existing services's info.
services := a.state.Services() services := a.state.Services()
if existing, ok := services[serviceID]; ok { if existing, ok := services[serviceID]; ok {
if !rule.ServiceWrite(existing.Service) { if !rule.ServiceWrite(existing.Service, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} else { } else {
@ -306,11 +306,11 @@ func (a *Agent) vetCheckRegister(token string, check *structs.HealthCheck) error
// Vet the check itself. // Vet the check itself.
if len(check.ServiceName) > 0 { if len(check.ServiceName) > 0 {
if !rule.ServiceWrite(check.ServiceName) { if !rule.ServiceWrite(check.ServiceName, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} else { } else {
if !rule.NodeWrite(a.config.NodeName) { if !rule.NodeWrite(a.config.NodeName, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }
@ -319,11 +319,11 @@ func (a *Agent) vetCheckRegister(token string, check *structs.HealthCheck) error
checks := a.state.Checks() checks := a.state.Checks()
if existing, ok := checks[check.CheckID]; ok { if existing, ok := checks[check.CheckID]; ok {
if len(existing.ServiceName) > 0 { if len(existing.ServiceName) > 0 {
if !rule.ServiceWrite(existing.ServiceName) { if !rule.ServiceWrite(existing.ServiceName, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} else { } else {
if !rule.NodeWrite(a.config.NodeName) { if !rule.NodeWrite(a.config.NodeName, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }
@ -347,11 +347,11 @@ func (a *Agent) vetCheckUpdate(token string, checkID types.CheckID) error {
checks := a.state.Checks() checks := a.state.Checks()
if existing, ok := checks[checkID]; ok { if existing, ok := checks[checkID]; ok {
if len(existing.ServiceName) > 0 { if len(existing.ServiceName) > 0 {
if !rule.ServiceWrite(existing.ServiceName) { if !rule.ServiceWrite(existing.ServiceName, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} else { } else {
if !rule.NodeWrite(a.config.NodeName) { if !rule.NodeWrite(a.config.NodeName, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }

View File

@ -207,7 +207,7 @@ func TestACL_Special_IDs(t *testing.T) {
if !acl.NodeRead("hello") { if !acl.NodeRead("hello") {
t.Fatalf("should be able to read any node") t.Fatalf("should be able to read any node")
} }
if acl.NodeWrite("hello") { if acl.NodeWrite("hello", nil) {
t.Fatalf("should not be able to write any node") t.Fatalf("should not be able to write any node")
} }
} }

View File

@ -637,7 +637,7 @@ func (s *HTTPServer) AgentNodeMaintenance(resp http.ResponseWriter, req *http.Re
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rule != nil && !rule.NodeWrite(s.agent.config.NodeName) { if rule != nil && !rule.NodeWrite(s.agent.config.NodeName, nil) {
return nil, acl.ErrPermissionDenied return nil, acl.ErrPermissionDenied
} }

View File

@ -9,6 +9,8 @@ import (
"github.com/armon/go-metrics" "github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sentinel"
"github.com/hashicorp/golang-lru" "github.com/hashicorp/golang-lru"
) )
@ -102,6 +104,9 @@ type aclCache struct {
// acls is a non-authoritative ACL cache. // acls is a non-authoritative ACL cache.
acls *lru.TwoQueueCache acls *lru.TwoQueueCache
// sentinel is the code engine (can be nil).
sentinel sentinel.Evaluator
// aclPolicyCache is a non-authoritative policy cache. // aclPolicyCache is a non-authoritative policy cache.
policies *lru.TwoQueueCache policies *lru.TwoQueueCache
@ -116,13 +121,14 @@ type aclCache struct {
// newACLCache returns a new non-authoritative cache for ACLs. This is used for // newACLCache returns a new non-authoritative cache for ACLs. This is used for
// performance, and is used inside the ACL datacenter on non-leader servers, and // performance, and is used inside the ACL datacenter on non-leader servers, and
// outside the ACL datacenter everywhere. // outside the ACL datacenter everywhere.
func newACLCache(conf *Config, logger *log.Logger, rpc rpcFn, local acl.FaultFunc) (*aclCache, error) { func newACLCache(conf *Config, logger *log.Logger, rpc rpcFn, local acl.FaultFunc, sentinel sentinel.Evaluator) (*aclCache, error) {
var err error var err error
cache := &aclCache{ cache := &aclCache{
config: conf, config: conf,
logger: logger, logger: logger,
rpc: rpc, rpc: rpc,
local: local, local: local,
sentinel: sentinel,
} }
// Initialize the non-authoritative ACL cache // Initialize the non-authoritative ACL cache
@ -206,7 +212,7 @@ func (c *aclCache) lookupACL(id, authDC string) (acl.ACL, error) {
goto ACL_DOWN goto ACL_DOWN
} }
policy, err := acl.Parse(rules) policy, err := acl.Parse(rules, c.sentinel)
if err != nil { if err != nil {
c.logger.Printf("[DEBUG] consul.acl: Failed to parse policy for replicated ACL: %v", err) c.logger.Printf("[DEBUG] consul.acl: Failed to parse policy for replicated ACL: %v", err)
goto ACL_DOWN goto ACL_DOWN
@ -268,7 +274,7 @@ func (c *aclCache) useACLPolicy(id, authDC string, cached *aclCacheEntry, p *str
} }
// Compile the ACL // Compile the ACL
acl, err := acl.New(parent, p.Policy) acl, err := acl.New(parent, p.Policy, c.sentinel)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -327,7 +333,6 @@ func (f *aclFilter) allowService(service string) bool {
if !f.enforceVersion8 && service == structs.ConsulServiceID { if !f.enforceVersion8 && service == structs.ConsulServiceID {
return true return true
} }
return f.acl.ServiceRead(service) return f.acl.ServiceRead(service)
} }
@ -564,8 +569,7 @@ func (f *aclFilter) filterPreparedQueries(queries *structs.PreparedQueries) {
} }
// filterACL is used to filter results from our service catalog based on the // filterACL is used to filter results from our service catalog based on the
// rules configured for the provided token. The subject is scrubbed and // rules configured for the provided token.
// modified in-place, leaving only resources the token can access.
func (s *Server) filterACL(token string, subj interface{}) error { func (s *Server) filterACL(token string, subj interface{}) error {
// Get the ACL from the token // Get the ACL from the token
acl, err := s.resolveToken(token) acl, err := s.resolveToken(token)
@ -646,11 +650,45 @@ func vetRegisterWithACL(rule acl.ACL, subj *structs.RegisterRequest,
return nil return nil
} }
// This gets called potentially from a few spots so we save it and
// return the structure we made if we have it.
var memo map[string]interface{}
scope := func() map[string]interface{} {
if memo != nil {
return memo
}
node := &api.Node{
ID: string(subj.ID),
Node: subj.Node,
Address: subj.Address,
Datacenter: subj.Datacenter,
TaggedAddresses: subj.TaggedAddresses,
Meta: subj.NodeMeta,
}
var service *api.AgentService
if subj.Service != nil {
service = &api.AgentService{
ID: subj.Service.ID,
Service: subj.Service.Service,
Tags: subj.Service.Tags,
Address: subj.Service.Address,
Port: subj.Service.Port,
EnableTagOverride: subj.Service.EnableTagOverride,
}
}
memo = sentinel.ScopeCatalogUpsert(node, service)
return memo
}
// Vet the node info. This allows service updates to re-post the required // Vet the node info. This allows service updates to re-post the required
// node info for each request without having to have node "write" // node info for each request without having to have node "write"
// privileges. // privileges.
needsNode := ns == nil || subj.ChangesNode(ns.Node) needsNode := ns == nil || subj.ChangesNode(ns.Node)
if needsNode && !rule.NodeWrite(subj.Node) {
if needsNode && !rule.NodeWrite(subj.Node, scope) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
@ -658,13 +696,17 @@ func vetRegisterWithACL(rule acl.ACL, subj *structs.RegisterRequest,
// the given service, and that we can write to any existing service that // the given service, and that we can write to any existing service that
// is being modified by id (if any). // is being modified by id (if any).
if subj.Service != nil { if subj.Service != nil {
if !rule.ServiceWrite(subj.Service.Service) { if !rule.ServiceWrite(subj.Service.Service, scope) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
if ns != nil { if ns != nil {
other, ok := ns.Services[subj.Service.ID] other, ok := ns.Services[subj.Service.ID]
if ok && !rule.ServiceWrite(other.Service) {
// This is effectively a delete, so we DO NOT apply the
// sentinel scope to the service we are overwriting, just
// the regular ACL policy.
if ok && !rule.ServiceWrite(other.Service, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }
@ -693,7 +735,7 @@ func vetRegisterWithACL(rule acl.ACL, subj *structs.RegisterRequest,
// Node-level check. // Node-level check.
if check.ServiceID == "" { if check.ServiceID == "" {
if !rule.NodeWrite(subj.Node) { if !rule.NodeWrite(subj.Node, scope) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
continue continue
@ -718,7 +760,10 @@ func vetRegisterWithACL(rule acl.ACL, subj *structs.RegisterRequest,
return fmt.Errorf("Unknown service '%s' for check '%s'", check.ServiceID, check.CheckID) return fmt.Errorf("Unknown service '%s' for check '%s'", check.ServiceID, check.CheckID)
} }
if !rule.ServiceWrite(other.Service) { // We are only adding a check here, so we don't add the scope,
// since the sentinel policy doesn't apply to adding checks at
// this time.
if !rule.ServiceWrite(other.Service, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }
@ -733,11 +778,15 @@ func vetRegisterWithACL(rule acl.ACL, subj *structs.RegisterRequest,
// be nil; similar for the HealthCheck for the referenced health check. // be nil; similar for the HealthCheck for the referenced health check.
func vetDeregisterWithACL(rule acl.ACL, subj *structs.DeregisterRequest, func vetDeregisterWithACL(rule acl.ACL, subj *structs.DeregisterRequest,
ns *structs.NodeService, nc *structs.HealthCheck) error { ns *structs.NodeService, nc *structs.HealthCheck) error {
// Fast path if ACLs are not enabled. // Fast path if ACLs are not enabled.
if rule == nil { if rule == nil {
return nil return nil
} }
// We don't apply sentinel in this path, since at this time sentinel
// only applies to create and update operations.
// This order must match the code in applyRegister() in fsm.go since it // This order must match the code in applyRegister() in fsm.go since it
// also evaluates things in this order, and will ignore fields based on // also evaluates things in this order, and will ignore fields based on
// this precedence. This lets us also ignore them from an ACL perspective. // this precedence. This lets us also ignore them from an ACL perspective.
@ -745,7 +794,7 @@ func vetDeregisterWithACL(rule acl.ACL, subj *structs.DeregisterRequest,
if ns == nil { if ns == nil {
return fmt.Errorf("Unknown service '%s'", subj.ServiceID) return fmt.Errorf("Unknown service '%s'", subj.ServiceID)
} }
if !rule.ServiceWrite(ns.Service) { if !rule.ServiceWrite(ns.Service, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} else if subj.CheckID != "" { } else if subj.CheckID != "" {
@ -753,16 +802,16 @@ func vetDeregisterWithACL(rule acl.ACL, subj *structs.DeregisterRequest,
return fmt.Errorf("Unknown check '%s'", subj.CheckID) return fmt.Errorf("Unknown check '%s'", subj.CheckID)
} }
if nc.ServiceID != "" { if nc.ServiceID != "" {
if !rule.ServiceWrite(nc.ServiceName) { if !rule.ServiceWrite(nc.ServiceName, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} else { } else {
if !rule.NodeWrite(subj.Node) { if !rule.NodeWrite(subj.Node, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }
} else { } else {
if !rule.NodeWrite(subj.Node) { if !rule.NodeWrite(subj.Node, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }

View File

@ -107,7 +107,7 @@ func aclApplyInternal(srv *Server, args *structs.ACLRequest, reply *string) erro
} }
// Validate the rules compile // Validate the rules compile
_, err := acl.Parse(args.ACL.Rules) _, err := acl.Parse(args.ACL.Rules, srv.sentinel)
if err != nil { if err != nil {
return fmt.Errorf("ACL rule compilation failed: %v", err) return fmt.Errorf("ACL rule compilation failed: %v", err)
} }

View File

@ -793,11 +793,11 @@ func TestACL_filterHealthChecks(t *testing.T) {
service "foo" { service "foo" {
policy = "read" policy = "read"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err := acl.New(acl.DenyAll(), policy) perms, err := acl.New(acl.DenyAll(), policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -827,11 +827,11 @@ service "foo" {
node "node1" { node "node1" {
policy = "read" policy = "read"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err = acl.New(perms, policy) perms, err = acl.New(perms, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -918,11 +918,11 @@ func TestACL_filterServiceNodes(t *testing.T) {
service "foo" { service "foo" {
policy = "read" policy = "read"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err := acl.New(acl.DenyAll(), policy) perms, err := acl.New(acl.DenyAll(), policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -952,11 +952,11 @@ service "foo" {
node "node1" { node "node1" {
policy = "read" policy = "read"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err = acl.New(perms, policy) perms, err = acl.New(perms, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1024,11 +1024,11 @@ func TestACL_filterNodeServices(t *testing.T) {
service "foo" { service "foo" {
policy = "read" policy = "read"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err := acl.New(acl.DenyAll(), policy) perms, err := acl.New(acl.DenyAll(), policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1058,11 +1058,11 @@ service "foo" {
node "node1" { node "node1" {
policy = "read" policy = "read"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err = acl.New(perms, policy) perms, err = acl.New(perms, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1130,11 +1130,11 @@ func TestACL_filterCheckServiceNodes(t *testing.T) {
service "foo" { service "foo" {
policy = "read" policy = "read"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err := acl.New(acl.DenyAll(), policy) perms, err := acl.New(acl.DenyAll(), policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1167,11 +1167,11 @@ service "foo" {
node "node1" { node "node1" {
policy = "read" policy = "read"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err = acl.New(perms, policy) perms, err = acl.New(perms, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1321,11 +1321,11 @@ func TestACL_filterNodeDump(t *testing.T) {
service "foo" { service "foo" {
policy = "read" policy = "read"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err := acl.New(acl.DenyAll(), policy) perms, err := acl.New(acl.DenyAll(), policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1361,11 +1361,11 @@ service "foo" {
node "node1" { node "node1" {
policy = "read" policy = "read"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err = acl.New(perms, policy) perms, err = acl.New(perms, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1563,11 +1563,11 @@ func TestACL_vetRegisterWithACL(t *testing.T) {
node "node" { node "node" {
policy = "write" policy = "write"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err := acl.New(acl.DenyAll(), policy) perms, err := acl.New(acl.DenyAll(), policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1608,11 +1608,11 @@ node "node" {
service "service" { service "service" {
policy = "write" policy = "write"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err = acl.New(perms, policy) perms, err = acl.New(perms, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1638,11 +1638,11 @@ service "service" {
service "other" { service "other" {
policy = "write" policy = "write"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err = acl.New(perms, policy) perms, err = acl.New(perms, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1712,11 +1712,11 @@ service "other" {
service "other" { service "other" {
policy = "deny" policy = "deny"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err = acl.New(perms, policy) perms, err = acl.New(perms, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1742,11 +1742,11 @@ service "other" {
node "node" { node "node" {
policy = "deny" policy = "deny"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err = acl.New(perms, policy) perms, err = acl.New(perms, policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -1792,11 +1792,11 @@ node "node" {
service "service" { service "service" {
policy = "write" policy = "write"
} }
`) `, nil)
if err != nil { if err != nil {
t.Fatalf("err %v", err) t.Fatalf("err %v", err)
} }
perms, err := acl.New(acl.DenyAll(), policy) perms, err := acl.New(acl.DenyAll(), policy, nil)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }

View File

@ -66,7 +66,7 @@ func (c *Catalog) Register(args *structs.RegisterRequest, reply *struct{}) error
// later if version 0.8 is enabled, so we can eventually just // later if version 0.8 is enabled, so we can eventually just
// delete this and do all the ACL checks down there. // delete this and do all the ACL checks down there.
if args.Service.Service != structs.ConsulServiceName { if args.Service.Service != structs.ConsulServiceName {
if rule != nil && !rule.ServiceWrite(args.Service.Service) { if rule != nil && !rule.ServiceWrite(args.Service.Service, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }
@ -149,6 +149,7 @@ func (c *Catalog) Deregister(args *structs.DeregisterRequest, reply *struct{}) e
if err := vetDeregisterWithACL(rule, args, ns, nc); err != nil { if err := vetDeregisterWithACL(rule, args, ns, nc); err != nil {
return err return err
} }
} }
if _, err := c.srv.raftApply(structs.DeregisterRequestType, args); err != nil { if _, err := c.srv.raftApply(structs.DeregisterRequestType, args); err != nil {

View File

@ -139,7 +139,10 @@ func (c *Coordinate) Update(args *structs.CoordinateUpdateRequest, reply *struct
return err return err
} }
if rule != nil && c.srv.config.ACLEnforceVersion8 { if rule != nil && c.srv.config.ACLEnforceVersion8 {
if !rule.NodeWrite(args.Node) { // We don't enforce the sentinel policy here, since at this time
// sentinel only applies to creating or updating node or service
// info, not updating coordinates.
if !rule.NodeWrite(args.Node, nil) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
} }

View File

@ -10,8 +10,8 @@ import (
func TestFilter_DirEnt(t *testing.T) { func TestFilter_DirEnt(t *testing.T) {
t.Parallel() t.Parallel()
policy, _ := acl.Parse(testFilterRules) policy, _ := acl.Parse(testFilterRules, nil)
aclR, _ := acl.New(acl.DenyAll(), policy) aclR, _ := acl.New(acl.DenyAll(), policy, nil)
type tcase struct { type tcase struct {
in []string in []string
@ -52,8 +52,8 @@ func TestFilter_DirEnt(t *testing.T) {
func TestFilter_Keys(t *testing.T) { func TestFilter_Keys(t *testing.T) {
t.Parallel() t.Parallel()
policy, _ := acl.Parse(testFilterRules) policy, _ := acl.Parse(testFilterRules, nil)
aclR, _ := acl.New(acl.DenyAll(), policy) aclR, _ := acl.New(acl.DenyAll(), policy, nil)
type tcase struct { type tcase struct {
in []string in []string
@ -84,8 +84,8 @@ func TestFilter_Keys(t *testing.T) {
func TestFilter_TxnResults(t *testing.T) { func TestFilter_TxnResults(t *testing.T) {
t.Parallel() t.Parallel()
policy, _ := acl.Parse(testFilterRules) policy, _ := acl.Parse(testFilterRules, nil)
aclR, _ := acl.New(acl.DenyAll(), policy) aclR, _ := acl.New(acl.DenyAll(), policy, nil)
type tcase struct { type tcase struct {
in []string in []string

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sentinel"
"github.com/hashicorp/go-memdb" "github.com/hashicorp/go-memdb"
) )
@ -22,6 +23,7 @@ type KVS struct {
// must only be done on the leader. // must only be done on the leader.
func kvsPreApply(srv *Server, rule acl.ACL, op api.KVOp, dirEnt *structs.DirEntry) (bool, error) { func kvsPreApply(srv *Server, rule acl.ACL, op api.KVOp, dirEnt *structs.DirEntry) (bool, error) {
// Verify the entry. // Verify the entry.
if dirEnt.Key == "" && op != api.KVDeleteTree { if dirEnt.Key == "" && op != api.KVDeleteTree {
return false, fmt.Errorf("Must provide key") return false, fmt.Errorf("Must provide key")
} }
@ -46,7 +48,10 @@ func kvsPreApply(srv *Server, rule acl.ACL, op api.KVOp, dirEnt *structs.DirEntr
} }
default: default:
if !rule.KeyWrite(dirEnt.Key) { scope := func() map[string]interface{} {
return sentinel.ScopeKVUpsert(dirEnt.Key, dirEnt.Value, dirEnt.Flags)
}
if !rule.KeyWrite(dirEnt.Key, scope) {
return false, acl.ErrPermissionDenied return false, acl.ErrPermissionDenied
} }
} }
@ -115,11 +120,10 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er
return err return err
} }
acl, err := k.srv.resolveToken(args.Token) aclRule, err := k.srv.resolveToken(args.Token)
if err != nil { if err != nil {
return err return err
} }
return k.srv.blockingQuery( return k.srv.blockingQuery(
&args.QueryOptions, &args.QueryOptions,
&reply.QueryMeta, &reply.QueryMeta,
@ -128,9 +132,10 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er
if err != nil { if err != nil {
return err return err
} }
if acl != nil && !acl.KeyRead(args.Key) { if aclRule != nil && !aclRule.KeyRead(args.Key) {
ent = nil ent = nil
} }
if ent == nil { if ent == nil {
// Must provide non-zero index to prevent blocking // Must provide non-zero index to prevent blocking
// Index 1 is impossible anyways (due to Raft internals) // Index 1 is impossible anyways (due to Raft internals)

View File

@ -25,6 +25,7 @@ import (
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/sentinel"
"github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/consul/types" "github.com/hashicorp/consul/types"
"github.com/hashicorp/raft" "github.com/hashicorp/raft"
@ -75,6 +76,9 @@ const (
// Server is Consul server which manages the service discovery, // Server is Consul server which manages the service discovery,
// health checking, DC forwarding, Raft, and multiple Serf pools. // health checking, DC forwarding, Raft, and multiple Serf pools.
type Server struct { type Server struct {
// sentinel is the Sentinel code engine (can be nil).
sentinel sentinel.Evaluator
// aclAuthCache is the authoritative ACL cache. // aclAuthCache is the authoritative ACL cache.
aclAuthCache *acl.Cache aclAuthCache *acl.Cache
@ -317,7 +321,8 @@ func NewServerLogger(config *Config, logger *log.Logger, tokens *token.Store) (*
s.statsFetcher = NewStatsFetcher(logger, s.connPool, s.config.Datacenter) s.statsFetcher = NewStatsFetcher(logger, s.connPool, s.config.Datacenter)
// Initialize the authoritative ACL cache. // Initialize the authoritative ACL cache.
s.aclAuthCache, err = acl.NewCache(aclCacheSize, s.aclLocalFault) s.sentinel = sentinel.New(logger)
s.aclAuthCache, err = acl.NewCache(aclCacheSize, s.aclLocalFault, s.sentinel)
if err != nil { if err != nil {
s.Shutdown() s.Shutdown()
return nil, fmt.Errorf("Failed to create authoritative ACL cache: %v", err) return nil, fmt.Errorf("Failed to create authoritative ACL cache: %v", err)
@ -329,7 +334,7 @@ func NewServerLogger(config *Config, logger *log.Logger, tokens *token.Store) (*
if s.IsACLReplicationEnabled() { if s.IsACLReplicationEnabled() {
local = s.aclLocalFault local = s.aclLocalFault
} }
if s.aclCache, err = newACLCache(config, logger, s.RPC, local); err != nil { if s.aclCache, err = newACLCache(config, logger, s.RPC, local, s.sentinel); err != nil {
s.Shutdown() s.Shutdown()
return nil, fmt.Errorf("Failed to create non-authoritative ACL cache: %v", err) return nil, fmt.Errorf("Failed to create non-authoritative ACL cache: %v", err)
} }

8
sentinel/evaluator.go Normal file
View File

@ -0,0 +1,8 @@
package sentinel
// Evaluator wraps the Sentinel evaluator from the HashiCorp Sentinel policy
// engine.
type Evaluator interface {
Compile(policy string) error
Execute(policy string, enforcementLevel string, data map[string]interface{}) bool
}

29
sentinel/scope.go Normal file
View File

@ -0,0 +1,29 @@
package sentinel
import (
"github.com/hashicorp/consul/api"
)
// ScopeFn is a callback that provides a sentinel scope. This is a callback
// so that if we don't run sentinel for some reason (not enabled or a basic
// policy check means we don't have to) then we don't spend the effort to make
// the map.
type ScopeFn func() map[string]interface{}
// ScopeKVUpsert returns the standard sentinel scope for a KV create or update.
func ScopeKVUpsert(key string, value []byte, flags uint64) map[string]interface{} {
return map[string]interface{}{
"key": key,
"value": string(value),
"flags": flags,
}
}
// ScopeCatalogUpsert returns the standard sentinel scope for a catalog create
// or update. Service is allowed to be nil.
func ScopeCatalogUpsert(node *api.Node, service *api.AgentService) map[string]interface{} {
return map[string]interface{}{
"node": node,
"service": service,
}
}

13
sentinel/sentinel_stub.go Normal file
View File

@ -0,0 +1,13 @@
// +build !ent
package sentinel
import (
"log"
)
// New returns a new instance of the Sentinel code engine. This is only available
// in Consul Enterprise so this version always returns nil.
func New(logger *log.Logger) Evaluator {
return nil
}