788 lines
33 KiB
Go
788 lines
33 KiB
Go
package orchestrator
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"scm.bstein.dev/bstein/ananke/internal/cluster"
|
|
"scm.bstein.dev/bstein/ananke/internal/config"
|
|
)
|
|
|
|
func testSecretJSON(username, password string) string {
|
|
return fmt.Sprintf(
|
|
`{"data":{"username":"%s","password":"%s"}}`,
|
|
base64.StdEncoding.EncodeToString([]byte(username)),
|
|
base64.StdEncoding.EncodeToString([]byte(password)),
|
|
)
|
|
}
|
|
|
|
func authSettings(baseURL string) config.ServiceChecklistAuthSettings {
|
|
return config.ServiceChecklistAuthSettings{
|
|
Mode: "keycloak_robotuser",
|
|
KeycloakBaseURL: baseURL,
|
|
Realm: "atlas",
|
|
RobotUsername: "robotuser",
|
|
AdminSecretNamespace: "sso",
|
|
AdminSecretName: "keycloak-admin",
|
|
AdminSecretUsernameKey: "username",
|
|
AdminSecretPasswordKey: "password",
|
|
}
|
|
}
|
|
|
|
func serveMockKeycloakAdminBrowserFlow(w http.ResponseWriter, r *http.Request) bool {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(
|
|
`<html><body><form id="kc-form-login" action="/realms/master/login-actions/authenticate?session_code=test&execution=test&client_id=security-admin-console&tab_id=test" method="post"></form></body></html>`,
|
|
))
|
|
return true
|
|
case strings.Contains(r.URL.Path, "/realms/master/login-actions/authenticate"):
|
|
http.Redirect(w, r, "/admin/master/console/", http.StatusFound)
|
|
return true
|
|
case strings.Contains(r.URL.Path, "/admin/master/console/"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("<html><body>console ok</body></html>"))
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// TestHookServiceAuthChecklistSuccess runs one orchestration or CLI step.
|
|
// Signature: TestHookServiceAuthChecklistSuccess(t *testing.T).
|
|
// Why: validates full robotuser-authenticated checklist flow with final URL and
|
|
// body markers so startup gates reflect real post-login user behavior.
|
|
func TestHookServiceAuthChecklistSuccess(t *testing.T) {
|
|
var appServer *httptest.Server
|
|
appMux := http.NewServeMux()
|
|
appMux.HandleFunc("/session/bootstrap", func(w http.ResponseWriter, _ *http.Request) {
|
|
http.SetCookie(w, &http.Cookie{Name: "robot_session", Value: "ok", Path: "/"})
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("bootstrap ok"))
|
|
})
|
|
appMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/" {
|
|
http.Redirect(w, r, "/app/home", http.StatusFound)
|
|
return
|
|
}
|
|
cookie, err := r.Cookie("robot_session")
|
|
if err != nil || strings.TrimSpace(cookie.Value) == "" {
|
|
http.Redirect(w, r, "/oauth2/sign_in", http.StatusFound)
|
|
return
|
|
}
|
|
if r.URL.Path == "/app/home" {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("OpenSearch Dashboards"))
|
|
return
|
|
}
|
|
if r.URL.Path == "/oauth2/sign_in" {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("sign in"))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNotFound)
|
|
})
|
|
appServer = httptest.NewTLSServer(appMux)
|
|
defer appServer.Close()
|
|
|
|
kcMux := http.NewServeMux()
|
|
kcMux.HandleFunc("/realms/master/protocol/openid-connect/token", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"access_token":"admin-token"}`))
|
|
})
|
|
kcMux.HandleFunc("/admin/realms/atlas/users", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`[{"id":"robot-id"}]`))
|
|
})
|
|
kcMux.HandleFunc("/admin/realms/atlas/users/robot-id/impersonation", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(fmt.Sprintf(`{"redirect":"%s/session/bootstrap"}`, appServer.URL)))
|
|
})
|
|
kcMux.HandleFunc("/realms/master/protocol/openid-connect/auth", func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
})
|
|
kcMux.HandleFunc("/realms/master/login-actions/authenticate", func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
})
|
|
kcMux.HandleFunc("/admin/master/console/", func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
})
|
|
kcServer := httptest.NewTLSServer(kcMux)
|
|
defer kcServer.Close()
|
|
|
|
cfg := lifecycleConfig(t)
|
|
cfg.Startup.ServiceChecklistAuth = authSettings(kcServer.URL)
|
|
|
|
recorder := &commandRecorder{}
|
|
base := lifecycleDispatcher(recorder)
|
|
run := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
|
|
command := name + " " + strings.Join(args, " ")
|
|
if name == "kubectl" && strings.Contains(command, "-n sso get secret keycloak-admin -o json") {
|
|
recorder.record(name, args)
|
|
return testSecretJSON("admin", "password"), nil
|
|
}
|
|
return base(ctx, timeout, name, args...)
|
|
}
|
|
orch, _ := newHookOrchestrator(t, cfg, run, run)
|
|
|
|
check := config.ServiceChecklistCheck{
|
|
Name: "logs-ui-user-session",
|
|
URL: appServer.URL + "/",
|
|
AcceptedStatuses: []int{200},
|
|
RequireRobotAuth: true,
|
|
FollowRedirects: true,
|
|
InsecureSkipTLS: true,
|
|
FinalURLContains: "/app/home",
|
|
FinalURLNotContains: "/oauth2/sign_in",
|
|
BodyContains: "OpenSearch Dashboards",
|
|
TimeoutSeconds: 5,
|
|
}
|
|
ok, detail := orch.TestHookServiceCheckReady(context.Background(), check)
|
|
if !ok {
|
|
t.Fatalf("expected authenticated checklist success, detail=%q", detail)
|
|
}
|
|
}
|
|
|
|
// TestHookServiceAuthModeAndSecretErrors runs one orchestration or CLI step.
|
|
// Signature: TestHookServiceAuthModeAndSecretErrors(t *testing.T).
|
|
// Why: covers auth mode guards and secret decode error branches to keep startup
|
|
// failures explicit when robot-auth prerequisites are missing.
|
|
func TestHookServiceAuthModeAndSecretErrors(t *testing.T) {
|
|
cfg := lifecycleConfig(t)
|
|
client := &http.Client{Timeout: time.Second}
|
|
|
|
cfgNone := lifecycleConfig(t)
|
|
cfgNone.Startup.ServiceChecklistAuth.Mode = "none"
|
|
orchNone, _ := newHookOrchestrator(t, cfgNone, nil, nil)
|
|
if err := orchNone.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected auth mode none to fail")
|
|
}
|
|
if _, err := orchNone.TestHookChecklistAuthHTTPClient(context.Background(), time.Second, false); err == nil {
|
|
t.Fatalf("expected checklist auth client init to fail when mode=none")
|
|
}
|
|
|
|
cfgBad := lifecycleConfig(t)
|
|
cfgBad.Startup.ServiceChecklistAuth.Mode = "bad-mode"
|
|
orchBad, _ := newHookOrchestrator(t, cfgBad, nil, nil)
|
|
if err := orchBad.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected unsupported auth mode to fail")
|
|
}
|
|
|
|
base := lifecycleDispatcher(&commandRecorder{})
|
|
runKubectlErr := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
|
|
if name == "kubectl" {
|
|
return "", errors.New("kubectl denied")
|
|
}
|
|
return base(ctx, timeout, name, args...)
|
|
}
|
|
orchKubectlErr, _ := newHookOrchestrator(t, cfg, runKubectlErr, runKubectlErr)
|
|
if _, err := orchKubectlErr.TestHookKubernetesSecretValue(context.Background(), "sso", "keycloak-admin", "username"); err == nil {
|
|
t.Fatalf("expected kubectl error branch")
|
|
}
|
|
if _, _, err := orchKubectlErr.TestHookKeycloakAdminCredentials(context.Background(), cfg.Startup.ServiceChecklistAuth); err == nil {
|
|
t.Fatalf("expected keycloakAdminCredentials to fail on username secret lookup")
|
|
}
|
|
if err := orchKubectlErr.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected auth session failure when secret lookup fails")
|
|
}
|
|
|
|
runBadJSON := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
|
|
if name == "kubectl" {
|
|
return "{bad", nil
|
|
}
|
|
return base(ctx, timeout, name, args...)
|
|
}
|
|
orchBadJSON, _ := newHookOrchestrator(t, cfg, runBadJSON, runBadJSON)
|
|
if _, err := orchBadJSON.TestHookKubernetesSecretValue(context.Background(), "sso", "keycloak-admin", "username"); err == nil {
|
|
t.Fatalf("expected secret decode error branch")
|
|
}
|
|
|
|
runMissingKey := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
|
|
if name == "kubectl" {
|
|
return `{"data":{"password":"cGFzcw=="}}`, nil
|
|
}
|
|
return base(ctx, timeout, name, args...)
|
|
}
|
|
orchMissingKey, _ := newHookOrchestrator(t, cfg, runMissingKey, runMissingKey)
|
|
if _, err := orchMissingKey.TestHookKubernetesSecretValue(context.Background(), "sso", "keycloak-admin", "username"); err == nil {
|
|
t.Fatalf("expected missing key branch")
|
|
}
|
|
if err := orchMissingKey.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected auth session failure when username key is missing")
|
|
}
|
|
|
|
runMissingPassword := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
|
|
if name == "kubectl" {
|
|
return `{"data":{"username":"YWRtaW4="}}`, nil
|
|
}
|
|
return base(ctx, timeout, name, args...)
|
|
}
|
|
orchMissingPassword, _ := newHookOrchestrator(t, cfg, runMissingPassword, runMissingPassword)
|
|
if _, _, err := orchMissingPassword.TestHookKeycloakAdminCredentials(context.Background(), cfg.Startup.ServiceChecklistAuth); err == nil {
|
|
t.Fatalf("expected keycloakAdminCredentials to fail on password secret lookup")
|
|
}
|
|
if err := orchMissingPassword.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected auth session failure when password key is missing")
|
|
}
|
|
|
|
runBadB64 := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
|
|
if name == "kubectl" {
|
|
return `{"data":{"username":"###"}}`, nil
|
|
}
|
|
return base(ctx, timeout, name, args...)
|
|
}
|
|
orchBadB64, _ := newHookOrchestrator(t, cfg, runBadB64, runBadB64)
|
|
if _, err := orchBadB64.TestHookKubernetesSecretValue(context.Background(), "sso", "keycloak-admin", "username"); err == nil {
|
|
t.Fatalf("expected base64 decode branch")
|
|
}
|
|
|
|
runEmptyValue := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
|
|
if name == "kubectl" {
|
|
return `{"data":{"username":"IA=="}}`, nil
|
|
}
|
|
return base(ctx, timeout, name, args...)
|
|
}
|
|
orchEmptyValue, _ := newHookOrchestrator(t, cfg, runEmptyValue, runEmptyValue)
|
|
if _, err := orchEmptyValue.TestHookKubernetesSecretValue(context.Background(), "sso", "keycloak-admin", "username"); err == nil {
|
|
t.Fatalf("expected empty decoded value branch")
|
|
}
|
|
|
|
if got := cluster.TestHookCompactHTTPBody([]byte(" hello world \n test ")); got != "hello world test" {
|
|
t.Fatalf("unexpected compact body %q", got)
|
|
}
|
|
if got := cluster.TestHookCompactHTTPBody([]byte(" \n\t ")); got != "" {
|
|
t.Fatalf("expected compact empty body, got %q", got)
|
|
}
|
|
if got := cluster.TestHookKeycloakBaseURL(config.ServiceChecklistAuthSettings{KeycloakBaseURL: "https://sso.bstein.dev/"}); got != "https://sso.bstein.dev" {
|
|
t.Fatalf("unexpected normalized base URL %q", got)
|
|
}
|
|
}
|
|
|
|
// TestHookServiceAuthHTTPErrorBranches runs one orchestration or CLI step.
|
|
// Signature: TestHookServiceAuthHTTPErrorBranches(t *testing.T).
|
|
// Why: covers token/user/impersonation parser and status branches so startup
|
|
// diagnostics remain actionable during auth failures.
|
|
func TestHookServiceAuthHTTPErrorBranches(t *testing.T) {
|
|
cfg := lifecycleConfig(t)
|
|
orch, _ := newHookOrchestrator(t, cfg, nil, nil)
|
|
client := &http.Client{Timeout: 2 * time.Second}
|
|
|
|
authBadURL := authSettings("://bad-url")
|
|
if _, err := orch.TestHookKeycloakAdminToken(context.Background(), client, authBadURL, "admin", "pw"); err == nil {
|
|
t.Fatalf("expected request-build failure for bad base URL")
|
|
}
|
|
if _, err := orch.TestHookKeycloakRobotUserID(context.Background(), client, authBadURL, "token"); err == nil {
|
|
t.Fatalf("expected robot-user request-build failure for bad base URL")
|
|
}
|
|
if _, err := orch.TestHookKeycloakImpersonationRedirect(context.Background(), client, authBadURL, "token", "robot"); err == nil {
|
|
t.Fatalf("expected impersonation request-build failure for bad base URL")
|
|
}
|
|
authRequestErr := authSettings("http://127.0.0.1:1")
|
|
if _, err := orch.TestHookKeycloakAdminToken(context.Background(), client, authRequestErr, "admin", "pw"); err == nil {
|
|
t.Fatalf("expected admin token request error branch")
|
|
}
|
|
if _, err := orch.TestHookKeycloakRobotUserID(context.Background(), client, authRequestErr, "token"); err == nil {
|
|
t.Fatalf("expected robot user request error branch")
|
|
}
|
|
if _, err := orch.TestHookKeycloakImpersonationRedirect(context.Background(), client, authRequestErr, "token", "robot"); err == nil {
|
|
t.Fatalf("expected impersonation request error branch")
|
|
}
|
|
|
|
kcError := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/token"):
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = w.Write([]byte(`{"error":"unauthorized"}`))
|
|
case strings.Contains(r.URL.Path, "/users") && strings.Contains(r.URL.RawQuery, "username=robotuser"):
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte(`{"error":"boom"}`))
|
|
default:
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
}
|
|
}))
|
|
defer kcError.Close()
|
|
authError := authSettings(kcError.URL)
|
|
if _, err := orch.TestHookKeycloakAdminToken(context.Background(), client, authError, "admin", "pw"); err == nil {
|
|
t.Fatalf("expected non-2xx token branch")
|
|
}
|
|
if _, err := orch.TestHookKeycloakRobotUserID(context.Background(), client, authError, "token"); err == nil {
|
|
t.Fatalf("expected non-2xx robot user branch")
|
|
}
|
|
|
|
kcDecode := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/token"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("not-json"))
|
|
case strings.Contains(r.URL.Path, "/users") && strings.Contains(r.URL.RawQuery, "username=robotuser"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("not-json"))
|
|
case strings.Contains(r.URL.Path, "/impersonation"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("not-json"))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer kcDecode.Close()
|
|
authDecode := authSettings(kcDecode.URL)
|
|
if _, err := orch.TestHookKeycloakAdminToken(context.Background(), client, authDecode, "admin", "pw"); err == nil {
|
|
t.Fatalf("expected token decode error branch")
|
|
}
|
|
if _, err := orch.TestHookKeycloakRobotUserID(context.Background(), client, authDecode, "token"); err == nil {
|
|
t.Fatalf("expected robot user decode error branch")
|
|
}
|
|
if _, err := orch.TestHookKeycloakImpersonationRedirect(context.Background(), client, authDecode, "token", "robot"); err == nil {
|
|
t.Fatalf("expected impersonation decode error branch")
|
|
}
|
|
|
|
kcMissing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/token"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"access_token":""}`))
|
|
case strings.Contains(r.URL.Path, "/users") && strings.Contains(r.URL.RawQuery, "username=robotuser"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`[]`))
|
|
case strings.Contains(r.URL.Path, "/impersonation"):
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(`{"error":"bad request"}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer kcMissing.Close()
|
|
authMissing := authSettings(kcMissing.URL)
|
|
if _, err := orch.TestHookKeycloakAdminToken(context.Background(), client, authMissing, "admin", "pw"); err == nil {
|
|
t.Fatalf("expected missing access_token branch")
|
|
}
|
|
if _, err := orch.TestHookKeycloakRobotUserID(context.Background(), client, authMissing, "token"); err == nil {
|
|
t.Fatalf("expected missing robot user branch")
|
|
}
|
|
if _, err := orch.TestHookKeycloakImpersonationRedirect(context.Background(), client, authMissing, "token", "robot"); err == nil {
|
|
t.Fatalf("expected impersonation non-2xx branch")
|
|
}
|
|
}
|
|
|
|
// TestHookKeycloakAdminBrowserLoginAndParserBranches runs one orchestration or CLI step.
|
|
// Signature: TestHookKeycloakAdminBrowserLoginAndParserBranches(t *testing.T).
|
|
// Why: covers admin-browser bootstrap and form-action parser branches that
|
|
// drive robotuser-authenticated startup checks.
|
|
func TestHookKeycloakAdminBrowserLoginAndParserBranches(t *testing.T) {
|
|
cfg := lifecycleConfig(t)
|
|
orch, _ := newHookOrchestrator(t, cfg, nil, nil)
|
|
client := &http.Client{Timeout: 2 * time.Second}
|
|
|
|
t.Run("invalid base url build failure", func(t *testing.T) {
|
|
auth := authSettings("://bad-url")
|
|
if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, auth, "admin", "pw"); err == nil {
|
|
t.Fatalf("expected browser login request-build failure")
|
|
}
|
|
})
|
|
|
|
t.Run("auth page non-2xx", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth") {
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
_, _ = w.Write([]byte("bad gateway"))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer kc.Close()
|
|
if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, authSettings(kc.URL), "admin", "pw"); err == nil {
|
|
t.Fatalf("expected non-2xx auth page failure")
|
|
}
|
|
})
|
|
|
|
t.Run("login submit request failure", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth") {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><body><form id="kc-form-login" action="http://127.0.0.1:1/login"></form></body></html>`))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer kc.Close()
|
|
if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, authSettings(kc.URL), "admin", "pw"); err == nil {
|
|
t.Fatalf("expected login submit request failure")
|
|
}
|
|
})
|
|
|
|
t.Run("build login request failure", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth") {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><body><form id="kc-form-login" action="http://[::1"></form></body></html>`))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer kc.Close()
|
|
if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, authSettings(kc.URL), "admin", "pw"); err == nil {
|
|
t.Fatalf("expected login request-build failure")
|
|
}
|
|
})
|
|
|
|
t.Run("login submit returns 5xx", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><body><form id="kc-form-login" action="/realms/master/login-actions/authenticate"></form></body></html>`))
|
|
case strings.Contains(r.URL.Path, "/realms/master/login-actions/authenticate"):
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte("boom"))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer kc.Close()
|
|
if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, authSettings(kc.URL), "admin", "pw"); err == nil {
|
|
t.Fatalf("expected login 5xx branch")
|
|
}
|
|
})
|
|
|
|
t.Run("login success path", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth"):
|
|
_, _ = w.Write([]byte(`<html><body><form id="kc-form-login" action="/realms/master/login-actions/authenticate"></form></body></html>`))
|
|
case strings.Contains(r.URL.Path, "/realms/master/login-actions/authenticate"):
|
|
http.Redirect(w, r, "/admin/master/console/", http.StatusFound)
|
|
case strings.Contains(r.URL.Path, "/admin/master/console/"):
|
|
_, _ = w.Write([]byte("<html><body>console ok</body></html>"))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer kc.Close()
|
|
if err := orch.TestHookKeycloakAdminBrowserLogin(context.Background(), client, authSettings(kc.URL), "admin", "pw"); err != nil {
|
|
t.Fatalf("expected browser login success, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("form action parser absolute and invalid", func(t *testing.T) {
|
|
action, err := cluster.TestHookKeycloakLoginFormAction(
|
|
`<html><form id="kc-form-login" action="https://example.test/login"></form></html>`,
|
|
"https://sso.bstein.dev",
|
|
)
|
|
if err != nil || action != "https://example.test/login" {
|
|
t.Fatalf("expected absolute action parse success, action=%q err=%v", action, err)
|
|
}
|
|
if _, err := cluster.TestHookKeycloakLoginFormAction(
|
|
`<html><form id="kc-form-login" action="javascript:void(0)"></form></html>`,
|
|
"https://sso.bstein.dev",
|
|
); err == nil {
|
|
t.Fatalf("expected unsupported action format failure")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestHookServiceChecklistProbeBranches runs one orchestration or CLI step.
|
|
// Signature: TestHookServiceChecklistProbeBranches(t *testing.T).
|
|
// Why: exercises redirect + final-url probe branches, including robot-auth
|
|
// initialization failures and redirect suppression behavior.
|
|
func TestHookServiceChecklistProbeBranches(t *testing.T) {
|
|
cfg := lifecycleConfig(t)
|
|
cfg.Startup.ServiceChecklistAuth.Mode = "none"
|
|
orch, _ := newHookOrchestrator(t, cfg, nil, nil)
|
|
if _, _, _, _, err := orch.TestHookHTTPChecklistProbeWithLocation(context.Background(), config.ServiceChecklistCheck{
|
|
URL: "https://example.invalid/",
|
|
RequireRobotAuth: true,
|
|
TimeoutSeconds: 1,
|
|
}); err == nil {
|
|
t.Fatalf("expected robot auth initialization failure when mode=none")
|
|
}
|
|
|
|
redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/next", http.StatusFound)
|
|
}))
|
|
defer redirectServer.Close()
|
|
|
|
orchNoAuth, _ := newHookOrchestrator(t, lifecycleConfig(t), nil, nil)
|
|
status, _, location, finalURL, err := orchNoAuth.TestHookHTTPChecklistProbeWithLocation(context.Background(), config.ServiceChecklistCheck{
|
|
URL: redirectServer.URL,
|
|
FollowRedirects: false,
|
|
TimeoutSeconds: 2,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected redirect probe error: %v", err)
|
|
}
|
|
if status != http.StatusFound {
|
|
t.Fatalf("expected 302 status when redirects disabled, got %d", status)
|
|
}
|
|
if !strings.Contains(location, "/next") {
|
|
t.Fatalf("expected location header for redirect response, got %q", location)
|
|
}
|
|
if !strings.Contains(finalURL, redirectServer.URL) {
|
|
t.Fatalf("expected final URL to remain original request URL, got %q", finalURL)
|
|
}
|
|
}
|
|
|
|
// TestHookAuthenticateRobotChecklistSessionFailureStages runs one orchestration or CLI step.
|
|
// Signature: TestHookAuthenticateRobotChecklistSessionFailureStages(t *testing.T).
|
|
// Why: drives authenticateRobotChecklistSession through downstream error stages
|
|
// (robot lookup, impersonation, redirect-build, redirect-request) to maintain
|
|
// resilient startup diagnostics.
|
|
func TestHookAuthenticateRobotChecklistSessionFailureStages(t *testing.T) {
|
|
client := &http.Client{Timeout: 3 * time.Second}
|
|
recorder := &commandRecorder{}
|
|
base := lifecycleDispatcher(recorder)
|
|
secretRun := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
|
|
command := name + " " + strings.Join(args, " ")
|
|
if name == "kubectl" && strings.Contains(command, "-n sso get secret keycloak-admin -o json") {
|
|
return testSecretJSON("admin", "password"), nil
|
|
}
|
|
return base(ctx, timeout, name, args...)
|
|
}
|
|
|
|
t.Run("robot-user lookup failure", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/token"):
|
|
_, _ = w.Write([]byte(`{"access_token":"admin-token"}`))
|
|
case strings.Contains(r.URL.Path, "/users"):
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
_, _ = w.Write([]byte(`{"error":"lookup failed"}`))
|
|
default:
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
}))
|
|
defer kc.Close()
|
|
cfg := lifecycleConfig(t)
|
|
cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL)
|
|
orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun)
|
|
if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected robot-user lookup failure branch")
|
|
}
|
|
})
|
|
|
|
t.Run("impersonation failure", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/token"):
|
|
_, _ = w.Write([]byte(`{"access_token":"admin-token"}`))
|
|
case strings.Contains(r.URL.Path, "/users"):
|
|
_, _ = w.Write([]byte(`[{"id":"robot-id"}]`))
|
|
case strings.Contains(r.URL.Path, "/impersonation"):
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
_, _ = w.Write([]byte(`{"error":"impersonation failed"}`))
|
|
default:
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
}))
|
|
defer kc.Close()
|
|
cfg := lifecycleConfig(t)
|
|
cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL)
|
|
orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun)
|
|
if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected impersonation failure branch")
|
|
}
|
|
})
|
|
|
|
t.Run("redirect url build failure", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/token"):
|
|
_, _ = w.Write([]byte(`{"access_token":"admin-token"}`))
|
|
case strings.Contains(r.URL.Path, "/users"):
|
|
_, _ = w.Write([]byte(`[{"id":"robot-id"}]`))
|
|
case strings.Contains(r.URL.Path, "/impersonation"):
|
|
_, _ = w.Write([]byte(`{"redirect":"://bad"}`))
|
|
default:
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
}))
|
|
defer kc.Close()
|
|
cfg := lifecycleConfig(t)
|
|
cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL)
|
|
orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun)
|
|
if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected redirect request-build failure branch")
|
|
}
|
|
})
|
|
|
|
t.Run("redirect request failure", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/token"):
|
|
_, _ = w.Write([]byte(`{"access_token":"admin-token"}`))
|
|
case strings.Contains(r.URL.Path, "/users"):
|
|
_, _ = w.Write([]byte(`[{"id":"robot-id"}]`))
|
|
case strings.Contains(r.URL.Path, "/impersonation"):
|
|
_, _ = w.Write([]byte(`{"redirect":"http://127.0.0.1:1/nowhere"}`))
|
|
default:
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
}))
|
|
defer kc.Close()
|
|
cfg := lifecycleConfig(t)
|
|
cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL)
|
|
orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun)
|
|
if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected redirect request failure branch")
|
|
}
|
|
})
|
|
|
|
t.Run("admin login page missing form action", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("<html><body>no form</body></html>"))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer kc.Close()
|
|
cfg := lifecycleConfig(t)
|
|
cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL)
|
|
orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun)
|
|
if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected admin login form parse failure branch")
|
|
}
|
|
})
|
|
|
|
t.Run("admin login remains on login form", func(t *testing.T) {
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/realms/master/protocol/openid-connect/auth"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(
|
|
`<html><body><form id="kc-form-login" action="/realms/master/login-actions/authenticate?session_code=test"></form></body></html>`,
|
|
))
|
|
case strings.Contains(r.URL.Path, "/realms/master/login-actions/authenticate"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><body><form id="kc-form-login" action="/retry"></form></body></html>`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer kc.Close()
|
|
cfg := lifecycleConfig(t)
|
|
cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL)
|
|
orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun)
|
|
if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected admin login failure branch when login form persists")
|
|
}
|
|
})
|
|
|
|
t.Run("robot bootstrap ends on keycloak login flow", func(t *testing.T) {
|
|
baseURL := ""
|
|
kc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/token"):
|
|
_, _ = w.Write([]byte(`{"access_token":"admin-token"}`))
|
|
case strings.Contains(r.URL.Path, "/users"):
|
|
_, _ = w.Write([]byte(`[{"id":"robot-id"}]`))
|
|
case strings.Contains(r.URL.Path, "/impersonation"):
|
|
_, _ = w.Write([]byte(fmt.Sprintf(`{"redirect":"%s/realms/atlas/protocol/openid-connect/auth?client_id=logs"}`, baseURL)))
|
|
case strings.Contains(r.URL.Path, "/realms/atlas/protocol/openid-connect/auth"):
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><body><form id="kc-form-login" action="/realms/atlas/login-actions/authenticate"></form></body></html>`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer kc.Close()
|
|
baseURL = kc.URL
|
|
cfg := lifecycleConfig(t)
|
|
cfg.Startup.ServiceChecklistAuth = authSettings(kc.URL)
|
|
orch, _ := newHookOrchestrator(t, cfg, secretRun, secretRun)
|
|
if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), client); err == nil {
|
|
t.Fatalf("expected robot bootstrap final-url auth-flow failure")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestHookServiceAuthFallbackRedirect runs one orchestration or CLI step.
|
|
// Signature: TestHookServiceAuthFallbackRedirect(t *testing.T).
|
|
// Why: covers empty impersonation redirect fallback to realm account URL so
|
|
// session bootstrap is resilient to Keycloak response shape differences.
|
|
func TestHookServiceAuthFallbackRedirect(t *testing.T) {
|
|
kcMux := http.NewServeMux()
|
|
kcMux.HandleFunc("/realms/master/protocol/openid-connect/token", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"access_token":"admin-token"}`))
|
|
})
|
|
kcMux.HandleFunc("/admin/realms/atlas/users", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`[{"id":"robot-id"}]`))
|
|
})
|
|
kcMux.HandleFunc("/admin/realms/atlas/users/robot-id/impersonation", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"redirect":""}`))
|
|
})
|
|
kcMux.HandleFunc("/realms/atlas/account/", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("account ok"))
|
|
})
|
|
kcMux.HandleFunc("/realms/master/protocol/openid-connect/auth", func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
})
|
|
kcMux.HandleFunc("/realms/master/login-actions/authenticate", func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
})
|
|
kcMux.HandleFunc("/admin/master/console/", func(w http.ResponseWriter, r *http.Request) {
|
|
if serveMockKeycloakAdminBrowserFlow(w, r) {
|
|
return
|
|
}
|
|
})
|
|
kcServer := httptest.NewTLSServer(kcMux)
|
|
defer kcServer.Close()
|
|
|
|
cfg := lifecycleConfig(t)
|
|
cfg.Startup.ServiceChecklistAuth = authSettings(kcServer.URL)
|
|
recorder := &commandRecorder{}
|
|
base := lifecycleDispatcher(recorder)
|
|
run := func(ctx context.Context, timeout time.Duration, name string, args ...string) (string, error) {
|
|
command := name + " " + strings.Join(args, " ")
|
|
if name == "kubectl" && strings.Contains(command, "-n sso get secret keycloak-admin -o json") {
|
|
return testSecretJSON("admin", "password"), nil
|
|
}
|
|
return base(ctx, timeout, name, args...)
|
|
}
|
|
orch, _ := newHookOrchestrator(t, cfg, run, run)
|
|
if err := orch.TestHookAuthenticateRobotChecklistSession(context.Background(), &http.Client{Timeout: 4 * time.Second, Transport: &http.Transport{}}); err == nil {
|
|
t.Fatalf("expected auth bootstrap without TLS skip to fail against TLS test server")
|
|
}
|
|
if _, err := orch.TestHookChecklistAuthHTTPClient(context.Background(), 4*time.Second, true); err != nil {
|
|
t.Fatalf("expected checklist auth client fallback redirect path success, got %v", err)
|
|
}
|
|
}
|