Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

etcdctl/ctlv3: auth: wildcard terminated paths specify ranges #6371

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}