Merge pull request #3523 from hashicorp/issue_3511
Introduces new 'list' permission that applies to KV store recursive reads
This commit is contained in:
commit
545175d228
27
acl/acl.go
27
acl/acl.go
|
@ -60,6 +60,9 @@ type ACL interface {
|
||||||
// EventWrite determines if a specific event may be fired.
|
// EventWrite determines if a specific event may be fired.
|
||||||
EventWrite(string) bool
|
EventWrite(string) bool
|
||||||
|
|
||||||
|
// KeyList checks for permission to list keys under a prefix
|
||||||
|
KeyList(string) bool
|
||||||
|
|
||||||
// KeyRead checks for permission to read a given key
|
// KeyRead checks for permission to read a given key
|
||||||
KeyRead(string) bool
|
KeyRead(string) bool
|
||||||
|
|
||||||
|
@ -155,6 +158,10 @@ func (s *StaticACL) KeyRead(string) bool {
|
||||||
return s.defaultAllow
|
return s.defaultAllow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StaticACL) KeyList(string) bool {
|
||||||
|
return s.defaultAllow
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StaticACL) KeyWrite(string, sentinel.ScopeFn) bool {
|
func (s *StaticACL) KeyWrite(string, sentinel.ScopeFn) bool {
|
||||||
return s.defaultAllow
|
return s.defaultAllow
|
||||||
}
|
}
|
||||||
|
@ -455,7 +462,7 @@ func (p *PolicyACL) KeyRead(key string) bool {
|
||||||
if ok {
|
if ok {
|
||||||
pr := rule.(PolicyRule)
|
pr := rule.(PolicyRule)
|
||||||
switch pr.aclPolicy {
|
switch pr.aclPolicy {
|
||||||
case PolicyRead, PolicyWrite:
|
case PolicyRead, PolicyWrite, PolicyList:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -466,6 +473,24 @@ func (p *PolicyACL) KeyRead(key string) bool {
|
||||||
return p.parent.KeyRead(key)
|
return p.parent.KeyRead(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyList returns if a key is allowed to be listed
|
||||||
|
func (p *PolicyACL) KeyList(key string) bool {
|
||||||
|
// Look for a matching rule
|
||||||
|
_, rule, ok := p.keyRules.LongestPrefix(key)
|
||||||
|
if ok {
|
||||||
|
pr := rule.(PolicyRule)
|
||||||
|
switch pr.aclPolicy {
|
||||||
|
case PolicyList, PolicyWrite:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No matching rule, use the parent.
|
||||||
|
return p.parent.KeyList(key)
|
||||||
|
}
|
||||||
|
|
||||||
// KeyWrite returns if a key is allowed to be written
|
// KeyWrite returns if a key is allowed to be written
|
||||||
func (p *PolicyACL) KeyWrite(key string, scope sentinel.ScopeFn) bool {
|
func (p *PolicyACL) KeyWrite(key string, scope sentinel.ScopeFn) bool {
|
||||||
// Look for a matching rule
|
// Look for a matching rule
|
||||||
|
|
|
@ -268,6 +268,10 @@ func TestPolicyACL(t *testing.T) {
|
||||||
Prefix: "zip/",
|
Prefix: "zip/",
|
||||||
Policy: PolicyRead,
|
Policy: PolicyRead,
|
||||||
},
|
},
|
||||||
|
&KeyPolicy{
|
||||||
|
Prefix: "zap/",
|
||||||
|
Policy: PolicyList,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
PreparedQueries: []*PreparedQueryPolicy{
|
PreparedQueries: []*PreparedQueryPolicy{
|
||||||
&PreparedQueryPolicy{
|
&PreparedQueryPolicy{
|
||||||
|
@ -316,15 +320,17 @@ func TestPolicyACL(t *testing.T) {
|
||||||
read bool
|
read bool
|
||||||
write bool
|
write bool
|
||||||
writePrefix bool
|
writePrefix bool
|
||||||
|
list bool
|
||||||
}
|
}
|
||||||
cases := []keycase{
|
cases := []keycase{
|
||||||
{"other", true, true, true},
|
{"other", true, true, true, true},
|
||||||
{"foo/test", true, true, true},
|
{"foo/test", true, true, true, true},
|
||||||
{"foo/priv/test", false, false, false},
|
{"foo/priv/test", false, false, false, false},
|
||||||
{"bar/any", false, false, false},
|
{"bar/any", false, false, false, false},
|
||||||
{"zip/test", true, false, false},
|
{"zip/test", true, false, false, false},
|
||||||
{"foo/", true, true, false},
|
{"foo/", true, true, false, true},
|
||||||
{"", true, true, false},
|
{"", true, true, false, true},
|
||||||
|
{"zap/test", true, false, false, true},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
if c.read != acl.KeyRead(c.inp) {
|
if c.read != acl.KeyRead(c.inp) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ const (
|
||||||
PolicyDeny = "deny"
|
PolicyDeny = "deny"
|
||||||
PolicyRead = "read"
|
PolicyRead = "read"
|
||||||
PolicyWrite = "write"
|
PolicyWrite = "write"
|
||||||
|
PolicyList = "list"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Policy is used to represent the policy specified by
|
// Policy is used to represent the policy specified by
|
||||||
|
@ -175,7 +176,7 @@ func Parse(rules string, sentinel sentinel.Evaluator) (*Policy, error) {
|
||||||
|
|
||||||
// Validate the key policy
|
// Validate the key policy
|
||||||
for _, kp := range p.Keys {
|
for _, kp := range p.Keys {
|
||||||
if !isPolicyValid(kp.Policy) {
|
if kp.Policy != PolicyList && !isPolicyValid(kp.Policy) {
|
||||||
return nil, fmt.Errorf("Invalid key policy: %#v", kp)
|
return nil, fmt.Errorf("Invalid key policy: %#v", kp)
|
||||||
}
|
}
|
||||||
if err := isSentinelValid(sentinel, kp.Policy, kp.Sentinel); err != nil {
|
if err := isSentinelValid(sentinel, kp.Policy, kp.Sentinel); err != nil {
|
||||||
|
|
|
@ -676,6 +676,9 @@ func (a *Agent) consulConfig() (*consul.Config, error) {
|
||||||
if a.config.ACLEnforceVersion8 {
|
if a.config.ACLEnforceVersion8 {
|
||||||
base.ACLEnforceVersion8 = a.config.ACLEnforceVersion8
|
base.ACLEnforceVersion8 = a.config.ACLEnforceVersion8
|
||||||
}
|
}
|
||||||
|
if a.config.ACLEnableKeyListPolicy {
|
||||||
|
base.ACLEnableKeyListPolicy = a.config.ACLEnableKeyListPolicy
|
||||||
|
}
|
||||||
if a.config.SessionTTLMin != 0 {
|
if a.config.SessionTTLMin != 0 {
|
||||||
base.SessionTTLMin = a.config.SessionTTLMin
|
base.SessionTTLMin = a.config.SessionTTLMin
|
||||||
}
|
}
|
||||||
|
|
|
@ -481,17 +481,18 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
|
||||||
ConsulServerHealthInterval: b.durationVal("consul.server.health_interval", c.Consul.Server.HealthInterval),
|
ConsulServerHealthInterval: b.durationVal("consul.server.health_interval", c.Consul.Server.HealthInterval),
|
||||||
|
|
||||||
// ACL
|
// ACL
|
||||||
ACLAgentMasterToken: b.stringVal(c.ACLAgentMasterToken),
|
ACLAgentMasterToken: b.stringVal(c.ACLAgentMasterToken),
|
||||||
ACLAgentToken: b.stringVal(c.ACLAgentToken),
|
ACLAgentToken: b.stringVal(c.ACLAgentToken),
|
||||||
ACLDatacenter: strings.ToLower(b.stringVal(c.ACLDatacenter)),
|
ACLDatacenter: strings.ToLower(b.stringVal(c.ACLDatacenter)),
|
||||||
ACLDefaultPolicy: b.stringVal(c.ACLDefaultPolicy),
|
ACLDefaultPolicy: b.stringVal(c.ACLDefaultPolicy),
|
||||||
ACLDownPolicy: b.stringVal(c.ACLDownPolicy),
|
ACLDownPolicy: b.stringVal(c.ACLDownPolicy),
|
||||||
ACLEnforceVersion8: b.boolVal(c.ACLEnforceVersion8),
|
ACLEnforceVersion8: b.boolVal(c.ACLEnforceVersion8),
|
||||||
ACLMasterToken: b.stringVal(c.ACLMasterToken),
|
ACLEnableKeyListPolicy: b.boolVal(c.ACLEnableKeyListPolicy),
|
||||||
ACLReplicationToken: b.stringVal(c.ACLReplicationToken),
|
ACLMasterToken: b.stringVal(c.ACLMasterToken),
|
||||||
ACLTTL: b.durationVal("acl_ttl", c.ACLTTL),
|
ACLReplicationToken: b.stringVal(c.ACLReplicationToken),
|
||||||
ACLToken: b.stringVal(c.ACLToken),
|
ACLTTL: b.durationVal("acl_ttl", c.ACLTTL),
|
||||||
EnableACLReplication: b.boolVal(c.EnableACLReplication),
|
ACLToken: b.stringVal(c.ACLToken),
|
||||||
|
EnableACLReplication: b.boolVal(c.EnableACLReplication),
|
||||||
|
|
||||||
// Autopilot
|
// Autopilot
|
||||||
AutopilotCleanupDeadServers: b.boolVal(c.Autopilot.CleanupDeadServers),
|
AutopilotCleanupDeadServers: b.boolVal(c.Autopilot.CleanupDeadServers),
|
||||||
|
|
|
@ -128,6 +128,7 @@ type Config struct {
|
||||||
ACLDatacenter *string `json:"acl_datacenter,omitempty" hcl:"acl_datacenter" mapstructure:"acl_datacenter"`
|
ACLDatacenter *string `json:"acl_datacenter,omitempty" hcl:"acl_datacenter" mapstructure:"acl_datacenter"`
|
||||||
ACLDefaultPolicy *string `json:"acl_default_policy,omitempty" hcl:"acl_default_policy" mapstructure:"acl_default_policy"`
|
ACLDefaultPolicy *string `json:"acl_default_policy,omitempty" hcl:"acl_default_policy" mapstructure:"acl_default_policy"`
|
||||||
ACLDownPolicy *string `json:"acl_down_policy,omitempty" hcl:"acl_down_policy" mapstructure:"acl_down_policy"`
|
ACLDownPolicy *string `json:"acl_down_policy,omitempty" hcl:"acl_down_policy" mapstructure:"acl_down_policy"`
|
||||||
|
ACLEnableKeyListPolicy *bool `json:"acl_enable_key_list_policy,omitempty" hcl:"acl_enable_key_list_policy" mapstructure:"acl_enable_key_list_policy"`
|
||||||
ACLEnforceVersion8 *bool `json:"acl_enforce_version_8,omitempty" hcl:"acl_enforce_version_8" mapstructure:"acl_enforce_version_8"`
|
ACLEnforceVersion8 *bool `json:"acl_enforce_version_8,omitempty" hcl:"acl_enforce_version_8" mapstructure:"acl_enforce_version_8"`
|
||||||
ACLMasterToken *string `json:"acl_master_token,omitempty" hcl:"acl_master_token" mapstructure:"acl_master_token"`
|
ACLMasterToken *string `json:"acl_master_token,omitempty" hcl:"acl_master_token" mapstructure:"acl_master_token"`
|
||||||
ACLReplicationToken *string `json:"acl_replication_token,omitempty" hcl:"acl_replication_token" mapstructure:"acl_replication_token"`
|
ACLReplicationToken *string `json:"acl_replication_token,omitempty" hcl:"acl_replication_token" mapstructure:"acl_replication_token"`
|
||||||
|
|
|
@ -54,6 +54,7 @@ type RuntimeConfig struct {
|
||||||
ACLDefaultPolicy string
|
ACLDefaultPolicy string
|
||||||
ACLDownPolicy string
|
ACLDownPolicy string
|
||||||
ACLEnforceVersion8 bool
|
ACLEnforceVersion8 bool
|
||||||
|
ACLEnableKeyListPolicy bool
|
||||||
ACLMasterToken string
|
ACLMasterToken string
|
||||||
ACLReplicationToken string
|
ACLReplicationToken string
|
||||||
ACLTTL time.Duration
|
ACLTTL time.Duration
|
||||||
|
|
|
@ -1932,6 +1932,7 @@ func TestFullConfig(t *testing.T) {
|
||||||
"acl_default_policy": "ArK3WIfE",
|
"acl_default_policy": "ArK3WIfE",
|
||||||
"acl_down_policy": "vZXMfMP0",
|
"acl_down_policy": "vZXMfMP0",
|
||||||
"acl_enforce_version_8": true,
|
"acl_enforce_version_8": true,
|
||||||
|
"acl_enable_key_list_policy": true,
|
||||||
"acl_master_token": "C1Q1oIwh",
|
"acl_master_token": "C1Q1oIwh",
|
||||||
"acl_replication_token": "LMmgy5dO",
|
"acl_replication_token": "LMmgy5dO",
|
||||||
"acl_token": "O1El0wan",
|
"acl_token": "O1El0wan",
|
||||||
|
@ -2347,6 +2348,7 @@ func TestFullConfig(t *testing.T) {
|
||||||
acl_default_policy = "ArK3WIfE"
|
acl_default_policy = "ArK3WIfE"
|
||||||
acl_down_policy = "vZXMfMP0"
|
acl_down_policy = "vZXMfMP0"
|
||||||
acl_enforce_version_8 = true
|
acl_enforce_version_8 = true
|
||||||
|
acl_enable_key_list_policy = true
|
||||||
acl_master_token = "C1Q1oIwh"
|
acl_master_token = "C1Q1oIwh"
|
||||||
acl_replication_token = "LMmgy5dO"
|
acl_replication_token = "LMmgy5dO"
|
||||||
acl_token = "O1El0wan"
|
acl_token = "O1El0wan"
|
||||||
|
@ -2905,6 +2907,7 @@ func TestFullConfig(t *testing.T) {
|
||||||
ACLDefaultPolicy: "ArK3WIfE",
|
ACLDefaultPolicy: "ArK3WIfE",
|
||||||
ACLDownPolicy: "vZXMfMP0",
|
ACLDownPolicy: "vZXMfMP0",
|
||||||
ACLEnforceVersion8: true,
|
ACLEnforceVersion8: true,
|
||||||
|
ACLEnableKeyListPolicy: true,
|
||||||
ACLMasterToken: "C1Q1oIwh",
|
ACLMasterToken: "C1Q1oIwh",
|
||||||
ACLReplicationToken: "LMmgy5dO",
|
ACLReplicationToken: "LMmgy5dO",
|
||||||
ACLTTL: 18060 * time.Second,
|
ACLTTL: 18060 * time.Second,
|
||||||
|
@ -3592,6 +3595,7 @@ func TestSanitize(t *testing.T) {
|
||||||
"ACLDefaultPolicy": "",
|
"ACLDefaultPolicy": "",
|
||||||
"ACLDisabledTTL": "0s",
|
"ACLDisabledTTL": "0s",
|
||||||
"ACLDownPolicy": "",
|
"ACLDownPolicy": "",
|
||||||
|
"ACLEnableKeyListPolicy": false,
|
||||||
"ACLEnforceVersion8": false,
|
"ACLEnforceVersion8": false,
|
||||||
"ACLMasterToken": "hidden",
|
"ACLMasterToken": "hidden",
|
||||||
"ACLReplicationToken": "hidden",
|
"ACLReplicationToken": "hidden",
|
||||||
|
|
|
@ -257,6 +257,11 @@ type Config struct {
|
||||||
// are opt-in prior to Consul 0.8 and opt-out in Consul 0.8 and later.
|
// are opt-in prior to Consul 0.8 and opt-out in Consul 0.8 and later.
|
||||||
ACLEnforceVersion8 bool
|
ACLEnforceVersion8 bool
|
||||||
|
|
||||||
|
// ACLEnableKeyListPolicy is used to gate enforcement of the new "list" policy that
|
||||||
|
// protects listing keys by prefix. This behavior is opt-in
|
||||||
|
// by default in Consul 1.0 and later.
|
||||||
|
ACLEnableKeyListPolicy bool
|
||||||
|
|
||||||
// TombstoneTTL is used to control how long KV tombstones are retained.
|
// TombstoneTTL is used to control how long KV tombstones are retained.
|
||||||
// This provides a window of time where the X-Consul-Index is monotonic.
|
// This provides a window of time where the X-Consul-Index is monotonic.
|
||||||
// Outside this window, the index may not be monotonic. This is a result
|
// Outside this window, the index may not be monotonic. This is a result
|
||||||
|
|
|
@ -133,7 +133,7 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if aclRule != nil && !aclRule.KeyRead(args.Key) {
|
if aclRule != nil && !aclRule.KeyRead(args.Key) {
|
||||||
ent = nil
|
return acl.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
if ent == nil {
|
if ent == nil {
|
||||||
|
@ -159,11 +159,15 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
acl, err := k.srv.resolveToken(args.Token)
|
aclToken, err := k.srv.resolveToken(args.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if aclToken != nil && k.srv.config.ACLEnableKeyListPolicy && !aclToken.KeyList(args.Key) {
|
||||||
|
return acl.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
return k.srv.blockingQuery(
|
return k.srv.blockingQuery(
|
||||||
&args.QueryOptions,
|
&args.QueryOptions,
|
||||||
&reply.QueryMeta,
|
&reply.QueryMeta,
|
||||||
|
@ -172,8 +176,8 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if acl != nil {
|
if aclToken != nil {
|
||||||
ent = FilterDirEnt(acl, ent)
|
ent = FilterDirEnt(aclToken, ent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ent) == 0 {
|
if len(ent) == 0 {
|
||||||
|
@ -199,11 +203,15 @@ func (k *KVS) ListKeys(args *structs.KeyListRequest, reply *structs.IndexedKeyLi
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
acl, err := k.srv.resolveToken(args.Token)
|
aclToken, err := k.srv.resolveToken(args.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if aclToken != nil && k.srv.config.ACLEnableKeyListPolicy && !aclToken.KeyList(args.Prefix) {
|
||||||
|
return acl.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
return k.srv.blockingQuery(
|
return k.srv.blockingQuery(
|
||||||
&args.QueryOptions,
|
&args.QueryOptions,
|
||||||
&reply.QueryMeta,
|
&reply.QueryMeta,
|
||||||
|
@ -221,8 +229,8 @@ func (k *KVS) ListKeys(args *structs.KeyListRequest, reply *structs.IndexedKeyLi
|
||||||
reply.Index = index
|
reply.Index = index
|
||||||
}
|
}
|
||||||
|
|
||||||
if acl != nil {
|
if aclToken != nil {
|
||||||
keys = FilterKeys(acl, keys)
|
keys = FilterKeys(aclToken, keys)
|
||||||
}
|
}
|
||||||
reply.Keys = keys
|
reply.Keys = keys
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/testrpc"
|
"github.com/hashicorp/consul/testrpc"
|
||||||
"github.com/hashicorp/net-rpc-msgpackrpc"
|
"github.com/hashicorp/net-rpc-msgpackrpc"
|
||||||
|
"github.com/pascaldekloe/goe/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestKVS_Apply(t *testing.T) {
|
func TestKVS_Apply(t *testing.T) {
|
||||||
|
@ -214,16 +215,10 @@ func TestKVS_Get_ACLDeny(t *testing.T) {
|
||||||
Key: "zip",
|
Key: "zip",
|
||||||
}
|
}
|
||||||
var dirent structs.IndexedDirEntries
|
var dirent structs.IndexedDirEntries
|
||||||
if err := msgpackrpc.CallWithCodec(codec, "KVS.Get", &getR, &dirent); err != nil {
|
if err := msgpackrpc.CallWithCodec(codec, "KVS.Get", &getR, &dirent); !acl.IsErrPermissionDenied(err) {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("Expected %v, got err: %v", acl.ErrPermissionDenied, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dirent.Index == 0 {
|
|
||||||
t.Fatalf("Bad: %v", dirent)
|
|
||||||
}
|
|
||||||
if len(dirent.Entries) != 0 {
|
|
||||||
t.Fatalf("Bad: %v", dirent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKVSEndpoint_List(t *testing.T) {
|
func TestKVSEndpoint_List(t *testing.T) {
|
||||||
|
@ -479,6 +474,134 @@ func TestKVSEndpoint_List_ACLDeny(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestKVSEndpoint_List_ACLEnableKeyListPolicy(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||||
|
c.ACLDatacenter = "dc1"
|
||||||
|
c.ACLMasterToken = "root"
|
||||||
|
c.ACLDefaultPolicy = "deny"
|
||||||
|
c.ACLEnableKeyListPolicy = true
|
||||||
|
})
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
defer codec.Close()
|
||||||
|
|
||||||
|
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||||
|
|
||||||
|
keys := []string{
|
||||||
|
"abe",
|
||||||
|
"bar/bar1",
|
||||||
|
"bar/bar2",
|
||||||
|
"zip",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
arg := structs.KVSRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Op: api.KVSet,
|
||||||
|
DirEnt: structs.DirEntry{
|
||||||
|
Key: key,
|
||||||
|
Flags: 1,
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||||
|
}
|
||||||
|
var out bool
|
||||||
|
if err := msgpackrpc.CallWithCodec(codec, "KVS.Apply", &arg, &out); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//write acl policy that denies recursive reads on ""
|
||||||
|
var testListRules1 = `
|
||||||
|
key "" {
|
||||||
|
policy = "deny"
|
||||||
|
}
|
||||||
|
key "bar" {
|
||||||
|
policy = "list"
|
||||||
|
}
|
||||||
|
key "zip" {
|
||||||
|
policy = "read"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
arg := structs.ACLRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Op: structs.ACLSet,
|
||||||
|
ACL: structs.ACL{
|
||||||
|
Name: "User token",
|
||||||
|
Type: structs.ACLTypeClient,
|
||||||
|
Rules: testListRules1,
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||||
|
}
|
||||||
|
var out string
|
||||||
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &arg, &out); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
id := out
|
||||||
|
|
||||||
|
//recursive read on empty prefix should fail
|
||||||
|
getReq := structs.KeyRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Key: "",
|
||||||
|
QueryOptions: structs.QueryOptions{Token: id},
|
||||||
|
}
|
||||||
|
var dirent structs.IndexedDirEntries
|
||||||
|
if err := msgpackrpc.CallWithCodec(codec, "KVS.List", &getReq, &dirent); !acl.IsErrPermissionDenied(err) {
|
||||||
|
t.Fatalf("expected %v but got err: %v", acl.ErrPermissionDenied, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//listing keys on empty prefix should fail
|
||||||
|
getKeysReq := structs.KeyListRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Prefix: "",
|
||||||
|
Seperator: "/",
|
||||||
|
QueryOptions: structs.QueryOptions{Token: id},
|
||||||
|
}
|
||||||
|
var keyList structs.IndexedKeyList
|
||||||
|
if err := msgpackrpc.CallWithCodec(codec, "KVS.ListKeys", &getKeysReq, &keyList); !acl.IsErrPermissionDenied(err) {
|
||||||
|
t.Fatalf("expected %v but got err: %v", acl.ErrPermissionDenied, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursive read with a prefix that has list permissions should succeed
|
||||||
|
getReq2 := structs.KeyRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Key: "bar",
|
||||||
|
QueryOptions: structs.QueryOptions{Token: id},
|
||||||
|
}
|
||||||
|
if err := msgpackrpc.CallWithCodec(codec, "KVS.List", &getReq2, &dirent); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedKeys := []string{"bar/bar1", "bar/bar2"}
|
||||||
|
var actualKeys []string
|
||||||
|
for _, entry := range dirent.Entries {
|
||||||
|
actualKeys = append(actualKeys, entry.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify.Values(t, "", actualKeys, expectedKeys)
|
||||||
|
|
||||||
|
// list keys with a prefix that has list permissions should succeed
|
||||||
|
getKeysReq2 := structs.KeyListRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Prefix: "bar",
|
||||||
|
QueryOptions: structs.QueryOptions{Token: id},
|
||||||
|
}
|
||||||
|
if err := msgpackrpc.CallWithCodec(codec, "KVS.ListKeys", &getKeysReq2, &keyList); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualKeys = []string{}
|
||||||
|
|
||||||
|
for _, key := range keyList.Keys {
|
||||||
|
actualKeys = append(actualKeys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify.Values(t, "", actualKeys, expectedKeys)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestKVSEndpoint_ListKeys(t *testing.T) {
|
func TestKVSEndpoint_ListKeys(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
dir1, s1 := testServer(t)
|
dir1, s1 := testServer(t)
|
||||||
|
|
|
@ -671,6 +671,32 @@ the example above, the rules allow read-only access to any key name with the emp
|
||||||
read-write access to any key name that starts with "foo", and deny all access to any key name that
|
read-write access to any key name that starts with "foo", and deny all access to any key name that
|
||||||
starts with "bar".
|
starts with "bar".
|
||||||
|
|
||||||
|
#### List Policy for Keys
|
||||||
|
|
||||||
|
Consul 1.0 introduces a new `list` policy for keys that is only enforced when opted in via the boolean config param "acl_enable_key_list_policy".
|
||||||
|
`list` controls access to recursively list entries and keys, and enables more fine grained policies. With "acl_enable_key_list_policy",
|
||||||
|
recursive reads via [the KV API](/api/kv.html#recurse) with an invalid token result in a 403. Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
key "" {
|
||||||
|
policy = "deny"
|
||||||
|
}
|
||||||
|
|
||||||
|
key "bar" {
|
||||||
|
policy = "list"
|
||||||
|
}
|
||||||
|
|
||||||
|
key "baz" {
|
||||||
|
policy = "read"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example above, the rules allow reading the key "baz", and only allow recursive reads on the prefix "bar".
|
||||||
|
|
||||||
|
A token with `write` access on a prefix also has `list` access. A token with `list` access on a prefix also has `read` access on all its suffixes.
|
||||||
|
|
||||||
|
#### Sentinel Integration
|
||||||
|
|
||||||
Consul Enterprise supports additional optional fields for key write policies for
|
Consul Enterprise supports additional optional fields for key write policies for
|
||||||
[Sentinel](https://docs.hashicorp.com/sentinel/app/consul/) integration. An example key rule with a
|
[Sentinel](https://docs.hashicorp.com/sentinel/app/consul/) integration. An example key rule with a
|
||||||
Sentinel code policy looks like this:
|
Sentinel code policy looks like this:
|
||||||
|
|
Loading…
Reference in a new issue