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"
|
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 {
|
||||||
|
|
Loading…
Reference in New Issue