Add ability to require parameters in ACLs (#3510)

This commit is contained in:
Chris Hoffman 2017-11-02 07:18:49 -04:00 committed by GitHub
parent e0669746b6
commit 3d8d887676
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 83 additions and 16 deletions

View File

@ -169,6 +169,18 @@ func NewACL(policies []*Policy) (*ACL, error) {
} }
} }
if len(pc.Permissions.RequiredParameters) > 0 {
if len(existingPerms.RequiredParameters) == 0 {
existingPerms.RequiredParameters = pc.Permissions.RequiredParameters
} else {
for _, v := range pc.Permissions.RequiredParameters {
if !strutil.StrListContains(existingPerms.RequiredParameters, v) {
existingPerms.RequiredParameters = append(existingPerms.RequiredParameters, v)
}
}
}
}
INSERT: INSERT:
tree.Insert(pc.Prefix, existingPerms) tree.Insert(pc.Prefix, existingPerms)
} }
@ -322,6 +334,12 @@ CHECK:
// Only check parameter permissions for operations that can modify // Only check parameter permissions for operations that can modify
// parameters. // parameters.
if op == logical.UpdateOperation || op == logical.CreateOperation { if op == logical.UpdateOperation || op == logical.CreateOperation {
for _, parameter := range permissions.RequiredParameters {
if _, ok := req.Data[strings.ToLower(parameter)]; !ok {
return
}
}
// If there are no data fields, allow // If there are no data fields, allow
if len(req.Data) == 0 { if len(req.Data) == 0 {
ret.Allowed = true ret.Allowed = true

View File

@ -232,6 +232,7 @@ func TestACL_PolicyMerge(t *testing.T) {
maxWrappingTTL *time.Duration maxWrappingTTL *time.Duration
allowed map[string][]interface{} allowed map[string][]interface{}
denied map[string][]interface{} denied map[string][]interface{}
required []string
} }
createDuration := func(seconds int) *time.Duration { createDuration := func(seconds int) *time.Duration {
@ -240,14 +241,14 @@ func TestACL_PolicyMerge(t *testing.T) {
} }
tcases := []tcase{ tcases := []tcase{
{"foo/bar", nil, nil, nil, map[string][]interface{}{"zip": []interface{}{}, "baz": []interface{}{}}}, {"foo/bar", nil, nil, nil, map[string][]interface{}{"zip": []interface{}{}, "baz": []interface{}{}}, []string{"baz"}},
{"hello/universe", createDuration(50), createDuration(200), map[string][]interface{}{"foo": []interface{}{}, "bar": []interface{}{}}, nil}, {"hello/universe", createDuration(50), createDuration(200), map[string][]interface{}{"foo": []interface{}{}, "bar": []interface{}{}}, nil, []string{"foo", "bar"}},
{"allow/all", nil, nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}, "test1": []interface{}{"foo"}}, nil}, {"allow/all", nil, nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}, "test1": []interface{}{"foo"}}, nil, nil},
{"allow/all1", nil, nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}, "test1": []interface{}{"foo"}}, nil}, {"allow/all1", nil, nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}, "test1": []interface{}{"foo"}}, nil, nil},
{"deny/all", nil, nil, nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}}}, {"deny/all", nil, nil, nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}}, nil},
{"deny/all1", nil, nil, nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}}}, {"deny/all1", nil, nil, nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}}, nil},
{"value/merge", nil, nil, map[string][]interface{}{"test": []interface{}{3, 4, 1, 2}}, map[string][]interface{}{"test": []interface{}{3, 4, 1, 2}}}, {"value/merge", nil, nil, map[string][]interface{}{"test": []interface{}{3, 4, 1, 2}}, map[string][]interface{}{"test": []interface{}{3, 4, 1, 2}}, nil},
{"value/empty", nil, nil, map[string][]interface{}{"empty": []interface{}{}}, map[string][]interface{}{"empty": []interface{}{}}}, {"value/empty", nil, nil, map[string][]interface{}{"empty": []interface{}{}}, map[string][]interface{}{"empty": []interface{}{}}, nil},
} }
for _, tc := range tcases { for _, tc := range tcases {
@ -263,6 +264,9 @@ func TestACL_PolicyMerge(t *testing.T) {
if !reflect.DeepEqual(tc.denied, p.DeniedParameters) { if !reflect.DeepEqual(tc.denied, p.DeniedParameters) {
t.Fatalf("Denied paramaters did not match, Expected: %#v, Got: %#v", tc.denied, p.DeniedParameters) t.Fatalf("Denied paramaters did not match, Expected: %#v, Got: %#v", tc.denied, p.DeniedParameters)
} }
if !reflect.DeepEqual(tc.required, p.RequiredParameters) {
t.Fatalf("Required paramaters did not match, Expected: %#v, Got: %#v", tc.required, p.RequiredParameters)
}
if tc.minWrappingTTL != nil && *tc.minWrappingTTL != p.MinWrappingTTL { if tc.minWrappingTTL != nil && *tc.minWrappingTTL != p.MinWrappingTTL {
t.Fatalf("Min wrapping TTL did not match, Expected: %#v, Got: %#v", tc.minWrappingTTL, p.MinWrappingTTL) t.Fatalf("Min wrapping TTL did not match, Expected: %#v, Got: %#v", tc.minWrappingTTL, p.MinWrappingTTL)
} }
@ -319,6 +323,8 @@ func TestACL_AllowOperation(t *testing.T) {
{"fruit/apple", nil, []string{"one"}, false}, {"fruit/apple", nil, []string{"one"}, false},
{"cold/weather", nil, []string{"four"}, true}, {"cold/weather", nil, []string{"four"}, true},
{"var/aws", nil, []string{"cold", "warm", "kitty"}, false}, {"var/aws", nil, []string{"cold", "warm", "kitty"}, false},
{"var/req", nil, []string{"cold", "warm", "kitty"}, false},
{"var/req", nil, []string{"cold", "warm", "kitty", "foo"}, true},
} }
for _, tc := range tcases { for _, tc := range tcases {
@ -510,6 +516,7 @@ path "foo/bar" {
denied_parameters = { denied_parameters = {
"baz" = [] "baz" = []
} }
required_parameters = ["baz"]
} }
path "foo/bar" { path "foo/bar" {
policy = "write" policy = "write"
@ -522,6 +529,7 @@ path "hello/universe" {
allowed_parameters = { allowed_parameters = {
"foo" = [] "foo" = []
} }
required_parameters = ["foo"]
max_wrapping_ttl = 300 max_wrapping_ttl = 300
min_wrapping_ttl = 100 min_wrapping_ttl = 100
} }
@ -530,6 +538,7 @@ path "hello/universe" {
allowed_parameters = { allowed_parameters = {
"bar" = [] "bar" = []
} }
required_parameters = ["bar"]
max_wrapping_ttl = 200 max_wrapping_ttl = 200
min_wrapping_ttl = 50 min_wrapping_ttl = 50
} }
@ -705,6 +714,10 @@ path "var/aws" {
"kitty" = [] "kitty" = []
} }
} }
path "var/req" {
policy = "write"
required_parameters = ["foo"]
}
` `
//allow operation testing //allow operation testing

