3
0

Rework RBAC rule indexing to improve raw lookup performance

This commit is contained in:
Tomaž Jerman
2023-12-17 13:18:32 +01:00
parent be7543e6d5
commit 5fdfd7f107
9 changed files with 579 additions and 228 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View 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
}

View 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))
}

View File

@@ -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] {

View File

@@ -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())
})
}
}

View File

@@ -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 (

View 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)
}