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-contracts/provisioning/go/clients/meshprovisionerclient" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider/harness" ) // 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, meshprovisionerclient.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 := meshprovisionerclient.Key(node, module, name, local) if err != nil { panic(err) } asked, err := meshprovisionerclient.Encode(meshprovisionerclient.Provision{Provision: name, Local: local, State: meshprovisionerclient.Asked, Consumer: meshprovisionerclient.End{Node: node, Module: module}, Provider: meshprovisionerclient.End{Node: "novox", Module: "keycloak"}}) if err != nil { panic(err) } _ = bus.Put(ctx, meshprovisionerclient.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, 60*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) } }) }