- 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.
295 lines
6.9 KiB
Go
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
|
|
}
|