diff --git a/auth/authpb/auth.pb.go b/auth/authpb/auth.pb.go index 00a63e464..9f8430808 100644 --- a/auth/authpb/auth.pb.go +++ b/auth/authpb/auth.pb.go @@ -72,8 +72,9 @@ func (*User) Descriptor() ([]byte, []int) { return fileDescriptorAuth, []int{0} // Permission is a single entity type Permission struct { - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - PermType Permission_Type `protobuf:"varint,2,opt,name=permType,proto3,enum=authpb.Permission_Type" json:"permType,omitempty"` + PermType Permission_Type `protobuf:"varint,1,opt,name=permType,proto3,enum=authpb.Permission_Type" json:"permType,omitempty"` + Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + RangeEnd []byte `protobuf:"bytes,3,opt,name=range_end,json=rangeEnd,proto3" json:"range_end,omitempty"` } func (m *Permission) Reset() { *m = Permission{} } @@ -158,16 +159,22 @@ func (m *Permission) MarshalTo(data []byte) (int, error) { _ = i var l int _ = l + if m.PermType != 0 { + data[i] = 0x8 + i++ + i = encodeVarintAuth(data, i, uint64(m.PermType)) + } if len(m.Key) > 0 { - data[i] = 0xa + data[i] = 0x12 i++ i = encodeVarintAuth(data, i, uint64(len(m.Key))) i += copy(data[i:], m.Key) } - if m.PermType != 0 { - data[i] = 0x10 + if len(m.RangeEnd) > 0 { + data[i] = 0x1a i++ - i = encodeVarintAuth(data, i, uint64(m.PermType)) + i = encodeVarintAuth(data, i, uint64(len(m.RangeEnd))) + i += copy(data[i:], m.RangeEnd) } return i, nil } @@ -258,12 +265,16 @@ func (m *User) Size() (n int) { func (m *Permission) Size() (n int) { var l int _ = l + if m.PermType != 0 { + n += 1 + sovAuth(uint64(m.PermType)) + } l = len(m.Key) if l > 0 { n += 1 + l + sovAuth(uint64(l)) } - if m.PermType != 0 { - n += 1 + sovAuth(uint64(m.PermType)) + l = len(m.RangeEnd) + if l > 0 { + n += 1 + l + sovAuth(uint64(l)) } return n } @@ -468,6 +479,25 @@ func (m *Permission) Unmarshal(data []byte) error { } switch fieldNum { case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field PermType", wireType) + } + m.PermType = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuth + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + m.PermType |= (Permission_Type(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) } @@ -498,11 +528,11 @@ func (m *Permission) Unmarshal(data []byte) error { m.Key = []byte{} } iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field PermType", wireType) + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RangeEnd", wireType) } - m.PermType = 0 + var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowAuth @@ -512,11 +542,23 @@ func (m *Permission) Unmarshal(data []byte) error { } b := data[iNdEx] iNdEx++ - m.PermType |= (Permission_Type(b) & 0x7F) << shift + byteLen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } + if byteLen < 0 { + return ErrInvalidLengthAuth + } + postIndex := iNdEx + byteLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RangeEnd = append(m.RangeEnd[:0], data[iNdEx:postIndex]...) + if m.RangeEnd == nil { + m.RangeEnd = []byte{} + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipAuth(data[iNdEx:]) @@ -756,22 +798,23 @@ var ( ) var fileDescriptorAuth = []byte{ - // 265 bytes of a gzipped FileDescriptorProto + // 276 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x4a, 0x2c, 0x2d, 0xc9, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x03, 0xb1, 0x0b, 0x92, 0xa4, 0x44, 0xd2, 0xf3, 0xd3, 0xf3, 0xc1, 0x42, 0xfa, 0x20, 0x16, 0x44, 0x56, 0xc9, 0x87, 0x8b, 0x25, 0xb4, 0x38, 0xb5, 0x48, 0x48, 0x88, 0x8b, 0x25, 0x2f, 0x31, 0x37, 0x55, 0x82, 0x51, 0x81, 0x51, 0x83, 0x27, 0x08, 0xcc, 0x16, 0x92, 0xe2, 0xe2, 0x28, 0x48, 0x2c, 0x2e, 0x2e, 0xcf, 0x2f, 0x4a, 0x91, 0x60, 0x02, 0x8b, 0xc3, 0xf9, 0x42, 0x22, 0x5c, 0xac, 0x45, 0xf9, 0x39, 0xa9, 0xc5, 0x12, 0xcc, 0x0a, 0xcc, - 0x1a, 0x9c, 0x41, 0x10, 0x8e, 0x52, 0x3d, 0x17, 0x57, 0x40, 0x6a, 0x51, 0x6e, 0x66, 0x71, 0x71, - 0x66, 0x7e, 0x9e, 0x90, 0x00, 0x17, 0x73, 0x76, 0x6a, 0x25, 0xd4, 0x48, 0x10, 0x53, 0xc8, 0x98, - 0x8b, 0xa3, 0x20, 0xb5, 0x28, 0x37, 0xa4, 0xb2, 0x20, 0x15, 0x6c, 0x22, 0x9f, 0x91, 0xb8, 0x1e, - 0xc4, 0x79, 0x7a, 0x08, 0x7d, 0x7a, 0x20, 0xe9, 0x20, 0xb8, 0x42, 0x25, 0x2d, 0x2e, 0x16, 0x10, - 0x2d, 0xc4, 0xc1, 0xc5, 0x12, 0xe4, 0xea, 0xe8, 0x22, 0xc0, 0x20, 0xc4, 0xc9, 0xc5, 0x1a, 0x1e, - 0xe4, 0x19, 0xe2, 0x2a, 0xc0, 0x28, 0xc4, 0xcb, 0xc5, 0x09, 0x12, 0x84, 0x70, 0x99, 0x94, 0x42, - 0xb8, 0x58, 0x82, 0xf2, 0x73, 0x52, 0xb1, 0x7a, 0xc7, 0x82, 0x8b, 0x37, 0x3b, 0xb5, 0x12, 0x61, - 0x8f, 0x04, 0x93, 0x02, 0xb3, 0x06, 0xb7, 0x91, 0x10, 0xa6, 0x0b, 0x82, 0x50, 0x15, 0x3a, 0x89, - 0x9c, 0x78, 0x28, 0xc7, 0x70, 0xe1, 0xa1, 0x1c, 0xc3, 0x89, 0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, - 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x98, 0xc4, 0x06, 0x0e, 0x41, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, - 0xff, 0x92, 0x06, 0xa1, 0xed, 0x6d, 0x01, 0x00, 0x00, + 0x1a, 0x9c, 0x41, 0x10, 0x8e, 0xd2, 0x1c, 0x46, 0x2e, 0xae, 0x80, 0xd4, 0xa2, 0xdc, 0xcc, 0xe2, + 0xe2, 0xcc, 0xfc, 0x3c, 0x21, 0x63, 0xa0, 0x01, 0x40, 0x5e, 0x48, 0x65, 0x01, 0xc4, 0x60, 0x3e, + 0x23, 0x71, 0x3d, 0x88, 0x6b, 0xf4, 0x10, 0xaa, 0xf4, 0x40, 0xd2, 0x41, 0x70, 0x85, 0x42, 0x02, + 0x5c, 0xcc, 0xd9, 0xa9, 0x95, 0x50, 0x0b, 0x41, 0x4c, 0x21, 0x69, 0x2e, 0xce, 0xa2, 0xc4, 0xbc, + 0xf4, 0xd4, 0xf8, 0xd4, 0xbc, 0x14, 0xa0, 0x7d, 0x60, 0x87, 0x80, 0x05, 0x5c, 0xf3, 0x52, 0x94, + 0xb4, 0xb8, 0x58, 0xc0, 0xda, 0x38, 0xb8, 0x58, 0x82, 0x5c, 0x1d, 0x5d, 0x04, 0x18, 0x84, 0x38, + 0xb9, 0x58, 0xc3, 0x83, 0x3c, 0x43, 0x5c, 0x05, 0x18, 0x85, 0x78, 0xb9, 0x38, 0x41, 0x82, 0x10, + 0x2e, 0x93, 0x52, 0x08, 0x50, 0x0d, 0xd0, 0x9d, 0x58, 0x3d, 0x6b, 0xc1, 0xc5, 0x0b, 0xb4, 0x0b, + 0xe1, 0x2c, 0xa0, 0x03, 0x98, 0x35, 0xb8, 0x8d, 0x84, 0x30, 0x1d, 0x1c, 0x84, 0xaa, 0xd0, 0x49, + 0xe4, 0xc4, 0x43, 0x39, 0x86, 0x0b, 0x40, 0x7c, 0xe2, 0x91, 0x1c, 0xe3, 0x05, 0x20, 0x7e, 0x00, + 0xc4, 0x49, 0x6c, 0xe0, 0xf0, 0x35, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x9e, 0x31, 0x53, 0xfd, + 0x8b, 0x01, 0x00, 0x00, } diff --git a/auth/authpb/auth.proto b/auth/authpb/auth.proto index 9308a179a..001d33435 100644 --- a/auth/authpb/auth.proto +++ b/auth/authpb/auth.proto @@ -18,14 +18,15 @@ message User { // Permission is a single entity message Permission { - bytes key = 1; - enum Type { READ = 0; WRITE = 1; READWRITE = 2; } - Type permType = 2; + Type permType = 1; + + bytes key = 2; + bytes range_end = 3; } // Role is a single entry in the bucket authRoles diff --git a/auth/range_perm_cache.go b/auth/range_perm_cache.go new file mode 100644 index 000000000..8ad39aa55 --- /dev/null +++ b/auth/range_perm_cache.go @@ -0,0 +1,195 @@ +// 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 ( + "sort" + "strings" + + "github.com/coreos/etcd/auth/authpb" + "github.com/coreos/etcd/mvcc/backend" +) + +func isSubset(a, b *rangePerm) bool { + // return true if a is a subset of b + return 0 <= strings.Compare(a.begin, b.begin) && strings.Compare(a.end, b.end) <= 0 +} + +func reduceSubsets(perms []*rangePerm) []*rangePerm { + // TODO(mitake): currently it is O(n^2), we need a better algorithm + ret := make([]*rangePerm, 0) + + for i := range perms { + subset := false + + for j := range perms { + if i != j && isSubset(perms[i], perms[j]) { + subset = true + break + } + } + + if subset { + continue + } + + ret = append(ret, perms[i]) + } + + return ret +} + +func unifyPerms(perms []*rangePerm) []*rangePerm { + ret := make([]*rangePerm, 0) + perms = reduceSubsets(perms) + sort.Sort(RangePermSliceByBegin(perms)) + + i := 0 + for i < len(perms) { + begin := i + for i+1 < len(perms) && perms[i].end >= perms[i+1].begin { + i++ + } + + if i == begin { + ret = append(ret, &rangePerm{begin: perms[i].begin, end: perms[i].end}) + } else { + ret = append(ret, &rangePerm{begin: perms[begin].begin, end: perms[i].end}) + } + + i++ + } + + return ret +} + +func (as *authStore) makeUnifiedPerms(tx backend.BatchTx, userName string) *unifiedRangePermissions { + user := getUser(tx, userName) + if user == nil { + plog.Errorf("invalid user name %s", userName) + return nil + } + + var readPerms, writePerms []*rangePerm + + for _, roleName := range user.Roles { + _, vs := tx.UnsafeRange(authRolesBucketName, []byte(roleName), nil, 0) + if len(vs) != 1 { + plog.Errorf("invalid role name %s", roleName) + return nil + } + + role := &authpb.Role{} + err := role.Unmarshal(vs[0]) + if err != nil { + plog.Errorf("failed to unmarshal a role %s: %s", roleName, err) + return nil + } + + for _, perm := range role.KeyPermission { + if len(perm.RangeEnd) == 0 { + continue + } + + if perm.PermType == authpb.READWRITE || perm.PermType == authpb.READ { + readPerms = append(readPerms, &rangePerm{begin: string(perm.Key), end: string(perm.RangeEnd)}) + } + + if perm.PermType == authpb.READWRITE || perm.PermType == authpb.WRITE { + writePerms = append(writePerms, &rangePerm{begin: string(perm.Key), end: string(perm.RangeEnd)}) + } + } + } + + return &unifiedRangePermissions{readPerms: unifyPerms(readPerms), writePerms: unifyPerms(writePerms)} +} + +func checkCachedPerm(cachedPerms *unifiedRangePermissions, userName string, key, rangeEnd string, write, read bool) bool { + var perms []*rangePerm + + if write { + perms = cachedPerms.writePerms + } else { + perms = cachedPerms.readPerms + } + + for _, perm := range perms { + if strings.Compare(rangeEnd, "") != 0 { + if strings.Compare(perm.begin, key) <= 0 && strings.Compare(rangeEnd, perm.end) <= 0 { + return true + } + } else { + if strings.Compare(perm.begin, key) <= 0 && strings.Compare(key, perm.end) <= 0 { + return true + } + } + } + + return false +} + +func (as *authStore) isRangeOpPermitted(tx backend.BatchTx, userName string, key, rangeEnd string, write, read bool) bool { + // assumption: tx is Lock()ed + _, ok := as.rangePermCache[userName] + if ok { + return checkCachedPerm(as.rangePermCache[userName], userName, key, rangeEnd, write, read) + } + + perms := as.makeUnifiedPerms(tx, userName) + if perms == nil { + plog.Errorf("failed to create a unified permission of user %s", userName) + return false + } + as.rangePermCache[userName] = perms + + return checkCachedPerm(as.rangePermCache[userName], userName, key, rangeEnd, write, read) + +} + +func (as *authStore) clearCachedPerm() { + as.rangePermCache = make(map[string]*unifiedRangePermissions) +} + +func (as *authStore) invalidateCachedPerm(userName string) { + delete(as.rangePermCache, userName) +} + +type unifiedRangePermissions struct { + // readPerms[i] and readPerms[j] (i != j) don't overlap + readPerms []*rangePerm + // writePerms[i] and writePerms[j] (i != j) don't overlap, too + writePerms []*rangePerm +} + +type rangePerm struct { + begin, end string +} + +type RangePermSliceByBegin []*rangePerm + +func (slice RangePermSliceByBegin) Len() int { + return len(slice) +} + +func (slice RangePermSliceByBegin) Less(i, j int) bool { + if slice[i].begin == slice[j].begin { + return slice[i].end < slice[j].end + } + return slice[i].begin < slice[j].begin +} + +func (slice RangePermSliceByBegin) Swap(i, j int) { + slice[i], slice[j] = slice[j], slice[i] +} diff --git a/auth/range_perm_cache_test.go b/auth/range_perm_cache_test.go new file mode 100644 index 000000000..d32ea50fb --- /dev/null +++ b/auth/range_perm_cache_test.go @@ -0,0 +1,96 @@ +// 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 ( + "testing" +) + +func isPermsEqual(a, b []*rangePerm) bool { + if len(a) != len(b) { + return false + } + + for i := range a { + if len(b) <= i { + return false + } + + if a[i].begin != b[i].begin || a[i].end != b[i].end { + return false + } + } + + return true +} + +func TestUnifyParams(t *testing.T) { + tests := []struct { + params []*rangePerm + want []*rangePerm + }{ + { + []*rangePerm{{"a", "b"}}, + []*rangePerm{{"a", "b"}}, + }, + { + []*rangePerm{{"a", "b"}, {"b", "c"}}, + []*rangePerm{{"a", "c"}}, + }, + { + []*rangePerm{{"a", "c"}, {"b", "d"}}, + []*rangePerm{{"a", "d"}}, + }, + { + []*rangePerm{{"a", "b"}, {"b", "c"}, {"d", "e"}}, + []*rangePerm{{"a", "c"}, {"d", "e"}}, + }, + { + []*rangePerm{{"a", "b"}, {"c", "d"}, {"e", "f"}}, + []*rangePerm{{"a", "b"}, {"c", "d"}, {"e", "f"}}, + }, + { + []*rangePerm{{"e", "f"}, {"c", "d"}, {"a", "b"}}, + []*rangePerm{{"a", "b"}, {"c", "d"}, {"e", "f"}}, + }, + { + []*rangePerm{{"a", "b"}, {"c", "d"}, {"a", "z"}}, + []*rangePerm{{"a", "z"}}, + }, + { + []*rangePerm{{"a", "b"}, {"c", "d"}, {"a", "z"}, {"1", "9"}}, + []*rangePerm{{"1", "9"}, {"a", "z"}}, + }, + { + []*rangePerm{{"a", "b"}, {"c", "d"}, {"a", "z"}, {"1", "a"}}, + []*rangePerm{{"1", "z"}}, + }, + { + []*rangePerm{{"a", "b"}, {"a", "z"}, {"5", "6"}, {"1", "9"}}, + []*rangePerm{{"1", "9"}, {"a", "z"}}, + }, + { + []*rangePerm{{"a", "b"}, {"b", "c"}, {"c", "d"}, {"d", "f"}, {"1", "9"}}, + []*rangePerm{{"1", "9"}, {"a", "f"}}, + }, + } + + for i, tt := range tests { + result := unifyPerms(tt.params) + if !isPermsEqual(result, tt.want) { + t.Errorf("#%d: result=%q, want=%q", i, result, tt.want) + } + } +} diff --git a/auth/store.go b/auth/store.go index 6fffa27b0..90872ecfd 100644 --- a/auth/store.go +++ b/auth/store.go @@ -103,13 +103,15 @@ type AuthStore interface { IsPutPermitted(header *pb.RequestHeader, key string) bool // IsRangePermitted checks range permission of the user - IsRangePermitted(header *pb.RequestHeader, key string) bool + IsRangePermitted(header *pb.RequestHeader, key, rangeEnd string) bool } type authStore struct { be backend.Backend enabled bool enabledMu sync.RWMutex + + rangePermCache map[string]*unifiedRangePermissions // username -> unifiedRangePermissions } func (as *authStore) AuthEnable() { @@ -126,6 +128,8 @@ func (as *authStore) AuthEnable() { as.enabled = true as.enabledMu.Unlock() + as.rangePermCache = make(map[string]*unifiedRangePermissions) + plog.Noticef("Authentication enabled") } @@ -301,6 +305,8 @@ func (as *authStore) UserGrantRole(r *pb.AuthUserGrantRoleRequest) (*pb.AuthUser tx.UnsafePut(authUsersBucketName, user.Name, marshaledUser) + as.invalidateCachedPerm(r.User) + plog.Noticef("granted role %s to user %s", r.Role, r.User) return &pb.AuthUserGrantRoleResponse{}, nil } @@ -357,6 +363,8 @@ func (as *authStore) UserRevokeRole(r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUs tx.UnsafePut(authUsersBucketName, updatedUser.Name, marshaledUser) + as.invalidateCachedPerm(r.Name) + plog.Noticef("revoked role %s from user %s", r.Role, r.Name) return &pb.AuthUserRevokeRoleResponse{}, nil } @@ -424,6 +432,10 @@ func (as *authStore) RoleRevokePermission(r *pb.AuthRoleRevokePermissionRequest) tx.UnsafePut(authRolesBucketName, updatedRole.Name, marshaledRole) + // TODO(mitake): currently single role update invalidates every cache + // It should be optimized. + as.clearCachedPerm() + plog.Noticef("revoked key %s from role %s", r.Key, r.Role) return &pb.AuthRoleRevokePermissionResponse{}, nil } @@ -546,12 +558,16 @@ func (as *authStore) RoleGrantPermission(r *pb.AuthRoleGrantPermissionRequest) ( tx.UnsafePut(authRolesBucketName, []byte(r.Name), marshaledRole) + // TODO(mitake): currently single role update invalidates every cache + // It should be optimized. + as.clearCachedPerm() + plog.Noticef("role %s's permission of key %s is updated as %s", r.Name, r.Perm.Key, authpb.Permission_Type_name[int32(r.Perm.PermType)]) return &pb.AuthRoleGrantPermissionResponse{}, nil } -func (as *authStore) isOpPermitted(userName string, key string, write bool, read bool) bool { +func (as *authStore) isOpPermitted(userName string, key, rangeEnd string, write bool, read bool) bool { // TODO(mitake): this function would be costly so we need a caching mechanism if !as.isAuthEnabled() { return true @@ -567,22 +583,26 @@ func (as *authStore) isOpPermitted(userName string, key string, write bool, read return false } - for _, roleName := range user.Roles { - _, vs := tx.UnsafeRange(authRolesBucketName, []byte(roleName), nil, 0) - if len(vs) != 1 { - plog.Errorf("invalid role name %s for permission checking", roleName) - return false - } + if strings.Compare(rangeEnd, "") == 0 { + for _, roleName := range user.Roles { + _, vs := tx.UnsafeRange(authRolesBucketName, []byte(roleName), nil, 0) + if len(vs) != 1 { + plog.Errorf("invalid role name %s for permission checking", roleName) + return false + } - role := &authpb.Role{} - err := role.Unmarshal(vs[0]) - if err != nil { - plog.Errorf("failed to unmarshal a role %s: %s", roleName, err) - return false - } + role := &authpb.Role{} + err := role.Unmarshal(vs[0]) + if err != nil { + plog.Errorf("failed to unmarshal a role %s: %s", roleName, err) + return false + } + + for _, perm := range role.KeyPermission { + if !bytes.Equal(perm.Key, []byte(key)) { + continue + } - for _, perm := range role.KeyPermission { - if bytes.Equal(perm.Key, []byte(key)) { if perm.PermType == authpb.READWRITE { return true } @@ -598,15 +618,19 @@ func (as *authStore) isOpPermitted(userName string, key string, write bool, read } } + if as.isRangeOpPermitted(tx, userName, key, rangeEnd, write, read) { + return true + } + return false } func (as *authStore) IsPutPermitted(header *pb.RequestHeader, key string) bool { - return as.isOpPermitted(header.Username, key, true, false) + return as.isOpPermitted(header.Username, key, "", true, false) } -func (as *authStore) IsRangePermitted(header *pb.RequestHeader, key string) bool { - return as.isOpPermitted(header.Username, key, false, true) +func (as *authStore) IsRangePermitted(header *pb.RequestHeader, key, rangeEnd string) bool { + return as.isOpPermitted(header.Username, key, rangeEnd, false, true) } func getUser(tx backend.BatchTx, username string) *authpb.User { diff --git a/etcdserver/apply.go b/etcdserver/apply.go index 8a3742a31..7bf55abc8 100644 --- a/etcdserver/apply.go +++ b/etcdserver/apply.go @@ -81,7 +81,7 @@ func (s *EtcdServer) applyV3Request(r *pb.InternalRaftRequest) *applyResult { ar := &applyResult{} switch { case r.Range != nil: - if s.AuthStore().IsRangePermitted(r.Header, string(r.Range.Key)) { + if s.AuthStore().IsRangePermitted(r.Header, string(r.Range.Key), string(r.Range.RangeEnd)) { ar.resp, ar.err = s.applyV3.Range(noTxn, r.Range) } else { ar.err = auth.ErrPermissionDenied