acl: Adding policy parsing with tests
This commit is contained in:
parent
53295748aa
commit
4c3373cdef
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue