diff --git a/server/pkg/rbac/resource.go b/server/pkg/rbac/resource.go index cbe191d79..d115000c2 100644 --- a/server/pkg/rbac/resource.go +++ b/server/pkg/rbac/resource.go @@ -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 diff --git a/server/pkg/rbac/resource_test.go b/server/pkg/rbac/resource_test.go index a288f370c..81cc2b065 100644 --- a/server/pkg/rbac/resource_test.go +++ b/server/pkg/rbac/resource_test.go @@ -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 { diff --git a/server/pkg/rbac/rule.go b/server/pkg/rbac/rule.go index 565179bf1..b30b99497 100644 --- a/server/pkg/rbac/rule.go +++ b/server/pkg/rbac/rule.go @@ -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 { diff --git a/server/pkg/rbac/rule_index.go b/server/pkg/rbac/rule_index.go new file mode 100644 index 000000000..659df2331 --- /dev/null +++ b/server/pkg/rbac/rule_index.go @@ -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 +} diff --git a/server/pkg/rbac/rule_index_test.go b/server/pkg/rbac/rule_index_test.go new file mode 100644 index 000000000..eb72082f4 --- /dev/null +++ b/server/pkg/rbac/rule_index_test.go @@ -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)) +} diff --git a/server/pkg/rbac/ruleset_checks.go b/server/pkg/rbac/ruleset_checks.go index 360d13569..ba5554d1e 100644 --- a/server/pkg/rbac/ruleset_checks.go +++ b/server/pkg/rbac/ruleset_checks.go @@ -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] { diff --git a/server/pkg/rbac/ruleset_checks_test.go b/server/pkg/rbac/ruleset_checks_test.go index 944bb4a8b..f46962b08 100644 --- a/server/pkg/rbac/ruleset_checks_test.go +++ b/server/pkg/rbac/ruleset_checks_test.go @@ -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()) - }) - } -} diff --git a/server/pkg/rbac/service.go b/server/pkg/rbac/service.go index 40537e170..8d58c83e9 100644 --- a/server/pkg/rbac/service.go +++ b/server/pkg/rbac/service.go @@ -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 ( diff --git a/server/pkg/rbac/service_test.go b/server/pkg/rbac/service_test.go new file mode 100644 index 000000000..bc26af29e --- /dev/null +++ b/server/pkg/rbac/service_test.go @@ -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) +}