From d7cac795854e524f740940f6a7d1911517c5e2b6 Mon Sep 17 00:00:00 2001 From: fix-sdk-pas Date: Fri, 16 Oct 2026 19:37:02 +0200 Subject: [PATCH] Carry the grant-file and keycloak-shape moved-path tests into harness (issue 849) --- go/provider/harness/grants_moved_test.go | 123 +++++++++++++++++++++ go/provider/harness/moved_test.go | 131 +++++++++++++++++++++-- 2 files changed, 245 insertions(+), 9 deletions(-) create mode 100644 go/provider/harness/grants_moved_test.go diff --git a/go/provider/harness/grants_moved_test.go b/go/provider/harness/grants_moved_test.go new file mode 100644 index 0000000..620aaf8 --- /dev/null +++ b/go/provider/harness/grants_moved_test.go @@ -0,0 +1,123 @@ +package harness + +// postgres handed to the provisioner still serves the consumers the controller's grant file gives (issue 937): a +// consumer not moved has no plan naming postgres, only its grant, until R1 (issue 682). + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" + "time" +) + +// granting gives the moved world a grant file, as the controller writes it for the consumers not moved. +func (w *movedWorld) granting(given ...map[string]any) { + if w.h.Receives == "" { + w.h.Receives = filepath.Join(w.t.TempDir(), "mesh.json") + } + dir := filepath.Dir(w.h.Receives) + for _, g := range given { + secret := filepath.Join(dir, g["as"].(string)+".secret") + if err := os.WriteFile(secret, []byte("pw-"+g["as"].(string)+"\n"), 0o600); err != nil { + w.t.Fatal(err) + } + g["secret"] = secret + } + if given == nil { + given = []map[string]any{} + } + raw, _ := json.Marshal(map[string]any{"requirement": ProvisionName, "given": given}) + if err := os.WriteFile(w.h.Receives, raw, 0o600); err != nil { + w.t.Fatal(err) + } +} + +// settled is enough passes and time for a retirement to be stable. +func (w *movedWorld) settled() { + for i := 0; i < 8; i++ { + w.pass() + } + w.now = w.now.Add(15 * time.Minute) + w.pass() + w.pass() +} + +// The logins the grant file gives are asked for: made as the grant path makes them, never offered for retirement, +// beside a moved consumer the helper makes from its plan. +func TestAConsumerOnlyTheGrantFileGivesIsAskedForAndMade(t *testing.T) { + bound, logins := bindings(1) + w := newMovedWorld(t, bound) + w.a.inv.Active = []string{"mesh_ace_baserow", "mesh_ace_letta", logins[0]} + w.bus.bind("ace", "app1", nil) + w.granting( + map[string]any{"as": "mesh_ace_baserow", "node": "ace", "values": map[string]any{"name": "baserow"}}, + map[string]any{"as": "mesh_ace_letta", "node": "ace", + "values": map[string]any{"name": "letta", "extensions": []any{"vector"}}}, + ) + w.settled() + if len(w.a.removed) != 0 || w.h.r.waiting != nil { + t.Fatalf("retired %v, waiting %+v: the grant file's logins were counted unasked", w.a.removed, w.h.r.waiting) + } + made := map[string]Provision{} + for _, c := range w.a.created { + made[c.As] = c + } + letta := made["mesh_ace_letta"] + if letta.Password != "pw-mesh_ace_letta" || letta.Consumer != "ace" || + !reflect.DeepEqual(letta.Values, map[string]any{"name": "letta", "extensions": []any{"vector"}}) { + t.Fatalf("made %+v for letta, as its grant gives it", letta) + } + if _, ok := made["mesh_ace_baserow"]; !ok { + t.Fatalf("baserow not made: %+v", w.a.created) + } + if _, ok := made[logins[0]]; !ok { + t.Fatalf("the moved consumer not made: %+v", w.a.created) + } +} + +// A login both a plan and the grant file name is the plan's: made once, with the plan's credential. +func TestALoginAPlanNamesIsNotMadeFromTheGrantFile(t *testing.T) { + bound, logins := bindings(1) + w := newMovedWorld(t, bound) + w.bus.bind("ace", "app1", nil) + w.granting(map[string]any{"as": logins[0], "node": "ace", "values": map[string]any{"name": "app1"}}) + w.pass() + w.pass() + for _, c := range w.a.created { + if c.As == logins[0] && c.Password != "pw-app1" { + t.Fatalf("made %s with the grant's password %q, not the plan's", c.As, c.Password) + } + } +} + +// An unreadable grant file is not nobody asking (issue 241): nothing is retired while it cannot be read. +func TestAnUnreadableGrantFileRetiresNothing(t *testing.T) { + bound, logins := bindings(1) + w := newMovedWorld(t, bound) + w.a.inv.Active = []string{"mesh_ace_baserow", logins[0]} + w.bus.bind("ace", "app1", nil) + w.h.Receives = filepath.Join(t.TempDir(), "absent.json") + w.settled() + if len(w.a.removed) != 0 || w.h.r.waiting != nil { + t.Fatalf("retired %v, waiting %+v with the grant file unreadable", w.a.removed, w.h.r.waiting) + } +} + +// A login the grant file stops giving is retired by the usual rules: stable, then retired, never dropped. +func TestALoginTheGrantFileStopsGivingIsRetired(t *testing.T) { + bound, _ := bindings(1) + w := newMovedWorld(t, bound) + w.bus.bind("ace", "app1", nil) + w.granting( + map[string]any{"as": "mesh_ace_baserow", "node": "ace", "values": map[string]any{"name": "baserow"}}, + map[string]any{"as": "mesh_ace_letta", "node": "ace", "values": map[string]any{"name": "letta"}}, + ) + w.pass() + w.granting(map[string]any{"as": "mesh_ace_baserow", "node": "ace", "values": map[string]any{"name": "baserow"}}) + w.settled() + if !reflect.DeepEqual(w.a.removed, []string{"mesh_ace_letta"}) || len(w.a.deleted) != 0 { + t.Fatalf("retired %v, deleted %v", w.a.removed, w.a.deleted) + } +} diff --git a/go/provider/harness/moved_test.go b/go/provider/harness/moved_test.go index 16e6219..ce79065 100644 --- a/go/provider/harness/moved_test.go +++ b/go/provider/harness/moved_test.go @@ -60,9 +60,14 @@ func (b *bucketBus) Delete(_ context.Context, bucket, key string) error { // bind is one consumer's assignment, whose plan names this machine's postgres as the provider and carries what the // consumer contributes (issue 776). func (b *bucketBus) bind(node, module string, contributes map[string]any) { + b.bindNamed("postgres", node, module, contributes, nil) +} + +// bindNamed is bind to a provider module, with the names the controller composed for the consumer (issue 792). +func (b *bucketBus) bindNamed(provides, node, module string, contributes map[string]any, names map[string]string) { plan, _ := json.Marshal(map[string]any{"node": node, "module": module, "provisioning": map[string]any{ - "provisions": []any{map[string]any{"provision": ProvisionName, "provider": "novox", "provider-module": "postgres", - "contributes": contributes}}}}) + "provisions": []any{map[string]any{"provision": ProvisionName, "provider": "novox", "provider-module": provides, + "contributes": contributes, "names": names}}}}) if err := b.Put(context.Background(), provider.AssignmentsBucket, node+"."+module, plan); err != nil { panic(err) } @@ -89,6 +94,7 @@ type movedWorld struct { a *recorder bus *bucketBus h *Harness + spec MovedSpec run *Moved p *provider.Provider now time.Time @@ -96,13 +102,17 @@ type movedWorld struct { } func newMovedWorld(t *testing.T, bound map[[2]string]string) *movedWorld { - w := &movedWorld{t: t, a: &recorder{}, bus: &bucketBus{buckets: map[string]map[string][]byte{}}, + return newMovedWorldOf(t, bound, postgresSpec) +} + +func newMovedWorldOf(t *testing.T, bound map[[2]string]string, spec MovedSpec) *movedWorld { + w := &movedWorld{t: t, spec: spec, a: &recorder{}, bus: &bucketBus{buckets: map[string]map[string][]byte{}}, now: time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC)} w.file = consumersFile(t, bound) w.h = &Harness{Resource: ProvisionName, Adapter: w.a, Node: "novox", Now: func() time.Time { return w.now }, Log: func(string, ...any) {}} var bus provider.Bus - w.run, w.p, bus, _ = NewMoved(w.file, w.h, postgresSpec) + w.run, w.p, bus, _ = NewMoved(w.file, w.h, spec) _ = bus if err := w.run.watch(ctx, w.bus); err != nil { t.Fatal(err) @@ -119,11 +129,10 @@ func (w *movedWorld) pass() []provider.Outcome { w.run.assignments[k] = v } // the helper reads its buckets from the in-memory bus - w.p = provider.New(provider.Options{Node: "novox", Module: "postgres", Bus: w.bus, - Vault: provider.ConsumersFile{Paths: []string{w.file}}, - Offers: map[string]provider.Offer{ProvisionName: {Credential: provider.PerConsumer, - Serves: map[string]any{"port": servedPort}}}, - Make: w.run.Make, Gone: w.run.Gone, Logf: func(string, ...any) {}}) + w.p = provider.New(provider.Options{Node: "novox", Module: w.spec.Module, Bus: w.bus, + Vault: provider.ConsumersFile{Paths: []string{w.file}}, + Offers: map[string]provider.Offer{ProvisionName: w.spec.Offer}, + Make: w.run.Make, Gone: w.run.Gone, Logf: func(string, ...any) {}}) out, err := w.p.Pass(ctx) if err != nil { w.t.Fatal(err) @@ -365,3 +374,107 @@ func TestASpecWithoutGrantsMakesUnderThePlansAsAndIgnoresTheGrantFile(t *testing t.Fatalf("made %+v", a.created) } } + +const testIssuer = "https://id.example.org/realms/Novox" + +var ( + grafanaContributes = map[string]any{"label": "grafana", "endpoint": "web", "callback": "/login/generic_oauth"} + grafanaNames = map[string]string{"name": "grafana.example.org", "internal-name": "grafana.home.internal"} +) + +// keycloakSpec is what a keycloak-like provider says: the client id is the plan's `as`, the values are what the +// consumer contributes with the controller's names over them, and no grant file is consulted. +var keycloakSpec = MovedSpec{Module: "keycloak", Provision: ProvisionName, + Offer: provider.Offer{Credential: provider.PerConsumer, Serves: map[string]any{"issuer": testIssuer, + "token-path": "/protocol/openid-connect/token"}}, + Login: func(e provider.Entry) string { return e.As }, + Values: func(e provider.Entry) map[string]any { + values := map[string]any{} + for k, v := range e.Contributes { + values[k] = v + } + for k, name := range e.Names { + values[k] = name + } + return values + }, + Noun: "client id", Made: "client", Backend: "Keycloak"} + +// The consumers' file makes the same call on the adapter as the equivalent grant file does (the harness), for a +// provider of the keycloak shape: the same client id, secret and values; and the entry tells the consumer what the +// provider serves. +func TestTheConsumersFileMakesWhatTheGrantFileMakesForTheKeycloakShape(t *testing.T) { + grafana := provider.ConsumerIdentity("home", "grafana") + values := map[string]any{"name": grafanaNames["name"], "internal-name": grafanaNames["internal-name"]} + for k, v := range grafanaContributes { + values[k] = v + } + g := newWorld(t) + g.give(map[string]any{"as": grafana, "node": "home", "values": values}) + g.h.Reconcile(ctx) + + w := newMovedWorldOf(t, map[[2]string]string{{"home", "grafana"}: "pw-" + grafana}, keycloakSpec) + w.bus.bindNamed("keycloak", "home", "grafana", grafanaContributes, grafanaNames) + for _, o := range w.pass() { + if o.Status != provider.Answered { + t.Fatalf("%+v", o) + } + } + if len(w.a.created) != 1 || len(g.a.created) != 1 { + t.Fatalf("created %d through the helper, %d through the harness, want 1 each", len(w.a.created), len(g.a.created)) + } + c, want := w.a.created[0], g.a.created[0] + if c.As != want.As || c.Password != want.Password || c.Consumer != want.Consumer || + !reflect.DeepEqual(orEmpty(c.Values), orEmpty(want.Values)) { + t.Fatalf("made %+v, the grant file makes %+v", c, want) + } + var entry provider.Entry + if err := json.Unmarshal(w.bus.buckets[provider.BucketOf("keycloak")]["home.grafana."+ProvisionName], &entry); err != nil { + t.Fatal(err) + } + if entry.Values["issuer"] != testIssuer || entry.Values["token-path"] != "/protocol/openid-connect/token" { + t.Fatalf("served %v", entry.Values) + } + w.a.held = true + w.pass() + w.now = w.now.Add(2 * time.Minute) + w.pass() + if len(w.a.created) != 1 { + t.Fatalf("an unchanged pass created again: %d", len(w.a.created)) + } + w.a.held = false + w.now = w.now.Add(2 * time.Minute) + w.pass() + if len(w.a.created) != 2 { + t.Fatalf("not made again: %d", len(w.a.created)) + } +} + +// A consumer whose plan names its client id (a slugged module) keeps it, and the retirement counts it as asked for. +func TestAConsumerNamedByItsPlanKeepsItsClientId(t *testing.T) { + agent := provider.ConsumerIdentity("home", "dashboard") + w := newMovedWorldOf(t, map[[2]string]string{{"home", "grafana-2"}: "pw"}, keycloakSpec) + plan, _ := json.Marshal(map[string]any{"node": "home", "module": "grafana-2", "provisioning": map[string]any{ + "provisions": []any{map[string]any{"provision": ProvisionName, "provider": "novox", "provider-module": "keycloak", + "as": agent, "contributes": grafanaContributes, "names": grafanaNames}}}}) + if err := w.bus.Put(ctx, provider.AssignmentsBucket, "home.grafana-2", plan); err != nil { + t.Fatal(err) + } + for _, o := range w.pass() { + if o.Status != provider.Answered { + t.Fatalf("%+v", o) + } + } + if len(w.a.created) != 1 || w.a.created[0].As != agent { + t.Fatalf("made %+v, want one under %s", w.a.created, agent) + } + for i := 0; i < 8; i++ { + w.pass() + } + w.now = w.now.Add(15 * time.Minute) + w.pass() + w.pass() + if len(w.a.removed) != 0 { + t.Fatalf("retired %v: the plan's client id was counted unasked", w.a.removed) + } +}