Switches GETs to a filtering model for ACLs.

This commit is contained in:
James Phillips 2016-05-13 15:58:55 -07:00
parent 77ae55c692
commit 0f94a7a326
5 changed files with 136 additions and 16 deletions

View File

@ -50,6 +50,35 @@ func FilterKeys(acl acl.ACL, keys []string) []string {
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
// in-place filter of a slice.
type Filter interface {

View File

@ -8,7 +8,7 @@ import (
"github.com/hashicorp/consul/consul/structs"
)
func TestFilterDirEnt(t *testing.T) {
func TestFilter_DirEnt(t *testing.T) {
policy, _ := acl.Parse(testFilterRules)
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)
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 = `
key "" {
policy = "deny"

View File

@ -31,9 +31,13 @@ func kvsPreApply(srv *Server, acl acl.ACL, op structs.KVSOp, dirEnt *structs.Dir
return false, permissionDeniedErr
}
case structs.KVSGet,
structs.KVSCheckSession,
structs.KVSCheckIndex:
case structs.KVSGet:
// Filtering for GETs is done on the output side.
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) {
return false, permissionDeniedErr
}

View File

@ -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
// just taking the two slices.
if txnResp, ok := resp.(structs.TxnResponse); ok {
if acl != nil {
txnResp.Results = FilterTxnResults(acl, txnResp.Results)
}
*reply = txnResp
} else {
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.
state := t.srv.fsm.State()
reply.Results, reply.Errors = state.TxnRO(args.Ops)
if acl != nil {
reply.Results = FilterTxnResults(acl, reply.Results)
}
return nil
}

View File

@ -112,6 +112,16 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
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.
var id string
{
@ -139,7 +149,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
KV: &structs.TxnKVOp{
Verb: structs.KVSSet,
DirEnt: structs.DirEntry{
Key: "foo",
Key: "nope",
},
},
},
@ -147,7 +157,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
KV: &structs.TxnKVOp{
Verb: structs.KVSDelete,
DirEnt: structs.DirEntry{
Key: "foo",
Key: "nope",
},
},
},
@ -155,7 +165,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
KV: &structs.TxnKVOp{
Verb: structs.KVSDeleteCAS,
DirEnt: structs.DirEntry{
Key: "foo",
Key: "nope",
},
},
},
@ -163,7 +173,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
KV: &structs.TxnKVOp{
Verb: structs.KVSDeleteTree,
DirEnt: structs.DirEntry{
Key: "foo",
Key: "nope",
},
},
},
@ -171,7 +181,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
KV: &structs.TxnKVOp{
Verb: structs.KVSCAS,
DirEnt: structs.DirEntry{
Key: "foo",
Key: "nope",
},
},
},
@ -179,7 +189,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
KV: &structs.TxnKVOp{
Verb: structs.KVSLock,
DirEnt: structs.DirEntry{
Key: "foo",
Key: "nope",
},
},
},
@ -187,7 +197,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
KV: &structs.TxnKVOp{
Verb: structs.KVSUnlock,
DirEnt: structs.DirEntry{
Key: "foo",
Key: "nope",
},
},
},
@ -227,8 +237,14 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
// Verify the transaction's return value.
var expected structs.TxnResponse
for i, _ := range arg.Ops {
expected.Errors = append(expected.Errors, &structs.TxnError{i, permissionDeniedErr.Error()})
for i, op := range arg.Ops {
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) {
t.Fatalf("bad %v", out)
@ -398,6 +414,16 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
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.
var id string
{
@ -461,8 +487,14 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
KnownLeader: true,
},
}
for i, _ := range arg.Ops {
expected.Errors = append(expected.Errors, &structs.TxnError{i, permissionDeniedErr.Error()})
for i, op := range arg.Ops {
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) {
t.Fatalf("bad %v", out)