View File

@ -96,10 +96,11 @@ type PathRules struct {
// These keys are used at the top level to make the HCL nicer; we store in // These keys are used at the top level to make the HCL nicer; we store in
// the ACLPermissions object though // the ACLPermissions object though
MinWrappingTTLHCL interface{} `hcl:"min_wrapping_ttl"` MinWrappingTTLHCL interface{} `hcl:"min_wrapping_ttl"`
MaxWrappingTTLHCL interface{} `hcl:"max_wrapping_ttl"` MaxWrappingTTLHCL interface{} `hcl:"max_wrapping_ttl"`
AllowedParametersHCL map[string][]interface{} `hcl:"allowed_parameters"` AllowedParametersHCL map[string][]interface{} `hcl:"allowed_parameters"`
DeniedParametersHCL map[string][]interface{} `hcl:"denied_parameters"` DeniedParametersHCL map[string][]interface{} `hcl:"denied_parameters"`
RequiredParametersHCL []string `hcl:"required_parameters"`
} }
type ACLPermissions struct { type ACLPermissions struct {
@ -108,6 +109,7 @@ type ACLPermissions struct {
MaxWrappingTTL time.Duration MaxWrappingTTL time.Duration
AllowedParameters map[string][]interface{} AllowedParameters map[string][]interface{}
DeniedParameters map[string][]interface{} DeniedParameters map[string][]interface{}
RequiredParameters []string
} }
func (p *ACLPermissions) Clone() (*ACLPermissions, error) { func (p *ACLPermissions) Clone() (*ACLPermissions, error) {
@ -115,6 +117,7 @@ func (p *ACLPermissions) Clone() (*ACLPermissions, error) {
CapabilitiesBitmap: p.CapabilitiesBitmap, CapabilitiesBitmap: p.CapabilitiesBitmap,
MinWrappingTTL: p.MinWrappingTTL, MinWrappingTTL: p.MinWrappingTTL,
MaxWrappingTTL: p.MaxWrappingTTL, MaxWrappingTTL: p.MaxWrappingTTL,
RequiredParameters: p.RequiredParameters[:],
} }
switch { switch {
@ -198,6 +201,7 @@ func parsePaths(result *Policy, list *ast.ObjectList) error {
"capabilities", "capabilities",
"allowed_parameters", "allowed_parameters",
"denied_parameters", "denied_parameters",
"required_parameters",
"min_wrapping_ttl", "min_wrapping_ttl",
"max_wrapping_ttl", "max_wrapping_ttl",
} }
@ -290,6 +294,9 @@ func parsePaths(result *Policy, list *ast.ObjectList) error {
pc.Permissions.MaxWrappingTTL < pc.Permissions.MinWrappingTTL { pc.Permissions.MaxWrappingTTL < pc.Permissions.MinWrappingTTL {
return errors.New("max_wrapping_ttl cannot be less than min_wrapping_ttl") return errors.New("max_wrapping_ttl cannot be less than min_wrapping_ttl")
} }
if len(pc.RequiredParametersHCL) > 0 {
pc.Permissions.RequiredParameters = pc.RequiredParametersHCL[:]
}
PathFinished: PathFinished:
paths = append(paths, &pc) paths = append(paths, &pc)

View File

@ -85,6 +85,10 @@ path "test/types" {
"bool" = [false] "bool" = [false]
} }
} }
path "test/req" {
capabilities = ["create", "sudo"]
required_parameters = ["foo"]
}
`) `)
func TestPolicy_Parse(t *testing.T) { func TestPolicy_Parse(t *testing.T) {
@ -225,6 +229,20 @@ func TestPolicy_Parse(t *testing.T) {
}, },
Glob: false, Glob: false,
}, },
&PathRules{
Prefix: "test/req",
Policy: "",
Capabilities: []string{
"create",
"sudo",
},
RequiredParametersHCL: []string{"foo"},
Permissions: &ACLPermissions{
CapabilitiesBitmap: (CreateCapabilityInt | SudoCapabilityInt),
RequiredParameters: []string{"foo"},
},
Glob: false,
},
} }
if !reflect.DeepEqual(p.Paths, expect) { if !reflect.DeepEqual(p.Paths, expect) {
t.Errorf("expected \n\n%#v\n\n to be \n\n%#v\n\n", p.Paths, expect) t.Errorf("expected \n\n%#v\n\n to be \n\n%#v\n\n", p.Paths, expect)

View File

@ -113,9 +113,9 @@ path "secret/super-secret" {
capabilities = ["deny"] capabilities = ["deny"]
} }
# Policies can also specify allowed and disallowed parameters. Here the key # Policies can also specify allowed, disallowed, and required parameters. Here
# "secret/restricted" can only contain "foo" (any value) and "bar" (one of "zip" # the key "secret/restricted" can only contain "foo" (any value) and "bar" (one
# or "zap"). # of "zip" or "zap").
path "secret/restricted" { path "secret/restricted" {
capabilities = ["create"] capabilities = ["create"]
allowed_parameters = { allowed_parameters = {
@ -217,13 +217,24 @@ In addition to the standard set of capabilities, Vault offers finer-grained
control over permissions at a given path. The capabilities associated with a control over permissions at a given path. The capabilities associated with a
path take precedence over permissions on parameters. path take precedence over permissions on parameters.
### Allowed and Denied Parameters ### Parameter Constraints
In Vault, data is represented as `key=value` pairs. Vault policies can In Vault, data is represented as `key=value` pairs. Vault policies can
optionally further restrict paths based on the keys and data at those keys when optionally further restrict paths based on the keys and data at those keys when
evaluating the permissions for a path. The optional finer-grained control evaluating the permissions for a path. The optional finer-grained control
options are: options are:
* `required_parameters` - A list of parameters that must be specified.
```ruby
# This requires the user to create "secret/foo" with a parameter named
# "bar" and "baz".
path "secret/foo" {
capabilities = ["create"]
required_parameters = ["bar", "baz"]
}
```
* `allowed_parameters` - Whitelists a list of keys and values that are * `allowed_parameters` - Whitelists a list of keys and values that are
permitted on the given path. permitted on the given path.