Files
mesh-catalog/modules/keycloak/cmd/keycloak-provider/live_retire_test.go
T
jochen e68ef88333 Retire a consumer the mesh stops asking for, and delete only on a person's word (hq ADR 0230)
The hourly release of ADR 0229's brake still ended in the mesh acting alone on
a mistake. A consumer now stays active until the same unasked set holds for
five passes, waits for a person past three or half of those held, is disabled
and marked rather than withdrawn, comes back as it was when asked again, and is
deleted only through the provider's delete tool. The backend keeps the mark, so
a restart forgets nothing and finds what was withdrawn before.
2026-10-06 13:54:10 +02:00

109 lines
3.6 KiB
Go

package main
// Retirement against a real Keycloak (novox/hq ADR 0230). Skipped unless MESH_KEYCLOAK_LIVE_URL reaches
// a throwaway Keycloak whose master admin's password is MESH_KEYCLOAK_LIVE_PASSWORD, e.g.:
//
// docker run -d --rm --name kc-retire -p 127.0.0.1:18081:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
// -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.0.8 start-dev
// MESH_KEYCLOAK_LIVE_URL=http://127.0.0.1:18081 MESH_KEYCLOAK_LIVE_PASSWORD=admin go test -run LiveRetire ./...
//
// It proves what the fake cannot: a retired client is refused by Keycloak itself at the authorization
// endpoint, keeps its secret and redirects, is served again once enabled, and is deleted only once
// retired.
import (
"net/http"
"net/url"
"os"
"testing"
"time"
)
func TestLiveRetirement(t *testing.T) {
base, pw := os.Getenv("MESH_KEYCLOAK_LIVE_URL"), os.Getenv("MESH_KEYCLOAK_LIVE_PASSWORD")
if base == "" || pw == "" {
t.Skip("no MESH_KEYCLOAK_LIVE_URL / MESH_KEYCLOAK_LIVE_PASSWORD")
}
kc := NewClient(base, "admin", func() (string, error) { return pw, nil }, "master")
o := OidcClients{KC: kc, Realm: "master"}
p := grafana("live-secret", nil)
p.As = "mesh_live_retire"
t.Cleanup(func() {
if found, _ := kc.FindClient(ctx, "master", p.As); found != nil {
id, _ := found["id"].(string)
kc.DeleteClientByID(ctx, "master", id)
}
})
if _, err := o.Ensure(ctx, p); err != nil {
t.Fatal(err)
}
// The authorization endpoint is where a consumer's users arrive: 200 with a login page while the
// client is enabled, refused while it is not.
authorize := func() int {
q := url.Values{"client_id": {p.As}, "response_type": {"code"}, "scope": {"openid"},
"redirect_uri": {"https://grafana.example.org/login/generic_oauth"}}
resp, err := (&http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).
Get(base + "/realms/master/protocol/openid-connect/auth?" + q.Encode())
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
return resp.StatusCode
}
if code := authorize(); code != 200 {
t.Fatalf("an enabled client is refused: %d", code)
}
mustHold(t, o, p, true)
at := time.Now().UTC().Truncate(time.Second)
if done, err := o.Retire(ctx, p.As, "the mesh stopped asking for it", at); done != "retired" || err != nil {
t.Fatal(done, err)
}
if code := authorize(); code == 200 {
t.Fatal("a retired client is still served")
}
// Retired, so a person may delete it.
if err := o.Delete(ctx, p.As); err != nil {
t.Fatal(err)
}
// Made again, retired, listed, and enabled again as it was.
if _, err := o.Ensure(ctx, p); err != nil {
t.Fatal(err)
}
o.Retire(ctx, p.As, "again", at)
inv, err := o.Inventory(ctx)
if err != nil {
t.Fatal(err)
}
found := false
for _, r := range inv.Retired {
found = found || (r.Consumer == p.As && r.RetiredAt.Equal(at) && r.Why == "again")
}
if !found {
t.Fatalf("not listed retired: %+v", inv)
}
secret, err := kc.ClientSecretByID(ctx, "master", clientID(t, kc, p.As))
if err != nil || secret != "live-secret" {
t.Fatal("the secret was not kept:", secret, err)
}
if _, err := o.Ensure(ctx, p); err != nil {
t.Fatal(err)
}
if code := authorize(); code != 200 {
t.Fatalf("re-enabled and still refused: %d", code)
}
mustHold(t, o, p, true)
if err := o.Delete(ctx, p.As); err == nil {
t.Fatal("deleted an enabled client")
}
}
func clientID(t *testing.T, kc *Client, clientId string) string {
found, err := kc.FindClient(ctx, "master", clientId)
if err != nil || found == nil {
t.Fatal(found, err)
}
id, _ := found["id"].(string)
return id
}