Files
mesh-catalog/modules/keycloak/cmd/keycloak-provider/grant_moved_test.go
T
jschoubben 35ff67bda0
mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
Design 47's last milestones: the keeper, self-check, healers, hand-acts, provisioner, vault, artifact sweep, disk readings, credential forms (issues 774, 1952, 1953, 1954, 1955)
One commit with exactly the tree of the reviewed head 35b4509c0a, on main 12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
2026-10-30 02:21:51 +01:00

167 lines
6.5 KiB
Go

package main
// Keycloak runs the moved path with the grant file still open for its consumers not moved (issue 937), fixed for
// keycloak by issue 1332: mesh-mail-web, grafana and car-hunter are not moved, so without this fix keycloak never
// made their clients and held them for retirement. These tests drive the SDK's harness.Moved exactly as main.go
// wires it, against a fake Keycloak and an in-memory bus — no real backend.
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
// grantGives writes a grant file (MESH_RECEIVES) naming consumers the plans do not move (issue 937): each one's
// secret file, and the json the grant path reads.
func grantGives(t *testing.T, given ...map[string]any) string {
t.Helper()
dir := t.TempDir()
for _, g := range given {
as := g["as"].(string)
secret := filepath.Join(dir, as+".secret")
if err := os.WriteFile(secret, []byte("pw-"+as+"\n"), 0o600); err != nil {
t.Fatal(err)
}
g["secret"] = secret
}
raw, _ := json.Marshal(map[string]any{"requirement": ProvisionName, "given": given})
at := filepath.Join(dir, "mesh.json")
if err := os.WriteFile(at, raw, 0o600); err != nil {
t.Fatal(err)
}
return at
}
// noOpPlan puts one plan the bus can read that binds nothing of keycloak's, so the moved path has plans it
// could read (m.asked answers "read", issue 241) even when nothing is moved.
func noOpPlan(bus *bucketBus, node, module string) {
seatPlan(bus, node, module, map[string]any{})
}
// seatPlan puts a module's plan as the seat mesh-provisioner keeps it, and for each of its provisions served by
// keycloak on novox the key the seat composes, asked (novox/hq ADR 0570 clauses 1 to 3).
func seatPlan(bus *bucketBus, node, module string, plan map[string]any) {
raw, _ := json.Marshal(map[string]any{"v": 1, "node": node, "module": module, "plan": plan})
_ = bus.Put(ctx, provisions.PlansBucket, node+"."+module, raw)
list, _ := plan["provisions"].([]any)
for _, e := range list {
entry := e.(map[string]any)
name, _ := entry["provision"].(string)
local, _ := entry["local"].(string)
key, err := provisions.Key(node, module, name, local)
if err != nil {
panic(err)
}
asked, err := provisions.Encode(provisions.Provision{Provision: name, Local: local, State: provisions.Asked,
Consumer: provisions.End{Node: node, Module: module}, Provider: provisions.End{Node: "novox", Module: "keycloak"}})
if err != nil {
panic(err)
}
_ = bus.Put(ctx, provisions.ProvisionsBucket, key, asked)
}
}
// waitUntil polls cond until it is true or the timeout passes.
func waitUntil(t *testing.T, timeout time.Duration, cond func() bool) {
t.Helper()
deadline := time.Now().Add(timeout)
for {
if cond() {
return
}
if time.Now().After(deadline) {
t.Fatalf("condition never held within %s", timeout)
}
time.Sleep(2 * time.Millisecond)
}
}
// movedWorld builds keycloak's moved path, as main.go wires it, against a fake Keycloak and a bucket bus. Nothing
// runs until start is called, so a test can seed the backend first; it is stopped when the test ends.
func movedWorld(t *testing.T, h *harness.Harness) (f *fakeKeycloak, start func()) {
t.Helper()
var o OidcClients
f, o = oidcWorld(t)
h.Adapter = provisioner{clients: o, announce: func(string, map[string]any) {}, log: func(string, ...any) {}}
if h.Every == 0 {
h.Every = 5 * time.Millisecond
}
consumersFile := filepath.Join(t.TempDir(), "consumers.json")
if err := os.WriteFile(consumersFile, []byte("[]"), 0o600); err != nil {
t.Fatal(err)
}
run, _, _, err := moved(consumersFile, h, testIssuer)
if err != nil {
t.Fatal(err)
}
bus := &bucketBus{buckets: map[string]map[string][]byte{}}
noOpPlan(bus, "elsewhere", "unrelated")
p := provider.New(provider.Options{Node: h.Node, Module: "keycloak", Bus: bus,
Vault: provider.ConsumersFile{Paths: []string{consumersFile}},
Offers: map[string]provider.Offer{ProvisionName: {Credential: provider.PerConsumer, Serves: servedValues(testIssuer)}},
Make: run.Make, Gone: run.Gone, Every: h.Every, Logf: func(string, ...any) {}})
return f, func() {
runCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
t.Cleanup(cancel)
go func() { _ = run.Run(runCtx, p, bus) }()
}
}
// 1332/AC1: a consumer only the grant file gives (oidc-client for novox.mesh-mail-web, not moved) is wanted and
// Adapter.Create is asked for its client, though keycloak runs on the moved path.
func TestAGrantFileConsumerNotMovedIsMadeOnTheMovedPath(t *testing.T) {
mailWeb := "mesh_novox_mail_web"
receives := grantGives(t, map[string]any{"as": mailWeb, "node": "novox", "values": map[string]any{
"label": "mail-web", "endpoint": "web", "callback": "/login/generic_oauth", "name": "mail.example.org"}})
h := &harness.Harness{Resource: ProvisionName, Receives: receives, Node: "novox"}
f, start := movedWorld(t, h)
start()
waitUntil(t, time.Second, func() bool {
found := false
f.set(func() {
for _, c := range f.clients {
if c["clientId"] == mailWeb {
found = true
}
}
})
return found
})
}
// 1332/AC2: a grant-file consumer whose client already exists in the backend (mesh_ace_grafana) counts as asked and
// is not held for retirement on the moved path.
func TestAGrantFileConsumerAlreadyInKeycloakIsNotRetiredOnTheMovedPath(t *testing.T) {
grafana := "mesh_ace_grafana"
receives := grantGives(t, map[string]any{"as": grafana, "node": "ace", "values": map[string]any{
"label": "grafana", "endpoint": "web", "callback": "/login/generic_oauth", "name": "grafana.example.org"}})
h := &harness.Harness{Resource: ProvisionName, Receives: receives, Node: "ace",
StablePasses: 1, StableFor: time.Millisecond}
f, start := movedWorld(t, h)
// Seed the backend with the client the mesh made earlier, exactly as grafana's already is — before anything
// runs, so the harness's one-time seed (issue 1332) sees it.
f.set(func() {
f.clients["existing-grafana"] = Rep{"id": "existing-grafana", "clientId": grafana, "enabled": true,
"attributes": map[string]any{Mark: "true"}}
})
start()
// Many passes, well past StablePasses and StableFor if the login were ever counted unasked.
time.Sleep(200 * time.Millisecond)
f.set(func() {
c := f.clients["existing-grafana"]
if c["enabled"] == false {
t.Fatalf("retired %v though its grant still gives it (issue 1332)", c)
}
if _, retired := attributes(c)[MarkRetired]; retired {
t.Fatalf("marked retired %v though its grant still gives it (issue 1332)", c)
}
})
}