Benchmark label expressions (#27474)

* add benchmark

* performance improvements

* update RFD
This commit is contained in:
Nic Klaassen 2023-06-08 11:23:19 -07:00 committed by GitHub
parent a9e4284255
commit fb4826ed63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 260 additions and 66 deletions

View file

@ -1185,16 +1185,26 @@ func TestSessionRecordingConfigRBAC(t *testing.T) {
})
}
// time go test ./lib/auth -bench=. -run=^$ -v
// go test ./lib/auth -bench=. -run=^$ -v -benchtime 1x
// goos: darwin
// goarch: amd64
// pkg: github.com/gravitational/teleport/lib/auth
// cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
// BenchmarkListNodes
// BenchmarkListNodes-16 1 1000469673 ns/op 518721960 B/op 8344858 allocs/op
// BenchmarkListNodes/simple_labels
// BenchmarkListNodes/simple_labels-16 1 1079886286 ns/op 525128104 B/op 8831939 allocs/op
// BenchmarkListNodes/simple_expression
// BenchmarkListNodes/simple_expression-16 1 770118479 ns/op 432667432 B/op 6514790 allocs/op
// BenchmarkListNodes/labels
// BenchmarkListNodes/labels-16 1 1931843502 ns/op 741444360 B/op 15159333 allocs/op
// BenchmarkListNodes/expression
// BenchmarkListNodes/expression-16 1 1040855282 ns/op 509643128 B/op 8120970 allocs/op
// BenchmarkListNodes/complex_labels
// BenchmarkListNodes/complex_labels-16 1 2274376396 ns/op 792948904 B/op 17084107 allocs/op
// BenchmarkListNodes/complex_expression
// BenchmarkListNodes/complex_expression-16 1 1518800599 ns/op 738532920 B/op 12483748 allocs/op
// PASS
// ok github.com/gravitational/teleport/lib/auth 3.695s
// go test ./lib/auth -bench=. -run=^$ -v 19.02s user 3.87s system 244% cpu 9.376 total
// ok github.com/gravitational/teleport/lib/auth 11.679s
func BenchmarkListNodes(b *testing.B) {
const nodeCount = 50_000
const roleCount = 32
@ -1208,47 +1218,149 @@ func BenchmarkListNodes(b *testing.B) {
ctx := context.Background()
srv := newTestTLSServer(b)
var values []string
var ids []string
for i := 0; i < roleCount; i++ {
values = append(values, uuid.New().String())
ids = append(ids, uuid.New().String())
}
values[0] = "hidden"
ids[0] = "hidden"
var hiddenNodes int
// Create test nodes.
for i := 0; i < nodeCount; i++ {
name := uuid.New().String()
val := values[i%len(values)]
if val == "hidden" {
id := ids[i%len(ids)]
if id == "hidden" {
hiddenNodes++
}
node, err := types.NewServerWithLabels(
name,
types.KindNode,
types.ServerSpecV2{},
map[string]string{"key": val},
map[string]string{
"key": id,
"group": "users",
},
)
require.NoError(b, err)
_, err = srv.Auth().UpsertNode(ctx, node)
require.NoError(b, err)
}
testNodes, err := srv.Auth().GetNodes(ctx, defaults.Namespace)
require.NoError(b, err)
require.Len(b, testNodes, nodeCount)
var roles []types.Role
for _, val := range values {
role, err := types.NewRole(fmt.Sprintf("role-%s", val), types.RoleSpecV6{})
require.NoError(b, err)
for _, tc := range []struct {
desc string
editRole func(types.Role, string)
}{
{
desc: "simple labels",
editRole: func(r types.Role, id string) {
if id == "hidden" {
r.SetNodeLabels(types.Deny, types.Labels{"key": {id}})
} else {
r.SetNodeLabels(types.Allow, types.Labels{"key": {id}})
}
},
},
{
desc: "simple expression",
editRole: func(r types.Role, id string) {
if id == "hidden" {
err = r.SetLabelMatchers(types.Deny, types.KindNode, types.LabelMatchers{
Expression: `labels.key == "hidden"`,
})
require.NoError(b, err)
} else {
err := r.SetLabelMatchers(types.Allow, types.KindNode, types.LabelMatchers{
Expression: fmt.Sprintf(`labels.key == %q`, id),
})
require.NoError(b, err)
}
},
},
{
desc: "labels",
editRole: func(r types.Role, id string) {
r.SetNodeLabels(types.Allow, types.Labels{
"key": {id},
"group": {"{{external.group}}"},
})
r.SetNodeLabels(types.Deny, types.Labels{"key": {"hidden"}})
},
},
{
desc: "expression",
editRole: func(r types.Role, id string) {
err := r.SetLabelMatchers(types.Allow, types.KindNode, types.LabelMatchers{
Expression: fmt.Sprintf(`labels.key == %q && contains(user.spec.traits["group"], labels["group"])`,
id),
})
require.NoError(b, err)
err = r.SetLabelMatchers(types.Deny, types.KindNode, types.LabelMatchers{
Expression: `labels.key == "hidden"`,
})
require.NoError(b, err)
},
},
{
desc: "complex labels",
editRole: func(r types.Role, id string) {
r.SetNodeLabels(types.Allow, types.Labels{
"key": {"other", id, "another"},
"group": {
`{{regexp.replace(external.group, "^(.*)$", "$1")}}`,
"{{email.local(external.email)}}",
},
})
r.SetNodeLabels(types.Deny, types.Labels{"key": {"hidden"}})
},
},
{
desc: "complex expression",
editRole: func(r types.Role, id string) {
expr := fmt.Sprintf(
`(labels.key == "other" || labels.key == %q || labels.key == "another") &&
(contains(email.local(user.spec.traits["email"]), labels["group"]) ||
contains(regexp.replace(user.spec.traits["group"], "^(.*)$", "$1"), labels["group"]))`,
id)
err := r.SetLabelMatchers(types.Allow, types.KindNode, types.LabelMatchers{
Expression: expr,
})
require.NoError(b, err)
err = r.SetLabelMatchers(types.Deny, types.KindNode, types.LabelMatchers{
Expression: `labels.key == "hidden"`,
})
require.NoError(b, err)
},
},
} {
b.Run(tc.desc, func(b *testing.B) {
benchmarkListNodes(
b, ctx,
nodeCount, roleCount, hiddenNodes,
srv,
ids,
tc.editRole,
)
})
}
}
if val == "hidden" {
role.SetNodeLabels(types.Deny, types.Labels{"key": {val}})
} else {
role.SetNodeLabels(types.Allow, types.Labels{"key": {val}})
}
func benchmarkListNodes(
b *testing.B, ctx context.Context,
nodeCount, roleCount, hiddenNodes int,
srv *TestTLSServer,
ids []string,
editRole func(r types.Role, id string),
) {
var roles []types.Role
for _, id := range ids {
role, err := types.NewRole(fmt.Sprintf("role-%s", id), types.RoleSpecV6{})
require.NoError(b, err)
editRole(role, id)
roles = append(roles, role)
}
@ -1257,6 +1369,12 @@ func BenchmarkListNodes(b *testing.B) {
user, err := CreateUser(srv.Auth(), username, roles...)
require.NoError(b, err)
user.SetTraits(map[string][]string{
"group": {"users"},
"email": {"test@example.com"},
})
err = srv.Auth().UpsertUser(user)
require.NoError(b, err)
identity := TestUser(user.GetName())
clt, err := srv.NewClient(identity)
require.NoError(b, err)

View file

@ -515,7 +515,7 @@ func (a *accessChecker) CheckDatabaseRoles(database types.Database) (create bool
rolesMap := make(map[string]struct{})
var matched bool
for _, role := range autoCreateRoles {
match, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Traits, database)
match, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Traits, database, false)
if err != nil {
return false, nil, trace.Wrap(err)
}
@ -528,7 +528,7 @@ func (a *accessChecker) CheckDatabaseRoles(database types.Database) (create bool
matched = true
}
for _, role := range autoCreateRoles {
match, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Traits, database)
match, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Traits, database, false)
if err != nil {
return false, nil, trace.Wrap(err)
}
@ -735,7 +735,7 @@ func (a *accessChecker) CheckAccessToRemoteCluster(rc types.RemoteCluster) error
// the deny role set prohibits access.
var errs []error
for _, role := range a.RoleSet {
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Deny, role, a.info.Traits, rc)
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Deny, role, a.info.Traits, rc, isDebugEnabled)
if err != nil {
return trace.Wrap(err)
}
@ -749,7 +749,7 @@ func (a *accessChecker) CheckAccessToRemoteCluster(rc types.RemoteCluster) error
// Check allow rules: label has to match in any role in the role set to be granted access.
for _, role := range a.RoleSet {
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Allow, role, a.info.Traits, rc)
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Allow, role, a.info.Traits, rc, isDebugEnabled)
if err != nil {
return trace.Wrap(err)
}
@ -780,7 +780,7 @@ func (a *accessChecker) CheckAccessToRemoteCluster(rc types.RemoteCluster) error
func (a *accessChecker) DesktopGroups(s types.WindowsDesktop) ([]string, error) {
groups := make(map[string]struct{})
for _, role := range a.RoleSet {
result, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Traits, s)
result, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Traits, s, false)
if err != nil {
return nil, trace.Wrap(err)
}
@ -799,7 +799,7 @@ func (a *accessChecker) DesktopGroups(s types.WindowsDesktop) ([]string, error)
}
}
for _, role := range a.RoleSet {
result, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Traits, s)
result, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Traits, s, false)
if err != nil {
return nil, trace.Wrap(err)
}
@ -828,7 +828,7 @@ func (a *accessChecker) HostUsers(s types.Server) (*HostUsersInfo, error) {
seenSudoers := make(map[string]struct{})
for _, role := range roleSet {
result, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Traits, s)
result, _, err := checkRoleLabelsMatch(types.Allow, role, a.info.Traits, s, false)
if err != nil {
return nil, trace.Wrap(err)
}
@ -856,7 +856,7 @@ func (a *accessChecker) HostUsers(s types.Server) (*HostUsersInfo, error) {
var finalSudoers []string
for _, role := range roleSet {
result, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Traits, s)
result, _, err := checkRoleLabelsMatch(types.Deny, role, a.info.Traits, s, false)
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -2277,7 +2277,7 @@ func (l *kubernetesClusterLabelMatcher) Match(role types.Role, typ types.RoleCon
if err != nil {
return false, trace.Wrap(err)
}
ok, _, err := checkLabelsMatch(typ, labelMatchers, l.userTraits, mapLabelGetter(l.clusterLabels))
ok, _, err := checkLabelsMatch(typ, labelMatchers, l.userTraits, mapLabelGetter(l.clusterLabels), false)
return ok, trace.Wrap(err)
}
@ -2353,7 +2353,7 @@ func (set RoleSet) checkAccess(r AccessCheckable, traits wrappers.Traits, state
continue
}
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Deny, role, traits, r)
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Deny, role, traits, r, isDebugEnabled)
if err != nil {
return trace.Wrap(err)
}
@ -2397,7 +2397,7 @@ func (set RoleSet) checkAccess(r AccessCheckable, traits wrappers.Traits, state
continue
}
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Allow, role, traits, r)
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Allow, role, traits, r, isDebugEnabled)
if err != nil {
return trace.Wrap(err)
}
@ -2489,12 +2489,13 @@ func checkRoleLabelsMatch(
role types.Role,
userTraits wrappers.Traits,
resource AccessCheckable,
debug bool,
) (bool, string, error) {
labelMatchers, err := role.GetLabelMatchers(condition, resource.GetKind())
if err != nil {
return false, "", trace.Wrap(err)
}
return checkLabelsMatch(condition, labelMatchers, userTraits, resource)
return checkLabelsMatch(condition, labelMatchers, userTraits, resource, debug)
}
// checkLabelsMatch checks if the [labelMatchers] match the labels of [resource]
@ -2515,40 +2516,52 @@ func checkLabelsMatch(
labelMatchers types.LabelMatchers,
userTraits wrappers.Traits,
resource LabelGetter,
debug bool,
) (bool, string, error) {
if labelMatchers.Empty() {
return false, "no label matchers or label expression", nil
}
var matches []bool
var messages []string
var message string
labelsUnsetOrMatch, expressionUnsetOrMatch := true, true
if len(labelMatchers.Labels) > 0 {
match, message, err := MatchLabelGetter(labelMatchers.Labels, resource)
match, msg, err := MatchLabelGetter(labelMatchers.Labels, resource)
if err != nil {
return false, "", trace.Wrap(err)
}
matches = append(matches, match)
messages = append(messages, "label="+message)
if debug {
message += "label=" + msg
}
// Deny rules are greedy, if either matches, it's a match.
if condition == types.Deny && match {
return true, message, nil
}
labelsUnsetOrMatch = match
}
if len(labelMatchers.Expression) > 0 {
match, message, err := matchLabelExpression(labelMatchers.Expression, resource, userTraits)
match, msg, err := matchLabelExpression(labelMatchers.Expression, resource, userTraits)
if err != nil {
return false, "", trace.Wrap(err)
}
matches = append(matches, match)
messages = append(messages, "expression="+message)
if debug {
message = strings.Join([]string{message, "expression=" + msg}, ", ")
}
// Deny rules are greedy, if either matches, it's a match.
if condition == types.Deny {
return match, message, nil
}
expressionUnsetOrMatch = match
}
message := strings.Join(messages, ", ")
// Deny rules are greedy, if either matched, it's a match.
if condition == types.Deny {
return slices.Contains(matches, true), message, nil
// Either branch would have returned if it was a match.
return false, message, nil
}
// Allow rules are not greedy, both must match if they are set.
return !slices.Contains(matches, false), message, nil
return labelsUnsetOrMatch && expressionUnsetOrMatch, message, nil
}
func matchLabelExpression(labelExpression string, resource LabelGetter, userTraits wrappers.Traits) (bool, string, error) {
@ -2803,7 +2816,7 @@ func (set RoleSet) CheckAccessToRule(ctx RuleContext, namespace string, resource
// GetKubeResources returns allowed and denied list of Kubernetes Resources configured in the RoleSet.
func (set RoleSet) GetKubeResources(cluster types.KubeCluster, userTraits wrappers.Traits) (allowed, denied []types.KubernetesResource) {
for _, role := range set {
matchLabels, _, err := checkRoleLabelsMatch(types.Allow, role, userTraits, cluster)
matchLabels, _, err := checkRoleLabelsMatch(types.Allow, role, userTraits, cluster, false)
if err != nil || !matchLabels {
continue
}
@ -2811,7 +2824,7 @@ func (set RoleSet) GetKubeResources(cluster types.KubeCluster, userTraits wrappe
}
for _, role := range set {
matchLabels, _, err := checkRoleLabelsMatch(types.Deny, role, userTraits, cluster)
matchLabels, _, err := checkRoleLabelsMatch(types.Deny, role, userTraits, cluster, false)
if err != nil || !matchLabels {
continue
}

View file

@ -735,7 +735,7 @@ func (e ternaryVariadicFuncExpr[TEnv, TArg1, TArg2, TVarArgs, TResult]) Evaluate
type booleanOperator[TEnv, TArgs any] struct {
name string
f func(a, b TArgs) bool
f func(env TEnv, a, b Expression[TEnv, TArgs]) (bool, error)
}
func (b booleanOperator[TEnv, TArgs]) buildExpression(lhs, rhs any) (Expression[TEnv, bool], error) {
@ -752,47 +752,87 @@ func (b booleanOperator[TEnv, TArgs]) buildExpression(lhs, rhs any) (Expression[
type booleanOperatorExpr[TEnv, TArgs any] struct {
name string
f func(a, b TArgs) bool
f func(env TEnv, a, b Expression[TEnv, TArgs]) (bool, error)
lhsExpr, rhsExpr Expression[TEnv, TArgs]
}
func (b booleanOperatorExpr[TEnv, TArgs]) Evaluate(env TEnv) (bool, error) {
lhs, err := b.lhsExpr.Evaluate(env)
if err != nil {
return false, trace.Wrap(err, "evaluating lhs of (%s) operator", b.name)
}
rhs, err := b.rhsExpr.Evaluate(env)
if err != nil {
return false, trace.Wrap(err, "evaluating rhs of (%s) operator", b.name)
}
return b.f(lhs, rhs), nil
return b.f(env, b.lhsExpr, b.rhsExpr)
}
func and[TEnv any]() func(lhs, rhs any) (Expression[TEnv, bool], error) {
return booleanOperator[TEnv, bool]{
name: "&&",
f: func(lhs, rhs bool) bool { return lhs && rhs },
f: func(env TEnv, lhsExpr, rhsExpr Expression[TEnv, bool]) (bool, error) {
lhs, err := lhsExpr.Evaluate(env)
if err != nil {
return false, trace.Wrap(err, "evaluating lhs of (&&) operator")
}
// Short-circuit if possible.
if !lhs {
return false, nil
}
rhs, err := rhsExpr.Evaluate(env)
if err != nil {
return false, trace.Wrap(err, "evaluating rhs of (&&) operator")
}
return rhs, nil
},
}.buildExpression
}
func or[TEnv any]() func(lhs, rhs any) (Expression[TEnv, bool], error) {
return booleanOperator[TEnv, bool]{
name: "||",
f: func(lhs, rhs bool) bool { return lhs || rhs },
f: func(env TEnv, lhsExpr, rhsExpr Expression[TEnv, bool]) (bool, error) {
lhs, err := lhsExpr.Evaluate(env)
if err != nil {
return false, trace.Wrap(err, "evaluating lhs of (||) operator")
}
// Short-circuit if possible.
if lhs {
return true, nil
}
rhs, err := rhsExpr.Evaluate(env)
if err != nil {
return false, trace.Wrap(err, "evaluating rhs of (||) operator")
}
return rhs, nil
},
}.buildExpression
}
func eq[TEnv any]() func(lhs, rhs any) (Expression[TEnv, bool], error) {
return booleanOperator[TEnv, string]{
name: "==",
f: func(lhs, rhs string) bool { return lhs == rhs },
f: func(env TEnv, lhsExpr, rhsExpr Expression[TEnv, string]) (bool, error) {
lhs, err := lhsExpr.Evaluate(env)
if err != nil {
return false, trace.Wrap(err, "evaluating lhs of (==) operator")
}
rhs, err := rhsExpr.Evaluate(env)
if err != nil {
return false, trace.Wrap(err, "evaluating rhs of (==) operator")
}
return lhs == rhs, nil
},
}.buildExpression
}
func neq[TEnv any]() func(lhs, rhs any) (Expression[TEnv, bool], error) {
return booleanOperator[TEnv, string]{
name: "!=",
f: func(lhs, rhs string) bool { return lhs != rhs },
f: func(env TEnv, lhsExpr, rhsExpr Expression[TEnv, string]) (bool, error) {
lhs, err := lhsExpr.Evaluate(env)
if err != nil {
return false, trace.Wrap(err, "evaluating lhs of (!=) operator")
}
rhs, err := rhsExpr.Evaluate(env)
if err != nil {
return false, trace.Wrap(err, "evaluating rhs of (!=) operator")
}
return lhs != rhs, nil
},
}.buildExpression
}

View file

@ -1,6 +1,6 @@
---
authors: Nic Klaassen (nic@goteleport.com)
state: draft
state: implemented (v13.2.0)
---
# RFD 116 - RBAC Label Expressions
@ -235,10 +235,33 @@ Ultimately, performance will be benchmarked for multiple scenarios with the goal
of staying within 10% of the performance of the existing implementation.
Benchmarks will be written comparing similar RBAC constraints written with both
the existing label matchers and the new label expressions.
Benchmarks will run `ListResources` with 50k unique (simulated) nodes, 64 unique
roles, and 2k unique users.
Benchmarks will run `ListResources` with 50k unique (simulated) nodes and 32
unique roles.
Benchmark results: TBD
Benchmark results:
```
$ go test ./lib/auth -bench=. -run=^$ -v -benchtime 1x
goos: darwin
goarch: amd64
pkg: github.com/gravitational/teleport/lib/auth
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkListNodes
BenchmarkListNodes/simple_labels
BenchmarkListNodes/simple_labels-16 1 1079886286 ns/op 525128104 B/op 8831939 allocs/op
BenchmarkListNodes/simple_expression
BenchmarkListNodes/simple_expression-16 1 770118479 ns/op 432667432 B/op 6514790 allocs/op
BenchmarkListNodes/labels
BenchmarkListNodes/labels-16 1 1931843502 ns/op 741444360 B/op 15159333 allocs/op
BenchmarkListNodes/expression
BenchmarkListNodes/expression-16 1 1040855282 ns/op 509643128 B/op 8120970 allocs/op
BenchmarkListNodes/complex_labels
BenchmarkListNodes/complex_labels-16 1 2274376396 ns/op 792948904 B/op 17084107 allocs/op
BenchmarkListNodes/complex_expression
BenchmarkListNodes/complex_expression-16 1 1518800599 ns/op 738532920 B/op 12483748 allocs/op
PASS
ok github.com/gravitational/teleport/lib/auth 11.679s
```
#### Caching