Add max/min wrapping TTL ACL statements (#2411)
This commit is contained in:
parent
a5d7259d84
commit
7f0a99e8eb
|
@ -1,27 +1,46 @@
|
|||
package duration
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseDurationSecond(inp string) (time.Duration, error) {
|
||||
var err error
|
||||
func ParseDurationSecond(in interface{}) (time.Duration, error) {
|
||||
var dur time.Duration
|
||||
// Look for a suffix otherwise its a plain second value
|
||||
if strings.HasSuffix(inp, "s") || strings.HasSuffix(inp, "m") || strings.HasSuffix(inp, "h") {
|
||||
dur, err = time.ParseDuration(inp)
|
||||
if err != nil {
|
||||
return dur, err
|
||||
switch in.(type) {
|
||||
case string:
|
||||
inp := in.(string)
|
||||
var err error
|
||||
// Look for a suffix otherwise its a plain second value
|
||||
if strings.HasSuffix(inp, "s") || strings.HasSuffix(inp, "m") || strings.HasSuffix(inp, "h") {
|
||||
dur, err = time.ParseDuration(inp)
|
||||
if err != nil {
|
||||
return dur, err
|
||||
}
|
||||
} else {
|
||||
// Plain integer
|
||||
secs, err := strconv.ParseInt(inp, 10, 64)
|
||||
if err != nil {
|
||||
return dur, err
|
||||
}
|
||||
dur = time.Duration(secs) * time.Second
|
||||
}
|
||||
} else {
|
||||
// Plain integer
|
||||
secs, err := strconv.ParseInt(inp, 10, 64)
|
||||
if err != nil {
|
||||
return dur, err
|
||||
}
|
||||
dur = time.Duration(secs) * time.Second
|
||||
case int:
|
||||
dur = time.Duration(in.(int)) * time.Second
|
||||
case int32:
|
||||
dur = time.Duration(in.(int32)) * time.Second
|
||||
case int64:
|
||||
dur = time.Duration(in.(int64)) * time.Second
|
||||
case uint:
|
||||
dur = time.Duration(in.(uint)) * time.Second
|
||||
case uint32:
|
||||
dur = time.Duration(in.(uint32)) * time.Second
|
||||
case uint64:
|
||||
dur = time.Duration(in.(uint64)) * time.Second
|
||||
default:
|
||||
return 0, errors.New("could not parse duration from input")
|
||||
}
|
||||
|
||||
return dur, nil
|
||||
|
|
40
vault/acl.go
40
vault/acl.go
|
@ -76,6 +76,28 @@ func NewACL(policies []*Policy) (*ACL, error) {
|
|||
pc.Permissions.CapabilitiesBitmap = existingPerms.CapabilitiesBitmap | pc.Permissions.CapabilitiesBitmap
|
||||
}
|
||||
|
||||
// Note: In these stanzas, we're preferring minimum lifetimes. So
|
||||
// we take the lesser of two specified max values, or we take the
|
||||
// lesser of two specified min values, the idea being, allowing
|
||||
// token lifetime to be minimum possible.
|
||||
//
|
||||
// If we have an existing max, and we either don't have a current
|
||||
// max, or the current is greater than the previous, use the
|
||||
// existing.
|
||||
if existingPerms.MaxWrappingTTL > 0 &&
|
||||
(pc.Permissions.MaxWrappingTTL == 0 ||
|
||||
existingPerms.MaxWrappingTTL < pc.Permissions.MaxWrappingTTL) {
|
||||
pc.Permissions.MaxWrappingTTL = existingPerms.MaxWrappingTTL
|
||||
}
|
||||
// If we have an existing min, and we either don't have a current
|
||||
// min, or the current is greater than the previous, use the
|
||||
// existing
|
||||
if existingPerms.MinWrappingTTL > 0 &&
|
||||
(pc.Permissions.MinWrappingTTL == 0 ||
|
||||
existingPerms.MinWrappingTTL < pc.Permissions.MinWrappingTTL) {
|
||||
pc.Permissions.MinWrappingTTL = existingPerms.MinWrappingTTL
|
||||
}
|
||||
|
||||
if len(existingPerms.AllowedParameters) > 0 {
|
||||
if pc.Permissions.AllowedParameters == nil {
|
||||
pc.Permissions.AllowedParameters = existingPerms.AllowedParameters
|
||||
|
@ -241,6 +263,24 @@ CHECK:
|
|||
return false, sudo
|
||||
}
|
||||
|
||||
if permissions.MaxWrappingTTL > 0 {
|
||||
if req.WrapInfo == nil || req.WrapInfo.TTL > permissions.MaxWrappingTTL {
|
||||
return false, sudo
|
||||
}
|
||||
}
|
||||
if permissions.MinWrappingTTL > 0 {
|
||||
if req.WrapInfo == nil || req.WrapInfo.TTL < permissions.MinWrappingTTL {
|
||||
return false, sudo
|
||||
}
|
||||
}
|
||||
// This situation can happen because of merging, even though in a single
|
||||
// path statement we check on ingress
|
||||
if permissions.MinWrappingTTL != 0 &&
|
||||
permissions.MaxWrappingTTL != 0 &&
|
||||
permissions.MaxWrappingTTL < permissions.MinWrappingTTL {
|
||||
return false, sudo
|
||||
}
|
||||
|
||||
// Only check parameter permissions for operations that can modify
|
||||
// parameters.
|
||||
if op == logical.UpdateOperation || op == logical.CreateOperation {
|
||||
|
|
|
@ -3,6 +3,7 @@ package vault
|
|||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
@ -225,20 +226,27 @@ func TestACL_PolicyMerge(t *testing.T) {
|
|||
}
|
||||
|
||||
type tcase struct {
|
||||
path string
|
||||
allowed map[string][]interface{}
|
||||
denied map[string][]interface{}
|
||||
path string
|
||||
minWrappingTTL *time.Duration
|
||||
maxWrappingTTL *time.Duration
|
||||
allowed map[string][]interface{}
|
||||
denied map[string][]interface{}
|
||||
}
|
||||
|
||||
createDuration := func(seconds int) *time.Duration {
|
||||
ret := time.Duration(seconds) * time.Second
|
||||
return &ret
|
||||
}
|
||||
|
||||
tcases := []tcase{
|
||||
{"foo/bar", nil, map[string][]interface{}{"zip": []interface{}{}, "baz": []interface{}{}}},
|
||||
{"hello/universe", map[string][]interface{}{"foo": []interface{}{}, "bar": []interface{}{}}, nil},
|
||||
{"allow/all", map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}, "test1": []interface{}{"foo"}}, nil},
|
||||
{"allow/all1", map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}, "test1": []interface{}{"foo"}}, nil},
|
||||
{"deny/all", nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}}},
|
||||
{"deny/all1", nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}}},
|
||||
{"value/merge", map[string][]interface{}{"test": []interface{}{1, 2, 3, 4}}, map[string][]interface{}{"test": []interface{}{1, 2, 3, 4}}},
|
||||
{"value/empty", map[string][]interface{}{"empty": []interface{}{}}, map[string][]interface{}{"empty": []interface{}{}}},
|
||||
{"foo/bar", nil, nil, nil, map[string][]interface{}{"zip": []interface{}{}, "baz": []interface{}{}}},
|
||||
{"hello/universe", createDuration(50), createDuration(200), map[string][]interface{}{"foo": []interface{}{}, "bar": []interface{}{}}, nil},
|
||||
{"allow/all", 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},
|
||||
{"deny/all", nil, nil, nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}}},
|
||||
{"deny/all1", nil, nil, nil, map[string][]interface{}{"*": []interface{}{}, "test": []interface{}{}}},
|
||||
{"value/merge", nil, nil, map[string][]interface{}{"test": []interface{}{1, 2, 3, 4}}, map[string][]interface{}{"test": []interface{}{1, 2, 3, 4}}},
|
||||
{"value/empty", nil, nil, map[string][]interface{}{"empty": []interface{}{}}, map[string][]interface{}{"empty": []interface{}{}}},
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
|
@ -254,6 +262,12 @@ func TestACL_PolicyMerge(t *testing.T) {
|
|||
if !reflect.DeepEqual(tc.denied, p.DeniedParameters) {
|
||||
t.Fatalf("Denied paramaters did not match, Expected: %#v, Got: %#v", tc.denied, p.DeniedParameters)
|
||||
}
|
||||
if tc.minWrappingTTL != nil && *tc.minWrappingTTL != p.MinWrappingTTL {
|
||||
t.Fatalf("Min wrapping TTL did not match, Expected: %#v, Got: %#v", tc.minWrappingTTL, p.MinWrappingTTL)
|
||||
}
|
||||
if tc.minWrappingTTL != nil && *tc.maxWrappingTTL != p.MaxWrappingTTL {
|
||||
t.Fatalf("Max wrapping TTL did not match, Expected: %#v, Got: %#v", tc.maxWrappingTTL, p.MaxWrappingTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,24 +285,39 @@ func TestACL_AllowOperation(t *testing.T) {
|
|||
logical.CreateOperation,
|
||||
}
|
||||
type tcase struct {
|
||||
path string
|
||||
parameters []string
|
||||
allowed bool
|
||||
path string
|
||||
wrappingTTL *time.Duration
|
||||
parameters []string
|
||||
allowed bool
|
||||
}
|
||||
|
||||
createDuration := func(seconds int) *time.Duration {
|
||||
ret := time.Duration(seconds) * time.Second
|
||||
return &ret
|
||||
}
|
||||
|
||||
tcases := []tcase{
|
||||
{"dev/ops", []string{"zip"}, true},
|
||||
{"foo/bar", []string{"zap"}, false},
|
||||
{"foo/baz", []string{"hello"}, true},
|
||||
{"foo/baz", []string{"zap"}, false},
|
||||
{"broken/phone", []string{"steve"}, false},
|
||||
{"hello/world", []string{"one"}, false},
|
||||
{"tree/fort", []string{"one"}, true},
|
||||
{"tree/fort", []string{"foo"}, false},
|
||||
{"fruit/apple", []string{"pear"}, false},
|
||||
{"fruit/apple", []string{"one"}, false},
|
||||
{"cold/weather", []string{"four"}, true},
|
||||
{"var/aws", []string{"cold", "warm", "kitty"}, false},
|
||||
{"dev/ops", nil, []string{"zip"}, true},
|
||||
{"foo/bar", nil, []string{"zap"}, false},
|
||||
{"foo/bar", nil, []string{"zip"}, false},
|
||||
{"foo/bar", createDuration(50), []string{"zip"}, false},
|
||||
{"foo/bar", createDuration(450), []string{"zip"}, false},
|
||||
{"foo/bar", createDuration(350), []string{"zip"}, true},
|
||||
{"foo/baz", nil, []string{"hello"}, false},
|
||||
{"foo/baz", createDuration(50), []string{"hello"}, false},
|
||||
{"foo/baz", createDuration(450), []string{"hello"}, true},
|
||||
{"foo/baz", nil, []string{"zap"}, false},
|
||||
{"broken/phone", nil, []string{"steve"}, false},
|
||||
{"working/phone", nil, []string{""}, false},
|
||||
{"working/phone", createDuration(450), []string{""}, false},
|
||||
{"working/phone", createDuration(350), []string{""}, true},
|
||||
{"hello/world", nil, []string{"one"}, false},
|
||||
{"tree/fort", nil, []string{"one"}, true},
|
||||
{"tree/fort", nil, []string{"foo"}, false},
|
||||
{"fruit/apple", nil, []string{"pear"}, false},
|
||||
{"fruit/apple", nil, []string{"one"}, false},
|
||||
{"cold/weather", nil, []string{"four"}, true},
|
||||
{"var/aws", nil, []string{"cold", "warm", "kitty"}, false},
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
|
@ -296,6 +325,11 @@ func TestACL_AllowOperation(t *testing.T) {
|
|||
for _, parameter := range tc.parameters {
|
||||
request.Data[parameter] = ""
|
||||
}
|
||||
if tc.wrappingTTL != nil {
|
||||
request.WrapInfo = &logical.RequestWrapInfo{
|
||||
TTL: *tc.wrappingTTL,
|
||||
}
|
||||
}
|
||||
for _, op := range toperations {
|
||||
request.Operation = op
|
||||
allowed, _ := acl.AllowOperation(&request)
|
||||
|
@ -454,12 +488,16 @@ path "hello/universe" {
|
|||
allowed_parameters = {
|
||||
"foo" = []
|
||||
}
|
||||
max_wrapping_ttl = 300
|
||||
min_wrapping_ttl = 100
|
||||
}
|
||||
path "hello/universe" {
|
||||
policy = "write"
|
||||
allowed_parameters = {
|
||||
"bar" = []
|
||||
}
|
||||
max_wrapping_ttl = 200
|
||||
min_wrapping_ttl = 50
|
||||
}
|
||||
path "allow/all" {
|
||||
policy = "write"
|
||||
|
@ -564,6 +602,8 @@ path "foo/bar" {
|
|||
denied_parameters = {
|
||||
"zap" = []
|
||||
}
|
||||
min_wrapping_ttl = 300
|
||||
max_wrapping_ttl = 400
|
||||
}
|
||||
path "foo/baz" {
|
||||
policy = "write"
|
||||
|
@ -573,6 +613,11 @@ path "foo/baz" {
|
|||
denied_parameters = {
|
||||
"zap" = []
|
||||
}
|
||||
min_wrapping_ttl = 300
|
||||
}
|
||||
path "working/phone" {
|
||||
policy = "write"
|
||||
max_wrapping_ttl = 400
|
||||
}
|
||||
path "broken/phone" {
|
||||
policy = "write"
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/hcl"
|
||||
"github.com/hashicorp/hcl/hcl/ast"
|
||||
"github.com/hashicorp/vault/helper/duration"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -64,14 +68,18 @@ type PathCapabilities struct {
|
|||
Glob bool
|
||||
Capabilities []string
|
||||
|
||||
// These two keys are used at the top level to make the HCL nicer; we store
|
||||
// in the Permissions object though
|
||||
// These keys are used at the top level to make the HCL nicer; we store in
|
||||
// the Permissions object though
|
||||
MinWrappingTTLHCL interface{} `hcl:"min_wrapping_ttl"`
|
||||
MaxWrappingTTLHCL interface{} `hcl:"max_wrapping_ttl"`
|
||||
AllowedParametersHCL map[string][]interface{} `hcl:"allowed_parameters"`
|
||||
DeniedParametersHCL map[string][]interface{} `hcl:"denied_parameters"`
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
CapabilitiesBitmap uint32
|
||||
MinWrappingTTL time.Duration
|
||||
MaxWrappingTTL time.Duration
|
||||
AllowedParameters map[string][]interface{}
|
||||
DeniedParameters map[string][]interface{}
|
||||
}
|
||||
|
@ -129,6 +137,8 @@ func parsePaths(result *Policy, list *ast.ObjectList) error {
|
|||
"capabilities",
|
||||
"allowed_parameters",
|
||||
"denied_parameters",
|
||||
"min_wrapping_ttl",
|
||||
"max_wrapping_ttl",
|
||||
}
|
||||
if err := checkHCLKeys(item.Val, valid); err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("path %q:", key))
|
||||
|
@ -200,6 +210,25 @@ func parsePaths(result *Policy, list *ast.ObjectList) error {
|
|||
pc.Permissions.DeniedParameters[strings.ToLower(key)] = val
|
||||
}
|
||||
}
|
||||
if pc.MinWrappingTTLHCL != nil {
|
||||
dur, err := duration.ParseDurationSecond(pc.MinWrappingTTLHCL)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("error parsing min_wrapping_ttl: {{err}}", err)
|
||||
}
|
||||
pc.Permissions.MinWrappingTTL = dur
|
||||
}
|
||||
if pc.MaxWrappingTTLHCL != nil {
|
||||
dur, err := duration.ParseDurationSecond(pc.MaxWrappingTTLHCL)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("error parsing max_wrapping_ttl: {{err}}", err)
|
||||
}
|
||||
pc.Permissions.MaxWrappingTTL = dur
|
||||
}
|
||||
if pc.Permissions.MinWrappingTTL != 0 &&
|
||||
pc.Permissions.MaxWrappingTTL != 0 &&
|
||||
pc.Permissions.MaxWrappingTTL < pc.Permissions.MinWrappingTTL {
|
||||
return errors.New("max_wrapping_ttl cannot be less than min_wrapping_ttl")
|
||||
}
|
||||
|
||||
PathFinished:
|
||||
paths = append(paths, &pc)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var rawPolicy = strings.TrimSpace(`
|
||||
|
@ -26,15 +27,21 @@ path "prod/version" {
|
|||
}
|
||||
|
||||
# Read access to foobar
|
||||
# Also tests stripping of leading slash
|
||||
# Also tests stripping of leading slash and parsing of min/max as string and
|
||||
# integer
|
||||
path "/foo/bar" {
|
||||
policy = "read"
|
||||
min_wrapping_ttl = 300
|
||||
max_wrapping_ttl = "1h"
|
||||
}
|
||||
|
||||
# Add capabilities for creation and sudo to foobar
|
||||
# This will be separate; they are combined when compiled into an ACL
|
||||
# Also tests reverse string/int handling to the above
|
||||
path "foo/bar" {
|
||||
capabilities = ["create", "sudo"]
|
||||
min_wrapping_ttl = "300s"
|
||||
max_wrapping_ttl = 3600
|
||||
}
|
||||
|
||||
# Check that only allowed_parameters are being added to foobar
|
||||
|
@ -133,8 +140,14 @@ func TestPolicy_Parse(t *testing.T) {
|
|||
"read",
|
||||
"list",
|
||||
},
|
||||
Permissions: &Permissions{CapabilitiesBitmap: (ReadCapabilityInt | ListCapabilityInt)},
|
||||
Glob: false,
|
||||
MinWrappingTTLHCL: 300,
|
||||
MaxWrappingTTLHCL: "1h",
|
||||
Permissions: &Permissions{
|
||||
CapabilitiesBitmap: (ReadCapabilityInt | ListCapabilityInt),
|
||||
MinWrappingTTL: 300 * time.Second,
|
||||
MaxWrappingTTL: 3600 * time.Second,
|
||||
},
|
||||
Glob: false,
|
||||
},
|
||||
&PathCapabilities{
|
||||
Prefix: "foo/bar",
|
||||
|
@ -143,8 +156,14 @@ func TestPolicy_Parse(t *testing.T) {
|
|||
"create",
|
||||
"sudo",
|
||||
},
|
||||
Permissions: &Permissions{CapabilitiesBitmap: (CreateCapabilityInt | SudoCapabilityInt)},
|
||||
Glob: false,
|
||||
MinWrappingTTLHCL: "300s",
|
||||
MaxWrappingTTLHCL: 3600,
|
||||
Permissions: &Permissions{
|
||||
CapabilitiesBitmap: (CreateCapabilityInt | SudoCapabilityInt),
|
||||
MinWrappingTTL: 300 * time.Second,
|
||||
MaxWrappingTTL: 3600 * time.Second,
|
||||
},
|
||||
Glob: false,
|
||||
},
|
||||
&PathCapabilities{
|
||||
Prefix: "foo/bar",
|
||||
|
@ -262,6 +281,23 @@ path "/" {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPolicy_ParseBadWrapping(t *testing.T) {
|
||||
_, err := Parse(strings.TrimSpace(`
|
||||
path "/" {
|
||||
policy = "read"
|
||||
min_wrapping_ttl = 400
|
||||
max_wrapping_ttl = 200
|
||||
}
|
||||
`))
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), `max_wrapping_ttl cannot be less than min_wrapping_ttl`) {
|
||||
t.Errorf("bad error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicy_ParseBadCapabilities(t *testing.T) {
|
||||
_, err := Parse(strings.TrimSpace(`
|
||||
path "/" {
|
||||
|
|
Loading…
Reference in New Issue