Files
Pulse/pkg/auth/rbac_manager.go
rcourtman 55f5f071ed fix: replace hallucinated upgrade URLs with correct pulserelay.pro
Previous LLM sessions incorrectly inserted fake URLs (pulse.sh/pro and
yourpulse.io/pro) for the Pro upgrade links. Neither domain exists.

Replaced all 34 instances with the correct URL: https://pulserelay.pro/

Fixes #1077
2026-01-10 22:45:40 +00:00

351 lines
7.6 KiB
Go

package auth
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
// FileManager implements the Manager interface with file-based persistence.
type FileManager struct {
mu sync.RWMutex
dataDir string
roles map[string]Role
assignments map[string]UserRoleAssignment
}
// NewFileManager creates a new file-based RBAC manager.
func NewFileManager(dataDir string) (*FileManager, error) {
m := &FileManager{
dataDir: dataDir,
roles: make(map[string]Role),
assignments: make(map[string]UserRoleAssignment),
}
// Initialize built-in roles
m.initBuiltInRoles()
// Load persisted data
if err := m.load(); err != nil {
// Non-fatal - just start fresh if no data exists
if !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to load RBAC data: %w", err)
}
}
return m, nil
}
func (m *FileManager) initBuiltInRoles() {
now := time.Now()
builtInRoles := []Role{
{
ID: RoleAdmin,
Name: "Administrator",
Description: "Full administrative access to all features",
Permissions: []Permission{
{Action: "admin", Resource: "*"},
},
IsBuiltIn: true,
CreatedAt: now,
UpdatedAt: now,
},
{
ID: RoleOperator,
Name: "Operator",
Description: "Can manage nodes and perform operational tasks",
Permissions: []Permission{
{Action: "read", Resource: "*"},
{Action: "write", Resource: "nodes"},
{Action: "write", Resource: "vms"},
{Action: "write", Resource: "containers"},
{Action: "write", Resource: "alerts"},
},
IsBuiltIn: true,
CreatedAt: now,
UpdatedAt: now,
},
{
ID: RoleViewer,
Name: "Viewer",
Description: "Read-only access to monitoring data",
Permissions: []Permission{
{Action: "read", Resource: "*"},
},
IsBuiltIn: true,
CreatedAt: now,
UpdatedAt: now,
},
{
ID: RoleAuditor,
Name: "Auditor",
Description: "Access to audit logs and compliance data",
Permissions: []Permission{
{Action: "read", Resource: "audit_logs"},
{Action: "read", Resource: "nodes"},
{Action: "read", Resource: "alerts"},
{Action: "read", Resource: "compliance"},
},
IsBuiltIn: true,
CreatedAt: now,
UpdatedAt: now,
},
}
for _, role := range builtInRoles {
m.roles[role.ID] = role
}
}
func (m *FileManager) rolesFile() string {
return filepath.Join(m.dataDir, "rbac_roles.json")
}
func (m *FileManager) assignmentsFile() string {
return filepath.Join(m.dataDir, "rbac_assignments.json")
}
func (m *FileManager) load() error {
// Load custom roles (built-in roles are always initialized)
if data, err := os.ReadFile(m.rolesFile()); err == nil {
var roles []Role
if err := json.Unmarshal(data, &roles); err != nil {
return err
}
for _, role := range roles {
if !role.IsBuiltIn {
m.roles[role.ID] = role
}
}
}
// Load assignments
if data, err := os.ReadFile(m.assignmentsFile()); err == nil {
var assignments []UserRoleAssignment
if err := json.Unmarshal(data, &assignments); err != nil {
return err
}
for _, a := range assignments {
m.assignments[a.Username] = a
}
}
return nil
}
func (m *FileManager) saveRoles() error {
roles := make([]Role, 0, len(m.roles))
for _, role := range m.roles {
roles = append(roles, role)
}
data, err := json.MarshalIndent(roles, "", " ")
if err != nil {
return err
}
return os.WriteFile(m.rolesFile(), data, 0600)
}
func (m *FileManager) saveAssignments() error {
assignments := make([]UserRoleAssignment, 0, len(m.assignments))
for _, a := range m.assignments {
assignments = append(assignments, a)
}
data, err := json.MarshalIndent(assignments, "", " ")
if err != nil {
return err
}
return os.WriteFile(m.assignmentsFile(), data, 0600)
}
// GetRoles returns all roles.
func (m *FileManager) GetRoles() []Role {
m.mu.RLock()
defer m.mu.RUnlock()
roles := make([]Role, 0, len(m.roles))
for _, role := range m.roles {
roles = append(roles, role)
}
return roles
}
// GetRole returns a role by ID.
func (m *FileManager) GetRole(id string) (Role, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
role, ok := m.roles[id]
return role, ok
}
// SaveRole creates or updates a role.
func (m *FileManager) SaveRole(role Role) error {
m.mu.Lock()
defer m.mu.Unlock()
// Cannot modify built-in roles
if existing, ok := m.roles[role.ID]; ok && existing.IsBuiltIn {
return fmt.Errorf("cannot modify built-in role: %s", role.ID)
}
role.UpdatedAt = time.Now()
if role.CreatedAt.IsZero() {
role.CreatedAt = role.UpdatedAt
}
m.roles[role.ID] = role
return m.saveRoles()
}
// DeleteRole removes a role by ID.
func (m *FileManager) DeleteRole(id string) error {
m.mu.Lock()
defer m.mu.Unlock()
role, ok := m.roles[id]
if !ok {
return nil // Already deleted
}
if role.IsBuiltIn {
return fmt.Errorf("cannot delete built-in role: %s", id)
}
delete(m.roles, id)
return m.saveRoles()
}
// GetUserAssignments returns all user role assignments.
func (m *FileManager) GetUserAssignments() []UserRoleAssignment {
m.mu.RLock()
defer m.mu.RUnlock()
assignments := make([]UserRoleAssignment, 0, len(m.assignments))
for _, a := range m.assignments {
assignments = append(assignments, a)
}
return assignments
}
// GetUserAssignment returns the role assignment for a user.
func (m *FileManager) GetUserAssignment(username string) (UserRoleAssignment, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
a, ok := m.assignments[username]
return a, ok
}
// AssignRole adds a role to a user.
func (m *FileManager) AssignRole(username string, roleID string) error {
m.mu.Lock()
defer m.mu.Unlock()
// Verify role exists
if _, ok := m.roles[roleID]; !ok {
return fmt.Errorf("role not found: %s", roleID)
}
a, ok := m.assignments[username]
if !ok {
a = UserRoleAssignment{
Username: username,
RoleIDs: []string{},
}
}
// Check if already assigned
for _, id := range a.RoleIDs {
if id == roleID {
return nil // Already assigned
}
}
a.RoleIDs = append(a.RoleIDs, roleID)
a.UpdatedAt = time.Now()
m.assignments[username] = a
return m.saveAssignments()
}
// UpdateUserRoles replaces all roles for a user.
func (m *FileManager) UpdateUserRoles(username string, roleIDs []string) error {
m.mu.Lock()
defer m.mu.Unlock()
// Verify all roles exist
for _, roleID := range roleIDs {
if _, ok := m.roles[roleID]; !ok {
return fmt.Errorf("role not found: %s", roleID)
}
}
m.assignments[username] = UserRoleAssignment{
Username: username,
RoleIDs: roleIDs,
UpdatedAt: time.Now(),
}
return m.saveAssignments()
}
// RemoveRole removes a role from a user.
func (m *FileManager) RemoveRole(username string, roleID string) error {
m.mu.Lock()
defer m.mu.Unlock()
a, ok := m.assignments[username]
if !ok {
return nil // No assignment exists
}
newRoles := make([]string, 0, len(a.RoleIDs))
for _, id := range a.RoleIDs {
if id != roleID {
newRoles = append(newRoles, id)
}
}
a.RoleIDs = newRoles
a.UpdatedAt = time.Now()
m.assignments[username] = a
return m.saveAssignments()
}
// GetUserPermissions returns the effective permissions for a user.
func (m *FileManager) GetUserPermissions(username string) []Permission {
m.mu.RLock()
defer m.mu.RUnlock()
a, ok := m.assignments[username]
if !ok {
return nil
}
// Collect unique permissions from all assigned roles
permMap := make(map[string]Permission)
for _, roleID := range a.RoleIDs {
if role, ok := m.roles[roleID]; ok {
for _, perm := range role.Permissions {
key := perm.Action + ":" + perm.Resource
permMap[key] = perm
}
}
}
perms := make([]Permission, 0, len(permMap))
for _, perm := range permMap {
perms = append(perms, perm)
}
return perms
}