Add comprehensive authentication and authorization enhancements: - tokens.go: New token management system for public task access and cloning * SHA-256 hashed token storage for security * Token generation, validation, and automatic cleanup * Support for public access and clone permissions - api_key.go: Extend User struct with Groups field * Lab group membership (ml-lab, nlp-group) * Integration with permission system for group-based access - flags.go: Security hardening - migrate to structured logging * Replace log.Printf with log/slog to prevent log injection attacks * Consistent structured output for all auth warnings * Safe handling of file paths and errors in logs - permissions.go: Add task sharing permission constants * PermissionTasksReadOwn: Access own tasks * PermissionTasksReadLab: Access lab group tasks * PermissionTasksReadAll: Admin/institution-wide access * PermissionTasksShare: Grant access to other users * PermissionTasksClone: Create copies of shared tasks * CanAccessTask() method with visibility checks - database.go: Improve error handling * Add structured error logging on row close failures
277 lines
7.5 KiB
Go
277 lines
7.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/domain"
|
|
)
|
|
|
|
// Permission constants for type safety
|
|
const (
|
|
// Job permissions
|
|
PermissionJobsCreate = "jobs:create"
|
|
PermissionJobsRead = "jobs:read"
|
|
PermissionJobsUpdate = "jobs:update"
|
|
PermissionJobsDelete = "jobs:delete"
|
|
|
|
// Data permissions
|
|
PermissionDataCreate = "data:create"
|
|
PermissionDataRead = "data:read"
|
|
PermissionDataUpdate = "data:update"
|
|
PermissionDataDelete = "data:delete"
|
|
|
|
// Model permissions
|
|
PermissionModelsCreate = "models:create"
|
|
PermissionModelsRead = "models:read"
|
|
PermissionModelsUpdate = "models:update"
|
|
PermissionModelsDelete = "models:delete"
|
|
|
|
// System permissions
|
|
PermissionSystemConfig = "system:config"
|
|
PermissionSystemMetrics = "system:metrics"
|
|
PermissionSystemLogs = "system:logs"
|
|
PermissionSystemUsers = "system:users"
|
|
|
|
// Task sharing permissions
|
|
PermissionTasksReadOwn = "tasks:read:own"
|
|
PermissionTasksReadLab = "tasks:read:lab"
|
|
PermissionTasksReadAll = "tasks:read:all"
|
|
PermissionTasksShare = "tasks:share"
|
|
PermissionTasksClone = "tasks:clone"
|
|
|
|
// Wildcard permission
|
|
PermissionAll = "*"
|
|
)
|
|
|
|
// Role constants
|
|
const (
|
|
RoleAdmin = "admin"
|
|
RoleDataScientist = "data_scientist"
|
|
RoleDataEngineer = "data_engineer"
|
|
RoleViewer = "viewer"
|
|
RoleOperator = "operator"
|
|
)
|
|
|
|
// PermissionGroup represents a group of related permissions
|
|
type PermissionGroup struct {
|
|
Name string
|
|
Description string
|
|
Permissions []string
|
|
}
|
|
|
|
// PermissionGroups defines built-in permission groups.
|
|
var PermissionGroups = map[string]PermissionGroup{
|
|
"full_access": {
|
|
Name: "Full Access",
|
|
Permissions: []string{PermissionAll},
|
|
Description: "Complete system access",
|
|
},
|
|
"job_management": {
|
|
Name: "Job Management",
|
|
Permissions: []string{
|
|
PermissionJobsCreate,
|
|
PermissionJobsRead,
|
|
PermissionJobsUpdate,
|
|
PermissionJobsDelete,
|
|
},
|
|
Description: "Create, read, update, and delete ML jobs",
|
|
},
|
|
"data_access": {
|
|
Name: "Data Access",
|
|
Permissions: []string{
|
|
PermissionDataRead,
|
|
PermissionDataCreate,
|
|
PermissionDataUpdate,
|
|
PermissionDataDelete,
|
|
},
|
|
Description: "Access and manage datasets",
|
|
},
|
|
"readonly": {
|
|
Name: "Read Only",
|
|
Permissions: []string{
|
|
PermissionJobsRead,
|
|
PermissionDataRead,
|
|
PermissionModelsRead,
|
|
PermissionSystemMetrics,
|
|
},
|
|
Description: "View-only access to system resources",
|
|
},
|
|
"system_admin": {
|
|
Name: "System Administration",
|
|
Permissions: []string{
|
|
PermissionSystemConfig,
|
|
PermissionSystemLogs,
|
|
PermissionSystemUsers,
|
|
PermissionSystemMetrics,
|
|
},
|
|
Description: "System configuration and user management",
|
|
},
|
|
}
|
|
|
|
// GetPermissionGroup returns a permission group by name
|
|
func GetPermissionGroup(name string) (PermissionGroup, bool) {
|
|
group, exists := PermissionGroups[name]
|
|
return group, exists
|
|
}
|
|
|
|
// ValidatePermission checks if a permission string is valid
|
|
func ValidatePermission(permission string) error {
|
|
if permission == PermissionAll {
|
|
return nil
|
|
}
|
|
|
|
// Check if permission matches known patterns
|
|
validPrefixes := []string{"jobs:", "data:", "models:", "system:"}
|
|
for _, prefix := range validPrefixes {
|
|
if strings.HasPrefix(permission, prefix) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("invalid permission format: %s", permission)
|
|
}
|
|
|
|
// ValidateRole checks if a role is valid
|
|
func ValidateRole(role string) error {
|
|
validRoles := []string{RoleAdmin, RoleDataScientist, RoleDataEngineer, RoleViewer, RoleOperator}
|
|
for _, validRole := range validRoles {
|
|
if role == validRole {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("invalid role: %s", role)
|
|
}
|
|
|
|
// ExpandPermissionGroups converts permission group names to actual permissions
|
|
func ExpandPermissionGroups(groups []string) ([]string, error) {
|
|
var permissions []string
|
|
|
|
for _, groupName := range groups {
|
|
if groupName == PermissionAll {
|
|
return []string{PermissionAll}, nil
|
|
}
|
|
|
|
group, exists := GetPermissionGroup(groupName)
|
|
if !exists {
|
|
return nil, fmt.Errorf("unknown permission group: %s", groupName)
|
|
}
|
|
|
|
permissions = append(permissions, group.Permissions...)
|
|
}
|
|
|
|
// Remove duplicates
|
|
unique := make(map[string]bool)
|
|
for _, perm := range permissions {
|
|
unique[perm] = true
|
|
}
|
|
|
|
result := make([]string, 0, len(unique))
|
|
for perm := range unique {
|
|
result = append(result, perm)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// PermissionCheckResult represents the result of a permission check
|
|
type PermissionCheckResult struct {
|
|
Permission string `json:"permission"`
|
|
User string `json:"user"`
|
|
Roles []string `json:"roles"`
|
|
Missing []string `json:"missing,omitempty"`
|
|
Allowed bool `json:"allowed"`
|
|
}
|
|
|
|
// CheckMultiplePermissions checks multiple permissions at once
|
|
func (u *User) CheckMultiplePermissions(permissions []string) []PermissionCheckResult {
|
|
results := make([]PermissionCheckResult, len(permissions))
|
|
|
|
for i, permission := range permissions {
|
|
allowed := u.HasPermission(permission)
|
|
missing := []string{}
|
|
if !allowed {
|
|
missing = []string{permission}
|
|
}
|
|
|
|
results[i] = PermissionCheckResult{
|
|
Allowed: allowed,
|
|
Permission: permission,
|
|
User: u.Name,
|
|
Roles: u.Roles,
|
|
Missing: missing,
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// GetEffectivePermissions returns all effective permissions for a user
|
|
func (u *User) GetEffectivePermissions() []string {
|
|
if u.Permissions[PermissionAll] {
|
|
return []string{PermissionAll}
|
|
}
|
|
|
|
permissions := make([]string, 0, len(u.Permissions))
|
|
for perm := range u.Permissions {
|
|
permissions = append(permissions, perm)
|
|
}
|
|
|
|
return permissions
|
|
}
|
|
|
|
// TaskAccessChecker defines the interface for checking task access permissions.
|
|
// Implemented by storage.DB to avoid circular imports.
|
|
type TaskAccessChecker interface {
|
|
TaskIsSharedWithUser(taskID, userID string) bool
|
|
UserSharesGroupWithTask(userID, taskID string) bool
|
|
TaskAllowsPublicClone(taskID string) bool
|
|
UserRoleInTaskGroups(userID, taskID string) string // NEW: returns highest role (admin/member/viewer)
|
|
}
|
|
|
|
// CanAccessTask checks if an authenticated user can access a specific task.
|
|
// For unauthenticated (token-based) access, use CanAccessWithToken() instead.
|
|
// NOTE: This method makes 1-2 DB calls per invocation. Never call it in a loop
|
|
// over a task list — use ListTasksForUser() instead, which resolves all access
|
|
// in a single query.
|
|
func (u *User) CanAccessTask(task *domain.Task, db TaskAccessChecker) bool {
|
|
if task.UserID == u.Name || task.CreatedBy == u.Name {
|
|
return true
|
|
}
|
|
if u.Admin {
|
|
return true
|
|
}
|
|
// Explicit share — expiry checked in SQL
|
|
if db.TaskIsSharedWithUser(task.ID, u.Name) {
|
|
return true
|
|
}
|
|
switch task.Visibility {
|
|
case domain.VisibilityPrivate:
|
|
return false
|
|
case domain.VisibilityLab:
|
|
return db.UserSharesGroupWithTask(u.Name, task.ID)
|
|
case domain.VisibilityInstitution, domain.VisibilityOpen:
|
|
// open tasks are accessible to authenticated users too, not only token holders
|
|
return u.HasPermission(PermissionTasksReadLab)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CanCloneTask checks if a user can clone (reproduce) a task.
|
|
// Clone is a distinct action from read. Viewing a task does not imply
|
|
// permission to reproduce it under a different account.
|
|
func (u *User) CanCloneTask(task *domain.Task, db TaskAccessChecker) bool {
|
|
if !u.CanAccessTask(task, db) {
|
|
return false
|
|
}
|
|
if task.Visibility == domain.VisibilityOpen {
|
|
return db.TaskAllowsPublicClone(task.ID)
|
|
}
|
|
// NEW: viewer-role members can see but not clone
|
|
// role == "" means access is via explicit task_shares (not group) - allow clone
|
|
role := db.UserRoleInTaskGroups(u.Name, task.ID)
|
|
if role == "viewer" {
|
|
return false
|
|
}
|
|
return u.HasPermission(PermissionTasksClone)
|
|
}
|