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 }), } }