startup: require real keycloak admin session before robotuser checks
This commit is contained in:
parent
c2c79e5821
commit
af2d94a53b
@ -6,10 +6,12 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
neturl "net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -32,15 +34,14 @@ type kubernetesSecret struct {
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
var keycloakLoginFormActionPattern = regexp.MustCompile(`(?is)<form[^>]*id=["']kc-form-login["'][^>]*action=["']([^"']+)["']`)
|
||||
|
||||
// checklistAuthHTTPClient runs one orchestration or CLI step.
|
||||
// Signature: (o *Orchestrator) checklistAuthHTTPClient(ctx context.Context, timeout time.Duration, insecureSkipTLS bool) (*http.Client, error).
|
||||
// Why: startup checklist checks that require real user behavior need an
|
||||
// authenticated robotuser browser-like session before probing service pages.
|
||||
func (o *Orchestrator) checklistAuthHTTPClient(ctx context.Context, timeout time.Duration, insecureSkipTLS bool) (*http.Client, error) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create cookie jar: %w", err)
|
||||
}
|
||||
jar, _ := cookiejar.New(nil)
|
||||
transport := &http.Transport{}
|
||||
if insecureSkipTLS {
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
@ -74,6 +75,9 @@ func (o *Orchestrator) authenticateRobotChecklistSession(ctx context.Context, cl
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.keycloakAdminBrowserLogin(ctx, client, auth, adminUser, adminPassword); err != nil {
|
||||
return fmt.Errorf("initialize keycloak admin browser session: %w", err)
|
||||
}
|
||||
adminToken, err := o.keycloakAdminToken(ctx, client, auth, adminUser, adminPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -101,6 +105,74 @@ func (o *Orchestrator) authenticateRobotChecklistSession(ctx context.Context, cl
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1024))
|
||||
finalURL := ""
|
||||
if resp.Request != nil && resp.Request.URL != nil {
|
||||
finalURL = strings.TrimSpace(resp.Request.URL.String())
|
||||
}
|
||||
if strings.Contains(finalURL, "/protocol/openid-connect/auth") || strings.Contains(finalURL, "/login-actions/authenticate") {
|
||||
return fmt.Errorf("robot session bootstrap ended on keycloak login flow: %s", finalURL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// keycloakAdminBrowserLogin runs one orchestration or CLI step.
|
||||
// Signature: (o *Orchestrator) keycloakAdminBrowserLogin(ctx context.Context, client *http.Client, auth config.ServiceChecklistAuthSettings, adminUser string, adminPassword string) error.
|
||||
// Why: Keycloak impersonation only yields a usable robot session cookie when the
|
||||
// client already has a real admin browser session; token-only API calls are not
|
||||
// sufficient for downstream OIDC-gated service checks.
|
||||
func (o *Orchestrator) keycloakAdminBrowserLogin(ctx context.Context, client *http.Client, auth config.ServiceChecklistAuthSettings, adminUser string, adminPassword string) error {
|
||||
baseURL := keycloakBaseURL(auth)
|
||||
authURL := baseURL + "/realms/master/protocol/openid-connect/auth?" + keycloakAdminConsoleAuthQuery(baseURL).Encode()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, authURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build keycloak admin auth request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "ananke/startup-checklist")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request keycloak admin auth page: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512*1024))
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return fmt.Errorf("keycloak admin auth page request failed status=%d body=%q", resp.StatusCode, compactHTTPBody(body))
|
||||
}
|
||||
actionURL, err := keycloakLoginFormAction(string(body), baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
form := neturl.Values{}
|
||||
form.Set("username", adminUser)
|
||||
form.Set("password", adminPassword)
|
||||
form.Set("credentialId", "")
|
||||
|
||||
loginReq, err := http.NewRequestWithContext(ctx, http.MethodPost, actionURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("build keycloak admin login request: %w", err)
|
||||
}
|
||||
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
loginReq.Header.Set("User-Agent", "ananke/startup-checklist")
|
||||
|
||||
loginResp, err := client.Do(loginReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request keycloak admin login submit: %w", err)
|
||||
}
|
||||
defer loginResp.Body.Close()
|
||||
loginBody, _ := io.ReadAll(io.LimitReader(loginResp.Body, 512*1024))
|
||||
finalURL := ""
|
||||
if loginResp.Request != nil && loginResp.Request.URL != nil {
|
||||
finalURL = strings.TrimSpace(loginResp.Request.URL.String())
|
||||
}
|
||||
if loginResp.StatusCode >= 500 {
|
||||
return fmt.Errorf("keycloak admin login failed status=%d body=%q", loginResp.StatusCode, compactHTTPBody(loginBody))
|
||||
}
|
||||
if strings.Contains(finalURL, "/login-actions/authenticate") || strings.Contains(finalURL, "/protocol/openid-connect/auth") {
|
||||
return fmt.Errorf("keycloak admin login did not complete (final_url=%q)", finalURL)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(string(loginBody)), "kc-form-login") {
|
||||
return fmt.Errorf("keycloak admin login form still present after submit")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -273,6 +345,42 @@ func keycloakBaseURL(auth config.ServiceChecklistAuthSettings) string {
|
||||
return strings.TrimRight(strings.TrimSpace(auth.KeycloakBaseURL), "/")
|
||||
}
|
||||
|
||||
// keycloakAdminConsoleAuthQuery runs one orchestration or CLI step.
|
||||
// Signature: keycloakAdminConsoleAuthQuery() neturl.Values.
|
||||
// Why: centralizes required Keycloak admin-console auth parameters, including
|
||||
// PKCE fields required by current Keycloak defaults.
|
||||
func keycloakAdminConsoleAuthQuery(baseURL string) neturl.Values {
|
||||
query := neturl.Values{}
|
||||
query.Set("client_id", "security-admin-console")
|
||||
query.Set("redirect_uri", strings.TrimRight(strings.TrimSpace(baseURL), "/")+"/admin/master/console/")
|
||||
query.Set("response_type", "code")
|
||||
query.Set("scope", "openid")
|
||||
query.Set("state", "ananke-startup-checklist")
|
||||
query.Set("nonce", "ananke-startup-checklist")
|
||||
query.Set("code_challenge_method", "S256")
|
||||
query.Set("code_challenge", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
return query
|
||||
}
|
||||
|
||||
// keycloakLoginFormAction runs one orchestration or CLI step.
|
||||
// Signature: keycloakLoginFormAction(page string, baseURL string) (string, error).
|
||||
// Why: Keycloak's login form action carries session-bound query params and must
|
||||
// be parsed from the rendered page before posting credentials.
|
||||
func keycloakLoginFormAction(page string, baseURL string) (string, error) {
|
||||
matches := keycloakLoginFormActionPattern.FindStringSubmatch(page)
|
||||
if len(matches) < 2 || strings.TrimSpace(matches[1]) == "" {
|
||||
return "", fmt.Errorf("keycloak login page missing kc-form-login action")
|
||||
}
|
||||
action := html.UnescapeString(strings.TrimSpace(matches[1]))
|
||||
if strings.HasPrefix(action, "/") {
|
||||
return strings.TrimRight(strings.TrimSpace(baseURL), "/") + action, nil
|
||||
}
|
||||
if strings.HasPrefix(action, "http://") || strings.HasPrefix(action, "https://") {
|
||||
return action, nil
|
||||
}
|
||||
return "", fmt.Errorf("keycloak login action uses unsupported format %q", action)
|
||||
}
|
||||
|
||||
// compactHTTPBody runs one orchestration or CLI step.
|
||||
// Signature: compactHTTPBody(raw []byte) string.
|
||||
// Why: checklist auth errors should include a readable body summary without
|
||||
|
||||
@ -57,6 +57,13 @@ func (o *Orchestrator) TestHookKeycloakImpersonationRedirect(ctx context.Context
|
||||
return o.keycloakImpersonationRedirect(ctx, client, auth, adminToken, robotUserID)
|
||||
}
|
||||
|
||||
// TestHookKeycloakAdminBrowserLogin runs one orchestration or CLI step.
|
||||
// Signature: (o *Orchestrator) TestHookKeycloakAdminBrowserLogin(ctx context.Context, client *http.Client, auth config.ServiceChecklistAuthSettings, adminUser string, adminPassword string) error.
|
||||
// Why: exposes browser-session bootstrap internals to top-level tests.
|
||||
func (o *Orchestrator) TestHookKeycloakAdminBrowserLogin(ctx context.Context, client *http.Client, auth config.ServiceChecklistAuthSettings, adminUser string, adminPassword string) error {
|
||||
return o.keycloakAdminBrowserLogin(ctx, client, auth, adminUser, adminPassword)
|
||||
}
|
||||
|
||||
// TestHookHTTPChecklistProbeWithLocation runs one orchestration or CLI step.
|
||||
// Signature: (o *Orchestrator) TestHookHTTPChecklistProbeWithLocation(ctx context.Context, check config.ServiceChecklistCheck) (int, string, string, string, error).
|
||||
// Why: exposes redirect-aware checklist probe internals to top-level tests.
|
||||
@ -77,3 +84,10 @@ func TestHookKeycloakBaseURL(auth config.ServiceChecklistAuthSettings) string {
|
||||
func TestHookCompactHTTPBody(raw []byte) string {
|
||||
return compactHTTPBody(raw)
|
||||
}
|
||||
|
||||
// TestHookKeycloakLoginFormAction runs one orchestration or CLI step.
|
||||
// Signature: TestHookKeycloakLoginFormAction(page string, baseURL string) (string, error).
|
||||
// Why: exposes login-form action parser internals to top-level tests.
|
||||
func TestHookKeycloakLoginFormAction(page string, baseURL string) (string, error) {
|
||||
return keycloakLoginFormAction(page, baseURL)
|
||||
}
|
||||
|
||||
@ -36,6 +36,26 @@ func authSettings(baseURL string) config.ServiceChecklistAuthSettings {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@ -86,6 +106,21 @@ func TestHookServiceAuthChecklistSuccess(t *testing.T) {
|
||||
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()
|
||||
|
||||
@ -343,6 +378,122 @@ func TestHookServiceAuthHTTPErrorBranches(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -403,6 +554,9 @@ func TestHookAuthenticateRobotChecklistSessionFailureStages(t *testing.T) {
|
||||
|
||||
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"}`))
|
||||
@ -424,6 +578,9 @@ func TestHookAuthenticateRobotChecklistSessionFailureStages(t *testing.T) {
|
||||
|
||||
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"}`))
|
||||
@ -447,6 +604,9 @@ func TestHookAuthenticateRobotChecklistSessionFailureStages(t *testing.T) {
|
||||
|
||||
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"}`))
|
||||
@ -469,6 +629,9 @@ func TestHookAuthenticateRobotChecklistSessionFailureStages(t *testing.T) {
|
||||
|
||||
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"}`))
|
||||
@ -488,6 +651,79 @@ func TestHookAuthenticateRobotChecklistSessionFailureStages(t *testing.T) {
|
||||
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.
|
||||
@ -512,6 +748,21 @@ func TestHookServiceAuthFallbackRedirect(t *testing.T) {
|
||||
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()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user