fetch_ml/internal/auth/permissions_loader.go
Jeremie Fraeys ea15af1833 Fix multi-user authentication and clean up debug code
- Fix YAML tags in auth config struct (json -> yaml)
- Update CLI configs to use pre-hashed API keys
- Remove double hashing in WebSocket client
- Fix port mapping (9102 -> 9103) in CLI commands
- Update permission keys to use jobs:read, jobs:create, etc.
- Clean up all debug logging from CLI and server
- All user roles now authenticate correctly:
  * Admin: Can queue jobs and see all jobs
  * Researcher: Can queue jobs and see own jobs
  * Analyst: Can see status (read-only access)

Multi-user authentication is now fully functional.
2025-12-06 12:35:32 -05:00

295 lines
6.9 KiB
Go

package auth
import (
"fmt"
"sync"
"github.com/jfraeys/fetch_ml/internal/fileutil"
"gopkg.in/yaml.v3"
)
// PermissionConfig represents the permissions configuration
type PermissionConfig struct {
Roles map[string]RoleConfig `yaml:"roles"`
Groups map[string]GroupConfig `yaml:"groups"`
Hierarchy map[string]HierarchyConfig `yaml:"hierarchy"`
Defaults DefaultsConfig `yaml:"defaults"`
}
// RoleConfig defines a role and its permissions
type RoleConfig struct {
Description string `yaml:"description"`
Permissions []string `yaml:"permissions"`
}
// GroupConfig defines a permission group
type GroupConfig struct {
Description string `yaml:"description"`
Inherits []string `yaml:"inherits"`
Permissions []string `yaml:"permissions"`
}
// HierarchyConfig defines resource hierarchy
type HierarchyConfig struct {
Children map[string]interface{} `yaml:"children"`
Special map[string]string `yaml:"special"`
}
// DefaultsConfig defines default settings
type DefaultsConfig struct {
NewUserRole string `yaml:"new_user_role"`
AdminUsers []string `yaml:"admin_users"`
}
// PermissionManager manages permissions from YAML file
type PermissionManager struct {
config *PermissionConfig
rolePerms map[string]map[string]bool
groupPerms map[string]map[string]bool
mu sync.RWMutex
loaded bool
}
// NewPermissionManager creates a new permission manager
func NewPermissionManager(configPath string) (*PermissionManager, error) {
pm := &PermissionManager{}
if err := pm.loadConfig(configPath); err != nil {
return nil, fmt.Errorf("failed to load permissions: %w", err)
}
return pm, nil
}
// loadConfig loads permissions from YAML file
func (pm *PermissionManager) loadConfig(configPath string) error {
pm.mu.Lock()
defer pm.mu.Unlock()
data, err := fileutil.SecureFileRead(configPath)
if err != nil {
return fmt.Errorf("failed to read permissions file: %w", err)
}
var config PermissionConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return fmt.Errorf("failed to parse permissions file: %w", err)
}
pm.config = &config
pm.rolePerms = make(map[string]map[string]bool)
pm.groupPerms = make(map[string]map[string]bool)
// Process role permissions
for roleName, role := range config.Roles {
perms := make(map[string]bool)
for _, perm := range role.Permissions {
perms[perm] = true
}
pm.rolePerms[roleName] = perms
}
// Process group permissions
for groupName, group := range config.Groups {
perms := make(map[string]bool)
// Add direct permissions
for _, perm := range group.Permissions {
perms[perm] = true
}
// Inherit permissions from other roles/groups
for _, inherit := range group.Inherits {
if rolePerms, exists := pm.rolePerms[inherit]; exists {
for perm, value := range rolePerms {
perms[perm] = value
}
}
if groupPerms, exists := pm.groupPerms[inherit]; exists {
for perm, value := range groupPerms {
perms[perm] = value
}
}
}
pm.groupPerms[groupName] = perms
}
pm.loaded = true
return nil
}
// GetRolePermissions returns permissions for a role
func (pm *PermissionManager) GetRolePermissions(role string) map[string]bool {
pm.mu.RLock()
defer pm.mu.RUnlock()
if !pm.loaded {
return make(map[string]bool)
}
if perms, exists := pm.rolePerms[role]; exists {
result := make(map[string]bool)
for perm, value := range perms {
result[perm] = value
}
return result
}
return make(map[string]bool)
}
// GetGroupPermissions returns permissions for a group
func (pm *PermissionManager) GetGroupPermissions(group string) map[string]bool {
pm.mu.RLock()
defer pm.mu.RUnlock()
if !pm.loaded {
return make(map[string]bool)
}
if perms, exists := pm.groupPerms[group]; exists {
result := make(map[string]bool)
for perm, value := range perms {
result[perm] = value
}
return result
}
return make(map[string]bool)
}
// GetAllRoles returns all available roles
func (pm *PermissionManager) GetAllRoles() map[string]RoleConfig {
pm.mu.RLock()
defer pm.mu.RUnlock()
if !pm.loaded {
return make(map[string]RoleConfig)
}
result := make(map[string]RoleConfig)
for name, role := range pm.config.Roles {
result[name] = role
}
return result
}
// GetAllGroups returns all available groups
func (pm *PermissionManager) GetAllGroups() map[string]GroupConfig {
pm.mu.RLock()
defer pm.mu.RUnlock()
if !pm.loaded {
return make(map[string]GroupConfig)
}
result := make(map[string]GroupConfig)
for name, group := range pm.config.Groups {
result[name] = group
}
return result
}
// GetDefaultRole returns the default role for new users
func (pm *PermissionManager) GetDefaultRole() string {
pm.mu.RLock()
defer pm.mu.RUnlock()
if !pm.loaded || pm.config.Defaults.NewUserRole == "" {
return "viewer"
}
return pm.config.Defaults.NewUserRole
}
// IsAdminUser checks if a username should have admin rights
func (pm *PermissionManager) IsAdminUser(username string) bool {
pm.mu.RLock()
defer pm.mu.RUnlock()
if !pm.loaded {
return false
}
for _, adminUser := range pm.config.Defaults.AdminUsers {
if adminUser == username {
return true
}
}
return false
}
// ReloadConfig reloads the permissions configuration
func (pm *PermissionManager) ReloadConfig(configPath string) error {
return pm.loadConfig(configPath)
}
// ValidatePermission checks if a permission string is valid
func (pm *PermissionManager) ValidatePermission(permission string) bool {
pm.mu.RLock()
defer pm.mu.RUnlock()
if !pm.loaded {
return false
}
// Wildcard is always valid
if permission == "*" {
return true
}
// Check if permission matches any defined role permissions
for _, rolePerms := range pm.rolePerms {
if _, exists := rolePerms[permission]; exists {
return true
}
}
// Check if permission matches any group permissions
for _, groupPerms := range pm.groupPerms {
if _, exists := groupPerms[permission]; exists {
return true
}
}
return false
}
// GetPermissionHierarchy returns the hierarchy for a resource
func (pm *PermissionManager) GetPermissionHierarchy(resource string) map[string]interface{} {
pm.mu.RLock()
defer pm.mu.RUnlock()
if !pm.loaded {
return make(map[string]interface{})
}
if hierarchy, exists := pm.config.Hierarchy[resource]; exists {
return hierarchy.Children
}
return make(map[string]interface{})
}
// Global permission manager instance
var globalPermissionManager *PermissionManager
var permissionManagerOnce sync.Once
// GetGlobalPermissionManager returns the global permission manager
func GetGlobalPermissionManager() *PermissionManager {
permissionManagerOnce.Do(func() {
// Try to load from default location
if pm, err := NewPermissionManager("configs/schema/permissions.yaml"); err == nil {
globalPermissionManager = pm
} else {
// Fallback to empty manager
globalPermissionManager = &PermissionManager{
rolePerms: make(map[string]map[string]bool),
groupPerms: make(map[string]map[string]bool),
loaded: false,
}
}
})
return globalPermissionManager
}