auth: store cached permission information in a form of interval tree
This commit change the type of cached permission information from the home made thing to interval tree. It improves computational complexity of permission checking from O(n) to O(lg n).release-3.2
parent
e9bfcc02ce
commit
ad2111a6f4
|
@ -15,87 +15,11 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/etcd/auth/authpb"
|
||||
"github.com/coreos/etcd/mvcc/backend"
|
||||
"github.com/coreos/etcd/pkg/adt"
|
||||
)
|
||||
|
||||
// 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
|
||||
return false
|
||||
case len(a.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:
|
||||
// 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 isRangeEqual(a, b *rangePerm) bool {
|
||||
return bytes.Equal(a.begin, b.begin) && bytes.Equal(a.end, b.end)
|
||||
}
|
||||
|
||||
// mergeRangePerms merges adjacent rangePerms.
|
||||
func mergeRangePerms(perms []*rangePerm) (rs []*rangePerm) {
|
||||
if len(perms) < 2 {
|
||||
return perms
|
||||
}
|
||||
sort.Sort(RangePermSliceByBegin(perms))
|
||||
|
||||
// initialize with first rangePerm
|
||||
rs = append(rs, perms[0])
|
||||
|
||||
// merge in-place from 2nd
|
||||
for i := 1; i < len(perms); i++ {
|
||||
rp := rs[len(rs)-1]
|
||||
|
||||
// skip duplicate range
|
||||
if isRangeEqual(rp, perms[i]) {
|
||||
continue
|
||||
}
|
||||
|
||||
// merge ["a", "") with ["a", "c")
|
||||
if bytes.Equal(rp.begin, perms[i].begin) && len(rp.end) == 0 && len(perms[i].end) > 0 {
|
||||
rp.end = perms[i].end
|
||||
continue
|
||||
}
|
||||
|
||||
// do not merge ["a", "b") with ["b", "")
|
||||
if bytes.Equal(rp.end, perms[i].begin) && len(perms[i].end) == 0 {
|
||||
rs = append(rs, perms[i])
|
||||
continue
|
||||
}
|
||||
|
||||
// rp.end < perms[i].begin; found beginning of next range
|
||||
if bytes.Compare(rp.end, perms[i].begin) < 0 {
|
||||
rs = append(rs, perms[i])
|
||||
continue
|
||||
}
|
||||
|
||||
rp.end = getMax(rp.end, perms[i].end)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getMax(a, b []byte) []byte {
|
||||
if bytes.Compare(a, b) > 0 {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func getMergedPerms(tx backend.BatchTx, userName string) *unifiedRangePermissions {
|
||||
user := getUser(tx, userName)
|
||||
if user == nil {
|
||||
|
@ -103,7 +27,8 @@ func getMergedPerms(tx backend.BatchTx, userName string) *unifiedRangePermission
|
|||
return nil
|
||||
}
|
||||
|
||||
var readPerms, writePerms []*rangePerm
|
||||
readPerms := &adt.IntervalTree{}
|
||||
writePerms := &adt.IntervalTree{}
|
||||
|
||||
for _, roleName := range user.Roles {
|
||||
role := getRole(tx, roleName)
|
||||
|
@ -112,30 +37,36 @@ func getMergedPerms(tx backend.BatchTx, userName string) *unifiedRangePermission
|
|||
}
|
||||
|
||||
for _, perm := range role.KeyPermission {
|
||||
rp := &rangePerm{begin: perm.Key, end: perm.RangeEnd}
|
||||
var ivl adt.Interval
|
||||
|
||||
if len(perm.RangeEnd) != 0 {
|
||||
ivl = adt.NewStringInterval(string(perm.Key), string(perm.RangeEnd))
|
||||
} else {
|
||||
ivl = adt.NewStringPoint(string(perm.Key))
|
||||
}
|
||||
|
||||
switch perm.PermType {
|
||||
case authpb.READWRITE:
|
||||
readPerms = append(readPerms, rp)
|
||||
writePerms = append(writePerms, rp)
|
||||
readPerms.Insert(ivl, struct{}{})
|
||||
writePerms.Insert(ivl, struct{}{})
|
||||
|
||||
case authpb.READ:
|
||||
readPerms = append(readPerms, rp)
|
||||
readPerms.Insert(ivl, struct{}{})
|
||||
|
||||
case authpb.WRITE:
|
||||
writePerms = append(writePerms, rp)
|
||||
writePerms.Insert(ivl, struct{}{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &unifiedRangePermissions{
|
||||
readPerms: mergeRangePerms(readPerms),
|
||||
writePerms: mergeRangePerms(writePerms),
|
||||
readPerms: readPerms,
|
||||
writePerms: writePerms,
|
||||
}
|
||||
}
|
||||
|
||||
func checkKeyPerm(cachedPerms *unifiedRangePermissions, key, rangeEnd []byte, permtyp authpb.Permission_Type) bool {
|
||||
var tocheck []*rangePerm
|
||||
func checkKeyInterval(cachedPerms *unifiedRangePermissions, key, rangeEnd string, permtyp authpb.Permission_Type) bool {
|
||||
var tocheck *adt.IntervalTree
|
||||
|
||||
switch permtyp {
|
||||
case authpb.READ:
|
||||
|
@ -146,18 +77,51 @@ func checkKeyPerm(cachedPerms *unifiedRangePermissions, key, rangeEnd []byte, pe
|
|||
plog.Panicf("unknown auth type: %v", permtyp)
|
||||
}
|
||||
|
||||
requiredPerm := &rangePerm{begin: key, end: rangeEnd}
|
||||
ivl := adt.NewStringInterval(key, rangeEnd)
|
||||
|
||||
for _, perm := range tocheck {
|
||||
if isSubset(requiredPerm, perm) {
|
||||
isContiguous := true
|
||||
var maxEnd, minBegin adt.Comparable
|
||||
|
||||
tocheck.Visit(ivl, func(n *adt.IntervalValue) bool {
|
||||
if minBegin == nil {
|
||||
minBegin = n.Ivl.Begin
|
||||
maxEnd = n.Ivl.End
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
if maxEnd.Compare(n.Ivl.Begin) < 0 {
|
||||
isContiguous = false
|
||||
return false
|
||||
}
|
||||
|
||||
if n.Ivl.End.Compare(maxEnd) > 0 {
|
||||
maxEnd = n.Ivl.End
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return isContiguous && maxEnd.Compare(ivl.End) >= 0 && minBegin.Compare(ivl.Begin) <= 0
|
||||
}
|
||||
|
||||
func (as *authStore) isRangeOpPermitted(tx backend.BatchTx, userName string, key, rangeEnd []byte, permtyp authpb.Permission_Type) bool {
|
||||
func checkKeyPoint(cachedPerms *unifiedRangePermissions, key string, permtyp authpb.Permission_Type) bool {
|
||||
var tocheck *adt.IntervalTree
|
||||
|
||||
switch permtyp {
|
||||
case authpb.READ:
|
||||
tocheck = cachedPerms.readPerms
|
||||
case authpb.WRITE:
|
||||
tocheck = cachedPerms.writePerms
|
||||
default:
|
||||
plog.Panicf("unknown auth type: %v", permtyp)
|
||||
}
|
||||
|
||||
pt := adt.NewStringPoint(key)
|
||||
|
||||
return tocheck.Contains(pt)
|
||||
}
|
||||
|
||||
func (as *authStore) isRangeOpPermitted(tx backend.BatchTx, userName string, key, rangeEnd string, permtyp authpb.Permission_Type) bool {
|
||||
// assumption: tx is Lock()ed
|
||||
_, ok := as.rangePermCache[userName]
|
||||
if !ok {
|
||||
|
@ -169,7 +133,11 @@ func (as *authStore) isRangeOpPermitted(tx backend.BatchTx, userName string, key
|
|||
as.rangePermCache[userName] = perms
|
||||
}
|
||||
|
||||
return checkKeyPerm(as.rangePermCache[userName], key, rangeEnd, permtyp)
|
||||
if len(rangeEnd) == 0 {
|
||||
return checkKeyPoint(as.rangePermCache[userName], key, permtyp)
|
||||
}
|
||||
|
||||
return checkKeyInterval(as.rangePermCache[userName], key, rangeEnd, permtyp)
|
||||
}
|
||||
|
||||
func (as *authStore) clearCachedPerm() {
|
||||
|
@ -181,35 +149,6 @@ func (as *authStore) invalidateCachedPerm(userName string) {
|
|||
}
|
||||
|
||||
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 []byte
|
||||
}
|
||||
|
||||
type RangePermSliceByBegin []*rangePerm
|
||||
|
||||
func (slice RangePermSliceByBegin) Len() int {
|
||||
return len(slice)
|
||||
}
|
||||
|
||||
func (slice RangePermSliceByBegin) Less(i, j int) bool {
|
||||
switch bytes.Compare(slice[i].begin, slice[j].begin) {
|
||||
case 0: // begin(i) == begin(j)
|
||||
return bytes.Compare(slice[i].end, slice[j].end) == -1
|
||||
|
||||
case -1: // begin(i) < begin(j)
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (slice RangePermSliceByBegin) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
readPerms *adt.IntervalTree
|
||||
writePerms *adt.IntervalTree
|
||||
}
|
||||
|
|
|
@ -15,155 +15,45 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/auth/authpb"
|
||||
"github.com/coreos/etcd/pkg/adt"
|
||||
)
|
||||
|
||||
func isPermsEqual(a, b []*rangePerm) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a {
|
||||
if len(b) <= i {
|
||||
return false
|
||||
}
|
||||
|
||||
if !bytes.Equal(a[i].begin, b[i].begin) || !bytes.Equal(a[i].end, b[i].end) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TestGetMergedPerms(t *testing.T) {
|
||||
func TestRangePermission(t *testing.T) {
|
||||
tests := []struct {
|
||||
params []*rangePerm
|
||||
want []*rangePerm
|
||||
perms []adt.Interval
|
||||
begin string
|
||||
end string
|
||||
want bool
|
||||
}{
|
||||
{ // subsets converge
|
||||
[]*rangePerm{{[]byte{2}, []byte{3}}, {[]byte{2}, []byte{5}}, {[]byte{1}, []byte{4}}},
|
||||
[]*rangePerm{{[]byte{1}, []byte{5}}},
|
||||
},
|
||||
{ // subsets converge
|
||||
[]*rangePerm{{[]byte{0}, []byte{3}}, {[]byte{0}, []byte{1}}, {[]byte{2}, []byte{4}}, {[]byte{0}, []byte{2}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{4}}},
|
||||
},
|
||||
{ // biggest range at the end
|
||||
[]*rangePerm{{[]byte{2}, []byte{3}}, {[]byte{0}, []byte{2}}, {[]byte{1}, []byte{4}}, {[]byte{0}, []byte{5}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{5}}},
|
||||
},
|
||||
{ // biggest range at the beginning
|
||||
[]*rangePerm{{[]byte{0}, []byte{5}}, {[]byte{2}, []byte{3}}, {[]byte{0}, []byte{2}}, {[]byte{1}, []byte{4}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{5}}},
|
||||
},
|
||||
{ // no overlapping ranges
|
||||
[]*rangePerm{{[]byte{2}, []byte{3}}, {[]byte{0}, []byte{1}}, {[]byte{4}, []byte{7}}, {[]byte{8}, []byte{15}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{1}}, {[]byte{2}, []byte{3}}, {[]byte{4}, []byte{7}}, {[]byte{8}, []byte{15}}},
|
||||
{
|
||||
[]adt.Interval{adt.NewStringInterval("a", "c"), adt.NewStringInterval("x", "z")},
|
||||
"a", "z",
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("00", "03"), makePerm("18", "19"), makePerm("02", "08"), makePerm("10", "12")},
|
||||
[]*rangePerm{makePerm("00", "08"), makePerm("10", "12"), makePerm("18", "19")},
|
||||
[]adt.Interval{adt.NewStringInterval("a", "f"), adt.NewStringInterval("c", "d"), adt.NewStringInterval("f", "z")},
|
||||
"a", "z",
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "b")},
|
||||
[]*rangePerm{makePerm("a", "b")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("b", "")},
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("b", "")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("b", "c")},
|
||||
[]*rangePerm{makePerm("a", "c")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "c"), makePerm("b", "d")},
|
||||
[]*rangePerm{makePerm("a", "d")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("b", "c"), makePerm("d", "e")},
|
||||
[]*rangePerm{makePerm("a", "c"), makePerm("d", "e")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("c", "d"), makePerm("e", "f")},
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("c", "d"), makePerm("e", "f")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("e", "f"), makePerm("c", "d"), makePerm("a", "b")},
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("c", "d"), makePerm("e", "f")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("c", "d"), makePerm("a", "z")},
|
||||
[]*rangePerm{makePerm("a", "z")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("c", "d"), makePerm("a", "z"), makePerm("1", "9")},
|
||||
[]*rangePerm{makePerm("1", "9"), makePerm("a", "z")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("c", "d"), makePerm("a", "z"), makePerm("1", "a")},
|
||||
[]*rangePerm{makePerm("1", "z")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("a", "z"), makePerm("5", "6"), makePerm("1", "9")},
|
||||
[]*rangePerm{makePerm("1", "9"), makePerm("a", "z")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "b"), makePerm("b", "c"), makePerm("c", "d"), makePerm("b", "f"), makePerm("1", "9")},
|
||||
[]*rangePerm{makePerm("1", "9"), makePerm("a", "f")},
|
||||
},
|
||||
// overlapping
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "f"), makePerm("b", "g")},
|
||||
[]*rangePerm{makePerm("a", "g")},
|
||||
},
|
||||
// keys
|
||||
{
|
||||
[]*rangePerm{makePerm("a", ""), makePerm("b", "")},
|
||||
[]*rangePerm{makePerm("a", ""), makePerm("b", "")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", ""), makePerm("a", "c")},
|
||||
[]*rangePerm{makePerm("a", "c")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", ""), makePerm("a", "c"), makePerm("b", "")},
|
||||
[]*rangePerm{makePerm("a", "c")},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{makePerm("a", ""), makePerm("b", "c"), makePerm("b", ""), makePerm("c", ""), makePerm("d", "")},
|
||||
[]*rangePerm{makePerm("a", ""), makePerm("b", "c"), makePerm("c", ""), makePerm("d", "")},
|
||||
},
|
||||
// duplicate ranges
|
||||
{
|
||||
[]*rangePerm{makePerm("a", "f"), makePerm("a", "f")},
|
||||
[]*rangePerm{makePerm("a", "f")},
|
||||
},
|
||||
// duplicate keys
|
||||
{
|
||||
[]*rangePerm{makePerm("a", ""), makePerm("a", ""), makePerm("a", "")},
|
||||
[]*rangePerm{makePerm("a", "")},
|
||||
[]adt.Interval{adt.NewStringInterval("a", "d"), adt.NewStringInterval("a", "b"), adt.NewStringInterval("c", "f")},
|
||||
"a", "f",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
result := mergeRangePerms(tt.params)
|
||||
if !isPermsEqual(result, tt.want) {
|
||||
t.Errorf("#%d: result=%q, want=%q", i, sprintPerms(result), sprintPerms(tt.want))
|
||||
readPerms := &adt.IntervalTree{}
|
||||
for _, p := range tt.perms {
|
||||
readPerms.Insert(p, struct{}{})
|
||||
}
|
||||
|
||||
result := checkKeyInterval(&unifiedRangePermissions{readPerms: readPerms}, tt.begin, tt.end, authpb.READ)
|
||||
if result != tt.want {
|
||||
t.Errorf("#%d: result=%t, want=%t", i, result, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makePerm(a, b string) *rangePerm {
|
||||
return &rangePerm{begin: []byte(a), end: []byte(b)}
|
||||
}
|
||||
|
||||
func sprintPerms(rs []*rangePerm) (s string) {
|
||||
for _, rp := range rs {
|
||||
s += fmt.Sprintf("[%s %s] ", rp.begin, rp.end)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -747,7 +747,7 @@ func (as *authStore) isOpPermitted(userName string, revision uint64, key, rangeE
|
|||
return nil
|
||||
}
|
||||
|
||||
if as.isRangeOpPermitted(tx, userName, key, rangeEnd, permTyp) {
|
||||
if as.isRangeOpPermitted(tx, userName, string(key), string(rangeEnd), permTyp) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue