Add max/min wrapping TTL ACL statements (#2411)

This commit is contained in:
Jeff Mitchell 2017-02-27 14:42:00 -05:00 committed by GitHub
parent a5d7259d84
commit 7f0a99e8eb
5 changed files with 216 additions and 47 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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"

View File

@ -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)

View File

@ -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 "/" {