From 8394ad08dbc69ce9addb26edbb1966a5afc2d686 Mon Sep 17 00:00:00 2001 From: Preetha Appan Date: Thu, 14 Sep 2017 14:31:01 -0500 Subject: [PATCH] Introduce Code Policy validation via sentinel, with a noop implementation --- acl/acl.go | 113 +++++++++++++++++++++------- acl/acl_test.go | 50 ++++++------ acl/cache.go | 9 ++- acl/cache_test.go | 14 ++-- acl/policy.go | 61 +++++++++++++-- acl/policy_test.go | 10 +-- agent/acl.go | 22 +++--- agent/acl_test.go | 2 +- agent/agent_endpoint.go | 2 +- agent/consul/acl.go | 87 ++++++++++++++++----- agent/consul/acl_endpoint.go | 2 +- agent/consul/acl_test.go | 64 ++++++++-------- agent/consul/catalog_endpoint.go | 3 +- agent/consul/coordinate_endpoint.go | 5 +- agent/consul/filter_test.go | 12 +-- agent/consul/kvs_endpoint.go | 13 +++- agent/consul/server.go | 9 ++- sentinel/evaluator.go | 8 ++ sentinel/scope.go | 29 +++++++ sentinel/sentinel_stub.go | 13 ++++ 20 files changed, 373 insertions(+), 155 deletions(-) create mode 100644 sentinel/evaluator.go create mode 100644 sentinel/scope.go create mode 100644 sentinel/sentinel_stub.go diff --git a/acl/acl.go b/acl/acl.go index 3ade9d405..3f867dbd9 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -2,6 +2,7 @@ package acl import ( "github.com/armon/go-radix" + "github.com/hashicorp/consul/sentinel" ) var ( @@ -17,6 +18,10 @@ var ( manageAll ACL ) +// DefaultPolicyEnforcementLevel will be used if the user leaves the level +// blank when configuring an ACL. +const DefaultPolicyEnforcementLevel = "hard-mandatory" + func init() { // Setup the singletons allowAll = &StaticACL{ @@ -59,7 +64,7 @@ type ACL interface { KeyRead(string) bool // 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 // 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 // given node. - NodeWrite(string) bool + NodeWrite(string, sentinel.ScopeFn) bool // OperatorRead determines if the read-only Consul operator functions // can be used. @@ -101,7 +106,7 @@ type ACL interface { // ServiceWrite checks for permission to create or update a given // service - ServiceWrite(string) bool + ServiceWrite(string, sentinel.ScopeFn) bool // SessionRead checks for permission to read sessions for a given node. SessionRead(string) bool @@ -150,7 +155,7 @@ func (s *StaticACL) KeyRead(string) bool { return s.defaultAllow } -func (s *StaticACL) KeyWrite(string) bool { +func (s *StaticACL) KeyWrite(string, sentinel.ScopeFn) bool { return s.defaultAllow } @@ -170,7 +175,7 @@ func (s *StaticACL) NodeRead(string) bool { return s.defaultAllow } -func (s *StaticACL) NodeWrite(string) bool { +func (s *StaticACL) NodeWrite(string, sentinel.ScopeFn) bool { return s.defaultAllow } @@ -194,7 +199,7 @@ func (s *StaticACL) ServiceRead(string) bool { return s.defaultAllow } -func (s *StaticACL) ServiceWrite(string) bool { +func (s *StaticACL) ServiceWrite(string, sentinel.ScopeFn) bool { 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 // the ACL interface. type PolicyACL struct { @@ -246,6 +261,10 @@ type PolicyACL struct { // no matching rule. parent ACL + // sentinel is an interface for validating and executing sentinel code + // policies. + sentinel sentinel.Evaluator + // agentRules contains the agent policies agentRules *radix.Tree @@ -278,7 +297,7 @@ type PolicyACL struct { // 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) { +func New(parent ACL, policy *Policy, sentinel sentinel.Evaluator) (*PolicyACL, error) { p := &PolicyACL{ parent: parent, agentRules: radix.New(), @@ -288,6 +307,7 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) { sessionRules: radix.New(), eventRules: radix.New(), preparedQueryRules: radix.New(), + sentinel: sentinel, } // Load the agent policy @@ -297,17 +317,29 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) { // Load the key policy 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 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 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 @@ -421,7 +453,8 @@ func (p *PolicyACL) KeyRead(key string) bool { // Look for a matching rule _, rule, ok := p.keyRules.LongestPrefix(key) if ok { - switch rule.(string) { + pr := rule.(PolicyRule) + switch pr.aclPolicy { case PolicyRead, PolicyWrite: return true default: @@ -434,27 +467,28 @@ func (p *PolicyACL) KeyRead(key string) bool { } // 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 _, rule, ok := p.keyRules.LongestPrefix(key) if ok { - switch rule.(string) { + pr := rule.(PolicyRule) + switch pr.aclPolicy { case PolicyWrite: - return true + return p.executeCodePolicy(&pr.sentinelPolicy, scope) default: return false } } // 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 func (p *PolicyACL) KeyWritePrefix(prefix string) bool { // Look for a matching rule that denies _, rule, ok := p.keyRules.LongestPrefix(prefix) - if ok && rule.(string) != PolicyWrite { + if ok && rule.(PolicyRule).aclPolicy != PolicyWrite { return false } @@ -462,7 +496,7 @@ func (p *PolicyACL) KeyWritePrefix(prefix string) bool { 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) != PolicyWrite { + if rule.(PolicyRule).aclPolicy != PolicyWrite { deny = true return true } @@ -522,7 +556,8 @@ func (p *PolicyACL) NodeRead(name string) bool { _, rule, ok := p.nodeRules.LongestPrefix(name) if ok { - switch rule { + pr := rule.(PolicyRule) + switch pr.aclPolicy { case PolicyRead, PolicyWrite: return true default: @@ -535,21 +570,22 @@ func (p *PolicyACL) NodeRead(name string) bool { } // 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 _, rule, ok := p.nodeRules.LongestPrefix(name) if ok { - switch rule { + pr := rule.(PolicyRule) + switch pr.aclPolicy { case PolicyWrite: - return true + return p.executeCodePolicy(&pr.sentinelPolicy, scope) default: return false } } // 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 @@ -603,9 +639,9 @@ func (p *PolicyACL) PreparedQueryWrite(prefix string) bool { func (p *PolicyACL) ServiceRead(name string) bool { // Check for an exact rule or catch-all _, rule, ok := p.serviceRules.LongestPrefix(name) - if ok { - switch rule { + pr := rule.(PolicyRule) + switch pr.aclPolicy { case PolicyRead, PolicyWrite: return true default: @@ -618,21 +654,21 @@ func (p *PolicyACL) ServiceRead(name string) bool { } // 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 _, rule, ok := p.serviceRules.LongestPrefix(name) - if ok { - switch rule { + pr := rule.(PolicyRule) + switch pr.aclPolicy { case PolicyWrite: - return true + return p.executeCodePolicy(&pr.sentinelPolicy, scope) default: return false } } // 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. @@ -670,3 +706,22 @@ func (p *PolicyACL) SessionWrite(node string) bool { // No matching rule, use the parent. 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()) +} diff --git a/acl/acl_test.go b/acl/acl_test.go index fb944d66d..c8fbb3d77 100644 --- a/acl/acl_test.go +++ b/acl/acl_test.go @@ -56,7 +56,7 @@ func TestStaticACL(t *testing.T) { if !all.KeyRead("foobar") { t.Fatalf("should allow") } - if !all.KeyWrite("foobar") { + if !all.KeyWrite("foobar", nil) { t.Fatalf("should allow") } if !all.KeyringRead() { @@ -68,7 +68,7 @@ func TestStaticACL(t *testing.T) { if !all.NodeRead("foobar") { t.Fatalf("should allow") } - if !all.NodeWrite("foobar") { + if !all.NodeWrite("foobar", nil) { t.Fatalf("should allow") } if !all.OperatorRead() { @@ -86,7 +86,7 @@ func TestStaticACL(t *testing.T) { if !all.ServiceRead("foobar") { t.Fatalf("should allow") } - if !all.ServiceWrite("foobar") { + if !all.ServiceWrite("foobar", nil) { t.Fatalf("should allow") } if !all.SessionRead("foobar") { @@ -126,7 +126,7 @@ func TestStaticACL(t *testing.T) { if none.KeyRead("foobar") { t.Fatalf("should not allow") } - if none.KeyWrite("foobar") { + if none.KeyWrite("foobar", nil) { t.Fatalf("should not allow") } if none.KeyringRead() { @@ -138,7 +138,7 @@ func TestStaticACL(t *testing.T) { if none.NodeRead("foobar") { t.Fatalf("should not allow") } - if none.NodeWrite("foobar") { + if none.NodeWrite("foobar", nil) { t.Fatalf("should not allow") } if none.OperatorRead() { @@ -156,7 +156,7 @@ func TestStaticACL(t *testing.T) { if none.ServiceRead("foobar") { t.Fatalf("should not allow") } - if none.ServiceWrite("foobar") { + if none.ServiceWrite("foobar", nil) { t.Fatalf("should not allow") } if none.SessionRead("foobar") { @@ -190,7 +190,7 @@ func TestStaticACL(t *testing.T) { if !manage.KeyRead("foobar") { t.Fatalf("should allow") } - if !manage.KeyWrite("foobar") { + if !manage.KeyWrite("foobar", nil) { t.Fatalf("should allow") } if !manage.KeyringRead() { @@ -202,7 +202,7 @@ func TestStaticACL(t *testing.T) { if !manage.NodeRead("foobar") { t.Fatalf("should allow") } - if !manage.NodeWrite("foobar") { + if !manage.NodeWrite("foobar", nil) { t.Fatalf("should allow") } if !manage.OperatorRead() { @@ -220,7 +220,7 @@ func TestStaticACL(t *testing.T) { if !manage.ServiceRead("foobar") { t.Fatalf("should allow") } - if !manage.ServiceWrite("foobar") { + if !manage.ServiceWrite("foobar", nil) { t.Fatalf("should allow") } 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 { t.Fatalf("err: %v", err) } @@ -330,7 +330,7 @@ func TestPolicyACL(t *testing.T) { if c.read != acl.KeyRead(c.inp) { 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) } if c.writePrefix != acl.KeyWritePrefix(c.inp) { @@ -357,7 +357,7 @@ func TestPolicyACL(t *testing.T) { if c.read != acl.ServiceRead(c.inp) { 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) } } @@ -444,7 +444,7 @@ func TestPolicyACL_Parent(t *testing.T) { }, }, } - root, err := New(deny, policyRoot) + root, err := New(deny, policyRoot, nil) if err != nil { 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 { t.Fatalf("err: %v", err) } @@ -499,7 +499,7 @@ func TestPolicyACL_Parent(t *testing.T) { if c.read != acl.KeyRead(c.inp) { 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) } if c.writePrefix != acl.KeyWritePrefix(c.inp) { @@ -523,7 +523,7 @@ func TestPolicyACL_Parent(t *testing.T) { if c.read != acl.ServiceRead(c.inp) { 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) } } @@ -585,7 +585,7 @@ func TestPolicyACL_Agent(t *testing.T) { }, }, } - root, err := New(deny, policyRoot) + root, err := New(deny, policyRoot, nil) if err != nil { 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 { t.Fatalf("err: %v", err) } @@ -659,7 +659,7 @@ func TestPolicyACL_Keyring(t *testing.T) { {PolicyDeny, false, false}, } for _, c := range cases { - acl, err := New(DenyAll(), &Policy{Keyring: c.inp}) + acl, err := New(DenyAll(), &Policy{Keyring: c.inp}, nil) if err != nil { t.Fatalf("bad: %s", err) } @@ -685,7 +685,7 @@ func TestPolicyACL_Operator(t *testing.T) { {PolicyDeny, false, false}, } for _, c := range cases { - acl, err := New(DenyAll(), &Policy{Operator: c.inp}) + acl, err := New(DenyAll(), &Policy{Operator: c.inp}, nil) if err != nil { 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 { 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 { t.Fatalf("err: %v", err) } @@ -775,7 +775,7 @@ func TestPolicyACL_Node(t *testing.T) { if c.read != acl.NodeRead(c.inp) { 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) } } @@ -803,7 +803,7 @@ func TestPolicyACL_Session(t *testing.T) { }, }, } - root, err := New(deny, policyRoot) + root, err := New(deny, policyRoot, nil) if err != nil { 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 { t.Fatalf("err: %v", err) } diff --git a/acl/cache.go b/acl/cache.go index 0387f9fbe..f3fdf6830 100644 --- a/acl/cache.go +++ b/acl/cache.go @@ -4,6 +4,7 @@ import ( "crypto/md5" "fmt" + "github.com/hashicorp/consul/sentinel" "github.com/hashicorp/golang-lru" ) @@ -24,10 +25,11 @@ type Cache struct { aclCache *lru.TwoQueueCache // Cache id -> acl policyCache *lru.TwoQueueCache // Cache policy -> acl ruleCache *lru.TwoQueueCache // Cache rules -> policy + sentinel sentinel.Evaluator } // 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 { return nil, fmt.Errorf("Must provide positive cache size") } @@ -52,6 +54,7 @@ func NewCache(size int, faultfn FaultFunc) (*Cache, error) { aclCache: ac, policyCache: pc, ruleCache: rc, + sentinel: sentinel, } return c, nil } @@ -69,7 +72,7 @@ func (c *Cache) getPolicy(id, rules string) (*Policy, error) { if ok { return raw.(*Policy), nil } - policy, err := Parse(rules) + policy, err := Parse(rules, c.sentinel) if err != nil { return nil, err } @@ -149,7 +152,7 @@ func (c *Cache) GetACL(id string) (ACL, error) { } // Compile the ACL - acl, err := New(parent, policy) + acl, err := New(parent, policy, c.sentinel) if err != nil { return nil, err } diff --git a/acl/cache_test.go b/acl/cache_test.go index d144b0009..53fde17b5 100644 --- a/acl/cache_test.go +++ b/acl/cache_test.go @@ -5,7 +5,7 @@ import ( ) func TestCache_GetPolicy(t *testing.T) { - c, err := NewCache(2, nil) + c, err := NewCache(2, nil, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -62,7 +62,7 @@ func TestCache_GetACL(t *testing.T) { return "deny", policies[id], nil } - c, err := NewCache(2, faultfn) + c, err := NewCache(2, faultfn, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -125,7 +125,7 @@ func TestCache_ClearACL(t *testing.T) { return "deny", policies[id], nil } - c, err := NewCache(16, faultfn) + c, err := NewCache(16, faultfn, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -160,7 +160,7 @@ func TestCache_Purge(t *testing.T) { return "deny", policies[id], nil } - c, err := NewCache(16, faultfn) + c, err := NewCache(16, faultfn, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -192,7 +192,7 @@ func TestCache_GetACLPolicy(t *testing.T) { faultfn := func(id string) (string, string, error) { return "deny", policies[id], nil } - c, err := NewCache(16, faultfn) + c, err := NewCache(16, faultfn, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -245,7 +245,7 @@ func TestCache_GetACL_Parent(t *testing.T) { return "", "", nil } - c, err := NewCache(16, faultfn) + c, err := NewCache(16, faultfn, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -276,7 +276,7 @@ func TestCache_GetACL_ParentCache(t *testing.T) { return "", "", nil } - c, err := NewCache(16, faultfn) + c, err := NewCache(16, faultfn, nil) if err != nil { t.Fatalf("err: %v", err) } diff --git a/acl/policy.go b/acl/policy.go index f7781b81e..9df0020a3 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -3,6 +3,7 @@ package acl import ( "fmt" + "github.com/hashicorp/consul/sentinel" "github.com/hashicorp/hcl" ) @@ -27,6 +28,12 @@ type Policy struct { 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 // with specific name prefixes. type AgentPolicy struct { @@ -40,8 +47,9 @@ func (a *AgentPolicy) GoString() string { // KeyPolicy represents a policy for a key type KeyPolicy struct { - Prefix string `hcl:",key"` - Policy string + Prefix string `hcl:",key"` + Policy string + Sentinel Sentinel } func (k *KeyPolicy) GoString() string { @@ -50,8 +58,9 @@ func (k *KeyPolicy) GoString() string { // NodePolicy represents a policy for a node type NodePolicy struct { - Name string `hcl:",key"` - Policy string + Name string `hcl:",key"` + Policy string + Sentinel Sentinel } func (n *NodePolicy) GoString() string { @@ -60,8 +69,9 @@ func (n *NodePolicy) GoString() string { // ServicePolicy represents a policy for a service type ServicePolicy struct { - Name string `hcl:",key"` - Policy string + Name string `hcl:",key"` + Policy string + Sentinel Sentinel } 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 // intermediary set of policies, before being compiled into // the ACL -func Parse(rules string) (*Policy, error) { +func Parse(rules string, sentinel sentinel.Evaluator) (*Policy, error) { // Decode the rules p := &Policy{} if rules == "" { @@ -140,6 +178,9 @@ func Parse(rules string) (*Policy, error) { if !isPolicyValid(kp.Policy) { 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 @@ -147,6 +188,9 @@ func Parse(rules string) (*Policy, error) { if !isPolicyValid(np.Policy) { 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 @@ -154,6 +198,9 @@ func Parse(rules string) (*Policy, error) { if !isPolicyValid(sp.Policy) { 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 diff --git a/acl/policy_test.go b/acl/policy_test.go index d7b8138e9..37b8216f5 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -163,7 +163,7 @@ query "bar" { }, } - out, err := Parse(inp) + out, err := Parse(inp, nil) if err != nil { 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 { t.Fatalf("err: %v", err) } @@ -362,7 +362,7 @@ keyring = "" Keyring: "", } - out, err := Parse(inp) + out, err := Parse(inp, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -380,7 +380,7 @@ operator = "" Operator: "", } - out, err := Parse(inp) + out, err := Parse(inp, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -403,7 +403,7 @@ func TestACLPolicy_Bad_Policy(t *testing.T) { `session "" { policy = "nope" }`, } for _, c := range cases { - _, err := Parse(c) + _, err := Parse(c, nil) if err == nil || !strings.Contains(err.Error(), "Invalid") { t.Fatalf("expected policy error, got: %#v", err) } diff --git a/agent/acl.go b/agent/acl.go index fd057ff61..49f7b9555 100644 --- a/agent/acl.go +++ b/agent/acl.go @@ -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 { 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 { return nil, err } @@ -252,14 +252,14 @@ func (a *Agent) vetServiceRegister(token string, service *structs.NodeService) e } // Vet the service itself. - if !rule.ServiceWrite(service.Service) { + if !rule.ServiceWrite(service.Service, nil) { return acl.ErrPermissionDenied } // Vet any service that might be getting overwritten. services := a.state.Services() if existing, ok := services[service.ID]; ok { - if !rule.ServiceWrite(existing.Service) { + if !rule.ServiceWrite(existing.Service, nil) { 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. services := a.state.Services() if existing, ok := services[serviceID]; ok { - if !rule.ServiceWrite(existing.Service) { + if !rule.ServiceWrite(existing.Service, nil) { return acl.ErrPermissionDenied } } else { @@ -306,11 +306,11 @@ func (a *Agent) vetCheckRegister(token string, check *structs.HealthCheck) error // Vet the check itself. if len(check.ServiceName) > 0 { - if !rule.ServiceWrite(check.ServiceName) { + if !rule.ServiceWrite(check.ServiceName, nil) { return acl.ErrPermissionDenied } } else { - if !rule.NodeWrite(a.config.NodeName) { + if !rule.NodeWrite(a.config.NodeName, nil) { return acl.ErrPermissionDenied } } @@ -319,11 +319,11 @@ func (a *Agent) vetCheckRegister(token string, check *structs.HealthCheck) error checks := a.state.Checks() if existing, ok := checks[check.CheckID]; ok { if len(existing.ServiceName) > 0 { - if !rule.ServiceWrite(existing.ServiceName) { + if !rule.ServiceWrite(existing.ServiceName, nil) { return acl.ErrPermissionDenied } } else { - if !rule.NodeWrite(a.config.NodeName) { + if !rule.NodeWrite(a.config.NodeName, nil) { return acl.ErrPermissionDenied } } @@ -347,11 +347,11 @@ func (a *Agent) vetCheckUpdate(token string, checkID types.CheckID) error { checks := a.state.Checks() if existing, ok := checks[checkID]; ok { if len(existing.ServiceName) > 0 { - if !rule.ServiceWrite(existing.ServiceName) { + if !rule.ServiceWrite(existing.ServiceName, nil) { return acl.ErrPermissionDenied } } else { - if !rule.NodeWrite(a.config.NodeName) { + if !rule.NodeWrite(a.config.NodeName, nil) { return acl.ErrPermissionDenied } } diff --git a/agent/acl_test.go b/agent/acl_test.go index ff45cec00..42ddbcaa2 100644 --- a/agent/acl_test.go +++ b/agent/acl_test.go @@ -207,7 +207,7 @@ func TestACL_Special_IDs(t *testing.T) { if !acl.NodeRead("hello") { 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") } } diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 02528c7bf..2d49fa693 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -637,7 +637,7 @@ func (s *HTTPServer) AgentNodeMaintenance(resp http.ResponseWriter, req *http.Re if err != nil { 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 } diff --git a/agent/consul/acl.go b/agent/consul/acl.go index 8e72ee2d5..92462d25c 100644 --- a/agent/consul/acl.go +++ b/agent/consul/acl.go @@ -9,6 +9,8 @@ import ( "github.com/armon/go-metrics" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sentinel" "github.com/hashicorp/golang-lru" ) @@ -102,6 +104,9 @@ type aclCache struct { // acls is a non-authoritative ACL cache. acls *lru.TwoQueueCache + // sentinel is the code engine (can be nil). + sentinel sentinel.Evaluator + // aclPolicyCache is a non-authoritative policy cache. policies *lru.TwoQueueCache @@ -116,13 +121,14 @@ type aclCache struct { // 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 // 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 cache := &aclCache{ - config: conf, - logger: logger, - rpc: rpc, - local: local, + config: conf, + logger: logger, + rpc: rpc, + local: local, + sentinel: sentinel, } // Initialize the non-authoritative ACL cache @@ -206,7 +212,7 @@ func (c *aclCache) lookupACL(id, authDC string) (acl.ACL, error) { goto ACL_DOWN } - policy, err := acl.Parse(rules) + policy, err := acl.Parse(rules, c.sentinel) if err != nil { c.logger.Printf("[DEBUG] consul.acl: Failed to parse policy for replicated ACL: %v", err) goto ACL_DOWN @@ -268,7 +274,7 @@ func (c *aclCache) useACLPolicy(id, authDC string, cached *aclCacheEntry, p *str } // Compile the ACL - acl, err := acl.New(parent, p.Policy) + acl, err := acl.New(parent, p.Policy, c.sentinel) if err != nil { return nil, err } @@ -327,7 +333,6 @@ func (f *aclFilter) allowService(service string) bool { if !f.enforceVersion8 && service == structs.ConsulServiceID { return true } - 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 -// rules configured for the provided token. The subject is scrubbed and -// modified in-place, leaving only resources the token can access. +// rules configured for the provided token. func (s *Server) filterACL(token string, subj interface{}) error { // Get the ACL from the token acl, err := s.resolveToken(token) @@ -646,11 +650,45 @@ func vetRegisterWithACL(rule acl.ACL, subj *structs.RegisterRequest, 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 // node info for each request without having to have node "write" // privileges. needsNode := ns == nil || subj.ChangesNode(ns.Node) - if needsNode && !rule.NodeWrite(subj.Node) { + + if needsNode && !rule.NodeWrite(subj.Node, scope) { 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 // is being modified by id (if any). if subj.Service != nil { - if !rule.ServiceWrite(subj.Service.Service) { + if !rule.ServiceWrite(subj.Service.Service, scope) { return acl.ErrPermissionDenied } if ns != nil { 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 } } @@ -693,7 +735,7 @@ func vetRegisterWithACL(rule acl.ACL, subj *structs.RegisterRequest, // Node-level check. if check.ServiceID == "" { - if !rule.NodeWrite(subj.Node) { + if !rule.NodeWrite(subj.Node, scope) { return acl.ErrPermissionDenied } 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) } - 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 } } @@ -733,11 +778,15 @@ func vetRegisterWithACL(rule acl.ACL, subj *structs.RegisterRequest, // be nil; similar for the HealthCheck for the referenced health check. func vetDeregisterWithACL(rule acl.ACL, subj *structs.DeregisterRequest, ns *structs.NodeService, nc *structs.HealthCheck) error { + // Fast path if ACLs are not enabled. if rule == 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 // also evaluates things in this order, and will ignore fields based on // 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 { return fmt.Errorf("Unknown service '%s'", subj.ServiceID) } - if !rule.ServiceWrite(ns.Service) { + if !rule.ServiceWrite(ns.Service, nil) { return acl.ErrPermissionDenied } } else if subj.CheckID != "" { @@ -753,16 +802,16 @@ func vetDeregisterWithACL(rule acl.ACL, subj *structs.DeregisterRequest, return fmt.Errorf("Unknown check '%s'", subj.CheckID) } if nc.ServiceID != "" { - if !rule.ServiceWrite(nc.ServiceName) { + if !rule.ServiceWrite(nc.ServiceName, nil) { return acl.ErrPermissionDenied } } else { - if !rule.NodeWrite(subj.Node) { + if !rule.NodeWrite(subj.Node, nil) { return acl.ErrPermissionDenied } } } else { - if !rule.NodeWrite(subj.Node) { + if !rule.NodeWrite(subj.Node, nil) { return acl.ErrPermissionDenied } } diff --git a/agent/consul/acl_endpoint.go b/agent/consul/acl_endpoint.go index bdfdc8d2b..437202917 100644 --- a/agent/consul/acl_endpoint.go +++ b/agent/consul/acl_endpoint.go @@ -107,7 +107,7 @@ func aclApplyInternal(srv *Server, args *structs.ACLRequest, reply *string) erro } // Validate the rules compile - _, err := acl.Parse(args.ACL.Rules) + _, err := acl.Parse(args.ACL.Rules, srv.sentinel) if err != nil { return fmt.Errorf("ACL rule compilation failed: %v", err) } diff --git a/agent/consul/acl_test.go b/agent/consul/acl_test.go index 0069b9a9d..92027b84d 100644 --- a/agent/consul/acl_test.go +++ b/agent/consul/acl_test.go @@ -793,11 +793,11 @@ func TestACL_filterHealthChecks(t *testing.T) { service "foo" { policy = "read" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err := acl.New(acl.DenyAll(), policy) + perms, err := acl.New(acl.DenyAll(), policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -827,11 +827,11 @@ service "foo" { node "node1" { policy = "read" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err = acl.New(perms, policy) + perms, err = acl.New(perms, policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -918,11 +918,11 @@ func TestACL_filterServiceNodes(t *testing.T) { service "foo" { policy = "read" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err := acl.New(acl.DenyAll(), policy) + perms, err := acl.New(acl.DenyAll(), policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -952,11 +952,11 @@ service "foo" { node "node1" { policy = "read" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err = acl.New(perms, policy) + perms, err = acl.New(perms, policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1024,11 +1024,11 @@ func TestACL_filterNodeServices(t *testing.T) { service "foo" { policy = "read" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err := acl.New(acl.DenyAll(), policy) + perms, err := acl.New(acl.DenyAll(), policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1058,11 +1058,11 @@ service "foo" { node "node1" { policy = "read" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err = acl.New(perms, policy) + perms, err = acl.New(perms, policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1130,11 +1130,11 @@ func TestACL_filterCheckServiceNodes(t *testing.T) { service "foo" { policy = "read" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err := acl.New(acl.DenyAll(), policy) + perms, err := acl.New(acl.DenyAll(), policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1167,11 +1167,11 @@ service "foo" { node "node1" { policy = "read" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err = acl.New(perms, policy) + perms, err = acl.New(perms, policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1321,11 +1321,11 @@ func TestACL_filterNodeDump(t *testing.T) { service "foo" { policy = "read" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err := acl.New(acl.DenyAll(), policy) + perms, err := acl.New(acl.DenyAll(), policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1361,11 +1361,11 @@ service "foo" { node "node1" { policy = "read" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err = acl.New(perms, policy) + perms, err = acl.New(perms, policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1563,11 +1563,11 @@ func TestACL_vetRegisterWithACL(t *testing.T) { node "node" { policy = "write" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err := acl.New(acl.DenyAll(), policy) + perms, err := acl.New(acl.DenyAll(), policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1608,11 +1608,11 @@ node "node" { service "service" { policy = "write" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err = acl.New(perms, policy) + perms, err = acl.New(perms, policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1638,11 +1638,11 @@ service "service" { service "other" { policy = "write" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err = acl.New(perms, policy) + perms, err = acl.New(perms, policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1712,11 +1712,11 @@ service "other" { service "other" { policy = "deny" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err = acl.New(perms, policy) + perms, err = acl.New(perms, policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1742,11 +1742,11 @@ service "other" { node "node" { policy = "deny" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err = acl.New(perms, policy) + perms, err = acl.New(perms, policy, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -1792,11 +1792,11 @@ node "node" { service "service" { policy = "write" } -`) +`, nil) if err != nil { t.Fatalf("err %v", err) } - perms, err := acl.New(acl.DenyAll(), policy) + perms, err := acl.New(acl.DenyAll(), policy, nil) if err != nil { t.Fatalf("err: %v", err) } diff --git a/agent/consul/catalog_endpoint.go b/agent/consul/catalog_endpoint.go index e8b6b9f60..9c8e6dbd2 100644 --- a/agent/consul/catalog_endpoint.go +++ b/agent/consul/catalog_endpoint.go @@ -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 // delete this and do all the ACL checks down there. 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 } } @@ -149,6 +149,7 @@ func (c *Catalog) Deregister(args *structs.DeregisterRequest, reply *struct{}) e if err := vetDeregisterWithACL(rule, args, ns, nc); err != nil { return err } + } if _, err := c.srv.raftApply(structs.DeregisterRequestType, args); err != nil { diff --git a/agent/consul/coordinate_endpoint.go b/agent/consul/coordinate_endpoint.go index 3ba1dcdc5..a001fad7b 100644 --- a/agent/consul/coordinate_endpoint.go +++ b/agent/consul/coordinate_endpoint.go @@ -139,7 +139,10 @@ func (c *Coordinate) Update(args *structs.CoordinateUpdateRequest, reply *struct return err } 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 } } diff --git a/agent/consul/filter_test.go b/agent/consul/filter_test.go index 52b40e0ad..ebecca213 100644 --- a/agent/consul/filter_test.go +++ b/agent/consul/filter_test.go @@ -10,8 +10,8 @@ import ( func TestFilter_DirEnt(t *testing.T) { t.Parallel() - policy, _ := acl.Parse(testFilterRules) - aclR, _ := acl.New(acl.DenyAll(), policy) + policy, _ := acl.Parse(testFilterRules, nil) + aclR, _ := acl.New(acl.DenyAll(), policy, nil) type tcase struct { in []string @@ -52,8 +52,8 @@ func TestFilter_DirEnt(t *testing.T) { func TestFilter_Keys(t *testing.T) { t.Parallel() - policy, _ := acl.Parse(testFilterRules) - aclR, _ := acl.New(acl.DenyAll(), policy) + policy, _ := acl.Parse(testFilterRules, nil) + aclR, _ := acl.New(acl.DenyAll(), policy, nil) type tcase struct { in []string @@ -84,8 +84,8 @@ func TestFilter_Keys(t *testing.T) { func TestFilter_TxnResults(t *testing.T) { t.Parallel() - policy, _ := acl.Parse(testFilterRules) - aclR, _ := acl.New(acl.DenyAll(), policy) + policy, _ := acl.Parse(testFilterRules, nil) + aclR, _ := acl.New(acl.DenyAll(), policy, nil) type tcase struct { in []string diff --git a/agent/consul/kvs_endpoint.go b/agent/consul/kvs_endpoint.go index ed0e0ec88..78a631f88 100644 --- a/agent/consul/kvs_endpoint.go +++ b/agent/consul/kvs_endpoint.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sentinel" "github.com/hashicorp/go-memdb" ) @@ -22,6 +23,7 @@ type KVS struct { // must only be done on the leader. func kvsPreApply(srv *Server, rule acl.ACL, op api.KVOp, dirEnt *structs.DirEntry) (bool, error) { // Verify the entry. + if dirEnt.Key == "" && op != api.KVDeleteTree { 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: - 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 } } @@ -115,11 +120,10 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er return err } - acl, err := k.srv.resolveToken(args.Token) + aclRule, err := k.srv.resolveToken(args.Token) if err != nil { return err } - return k.srv.blockingQuery( &args.QueryOptions, &reply.QueryMeta, @@ -128,9 +132,10 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er if err != nil { return err } - if acl != nil && !acl.KeyRead(args.Key) { + if aclRule != nil && !aclRule.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) diff --git a/agent/consul/server.go b/agent/consul/server.go index 06716f000..390797ef9 100644 --- a/agent/consul/server.go +++ b/agent/consul/server.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/lib" + "github.com/hashicorp/consul/sentinel" "github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/consul/types" "github.com/hashicorp/raft" @@ -75,6 +76,9 @@ const ( // Server is Consul server which manages the service discovery, // health checking, DC forwarding, Raft, and multiple Serf pools. type Server struct { + // sentinel is the Sentinel code engine (can be nil). + sentinel sentinel.Evaluator + // aclAuthCache is the authoritative 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) // 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 { s.Shutdown() 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() { 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() return nil, fmt.Errorf("Failed to create non-authoritative ACL cache: %v", err) } diff --git a/sentinel/evaluator.go b/sentinel/evaluator.go new file mode 100644 index 000000000..607e22cc7 --- /dev/null +++ b/sentinel/evaluator.go @@ -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 +} diff --git a/sentinel/scope.go b/sentinel/scope.go new file mode 100644 index 000000000..17af24118 --- /dev/null +++ b/sentinel/scope.go @@ -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, + } +} diff --git a/sentinel/sentinel_stub.go b/sentinel/sentinel_stub.go new file mode 100644 index 000000000..15b287b4b --- /dev/null +++ b/sentinel/sentinel_stub.go @@ -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 +}