- Add Argon2id-based API key hashing with salt support - Implement Ed25519 manifest signing (key generation, sign, verify) - Add gen-keys CLI tool for manifest signing keys - Fix hash-key command to hash provided key (not generate new one) - Complete isHex helper function
179 lines
4.6 KiB
Go
179 lines
4.6 KiB
Go
// Package main implements the fetch_ml user management CLI
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/auth"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ConfigWithAuth wraps auth configuration for user management.
|
|
type ConfigWithAuth struct {
|
|
Auth auth.Config `yaml:"auth"`
|
|
}
|
|
|
|
func main() {
|
|
var (
|
|
configFile = flag.String("config", "", "Configuration file path")
|
|
command = flag.String("cmd", "", "Command: generate-key, list-users, hash-key")
|
|
username = flag.String("username", "", "Username for generate-key")
|
|
role = flag.String("role", "", "Role for generate-key")
|
|
admin = flag.Bool("admin", false, "Admin flag for generate-key")
|
|
apiKey = flag.String("key", "", "API key to hash")
|
|
)
|
|
flag.Parse()
|
|
|
|
if *configFile == "" || *command == "" {
|
|
fmt.Println("Usage: user_manager --config <config.yaml> --cmd <command> [options]")
|
|
fmt.Println("Commands: generate-key, list-users, hash-key")
|
|
os.Exit(1)
|
|
}
|
|
|
|
switch *command {
|
|
case "generate-key":
|
|
if *username == "" {
|
|
log.Fatal("Usage: --cmd generate-key --username <name> [--admin] [--role <role>]")
|
|
}
|
|
|
|
// Load config
|
|
data, err := os.ReadFile(*configFile)
|
|
if err != nil {
|
|
log.Fatalf("Failed to read config: %v", err)
|
|
}
|
|
|
|
var config ConfigWithAuth
|
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
|
log.Fatalf("Failed to parse config: %v", err)
|
|
}
|
|
|
|
// Determine admin status and roles
|
|
adminStatus := *admin
|
|
roles := []string{"viewer"}
|
|
permissions := make(map[string]bool)
|
|
|
|
if !adminStatus && *role == "" {
|
|
fmt.Printf("Make user '%s' an admin? (y/N): ", *username)
|
|
var response string
|
|
_, _ = fmt.Scanln(&response)
|
|
adminStatus = strings.ToLower(strings.TrimSpace(response)) == "y"
|
|
}
|
|
|
|
if adminStatus {
|
|
roles = []string{"admin"}
|
|
permissions["*"] = true
|
|
} else if *role != "" {
|
|
roles = []string{*role}
|
|
rolePerms := getRolePermissions(*role)
|
|
for perm, value := range rolePerms {
|
|
permissions[perm] = value
|
|
}
|
|
}
|
|
|
|
// Generate API key using new Argon2id method
|
|
apiKey, apiKeyEntry, err := auth.GenerateNewAPIKey(adminStatus, roles, permissions)
|
|
if err != nil {
|
|
log.Fatalf("Failed to generate API key: %v", err)
|
|
}
|
|
|
|
// Setup user
|
|
if config.Auth.APIKeys == nil {
|
|
config.Auth.APIKeys = make(map[auth.Username]auth.APIKeyEntry)
|
|
}
|
|
|
|
config.Auth.APIKeys[auth.Username(*username)] = apiKeyEntry
|
|
|
|
data, err = yaml.Marshal(config)
|
|
if err != nil {
|
|
log.Fatalf("Failed to marshal config: %v", err)
|
|
}
|
|
|
|
if err := os.WriteFile(*configFile, data, 0600); err != nil {
|
|
log.Fatalf("Failed to write config: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Generated API key for user '%s':\nKey: %s\n", *username, apiKey)
|
|
|
|
case "list-users":
|
|
data, err := os.ReadFile(*configFile)
|
|
if err != nil {
|
|
log.Fatalf("Failed to read config: %v", err)
|
|
}
|
|
|
|
var config ConfigWithAuth
|
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
|
log.Fatalf("Failed to parse config: %v", err)
|
|
}
|
|
|
|
fmt.Println("Configured Users:")
|
|
fmt.Println("=================")
|
|
for username, entry := range config.Auth.APIKeys {
|
|
fmt.Printf("User: %s\n", string(username))
|
|
fmt.Printf(" Admin: %v\n", entry.Admin)
|
|
if len(entry.Roles) > 0 {
|
|
fmt.Printf(" Roles: %v\n", entry.Roles)
|
|
}
|
|
if len(entry.Permissions) > 0 {
|
|
fmt.Printf(" Permissions: %d\n", len(entry.Permissions))
|
|
}
|
|
fmt.Printf(" Key Hash: %s...\n\n", string(entry.Hash)[:8])
|
|
}
|
|
|
|
case "hash-key":
|
|
if *apiKey == "" {
|
|
log.Fatal("Usage: --cmd hash-key --key <api-key>")
|
|
}
|
|
// Hash the provided key with Argon2id
|
|
hashed, err := auth.HashAPIKeyArgon2id(*apiKey)
|
|
if err != nil {
|
|
log.Fatalf("Failed to hash key: %v", err)
|
|
}
|
|
fmt.Printf("Hash: %s\nSalt: %s\nAlgorithm: %s\n", hashed.Hash, hashed.Salt, hashed.Algorithm)
|
|
|
|
default:
|
|
log.Fatalf("Unknown command: %s", *command)
|
|
}
|
|
}
|
|
|
|
// getRolePermissions returns permissions for a role
|
|
func getRolePermissions(role string) map[string]bool {
|
|
rolePermissions := map[string]map[string]bool{
|
|
"admin": {
|
|
"*": true,
|
|
},
|
|
"data_scientist": {
|
|
"jobs:create": true,
|
|
"jobs:read": true,
|
|
"jobs:update": true,
|
|
"data:read": true,
|
|
"models:read": true,
|
|
},
|
|
"data_engineer": {
|
|
"data:create": true,
|
|
"data:read": true,
|
|
"data:update": true,
|
|
"data:delete": true,
|
|
},
|
|
"viewer": {
|
|
"jobs:read": true,
|
|
"data:read": true,
|
|
"models:read": true,
|
|
"metrics:read": true,
|
|
},
|
|
"operator": {
|
|
"jobs:read": true,
|
|
"jobs:update": true,
|
|
"metrics:read": true,
|
|
"system:read": true,
|
|
},
|
|
}
|
|
|
|
if perms, exists := rolePermissions[role]; exists {
|
|
return perms
|
|
}
|
|
return make(map[string]bool)
|
|
}
|