Files
mesh-catalog/modules/keycloak/cmd/keycloak-provider/tools.go
T
jochen 77fb1ecfb2 keycloak: port to Go and repair an admin that refuses the mesh's secret
Twice the identity provider's admin kept an older password than the one the
mesh minted (an adopted, then a moved database), and the provisioner failed
every consumer until it was repaired by hand (hq issue 179). The module now
checks the admin's login and repairs a refusal itself through the server's
bootstrap command, verifies, brakes a failed repair and announces it, and
stops asking the server while refused. Ported to Go to change it.
2026-10-06 00:13:42 +02:00

415 lines
18 KiB
Go

package main
// keycloak's tools — ported from tools/index.ts with the same names, arguments and answers. Write
// actions announce themselves at the point they succeed, in the module's single event vocabulary:
//
// user.created / user.deleted an identity appeared or was removed
// password.reset a user's credential was reset (no secret in the body)
// client.created an OIDC client was registered
// group.created / role.created a group or a realm role was created
// admin.repaired / .unrepaired the guard set the admin to the mesh's password, or could not
//
// Keycloak consumes nothing: it is upstream of everything that authenticates against it.
import (
"context"
"fmt"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
func prop(kind, description string) map[string]any {
return map[string]any{"type": kind, "description": description}
}
var realmProp = prop("string", "realm name (defaults to the module's realm)")
func str(args map[string]any, key string) string {
switch v := args[key].(type) {
case string:
return v
case nil:
return ""
default:
return fmt.Sprint(v)
}
}
func flag(args map[string]any, key string) (value, set bool) {
v, ok := args[key].(bool)
return v, ok
}
func number(args map[string]any, key string) int {
switch v := args[key].(type) {
case float64:
return int(v)
case int:
return v
}
return 0
}
func pick(r Rep, keys ...string) Rep {
out := Rep{}
for _, k := range keys {
if v, ok := r[k]; ok {
out[k] = v
}
}
return out
}
func bg() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), time.Minute)
}
// Tools are keycloak's own; the guard answers keycloak_admin_check.
func Tools(kc *Client, guard *Guard) []stdio.Tool {
realmOf := func(args map[string]any) string {
if r := str(args, "realm"); r != "" {
return r
}
return kc.DefaultRealm
}
tool := func(name, description string, input map[string]any, run func(ctx context.Context, args map[string]any) (any, error)) stdio.Tool {
return stdio.Tool{Name: name, Description: description, Input: input, Run: func(args map[string]any) (any, error) {
ctx, cancel := bg()
defer cancel()
return run(ctx, args)
}}
}
userID := prop("string", "user ID (UUID)")
return []stdio.Tool{
// The guard
{
Name: "keycloak_admin_check",
Description: "Check that Keycloak's admin logs in with the password the mesh minted. With repair: true, a " +
"refused admin is repaired now — its password set to the mesh's through a temporary bootstrap admin " +
"inside the container, which is removed again — even inside the brake a failed automatic repair set.",
Input: map[string]any{"repair": prop("boolean", "repair a refused admin now (default false: only check)")},
Run: func(args map[string]any) (any, error) {
repair, _ := flag(args, "repair")
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
defer cancel()
return guard.Ensure(ctx, repair, true), nil
},
},
// Realms & sessions
tool("keycloak_list_realms", "List all Keycloak realms.", map[string]any{},
func(ctx context.Context, _ map[string]any) (any, error) {
realms, err := kc.ListRealms(ctx)
if err != nil {
return nil, err
}
out := []Rep{}
for _, r := range realms {
out = append(out, pick(r, "id", "realm", "displayName", "enabled"))
}
return map[string]any{"realms": out}, nil
}),
tool("keycloak_list_sessions", "List active sessions for a user in a Keycloak realm.",
map[string]any{"realm": realmProp, "user_id": userID},
func(ctx context.Context, a map[string]any) (any, error) {
s, err := kc.UserSessions(ctx, realmOf(a), str(a, "user_id"))
return map[string]any{"sessions": s}, err
}),
// Users
tool("keycloak_list_users", "List users in a Keycloak realm.",
map[string]any{"realm": realmProp, "search": prop("string", "search by username, email, first/last name"),
"max": prop("number", "maximum number of results")},
func(ctx context.Context, a map[string]any) (any, error) {
u, err := kc.ListUsers(ctx, realmOf(a), str(a, "search"), number(a, "max"))
return map[string]any{"users": u}, err
}),
tool("keycloak_create_user", "Create a user in a Keycloak realm.",
map[string]any{"realm": realmProp, "username": prop("string", "username"), "email": prop("string", "email address"),
"password": prop("string", "initial password"),
"temporary_password": prop("boolean", "require a password change on first login (default true)")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, username, email := realmOf(a), str(a, "username"), str(a, "email")
rep := Rep{"username": username}
if email != "" {
rep["email"] = email
}
if pw := str(a, "password"); pw != "" {
temporary, set := flag(a, "temporary_password")
rep["credentials"] = []any{Rep{"type": "password", "value": pw, "temporary": temporary || !set}}
}
if err := kc.CreateUser(ctx, realm, rep); err != nil {
return nil, err
}
body := map[string]any{"realm": realm, "username": username}
if email != "" {
body["email"] = email
}
announce("user.created", body)
return map[string]any{"created": body}, nil
}),
tool("keycloak_delete_user", "Delete a user from a Keycloak realm (requires confirm).",
map[string]any{"realm": realmProp, "user_id": userID, "confirm": prop("boolean", "must be true to confirm deletion")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, id := realmOf(a), str(a, "user_id")
if ok, _ := flag(a, "confirm"); !ok {
return map[string]any{"aborted": "confirm must be true to delete a user"}, nil
}
if err := kc.DeleteUser(ctx, realm, id); err != nil {
return nil, err
}
announce("user.deleted", map[string]any{"realm": realm, "userId": id})
return map[string]any{"deleted": map[string]any{"realm": realm, "userId": id}}, nil
}),
tool("keycloak_update_user", "Update a user's attributes in a Keycloak realm (enable/disable, change email, name).",
map[string]any{"realm": realmProp, "user_id": userID, "enabled": prop("boolean", "enable or disable the user"),
"email": prop("string", "new email address"), "firstName": prop("string", "new first name"),
"lastName": prop("string", "new last name")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, id := realmOf(a), str(a, "user_id")
updates := Rep{}
var fields []string
if v, set := flag(a, "enabled"); set {
updates["enabled"], fields = v, append(fields, "enabled")
}
for _, k := range []string{"email", "firstName", "lastName"} {
if _, set := a[k]; set {
updates[k], fields = str(a, k), append(fields, k)
}
}
if len(updates) == 0 {
return map[string]any{"aborted": "no updates provided"}, nil
}
if err := kc.UpdateUser(ctx, realm, id, updates); err != nil {
return nil, err
}
return map[string]any{"updated": map[string]any{"realm": realm, "userId": id, "fields": fields}}, nil
}),
tool("keycloak_reset_password", "Reset a user's password in a Keycloak realm.",
map[string]any{"realm": realmProp, "user_id": userID, "password": prop("string", "new password"),
"temporary": prop("boolean", "require a password change on next login (default false)")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, id := realmOf(a), str(a, "user_id")
temporary, _ := flag(a, "temporary")
if err := kc.ResetPassword(ctx, realm, id, str(a, "password"), temporary); err != nil {
return nil, err
}
announce("password.reset", map[string]any{"realm": realm, "userId": id})
return map[string]any{"reset": map[string]any{"realm": realm, "userId": id}}, nil
}),
// Clients
tool("keycloak_list_clients", "List OIDC clients in a Keycloak realm.", map[string]any{"realm": realmProp},
func(ctx context.Context, a map[string]any) (any, error) {
clients, err := kc.ListClients(ctx, realmOf(a))
if err != nil {
return nil, err
}
out := []Rep{}
for _, c := range clients {
out = append(out, pick(c, "id", "clientId", "name", "enabled", "protocol", "publicClient", "rootUrl"))
}
return map[string]any{"clients": out}, nil
}),
tool("keycloak_create_client", "Create an OIDC client in a Keycloak realm.",
map[string]any{"realm": realmProp, "client_id": prop("string", "client ID (e.g. 'my-app')"),
"name": prop("string", "display name"), "root_url": prop("string", "root URL of the application"),
"redirect_uris": prop("array", "allowed redirect URIs"),
"public_client": prop("boolean", "public client, no client secret (default true)")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, clientID, name := realmOf(a), str(a, "client_id"), str(a, "name")
public, set := flag(a, "public_client")
rep := Rep{"protocol": "openid-connect", "enabled": true, "clientId": clientID, "publicClient": public || !set}
if name != "" {
rep["name"] = name
}
if u := str(a, "root_url"); u != "" {
rep["rootUrl"] = u
}
if list, ok := a["redirect_uris"].([]any); ok {
uris := []any{}
for _, u := range list {
uris = append(uris, fmt.Sprint(u))
}
rep["redirectUris"] = uris
}
if err := kc.CreateClient(ctx, realm, rep); err != nil {
return nil, err
}
body := map[string]any{"realm": realm, "clientId": clientID}
if name != "" {
body["name"] = name
}
announce("client.created", body)
return map[string]any{"created": body}, nil
}),
tool("keycloak_delete_client", "Delete an OIDC client from a Keycloak realm (requires confirm).",
map[string]any{"realm": realmProp, "client_id": prop("string", "client ID (e.g. 'my-app')"),
"confirm": prop("boolean", "must be true to confirm deletion")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, clientID := realmOf(a), str(a, "client_id")
if ok, _ := flag(a, "confirm"); !ok {
return map[string]any{"aborted": "confirm must be true to delete a client"}, nil
}
if err := kc.DeleteClient(ctx, realm, clientID); err != nil {
return nil, err
}
return map[string]any{"deleted": map[string]any{"realm": realm, "clientId": clientID}}, nil
}),
tool("keycloak_get_client_secret", "Get the client secret for a confidential OIDC client.",
map[string]any{"realm": realmProp, "client_id": prop("string", "client ID")},
func(ctx context.Context, a map[string]any) (any, error) {
s, err := kc.GetClientSecret(ctx, realmOf(a), str(a, "client_id"))
return map[string]any{"secret": s}, err
}),
tool("keycloak_add_protocol_mapper",
"Add a protocol mapper to an OIDC client. Common types: oidc-usermodel-realm-role-mapper "+
"(realm roles), oidc-usermodel-attribute-mapper (user attributes), oidc-audience-mapper.",
map[string]any{"realm": realmProp, "client_id": prop("string", "client ID (e.g. 'grafana')"),
"name": prop("string", "mapper name (e.g. 'realm roles')"),
"mapper_type": prop("string", "protocol mapper type (e.g. 'oidc-usermodel-realm-role-mapper')"),
"claim_name": prop("string", "token claim name (e.g. 'realm_access.roles')"),
"claim_type": prop("string", "JSON type: String, long, int, boolean (default String)"),
"multivalued": prop("boolean", "whether the claim has multiple values (default false)"),
"id_token": prop("boolean", "include in ID token (default true)"),
"access_token": prop("boolean", "include in access token (default true)"),
"userinfo": prop("boolean", "include in userinfo response (default true)")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, clientID, name := realmOf(a), str(a, "client_id"), str(a, "name")
claimType := str(a, "claim_type")
if claimType == "" {
claimType = "String"
}
on := func(key string) string {
v, set := flag(a, key)
return fmt.Sprint(v || !set)
}
multi, _ := flag(a, "multivalued")
err := kc.AddProtocolMapper(ctx, realm, clientID, Rep{
"name": name, "protocolMapper": str(a, "mapper_type"),
"config": map[string]any{
"claim.name": str(a, "claim_name"), "jsonType.label": claimType,
"multivalued": fmt.Sprint(multi), "id.token.claim": on("id_token"),
"access.token.claim": on("access_token"), "userinfo.token.claim": on("userinfo"),
},
})
if err != nil {
return nil, err
}
return map[string]any{"added": map[string]any{"realm": realm, "clientId": clientID, "mapper": name}}, nil
}),
// Groups
tool("keycloak_list_groups", "List groups in a Keycloak realm.", map[string]any{"realm": realmProp},
func(ctx context.Context, a map[string]any) (any, error) {
g, err := kc.ListGroups(ctx, realmOf(a))
return map[string]any{"groups": g}, err
}),
tool("keycloak_create_group", "Create a group in a Keycloak realm.",
map[string]any{"realm": realmProp, "name": prop("string", "group name")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, name := realmOf(a), str(a, "name")
if err := kc.CreateGroup(ctx, realm, name); err != nil {
return nil, err
}
announce("group.created", map[string]any{"realm": realm, "name": name})
return map[string]any{"created": map[string]any{"realm": realm, "group": name}}, nil
}),
tool("keycloak_get_user_groups", "List the groups a user belongs to in a Keycloak realm.",
map[string]any{"realm": realmProp, "user_id": userID},
func(ctx context.Context, a map[string]any) (any, error) {
g, err := kc.UserGroups(ctx, realmOf(a), str(a, "user_id"))
return map[string]any{"groups": g}, err
}),
tool("keycloak_add_user_to_group", "Add a user to a group in a Keycloak realm.",
map[string]any{"realm": realmProp, "user_id": userID, "group_id": prop("string", "group ID (UUID)")},
func(ctx context.Context, a map[string]any) (any, error) {
realm := realmOf(a)
if err := kc.AddUserToGroup(ctx, realm, str(a, "user_id"), str(a, "group_id")); err != nil {
return nil, err
}
return map[string]any{"added": map[string]any{"realm": realm, "userId": str(a, "user_id"), "groupId": str(a, "group_id")}}, nil
}),
tool("keycloak_remove_user_from_group", "Remove a user from a group in a Keycloak realm.",
map[string]any{"realm": realmProp, "user_id": userID, "group_id": prop("string", "group ID (UUID)")},
func(ctx context.Context, a map[string]any) (any, error) {
realm := realmOf(a)
if err := kc.RemoveUserFromGroup(ctx, realm, str(a, "user_id"), str(a, "group_id")); err != nil {
return nil, err
}
return map[string]any{"removed": map[string]any{"realm": realm, "userId": str(a, "user_id"), "groupId": str(a, "group_id")}}, nil
}),
// Roles
tool("keycloak_get_user_roles", "List the realm roles assigned to a user in a Keycloak realm.",
map[string]any{"realm": realmProp, "user_id": userID},
func(ctx context.Context, a map[string]any) (any, error) {
r, err := kc.UserRealmRoles(ctx, realmOf(a), str(a, "user_id"))
return map[string]any{"roles": r}, err
}),
tool("keycloak_create_role", "Create a realm role in a Keycloak realm.",
map[string]any{"realm": realmProp, "role_name": prop("string", "role name"), "description": prop("string", "role description")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, name := realmOf(a), str(a, "role_name")
rep := Rep{"name": name}
if d := str(a, "description"); d != "" {
rep["description"] = d
}
if err := kc.CreateRealmRole(ctx, realm, rep); err != nil {
return nil, err
}
announce("role.created", map[string]any{"realm": realm, "name": name})
return map[string]any{"created": map[string]any{"realm": realm, "role": name}}, nil
}),
tool("keycloak_assign_user_role", "Assign an existing realm role to a user. Create it first with keycloak_create_role if needed.",
map[string]any{"realm": realmProp, "user_id": userID, "role_name": prop("string", "role name to assign")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, id, role := realmOf(a), str(a, "user_id"), str(a, "role_name")
// The mapping API needs the role's UUID, which only the "available" list carries; a role
// neither available nor assigned does not exist in this realm.
available, err := kc.AvailableRealmRoles(ctx, realm, id)
if err != nil {
return nil, err
}
for _, r := range available {
if r["name"] == role {
if err := kc.AssignRealmRoles(ctx, realm, id, []Rep{pick(r, "id", "name")}); err != nil {
return nil, err
}
return map[string]any{"assigned": map[string]any{"realm": realm, "userId": id, "role": role}}, nil
}
}
assigned, err := kc.UserRealmRoles(ctx, realm, id)
if err != nil {
return nil, err
}
for _, r := range assigned {
if r["name"] == role {
return map[string]any{"alreadyAssigned": map[string]any{"realm": realm, "userId": id, "role": role}}, nil
}
}
return map[string]any{"notFound": map[string]any{"realm": realm, "role": role}}, nil
}),
tool("keycloak_remove_user_role", "Remove a realm role from a user in a Keycloak realm.",
map[string]any{"realm": realmProp, "user_id": userID, "role_name": prop("string", "role name to remove")},
func(ctx context.Context, a map[string]any) (any, error) {
realm, id, role := realmOf(a), str(a, "user_id"), str(a, "role_name")
assigned, err := kc.UserRealmRoles(ctx, realm, id)
if err != nil {
return nil, err
}
for _, r := range assigned {
if r["name"] == role {
if err := kc.RemoveRealmRoles(ctx, realm, id, []Rep{pick(r, "id", "name")}); err != nil {
return nil, err
}
return map[string]any{"removed": map[string]any{"realm": realm, "userId": id, "role": role}}, nil
}
}
return map[string]any{"notAssigned": map[string]any{"realm": realm, "userId": id, "role": role}}, nil
}),
}
}