Skip to content

Commit

Permalink
Change RBAC index alg
Browse files Browse the repository at this point in the history
  • Loading branch information
tjerman committed Feb 27, 2025
1 parent 743cdd8 commit 5132ef0
Show file tree
Hide file tree
Showing 16 changed files with 1,194 additions and 1,271 deletions.
8 changes: 7 additions & 1 deletion server/app/resources.cue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ resources: { [key=_]: {"handle": key, "component": "system", "platform": "cortez
identPlural: "rules"
expIdent: "Rule"

features: _allFeaturesDisabled
features: {
sorting: true
labels: false
paging: false
checkFn: false
}


model: {
Expand Down Expand Up @@ -59,6 +64,7 @@ resources: { [key=_]: {"handle": key, "component": "system", "platform": "cortez
}

byValue: ["resource", "operation", "role_id"]
rawFilter: true
}

store: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ func {{ .expIdent }}Filter(d drivers.Dialect, f {{ .goFilterType }})(ee []goqu.E
}
{{ end }}

{{ if .filter.rawFilter }}
if f.RawFilter != "" {
// @note this is only acceptable for system-generated queries.
// This should be improved regardless.
ee = append(ee, goqu.Literal(f.RawFilter))
}
{{- end }}

return ee, f, err
}
{{ end }}
Expand Down
3 changes: 3 additions & 0 deletions server/codegen/schema/resource.cue
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ import (

// filter resources by fields (eq)
"byValue": [...string]

// filter allows raw query construction
"rawFilter": bool | *false
}
// operations: #Operations

Expand Down
2 changes: 2 additions & 0 deletions server/codegen/server.store.cue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ _StoreResource: {
"byValue": [ for name in res.filter.byValue {res.filter.struct[name]}]
"byLabel": res.features.labels
"byFlag": res.features.flags

"rawFilter": res.filter.rawFilter
}

auxIdent: "aux\(expIdent)"
Expand Down
87 changes: 87 additions & 0 deletions server/pkg/rbac/mocks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package rbac

import (
"context"
"strings"
"time"

"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cast"
)

type (
mockRuleStore struct {
searchResponse []*Rule

searchResponses [][]*Rule
searchCount int

searches []RuleFilter
access Access
}
)

func (svc *mockRuleStore) SearchRbacRules(ctx context.Context, f RuleFilter) (out RuleSet, _ RuleFilter, err error) {
svc.searches = append(svc.searches, f)

if svc.searchResponses != nil {
out = svc.searchResponses[svc.searchCount]
svc.searchCount++
return
}

if svc.searchResponse != nil {
return svc.searchResponse, f, nil
}

if f.RawFilter != "" {
// (rel_role=%d and (%s))
combos := strings.Split(f.RawFilter, ") or (")

for _, c := range combos {
roleRules := strings.Split(c, " and (")

role := strings.Split(roleRules[0], "=")[1]

for _, rule := range strings.Split(roleRules[1], " or ") {
rs := strings.Split(rule, "resource=")[1]

out = append(out, &Rule{
RoleID: cast.ToUint64(role),
Resource: rs[1 : len(rs)-1],
Operation: "read",
Access: svc.access,
})
}
}
} else {
for _, r := range f.Resource {
out = append(out, &Rule{
RoleID: f.RoleID,
Resource: r,
Operation: f.Operation,
Access: svc.access,
})
}
}

// Give a slight delay to better simulate what we'd expect to see in real-world
time.Sleep(time.Millisecond * 5)
return
}

func (svc mockRuleStore) UpsertRbacRule(ctx context.Context, rr ...*Rule) (err error) {
return
}

func (svc mockRuleStore) DeleteRbacRule(ctx context.Context, rr ...*Rule) (err error) {
return
}

func (svc mockRuleStore) TruncateRbacRules(ctx context.Context) (err error) {
return
}

func (svc mockRuleStore) SearchRoles(ctx context.Context, f types.RoleFilter) (out types.RoleSet, _ types.RoleFilter, err error) {
return
}
141 changes: 22 additions & 119 deletions server/pkg/rbac/rule_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,13 @@ package rbac

import (
"sort"
"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 {
children map[uint64]*ruleIndexNode
}

ruleIndexNode struct {
children map[string]*ruleIndexNode
isLeaf bool
access Access
rule *Rule

count int
// children map[uint64]*ruleIndexNode
bits map[string]map[string]*Rule
}
)

Expand All @@ -36,45 +23,27 @@ func buildRuleIndex(rules []*Rule) (index *ruleIndex) {

// add adds a new Rule to the index
func (index *ruleIndex) add(rules ...*Rule) {
if index.children == nil {
index.children = make(map[uint64]*ruleIndexNode, len(rules)/2)
if index.bits == nil {
index.bits = make(map[string]map[string]*Rule, len(rules)/2)
}

for _, r := range rules {
if _, ok := index.children[r.RoleID]; !ok {
index.children[r.RoleID] = &ruleIndexNode{
children: make(map[string]*ruleIndexNode, 4),
}
}
index.children[r.RoleID].count++
n := index.children[r.RoleID]

bits := append([]string{r.Operation}, strings.Split(r.Resource, "/")...)
for _, b := range bits {
if _, ok := n.children[b]; !ok {
n.children[b] = &ruleIndexNode{
children: make(map[string]*ruleIndexNode, 4),
}
}
n.children[b].count++

n = n.children[b]
if _, ok := index.bits[r.Operation]; !ok {
index.bits[r.Operation] = make(map[string]*Rule, 4)
}

n.isLeaf = true
n.access = r.Access
n.rule = r
index.bits[r.Operation][r.Resource] = r
}
}

// has checks if the rule is already in there
func (t *ruleIndex) has(r *Rule) bool {
return len(t.collect(true, r.RoleID, r.Operation, r.Resource)) > 0
return len(t.collect(true, r.Operation, r.Resource)) > 0
}

// get returns the matching rules
func (t *ruleIndex) get(role uint64, op, res string) (out []*Rule) {
return t.collect(false, role, op, res)
func (t *ruleIndex) get(op, res string) (out []*Rule) {
return t.collect(false, op, res)
}

// get returns all RBAC rules matching these constraints
Expand All @@ -83,97 +52,31 @@ func (t *ruleIndex) get(role uint64, op, res string) (out []*Rule) {
// the operation + 1 for the role.
//
// Our longest bit will be 6 so this is essentially constant time.
func (t *ruleIndex) collect(exact bool, role uint64, op, res string) (out []*Rule) {
if t.children == nil {
return
}

if _, ok := t.children[role]; !ok {
return
}

// An edge case implied by the test suite
if op == "" && res == "" {
if t.children[role].children[""] == nil || t.children[role].children[""].children[""] == nil {
return
}

out = append(out, t.children[role].children[""].children[""].rule)
return
}

// Pull out the nodes for the role
aux, ok := t.children[role]
if !ok {
return
}

aux, ok = aux.children[op]
if !ok {
func (t *ruleIndex) collect(exact bool, op, res string) (out []*Rule) {
if t.bits[op] == nil {
return
}

return aux.get(exact, res, 0)
}

// get returns all of the rules matching these constraints
//
// Under the hood...
// We're avoiding string processing (concatenation, splitting, ...) as that can
// be a memory hog in scenarios where we're pounding this function.
//
// The from denotes the substring we've not yet processed.
func (n *ruleIndexNode) get(exact bool, res string, from int) (out []*Rule) {
if n == nil || n.children == nil {
return
}

// If we've reached the leaf node but haven't yet processed the entire resource,
// we've reached an invalid scenario since we can't go any deeper
to := len(res)
if n.isLeaf && from < to {
return
}

// Once from passes to, we've processed the entire resource
if from >= to {
if n.isLeaf {
out = append(out, n.rule)
return
for _, res := range permuteResource(res) {
aux := t.bits[op][res]
if aux == nil {
continue
}
}

// Get the next / delimiter.
// Clamp the index to the length of the resource.
// Adjust the index to account the from (the start index of the remaining resource)
nextDelim := strings.Index(res[from:to], "/")
if nextDelim < 0 {
nextDelim = len(res)
} else {
nextDelim += from
}

// Get RBAC rules down the actual path
pathBit := res[from:nextDelim]
if n.children[pathBit] != nil {
out = append(out, n.children[pathBit].get(exact, res, nextDelim+1)...)
}

// Get RBAC rules down the wildcard path
if !exact && n.children[wildcard] != nil {
out = append(out, n.children[wildcard].get(exact, res, nextDelim+1)...)
out = append(out, t.bits[op][res])
}

return
return out
}

// empty returns true if the index is empty
func (t *ruleIndex) empty() bool {
return t == nil || t.children == nil || len(t.children) == 0
return t == nil || t.bits == nil || len(t.bits) == 0
}

func (t *ruleIndex) matchingRule(role uint64, op, res string) (out *Rule) {
set := RuleSet(t.get(role, op, res))
// matchingRule returns the first matching rule for the role, op, res
func (t *ruleIndex) matchingRule(op, res string) (out *Rule) {
set := RuleSet(t.get(op, res))
sort.Sort(set)

for _, s := range set {
Expand Down
Loading

0 comments on commit 5132ef0

Please sign in to comment.