124 lines
4.3 KiB
Go
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...) }
|