mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
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
351 lines
7.6 KiB
Go
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
|
|
}
|