Skip to content

Commit

Permalink
Merge pull request #506 from hashicorp/f-service-acl
Browse files Browse the repository at this point in the history
Service ACL support
  • Loading branch information
armon committed Dec 1, 2014
2 parents 81d4e5c + 4861e4d commit 53de386
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 16 deletions.
69 changes: 67 additions & 2 deletions acl/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ type ACL interface {
// that deny a write.
KeyWritePrefix(string) bool

// ServiceWrite checks for permission to read a given service
ServiceWrite(string) bool

// ServiceRead checks for permission to read a given service
ServiceRead(string) bool

// ACLList checks for permission to list all the ACLs
ACLList() bool

Expand Down Expand Up @@ -73,6 +79,14 @@ func (s *StaticACL) KeyWritePrefix(string) bool {
return s.defaultAllow
}

func (s *StaticACL) ServiceRead(string) bool {
return s.defaultAllow
}

func (s *StaticACL) ServiceWrite(string) bool {
return s.defaultAllow
}

func (s *StaticACL) ACLList() bool {
return s.allowManage
}
Expand Down Expand Up @@ -119,20 +133,29 @@ type PolicyACL struct {

// keyRules contains the key policies
keyRules *radix.Tree

// serviceRules contains the service policies
serviceRules map[string]string
}

// New is used to construct a policy based ACL from a set of policies
// and a parent policy to resolve missing cases.
func New(parent ACL, policy *Policy) (*PolicyACL, error) {
p := &PolicyACL{
parent: parent,
keyRules: radix.New(),
parent: parent,
keyRules: radix.New(),
serviceRules: make(map[string]string, len(policy.Services)),
}

// Load the key policy
for _, kp := range policy.Keys {
p.keyRules.Insert(kp.Prefix, kp.Policy)
}

// Load the service policy
for _, sp := range policy.Services {
p.serviceRules[sp.Name] = sp.Policy
}
return p, nil
}

Expand Down Expand Up @@ -205,6 +228,48 @@ func (p *PolicyACL) KeyWritePrefix(prefix string) bool {
return p.parent.KeyWritePrefix(prefix)
}

// ServiceRead checks if reading (discovery) of a service is allowed
func (p *PolicyACL) ServiceRead(name string) bool {
// Check for an exact rule or catch-all
rule, ok := p.serviceRules[name]
if !ok {
rule, ok = p.serviceRules[""]
}
if ok {
switch rule {
case ServicePolicyWrite:
return true
case ServicePolicyRead:
return true
default:
return false
}
}

// No matching rule, use the parent.
return p.parent.ServiceRead(name)
}

// ServiceWrite checks if writing (registering) a service is allowed
func (p *PolicyACL) ServiceWrite(name string) bool {
// Check for an exact rule or catch-all
rule, ok := p.serviceRules[name]
if !ok {
rule, ok = p.serviceRules[""]
}
if ok {
switch rule {
case ServicePolicyWrite:
return true
default:
return false
}
}

// No matching rule, use the parent.
return p.parent.ServiceWrite(name)
}

// ACLList checks if listing of ACLs is allowed
func (p *PolicyACL) ACLList() bool {
return p.parent.ACLList()
Expand Down
97 changes: 93 additions & 4 deletions acl/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ func TestStaticACL(t *testing.T) {
if !all.KeyWrite("foobar") {
t.Fatalf("should allow")
}
if !all.ServiceRead("foobar") {
t.Fatalf("should allow")
}
if !all.ServiceWrite("foobar") {
t.Fatalf("should allow")
}
if all.ACLList() {
t.Fatalf("should not allow")
}
Expand All @@ -54,6 +60,12 @@ func TestStaticACL(t *testing.T) {
if none.KeyWrite("foobar") {
t.Fatalf("should not allow")
}
if none.ServiceRead("foobar") {
t.Fatalf("should not allow")
}
if none.ServiceWrite("foobar") {
t.Fatalf("should not allow")
}
if none.ACLList() {
t.Fatalf("should not noneow")
}
Expand All @@ -67,6 +79,12 @@ func TestStaticACL(t *testing.T) {
if !manage.KeyWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.ServiceRead("foobar") {
t.Fatalf("should allow")
}
if !manage.ServiceWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.ACLList() {
t.Fatalf("should allow")
}
Expand Down Expand Up @@ -96,19 +114,33 @@ func TestPolicyACL(t *testing.T) {
Policy: KeyPolicyRead,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Name: "",
Policy: ServicePolicyWrite,
},
&ServicePolicy{
Name: "foo",
Policy: ServicePolicyRead,
},
&ServicePolicy{
Name: "bar",
Policy: ServicePolicyDeny,
},
},
}
acl, err := New(all, policy)
if err != nil {
t.Fatalf("err: %v", err)
}

type tcase struct {
type keycase struct {
inp string
read bool
write bool
writePrefix bool
}
cases := []tcase{
cases := []keycase{
{"other", true, true, true},
{"foo/test", true, true, true},
{"foo/priv/test", false, false, false},
Expand All @@ -128,6 +160,26 @@ func TestPolicyACL(t *testing.T) {
t.Fatalf("Write prefix fail: %#v", c)
}
}

// Test the services
type servicecase struct {
inp string
read bool
write bool
}
scases := []servicecase{
{"other", true, true},
{"foo", true, false},
{"bar", false, false},
}
for _, c := range scases {
if c.read != acl.ServiceRead(c.inp) {
t.Fatalf("Read fail: %#v", c)
}
if c.write != acl.ServiceWrite(c.inp) {
t.Fatalf("Write fail: %#v", c)
}
}
}

func TestPolicyACL_Parent(t *testing.T) {
Expand All @@ -143,6 +195,16 @@ func TestPolicyACL_Parent(t *testing.T) {
Policy: KeyPolicyRead,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Name: "other",
Policy: ServicePolicyWrite,
},
&ServicePolicy{
Name: "foo",
Policy: ServicePolicyRead,
},
},
}
root, err := New(deny, policyRoot)
if err != nil {
Expand All @@ -164,19 +226,25 @@ func TestPolicyACL_Parent(t *testing.T) {
Policy: KeyPolicyRead,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Name: "bar",
Policy: ServicePolicyDeny,
},
},
}
acl, err := New(root, policy)
if err != nil {
t.Fatalf("err: %v", err)
}

type tcase struct {
type keycase struct {
inp string
read bool
write bool
writePrefix bool
}
cases := []tcase{
cases := []keycase{
{"other", false, false, false},
{"foo/test", true, true, true},
{"foo/priv/test", true, false, false},
Expand All @@ -194,4 +262,25 @@ func TestPolicyACL_Parent(t *testing.T) {
t.Fatalf("Write prefix fail: %#v", c)
}
}

// Test the services
type servicecase struct {
inp string
read bool
write bool
}
scases := []servicecase{
{"fail", false, false},
{"other", true, true},
{"foo", true, false},
{"bar", false, false},
}
for _, c := range scases {
if c.read != acl.ServiceRead(c.inp) {
t.Fatalf("Read fail: %#v", c)
}
if c.write != acl.ServiceWrite(c.inp) {
t.Fatalf("Write fail: %#v", c)
}
}
}
37 changes: 32 additions & 5 deletions acl/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@ package acl

import (
"fmt"

"github.com/hashicorp/hcl"
)

const (
KeyPolicyDeny = "deny"
KeyPolicyRead = "read"
KeyPolicyWrite = "write"
KeyPolicyDeny = "deny"
KeyPolicyRead = "read"
KeyPolicyWrite = "write"
ServicePolicyDeny = "deny"
ServicePolicyRead = "read"
ServicePolicyWrite = "write"
)

// Policy is used to represent the policy specified by
// an ACL configuration.
type Policy struct {
ID string `hcl:"-"`
Keys []*KeyPolicy `hcl:"key,expand"`
ID string `hcl:"-"`
Keys []*KeyPolicy `hcl:"key,expand"`
Services []*ServicePolicy `hcl:"service,expand"`
}

// KeyPolicy represents a policy for a key
Expand All @@ -28,6 +33,16 @@ func (k *KeyPolicy) GoString() string {
return fmt.Sprintf("%#v", *k)
}

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

// Parse is used to parse the specified ACL rules into an
// intermediary set of policies, before being compiled into
// the ACL
Expand All @@ -53,5 +68,17 @@ func Parse(rules string) (*Policy, error) {
return nil, fmt.Errorf("Invalid key policy: %#v", kp)
}
}

// Validate the service policy
for _, sp := range p.Services {
switch sp.Policy {
case ServicePolicyDeny:
case ServicePolicyRead:
case ServicePolicyWrite:
default:
return nil, fmt.Errorf("Invalid service policy: %#v", sp)
}
}

return p, nil
}
Loading

0 comments on commit 53de386

Please sign in to comment.