Files
mesh-catalog/modules/keycloak/cmd/keycloak-provider/provisioner.go
T

124 lines
4.3 KiB
Go

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"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
"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 harness.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) (harness.Inventory, error) {
if err := a.refused(); err != nil {
return harness.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 harness.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 harness.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 harness.ClassCredentials
}
return ""
}
func logStderr(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) }