267 lines
9.6 KiB
Go
267 lines
9.6 KiB
Go
package main
|
|
|
|
// What holds keycloak to the `oidc-client` provision: one confidential client per consumer, under
|
|
// the id and secret the mesh gave, redirecting only to the consumer's own callback under the names
|
|
// the mesh composed; made once and brought back on every apply; and a client the mesh did not make —
|
|
// same id or not — never adopted, changed or deleted. Ported from test/oidc.test.ts.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"git.novox.be/novox/mesh-sdk/go/provider/harness"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func oidcWorld(t *testing.T) (*fakeKeycloak, OidcClients) {
|
|
f := newFakeKeycloak(t, "Novox", "pw")
|
|
return f, OidcClients{KC: f.client("pw"), Realm: "Novox"}
|
|
}
|
|
|
|
// grafana is a dashboard on the home server, as the mesh hands it to the provisioner.
|
|
func grafana(secret string, extra map[string]any) harness.Provision {
|
|
values := map[string]any{"label": "grafana", "endpoint": "web", "port": 20010.0, "callback": "/login/generic_oauth",
|
|
"name": "grafana.example.org", "internal-name": "grafana.home.internal"}
|
|
for k, v := range extra {
|
|
values[k] = v
|
|
}
|
|
return harness.Provision{As: "mesh_home_grafana", Password: secret, Consumer: "home", Values: values}
|
|
}
|
|
|
|
func only(t *testing.T, f *fakeKeycloak, clientID string) Rep {
|
|
t.Helper()
|
|
var found []Rep
|
|
for _, c := range f.clients {
|
|
if c["clientId"] == clientID {
|
|
found = append(found, c)
|
|
}
|
|
}
|
|
if len(found) != 1 {
|
|
t.Fatalf("exactly one client %s, found %d", clientID, len(found))
|
|
}
|
|
return found[0]
|
|
}
|
|
|
|
func mustHold(t *testing.T, o OidcClients, p harness.Provision, want bool) {
|
|
t.Helper()
|
|
held, err := o.Holds(ctx, p)
|
|
if err != nil || held != want {
|
|
t.Fatalf("holds = %v, %v; want %v", held, err, want)
|
|
}
|
|
}
|
|
|
|
func TestTheRealmIsReadOutOfTheIssuer(t *testing.T) {
|
|
for issuer, want := range map[string]string{
|
|
"https://id.example.org/realms/Novox": "Novox",
|
|
"https://id.example.org/realms/Novox/": "Novox",
|
|
"http://127.0.0.1:18500/realms/master": "master",
|
|
} {
|
|
if got, err := RealmOf(issuer); err != nil || got != want {
|
|
t.Errorf("%s: %q %v", issuer, got, err)
|
|
}
|
|
}
|
|
if _, err := RealmOf("https://id.example.org"); err == nil || !strings.Contains(err.Error(), "realms") {
|
|
t.Error(err)
|
|
}
|
|
if _, err := RealmOf("keycloak"); err == nil || !strings.Contains(err.Error(), "not a URL") {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestTheRedirectIsTheCallbackUnderEveryComposedName(t *testing.T) {
|
|
root, redirects, err := RedirectsOf(grafana("s", nil).Values)
|
|
if err != nil || root != "https://grafana.example.org" || !reflect.DeepEqual(redirects,
|
|
[]string{"https://grafana.example.org/login/generic_oauth", "https://grafana.home.internal/login/generic_oauth"}) {
|
|
t.Fatal(root, redirects, err)
|
|
}
|
|
if _, r, _ := RedirectsOf(map[string]any{"callback": "/cb", "internal-name": "x.home.internal"}); !reflect.DeepEqual(r, []string{"https://x.home.internal/cb"}) {
|
|
t.Fatal(r)
|
|
}
|
|
for _, values := range []map[string]any{{"name": "g"}, {"name": "g", "callback": "login"}} {
|
|
if _, _, err := RedirectsOf(values); err == nil || !strings.Contains(err.Error(), "callback") {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
if _, _, err := RedirectsOf(map[string]any{"callback": "/cb"}); err == nil || !strings.Contains(err.Error(), "label") {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestAConsumerIsGivenOneConfidentialClient(t *testing.T) {
|
|
f, o := oidcWorld(t)
|
|
if done, err := o.Ensure(ctx, grafana("s3cret", nil)); err != nil || done != "created" {
|
|
t.Fatal(done, err)
|
|
}
|
|
c := only(t, f, "mesh_home_grafana")
|
|
for k, v := range map[string]any{"publicClient": false, "clientAuthenticatorType": "client-secret", "secret": "s3cret",
|
|
"enabled": true, "standardFlowEnabled": true, "directAccessGrantsEnabled": false, "implicitFlowEnabled": false} {
|
|
if c[k] != v {
|
|
t.Errorf("%s = %v, want %v", k, c[k], v)
|
|
}
|
|
}
|
|
if attributes(c)[Mark] != "true" || len(c["protocolMappers"].([]any)) != 1 {
|
|
t.Fatal(c)
|
|
}
|
|
mustHold(t, o, grafana("s3cret", nil), true)
|
|
}
|
|
|
|
func TestApplyingTheSameGrantAgainMakesNoSecondClient(t *testing.T) {
|
|
f, o := oidcWorld(t)
|
|
o.Ensure(ctx, grafana("s", nil))
|
|
for i := 0; i < 2; i++ {
|
|
if done, err := o.Ensure(ctx, grafana("s", nil)); err != nil || done != "updated" {
|
|
t.Fatal(done, err)
|
|
}
|
|
}
|
|
if n := len(only(t, f, "mesh_home_grafana")["protocolMappers"].([]any)); n != 1 {
|
|
t.Fatalf("the roles mapper was added %d times", n)
|
|
}
|
|
}
|
|
|
|
func TestANewSecretIsAppliedInPlaceAndWhatTheMeshDoesNotOwnSurvives(t *testing.T) {
|
|
f, o := oidcWorld(t)
|
|
o.Ensure(ctx, grafana("s", nil))
|
|
c := only(t, f, "mesh_home_grafana")
|
|
id := c["id"]
|
|
c["consentRequired"] = true
|
|
attributes(c)["post.logout.redirect.uris"] = "+"
|
|
|
|
mustHold(t, o, grafana("rotated", nil), false)
|
|
if _, err := o.Ensure(ctx, grafana("rotated", map[string]any{"name": "dash.example.org"})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c = only(t, f, "mesh_home_grafana")
|
|
if c["id"] != id || c["secret"] != "rotated" || c["rootUrl"] != "https://dash.example.org" ||
|
|
c["consentRequired"] != true || attributes(c)["post.logout.redirect.uris"] != "+" || attributes(c)[Mark] != "true" {
|
|
raw, _ := json.Marshal(c)
|
|
t.Fatal(string(raw))
|
|
}
|
|
mustHold(t, o, grafana("rotated", map[string]any{"name": "dash.example.org"}), true)
|
|
}
|
|
|
|
func TestAClientEditedBehindTheMeshsBackIsNotHeldAndIsMadeWhole(t *testing.T) {
|
|
f, o := oidcWorld(t)
|
|
o.Ensure(ctx, grafana("s", nil))
|
|
only(t, f, "mesh_home_grafana")["redirectUris"] = []any{"*"}
|
|
mustHold(t, o, grafana("s", nil), false)
|
|
o.Ensure(ctx, grafana("s", nil))
|
|
mustHold(t, o, grafana("s", nil), true)
|
|
|
|
only(t, f, "mesh_home_grafana")["protocolMappers"] = []any{}
|
|
mustHold(t, o, grafana("s", nil), false)
|
|
o.Ensure(ctx, grafana("s", nil))
|
|
mustHold(t, o, grafana("s", nil), true)
|
|
|
|
f.clients = map[string]Rep{}
|
|
mustHold(t, o, grafana("s", nil), false)
|
|
}
|
|
|
|
func TestAClientTheMeshDidNotMakeIsRefusedAndLeftAlone(t *testing.T) {
|
|
f, o := oidcWorld(t)
|
|
f.clients["theirs"] = Rep{"id": "theirs", "clientId": "mesh_home_grafana", "secret": "their-secret", "redirectUris": []any{"*"}}
|
|
before, _ := json.Marshal(f.clients["theirs"])
|
|
from := len(f.calls)
|
|
if _, err := o.Ensure(ctx, grafana("s", nil)); err == nil || !strings.Contains(err.Error(), "did not make") {
|
|
t.Fatal(err)
|
|
}
|
|
after, _ := json.Marshal(f.clients["theirs"])
|
|
if string(before) != string(after) {
|
|
t.Fatal("changed")
|
|
}
|
|
for _, c := range f.calls[from:] {
|
|
if !strings.HasPrefix(c, "GET") && !strings.HasPrefix(c, "POST /realms/master") {
|
|
t.Fatalf("wrote: %v", f.calls[from:])
|
|
}
|
|
}
|
|
mustHold(t, o, grafana("s", nil), false)
|
|
if done, _ := o.Retire(ctx, "mesh_home_grafana", "x", time.Now()); done != "not ours" || f.clients["theirs"]["enabled"] == false {
|
|
t.Fatal(done)
|
|
}
|
|
if err := o.Delete(ctx, "mesh_home_grafana"); err == nil || f.clients["theirs"] == nil {
|
|
t.Fatal("deleted a client the mesh did not make")
|
|
}
|
|
if inv, _ := o.Inventory(ctx); len(inv.Active)+len(inv.Retired) != 0 {
|
|
t.Fatalf("listed a client the mesh did not make: %+v", inv)
|
|
}
|
|
}
|
|
|
|
// Retired is disabled and marked, everything else kept; asked for again it is enabled as it was; only
|
|
// a deletion removes it, and never while enabled (novox/hq ADR 0230).
|
|
func TestARetiredClientIsDisabledKeptAndEnabledAgainAsItWas(t *testing.T) {
|
|
f, o := oidcWorld(t)
|
|
p := grafana("s", nil)
|
|
if _, err := o.Ensure(ctx, p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var id string
|
|
for k := range f.clients {
|
|
id = k
|
|
}
|
|
f.clients[id]["description2"] = "an operator's own field"
|
|
at := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
|
|
if done, err := o.Retire(ctx, "mesh_home_grafana", "the mesh stopped asking for it", at); done != "retired" || err != nil {
|
|
t.Fatal(done, err)
|
|
}
|
|
c := f.clients[id]
|
|
if c["enabled"] != false || c["secret"] != "s" || attributes(c)[MarkRetired] != "2026-10-06T12:00:00Z" ||
|
|
attributes(c)[MarkRetiredWhy] != "the mesh stopped asking for it" || c["description2"] == nil {
|
|
t.Fatalf("%v", c)
|
|
}
|
|
mustHold(t, o, p, false)
|
|
inv, err := o.Inventory(ctx)
|
|
if err != nil || len(inv.Active) != 0 || len(inv.Retired) != 1 || !inv.Retired[0].RetiredAt.Equal(at) ||
|
|
inv.Retired[0].Consumer != "mesh_home_grafana" {
|
|
t.Fatalf("%+v %v", inv, err)
|
|
}
|
|
// Asked for again: enabled, unmarked, the same client.
|
|
if _, err := o.Ensure(ctx, p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c = f.clients[id]
|
|
if c["enabled"] != true || attributes(c)[MarkRetired] != nil || attributes(c)[MarkRetiredWhy] != nil || c["description2"] == nil {
|
|
t.Fatalf("%v", c)
|
|
}
|
|
mustHold(t, o, p, true)
|
|
if inv, _ := o.Inventory(ctx); len(inv.Active) != 1 || len(inv.Retired) != 0 {
|
|
t.Fatalf("%+v", inv)
|
|
}
|
|
// Never deleted while enabled; deleted once retired; absent is no error.
|
|
if err := o.Delete(ctx, "mesh_home_grafana"); err == nil || len(f.clients) != 1 {
|
|
t.Fatal("deleted an enabled client")
|
|
}
|
|
o.Retire(ctx, "mesh_home_grafana", "again", at)
|
|
if err := o.Delete(ctx, "mesh_home_grafana"); err != nil || len(f.clients) != 0 {
|
|
t.Fatal(err, f.clients)
|
|
}
|
|
if err := o.Delete(ctx, "mesh_home_grafana"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if done, err := o.Retire(ctx, "mesh_home_grafana", "x", at); done != "absent" || err != nil {
|
|
t.Fatal(done, err)
|
|
}
|
|
}
|
|
|
|
// A client the mesh made and found disabled without the mark — before ADR 0230 — is listed retired
|
|
// with no date, which the loop adopts.
|
|
func TestAClientFoundDisabledIsListedRetiredWithoutADate(t *testing.T) {
|
|
f, o := oidcWorld(t)
|
|
o.Ensure(ctx, grafana("s", nil))
|
|
for _, c := range f.clients {
|
|
c["enabled"] = false
|
|
}
|
|
inv, err := o.Inventory(ctx)
|
|
if err != nil || len(inv.Retired) != 1 || !inv.Retired[0].RetiredAt.IsZero() {
|
|
t.Fatalf("%+v %v", inv, err)
|
|
}
|
|
}
|
|
|
|
func TestAContributionWithNoCallbackMakesNoClient(t *testing.T) {
|
|
f, o := oidcWorld(t)
|
|
p := grafana("s", nil)
|
|
p.Values = map[string]any{"name": "grafana.example.org"}
|
|
if _, err := o.Ensure(ctx, p); err == nil || !strings.Contains(err.Error(), "callback") || len(f.clients) != 0 {
|
|
t.Fatal(err)
|
|
}
|
|
}
|