vault: Adding ACL representation

This commit is contained in:
Armon Dadgar 2015-03-17 18:31:20 -07:00
parent ddab671bf4
commit 99abc11ec5
3 changed files with 269 additions and 0 deletions

105
vault/acl.go Normal file
View File

@ -0,0 +1,105 @@
package vault
import (
"github.com/armon/go-radix"
"github.com/hashicorp/vault/logical"
)
// operationPolicyLevel is used to map each logical operation
// into the minimum required permissions to allow the operation.
var operationPolicyLevel = map[logical.Operation]int{
logical.ReadOperation: pathPolicyLevel[PathPolicyRead],
logical.WriteOperation: pathPolicyLevel[PathPolicyWrite],
logical.DeleteOperation: pathPolicyLevel[PathPolicyWrite],
logical.ListOperation: pathPolicyLevel[PathPolicyRead],
logical.RevokeOperation: pathPolicyLevel[PathPolicyWrite],
logical.RenewOperation: pathPolicyLevel[PathPolicyRead],
logical.HelpOperation: pathPolicyLevel[PathPolicyDeny],
}
// ACL is used to wrap a set of policies to provide
// an efficient interface for access control.
type ACL struct {
// pathRules contains the path policies
pathRules *radix.Tree
// root is enabled if the "root" named policy is present.
root bool
}
// New is used to construct a policy based ACL from a set of policies.
func NewACL(policies []*Policy) (*ACL, error) {
// Initialize
a := &ACL{
pathRules: radix.New(),
root: false,
}
// Inject each policy
for _, policy := range policies {
// Check if this is root
if policy.Name == "root" {
a.root = true
}
for _, pp := range policy.Paths {
// Convert to a policy level
policyLevel := pathPolicyLevel[pp.Policy]
// Check for an existing policy
raw, ok := a.pathRules.Get(pp.Prefix)
if !ok {
a.pathRules.Insert(pp.Prefix, policyLevel)
continue
}
existing := raw.(int)
// Check if this policy is a higher access level,
// we want to store the highest permission permitted.
if policyLevel > existing {
a.pathRules.Insert(pp.Prefix, policyLevel)
}
}
}
return a, nil
}
// AllowOperation is used to check if the given operation is permitted
func (a *ACL) AllowOperation(op logical.Operation, path string) bool {
// Fast-path root
if a.root {
return true
}
// Find a matching rule, default deny if no match
policyLevel := 0
_, rule, ok := a.pathRules.LongestPrefix(path)
if ok {
policyLevel = rule.(int)
}
// Convert the operation to a minimum required level
requiredLevel := operationPolicyLevel[op]
// Check if the minimum permissions are met
return policyLevel >= requiredLevel
}
// RootPrivilege checks if the user has root level permission
// to given path. This requires that the user be root, or that
// sudo privilege is available on that path.
func (a *ACL) RootPrivilege(path string) bool {
// Fast-path root
if a.root {
return true
}
// Check the rules for a match
_, rule, ok := a.pathRules.LongestPrefix(path)
if !ok {
return false
}
// Check the policy level
policyLevel := rule.(int)
return policyLevel == pathPolicyLevel[PathPolicySudo]
}

155
vault/acl_test.go Normal file
View File

@ -0,0 +1,155 @@
package vault
import (
"testing"
"github.com/hashicorp/vault/logical"
)
func TestACL_Root(t *testing.T) {
// Create the root policy ACL
policy := []*Policy{&Policy{Name: "root"}}
acl, err := NewACL(policy)
if err != nil {
t.Fatalf("err: %v", err)
}
if !acl.RootPrivilege("sys/mount/foo") {
t.Fatalf("expected root")
}
if !acl.AllowOperation(logical.WriteOperation, "sys/mount/foo") {
t.Fatalf("expected permission")
}
}
func TestACL_Single(t *testing.T) {
policy, err := Parse(aclPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
acl, err := NewACL([]*Policy{policy})
if err != nil {
t.Fatalf("err: %v", err)
}
if acl.RootPrivilege("sys/mount/foo") {
t.Fatalf("unexpected root")
}
type tcase struct {
op logical.Operation
path string
expect bool
}
tcases := []tcase{
{logical.ReadOperation, "root", false},
{logical.HelpOperation, "root", true},
{logical.ReadOperation, "dev/foo", true},
{logical.WriteOperation, "dev/foo", true},
{logical.DeleteOperation, "stage/foo", true},
{logical.WriteOperation, "stage/aws/foo", false},
{logical.WriteOperation, "stage/aws/policy/foo", true},
{logical.DeleteOperation, "prod/foo", false},
{logical.WriteOperation, "prod/foo", false},
{logical.ReadOperation, "prod/foo", true},
{logical.ListOperation, "prod/foo", true},
{logical.ReadOperation, "prod/aws/foo", false},
}
for _, tc := range tcases {
out := acl.AllowOperation(tc.op, tc.path)
if out != tc.expect {
t.Fatalf("bad: case %#v: %v", tc, out)
}
}
}
func TestACL_Layered(t *testing.T) {
policy1, err := Parse(aclPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
policy2, err := Parse(aclPolicy2)
if err != nil {
t.Fatalf("err: %v", err)
}
acl, err := NewACL([]*Policy{policy1, policy2})
if err != nil {
t.Fatalf("err: %v", err)
}
if acl.RootPrivilege("sys/mount/foo") {
t.Fatalf("unexpected root")
}
type tcase struct {
op logical.Operation
path string
expect bool
}
tcases := []tcase{
{logical.ReadOperation, "root", false},
{logical.HelpOperation, "root", true},
{logical.ReadOperation, "dev/hide/foo", false},
{logical.WriteOperation, "dev/hide/foo", false},
{logical.DeleteOperation, "stage/foo", true},
{logical.WriteOperation, "stage/aws/foo", false},
{logical.WriteOperation, "stage/aws/policy/foo", true},
{logical.DeleteOperation, "prod/foo", true},
{logical.WriteOperation, "prod/foo", true},
{logical.ReadOperation, "prod/foo", true},
{logical.ListOperation, "prod/foo", true},
{logical.ReadOperation, "prod/aws/foo", false},
}
for _, tc := range tcases {
out := acl.AllowOperation(tc.op, tc.path)
if out != tc.expect {
t.Fatalf("bad: case %#v: %v", tc, out)
}
}
}
var aclPolicy = `
name = "dev"
path "dev/" {
policy = "sudo"
}
path "stage/" {
policy = "write"
}
path "stage/aws/" {
policy = "read"
}
path "stage/aws/policy/" {
policy = "sudo"
}
path "prod/" {
policy = "read"
}
path "prod/aws/" {
policy = "deny"
}
`
var aclPolicy2 = `
name = "ops"
path "dev/hide/" {
policy = "deny"
}
path "stage/aws/policy/" {
policy = "deny"
}
path "prod/" {
policy = "write"
}
`

View File

@ -13,6 +13,15 @@ const (
PathPolicySudo = "sudo" PathPolicySudo = "sudo"
) )
var (
pathPolicyLevel = map[string]int{
PathPolicyDeny: 0,
PathPolicyRead: 1,
PathPolicyWrite: 2,
PathPolicySudo: 3,
}
)
// Policy is used to represent the policy specified by // Policy is used to represent the policy specified by
// an ACL configuration. // an ACL configuration.
type Policy struct { type Policy struct {