Files
mesh-catalog/modules/keycloak/cmd/keycloak-provider/oidc.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

393 lines
12 KiB
Go

package main
// What the `oidc-client` provision means in Keycloak: one confidential OpenID Connect client per
// consumer, in the realm this module serves, under the name and secret the mesh gave both ends.
// Ported from oidc.ts, behaviour for behaviour.
//
// **The client id and the secret are the mesh's, not Keycloak's (novox/hq ADR 0048).** The mesh
// derives the consumer's identity (`as`) and hands it to both ends, and mints the secret, which this
// sets as the client's secret. Keycloak generates neither.
//
// **Where the consumer's browser comes back to is the consumer's to say.** Its contribution carries
// `callback` (a path) and the mesh composes its endpoint's names into `name` (public) and
// `internal-name` (private network) exactly as it does for a route (novox/hq ADR 0056, 0138).
//
// **Only what the mesh made is touched.** A client this module creates carries the attribute
// `mesh.provisioned=true`. A client with the same id that lacks the mark is somebody else's: it is
// refused, never adopted, never updated, never deleted.
//
// **Retired is the client disabled and marked** (novox/hq ADR 0230). `enabled: false` — Keycloak then
// refuses the client's authorization and token requests, so nobody signs in through it — and the
// attributes `mesh.retired` (when) and `mesh.retired-why` (why). Its secret, redirects, mappers and
// every other setting are kept, so asked for again it is enabled as it was: Ensure sets `enabled` and
// removes the two attributes. Deleted — only through `cleanup delete` — the client is removed.
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/url"
"regexp"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
)
// Mark is the attribute marking a client as the mesh's own work.
const Mark = "mesh.provisioned"
// MarkRetired and MarkRetiredWhy mark a retired client: when and why (novox/hq ADR 0230).
const (
MarkRetired = "mesh.retired"
MarkRetiredWhy = "mesh.retired-why"
)
// RolesMapper is the mapper every mesh client carries: realm roles as a flat `roles` claim in the id
// token, the access token and userinfo — what a consumer maps its own roles from.
func RolesMapper() Rep {
return Rep{
"name": "realm roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"config": map[string]any{
"claim.name": "roles",
"jsonType.label": "String",
"multivalued": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
},
}
}
var realmPath = regexp.MustCompile(`/realms/([^/]+)/?$`)
// RealmOf is the realm named by an issuer URL — `https://id.example/realms/Novox` is realm `Novox`.
// The issuer is the one value an assignment sets, so the realm is read out of it rather than set a
// second time where the two could disagree.
func RealmOf(issuer string) (string, error) {
u, err := url.Parse(issuer)
if err != nil || u.Scheme == "" || u.Host == "" {
return "", fmt.Errorf("the issuer %q is not a URL", issuer)
}
m := realmPath.FindStringSubmatch(u.EscapedPath())
if m == nil {
return "", fmt.Errorf("the issuer %q does not end in /realms/<realm>", issuer)
}
realm, err := url.PathUnescape(m[1])
if err != nil {
return "", err
}
return realm, nil
}
// RedirectsOf is the redirect URIs a consumer's contribution asks for: its callback under each name
// the mesh composed for its endpoint. Refused when there is nothing to register.
func RedirectsOf(values map[string]any) (root string, redirects []string, err error) {
callback, _ := values["callback"].(string)
if !strings.HasPrefix(callback, "/") {
raw, _ := json.Marshal(values["callback"])
return "", nil, fmt.Errorf("contributes no callback path (`callback`, starting with \"/\"): %s", raw)
}
var names []string
for _, key := range []string{"name", "internal-name"} {
n, _ := values[key].(string)
n = strings.TrimSpace(n)
if n != "" && !contains(names, n) {
names = append(names, n)
}
}
if len(names) == 0 {
return "", nil, errors.New("has no name the mesh composed (`name` / `internal-name`) — contribute a `label` and the `endpoint` it is reached on")
}
for _, n := range names {
redirects = append(redirects, "https://"+n+callback)
}
return "https://" + names[0], redirects, nil
}
func contains(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}
// wanted is the fields the mesh owns on a client it made. Everything else is left as found.
func wanted(p harness.Provision) (Rep, []string, error) {
root, redirects, err := RedirectsOf(p.Values)
if err != nil {
return nil, nil, err
}
whose := "a consumer"
if p.Consumer != "" {
whose = "a module on " + p.Consumer
}
uris := make([]any, len(redirects))
for i, r := range redirects {
uris[i] = r
}
return Rep{
"clientId": p.As,
"name": p.As,
"description": "made by the mesh for " + whose + " — do not edit; it is reset",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"clientAuthenticatorType": "client-secret",
"secret": p.Password,
"rootUrl": root,
"baseUrl": root,
"redirectUris": uris,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
}, redirects, nil
}
func stringList(v any) []string {
list, _ := v.([]any)
out := make([]string, 0, len(list))
for _, x := range list {
if s, ok := x.(string); ok {
out = append(out, s)
}
}
return out
}
func sameSet(a, b []string) bool {
x, y := append([]string(nil), a...), append([]string(nil), b...)
sort.Strings(x)
sort.Strings(y)
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}
func attributes(c Rep) map[string]any {
if a, ok := c["attributes"].(map[string]any); ok {
return a
}
return map[string]any{}
}
func marked(c Rep) bool { return attributes(c)[Mark] == "true" }
// OidcClients is the provision's meaning in one realm.
type OidcClients struct {
KC *Client
Realm string
}
// Ensure creates the consumer's client, or brings the mesh's existing one back to what the grant
// says. Answers "created" or "updated". Idempotent.
func (o OidcClients) Ensure(ctx context.Context, p harness.Provision) (string, error) {
want, _, err := wanted(p)
if err != nil {
return "", err
}
found, err := o.KC.FindClient(ctx, o.Realm, p.As)
if err != nil {
return "", err
}
if found != nil && !marked(found) {
return "", fmt.Errorf("realm %s already has a client %s the mesh did not make — left alone; "+
"delete or rename it if the mesh should own that id", o.Realm, p.As)
}
if found == nil {
want["attributes"] = map[string]any{Mark: "true"}
want["protocolMappers"] = []any{RolesMapper()}
if err := o.KC.CreateClient(ctx, o.Realm, want); err != nil {
return "", err
}
return "created", nil
}
// Overlay what the mesh owns on what is there, so a field Keycloak added or an operator set on a
// field the mesh does not own survives the update.
merged := Rep{}
for k, v := range found {
merged[k] = v
}
for k, v := range want {
merged[k] = v
}
attrs := map[string]any{}
for k, v := range attributes(found) {
attrs[k] = v
}
attrs[Mark] = "true"
// Asked for again: no longer marked to delete (novox/hq ADR 0230).
delete(attrs, MarkRetired)
delete(attrs, MarkRetiredWhy)
merged["attributes"] = attrs
id, _ := found["id"].(string)
if err := o.KC.UpdateClient(ctx, o.Realm, id, merged); err != nil {
return "", err
}
return "updated", o.ensureMapper(ctx, id)
}
func (o OidcClients) ensureMapper(ctx context.Context, id string) error {
want := RolesMapper()
mappers, err := o.KC.ListClientMappers(ctx, o.Realm, id)
if err != nil {
return err
}
var have Rep
for _, m := range mappers {
if m["name"] == want["name"] {
have = m
break
}
}
if have == nil {
return o.KC.AddClientMapper(ctx, o.Realm, id, want)
}
drifted := have["protocolMapper"] != want["protocolMapper"]
hc, _ := have["config"].(map[string]any)
for k, v := range want["config"].(map[string]any) {
if hc[k] != v {
drifted = true
}
}
if drifted {
want["id"] = have["id"]
return o.KC.UpdateClientMapper(ctx, o.Realm, id, want)
}
return nil
}
// Holds says whether Keycloak still holds this consumer's client exactly as the grant says: present,
// the mesh's, enabled, confidential, with the mesh's secret and the redirects asked for. Reads only.
func (o OidcClients) Holds(ctx context.Context, p harness.Provision) (bool, error) {
_, redirects, err := wanted(p)
if err != nil {
return false, err
}
found, err := o.KC.FindClient(ctx, o.Realm, p.As)
if err != nil {
return false, err
}
if found == nil || !marked(found) || found["enabled"] == false || found["publicClient"] == true {
return false, nil
}
if !sameSet(stringList(found["redirectUris"]), redirects) {
return false, nil
}
id, _ := found["id"].(string)
mappers, err := o.KC.ListClientMappers(ctx, o.Realm, id)
if err != nil {
return false, err
}
hasMapper := false
for _, m := range mappers {
if m["name"] == RolesMapper()["name"] {
hasMapper = true
}
}
if !hasMapper {
return false, nil
}
secret, err := o.KC.ClientSecretByID(ctx, o.Realm, id)
if err != nil {
return false, err
}
return secret == p.Password, nil
}
// Retire, on the operator's yes (novox/hq ADR 0570 clause 5), removes the client's secret — replaced by one nobody
// is given, so the mesh's credential no longer authenticates — and disables and marks the client with when and why.
// The client and its settings are kept; asked for again, Ensure sets the mesh's secret and enables it. Only a client
// the mesh made. Answers what happened: "retired", "absent" or "not ours".
func (o OidcClients) Retire(ctx context.Context, as, why string, at time.Time) (string, error) {
found, err := o.KC.FindClient(ctx, o.Realm, as)
if err != nil {
return "", err
}
if found == nil {
return "absent", nil
}
if !marked(found) {
return "not ours", nil
}
merged := Rep{}
for k, v := range found {
merged[k] = v
}
attrs := map[string]any{}
for k, v := range attributes(found) {
attrs[k] = v
}
attrs[MarkRetired] = at.UTC().Format(time.RFC3339)
attrs[MarkRetiredWhy] = why
merged["attributes"] = attrs
merged["enabled"] = false
unknown := make([]byte, 32)
if _, err := rand.Read(unknown); err != nil {
return "", err
}
merged["secret"] = hex.EncodeToString(unknown) // never kept, never said
id, _ := found["id"].(string)
return "retired", o.KC.UpdateClient(ctx, o.Realm, id, merged)
}
// Inventory is every client in the realm the mesh made: enabled ones active, disabled ones retired —
// with when and why from the mark, or none for one disabled before the mark existed.
func (o OidcClients) Inventory(ctx context.Context) (harness.Inventory, error) {
inv := harness.Inventory{Active: []string{}, Retired: []harness.Retired{}}
clients, err := o.KC.ListClients(ctx, o.Realm)
if err != nil {
return inv, err
}
for _, c := range clients {
if !marked(c) {
continue
}
id, _ := c["clientId"].(string)
a := attributes(c)
when, _ := a[MarkRetired].(string)
if c["enabled"] != false && when == "" {
inv.Active = append(inv.Active, id)
continue
}
at, _ := time.Parse(time.RFC3339, when)
why, _ := a[MarkRetiredWhy].(string)
inv.Retired = append(inv.Retired, harness.Retired{Consumer: id, RetiredAt: at, Why: why, SizeBytes: -1, Kind: harness.KindConsumer})
}
return inv, nil
}
// Delete removes a retired client — only one the mesh made, and only while it is disabled.
func (o OidcClients) Delete(ctx context.Context, as string) error {
found, err := o.KC.FindClient(ctx, o.Realm, as)
if err != nil {
return err
}
if found == nil {
return nil
}
if !marked(found) {
return fmt.Errorf("realm %s's client %s was not made by the mesh — left alone", o.Realm, as)
}
if found["enabled"] != false {
return fmt.Errorf("client %s is enabled — it is active, not retired, and is not deleted", as)
}
id, _ := found["id"].(string)
return o.KC.DeleteClientByID(ctx, o.Realm, id)
}