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) }