acl: Adding policy parsing with tests

This commit is contained in:
Armon Dadgar 2017-08-03 17:41:33 -07:00
parent 53295748aa
commit 4c3373cdef
2 changed files with 323 additions and 0 deletions

157
acl/policy.go Normal file
View File

@ -0,0 +1,157 @@
package acl
import (
"fmt"
"github.com/hashicorp/hcl"
)
const (
PolicyDeny = "deny"
PolicyRead = "read"
PolicyWrite = "write"
)
const (
NamespaceCapabilityDeny = "deny"
NamespaceCapabilityListJobs = "list-jobs"
NamespaceCapabilityReadJob = "read-job"
NamespaceCapabilitySubmitJob = "submit-job"
NamespaceCapabilityReadLogs = "read-logs"
NamespaceCapabilityReadFS = "read-fs"
)
// Policy represents a parsed HCL or JSON policy.
type Policy struct {
Namespaces []*NamespacePolicy `hcl:"namespace,expand"`
Agent *AgentPolicy `hcl:"agent"`
Node *NodePolicy `hcl:"node"`
Operator *OperatorPolicy `hcl:"operator"`
Raw string `hcl:"-"`
}
// NamespacePolicy is the policy for a specific namespace
type NamespacePolicy struct {
Name string `hcl:",key"`
Policy string
Capabilities []string
}
type AgentPolicy struct {
Policy string
}
type NodePolicy struct {
Policy string
}
type OperatorPolicy struct {
Policy string
}
// isPolicyValid makes sure the given string matches one of the valid policies.
func isPolicyValid(policy string) bool {
switch policy {
case PolicyDeny:
return true
case PolicyRead:
return true
case PolicyWrite:
return true
default:
return false
}
}
// isNamespaceCapabilityValid ensures the given capability is valid for a namespace policy
func isNamespaceCapabilityValid(cap string) bool {
switch cap {
case NamespaceCapabilityDeny:
return true
case NamespaceCapabilityListJobs:
return true
case NamespaceCapabilityReadJob:
return true
case NamespaceCapabilitySubmitJob:
return true
case NamespaceCapabilityReadLogs:
return true
case NamespaceCapabilityReadFS:
return true
default:
return false
}
}
// expandNamespacePolicy provides the equivalent set of capabilities for
// a namespace policy
func expandNamespacePolicy(policy string) []string {
switch policy {
case PolicyDeny:
return []string{NamespaceCapabilityDeny}
case PolicyRead:
return []string{
NamespaceCapabilityListJobs,
NamespaceCapabilityReadJob,
}
case PolicyWrite:
return []string{
NamespaceCapabilityListJobs,
NamespaceCapabilityReadJob,
NamespaceCapabilitySubmitJob,
NamespaceCapabilityReadLogs,
NamespaceCapabilityReadFS,
}
default:
return nil
}
}
// 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) {
// Decode the rules
p := &Policy{Raw: rules}
if rules == "" {
// Hot path for empty rules
return p, nil
}
// Attempt to parse
if err := hcl.Decode(p, rules); err != nil {
return nil, fmt.Errorf("Failed to parse ACL Policy: %v", err)
}
// Validate the policy
for _, ns := range p.Namespaces {
if ns.Policy != "" && !isPolicyValid(ns.Policy) {
return nil, fmt.Errorf("Invalid namespace policy: %#v", ns)
}
for _, cap := range ns.Capabilities {
if !isNamespaceCapabilityValid(cap) {
return nil, fmt.Errorf("Invalid namespace capability: %#v", ns)
}
}
// Expand the short hand policy to the capabilities and
// add to any existing capabilities
if ns.Policy != "" {
extraCap := expandNamespacePolicy(ns.Policy)
ns.Capabilities = append(ns.Capabilities, extraCap...)
}
}
if p.Agent != nil && !isPolicyValid(p.Agent.Policy) {
return nil, fmt.Errorf("Invalid agent policy: %#v", p.Agent)
}
if p.Node != nil && !isPolicyValid(p.Node.Policy) {
return nil, fmt.Errorf("Invalid node policy: %#v", p.Node)
}
if p.Operator != nil && !isPolicyValid(p.Operator.Policy) {
return nil, fmt.Errorf("Invalid operator policy: %#v", p.Operator)
}
return p, nil
}

166
acl/policy_test.go Normal file
View File

@ -0,0 +1,166 @@
package acl
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParse(t *testing.T) {
type tcase struct {
Raw string
ErrStr string
Expect *Policy
}
tcases := []tcase{
{
`
namespace "default" {
policy = "read"
}
`,
"",
&Policy{
Namespaces: []*NamespacePolicy{
&NamespacePolicy{
Name: "default",
Policy: PolicyRead,
Capabilities: []string{
NamespaceCapabilityListJobs,
NamespaceCapabilityReadJob,
},
},
},
},
},
{
`
namespace "default" {
policy = "read"
}
namespace "other" {
policy = "write"
}
namespace "secret" {
capabilities = ["deny", "read-logs"]
}
agent {
policy = "read"
}
node {
policy = "write"
}
operator {
policy = "deny"
}
`,
"",
&Policy{
Namespaces: []*NamespacePolicy{
&NamespacePolicy{
Name: "default",
Policy: PolicyRead,
Capabilities: []string{
NamespaceCapabilityListJobs,
NamespaceCapabilityReadJob,
},
},
&NamespacePolicy{
Name: "other",
Policy: PolicyWrite,
Capabilities: []string{
NamespaceCapabilityListJobs,
NamespaceCapabilityReadJob,
NamespaceCapabilitySubmitJob,
NamespaceCapabilityReadLogs,
NamespaceCapabilityReadFS,
},
},
&NamespacePolicy{
Name: "secret",
Capabilities: []string{
NamespaceCapabilityDeny,
NamespaceCapabilityReadLogs,
},
},
},
Agent: &AgentPolicy{
Policy: PolicyRead,
},
Node: &NodePolicy{
Policy: PolicyWrite,
},
Operator: &OperatorPolicy{
Policy: PolicyDeny,
},
},
},
{
`
namespace "default" {
policy = "foo"
}
`,
"Invalid namespace policy",
nil,
},
{
`
namespace "default" {
capabilities = ["deny", "foo"]
}
`,
"Invalid namespace capability",
nil,
},
{
`
agent {
policy = "foo"
}
`,
"Invalid agent policy",
nil,
},
{
`
node {
policy = "foo"
}
`,
"Invalid node policy",
nil,
},
{
`
operator {
policy = "foo"
}
`,
"Invalid operator policy",
nil,
},
}
for idx, tc := range tcases {
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
p, err := Parse(tc.Raw)
if err != nil {
if tc.ErrStr == "" {
t.Fatalf("Unexpected err: %v", err)
}
if !strings.Contains(err.Error(), tc.ErrStr) {
t.Fatalf("Unexpected err: %v", err)
}
return
}
if err == nil && tc.ErrStr != "" {
t.Fatalf("Missing expected err")
}
tc.Expect.Raw = tc.Raw
assert.EqualValues(t, tc.Expect, p)
})
}
}