diff --git a/acl/acl.go b/acl/acl.go index 2a09ad6b15..5b2be3f989 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -65,6 +65,13 @@ type ACL interface { // KeyringWrite determines if the keyring can be manipulated KeyringWrite() bool + // NodeRead checks for permission to read (discover) a given node. + NodeRead(string) bool + + // NodeWrite checks for permission to create or update (register) a + // given node. + NodeWrite(string) bool + // OperatorRead determines if the read-only Consul operator functions // can be used. OperatorRead() bool @@ -84,7 +91,8 @@ type ACL interface { // ServiceRead checks for permission to read a given service ServiceRead(string) bool - // ServiceWrite checks for permission to read a given service + // ServiceWrite checks for permission to create or update a given + // service ServiceWrite(string) bool // Snapshot checks for permission to take and restore snapshots. @@ -135,6 +143,14 @@ func (s *StaticACL) KeyringWrite() bool { return s.defaultAllow } +func (s *StaticACL) NodeRead(string) bool { + return s.defaultAllow +} + +func (s *StaticACL) NodeWrite(string) bool { + return s.defaultAllow +} + func (s *StaticACL) OperatorRead() bool { return s.defaultAllow } @@ -202,6 +218,9 @@ type PolicyACL struct { // keyRules contains the key policies keyRules *radix.Tree + // nodeRules contains the node policies + nodeRules *radix.Tree + // serviceRules contains the service policies serviceRules *radix.Tree @@ -226,6 +245,7 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) { p := &PolicyACL{ parent: parent, keyRules: radix.New(), + nodeRules: radix.New(), serviceRules: radix.New(), eventRules: radix.New(), preparedQueryRules: radix.New(), @@ -236,6 +256,11 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) { p.keyRules.Insert(kp.Prefix, kp.Policy) } + // Load the node policy + for _, np := range policy.Nodes { + p.nodeRules.Insert(np.Name, np.Policy) + } + // Load the service policy for _, sp := range policy.Services { p.serviceRules.Insert(sp.Name, sp.Policy) @@ -404,6 +429,42 @@ func (p *PolicyACL) OperatorRead() bool { } } +// NodeRead checks if reading (discovery) of a node is allowed +func (p *PolicyACL) NodeRead(name string) bool { + // Check for an exact rule or catch-all + _, rule, ok := p.nodeRules.LongestPrefix(name) + + if ok { + switch rule { + case PolicyRead, PolicyWrite: + return true + default: + return false + } + } + + // No matching rule, use the parent. + return p.parent.NodeRead(name) +} + +// NodeWrite checks if writing (registering) a node is allowed +func (p *PolicyACL) NodeWrite(name string) bool { + // Check for an exact rule or catch-all + _, rule, ok := p.nodeRules.LongestPrefix(name) + + if ok { + switch rule { + case PolicyWrite: + return true + default: + return false + } + } + + // No matching rule, use the parent. + return p.parent.NodeWrite(name) +} + // OperatorWrite determines if the state-changing operator functions are // allowed. func (p *PolicyACL) OperatorWrite() bool { diff --git a/acl/acl_test.go b/acl/acl_test.go index 9a6e710f41..e398e50c90 100644 --- a/acl/acl_test.go +++ b/acl/acl_test.go @@ -59,6 +59,12 @@ func TestStaticACL(t *testing.T) { if !all.KeyringWrite() { t.Fatalf("should allow") } + if !all.NodeRead("foobar") { + t.Fatalf("should allow") + } + if !all.NodeWrite("foobar") { + t.Fatalf("should allow") + } if !all.OperatorRead() { t.Fatalf("should allow") } @@ -111,6 +117,12 @@ func TestStaticACL(t *testing.T) { if none.KeyringWrite() { t.Fatalf("should not allow") } + if none.NodeRead("foobar") { + t.Fatalf("should not allow") + } + if none.NodeWrite("foobar") { + t.Fatalf("should not allow") + } if none.OperatorRead() { t.Fatalf("should now allow") } @@ -157,6 +169,12 @@ func TestStaticACL(t *testing.T) { if !manage.KeyringWrite() { t.Fatalf("should allow") } + if !manage.NodeRead("foobar") { + t.Fatalf("should allow") + } + if !manage.NodeWrite("foobar") { + t.Fatalf("should allow") + } if !manage.OperatorRead() { t.Fatalf("should allow") } @@ -560,3 +578,86 @@ func TestPolicyACL_Operator(t *testing.T) { } } } + +func TestPolicyACL_Node(t *testing.T) { + deny := DenyAll() + policyRoot := &Policy{ + Nodes: []*NodePolicy{ + &NodePolicy{ + Name: "root-nope", + Policy: PolicyDeny, + }, + &NodePolicy{ + Name: "root-ro", + Policy: PolicyRead, + }, + &NodePolicy{ + Name: "root-rw", + Policy: PolicyWrite, + }, + &NodePolicy{ + Name: "override", + Policy: PolicyDeny, + }, + }, + } + root, err := New(deny, policyRoot) + if err != nil { + t.Fatalf("err: %v", err) + } + + policy := &Policy{ + Nodes: []*NodePolicy{ + &NodePolicy{ + Name: "child-nope", + Policy: PolicyDeny, + }, + &NodePolicy{ + Name: "child-ro", + Policy: PolicyRead, + }, + &NodePolicy{ + Name: "child-rw", + Policy: PolicyWrite, + }, + &NodePolicy{ + Name: "override", + Policy: PolicyWrite, + }, + }, + } + acl, err := New(root, policy) + if err != nil { + t.Fatalf("err: %v", err) + } + + type nodecase struct { + inp string + read bool + write bool + } + cases := []nodecase{ + {"nope", false, false}, + {"root-nope", false, false}, + {"root-ro", true, false}, + {"root-rw", true, true}, + {"root-nope-prefix", false, false}, + {"root-ro-prefix", true, false}, + {"root-rw-prefix", true, true}, + {"child-nope", false, false}, + {"child-ro", true, false}, + {"child-rw", true, true}, + {"child-nope-prefix", false, false}, + {"child-ro-prefix", true, false}, + {"child-rw-prefix", true, true}, + {"override", true, true}, + } + for _, c := range cases { + if c.read != acl.NodeRead(c.inp) { + t.Fatalf("Read fail: %#v", c) + } + if c.write != acl.NodeWrite(c.inp) { + t.Fatalf("Write fail: %#v", c) + } + } +} diff --git a/acl/policy.go b/acl/policy.go index ae69067fea..c01dbaea18 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -17,6 +17,7 @@ const ( type Policy struct { ID string `hcl:"-"` Keys []*KeyPolicy `hcl:"key,expand"` + Nodes []*NodePolicy `hcl:"node,expand"` Services []*ServicePolicy `hcl:"service,expand"` Events []*EventPolicy `hcl:"event,expand"` PreparedQueries []*PreparedQueryPolicy `hcl:"query,expand"` @@ -34,14 +35,24 @@ func (k *KeyPolicy) GoString() string { return fmt.Sprintf("%#v", *k) } +// NodePolicy represents a policy for a node +type NodePolicy struct { + Name string `hcl:",key"` + Policy string +} + +func (n *NodePolicy) GoString() string { + return fmt.Sprintf("%#v", *n) +} + // ServicePolicy represents a policy for a service type ServicePolicy struct { Name string `hcl:",key"` Policy string } -func (k *ServicePolicy) GoString() string { - return fmt.Sprintf("%#v", *k) +func (s *ServicePolicy) GoString() string { + return fmt.Sprintf("%#v", *s) } // EventPolicy represents a user event policy. @@ -60,8 +71,8 @@ type PreparedQueryPolicy struct { Policy string } -func (e *PreparedQueryPolicy) GoString() string { - return fmt.Sprintf("%#v", *e) +func (p *PreparedQueryPolicy) GoString() string { + return fmt.Sprintf("%#v", *p) } // isPolicyValid makes sure the given string matches one of the valid policies. @@ -100,7 +111,14 @@ func Parse(rules string) (*Policy, error) { } } - // Validate the service policy + // Validate the node policies + for _, np := range p.Nodes { + if !isPolicyValid(np.Policy) { + return nil, fmt.Errorf("Invalid node policy: %#v", np) + } + } + + // Validate the service policies for _, sp := range p.Services { if !isPolicyValid(sp.Policy) { return nil, fmt.Errorf("Invalid service policy: %#v", sp) diff --git a/acl/policy_test.go b/acl/policy_test.go index f086c4d13f..23504ddbec 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -30,6 +30,15 @@ key "foo/bar/baz" { policy = "deny" } keyring = "deny" +node "" { + policy = "read" +} +node "foo" { + policy = "write" +} +node "bar" { + policy = "deny" +} operator = "deny" service "" { policy = "write" @@ -81,6 +90,20 @@ query "bar" { Policy: PolicyDeny, }, }, + Nodes: []*NodePolicy{ + &NodePolicy{ + Name: "", + Policy: PolicyRead, + }, + &NodePolicy{ + Name: "foo", + Policy: PolicyWrite, + }, + &NodePolicy{ + Name: "bar", + Policy: PolicyDeny, + }, + }, Operator: PolicyDeny, PreparedQueries: []*PreparedQueryPolicy{ &PreparedQueryPolicy{ @@ -146,6 +169,17 @@ func TestACLPolicy_Parse_JSON(t *testing.T) { } }, "keyring": "deny", + "node": { + "": { + "policy": "read" + }, + "foo": { + "policy": "write" + }, + "bar": { + "policy": "deny" + } + }, "operator": "deny", "query": { "": { @@ -201,6 +235,20 @@ func TestACLPolicy_Parse_JSON(t *testing.T) { Policy: PolicyDeny, }, }, + Nodes: []*NodePolicy{ + &NodePolicy{ + Name: "", + Policy: PolicyRead, + }, + &NodePolicy{ + Name: "foo", + Policy: PolicyWrite, + }, + &NodePolicy{ + Name: "bar", + Policy: PolicyDeny, + }, + }, Operator: PolicyDeny, PreparedQueries: []*PreparedQueryPolicy{ &PreparedQueryPolicy{ @@ -279,6 +327,7 @@ func TestACLPolicy_Bad_Policy(t *testing.T) { `event "" { policy = "nope" }`, `key "" { policy = "nope" }`, `keyring = "nope"`, + `node "" { policy = "nope" }`, `operator = "nope"`, `query "" { policy = "nope" }`, `service "" { policy = "nope" }`,