package auth import ( "os" "strings" "testing" ) func TestConfigFileProvider_Authenticate(t *testing.T) { yaml := ` provider: config_file config_file: users: - username: admin password: secret roles: [cluster_admin] - username: viewer password: pass roles: [basic_user] ` provider, err := NewConfigFileProvider([]byte(yaml)) if err != nil { t.Fatalf("NewConfigFileProvider: %v", err) } tests := []struct { name string user string pass string wantErr bool }{ {"valid admin", "admin", "secret", false}, {"valid viewer", "viewer", "pass", false}, {"wrong password", "admin", "wrong", true}, {"unknown user", "nobody", "pass", true}, {"empty user", "", "pass", true}, {"empty password", "admin", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := provider.Authenticate(tt.user, tt.pass) if (err != nil) != tt.wantErr { t.Errorf("Authenticate() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestConfigFileProvider_GetUserRoles(t *testing.T) { yaml := ` provider: config_file config_file: users: - username: admin password: admin roles: [cluster_admin, operator] - username: guest password: guest roles: [] ` provider, err := NewConfigFileProvider([]byte(yaml)) if err != nil { t.Fatalf("NewConfigFileProvider: %v", err) } if roles := provider.GetUserRoles("admin"); len(roles) != 2 { t.Errorf("admin roles = %v, want 2", roles) } if roles := provider.GetUserRoles("guest"); len(roles) != 0 { t.Errorf("guest roles = %v, want empty", roles) } if roles := provider.GetUserRoles("unknown"); roles != nil { t.Errorf("unknown roles = %v, want nil", roles) } } func TestConfigFileProvider_Name(t *testing.T) { p := &ConfigFileProvider{} if got := p.Name(); got != "config_file" { t.Errorf("Name() = %q, want %q", got, "config_file") } } func TestNewConfigFileProvider_Errors(t *testing.T) { t.Run("invalid yaml", func(t *testing.T) { _, err := NewConfigFileProvider([]byte("{{invalid")) if err == nil { t.Error("expected error for invalid yaml") } }) t.Run("no users", func(t *testing.T) { _, err := NewConfigFileProvider([]byte("provider: config_file\nconfig_file:\n users: []")) if err == nil { t.Error("expected error for empty users list") } }) // bug-1951: an auth.yaml without a config_file block must be a clear // configuration error, not a nil-pointer panic. t.Run("missing config_file block", func(t *testing.T) { _, err := NewConfigFileProvider([]byte("provider: config_file\n")) if err == nil { t.Fatal("expected error for missing config_file block") } if !strings.Contains(err.Error(), "config_file") { t.Errorf("error %q should name the missing config_file block", err) } }) t.Run("empty username", func(t *testing.T) { _, err := NewConfigFileProvider([]byte("provider: config_file\nconfig_file:\n users:\n - username: ''\n password: x")) if err == nil { t.Error("expected error for empty username") } }) } func TestPAMProvider_Name(t *testing.T) { p := &PAMProvider{} if got := p.Name(); got != "pam" { t.Errorf("Name() = %q, want %q", got, "pam") } } func TestLDAPProvider_Name(t *testing.T) { p := &LDAPProvider{} if got := p.Name(); got != "ldap" { t.Errorf("Name() = %q, want %q", got, "ldap") } if err := p.Authenticate("u", "p"); err == nil { t.Error("expected not-implemented error") } } func TestOAuth2Provider_Name(t *testing.T) { p := &OAuth2Provider{} if got := p.Name(); got != "oauth2" { t.Errorf("Name() = %q, want %q", got, "oauth2") } if err := p.Authenticate("u", "p"); err == nil { t.Error("expected not-implemented error") } } // bcrypt hashes (cost 10) used by the bug-1947 tests. const ( testS3cretHash = "$2a$10$lvIFyyQJsUB5cZz68N0Yl.9sMCtqXARtlLnRmPc//gjDJfiOzplPq" // "s3cret" testAdminHash = "$2a$10$WVwHd5Nd22UP1DfIpkekjum0yYVGctD2N.hheBP.5ZxGL92W/u5Ja" // "admin" ) // bug-1947: auth.yaml passwords may be stored as bcrypt hashes. func TestConfigFileProvider_BcryptPassword(t *testing.T) { yaml := ` provider: config_file config_file: users: - username: alice password: "` + testS3cretHash + `" roles: [basic_user] ` provider, err := NewConfigFileProvider([]byte(yaml)) if err != nil { t.Fatalf("NewConfigFileProvider: %v", err) } tests := []struct { name string pass string wantErr bool }{ {"correct password", "s3cret", false}, {"wrong password", "S3cret", true}, {"empty password", "", true}, {"the stored hash itself", testS3cretHash, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := provider.Authenticate("alice", tt.pass) if (err != nil) != tt.wantErr { t.Errorf("Authenticate() error = %v, wantErr %v", err, tt.wantErr) } }) } } // bug-1947: startup warnings for plaintext entries and known default // credentials; none for a bcrypt entry with a non-default password. func TestConfigFileProvider_SecurityWarnings(t *testing.T) { yaml := ` provider: config_file config_file: users: - username: alice password: "` + testS3cretHash + `" roles: [basic_user] - username: bob password: plain-not-default roles: [basic_user] - username: admin password: "` + testAdminHash + `" roles: [cluster_admin] ` provider, err := NewConfigFileProvider([]byte(yaml)) if err != nil { t.Fatalf("NewConfigFileProvider: %v", err) } warnings := provider.SecurityWarnings() joined := strings.Join(warnings, "\n") if !strings.Contains(joined, `"bob"`) || !strings.Contains(joined, "plaintext") { t.Errorf("expected a plaintext warning for bob, got %q", warnings) } if !strings.Contains(joined, `"admin"`) || !strings.Contains(joined, "default") { t.Errorf("expected a default-credential warning for admin, got %q", warnings) } if strings.Contains(joined, `"alice"`) { t.Errorf("alice has a bcrypt non-default password, got warning %q", warnings) } for _, w := range warnings { if strings.Contains(w, "plain-not-default") || strings.Contains(w, "$2a$") { t.Errorf("warning must not contain a password or hash: %q", w) } } } // bug-1947: the shipped auth.yaml (and its example) store only bcrypt // hashes; the dev default admin/admin still logs in (tests/regression/run.sh // relies on it) but is reported as a default credential at startup. func TestShippedAuthConfig_HashedDefaults(t *testing.T) { for _, path := range []string{"../../config/auth.yaml", "../../config/auth.yaml.example"} { t.Run(path, func(t *testing.T) { data, err := os.ReadFile(path) if err != nil { t.Fatalf("read %s: %v", path, err) } provider, err := NewConfigFileProvider(data) if err != nil { t.Fatalf("NewConfigFileProvider: %v", err) } for name, u := range provider.users { if !isBcryptHash(u.Password) { t.Errorf("user %q: shipped password is not a bcrypt hash", name) } } if err := provider.Authenticate("admin", "admin"); err != nil { t.Errorf("admin/admin dev login must keep working: %v", err) } if !strings.Contains(strings.Join(provider.SecurityWarnings(), "\n"), `"admin"`) { t.Errorf("admin/admin must be reported as a default credential") } }) } }