Switches GETs to a filtering model for ACLs.
This commit is contained in:
parent
77ae55c692
commit
0f94a7a326
|
@ -50,6 +50,35 @@ func FilterKeys(acl acl.ACL, keys []string) []string {
|
||||||
return keys[:FilterEntries(&kf)]
|
return keys[:FilterEntries(&kf)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type txnResultsFilter struct {
|
||||||
|
acl acl.ACL
|
||||||
|
results structs.TxnResults
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *txnResultsFilter) Len() int {
|
||||||
|
return len(t.results)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *txnResultsFilter) Filter(i int) bool {
|
||||||
|
result := t.results[i]
|
||||||
|
if result.KV != nil {
|
||||||
|
return !t.acl.KeyRead(result.KV.Key)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *txnResultsFilter) Move(dst, src, span int) {
|
||||||
|
copy(t.results[dst:dst+span], t.results[src:src+span])
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterTxnResults is used to filter a list of transaction results by
|
||||||
|
// applying an ACL policy.
|
||||||
|
func FilterTxnResults(acl acl.ACL, results structs.TxnResults) structs.TxnResults {
|
||||||
|
rf := txnResultsFilter{acl: acl, results: results}
|
||||||
|
return results[:FilterEntries(&rf)]
|
||||||
|
}
|
||||||
|
|
||||||
// Filter interface is used with FilterEntries to do an
|
// Filter interface is used with FilterEntries to do an
|
||||||
// in-place filter of a slice.
|
// in-place filter of a slice.
|
||||||
type Filter interface {
|
type Filter interface {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/hashicorp/consul/consul/structs"
|
"github.com/hashicorp/consul/consul/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFilterDirEnt(t *testing.T) {
|
func TestFilter_DirEnt(t *testing.T) {
|
||||||
policy, _ := acl.Parse(testFilterRules)
|
policy, _ := acl.Parse(testFilterRules)
|
||||||
aclR, _ := acl.New(acl.DenyAll(), policy)
|
aclR, _ := acl.New(acl.DenyAll(), policy)
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ func TestFilterDirEnt(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeys(t *testing.T) {
|
func TestFilter_Keys(t *testing.T) {
|
||||||
policy, _ := acl.Parse(testFilterRules)
|
policy, _ := acl.Parse(testFilterRules)
|
||||||
aclR, _ := acl.New(acl.DenyAll(), policy)
|
aclR, _ := acl.New(acl.DenyAll(), policy)
|
||||||
|
|
||||||
|
@ -80,6 +80,55 @@ func TestKeys(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilter_TxnResults(t *testing.T) {
|
||||||
|
policy, _ := acl.Parse(testFilterRules)
|
||||||
|
aclR, _ := acl.New(acl.DenyAll(), policy)
|
||||||
|
|
||||||
|
type tcase struct {
|
||||||
|
in []string
|
||||||
|
out []string
|
||||||
|
}
|
||||||
|
cases := []tcase{
|
||||||
|
tcase{
|
||||||
|
in: []string{"foo/test", "foo/priv/nope", "foo/other", "zoo"},
|
||||||
|
out: []string{"foo/test", "foo/other"},
|
||||||
|
},
|
||||||
|
tcase{
|
||||||
|
in: []string{"abe", "lincoln"},
|
||||||
|
out: nil,
|
||||||
|
},
|
||||||
|
tcase{
|
||||||
|
in: []string{"abe", "foo/1", "foo/2", "foo/3", "nope"},
|
||||||
|
out: []string{"foo/1", "foo/2", "foo/3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
results := structs.TxnResults{}
|
||||||
|
for _, in := range tc.in {
|
||||||
|
results = append(results, &structs.TxnResult{KV: &structs.DirEntry{Key: in}})
|
||||||
|
}
|
||||||
|
|
||||||
|
results = FilterTxnResults(aclR, results)
|
||||||
|
var outL []string
|
||||||
|
for _, r := range results {
|
||||||
|
outL = append(outL, r.KV.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(outL, tc.out) {
|
||||||
|
t.Fatalf("bad: %#v %#v", outL, tc.out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a non-KV result.
|
||||||
|
results := structs.TxnResults{}
|
||||||
|
results = append(results, &structs.TxnResult{})
|
||||||
|
results = FilterTxnResults(aclR, results)
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("should not have filtered non-KV result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var testFilterRules = `
|
var testFilterRules = `
|
||||||
key "" {
|
key "" {
|
||||||
policy = "deny"
|
policy = "deny"
|
||||||
|
|
|
@ -31,9 +31,13 @@ func kvsPreApply(srv *Server, acl acl.ACL, op structs.KVSOp, dirEnt *structs.Dir
|
||||||
return false, permissionDeniedErr
|
return false, permissionDeniedErr
|
||||||
}
|
}
|
||||||
|
|
||||||
case structs.KVSGet,
|
case structs.KVSGet:
|
||||||
structs.KVSCheckSession,
|
// Filtering for GETs is done on the output side.
|
||||||
structs.KVSCheckIndex:
|
|
||||||
|
case structs.KVSCheckSession, structs.KVSCheckIndex:
|
||||||
|
// These could reveal information based on the outcome
|
||||||
|
// of the transaction, and they operate on individual
|
||||||
|
// keys so we check them here.
|
||||||
if !acl.KeyRead(dirEnt.Key) {
|
if !acl.KeyRead(dirEnt.Key) {
|
||||||
return false, permissionDeniedErr
|
return false, permissionDeniedErr
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,9 @@ func (t *Txn) Apply(args *structs.TxnRequest, reply *structs.TxnResponse) error
|
||||||
// Convert the return type. This should be a cheap copy since we are
|
// Convert the return type. This should be a cheap copy since we are
|
||||||
// just taking the two slices.
|
// just taking the two slices.
|
||||||
if txnResp, ok := resp.(structs.TxnResponse); ok {
|
if txnResp, ok := resp.(structs.TxnResponse); ok {
|
||||||
|
if acl != nil {
|
||||||
|
txnResp.Results = FilterTxnResults(acl, txnResp.Results)
|
||||||
|
}
|
||||||
*reply = txnResp
|
*reply = txnResp
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("unexpected return type %T", resp)
|
return fmt.Errorf("unexpected return type %T", resp)
|
||||||
|
@ -103,5 +106,8 @@ func (t *Txn) Read(args *structs.TxnReadRequest, reply *structs.TxnReadResponse)
|
||||||
// Run the read transaction.
|
// Run the read transaction.
|
||||||
state := t.srv.fsm.State()
|
state := t.srv.fsm.State()
|
||||||
reply.Results, reply.Errors = state.TxnRO(args.Ops)
|
reply.Results, reply.Errors = state.TxnRO(args.Ops)
|
||||||
|
if acl != nil {
|
||||||
|
reply.Results = FilterTxnResults(acl, reply.Results)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,16 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
|
||||||
|
|
||||||
testutil.WaitForLeader(t, s1.RPC, "dc1")
|
testutil.WaitForLeader(t, s1.RPC, "dc1")
|
||||||
|
|
||||||
|
// Put in a key to read back.
|
||||||
|
state := s1.fsm.State()
|
||||||
|
d := &structs.DirEntry{
|
||||||
|
Key: "nope",
|
||||||
|
Value: []byte("hello"),
|
||||||
|
}
|
||||||
|
if err := state.KVSSet(1, d); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Create the ACL.
|
// Create the ACL.
|
||||||
var id string
|
var id string
|
||||||
{
|
{
|
||||||
|
@ -139,7 +149,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
|
||||||
KV: &structs.TxnKVOp{
|
KV: &structs.TxnKVOp{
|
||||||
Verb: structs.KVSSet,
|
Verb: structs.KVSSet,
|
||||||
DirEnt: structs.DirEntry{
|
DirEnt: structs.DirEntry{
|
||||||
Key: "foo",
|
Key: "nope",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -147,7 +157,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
|
||||||
KV: &structs.TxnKVOp{
|
KV: &structs.TxnKVOp{
|
||||||
Verb: structs.KVSDelete,
|
Verb: structs.KVSDelete,
|
||||||
DirEnt: structs.DirEntry{
|
DirEnt: structs.DirEntry{
|
||||||
Key: "foo",
|
Key: "nope",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -155,7 +165,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
|
||||||
KV: &structs.TxnKVOp{
|
KV: &structs.TxnKVOp{
|
||||||
Verb: structs.KVSDeleteCAS,
|
Verb: structs.KVSDeleteCAS,
|
||||||
DirEnt: structs.DirEntry{
|
DirEnt: structs.DirEntry{
|
||||||
Key: "foo",
|
Key: "nope",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -163,7 +173,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
|
||||||
KV: &structs.TxnKVOp{
|
KV: &structs.TxnKVOp{
|
||||||
Verb: structs.KVSDeleteTree,
|
Verb: structs.KVSDeleteTree,
|
||||||
DirEnt: structs.DirEntry{
|
DirEnt: structs.DirEntry{
|
||||||
Key: "foo",
|
Key: "nope",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -171,7 +181,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
|
||||||
KV: &structs.TxnKVOp{
|
KV: &structs.TxnKVOp{
|
||||||
Verb: structs.KVSCAS,
|
Verb: structs.KVSCAS,
|
||||||
DirEnt: structs.DirEntry{
|
DirEnt: structs.DirEntry{
|
||||||
Key: "foo",
|
Key: "nope",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -179,7 +189,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
|
||||||
KV: &structs.TxnKVOp{
|
KV: &structs.TxnKVOp{
|
||||||
Verb: structs.KVSLock,
|
Verb: structs.KVSLock,
|
||||||
DirEnt: structs.DirEntry{
|
DirEnt: structs.DirEntry{
|
||||||
Key: "foo",
|
Key: "nope",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -187,7 +197,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
|
||||||
KV: &structs.TxnKVOp{
|
KV: &structs.TxnKVOp{
|
||||||
Verb: structs.KVSUnlock,
|
Verb: structs.KVSUnlock,
|
||||||
DirEnt: structs.DirEntry{
|
DirEnt: structs.DirEntry{
|
||||||
Key: "foo",
|
Key: "nope",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -227,8 +237,14 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
|
||||||
|
|
||||||
// Verify the transaction's return value.
|
// Verify the transaction's return value.
|
||||||
var expected structs.TxnResponse
|
var expected structs.TxnResponse
|
||||||
for i, _ := range arg.Ops {
|
for i, op := range arg.Ops {
|
||||||
expected.Errors = append(expected.Errors, &structs.TxnError{i, permissionDeniedErr.Error()})
|
switch op.KV.Verb {
|
||||||
|
case structs.KVSGet:
|
||||||
|
// These get filtered but won't result in an error.
|
||||||
|
|
||||||
|
default:
|
||||||
|
expected.Errors = append(expected.Errors, &structs.TxnError{i, permissionDeniedErr.Error()})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(out, expected) {
|
if !reflect.DeepEqual(out, expected) {
|
||||||
t.Fatalf("bad %v", out)
|
t.Fatalf("bad %v", out)
|
||||||
|
@ -398,6 +414,16 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
|
||||||
|
|
||||||
testutil.WaitForLeader(t, s1.RPC, "dc1")
|
testutil.WaitForLeader(t, s1.RPC, "dc1")
|
||||||
|
|
||||||
|
// Put in a key to read back.
|
||||||
|
state := s1.fsm.State()
|
||||||
|
d := &structs.DirEntry{
|
||||||
|
Key: "nope",
|
||||||
|
Value: []byte("hello"),
|
||||||
|
}
|
||||||
|
if err := state.KVSSet(1, d); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Create the ACL.
|
// Create the ACL.
|
||||||
var id string
|
var id string
|
||||||
{
|
{
|
||||||
|
@ -461,8 +487,14 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
|
||||||
KnownLeader: true,
|
KnownLeader: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i, _ := range arg.Ops {
|
for i, op := range arg.Ops {
|
||||||
expected.Errors = append(expected.Errors, &structs.TxnError{i, permissionDeniedErr.Error()})
|
switch op.KV.Verb {
|
||||||
|
case structs.KVSGet:
|
||||||
|
// These get filtered but won't result in an error.
|
||||||
|
|
||||||
|
default:
|
||||||
|
expected.Errors = append(expected.Errors, &structs.TxnError{i, permissionDeniedErr.Error()})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(out, expected) {
|
if !reflect.DeepEqual(out, expected) {
|
||||||
t.Fatalf("bad %v", out)
|
t.Fatalf("bad %v", out)
|
||||||
|
|
Loading…
Reference in New Issue