Rework RBAC rule indexing to improve raw lookup performance
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"github.com/spf13/cast"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -94,7 +95,7 @@ func matchResource(matcher, resource string) (m bool) {
|
||||
}
|
||||
|
||||
func hasWildcards(resource string) bool {
|
||||
return strings.Index(resource, wildcard) != -1
|
||||
return strings.Contains(resource, wildcard)
|
||||
}
|
||||
|
||||
// returns level for the given resource match
|
||||
|
||||
@@ -72,29 +72,6 @@ func TestResourceMatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
//cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
||||
//Benchmark_MatchResource100-16 6527383 183.2 ns/op
|
||||
//Benchmark_MatchResource1000-16 6335626 183.5 ns/op
|
||||
//Benchmark_MatchResource10000-16 6565214 183.5 ns/op
|
||||
//Benchmark_MatchResource100000-16 6541002 183.7 ns/op
|
||||
//Benchmark_MatchResource1000000-16 6542052 183.5 ns/op
|
||||
func benchmarkMatchResource(b *testing.B, c int) {
|
||||
b.StartTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
matchResource("corteza::test/a/1/1/1", "corteza::test/a/1/1/1")
|
||||
matchResource("corteza::test/a/*/*/1", "corteza::test/a/1/1/1")
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
func Benchmark_MatchResource100(b *testing.B) { benchmarkMatchResource(b, 100) }
|
||||
func Benchmark_MatchResource1000(b *testing.B) { benchmarkMatchResource(b, 1000) }
|
||||
func Benchmark_MatchResource10000(b *testing.B) { benchmarkMatchResource(b, 10000) }
|
||||
func Benchmark_MatchResource100000(b *testing.B) { benchmarkMatchResource(b, 100000) }
|
||||
func Benchmark_MatchResource1000000(b *testing.B) { benchmarkMatchResource(b, 1000000) }
|
||||
|
||||
func TestLevel(t *testing.T) {
|
||||
var (
|
||||
tcc = []struct {
|
||||
|
||||
@@ -20,32 +20,12 @@ type (
|
||||
}
|
||||
|
||||
RuleSet []*Rule
|
||||
|
||||
// OptRuleSet RBAC rule index (operation / role ID / rules)
|
||||
OptRuleSet map[string]map[uint64]RuleSet
|
||||
)
|
||||
|
||||
func (r Rule) String() string {
|
||||
return fmt.Sprintf("%s %d to %s on %s", r.Access, r.RoleID, r.Operation, r.Resource)
|
||||
}
|
||||
|
||||
func indexRules(rules []*Rule) OptRuleSet {
|
||||
i := make(OptRuleSet)
|
||||
for _, r := range rules {
|
||||
if i[r.Operation] == nil {
|
||||
i[r.Operation] = make(map[uint64]RuleSet)
|
||||
}
|
||||
|
||||
if i[r.Operation][r.RoleID] == nil {
|
||||
i[r.Operation][r.RoleID] = RuleSet{}
|
||||
}
|
||||
|
||||
i[r.Operation][r.RoleID] = append(i[r.Operation][r.RoleID], r)
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func (set RuleSet) Len() int { return len(set) }
|
||||
func (set RuleSet) Swap(i, j int) { set[i], set[j] = set[j], set[i] }
|
||||
func (set RuleSet) Less(i, j int) bool {
|
||||
|
||||
124
server/pkg/rbac/rule_index.go
Normal file
124
server/pkg/rbac/rule_index.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
// ruleIndex indexes all given RBAC rules to optimize lookup times
|
||||
//
|
||||
// The algorithm is based on the standard trie structure.
|
||||
// The max depth for a check operation is M+2 where M is the number of
|
||||
// RBAC resource path elements + component + some meta.
|
||||
ruleIndex struct {
|
||||
root *ruleIndexNode
|
||||
}
|
||||
|
||||
ruleIndexNode struct {
|
||||
children map[string]*ruleIndexNode
|
||||
|
||||
isLeaf bool
|
||||
access Access
|
||||
rule *Rule
|
||||
}
|
||||
)
|
||||
|
||||
func mkInitial(op string, roleID uint64) (out string) {
|
||||
return op + "-" + strconv.FormatUint(roleID, 10)
|
||||
}
|
||||
|
||||
func explodeThing(r string) (out []string) {
|
||||
return strings.Split(r, "/")
|
||||
}
|
||||
|
||||
// buildRuleIndex indexes the given rules for optimal lookups
|
||||
func buildRuleIndex(rules []*Rule) (ix *ruleIndex) {
|
||||
ix = &ruleIndex{}
|
||||
|
||||
for _, r := range rules {
|
||||
n := ix.root
|
||||
if n == nil {
|
||||
n = &ruleIndexNode{children: make(map[string]*ruleIndexNode, 2)}
|
||||
ix.root = n
|
||||
}
|
||||
|
||||
bits := append([]string{mkInitial(r.Operation, r.RoleID)}, explodeThing(r.Resource)...)
|
||||
for _, b := range bits {
|
||||
if _, ok := n.children[b]; !ok {
|
||||
n.children[b] = &ruleIndexNode{
|
||||
children: make(map[string]*ruleIndexNode, 4),
|
||||
}
|
||||
}
|
||||
|
||||
n = n.children[b]
|
||||
}
|
||||
|
||||
n.isLeaf = true
|
||||
n.access = r.Access
|
||||
n.rule = r
|
||||
}
|
||||
|
||||
return ix
|
||||
}
|
||||
|
||||
func (t *ruleIndex) matchingRule(role uint64, op, res string) (out *Rule) {
|
||||
set := RuleSet(t.get(role, op, res))
|
||||
sort.Sort(set)
|
||||
|
||||
for _, s := range set {
|
||||
if s.Access == Inherit {
|
||||
continue
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// get returns all rules matching the given params
|
||||
func (t *ruleIndex) get(role uint64, op, res string) (out []*Rule) {
|
||||
if t.root == nil {
|
||||
return
|
||||
}
|
||||
|
||||
bits := append([]string{mkInitial(op, role)}, explodeThing(res)...)
|
||||
return t.root.get(bits)
|
||||
}
|
||||
|
||||
func (n *ruleIndexNode) get(bits []string) (out []*Rule) {
|
||||
if n == nil || n.children == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if n.isLeaf && len(bits) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(bits) == 0 {
|
||||
if n.isLeaf {
|
||||
out = append(out, n.rule)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
b := bits[0]
|
||||
bits = bits[1:]
|
||||
|
||||
if n.children[b] != nil {
|
||||
out = append(out, n.children[b].get(bits)...)
|
||||
}
|
||||
|
||||
if n.children[wildcard] != nil {
|
||||
out = append(out, n.children[wildcard].get(bits)...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// empty returns true if the index is empty
|
||||
func (t *ruleIndex) empty() bool {
|
||||
return t == nil || t.root == nil
|
||||
}
|
||||
180
server/pkg/rbac/rule_index_test.go
Normal file
180
server/pkg/rbac/rule_index_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
tcc := []struct {
|
||||
name string
|
||||
in []*Rule
|
||||
out []int
|
||||
|
||||
role uint64
|
||||
op string
|
||||
res string
|
||||
}{{
|
||||
name: "empty",
|
||||
in: nil,
|
||||
out: nil,
|
||||
|
||||
role: 1,
|
||||
op: "read",
|
||||
res: "a:b/c/d",
|
||||
}, {
|
||||
name: "match",
|
||||
in: []*Rule{{
|
||||
RoleID: 1,
|
||||
Resource: "a:b/c/d",
|
||||
Operation: "read",
|
||||
Access: Allow,
|
||||
}},
|
||||
out: []int{0},
|
||||
|
||||
role: 1,
|
||||
op: "read",
|
||||
res: "a:b/c/d",
|
||||
}, {
|
||||
name: "multiple matches",
|
||||
in: []*Rule{{
|
||||
RoleID: 1,
|
||||
Resource: "a:b/c/d",
|
||||
Operation: "read",
|
||||
Access: Allow,
|
||||
}, {
|
||||
RoleID: 1,
|
||||
Resource: "a:b/*/*",
|
||||
Operation: "read",
|
||||
Access: Inherit,
|
||||
}},
|
||||
out: []int{0, 1},
|
||||
|
||||
role: 1,
|
||||
op: "read",
|
||||
res: "a:b/c/d",
|
||||
}, {
|
||||
name: "one match one role missmatch",
|
||||
in: []*Rule{{
|
||||
RoleID: 2,
|
||||
Resource: "a:b/c/d",
|
||||
Operation: "read",
|
||||
Access: Allow,
|
||||
}, {
|
||||
RoleID: 1,
|
||||
Resource: "a:b/*/*",
|
||||
Operation: "read",
|
||||
Access: Inherit,
|
||||
}},
|
||||
out: []int{1},
|
||||
|
||||
role: 1,
|
||||
op: "read",
|
||||
res: "a:b/c/d",
|
||||
}, {
|
||||
name: "role missmatch",
|
||||
in: []*Rule{{
|
||||
RoleID: 2,
|
||||
Resource: "a:b/c/d",
|
||||
Operation: "read",
|
||||
Access: Allow,
|
||||
}, {
|
||||
RoleID: 3,
|
||||
Resource: "a:b/*/*",
|
||||
Operation: "read",
|
||||
Access: Inherit,
|
||||
}},
|
||||
out: nil,
|
||||
|
||||
role: 1,
|
||||
op: "read",
|
||||
res: "a:b/c/d",
|
||||
}, {
|
||||
name: "path missmatch",
|
||||
in: []*Rule{{
|
||||
RoleID: 1,
|
||||
Resource: "a:b/c/e",
|
||||
Operation: "read",
|
||||
Access: Allow,
|
||||
}},
|
||||
out: nil,
|
||||
|
||||
role: 1,
|
||||
op: "read",
|
||||
res: "a:b/c/d",
|
||||
}, {
|
||||
name: "operation missmatch",
|
||||
in: []*Rule{{
|
||||
RoleID: 1,
|
||||
Resource: "a:b/c/d",
|
||||
Operation: "write",
|
||||
Access: Allow,
|
||||
}},
|
||||
out: nil,
|
||||
|
||||
role: 1,
|
||||
op: "read",
|
||||
res: "a:b/c/d",
|
||||
}}
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ix := buildRuleIndex(tc.in)
|
||||
out := RuleSet(ix.get(tc.role, tc.op, tc.res))
|
||||
sort.Sort(out)
|
||||
|
||||
want := RuleSet(graby(tc.in, tc.out))
|
||||
sort.Sort(want)
|
||||
|
||||
require.Len(t, out, len(want))
|
||||
for i, o := range out {
|
||||
require.Equal(t, want[i], o)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func graby(rr []*Rule, want []int) (out []*Rule) {
|
||||
out = make([]*Rule, 0, len(want))
|
||||
|
||||
for _, w := range want {
|
||||
out = append(out, rr[w])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/cortezaproject/corteza/server/pkg/rbac
|
||||
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
|
||||
// BenchmarkIndexBuild_100-12 10000 102361 ns/op 88064 B/op 1271 allocs/op
|
||||
// BenchmarkIndexBuild_1000-12 1149 1024872 ns/op 755375 B/op 11183 allocs/op
|
||||
// BenchmarkIndexBuild_10000-12 128 8986248 ns/op 4406477 B/op 82453 allocs/op
|
||||
// BenchmarkIndexBuild_100000-12 14 81871407 ns/op 20627785 B/op 543568 allocs/op
|
||||
func benchmarkIndexBuild(b *testing.B, rules []*Rule) {
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
buildRuleIndex(rules)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIndexBuild_100(b *testing.B) {
|
||||
benchmarkIndexBuild(b, makeRuleSet(100, 10))
|
||||
}
|
||||
|
||||
func BenchmarkIndexBuild_1000(b *testing.B) {
|
||||
benchmarkIndexBuild(b, makeRuleSet(1000, 10))
|
||||
}
|
||||
|
||||
func BenchmarkIndexBuild_10000(b *testing.B) {
|
||||
benchmarkIndexBuild(b, makeRuleSet(10000, 10))
|
||||
}
|
||||
|
||||
func BenchmarkIndexBuild_100000(b *testing.B) {
|
||||
benchmarkIndexBuild(b, makeRuleSet(100000, 10))
|
||||
}
|
||||
@@ -1,24 +1,19 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// function checks all given rules
|
||||
//
|
||||
// - indexRules are rules optimized for quick lookup rules ar grouped in 2 levels:
|
||||
// by operation, and role, containing slice of rules (for specific operation and role)
|
||||
// at the deepest level
|
||||
// - indexRules are rules optimized for quick lookup.
|
||||
// The trie data structure approach is used to optimize lookups and reduce
|
||||
// memory consumption.
|
||||
//
|
||||
// - rolesByKind are roles optimized for quick lookup
|
||||
// roles are grouped by kind and each kind contains fast-lookup (map[role-id]bool)
|
||||
// - rolesByKind are roles optimized for quick lookup
|
||||
// roles are grouped by kind and each kind contains fast-lookup (map[role-id]bool)
|
||||
//
|
||||
// - op and res represent operation and resource that are checked
|
||||
// - op and res represent operation and resource that are checked
|
||||
//
|
||||
// - trace is optional; when not nil, function will update trace struct
|
||||
// with information as it traverses and checks the rules
|
||||
//
|
||||
func check(indexedRules OptRuleSet, rolesByKind partRoles, op, res string, trace *Trace) Access {
|
||||
// - trace is optional; when not nil, function will update trace struct
|
||||
// with information as it traverses and checks the rules
|
||||
func check(indexedRules *ruleIndex, rolesByKind partRoles, op, res string, trace *Trace) Access {
|
||||
baseTraceInfo(trace, res, op, rolesByKind)
|
||||
|
||||
if member(rolesByKind, AnonymousRole) && len(rolesByKind) > 1 {
|
||||
@@ -32,7 +27,7 @@ func check(indexedRules OptRuleSet, rolesByKind partRoles, op, res string, trace
|
||||
return resolve(trace, Allow, bypassRoleMembership)
|
||||
}
|
||||
|
||||
if len(indexedRules) == 0 {
|
||||
if indexedRules.empty() {
|
||||
// no rules to check
|
||||
return resolve(trace, Inherit, noRules)
|
||||
}
|
||||
@@ -71,18 +66,11 @@ func check(indexedRules OptRuleSet, rolesByKind partRoles, op, res string, trace
|
||||
// for each role kind
|
||||
allowed = false
|
||||
|
||||
for roleID, rr := range indexedRules[op] {
|
||||
if !rolesByKind[kind][roleID] {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(rr) == 0 {
|
||||
// no rules found
|
||||
continue
|
||||
}
|
||||
for r := range rolesByKind[kind] {
|
||||
match = indexedRules.matchingRule(r, op, res)
|
||||
|
||||
// check all rules for each role the security-context
|
||||
if match = findRuleByResOp(rr, op, res); match == nil {
|
||||
if match == nil {
|
||||
// no rules match
|
||||
continue
|
||||
}
|
||||
@@ -116,29 +104,6 @@ func check(indexedRules OptRuleSet, rolesByKind partRoles, op, res string, trace
|
||||
return resolve(trace, Inherit, noMatch)
|
||||
}
|
||||
|
||||
// Check given resource match and operation on all given rules
|
||||
func findRuleByResOp(set RuleSet, op, res string) *Rule {
|
||||
// Make sure rules are always sorted (by level)
|
||||
// to avoid any kind of unstable behaviour
|
||||
sort.Sort(set)
|
||||
|
||||
for _, r := range set {
|
||||
if !matchResource(r.Resource, res) {
|
||||
continue
|
||||
}
|
||||
|
||||
if op != r.Operation {
|
||||
continue
|
||||
}
|
||||
|
||||
if r.Access != Inherit {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// at least one of the roles must be set to true
|
||||
func member(r partRoles, k roleKind) bool {
|
||||
for _, is := range r[k] {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -104,7 +102,7 @@ func Test_check(t *testing.T) {
|
||||
|
||||
for _, c := range cc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
require.Equal(t, c.exp.String(), check(indexRules(c.set), partitionRoles(c.rr...), c.op, c.res, nil).String())
|
||||
require.Equal(t, c.exp.String(), check(buildRuleIndex(c.set), partitionRoles(c.rr...), c.op, c.res, nil).String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -232,93 +230,9 @@ func Test_checkWithTrace(t *testing.T) {
|
||||
for _, c := range cc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
trace = new(Trace)
|
||||
check(indexRules(c.set), partitionRoles(c.rr...), "op-trace", c.res, trace)
|
||||
check(buildRuleIndex(c.set), partitionRoles(c.rr...), "op-trace", c.res, trace)
|
||||
require.Equal(t, c.trace, trace)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
||||
//Benchmark_Check100-16 12395438 95.24 ns/op
|
||||
//Benchmark_Check1000-16 12507883 96.34 ns/op
|
||||
//Benchmark_Check10000-16 11788594 96.85 ns/op
|
||||
//Benchmark_Check100000-16 11679951 100.1 ns/op
|
||||
//Benchmark_Check1000000-16 4670353 287.3 ns/op
|
||||
func benchmarkCheck(b *testing.B, c int) {
|
||||
var (
|
||||
// resting with 50 roles
|
||||
rules = make(RuleSet, 0, c)
|
||||
|
||||
pr = partitionRoles(
|
||||
&Role{id: 1, kind: CommonRole},
|
||||
&Role{id: 2, kind: CommonRole},
|
||||
&Role{id: 3, kind: CommonRole},
|
||||
&Role{id: 4, kind: ContextRole},
|
||||
&Role{id: 5, kind: ContextRole},
|
||||
&Role{id: 6, kind: AuthenticatedRole},
|
||||
)
|
||||
)
|
||||
|
||||
for i := 0; i < cap(rules); i++ {
|
||||
rules = append(rules, &Rule{
|
||||
RoleID: uint64(rand.Int31n(50)),
|
||||
Resource: fmt.Sprintf("res-%d", rand.Int31n(1000)),
|
||||
Operation: fmt.Sprintf("op-%d", rand.Int31n(100)),
|
||||
Access: Access(rand.Int31n(2)),
|
||||
})
|
||||
}
|
||||
|
||||
iRules := indexRules(rules)
|
||||
|
||||
b.StartTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
check(iRules, pr, "res-0", "op-0", nil)
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
func Benchmark_Check100(b *testing.B) { benchmarkCheck(b, 100) }
|
||||
func Benchmark_Check1000(b *testing.B) { benchmarkCheck(b, 1000) }
|
||||
func Benchmark_Check10000(b *testing.B) { benchmarkCheck(b, 10000) }
|
||||
func Benchmark_Check100000(b *testing.B) { benchmarkCheck(b, 100000) }
|
||||
func Benchmark_Check1000000(b *testing.B) { benchmarkCheck(b, 1000000) }
|
||||
|
||||
func Test_checkRulesByResource(t *testing.T) {
|
||||
var (
|
||||
cc = []struct {
|
||||
exp Access
|
||||
res string
|
||||
op string
|
||||
set []*Rule
|
||||
}{
|
||||
{Inherit, "", "", nil},
|
||||
{Inherit, "res", "op", nil},
|
||||
{Allow, "res/1", "op", []*Rule{
|
||||
{Resource: "---", Operation: "--", Access: Deny},
|
||||
{Resource: "res/1", Operation: "op", Access: Allow},
|
||||
}},
|
||||
{Allow, "res/2", "op", []*Rule{
|
||||
{Resource: "res/*", Operation: "op", Access: Allow},
|
||||
}},
|
||||
{Allow, "res/3", "op", []*Rule{
|
||||
{Resource: "res/3", Operation: "op", Access: Allow},
|
||||
{Resource: "res/*", Operation: "op", Access: Deny},
|
||||
}},
|
||||
}
|
||||
)
|
||||
|
||||
for _, c := range cc {
|
||||
t.Run(c.res, func(t *testing.T) {
|
||||
a := findRuleByResOp(c.set, c.op, c.res)
|
||||
|
||||
if a == nil {
|
||||
a = InheritRule(0, "", "")
|
||||
}
|
||||
|
||||
require.Equal(t, c.exp.String(), a.Access.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,10 @@ package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza/server/pkg/sentry"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -22,7 +19,7 @@ type (
|
||||
f chan bool
|
||||
|
||||
rules RuleSet
|
||||
indexed OptRuleSet
|
||||
indexed *ruleIndex
|
||||
|
||||
roles []*Role
|
||||
|
||||
@@ -107,15 +104,6 @@ func (svc *service) Check(ses Session, op string, res Resource) (a Access) {
|
||||
|
||||
a = check(svc.indexed, fRoles, op, res.RbacResource(), nil)
|
||||
|
||||
svc.logger.Debug(a.String()+" "+op+" for "+res.RbacResource(),
|
||||
append(
|
||||
fRoles.LogFields(),
|
||||
logger.Uint64("identity", ses.Identity()),
|
||||
zap.Any("indexed", len(svc.indexed)),
|
||||
zap.Any("rules", len(svc.rules)),
|
||||
)...,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -187,7 +175,7 @@ func (svc *service) Grant(ctx context.Context, rules ...*Rule) (err error) {
|
||||
|
||||
func (svc *service) grant(rules ...*Rule) {
|
||||
svc.rules = merge(svc.rules, rules...)
|
||||
svc.indexed = indexRules(svc.rules)
|
||||
svc.indexed = buildRuleIndex(svc.rules)
|
||||
}
|
||||
|
||||
// Watch reloads RBAC rules in intervals and on request
|
||||
@@ -239,7 +227,7 @@ func (svc *service) Clear() {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
svc.rules = RuleSet{}
|
||||
svc.indexed = OptRuleSet{}
|
||||
svc.indexed = &ruleIndex{}
|
||||
}
|
||||
|
||||
func (svc *service) reloadRules(ctx context.Context) {
|
||||
@@ -252,11 +240,15 @@ func (svc *service) reloadRules(ctx context.Context) {
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
svc.rules = rr
|
||||
svc.indexed = indexRules(rr)
|
||||
svc.setRules(rr)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *service) setRules(rr RuleSet) {
|
||||
svc.rules = rr
|
||||
svc.indexed = buildRuleIndex(rr)
|
||||
}
|
||||
|
||||
// UpdateRoles updates RBAC roles
|
||||
//
|
||||
// Warning: this REPLACES all existing roles that are recognized by RBAC subsystem
|
||||
@@ -319,36 +311,6 @@ func (svc *service) SignificantRoles(res Resource, op string) (aRR, dRR []uint64
|
||||
return svc.rules.sigRoles(res.RbacResource(), op)
|
||||
}
|
||||
|
||||
func (svc *service) String() (out string) {
|
||||
tpl := "%-5v %-20s to %-20s %-30s\n"
|
||||
out += strings.Repeat("-", 120) + "\n"
|
||||
|
||||
role := func(id uint64) string {
|
||||
for _, r := range svc.roles {
|
||||
if r.id == id {
|
||||
if r.handle != "" {
|
||||
return fmt.Sprintf("%s [%d]", r.handle, r.kind)
|
||||
}
|
||||
return fmt.Sprintf("%d [%d]", id, r.kind)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d [?]", id)
|
||||
}
|
||||
|
||||
for _, byRole := range svc.indexed {
|
||||
for _, rr := range byRole {
|
||||
for _, r := range rr {
|
||||
out += fmt.Sprintf(tpl, r.Access, r.Operation, role(r.RoleID), r.Resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out += strings.Repeat("-", 120) + "\n"
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CloneRulesByRoleID clone all rules of a Role S to a specific Role T by removing its existing rules
|
||||
func (svc *service) CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) (err error) {
|
||||
var (
|
||||
|
||||
248
server/pkg/rbac/service_test.go
Normal file
248
server/pkg/rbac/service_test.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
matchBenchCfg struct {
|
||||
rules RuleSet
|
||||
roles []*Role
|
||||
res Resource
|
||||
op string
|
||||
}
|
||||
)
|
||||
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/cortezaproject/corteza/server/pkg/rbac
|
||||
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
|
||||
// Benchmark_AccessCheck_role5_rule500-12 378988 3026 ns/op 615 B/op 16 allocs/op
|
||||
// Benchmark_AccessCheck_role5_rule1000-12 253071 4087 ns/op 615 B/op 16 allocs/op
|
||||
// Benchmark_AccessCheck_role10_rule10000-12 237085 5429 ns/op 1026 B/op 29 allocs/op
|
||||
// Benchmark_AccessCheck_role20_rule50000-12 128914 9344 ns/op 2335 B/op 71 allocs/op
|
||||
// Benchmark_AccessCheck_role30_rule100000-12 79963 20670 ns/op 3371 B/op 85 allocs/op
|
||||
// Benchmark_AccessCheck_role100_rule500000-12 16927 79106 ns/op 12796 B/op 391 allocs/op
|
||||
func benchmark_AccessCheck(b *testing.B, cfg matchBenchCfg) {
|
||||
svc := NewService(zap.NewNop(), nil)
|
||||
svc.UpdateRoles(cfg.roles...)
|
||||
svc.setRules(cfg.rules)
|
||||
|
||||
ctx := context.Background()
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
svc.Can(session{
|
||||
id: 90001,
|
||||
rr: yankRandRoles(cfg.roles),
|
||||
ctx: ctx,
|
||||
}, cfg.op, cfg.res)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_AccessCheck_role5_rule500(b *testing.B) {
|
||||
roles := 5
|
||||
rules := 500
|
||||
benchmark_AccessCheck(b, matchBenchCfg{
|
||||
res: makeResource(),
|
||||
op: randomOperation(),
|
||||
rules: makeRuleSet(rules, roles),
|
||||
roles: makeRoleSet(roles),
|
||||
})
|
||||
}
|
||||
|
||||
func Benchmark_AccessCheck_role5_rule1000(b *testing.B) {
|
||||
roles := 5
|
||||
rules := 1000
|
||||
benchmark_AccessCheck(b, matchBenchCfg{
|
||||
res: makeResource(),
|
||||
op: randomOperation(),
|
||||
rules: makeRuleSet(rules, roles),
|
||||
roles: makeRoleSet(roles),
|
||||
})
|
||||
}
|
||||
|
||||
func Benchmark_AccessCheck_role10_rule10000(b *testing.B) {
|
||||
roles := 10
|
||||
rules := 10000
|
||||
benchmark_AccessCheck(b, matchBenchCfg{
|
||||
res: makeResource(),
|
||||
op: randomOperation(),
|
||||
rules: makeRuleSet(rules, roles),
|
||||
roles: makeRoleSet(roles),
|
||||
})
|
||||
}
|
||||
|
||||
func Benchmark_AccessCheck_role20_rule50000(b *testing.B) {
|
||||
roles := 20
|
||||
rules := 50000
|
||||
benchmark_AccessCheck(b, matchBenchCfg{
|
||||
res: makeResource(),
|
||||
op: randomOperation(),
|
||||
rules: makeRuleSet(rules, roles),
|
||||
roles: makeRoleSet(roles),
|
||||
})
|
||||
}
|
||||
|
||||
func Benchmark_AccessCheck_role30_rule100000(b *testing.B) {
|
||||
roles := 30
|
||||
rules := 100000
|
||||
benchmark_AccessCheck(b, matchBenchCfg{
|
||||
res: makeResource(),
|
||||
op: randomOperation(),
|
||||
rules: makeRuleSet(rules, roles),
|
||||
roles: makeRoleSet(roles),
|
||||
})
|
||||
}
|
||||
|
||||
func Benchmark_AccessCheck_role100_rule500000(b *testing.B) {
|
||||
roles := 100
|
||||
rules := 500000
|
||||
benchmark_AccessCheck(b, matchBenchCfg{
|
||||
res: makeResource(),
|
||||
op: randomOperation(),
|
||||
rules: makeRuleSet(rules, roles),
|
||||
roles: makeRoleSet(roles),
|
||||
})
|
||||
}
|
||||
|
||||
func yankRandRoles(base []*Role) (out []uint64) {
|
||||
count := rand.Intn(len(base))
|
||||
if count <= 0 {
|
||||
count = len(base) / 2
|
||||
}
|
||||
|
||||
out = make([]uint64, count)
|
||||
for i := 0; i < count; i++ {
|
||||
out[i] = base[i].id
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeRoleSet(count int) (out []*Role) {
|
||||
for i := 0; i < count; i++ {
|
||||
out = append(out, makeRole(uint64(1000+i), fmt.Sprintf("rl_%d", 1000+i)))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeRole(id uint64, handle string) *Role {
|
||||
rx := rand.Float64()
|
||||
|
||||
if rx <= 1 {
|
||||
return CommonRole.Make(id, handle)
|
||||
}
|
||||
|
||||
return makeContextualRole(id, handle)
|
||||
}
|
||||
|
||||
func makeContextualRole(id uint64, handle string) *Role {
|
||||
rx := rand.Float64()
|
||||
|
||||
if rx < 0.7 {
|
||||
return makeContextualRolePassing(id, handle)
|
||||
}
|
||||
return makeContextualRoleFailing(id, handle)
|
||||
}
|
||||
|
||||
func makeContextualRolePassing(id uint64, handle string) *Role {
|
||||
p := expr.NewParser()
|
||||
eval, err := p.Parse("true == true && true == true && 1 <= 1")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
check := func(scope map[string]interface{}) bool {
|
||||
vars, err := expr.NewVars(scope)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
test, err := eval.Test(ctx, vars)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return test
|
||||
}
|
||||
|
||||
return MakeContextRole(id, handle, check, "corteza::compose:record")
|
||||
}
|
||||
|
||||
func makeContextualRoleFailing(id uint64, handle string) *Role {
|
||||
p := expr.NewParser()
|
||||
eval, err := p.Parse("false == false || false == false || 1 > 1")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
check := func(scope map[string]interface{}) bool {
|
||||
vars, err := expr.NewVars(scope)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
test, err := eval.Test(ctx, vars)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return test
|
||||
}
|
||||
|
||||
return MakeContextRole(id, handle, check, "corteza::compose:record")
|
||||
}
|
||||
|
||||
func makeResource() (out Resource) {
|
||||
return resource(randomResource())
|
||||
}
|
||||
|
||||
func makeRuleSet(count int, roleCount int) (out RuleSet) {
|
||||
for i := 0; i < count; i++ {
|
||||
out = append(out, &Rule{
|
||||
RoleID: uint64(1000 + int(rand.Intn(roleCount))),
|
||||
Resource: randomResource(),
|
||||
Operation: randomOperation(),
|
||||
Access: randomAccess(),
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func randomAccess() (out Access) {
|
||||
x := rand.Float64()
|
||||
if x < 0.7 {
|
||||
return Allow
|
||||
}
|
||||
return Inherit
|
||||
}
|
||||
|
||||
func randomOperation() (out string) {
|
||||
ops := []string{"read", "write", "delete"}
|
||||
return ops[rand.Intn(len(ops))]
|
||||
}
|
||||
|
||||
func randomResource() (out string) {
|
||||
return fmt.Sprintf("%s:%s/%s/%s", RandStringRunes(1), RandStringRunes(1), RandStringRunes(1), RandStringRunes(1))
|
||||
}
|
||||
|
||||
var letterRunes = []rune("abcdefg")
|
||||
|
||||
func RandStringRunes(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user