fetch_ml/tests/unit/auth/api_key_test.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

287 lines
6.4 KiB
Go

package auth
import (
"testing"
"github.com/jfraeys/fetch_ml/internal/auth"
)
func TestGenerateAPIKey(t *testing.T) {
t.Parallel() // Enable parallel execution
key1 := auth.GenerateAPIKey()
if len(key1) != 64 { // 32 bytes = 64 hex chars
t.Errorf("Expected key length 64, got %d", len(key1))
}
// Test uniqueness
key2 := auth.GenerateAPIKey()
if key1 == key2 {
t.Error("Generated keys should be unique")
}
}
func TestUserHasPermission(t *testing.T) {
t.Parallel()
tests := []struct {
name string
user *auth.User
permission string
want bool
}{
{
name: "wildcard grants all",
user: &auth.User{Permissions: map[string]bool{"*": true}},
want: true,
},
{
name: "direct permission match",
user: &auth.User{Permissions: map[string]bool{"jobs:create": true}},
permission: "jobs:create",
want: true,
},
{
name: "hierarchical permission match",
user: &auth.User{Permissions: map[string]bool{"jobs": true}},
permission: "jobs:create",
want: true,
},
{
name: "missing permission",
user: &auth.User{Permissions: map[string]bool{"jobs:read": true}},
permission: "jobs:create",
want: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := tt.user.HasPermission(tt.permission); got != tt.want {
t.Fatalf("HasPermission(%q) = %v, want %v", tt.permission, got, tt.want)
}
})
}
}
func TestUserHasRole(t *testing.T) {
t.Parallel()
user := &auth.User{
Roles: []string{"admin", "data_scientist"},
}
if !user.HasRole("admin") {
t.Fatal("expected admin role to be present")
}
if user.HasRole("operator") {
t.Fatal("did not expect operator role to be present")
}
}
func TestHashAPIKey(t *testing.T) {
t.Parallel() // Enable parallel execution
key := "test-key-123"
hash := auth.HashAPIKey(key)
if len(hash) != 64 { // SHA256 = 64 hex chars
t.Errorf("Expected hash length 64, got %d", len(hash))
}
// Test consistency
hash2 := auth.HashAPIKey(key)
if hash != hash2 {
t.Error("Hash should be consistent for same key")
}
// Test different keys produce different hashes
hash3 := auth.HashAPIKey("different-key")
if hash == hash3 {
t.Error("Different keys should produce different hashes")
}
}
func TestHashAPIKeyKnownValues(t *testing.T) {
t.Parallel()
tests := []struct {
name string
key string
expected string
}{
{
name: "password hash",
key: "password",
expected: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
},
{
name: "test hash",
key: "test",
expected: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := auth.HashAPIKey(tt.key); got != tt.expected {
t.Fatalf("HashAPIKey(%q) = %s, want %s", tt.key, got, tt.expected)
}
})
}
}
func TestHashAPIKeyConsistency(t *testing.T) {
t.Parallel()
key := "consistency-key"
hash1 := auth.HashAPIKey(key)
hash2 := auth.HashAPIKey(key)
if hash1 != hash2 {
t.Fatalf("HashAPIKey() not deterministic: %s vs %s", hash1, hash2)
}
if len(hash1) != 64 {
t.Fatalf("HashAPIKey() length = %d, want 64", len(hash1))
}
}
func TestValidateAPIKey(t *testing.T) {
t.Parallel() // Enable parallel execution
config := auth.Config{
Enabled: true,
APIKeys: map[auth.Username]auth.APIKeyEntry{
"admin": {
Hash: auth.APIKeyHash(auth.HashAPIKey("admin-key")),
Admin: true,
},
"data_scientist": {
Hash: auth.APIKeyHash(auth.HashAPIKey("ds-key")),
Admin: false,
},
},
}
tests := []struct {
name string
apiKey string
wantErr bool
wantUser string
wantAdmin bool
}{
{
name: "valid admin key",
apiKey: "admin-key",
wantErr: false,
wantUser: "admin",
wantAdmin: true,
},
{
name: "valid user key",
apiKey: "ds-key",
wantErr: false,
wantUser: "data_scientist",
wantAdmin: false,
},
{
name: "invalid key",
apiKey: "wrong-key",
wantErr: true,
},
{
name: "empty key",
apiKey: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, err := config.ValidateAPIKey(tt.apiKey)
if tt.wantErr {
if err == nil {
t.Error("Expected error but got none")
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
if user.Name != tt.wantUser {
t.Errorf("Expected user %s, got %s", tt.wantUser, user.Name)
}
if user.Admin != tt.wantAdmin {
t.Errorf("Expected admin %v, got %v", tt.wantAdmin, user.Admin)
}
})
}
}
func TestValidateAPIKeyAuthDisabled(t *testing.T) {
t.Setenv("FETCH_ML_ALLOW_INSECURE_AUTH", "1")
defer t.Setenv("FETCH_ML_ALLOW_INSECURE_AUTH", "")
config := auth.Config{
Enabled: false,
APIKeys: map[auth.Username]auth.APIKeyEntry{}, // Empty
}
user, err := config.ValidateAPIKey("any-key")
if err != nil {
t.Errorf("Unexpected error when auth disabled: %v", err)
}
if user == nil {
t.Fatal("Expected user, got nil")
}
if user.Name != "default" {
t.Errorf("Expected default user, got %s", user.Name)
}
if !user.Admin {
t.Error("Default user should be admin")
}
}
func TestAdminDetection(t *testing.T) {
t.Parallel() // Enable parallel execution
config := auth.Config{
Enabled: true,
APIKeys: map[auth.Username]auth.APIKeyEntry{
"admin": {Hash: auth.APIKeyHash(auth.HashAPIKey("key1")), Admin: true},
"admin_user": {Hash: auth.APIKeyHash(auth.HashAPIKey("key2")), Admin: true},
"superadmin": {Hash: auth.APIKeyHash(auth.HashAPIKey("key3")), Admin: true},
"regular": {Hash: auth.APIKeyHash(auth.HashAPIKey("key4")), Admin: false},
"user_admin": {Hash: auth.APIKeyHash(auth.HashAPIKey("key5")), Admin: false},
},
}
tests := []struct {
apiKey string
expected bool
}{
{"key1", true}, // admin
{"key2", true}, // admin_user
{"key3", true}, // superadmin
{"key4", false}, // regular
{"key5", false}, // user_admin (not admin based on explicit flag)
}
for _, tt := range tests {
t.Run(tt.apiKey, func(t *testing.T) {
user, err := config.ValidateAPIKey(tt.apiKey)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if user.Admin != tt.expected {
t.Errorf("Expected admin=%v for key %s, got %v", tt.expected, tt.apiKey, user.Admin)
}
})
}
}