- 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.
287 lines
6.4 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|