diff --git a/auth/range_perm_cache.go b/auth/range_perm_cache.go index 108ee961238a..9897fd7b36e6 100644 --- a/auth/range_perm_cache.go +++ b/auth/range_perm_cache.go @@ -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 } } @@ -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 } @@ -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 } @@ -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 +} diff --git a/auth/range_perm_cache_test.go b/auth/range_perm_cache_test.go index b5451efa3261..4e291d715457 100644 --- a/auth/range_perm_cache_test.go +++ b/auth/range_perm_cache_test.go @@ -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 { @@ -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) } } } diff --git a/etcdserver/apply_auth.go b/etcdserver/apply_auth.go index 4868e855ca12..ae6538c11194 100644 --- a/etcdserver/apply_auth.go +++ b/etcdserver/apply_auth.go @@ -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 { @@ -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 } diff --git a/etcdserver/etcdserverpb/range.go b/etcdserver/etcdserverpb/range.go new file mode 100644 index 000000000000..bf66bca49607 --- /dev/null +++ b/etcdserver/etcdserverpb/range.go @@ -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 +} diff --git a/integration/v3_wildcard_test.go b/integration/v3_wildcard_test.go new file mode 100644 index 000000000000..cf1449d9015c --- /dev/null +++ b/integration/v3_wildcard_test.go @@ -0,0 +1,49 @@ +// 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) + } + + 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) + } +} diff --git a/wildcard/wildcard.go b/wildcard/wildcard.go new file mode 100644 index 000000000000..7f163642b6a1 --- /dev/null +++ b/wildcard/wildcard.go @@ -0,0 +1,85 @@ +// 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: \* + 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) +}