vault: Adding ACL representation
This commit is contained in:
parent
ddab671bf4
commit
99abc11ec5
|
@ -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]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
`
|
|
@ -13,6 +13,15 @@ const (
|
|||
PathPolicySudo = "sudo"
|
||||
)
|
||||
|
||||
var (
|
||||
pathPolicyLevel = map[string]int{
|
||||
PathPolicyDeny: 0,
|
||||
PathPolicyRead: 1,
|
||||
PathPolicyWrite: 2,
|
||||
PathPolicySudo: 3,
|
||||
}
|
||||
)
|
||||
|
||||
// Policy is used to represent the policy specified by
|
||||
// an ACL configuration.
|
||||
type Policy struct {
|
||||
|
|
Loading…
Reference in New Issue