Files
Pulse/pkg/auth/sqlite_manager_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

580 lines
16 KiB
Go

package auth
import (
"os"
"path/filepath"
"testing"
"time"
)
func TestSQLiteManager(t *testing.T) {
// Create temp directory for tests
tmpDir, err := os.MkdirTemp("", "rbac-sqlite-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()
t.Run("Built-in roles exist", func(t *testing.T) {
roles := m.GetRoles()
if len(roles) < 4 {
t.Errorf("Expected at least 4 built-in roles, got %d", len(roles))
}
// Check admin role exists
admin, ok := m.GetRole(RoleAdmin)
if !ok {
t.Error("Admin role not found")
}
if !admin.IsBuiltIn {
t.Error("Admin role should be built-in")
}
})
t.Run("Cannot delete built-in role", func(t *testing.T) {
err := m.DeleteRole(RoleAdmin)
if err == nil {
t.Error("Expected error when deleting built-in role")
}
})
t.Run("Cannot modify built-in role", func(t *testing.T) {
admin, _ := m.GetRole(RoleAdmin)
admin.Name = "Modified Admin"
err := m.SaveRole(admin)
if err == nil {
t.Error("Expected error when modifying built-in role")
}
})
t.Run("Create custom role with conditions", func(t *testing.T) {
customRole := Role{
ID: "custom-abac",
Name: "Custom ABAC Role",
Description: "A custom role with ABAC conditions",
Permissions: []Permission{
{Action: "read", Resource: "nodes", Effect: EffectAllow},
{Action: "write", Resource: "nodes:production", Effect: EffectDeny},
{
Action: "read",
Resource: "nodes:*",
Effect: EffectAllow,
Conditions: map[string]string{"tag": "test", "owner": "${user}"},
},
},
}
if err := m.SaveRole(customRole); err != nil {
t.Errorf("Failed to save custom role: %v", err)
}
retrieved, ok := m.GetRole("custom-abac")
if !ok {
t.Error("Custom role not found after save")
}
if retrieved.Name != "Custom ABAC Role" {
t.Errorf("Expected name 'Custom ABAC Role', got '%s'", retrieved.Name)
}
if len(retrieved.Permissions) != 3 {
t.Errorf("Expected 3 permissions, got %d", len(retrieved.Permissions))
}
// Check permission with conditions
var foundCondPerm bool
for _, p := range retrieved.Permissions {
if p.Conditions != nil && p.Conditions["tag"] == "test" {
foundCondPerm = true
if p.Conditions["owner"] != "${user}" {
t.Errorf("Expected owner condition '${user}', got '%s'", p.Conditions["owner"])
}
}
}
if !foundCondPerm {
t.Error("Permission with conditions not found")
}
})
t.Run("Create role with inheritance", func(t *testing.T) {
// First create parent role
parentRole := Role{
ID: "parent-role",
Name: "Parent Role",
Description: "A parent role",
Permissions: []Permission{
{Action: "read", Resource: "settings"},
},
}
if err := m.SaveRole(parentRole); err != nil {
t.Errorf("Failed to save parent role: %v", err)
}
// Create child role
childRole := Role{
ID: "child-role",
Name: "Child Role",
Description: "A child role inheriting from parent",
ParentID: "parent-role",
Permissions: []Permission{
{Action: "write", Resource: "settings"},
},
}
if err := m.SaveRole(childRole); err != nil {
t.Errorf("Failed to save child role: %v", err)
}
// Get child with inheritance
role, effectivePerms, ok := m.GetRoleWithInheritance("child-role")
if !ok {
t.Error("Child role not found")
}
if role.ParentID != "parent-role" {
t.Errorf("Expected parent ID 'parent-role', got '%s'", role.ParentID)
}
// Should have both own permissions and inherited permissions
if len(effectivePerms) < 2 {
t.Errorf("Expected at least 2 effective permissions, got %d", len(effectivePerms))
}
var hasRead, hasWrite bool
for _, p := range effectivePerms {
if p.Action == "read" && p.Resource == "settings" {
hasRead = true
}
if p.Action == "write" && p.Resource == "settings" {
hasWrite = true
}
}
if !hasRead {
t.Error("Missing inherited read permission")
}
if !hasWrite {
t.Error("Missing own write permission")
}
})
t.Run("Assign roles to user", func(t *testing.T) {
if err := m.AssignRole("testuser", RoleViewer); err != nil {
t.Errorf("Failed to assign role: %v", err)
}
if err := m.AssignRole("testuser", "child-role"); err != nil {
t.Errorf("Failed to assign child role: %v", err)
}
assignment, ok := m.GetUserAssignment("testuser")
if !ok {
t.Error("User assignment not found")
}
if len(assignment.RoleIDs) != 2 {
t.Errorf("Expected 2 roles, got %d", len(assignment.RoleIDs))
}
})
t.Run("Get roles with inheritance for user", func(t *testing.T) {
roles := m.GetRolesWithInheritance("testuser")
// Should include viewer, child-role, and parent-role (inherited)
if len(roles) < 3 {
t.Errorf("Expected at least 3 roles with inheritance, got %d", len(roles))
}
var hasParent bool
for _, r := range roles {
if r.ID == "parent-role" {
hasParent = true
}
}
if !hasParent {
t.Error("Parent role should be included via inheritance")
}
})
t.Run("Get user permissions", func(t *testing.T) {
perms := m.GetUserPermissions("testuser")
if len(perms) == 0 {
t.Error("Expected permissions for user")
}
})
t.Run("Delete custom role", func(t *testing.T) {
if err := m.DeleteRole("custom-abac"); err != nil {
t.Errorf("Failed to delete custom role: %v", err)
}
_, ok := m.GetRole("custom-abac")
if ok {
t.Error("Custom role should not exist after delete")
}
})
t.Run("Changelog is recorded", func(t *testing.T) {
logs := m.GetChangeLogs(100, 0)
if len(logs) == 0 {
t.Error("Expected changelog entries")
}
// Should have entries for role creation
var hasRoleCreated bool
for _, l := range logs {
if l.Action == ActionRoleCreated {
hasRoleCreated = true
}
}
if !hasRoleCreated {
t.Error("Missing role_created changelog entry")
}
})
t.Run("Get changelog for entity", func(t *testing.T) {
logs := m.GetChangeLogsForEntity("role", "parent-role")
if len(logs) == 0 {
t.Error("Expected changelog entries for parent-role")
}
})
}
func TestSQLiteManagerMigration(t *testing.T) {
// Create temp directory for tests
tmpDir, err := os.MkdirTemp("", "rbac-migration-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// First create file-based manager with some data
fileManager, err := NewFileManager(tmpDir)
if err != nil {
t.Fatalf("Failed to create FileManager: %v", err)
}
// Add custom role and assignment
customRole := Role{
ID: "migrate-test",
Name: "Migration Test Role",
Description: "Should be migrated",
Permissions: []Permission{{Action: "read", Resource: "alerts"}},
}
if err := fileManager.SaveRole(customRole); err != nil {
t.Fatalf("Failed to save role in FileManager: %v", err)
}
if err := fileManager.AssignRole("migrateuser", "migrate-test"); err != nil {
t.Fatalf("Failed to assign role in FileManager: %v", err)
}
// Now create SQLite manager with migration enabled
sqliteManager, err := NewSQLiteManager(SQLiteManagerConfig{
DataDir: tmpDir,
MigrateFromFiles: true,
})
if err != nil {
t.Fatalf("Failed to create SQLiteManager: %v", err)
}
defer sqliteManager.Close()
// Check custom role was migrated
t.Run("Custom role migrated", func(t *testing.T) {
role, ok := sqliteManager.GetRole("migrate-test")
if !ok {
t.Error("Custom role should be migrated to SQLite")
}
if role.Name != "Migration Test Role" {
t.Errorf("Expected 'Migration Test Role', got '%s'", role.Name)
}
})
// Check assignment was migrated
t.Run("User assignment migrated", func(t *testing.T) {
assignment, ok := sqliteManager.GetUserAssignment("migrateuser")
if !ok {
t.Error("User assignment should be migrated to SQLite")
}
hasRole := false
for _, rid := range assignment.RoleIDs {
if rid == "migrate-test" {
hasRole = true
}
}
if !hasRole {
t.Error("User should have migrated role")
}
})
// Check backup files created
t.Run("Backup files created", func(t *testing.T) {
rolesBackup := filepath.Join(tmpDir, "rbac_roles.json.bak")
if _, err := os.Stat(rolesBackup); os.IsNotExist(err) {
t.Error("Roles backup file should be created")
}
assignmentsBackup := filepath.Join(tmpDir, "rbac_assignments.json.bak")
if _, err := os.Stat(assignmentsBackup); os.IsNotExist(err) {
t.Error("Assignments backup file should be created")
}
})
}
func TestSQLiteManagerCircularInheritance(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "rbac-circular-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()
// Create role A
roleA := Role{ID: "role-a", Name: "Role A", Permissions: []Permission{{Action: "read", Resource: "a"}}}
m.SaveRole(roleA)
// Create role B with parent A
roleB := Role{ID: "role-b", Name: "Role B", ParentID: "role-a", Permissions: []Permission{{Action: "read", Resource: "b"}}}
m.SaveRole(roleB)
// Create role C with parent B
roleC := Role{ID: "role-c", Name: "Role C", ParentID: "role-b", Permissions: []Permission{{Action: "read", Resource: "c"}}}
m.SaveRole(roleC)
// Try to make A inherit from C (creating cycle)
roleA.ParentID = "role-c"
err = m.SaveRole(roleA)
if err == nil {
t.Error("Expected error when creating circular inheritance")
}
}
func TestSQLiteManagerContextOperations(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "rbac-context-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()
t.Run("SaveRoleWithContext records user", func(t *testing.T) {
role := Role{
ID: "context-role",
Name: "Context Test Role",
Permissions: []Permission{{Action: "read", Resource: "test"}},
}
if err := m.SaveRoleWithContext(role, "admin-user"); err != nil {
t.Errorf("Failed to save role with context: %v", err)
}
// Check changelog has user
logs := m.GetChangeLogsForEntity("role", "context-role")
if len(logs) == 0 {
t.Fatal("Expected changelog entry")
}
if logs[0].User != "admin-user" {
t.Errorf("Expected user 'admin-user', got '%s'", logs[0].User)
}
})
t.Run("DeleteRoleWithContext records user", func(t *testing.T) {
if err := m.DeleteRoleWithContext("context-role", "admin-user"); err != nil {
t.Errorf("Failed to delete role with context: %v", err)
}
logs := m.GetChangeLogsForEntity("role", "context-role")
var hasDelete bool
for _, l := range logs {
if l.Action == ActionRoleDeleted && l.User == "admin-user" {
hasDelete = true
}
}
if !hasDelete {
t.Error("Missing delete changelog with user")
}
})
t.Run("UpdateUserRolesWithContext records user", func(t *testing.T) {
if err := m.UpdateUserRolesWithContext("context-user", []string{RoleViewer}, "admin-user"); err != nil {
t.Errorf("Failed to update user roles with context: %v", err)
}
logs := m.GetChangeLogsForEntity("assignment", "context-user")
if len(logs) == 0 {
t.Fatal("Expected changelog entry")
}
if logs[0].User != "admin-user" {
t.Errorf("Expected user 'admin-user', got '%s'", logs[0].User)
}
})
}
func TestSQLiteManagerPersistence(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "rbac-persist-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create manager and add data
m1, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
if err != nil {
t.Fatalf("Failed to create SQLiteManager: %v", err)
}
role := Role{
ID: "persist-role",
Name: "Persist Test",
ParentID: RoleViewer,
Permissions: []Permission{{Action: "write", Resource: "persist", Effect: EffectAllow}},
}
m1.SaveRole(role)
m1.AssignRole("persist-user", "persist-role")
m1.Close()
// Reopen and verify
m2, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
if err != nil {
t.Fatalf("Failed to reopen SQLiteManager: %v", err)
}
defer m2.Close()
t.Run("Role persisted", func(t *testing.T) {
r, ok := m2.GetRole("persist-role")
if !ok {
t.Error("Role should persist")
}
if r.ParentID != RoleViewer {
t.Errorf("Expected parent %s, got %s", RoleViewer, r.ParentID)
}
})
t.Run("Assignment persisted", func(t *testing.T) {
a, ok := m2.GetUserAssignment("persist-user")
if !ok {
t.Error("Assignment should persist")
}
if len(a.RoleIDs) != 1 {
t.Errorf("Expected 1 role, got %d", len(a.RoleIDs))
}
})
t.Run("Changelog persisted", func(t *testing.T) {
logs := m2.GetChangeLogs(100, 0)
if len(logs) == 0 {
t.Error("Changelog should persist")
}
})
}
func TestSQLiteManagerDenyPermission(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "rbac-deny-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()
// Create role with both allow and deny permissions
role := Role{
ID: "deny-test",
Name: "Deny Test Role",
Permissions: []Permission{
{Action: "read", Resource: "*", Effect: EffectAllow}, // Allow all reads
{Action: "read", Resource: "secrets", Effect: EffectDeny}, // But deny reading secrets
{Action: "write", Resource: "settings", Effect: EffectAllow}, // Allow writing settings
{Action: "write", Resource: "settings:admin", Effect: EffectDeny}, // But deny admin settings
},
}
if err := m.SaveRole(role); err != nil {
t.Fatalf("Failed to save role: %v", err)
}
retrieved, ok := m.GetRole("deny-test")
if !ok {
t.Fatal("Role not found")
}
// Verify deny effects are preserved
var denyCount, allowCount int
for _, p := range retrieved.Permissions {
if p.GetEffect() == EffectDeny {
denyCount++
} else {
allowCount++
}
}
if denyCount != 2 {
t.Errorf("Expected 2 deny permissions, got %d", denyCount)
}
if allowCount != 2 {
t.Errorf("Expected 2 allow permissions, got %d", allowCount)
}
}
func TestSQLiteManagerChangeLogRetention(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "rbac-retention-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()
// Create several roles to generate changelog entries
for i := 0; i < 5; i++ {
role := Role{
ID: "retention-role-" + string(rune('a'+i)),
Name: "Retention Test " + string(rune('a'+i)),
Permissions: []Permission{{Action: "read", Resource: "test"}},
}
m.SaveRole(role)
// Add slight delay to ensure different timestamps
time.Sleep(10 * time.Millisecond)
}
t.Run("Pagination works", func(t *testing.T) {
// Get first 2
logs1 := m.GetChangeLogs(2, 0)
if len(logs1) != 2 {
t.Errorf("Expected 2 logs, got %d", len(logs1))
}
// Get next 2
logs2 := m.GetChangeLogs(2, 2)
if len(logs2) != 2 {
t.Errorf("Expected 2 logs, got %d", len(logs2))
}
// Ensure they're different
if len(logs1) > 0 && len(logs2) > 0 && logs1[0].ID == logs2[0].ID {
t.Error("Pagination returned same entries")
}
})
t.Run("Logs ordered by timestamp desc", func(t *testing.T) {
logs := m.GetChangeLogs(100, 0)
for i := 1; i < len(logs); i++ {
if logs[i].Timestamp.After(logs[i-1].Timestamp) {
t.Error("Logs should be ordered by timestamp descending")
}
}
})
}