Skip to content

Commit

Permalink
etcdctl/ctlv3: auth: wildcard terminated paths specify ranges.
Browse files Browse the repository at this point in the history
 Implement wildcard '*' for get and del.
 When '*' is the last symbol in a key,
 the key is converted to a prefix range.
 The wildcard may only appear as the last
 symbol in a key.

 Also: fix several fencepost bugs fixed in
 the range sorting/merge code, including one
 incorrect test in auth/range_perm_cache_test.go

 Fixes etcd-io#6359
  • Loading branch information
glycerine committed Sep 8, 2016
1 parent 0b63502 commit c26e737
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 10 deletions.
51 changes: 43 additions & 8 deletions auth/range_perm_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,27 @@ import (

"github.com/coreos/etcd/auth/authpb"
"github.com/coreos/etcd/mvcc/backend"
"github.com/coreos/etcd/wildcard"
)

// isSubset returns true if a is a subset of b
// isSubset returns true if a is a subset of b.
// If a is a prefix of b, then a is a subset of b.
// Given intervals [a1,a2) and [b1,b2), is
// the a interval a subset of b?
func isSubset(a, b *rangePerm) bool {
switch {
case len(a.end) == 0 && len(b.end) == 0:
// a, b are both keys
return bytes.Equal(a.begin, b.begin)
case len(b.end) == 0:
// b is a key, a is a range
// b is a key, a is a range (even a prefix range has infinite membership > 1)
return false
case len(a.end) == 0:
return 0 <= bytes.Compare(a.begin, b.begin) && bytes.Compare(a.begin, b.end) <= 0
// a is a key, b is a range. need b1 <= a1 and a1 < b2
return bytes.Compare(b.begin, a.begin) <= 0 && bytes.Compare(a.begin, b.end) < 0
default:
return 0 <= bytes.Compare(a.begin, b.begin) && bytes.Compare(a.end, b.end) <= 0
// both are ranges. need b1 <= a1 and a2 <= b2
return bytes.Compare(b.begin, a.begin) <= 0 && bytes.Compare(a.end, b.end) <= 0
}
}

