diff --git a/acl/acl.go b/acl/acl.go index 5b2be3f989..46288a9d7d 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -95,6 +95,13 @@ type ACL interface { // service ServiceWrite(string) bool + // SessionRead checks for permission to read sessions for a given node. + SessionRead(string) bool + + // SessionWrite checks for permission to create sessions for a given + // node. + SessionWrite(string) bool + // Snapshot checks for permission to take and restore snapshots. Snapshot() bool } @@ -175,6 +182,14 @@ func (s *StaticACL) ServiceWrite(string) bool { return s.defaultAllow } +func (s *StaticACL) SessionRead(string) bool { + return s.defaultAllow +} + +func (s *StaticACL) SessionWrite(string) bool { + return s.defaultAllow +} + func (s *StaticACL) Snapshot() bool { return s.allowManage } @@ -224,6 +239,9 @@ type PolicyACL struct { // serviceRules contains the service policies serviceRules *radix.Tree + // sessionRules contains the session policies + sessionRules *radix.Tree + // eventRules contains the user event policies eventRules *radix.Tree @@ -247,6 +265,7 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) { keyRules: radix.New(), nodeRules: radix.New(), serviceRules: radix.New(), + sessionRules: radix.New(), eventRules: radix.New(), preparedQueryRules: radix.New(), } @@ -266,6 +285,11 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) { p.serviceRules.Insert(sp.Name, sp.Policy) } + // Load the session policy + for _, sp := range policy.Sessions { + p.sessionRules.Insert(sp.Node, sp.Policy) + } + // Load the event policy for _, ep := range policy.Events { p.eventRules.Insert(ep.Event, ep.Policy) @@ -547,3 +571,39 @@ func (p *PolicyACL) ServiceWrite(name string) bool { // No matching rule, use the parent. return p.parent.ServiceWrite(name) } + +// SessionRead checks for permission to read sessions for a given node. +func (p *PolicyACL) SessionRead(node string) bool { + // Check for an exact rule or catch-all + _, rule, ok := p.sessionRules.LongestPrefix(node) + + if ok { + switch rule { + case PolicyRead, PolicyWrite: + return true + default: + return false + } + } + + // No matching rule, use the parent. + return p.parent.SessionRead(node) +} + +// SessionWrite checks for permission to create sessions for a given node. +func (p *PolicyACL) SessionWrite(node string) bool { + // Check for an exact rule or catch-all + _, rule, ok := p.sessionRules.LongestPrefix(node) + + if ok { + switch rule { + case PolicyWrite: + return true + default: + return false + } + } + + // No matching rule, use the parent. + return p.parent.SessionWrite(node) +} diff --git a/acl/acl_test.go b/acl/acl_test.go index e398e50c90..197f9390cf 100644 --- a/acl/acl_test.go +++ b/acl/acl_test.go @@ -83,6 +83,12 @@ func TestStaticACL(t *testing.T) { if !all.ServiceWrite("foobar") { t.Fatalf("should allow") } + if !all.SessionRead("foobar") { + t.Fatalf("should allow") + } + if !all.SessionWrite("foobar") { + t.Fatalf("should allow") + } if all.Snapshot() { t.Fatalf("should not allow") } @@ -141,6 +147,12 @@ func TestStaticACL(t *testing.T) { if none.ServiceWrite("foobar") { t.Fatalf("should not allow") } + if none.SessionRead("foobar") { + t.Fatalf("should not allow") + } + if none.SessionWrite("foobar") { + t.Fatalf("should not allow") + } if none.Snapshot() { t.Fatalf("should not allow") } @@ -193,6 +205,12 @@ func TestStaticACL(t *testing.T) { if !manage.ServiceWrite("foobar") { t.Fatalf("should allow") } + if !manage.SessionRead("foobar") { + t.Fatalf("should allow") + } + if !manage.SessionWrite("foobar") { + t.Fatalf("should allow") + } if !manage.Snapshot() { t.Fatalf("should allow") } @@ -661,3 +679,86 @@ func TestPolicyACL_Node(t *testing.T) { } } } + +func TestPolicyACL_Session(t *testing.T) { + deny := DenyAll() + policyRoot := &Policy{ + Sessions: []*SessionPolicy{ + &SessionPolicy{ + Node: "root-nope", + Policy: PolicyDeny, + }, + &SessionPolicy{ + Node: "root-ro", + Policy: PolicyRead, + }, + &SessionPolicy{ + Node: "root-rw", + Policy: PolicyWrite, + }, + &SessionPolicy{ + Node: "override", + Policy: PolicyDeny, + }, + }, + } + root, err := New(deny, policyRoot) + if err != nil { + t.Fatalf("err: %v", err) + } + + policy := &Policy{ + Sessions: []*SessionPolicy{ + &SessionPolicy{ + Node: "child-nope", + Policy: PolicyDeny, + }, + &SessionPolicy{ + Node: "child-ro", + Policy: PolicyRead, + }, + &SessionPolicy{ + Node: "child-rw", + Policy: PolicyWrite, + }, + &SessionPolicy{ + Node: "override", + Policy: PolicyWrite, + }, + }, + } + acl, err := New(root, policy) + if err != nil { + t.Fatalf("err: %v", err) + } + + type sessioncase struct { + inp string + read bool + write bool + } + cases := []sessioncase{ + {"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.SessionRead(c.inp) { + t.Fatalf("Read fail: %#v", c) + } + if c.write != acl.SessionWrite(c.inp) { + t.Fatalf("Write fail: %#v", c) + } + } +} diff --git a/acl/policy.go b/acl/policy.go index c01dbaea18..13d0d99124 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -19,6 +19,7 @@ type Policy struct { Keys []*KeyPolicy `hcl:"key,expand"` Nodes []*NodePolicy `hcl:"node,expand"` Services []*ServicePolicy `hcl:"service,expand"` + Sessions []*SessionPolicy `hcl:"session,expand"` Events []*EventPolicy `hcl:"event,expand"` PreparedQueries []*PreparedQueryPolicy `hcl:"query,expand"` Keyring string `hcl:"keyring"` @@ -55,6 +56,17 @@ func (s *ServicePolicy) GoString() string { return fmt.Sprintf("%#v", *s) } +// SessionPolicy represents a policy for making sessions tied to specific node +// name prefixes. +type SessionPolicy struct { + Node string `hcl:",key"` + Policy string +} + +func (s *SessionPolicy) GoString() string { + return fmt.Sprintf("%#v", *s) +} + // EventPolicy represents a user event policy. type EventPolicy struct { Event string `hcl:",key"` @@ -125,6 +137,13 @@ func Parse(rules string) (*Policy, error) { } } + // Validate the session policies + for _, sp := range p.Sessions { + if !isPolicyValid(sp.Policy) { + return nil, fmt.Errorf("Invalid session policy: %#v", sp) + } + } + // Validate the user event policies for _, ep := range p.Events { if !isPolicyValid(ep.Policy) { diff --git a/acl/policy_test.go b/acl/policy_test.go index 23504ddbec..9120049168 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -46,6 +46,12 @@ service "" { service "foo" { policy = "read" } +session "foo" { + policy = "write" +} +session "bar" { + policy = "deny" +} query "" { policy = "read" } @@ -129,6 +135,16 @@ query "bar" { Policy: PolicyRead, }, }, + Sessions: []*SessionPolicy{ + &SessionPolicy{ + Node: "foo", + Policy: PolicyWrite, + }, + &SessionPolicy{ + Node: "bar", + Policy: PolicyDeny, + }, + }, } out, err := Parse(inp) @@ -199,6 +215,14 @@ func TestACLPolicy_Parse_JSON(t *testing.T) { "foo": { "policy": "read" } + }, + "session": { + "foo": { + "policy": "write" + }, + "bar": { + "policy": "deny" + } } }` exp := &Policy{ @@ -274,6 +298,16 @@ func TestACLPolicy_Parse_JSON(t *testing.T) { Policy: PolicyRead, }, }, + Sessions: []*SessionPolicy{ + &SessionPolicy{ + Node: "foo", + Policy: PolicyWrite, + }, + &SessionPolicy{ + Node: "bar", + Policy: PolicyDeny, + }, + }, } out, err := Parse(inp) @@ -331,6 +365,7 @@ func TestACLPolicy_Bad_Policy(t *testing.T) { `operator = "nope"`, `query "" { policy = "nope" }`, `service "" { policy = "nope" }`, + `session "" { policy = "nope" }`, } for _, c := range cases { _, err := Parse(c)