Files
Pulse/pkg/auth/policy_evaluator_test.go
rcourtman 0ddbf37c59 feat(auth): add policy evaluator and SQLite auth manager for RBAC
- Add policy evaluator for fine-grained access control
- Implement SQLite-backed auth manager for user/role persistence
- Support role-based permissions evaluation
2026-01-12 15:20:49 +00:00

436 lines
11 KiB
Go

package auth
import (
"context"
"os"
"testing"
)
func TestPolicyEvaluator(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "policy-eval-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
m, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
if err != nil {
t.Fatalf("Failed to create SQLiteManager: %v", err)
}
defer m.Close()
evaluator := NewPolicyEvaluator(m)
// Setup test data
setupTestRoles(t, m)
t.Run("Allow permission works", func(t *testing.T) {
ctx := WithUser(context.Background(), "allow-user")
m.UpdateUserRoles("allow-user", []string{"allow-role"})
allowed, err := evaluator.Authorize(ctx, "read", "nodes")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !allowed {
t.Error("Expected access to be allowed")
}
})
t.Run("No matching permission denies", func(t *testing.T) {
ctx := WithUser(context.Background(), "allow-user")
allowed, err := evaluator.Authorize(ctx, "delete", "nodes")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if allowed {
t.Error("Expected access to be denied (no matching permission)")
}
})
t.Run("Deny takes precedence over allow", func(t *testing.T) {
ctx := WithUser(context.Background(), "deny-user")
m.UpdateUserRoles("deny-user", []string{"deny-role"})
// deny-role has allow on nodes:* but deny on nodes:production
allowed, err := evaluator.Authorize(ctx, "write", "nodes:test")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !allowed {
t.Error("Expected write to nodes:test to be allowed")
}
allowed, err = evaluator.Authorize(ctx, "write", "nodes:production")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if allowed {
t.Error("Expected write to nodes:production to be denied")
}
})
t.Run("Multiple roles combined", func(t *testing.T) {
ctx := WithUser(context.Background(), "multi-user")
m.UpdateUserRoles("multi-user", []string{"allow-role", "extra-role"})
// allow-role grants read:nodes, extra-role grants write:alerts
allowed, err := evaluator.Authorize(ctx, "read", "nodes")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !allowed {
t.Error("Expected read nodes allowed from allow-role")
}
allowed, err = evaluator.Authorize(ctx, "write", "alerts")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !allowed {
t.Error("Expected write alerts allowed from extra-role")
}
})
t.Run("No user in context denies", func(t *testing.T) {
ctx := context.Background() // No user
allowed, err := evaluator.Authorize(ctx, "read", "nodes")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if allowed {
t.Error("Expected access denied with no user in context")
}
})
t.Run("User with no roles denies", func(t *testing.T) {
ctx := WithUser(context.Background(), "no-role-user")
// Don't assign any roles
allowed, err := evaluator.Authorize(ctx, "read", "nodes")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if allowed {
t.Error("Expected access denied with no roles")
}
})
}
func TestPolicyEvaluatorWithAttributes(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "policy-abac-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
m, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
if err != nil {
t.Fatalf("Failed to create SQLiteManager: %v", err)
}
defer m.Close()
evaluator := NewPolicyEvaluator(m)
// Create role with conditions
condRole := Role{
ID: "cond-role",
Name: "Conditional Role",
Permissions: []Permission{
{
Action: "read",
Resource: "nodes:*",
Effect: EffectAllow,
Conditions: map[string]string{"env": "test"},
},
{
Action: "write",
Resource: "nodes:*",
Effect: EffectAllow,
Conditions: map[string]string{"owner": "${user}"},
},
},
}
m.SaveRole(condRole)
m.UpdateUserRoles("cond-user", []string{"cond-role"})
t.Run("Condition matches", func(t *testing.T) {
ctx := WithUser(context.Background(), "cond-user")
allowed, err := evaluator.AuthorizeWithAttributes(ctx, "read", "nodes:dev", map[string]string{"env": "test"})
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !allowed {
t.Error("Expected access allowed when condition matches")
}
})
t.Run("Condition does not match", func(t *testing.T) {
ctx := WithUser(context.Background(), "cond-user")
allowed, err := evaluator.AuthorizeWithAttributes(ctx, "read", "nodes:dev", map[string]string{"env": "prod"})
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if allowed {
t.Error("Expected access denied when condition doesn't match")
}
})
t.Run("Variable substitution works", func(t *testing.T) {
ctx := WithUser(context.Background(), "cond-user")
// ${user} should be replaced with "cond-user"
allowed, err := evaluator.AuthorizeWithAttributes(ctx, "write", "nodes:dev", map[string]string{"owner": "cond-user"})
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !allowed {
t.Error("Expected access allowed when ${user} matches current user")
}
// Different owner should fail
allowed, err = evaluator.AuthorizeWithAttributes(ctx, "write", "nodes:dev", map[string]string{"owner": "other-user"})
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if allowed {
t.Error("Expected access denied when owner doesn't match current user")
}
})
t.Run("Missing attribute denies", func(t *testing.T) {
ctx := WithUser(context.Background(), "cond-user")
// No env attribute provided
allowed, err := evaluator.AuthorizeWithAttributes(ctx, "read", "nodes:dev", nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if allowed {
t.Error("Expected access denied when required attribute is missing")
}
})
}
func TestPolicyEvaluatorWithInheritance(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "policy-inherit-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
m, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
if err != nil {
t.Fatalf("Failed to create SQLiteManager: %v", err)
}
defer m.Close()
evaluator := NewPolicyEvaluator(m)
// Create parent role
parentRole := Role{
ID: "parent",
Name: "Parent",
Permissions: []Permission{{Action: "read", Resource: "base"}},
}
m.SaveRole(parentRole)
// Create child role
childRole := Role{
ID: "child",
Name: "Child",
ParentID: "parent",
Permissions: []Permission{{Action: "write", Resource: "child"}},
}
m.SaveRole(childRole)
m.UpdateUserRoles("inherit-user", []string{"child"})
t.Run("Inherited permission works", func(t *testing.T) {
ctx := WithUser(context.Background(), "inherit-user")
// Should have access via inheritance
allowed, err := evaluator.Authorize(ctx, "read", "base")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !allowed {
t.Error("Expected inherited permission to grant access")
}
})
t.Run("Own permission works", func(t *testing.T) {
ctx := WithUser(context.Background(), "inherit-user")
allowed, err := evaluator.Authorize(ctx, "write", "child")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !allowed {
t.Error("Expected own permission to grant access")
}
})
}
func TestRBACAuthorizer(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "rbac-auth-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
m, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
if err != nil {
t.Fatalf("Failed to create SQLiteManager: %v", err)
}
defer m.Close()
authorizer := NewRBACAuthorizer(m)
// Setup
setupTestRoles(t, m)
m.UpdateUserRoles("normal-user", []string{"allow-role"})
t.Run("Normal user authorization", func(t *testing.T) {
ctx := WithUser(context.Background(), "normal-user")
allowed, err := authorizer.Authorize(ctx, "read", "nodes")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !allowed {
t.Error("Expected normal user to have read access")
}
})
t.Run("Admin bypass", func(t *testing.T) {
authorizer.SetAdminUser("superadmin")
ctx := WithUser(context.Background(), "superadmin")
// Admin should have access even without roles
allowed, err := authorizer.Authorize(ctx, "delete", "everything")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !allowed {
t.Error("Expected admin user to have full access")
}
})
t.Run("Non-admin still checks permissions", func(t *testing.T) {
ctx := WithUser(context.Background(), "normal-user")
// normal-user doesn't have delete permission
allowed, err := authorizer.Authorize(ctx, "delete", "nodes")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if allowed {
t.Error("Expected non-admin to be denied")
}
})
}
func TestMatchesResource(t *testing.T) {
tests := []struct {
pattern string
resource string
expected bool
}{
{"*", "anything", true},
{"*", "nodes", true},
{"nodes", "nodes", true},
{"nodes", "alerts", false},
{"nodes:pve1", "nodes:pve1", true},
{"nodes:pve1", "nodes:pve2", false},
{"nodes:*", "nodes:pve1", true},
{"nodes:*", "nodes:pve2", true},
{"nodes:*", "nodes", true},
{"nodes:*", "alerts", false},
{"settings:*", "settings:admin", true},
{"settings:*", "settings", true},
{"admin:*", "admin:users", true},
}
for _, tt := range tests {
t.Run(tt.pattern+"_"+tt.resource, func(t *testing.T) {
result := MatchesResource(tt.pattern, tt.resource)
if result != tt.expected {
t.Errorf("MatchesResource(%q, %q) = %v, expected %v", tt.pattern, tt.resource, result, tt.expected)
}
})
}
}
func TestMatchesAction(t *testing.T) {
tests := []struct {
permAction string
requestedAction string
expected bool
}{
{"admin", "read", true},
{"admin", "write", true},
{"admin", "delete", true},
{"admin", "admin", true},
{"read", "read", true},
{"read", "write", false},
{"write", "write", true},
{"write", "read", false},
{"delete", "delete", true},
{"delete", "read", false},
}
for _, tt := range tests {
t.Run(tt.permAction+"_"+tt.requestedAction, func(t *testing.T) {
result := MatchesAction(tt.permAction, tt.requestedAction)
if result != tt.expected {
t.Errorf("MatchesAction(%q, %q) = %v, expected %v", tt.permAction, tt.requestedAction, result, tt.expected)
}
})
}
}
// Helper function to setup test roles
func setupTestRoles(t *testing.T, m *SQLiteManager) {
// Allow role - basic read access
allowRole := Role{
ID: "allow-role",
Name: "Allow Role",
Permissions: []Permission{
{Action: "read", Resource: "nodes", Effect: EffectAllow},
},
}
if err := m.SaveRole(allowRole); err != nil {
t.Fatalf("Failed to create allow-role: %v", err)
}
// Deny role - allow with specific deny
denyRole := Role{
ID: "deny-role",
Name: "Deny Role",
Permissions: []Permission{
{Action: "write", Resource: "nodes:*", Effect: EffectAllow},
{Action: "write", Resource: "nodes:production", Effect: EffectDeny},
},
}
if err := m.SaveRole(denyRole); err != nil {
t.Fatalf("Failed to create deny-role: %v", err)
}
// Extra role for multi-role tests
extraRole := Role{
ID: "extra-role",
Name: "Extra Role",
Permissions: []Permission{
{Action: "write", Resource: "alerts", Effect: EffectAllow},
},
}
if err := m.SaveRole(extraRole); err != nil {
t.Fatalf("Failed to create extra-role: %v", err)
}
}