package main // keycloak's provisioner — the adapter that makes keycloak a provider of the mesh `oidc-client` // interface (novox/hq ADR 0039/0040/0048). The reconcile loop is harness.go's; this writes only how // Keycloak creates, checks and removes a consumer's client. What a client is, is oidc.go's. // // **The realm is read out of the issuer**, the one value an assignment sets (settings reach both the // served facts and this module's config.json): a realm set in one place and an issuer in another // would let the consumer be told one realm while its client is made in another. // // **While the admin is refused, Keycloak is not asked** (admin.go): every attempt would be one more // failed login against the admin. The harness still counts each pass as a failure of the consumer, // classed credentials-rejected, so the standing it announces says what is wrong. import ( "context" "errors" "fmt" "os" "time" ) // Issuer is the issuer this assignment serves, from the settings-merged config the mesh delivers. func Issuer(getenv func(string) string) (string, error) { if said := cfgString(meshConfig(getenv("MESH_KEYCLOAK_CONFIG_FILE")), "issuer"); said != "" { return said, nil } if said := getenv("MESH_KEYCLOAK_ISSUER"); said != "" { return said, nil } return "", errors.New("no issuer — the module's config.json carries none and MESH_KEYCLOAK_ISSUER is unset") } // provisioner is the adapter. type provisioner struct { clients OidcClients guard *Guard announce func(event string, body map[string]any) log func(format string, args ...any) } func (a provisioner) refused() error { if a.guard != nil && a.guard.Refused() { return ErrAdminRejected } return nil } func (a provisioner) Create(ctx context.Context, p Provision) error { if err := a.refused(); err != nil { return err } done, err := a.clients.Ensure(ctx, p) if err != nil { return err } if done == "created" { a.log("[provisioner:oidc-client] created client %s in realm %s", p.As, a.clients.Realm) a.announce("client.created", map[string]any{"realm": a.clients.Realm, "clientId": p.As, "consumer": p.Consumer}) } return nil } // Retire disables the consumer's client and marks it, never deletes it (novox/hq ADR 0230). func (a provisioner) Retire(ctx context.Context, as string, _ map[string]any, why string, at time.Time) error { if err := a.refused(); err != nil { return err } done, err := a.clients.Retire(ctx, as, why, at) if err != nil { return err } switch done { case "not ours": a.log("[provisioner:oidc-client] %s: a client of that id exists that the mesh did not make — left alone", as) case "retired": a.log("[provisioner:oidc-client] retired client %s in realm %s: disabled, kept, marked to delete", as, a.clients.Realm) a.announce("client.retired", map[string]any{"realm": a.clients.Realm, "clientId": as}) } return nil } // Inventory is every client in the realm the mesh made, active and retired. func (a provisioner) Inventory(ctx context.Context) (Inventory, error) { if err := a.refused(); err != nil { return Inventory{}, err } return a.clients.Inventory(ctx) } // Delete removes a retired client, a person's act through `cleanup delete`. Nothing is freed that // Keycloak counts in bytes. func (a provisioner) Delete(ctx context.Context, r Retired) (int64, error) { if err := a.refused(); err != nil { return 0, err } if err := a.clients.Delete(ctx, r.Consumer); err != nil { return 0, err } a.log("[provisioner:oidc-client] deleted retired client %s from realm %s", r.Consumer, a.clients.Realm) return -1, nil } // Holds is asked every minute by the harness: whether Keycloak still holds this consumer's client // exactly as the mesh gave it, so one deleted or edited behind the mesh's back is made again // (novox/hq issue 120). func (a provisioner) Holds(ctx context.Context, p Provision) (bool, error) { if err := a.refused(); err != nil { return false, err } return a.clients.Holds(ctx, p) } // Class says a refused admin is a credentials problem, whatever the words around it. func (provisioner) Class(err error) string { if errors.Is(err, ErrRejected) { return ClassCredentials } return "" } func logStderr(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) }