194 lines
6.9 KiB
Go
194 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/provider"
|
|
"git.novox.be/novox/mesh-sdk/go/provider/harness"
|
|
)
|
|
|
|
const testIssuer = "https://id.example.org/realms/Novox"
|
|
|
|
// keycloak's own part of the moved path (issue 849): the shared driving of it is proven in the SDK's harness package.
|
|
|
|
// What a consumer is told is what it contributes, with the names the controller composed for it over them (issue 792).
|
|
func TestAConsumersValuesAreWhatItContributesWithItsComposedNames(t *testing.T) {
|
|
got := valuesOf(provider.Entry{Contributes: map[string]any{"label": "x", "name": "old"},
|
|
Names: map[string]string{"name": "x.example.org"}})
|
|
if want := map[string]any{"label": "x", "name": "x.example.org"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("%v, want %v", got, want)
|
|
}
|
|
if served := servedValues(testIssuer); served["issuer"] != testIssuer {
|
|
t.Fatalf("%v", served)
|
|
}
|
|
}
|
|
|
|
// Where the file is not there, keycloak takes the grant directory, as before; where it is, the helper.
|
|
func TestTheGrantPathIsTakenWhereTheConsumersFileIsAbsent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
at := filepath.Join(dir, "consumers.json")
|
|
env := func(k string) string {
|
|
if k == "MESH_CONSUMERS_FILE" {
|
|
return at
|
|
}
|
|
return ""
|
|
}
|
|
if got := handedConsumersFile(env, os.Stat); got != "" {
|
|
t.Fatalf("took %q for a file that is not there", got)
|
|
}
|
|
if got := handedConsumersFile(func(string) string { return "" }, os.Stat); got != "" {
|
|
t.Fatalf("took %q with no environment", got)
|
|
}
|
|
if err := os.WriteFile(at, []byte("[]"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := handedConsumersFile(env, os.Stat); got != at {
|
|
t.Fatalf("took %q, want %q", got, at)
|
|
}
|
|
}
|
|
|
|
// The grant path restarts itself when the consumers file is first filled.
|
|
func TestTheGrantPathLeavesWhenTheConsumersFileIsFilled(t *testing.T) {
|
|
at := filepath.Join(t.TempDir(), "consumers.json")
|
|
done := make(chan struct{})
|
|
go exitWhenFilled(at, os.Stat, time.Millisecond, func() { close(done) })
|
|
select {
|
|
case <-done:
|
|
t.Fatal("left before the file was there")
|
|
case <-time.After(20 * time.Millisecond):
|
|
}
|
|
if err := os.WriteFile(at, []byte("[]"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("did not leave when the file was filled")
|
|
}
|
|
}
|
|
|
|
// bucketBus is the helper's bus over in-memory buckets.
|
|
type bucketBus struct {
|
|
mu sync.Mutex
|
|
buckets map[string]map[string][]byte
|
|
}
|
|
|
|
func (b *bucketBus) Watch(_ context.Context, bucket string, handle func(string, []byte)) error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
for k, v := range b.buckets[bucket] {
|
|
handle(k, v)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *bucketBus) Put(_ context.Context, bucket, key string, value []byte) error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if b.buckets[bucket] == nil {
|
|
b.buckets[bucket] = map[string][]byte{}
|
|
}
|
|
b.buckets[bucket][key] = append([]byte(nil), value...)
|
|
return nil
|
|
}
|
|
|
|
func (b *bucketBus) Delete(_ context.Context, bucket, key string) error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
delete(b.buckets[bucket], key)
|
|
return nil
|
|
}
|
|
|
|
// grafanaContributes and grafanaNames are what grafana's plan entry carries for keycloak: its contribution, and the
|
|
// names the controller composed for it (issues 776, 792).
|
|
var grafanaContributes = map[string]any{"label": "grafana", "endpoint": "web", "callback": "/login/generic_oauth"}
|
|
var grafanaNames = map[string]string{"name": "grafana.example.org", "internal-name": "grafana.home.internal"}
|
|
|
|
// Against a Keycloak, both paths leave the same client: the grant file through the harness, and the consumers' file
|
|
// through the SDK's moved path with keycloak's spec (issue 849).
|
|
func TestBothPathsLeaveTheSameClientInKeycloak(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
|
|
}
|
|
now := time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC)
|
|
made := func(through func(p provisioner)) Rep {
|
|
f, o := oidcWorld(t)
|
|
through(provisioner{clients: o, announce: func(string, map[string]any) {}, log: func(string, ...any) {}})
|
|
return only(t, f, grafana)
|
|
}
|
|
viaGrant := made(func(p provisioner) {
|
|
dir := t.TempDir()
|
|
secret := filepath.Join(dir, grafana+".secret")
|
|
if err := os.WriteFile(secret, []byte("pw-"+grafana+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := json.Marshal(map[string]any{"requirement": ProvisionName, "given": []map[string]any{
|
|
{"as": grafana, "node": "home", "values": values, "secret": secret}}})
|
|
receives := filepath.Join(dir, "mesh.json")
|
|
if err := os.WriteFile(receives, raw, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := &harness.Harness{Resource: ProvisionName, Receives: receives, Adapter: p,
|
|
Now: func() time.Time { return now }, Log: func(string, ...any) {}}
|
|
h.Reconcile(ctx)
|
|
})
|
|
viaFile := made(func(p provisioner) {
|
|
list, _ := json.Marshal([]provider.ConsumerCredential{{Consumer: "home", Module: "grafana",
|
|
Credential: provider.PairName("novox", ProvisionName, "home", "grafana", ""), Value: "pw-" + grafana}})
|
|
file := filepath.Join(t.TempDir(), "consumers.json")
|
|
if err := os.WriteFile(file, list, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := &harness.Harness{Resource: ProvisionName, Adapter: p, Node: "novox",
|
|
Now: func() time.Time { return now }, Log: func(string, ...any) {}}
|
|
run, _, _, err := moved(file, h, testIssuer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bus := &bucketBus{buckets: map[string]map[string][]byte{}}
|
|
plan, _ := json.Marshal(map[string]any{"node": "home", "module": "grafana", "provisioning": map[string]any{
|
|
"provisions": []any{map[string]any{"provision": ProvisionName, "provider": "novox",
|
|
"provider-module": "keycloak", "contributes": grafanaContributes, "names": grafanaNames}}}})
|
|
_ = bus.Put(ctx, provider.AssignmentsBucket, "home.grafana", plan)
|
|
pr := provider.New(provider.Options{Node: "novox", Module: "keycloak", Bus: bus,
|
|
Vault: provider.ConsumersFile{Paths: []string{file}},
|
|
Offers: map[string]provider.Offer{ProvisionName: {Credential: provider.PerConsumer,
|
|
Serves: servedValues(testIssuer)}},
|
|
Make: run.Make, Gone: run.Gone, Logf: func(string, ...any) {}})
|
|
out, err := pr.Pass(ctx)
|
|
if err != nil || len(out) != 1 || out[0].Status != provider.Answered {
|
|
t.Fatalf("%+v %v", out, err)
|
|
}
|
|
})
|
|
// Keycloak's own ids differ between two runs; nothing the mesh sets does.
|
|
for _, c := range []Rep{viaGrant, viaFile} {
|
|
delete(c, "id")
|
|
raw, _ := json.Marshal(c)
|
|
var again Rep
|
|
_ = json.Unmarshal(raw, &again)
|
|
for k := range c {
|
|
delete(c, k)
|
|
}
|
|
for k, v := range again {
|
|
c[k] = v
|
|
}
|
|
if mappers, ok := c["protocolMappers"].([]any); ok {
|
|
for _, m := range mappers {
|
|
delete(m.(map[string]any), "id")
|
|
}
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(viaGrant, viaFile) {
|
|
t.Fatalf("through the file %v, through the grant %v", viaFile, viaGrant)
|
|
}
|
|
}
|