diff --git a/auth/prefix_perm.go b/auth/prefix_perm.go new file mode 100644 index 000000000000..05bb95d12efa --- /dev/null +++ b/auth/prefix_perm.go @@ -0,0 +1,57 @@ +// 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 auth + +import ( + "bytes" +) + +// isPrefixWithSlash returns true if and only if +// a) both prefix and path are single keys; and +// b) prefix ends in `/`; and +// c) path starts with prefix. +func isPrefixWithSlash(path, prefix *rangePerm) bool { + + if len(prefix.end) != 0 || len(path.end) != 0 { + return false // not single keys + } + lenpre := len(prefix.begin) + if lenpre == 0 { + return false + } + if prefix.begin[lenpre-1] != '/' { + return false + } + return bytes.HasPrefix(path.begin, prefix.begin) +} + +// isPrefixPlusWildcard returns true if and only if +// a) both prefix and path are single keys; and +// b) prefix ends in `*`; and +// c) path starts with prefix up until the `*`. +func isPrefixPlusWildcard(path, prefix *rangePerm) bool { + + if len(prefix.end) != 0 || len(path.end) != 0 { + return false // not single keys + } + lenpre := len(prefix.begin) + if lenpre == 0 { + return false + } + if prefix.begin[lenpre-1] != '*' { + return false + } + return bytes.HasPrefix(path.begin, prefix.begin[:(lenpre-1)]) +} diff --git a/auth/range_perm_cache.go b/auth/range_perm_cache.go index 108ee961238a..fb5f9c5ecd0f 100644 --- a/auth/range_perm_cache.go +++ b/auth/range_perm_cache.go @@ -22,22 +22,53 @@ import ( "github.com/coreos/etcd/mvcc/backend" ) -// 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 } } +func rangeIsPrefix(a *rangePerm) bool { + if len(a.end) == 0 { + return true + } + lenbeg := len(a.begin) + lenend := len(a.end) + x := a.begin[lenbeg-1] + y := a.end[lenend-1] + if lenbeg == lenend { + if x+1 == y { + return true + } + return false + } + // check for overflow + if lenend != 1+lenbeg { + return false + } + // INVAR: lenend is one greater than lenbeg, might have overflowed + if x+1 == 0 && y == 0 { + // yep, overflowed. + return true + } + return false +} + func isRangeEqual(a, b *rangePerm) bool { return bytes.Equal(a.begin, b.begin) && bytes.Equal(a.end, b.end) } @@ -88,12 +119,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 } @@ -156,6 +193,12 @@ func checkKeyPerm(cachedPerms *unifiedRangePermissions, key, rangeEnd []byte, pe if isSubset(requiredPerm, perm) { return true } + if isPrefixWithSlash(requiredPerm, perm) { + return true + } + if isPrefixPlusWildcard(requiredPerm, perm) { + return true + } } return false 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..e2196345b8bc 100644 --- a/etcdserver/apply_auth.go +++ b/etcdserver/apply_auth.go @@ -75,10 +75,66 @@ 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 } + AllowWildcardGets(r) return aa.applierV3.Range(txnID, r) } +// AllowWildcardGets enables get '*' to return all keys, +// and get '/home/user/*' to return all keys with +// the prefix '/home/user/'. +func AllowWildcardGets(r *pb.RangeRequest) { + lenkey := len(r.Key) + lenend := len(r.RangeEnd) + if lenend == 0 && lenkey > 0 && r.Key[lenkey-1] == '*' { + if lenkey == 1 { + // request for all keys + r.Key = []byte{0} + r.RangeEnd = []byte{0} + return + } + // we have a wildcard query, with keylen >= 2. Fix the begin and end: + r.Key = r.Key[:lenkey-1] // remove the '*' + // setting RangeEnd one bit higher makes a prefix query + r.RangeEnd = make([]byte, lenkey-1) + copy(r.RangeEnd, r.Key) + r.RangeEnd[lenkey-2]++ + // check for overflow + if r.RangeEnd[lenkey-2] == 0 { + // yep, overflowed. + r.RangeEnd = append(r.RangeEnd, 0) + } + } +} + +// AllowWildcardDeletes enables del '*' to delete all keys, +// and del '/home/user/*' to delete all keys with +// the prefix '/home/user/'. +func AllowWildcardDeletes(r *pb.DeleteRangeRequest) { + lenkey := len(r.Key) + lenend := len(r.RangeEnd) + if lenend == 0 && lenkey > 0 && r.Key[lenkey-1] == '*' { + if lenkey == 1 { + // request for all keys + r.Key = []byte{0} + r.RangeEnd = []byte{0} + return + } + // we have a wildcard query, with keylen >= 2. Fix the begin and end: + r.Key = r.Key[:lenkey-1] // remove the '*' + // setting RangeEnd one bit higher makes a prefix query + r.RangeEnd = make([]byte, lenkey-1) + copy(r.RangeEnd, r.Key) + r.RangeEnd[lenkey-2]++ + // check for overflow + if r.RangeEnd[lenkey-2] == 0 { + // yep, overflowed. + r.RangeEnd = append(r.RangeEnd, 0) + } + } +} + func (aa *authApplierV3) DeleteRange(txnID int64, r *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error) { + AllowWildcardDeletes(r) if err := aa.as.IsDeleteRangePermitted(&aa.authInfo, r.Key, r.RangeEnd); err != nil { return nil, err }