The hourly release of ADR 0229's brake still ended in the mesh acting alone on a mistake. A consumer now stays active until the same unasked set holds for five passes, waits for a person past three or half of those held, is disabled and marked rather than withdrawn, comes back as it was when asked again, and is deleted only through the provider's delete tool. The backend keeps the mark, so a restart forgets nothing and finds what was withdrawn before.
123 lines
4.2 KiB
Go
123 lines
4.2 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"
|
|
"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...) }
|