mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-02 05:20:18 +00:00
247 lines
7.2 KiB
Go
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)])
|
|
}
|
|
}
|