Pulse/pkg/auth/sqlite_manager_bench_test.go
2026-03-18 16:06:30 +00:00

247 lines
7.2 KiB
Go

package auth
import (
"context"
"fmt"
"testing"
)
// newBenchManager creates an ephemeral SQLiteManager pre-populated with
// built-in roles. Caller must close via b.Cleanup.
func newBenchManager(b *testing.B) *SQLiteManager {
b.Helper()
dir := b.TempDir()
m, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: dir})
if err != nil {
b.Fatalf("NewSQLiteManager: %v", err)
}
b.Cleanup(func() { m.Close() })
return m
}
// seedBenchUsers creates numUsers users, each assigned to one of the
// built-in roles in a round-robin pattern (admin, operator, viewer, auditor).
func seedBenchUsers(b *testing.B, m *SQLiteManager, numUsers int) []string {
b.Helper()
roles := []string{RoleAdmin, RoleOperator, RoleViewer, RoleAuditor}
users := make([]string, numUsers)
for i := 0; i < numUsers; i++ {
username := fmt.Sprintf("bench-user-%04d", i)
users[i] = username
roleID := roles[i%len(roles)]
if err := m.AssignRole(username, roleID); err != nil {
b.Fatalf("AssignRole(%s, %s): %v", username, roleID, err)
}
}
return users
}
// seedBenchCustomRoles creates numRoles custom roles, each with numPerms
// permissions. Returns the role IDs.
func seedBenchCustomRoles(b *testing.B, m *SQLiteManager, numRoles, numPerms int) []string {
b.Helper()
actions := []string{"read", "write", "delete"}
resources := []string{"nodes", "vms", "containers", "alerts", "settings", "ai", "discovery", "audit_logs"}
ids := make([]string, numRoles)
for i := 0; i < numRoles; i++ {
id := fmt.Sprintf("custom-role-%04d", i)
ids[i] = id
perms := make([]Permission, numPerms)
for j := 0; j < numPerms; j++ {
perms[j] = Permission{
Action: actions[j%len(actions)],
Resource: resources[j%len(resources)],
Effect: EffectAllow,
}
}
if err := m.SaveRole(Role{
ID: id,
Name: fmt.Sprintf("Custom Role %d", i),
Description: "Benchmark custom role",
Permissions: perms,
}); err != nil {
b.Fatalf("SaveRole(%s): %v", id, err)
}
}
return ids
}
// BenchmarkGetUserPermissions measures the hot path of resolving a user's
// effective permissions from SQLite. This is called on every permissioned
// API request via the PolicyEvaluator → SQLiteManager chain.
func BenchmarkGetUserPermissions(b *testing.B) {
for _, numUsers := range []int{10, 50, 200} {
b.Run(fmt.Sprintf("users=%d", numUsers), func(b *testing.B) {
m := newBenchManager(b)
users := seedBenchUsers(b, m, numUsers)
// Verify permissions are returned (guard against silent regressions).
perms := m.GetUserPermissions(users[0])
if len(perms) == 0 {
b.Fatalf("expected non-empty permissions for %s", users[0])
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = m.GetUserPermissions(users[i%len(users)])
}
})
}
}
// BenchmarkGetRole measures single role lookup by ID — called during
// permission resolution and role inheritance walks.
func BenchmarkGetRole(b *testing.B) {
m := newBenchManager(b)
// Create 10 custom roles to add variety beyond built-ins.
customIDs := seedBenchCustomRoles(b, m, 10, 6)
allRoleIDs := append([]string{RoleAdmin, RoleOperator, RoleViewer, RoleAuditor}, customIDs...)
// Verify every role is found (guard against silent regressions).
for _, id := range allRoleIDs {
if _, ok := m.GetRole(id); !ok {
b.Fatalf("role %q not found", id)
}
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, ok := m.GetRole(allRoleIDs[i%len(allRoleIDs)]); !ok {
b.Fatal("unexpected miss in timed loop")
}
}
}
// BenchmarkGetUserAssignment measures user-to-role assignment lookup — a
// prerequisite for every permission resolution.
func BenchmarkGetUserAssignment(b *testing.B) {
for _, numUsers := range []int{10, 100} {
b.Run(fmt.Sprintf("users=%d", numUsers), func(b *testing.B) {
m := newBenchManager(b)
users := seedBenchUsers(b, m, numUsers)
// Verify assignments exist (guard against silent regressions).
if _, ok := m.GetUserAssignment(users[0]); !ok {
b.Fatalf("expected assignment for %s", users[0])
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, ok := m.GetUserAssignment(users[i%len(users)]); !ok {
b.Fatal("unexpected miss in timed loop")
}
}
})
}
}
// BenchmarkAuthorize measures the full RBAC authorization chain:
// RBACAuthorizer → PolicyEvaluator → SQLiteManager. This is the complete
// per-request hot path when RBAC is active. Contexts are prebuilt outside
// the timed loop to isolate auth-chain cost from context allocation.
func BenchmarkAuthorize(b *testing.B) {
m := newBenchManager(b)
users := seedBenchUsers(b, m, 50)
authorizer := NewRBACAuthorizer(m)
// Prebuild contexts and request descriptors.
type authReq struct {
ctx context.Context
action string
resource string
}
requests := make([]authReq, len(users))
for i, u := range users {
requests[i] = authReq{
ctx: WithUser(context.Background(), u),
action: ActionRead,
resource: ResourceNodes,
}
}
// Verify the authorization path works.
allowed, err := authorizer.Authorize(requests[0].ctx, ActionRead, ResourceNodes)
if err != nil {
b.Fatalf("Authorize error: %v", err)
}
// users[0] is assigned to admin (round-robin), admin has action=admin resource=*
if !allowed {
b.Fatal("expected user-0 (admin) to be allowed")
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := requests[i%len(requests)]
authorizer.Authorize(req.ctx, req.action, req.resource)
}
}
// BenchmarkGetRolesWithInheritance measures the inheritance-aware role
// resolution path. This exercises the recursive parent-chain walk that
// occurs when the ExtendedManager interface is detected by PolicyEvaluator.
func BenchmarkGetRolesWithInheritance(b *testing.B) {
m := newBenchManager(b)
// Create a 3-level role hierarchy: base → mid → leaf.
if err := m.SaveRole(Role{
ID: "bench-base",
Name: "Base",
Description: "Base role",
Permissions: []Permission{
{Action: "read", Resource: "nodes"},
{Action: "read", Resource: "vms"},
},
}); err != nil {
b.Fatalf("SaveRole(base): %v", err)
}
if err := m.SaveRole(Role{
ID: "bench-mid",
Name: "Mid",
Description: "Mid role",
ParentID: "bench-base",
Permissions: []Permission{
{Action: "write", Resource: "nodes"},
},
}); err != nil {
b.Fatalf("SaveRole(mid): %v", err)
}
if err := m.SaveRole(Role{
ID: "bench-leaf",
Name: "Leaf",
Description: "Leaf role",
ParentID: "bench-mid",
Permissions: []Permission{
{Action: "write", Resource: "alerts"},
},
}); err != nil {
b.Fatalf("SaveRole(leaf): %v", err)
}
// Assign users to the leaf role (triggers 3-level inheritance walk).
const numUsers = 20
users := make([]string, numUsers)
for i := 0; i < numUsers; i++ {
username := fmt.Sprintf("inherit-user-%04d", i)
users[i] = username
if err := m.AssignRole(username, "bench-leaf"); err != nil {
b.Fatalf("AssignRole: %v", err)
}
}
// Verify inheritance resolves 3 roles.
roles := m.GetRolesWithInheritance(users[0])
if len(roles) != 3 {
b.Fatalf("expected 3 inherited roles, got %d", len(roles))
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
m.GetRolesWithInheritance(users[i%len(users)])
}
}