Expand Down Expand Up @@ -88,12 +94,18 @@ func mergeRangePerms(perms []*rangePerm) []*rangePerm {
i := 0
for i < len(perms) {
begin, next := i, i
for next+1 < len(perms) && bytes.Compare(perms[next].end, perms[next+1].begin) != -1 {
for next+1 < len(perms) && bytes.Compare(perms[next].end, perms[next+1].begin) >= 0 {
next++
}

merged = append(merged, &rangePerm{begin: perms[begin].begin, end: perms[next].end})

// don't merge ["a", "b") with ["b", ""), because perms[next+1].end is empty.
if next != begin && len(perms[next].end) > 0 {
merged = append(merged, &rangePerm{begin: perms[begin].begin, end: perms[next].end})
} else {
merged = append(merged, perms[begin])
if next != begin {
merged = append(merged, perms[next])
}
}
i = next + 1
}

Expand Down Expand Up @@ -151,8 +163,11 @@ func checkKeyPerm(cachedPerms *unifiedRangePermissions, key, rangeEnd []byte, pe
}

requiredPerm := &rangePerm{begin: key, end: rangeEnd}
wildcard.ExpandWildcardToRange(requiredPerm, true)

for _, perm := range tocheck {
wildcard.ExpandWildcardToRange(perm, true)

if isSubset(requiredPerm, perm) {
return true
}
Expand Down Expand Up @@ -217,3 +232,23 @@ func (slice RangePermSliceByBegin) Less(i, j int) bool {
func (slice RangePermSliceByBegin) Swap(i, j int) {
slice[i], slice[j] = slice[j], slice[i]
}

// GetBeg implements part of the BegEndRange interface.
func (r *rangePerm) GetBeg() []byte {
return r.begin
}

// SetBeg implements part of the BegEndRange interface.
func (r *rangePerm) SetBeg(beg []byte) {
r.begin = beg
}

// GetEnd implements part of the BegEndRange interface.
func (r *rangePerm) GetEnd() []byte {
return r.end
}

// SetEnd implements part of the BegEndRange interface.
func (r *rangePerm) SetEnd(end []byte) {
r.end = end
}
4 changes: 2 additions & 2 deletions auth/range_perm_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func TestGetMergedPerms(t *testing.T) {
},
{
[]*rangePerm{{[]byte("a"), []byte("")}, {[]byte("b"), []byte("c")}, {[]byte("b"), []byte("")}, {[]byte("c"), []byte("")}, {[]byte("d"), []byte("")}},
[]*rangePerm{{[]byte("a"), []byte("")}, {[]byte("b"), []byte("c")}, {[]byte("d"), []byte("")}},
[]*rangePerm{{[]byte("a"), []byte("")}, {[]byte("b"), []byte("c")}, {[]byte("c"), []byte("")}, {[]byte("d"), []byte("")}},
},
// duplicate ranges
{
Expand All @@ -123,7 +123,7 @@ func TestGetMergedPerms(t *testing.T) {
for i, tt := range tests {
result := mergeRangePerms(tt.params)
if !isPermsEqual(result, tt.want) {
t.Errorf("#%d: result=%q, want=%q", i, result, tt.want)
t.Fatalf("#%d: result=%q, want=%q", i, result, tt.want)
}
}
}
3 changes: 3 additions & 0 deletions etcdserver/apply_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

"github.com/coreos/etcd/auth"
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/wildcard"
)

type authApplierV3 struct {
Expand Down Expand Up @@ -75,10 +76,12 @@ func (aa *authApplierV3) Range(txnID int64, r *pb.RangeRequest) (*pb.RangeRespon
if err := aa.as.IsRangePermitted(&aa.authInfo, r.Key, r.RangeEnd); err != nil {
return nil, err
}
wildcard.ExpandWildcardToRange(r, true)
return aa.applierV3.Range(txnID, r)
}

func (aa *authApplierV3) DeleteRange(txnID int64, r *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error) {
wildcard.ExpandWildcardToRange(r, true)
if err := aa.as.IsDeleteRangePermitted(&aa.authInfo, r.Key, r.RangeEnd); err != nil {
return nil, err
}
Expand Down
45 changes: 45 additions & 0 deletions etcdserver/etcdserverpb/range.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package etcdserverpb

// See github.com/coreos/etcd/wildcard/wildcard.go
// for the definition of the BegEndRange interface.
// The methods allow a uniform treatment of ranges.

// GetBeg implements part of the BegEndRange interface.
func (r *RangeRequest) GetBeg() []byte {
return r.Key
}

// SetBeg implements part of the BegEndRange interface.
func (r *RangeRequest) SetBeg(beg []byte) {
r.Key = beg
}

// GetEnd implements part of the BegEndRange interface.
func (r *RangeRequest) GetEnd() []byte {
return r.RangeEnd
}

// SetEnd implements part of the BegEndRange interface.
func (r *RangeRequest) SetEnd(end []byte) {
r.RangeEnd = end
}

// GetBeg implements part of the BegEndRange interface.
func (r *DeleteRangeRequest) GetBeg() []byte {
return r.Key
}

// SetBeg implements part of the BegEndRange interface.
func (r *DeleteRangeRequest) SetBeg(beg []byte) {
r.Key = beg
}

// GetEnd implements part of the BegEndRange interface.
func (r *DeleteRangeRequest) GetEnd() []byte {
return r.RangeEnd
}

// SetEnd implements part of the BegEndRange interface.
func (r *DeleteRangeRequest) SetEnd(end []byte) {
r.RangeEnd = end
}
71 changes: 71 additions & 0 deletions integration/v3_wildcard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package integration

import (
"golang.org/x/net/context"
"testing"
)

func TestWildcardPathQueries(t *testing.T) {
clus := NewClusterV3(t, &ClusterConfig{Size: 1})
defer clus.Terminate(t)

cli := clus.RandClient()
if _, err := cli.Put(context.TODO(), "/home/joe/car", "margaret"); err != nil {
t.Fatal(err)
}
if _, err := cli.Put(context.TODO(), "/home/joe/boat", "felicity"); err != nil {
t.Fatal(err)
}

fetched, err := cli.Get(context.TODO(), "/home/joe/*")
if err != nil {
t.Fatal(err)
}
if fetched.Count != 2 {
t.Fatalf("expected 2, got %v", fetched.Count)
}

// escaped stars do nothing
esc, err := cli.Delete(context.TODO(), `/home/joe/\*`)
if err != nil {
t.Fatal(err)
}
if esc.Deleted != 0 {
t.Fatalf("expected 0, got %v", esc.Deleted)
}

del, err := cli.Delete(context.TODO(), `/home/joe/*`)
if err != nil {
t.Fatal(err)
}
if del.Deleted != 2 {
t.Fatalf("expected 2, got %v", del.Deleted)
}

// read back a key that ends in star
if _, err = cli.Put(context.TODO(), "abc*", "felicity"); err != nil {
t.Fatal(err)
}

get, err := cli.Get(context.TODO(), `abc\*`)
if err != nil {
t.Fatal(err)
}
if get.Count != 1 {
t.Fatalf("expected 1, got %v", get.Count)
}
}
88 changes: 88 additions & 0 deletions wildcard/wildcard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*
The wildcard package provides support for converting
single keys ending in the '*' star wildcard character into
prefix ranges. Backslash escaped '\*' stars are ignored.
*/
package wildcard

// ExpandWildcardToRange enables get '*' to return all
// keys within the range specified by a prefix.
//
// Example: get '/home/user/*' will return all keys with
// the prefix '/home/user/'.
//
// Returns true if conversion/expansion took place.
//
// If removeWild is true, then we will remove the wildcard byte
// before creating the range.
// Backslash escaped (e.g. `\*`) wildcards will not be changed.
//
func ExpandWildcardToRange(r BegEndRange, removeWild bool) bool {
beg := r.GetBeg()
end := r.GetEnd()
lenkey := len(beg)
lenend := len(end)
if lenkey == 0 || lenend > 0 {
return false
}
if beg[lenkey-1] != '*' {
return false
}
if lenkey == 1 {
// request for all keys
beg = []byte{0}
end = []byte{0}
r.SetBeg(beg)
r.SetEnd(end)
return true
}
if beg[lenkey-2] == '\\' {
// the star was escaped with a backslash: \*
beg = beg[:lenkey-1]
beg[lenkey-2] = '*'
r.SetBeg(beg)
return false
}

// we have a wildcard query, with keylen >= 2.
// Fix the begin and end:
if removeWild {
beg = beg[:lenkey-1] // remove the '*'
}
// setting RangeEnd one bit higher makes a prefix query
end = make([]byte, lenkey-1)
copy(end, beg)
end[lenkey-2]++
// check for overflow
if end[lenkey-2] == 0 {
// yep, overflowed.
end = append(end, 1)
}
r.SetBeg(beg)
r.SetEnd(end)
return true
}

// BegEndRange unifies rangePerm, pb.RangeRequest,
// and pb.DeleteRangeRequest
type BegEndRange interface {
GetBeg() []byte
SetBeg(beg []byte)

GetEnd() []byte
SetEnd(end []byte)
}

0 comments on commit c26e737

Please sign in to comment.