Commit 4def9298 authored by Armon Dadgar's avatar Armon Dadgar
Browse files

acl: Adding compiled ACL object

parent 39fa8c55
Showing with 398 additions and 0 deletions
+398 -0
acl/acl.go 0 → 100644
package acl
import (
iradix "github.com/hashicorp/go-immutable-radix"
)
// capabilitySet is a type wrapper to help managing a set of capabilities
type capabilitySet map[string]struct{}
func (c capabilitySet) Check(k string) bool {
_, ok := c[k]
return ok
}
func (c capabilitySet) Set(k string) {
c[k] = struct{}{}
}
func (c capabilitySet) Clear() {
for cap := range c {
delete(c, cap)
}
}
// ACL object is used to convert a set of policies into a structure that
// can be efficiently evaluated to determine if an action is allowed.
type ACL struct {
// management tokens are allowed to do anything
management bool
// namespaces maps a namespace to a capabilitySet
namespaces *iradix.Tree
agent string
node string
operator string
}
// maxPrivilege returns the policy which grants the most privilege
func maxPrivilege(a, b string) string {
switch {
case a == PolicyDeny || b == PolicyDeny:
return PolicyDeny
case a == PolicyWrite || b == PolicyWrite:
return PolicyWrite
case a == PolicyRead || b == PolicyRead:
return PolicyRead
default:
return ""
}
}
// NewACL compiles a set of policies into an ACL object
func NewACL(management bool, policies []*Policy) (*ACL, error) {
// Hot-path management tokens
if management {
return &ACL{management: true}, nil
}
// Create the ACL object
acl := &ACL{}
nsTxn := iradix.New().Txn()
for _, policy := range policies {
NAMESPACES:
for _, ns := range policy.Namespaces {
// Check for existing capabilities
var capabilities capabilitySet
raw, ok := nsTxn.Get([]byte(ns.Name))
if ok {
capabilities = raw.(capabilitySet)
} else {
capabilities = make(capabilitySet)
nsTxn.Insert([]byte(ns.Name), capabilities)
}
// Deny always takes precedence
if capabilities.Check(NamespaceCapabilityDeny) {
continue NAMESPACES
}
// Add in all the capabilities
for _, cap := range ns.Capabilities {
if cap == NamespaceCapabilityDeny {
// Overwrite any existing capabilities
capabilities.Clear()
capabilities.Set(NamespaceCapabilityDeny)
continue NAMESPACES
}
capabilities.Set(cap)
}
}
// Take the maximum privilege for agent, node, and operator
if policy.Agent != nil {
acl.agent = maxPrivilege(acl.agent, policy.Agent.Policy)
}
if policy.Node != nil {
acl.node = maxPrivilege(acl.node, policy.Node.Policy)
}
if policy.Operator != nil {
acl.operator = maxPrivilege(acl.operator, policy.Operator.Policy)
}
}
// Finalize the namespaces
acl.namespaces = nsTxn.Commit()
return acl, nil
}
// AllowNamespaceOperation checks if a given operation is allowed for a namespace
func (a *ACL) AllowNamespaceOperation(ns string, op string) bool {
// Hot path management tokens
if a.management {
return true
}
// Check for a matching capability set
raw, ok := a.namespaces.Get([]byte(ns))
if !ok {
return false
}
// Check if the capability has been granted
capabilities := raw.(capabilitySet)
return capabilities.Check(op)
}
// AllowAgentRead checks if read operations are allowed for an agent
func (a *ACL) AllowAgentRead() bool {
switch {
case a.management:
return true
case a.agent == PolicyWrite:
return true
case a.agent == PolicyRead:
return true
default:
return false
}
}
// AllowAgentWrite checks if write operations are allowed for an agent
func (a *ACL) AllowAgentWrite() bool {
switch {
case a.management:
return true
case a.agent == PolicyWrite:
return true
default:
return false
}
}
// AllowNodeRead checks if read operations are allowed for a node
func (a *ACL) AllowNodeRead() bool {
switch {
case a.management:
return true
case a.node == PolicyWrite:
return true
case a.node == PolicyRead:
return true
default:
return false
}
}
// AllowNodeWrite checks if write operations are allowed for a node
func (a *ACL) AllowNodeWrite() bool {
switch {
case a.management:
return true
case a.node == PolicyWrite:
return true
default:
return false
}
}
// AllowOperatorRead checks if read operations are allowed for a operator
func (a *ACL) AllowOperatorRead() bool {
switch {
case a.management:
return true
case a.operator == PolicyWrite:
return true
case a.operator == PolicyRead:
return true
default:
return false
}
}
// AllowOperatorWrite checks if write operations are allowed for a operator
func (a *ACL) AllowOperatorWrite() bool {
switch {
case a.management:
return true
case a.operator == PolicyWrite:
return true
default:
return false
}
}
package acl
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCapabilitySet(t *testing.T) {
var cs capabilitySet = make(map[string]struct{})
// Check no capabilities by default
if cs.Check(PolicyDeny) {
t.Fatalf("unexpected check")
}
// Do a set and check
cs.Set(PolicyDeny)
if !cs.Check(PolicyDeny) {
t.Fatalf("missing check")
}
// Clear and check
cs.Clear()
if cs.Check(PolicyDeny) {
t.Fatalf("unexpected check")
}
}
func TestMaxPrivilege(t *testing.T) {
type tcase struct {
Privilege string
PrecedenceOver []string
}
tcases := []tcase{
{
PolicyDeny,
[]string{PolicyDeny, PolicyWrite, PolicyRead, ""},
},
{
PolicyWrite,
[]string{PolicyWrite, PolicyRead, ""},
},
{
PolicyRead,
[]string{PolicyRead, ""},
},
}
for idx1, tc := range tcases {
for idx2, po := range tc.PrecedenceOver {
if maxPrivilege(tc.Privilege, po) != tc.Privilege {
t.Fatalf("failed %d %d", idx1, idx2)
}
if maxPrivilege(po, tc.Privilege) != tc.Privilege {
t.Fatalf("failed %d %d", idx1, idx2)
}
}
}
}
func TestACLManagement(t *testing.T) {
// Create management ACL
acl, err := NewACL(true, nil)
assert.Nil(t, err)
// Check default namespace rights
assert.Equal(t, true, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
assert.Equal(t, true, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
// Check non-specified namespace
assert.Equal(t, true, acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
// Check the other simpler operations
assert.Equal(t, true, acl.AllowAgentRead())
assert.Equal(t, true, acl.AllowAgentWrite())
assert.Equal(t, true, acl.AllowNodeRead())
assert.Equal(t, true, acl.AllowNodeWrite())
assert.Equal(t, true, acl.AllowOperatorRead())
assert.Equal(t, true, acl.AllowOperatorWrite())
}
func TestACLMerge(t *testing.T) {
// Merge read + write policy
p1, err := Parse(readAll)
assert.Nil(t, err)
p2, err := Parse(writeAll)
assert.Nil(t, err)
acl, err := NewACL(false, []*Policy{p1, p2})
assert.Nil(t, err)
// Check default namespace rights
assert.Equal(t, true, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
assert.Equal(t, true, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
// Check non-specified namespace
assert.Equal(t, false, acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
// Check the other simpler operations
assert.Equal(t, true, acl.AllowAgentRead())
assert.Equal(t, true, acl.AllowAgentWrite())
assert.Equal(t, true, acl.AllowNodeRead())
assert.Equal(t, true, acl.AllowNodeWrite())
assert.Equal(t, true, acl.AllowOperatorRead())
assert.Equal(t, true, acl.AllowOperatorWrite())
// Merge read + blank
p3, err := Parse("")
assert.Nil(t, err)
acl, err = NewACL(false, []*Policy{p1, p3})
assert.Nil(t, err)
// Check default namespace rights
assert.Equal(t, true, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
assert.Equal(t, false, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
// Check non-specified namespace
assert.Equal(t, false, acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
// Check the other simpler operations
assert.Equal(t, true, acl.AllowAgentRead())
assert.Equal(t, false, acl.AllowAgentWrite())
assert.Equal(t, true, acl.AllowNodeRead())
assert.Equal(t, false, acl.AllowNodeWrite())
assert.Equal(t, true, acl.AllowOperatorRead())
assert.Equal(t, false, acl.AllowOperatorWrite())
// Merge read + deny
p4, err := Parse(denyAll)
assert.Nil(t, err)
acl, err = NewACL(false, []*Policy{p1, p4})
assert.Nil(t, err)
// Check default namespace rights
assert.Equal(t, false, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
assert.Equal(t, false, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
// Check non-specified namespace
assert.Equal(t, false, acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
// Check the other simpler operations
assert.Equal(t, false, acl.AllowAgentRead())
assert.Equal(t, false, acl.AllowAgentWrite())
assert.Equal(t, false, acl.AllowNodeRead())
assert.Equal(t, false, acl.AllowNodeWrite())
assert.Equal(t, false, acl.AllowOperatorRead())
assert.Equal(t, false, acl.AllowOperatorWrite())
}
var readAll = `
namespace "default" {
policy = "read"
}
agent {
policy = "read"
}
node {
policy = "read"
}
operator {
policy = "read"
}
`
var writeAll = `
namespace "default" {
policy = "write"
}
agent {
policy = "write"
}
node {
policy = "write"
}
operator {
policy = "write"
}
`
var denyAll = `
namespace "default" {
policy = "deny"
}
agent {
policy = "deny"
}
node {
policy = "deny"
}
operator {
policy = "deny"
}
`
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment