Ruby client that acts as a client for the Keycloak REST API.
This gem basically acts as an url builder using Faraday to get responses and serialize them into representation objects.
Warning: This beta gem is currently used for personal use. Most Keycloak Admin features are not implemented yet.
Ruby 3.1 or greater. The gem is tested against Ruby 3.1, 3.2, 3.3 and 3.4.
This gem does not require Rails.
For example, using bundle, add this line to your Gemfile.
gem "keycloak-admin", "2.0.3"To login on Keycloak's Admin API, you first need to setup a client.
Go to your realm administration page and open Clients. Then, click on the Create button.
On the first screen, enter:
Client ID: e.g. my-app-admin-clientClient Protocol: selectopenid-connectRoot URL: let it blank
The next screen must be configured depending on how you want to authenticate:
username/passwordwith a user of the realmDirect Access Grantswith a service account
-
In Keycloak, during the client setup:
Access Type:publicorconfidentialService Accounts Enabled(whenconfidential):false- After saving your client, if you have chosen a
confidentialclient, go toCredentialstab and copy theClient Secret
-
In Keycloak, create a dedicated user (and her credentials):
- Go to
Users - Click on the
Add userbutton - Setup her mandatory information, depending on your realm's configuration
- On the
Credentialstab, create her a password (toggle offTemporary)
- Go to
-
In this gem's configuration (see Section
Configuration):- Setup
usernameandpasswordaccording to your user's configuration - Setup
client_idwith yourClient ID(e.g. my-app-admin-client) - If your client is
confidential, copy its Client Secret toclient_secret
- Setup
Using a service account to use the REST Admin API does not require to create a dedicated user (https://www.keycloak.org/docs/latest/server_admin/#_service_accounts).
-
In Keycloak, during the client setup:
Access Type:confidentialService Accounts Enabled(whenconfidential):trueStandard Flow Enabled:falseImplicit Flow Enabled:falseDirect Access Grants Enabled:true- After saving this client
- open the
Service Account Rolesand add relevantrealm-management.client's roles. For instance:view-usersif you want to search for users using this gem. - open the
Credentialstab and copy theClient Secret
- open the
-
In this gem's configuration (see Section
Configuration):- Set
use_service_accounttotrue - Setup
client_idwith yourClient ID(e.g. my-app-admin-client) - Copy its Client Secret to
client_secret
- Set
To configure this gem, call KeycloakAdmin.configure.
For instance, to configure this gem based on environment variables, write (and load if required) a keycloak_admin.rb:
KeycloakAdmin.configure do |config|
config.use_service_account = false
config.server_url = ENV["KEYCLOAK_SERVER_URL"]
config.server_domain = ENV["KEYCLOAK_SERVER_DOMAIN"]
config.client_id = ENV["KEYCLOAK_ADMIN_CLIENT_ID"]
config.client_realm_name = ENV["KEYCLOAK_REALM_ID"]
config.username = ENV["KEYCLOAK_ADMIN_USER"]
config.password = ENV["KEYCLOAK_ADMIN_PASSWORD"]
config.logger = Rails.logger
# You configure Faraday to your liking – see https://lostisland.github.io/faraday/#/customization/connection-options for available options.
config.faraday_options = { request: { timeout: 5 } }
endThis example is autoloaded in a Rails environment.
All options have a default value. However, all of them can be changed in your initializer file.
| Option | Default Value | Type | Required? | Description | Example |
|---|---|---|---|---|---|
server_url |
nil |
String | Required | The base url where your Keycloak server is located (a URL that starts with http and that ends with /auth). This value can be retrieved in your Keycloak client configuration. |
http://auth:8080/auth |
server_domain |
nil |
String | Required | Public domain that identify your authentication cookies. | auth.service.io |
client_realm_name |
"" |
String | Required | Name of the realm that contains the admin client. | master |
client_id |
admin-cli |
String | Required | Client that should be used to access admin capabilities. | api-cli |
client_secret |
nil |
String | Optional | If your client is confidential, this parameter must be specified. |
4e3c481c-f823-4a6a-b8a7-bf8c86e3eac3 |
use_service_account |
true |
Boolean | Required | true if the connection to the client uses a Service Account. false if the connection to the client uses a username/password credential. |
false |
username |
nil |
String | Optional | Username to access the Admin REST API. Recommended if user_service_account is set to false. |
mummy |
password |
nil |
String | Optional | Clear password to access the Admin REST API. Recommended if user_service_account is set to false. |
bobby |
logger |
Logger.new(STDOUT) |
Logger | Optional | The logger used by keycloak-admin |
Rails.logger |
faraday_options |
{} |
Hash | Optional | Options to pass to Faraday.new (e.g. request:, ssl:, proxy:) |
{ request: { timeout: 5 } } |
faraday_adapter |
nil |
Symbol or Array | Optional | Faraday adapter to run requests through. nil uses Faraday.default_adapter. See Connection reuse below. |
:net_http_persistent |
By default this gem runs on Faraday.default_adapter (net_http), which wraps every request in Net::HTTP#start: the TCP connection is opened and closed again for each call. On a loop of admin calls, that handshake dominates the time spent.
Set faraday_adapter to a pooling adapter to keep connections alive between calls. The adapter is not bundled, so add its gem to your own Gemfile alongside this one:
# Gemfile
gem "faraday-net_http_persistent"KeycloakAdmin.configure do |config|
config.faraday_adapter = :net_http_persistent
endAn adapter needing arguments is given as an array, splatted into Faraday's adapter call:
config.faraday_adapter = [:net_http_persistent, { pool_size: 5 }]Every HTTP failure raises a KeycloakAdmin::ApiError carrying the status, body and headers
of the response, so you never have to parse the message. Rescue as narrowly or as broadly as you need:
begin
KeycloakAdmin.realm("a-realm").users.get(user_id)
rescue KeycloakAdmin::NotFoundError => e # exactly 404
nil
rescue KeycloakAdmin::ClientError => e # any 4xx
logger.warn("Rejected with #{e.status}: #{e.body}")
raise
rescue KeycloakAdmin::ServerError => e # any 5xx
raise
end| Class | Raised on |
|---|---|
KeycloakAdmin::Error |
Base class of every error this gem raises |
KeycloakAdmin::ApiError |
Any non-2xx answer; parent of the two families below |
KeycloakAdmin::ClientError |
Any 4xx |
KeycloakAdmin::BadRequestError |
400 |
KeycloakAdmin::UnauthorizedError |
401 |
KeycloakAdmin::ForbiddenError |
403 |
KeycloakAdmin::NotFoundError |
404 |
KeycloakAdmin::ConflictError |
409 |
KeycloakAdmin::ServerError |
Any 5xx |
KeycloakAdmin::UnexpectedResponseError |
A create call answered something other than 201 Created |
Faraday::TimeoutError and the other connection-level Faraday errors are left untouched and propagate as-is, since no response ever came back to describe.
A 401 is handled before it reaches you: the cached access token is dropped and the request is replayed once with a freshly fetched one, so a token revoked before its advertised expiry does not fail every call until it lapses KeycloakAdmin::UnauthorizedError is raised only if the replay is
refused too.
- Get an access token
- Create/update/get/delete a user
- Get list of users, search for user(s)
- List credentials of a user
- Reset credentials
- Impersonate a user
- Exchange a configurable token
- Get list of clients, or find a client by its id or client_id
- Create, update, and delete clients
- Get list of groups, create/save a group
- Get list of roles, save a role
- Get list of realms, save/update/delete a realm
- Get list of client role mappings for a user/group
- Get list of members of a group
- Get list of groups that have a specific role assigned
- Get list of realm-roles assigned to a group, add a realm-role to a group
- Save client role mappings for a user/group
- Save realm-level role mappings for a user/group
- Add a Group on a User
- Remove a Group from a User
- Get list of Identity Providers
- Create Identity Providers
- Link/Unlink users to federated identity provider brokers
- Execute actions emails
- Send forgot passsword mail
- Client Authorization, create, update, get, delete Resource, Scope, Policy, Permission, Policy Enforcer
- Get list of client scopes, create/save/get/delete/search a client scope
- Get list of protocol mappers for a client scope, create/save/get/delete a protocol mapper
- Get list of organizations, create/update/get/delete an organization
- Get list of members of an organization, add/remove members
- Invite new or existing users to an organization
- List, add, and remove Identity Providers for an organization
- Get list of organizations associated with a specific user
Returns an instance of KeycloakAdmin::TokenRepresentation.
KeycloakAdmin.realm("a_realm").token.getReturns an instance of KeycloakAdmin::UserRepresentation or nil when this user does not exist.
user_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
KeycloakAdmin.realm("a_realm").users.get(user_id)Returns an array of KeycloakAdmin::UserRepresentation.
According to the documentation:
- When providing a
Stringparameter, this produces an arbitrary search string - When providing a
Hash, you can search for specific field (e.g an email)
KeycloakAdmin.realm("a_realm").users.search("a_username_or_an_email")KeycloakAdmin.realm("a_realm").users.search({ email: "john@doe.com" })Returns an array of KeycloakAdmin::UserRepresentation.
KeycloakAdmin.realm("a_realm").users.listKeycloak answers this endpoint with at most 100 users unless told otherwise, so a bare list silently truncates a larger realm. Pass max (and first to walk the pages) to go past it:
KeycloakAdmin.realm("a_realm").users.list(max: 500)
KeycloakAdmin.realm("a_realm").users.list(first: 100, max: 100)Returns the provided user, which must be of type KeycloakAdmin::UserRepresentation.
KeycloakAdmin.realm("a_realm").users.save(user)If you want to update its entire entity. To update some specific attributes, provide an object implementing to_json, such as a Hash.
KeycloakAdmin.realm("a_realm").users.update("05c135c6-5ad8-4e17-b1fa-635fc089fd71", {
email: "hello@gmail.com",
username: "hello",
first_name: "Jean",
last_name: "Dupond"
})Attention point: Since Keycloak 24.0.4, when updating a user, all the writable profile attributes must be passed, otherwise they will be removed. (https://www.keycloak.org/docs/24.0.4/upgrading/)
KeycloakAdmin.realm("a_realm").users.delete(user_id)Returns the created user of type KeycloakAdmin::UserRepresentation.
username = "pioupioux"
email = "pioupioux@email.com"
password = "acme0"
email_verified = true
locale = "en"
KeycloakAdmin.realm("a_realm").users.create!(username, email, password, email_verified, locale)user_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
new_password = "coco"
KeycloakAdmin.realm("a_realm").users.update_password(user_id, new_password)Pass temporary: true to force the user to choose a new password at their next login. Keycloak
records this as an UPDATE_PASSWORD required action on the account; a later permanent reset
clears it.
KeycloakAdmin.realm("a_realm").users.update_password(user_id, new_password, temporary: true)user_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
KeycloakAdmin.realm("a_realm").users.credentials(user_id)Returns an instance of KeycloakAdmin::ImpersonationRepresentation.
user_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
KeycloakAdmin.realm("a_realm").users.impersonate(user_id)To have enough information to execute an impersonation by yourself, get_redirect_impersonation returns an instance of KeycloakAdmin::ImpersonationRedirectionRepresentation.
user_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
KeycloakAdmin.realm("a_realm").users.get_redirect_impersonation(user_id)KeycloakAdmin.realm("a_realm").organizations.listRequires your Keycloak server to have deployed the Custom REST API configurable-token (https://github.com/looorent/keycloak-configurable-token-api)
Returns an instance of KeycloakAdmin::TokenRepresentation.
user_access_token = "abqsdofnqdsogn"
token_lifespan_in_seconds = 20
KeycloakAdmin.realm("a_realm").configurable_token.exchange_with(user_access_token, token_lifespan_in_seconds)Returns an array of KeycloakAdmin::RealmRepresentation.
KeycloakAdmin.realm("master").listTakes realm of type KeycloakAdmin::RealmRepresentation, or an object implementing to_json, such as a Hash.
KeycloakAdmin.realm(nil).save(realm)If you want to update its entire entity. To update some specific attributes, provide an object implementing to_json, such as a Hash.
KeycloakAdmin.realm("a_realm").update({
smtpServer: { host: 'test_host' }
})KeycloakAdmin.realm("a_realm").deleteReturns an array of KeycloakAdmin::ClientRepresentation or a single KeycloakAdmin::ClientRepresentation
Finding a client by its client_id is a somewhat slow operation, as it requires fetching all clients and then filtering. Keycloak's API does not support fetching a client by its client_id directly.
KeycloakAdmin.realm("a_realm").clients.list
KeycloakAdmin.realm("a_realm").clients.get(id) # id is Keycloak's database id, not the client_id
KeycloakAdmin.realm("a_realm").clients.find_by_client_id(client_id)my_client = KeycloakAdmin.realm("a_realm").clients.get(id)
my_client.name = "My new client name"
my_client.description = "This is a new description"
my_client.redirect_uris << "https://www.example.com/auth/callback"
KeycloakAdmin.realm("a_realm").clients.update(client) # Returns the updated clientReturns an array of KeycloakAdmin::GroupRepresentation.
KeycloakAdmin.realm("a_realm").groups.listThis endpoint applies no default cap, so a bare list already returns every group. first and max are still accepted, to page through a realm holding a lot of them:
KeycloakAdmin.realm("a_realm").groups.list(first: 0, max: 100)Returns an array of KeycloakAdmin::GroupRepresentation.
According to the documentation:
- When providing a
Stringparameter, this produces an arbitrary search string - When providing a
Hash, you can specify other fields (e.g q, max, first)
KeycloakAdmin.realm("a_realm").groups.search("MyGroup")KeycloakAdmin.realm("a_realm").groups.search({query: "MyGroup", exact: true, max: 1})Returns the id of saved group provided, which must be of type KeycloakAdmin::GroupRepresentation.
KeycloakAdmin.realm("a_realm").groups.save(group)Returns the id of created group.
group_name = "test"
group_path = "/top"
group_id = KeycloakAdmin.realm("a_realm").groups.create!(group_name, group_path)Create a new group as the child of an existing group.
parent_id = "7686af34-204c-4515-8122-78d19febbf6e"
group_name = "test"
sub_group_id = KeycloakAdmin.realm("a_realm").groups.create_subgroup!(parent_id, group_name)Returns an array of KeycloakAdmin::UserRepresentation.
KeycloakAdmin.realm("a_realm").group("group_id").membersYou can specify paging with first and max:
KeycloakAdmin.realm("a_realm").group("group_id").members(first:0, max:100)Returns an array of KeycloakAdmin::GroupRepresentation
KeycloakAdmin.realm("a_realm").roles.list_groups("role_name")Returns an array of KeycloakAdmin::RoleRepresentation
KeycloakAdmin.realm("a_realm").groups.get_realm_level_roles("group_id")Returns added KeycloakAdmin::RoleRepresentation
KeycloakAdmin.realm("a_realm").groups.add_realm_level_role_name!("group_id", "role_name")Returns an array of KeycloakAdmin::RoleRepresentation.
KeycloakAdmin.realm("a_realm").roles.listTakes role, which must be of type KeycloakAdmin::RoleRepresentation.
KeycloakAdmin.realm("a_realm").roles.save(role)Returns an array of KeycloakAdmin::RoleRepresentation.
user_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
client_id = "1869e876-71b4-4de2-849e-66540db3a098"
KeycloakAdmin.realm("a_realm").user(user_id).client_role_mappings(client_id).list_availableor
group_id = "3a63b5c0-ef8a-47fd-86ed-b5fead18d9b8"
client_id = "1869e876-71b4-4de2-849e-66540db3a098"
KeycloakAdmin.realm("a_realm").group(group_id).client_role_mappings(client_id).list_availableTakes role_list, which must be an array of type KeycloakAdmin::RoleRepresentation.
user_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
client_id = "1869e876-71b4-4de2-849e-66540db3a098"
KeycloakAdmin.realm("a_realm").user(user_id).client_role_mappings(client_id).save(role_list)or
group_id = "3a63b5c0-ef8a-47fd-86ed-b5fead18d9b8"
client_id = "1869e876-71b4-4de2-849e-66540db3a098"
KeycloakAdmin.realm("a_realm").group(group_id).client_role_mappings(client_id).save(role_list)Takes role_list, which must be an array of type KeycloakAdmin::RoleRepresentation.
user_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
KeycloakAdmin.realm("a_realm").user(user_id).role_mapper.save_realm_level(role_list)or
group_id = "3a63b5c0-ef8a-47fd-86ed-b5fead18d9b8"
KeycloakAdmin.realm("a_realm").group(group_id).role_mapper.save_realm_level(role_list)Note: This client requires the realm-management.view-identity-providers role.
Returns an array of KeycloakAdmin::IdentityProviderRepresentation.
KeycloakAdmin.realm("a_realm").identity_providers.listIn order to use authorization, you need to enable the client's authorization_services_enabled attribute.
client_id = "dummy-client"
client = KeycloakAdmin.realm("realm_a").clients.find_by_client_id(client_id)
client.authorization_services_enabled = true
KeycloakAdmin.realm("a_realm").clients.update(client)Returns added KeycloakAdmin::ClientAuthzScopeRepresentation
KeycloakAdmin.realm("a_realm").authz_scopes(client_id).create!("POST_1", "POST 1 scope description", "http://icon.url")Returns array of KeycloakAdmin::ClientAuthzScopeRepresentation
KeycloakAdmin.realm("a_realm").authz_scopes(client.id).search("POST")Returns KeycloakAdmin::ClientAuthzScopeRepresentation
KeycloakAdmin.realm("a_realm").authz_scopes(client.id).get(scope_id)KeycloakAdmin.realm("a_realm").authz_scopes(client.id).delete(scope.id)note: for scopes, use {name: scope.name} to reference the scope object
Returns added KeycloakAdmin::ClientAuthzResourceRepresentation
KeycloakAdmin.realm("realm_id")
.authz_resources(client.id)
.create!(
"Dummy Resource",
"type",
["/resource_1/*", "/resource_1/"],
true,
"display_name",
[ {name: scope_1.name} ],
{"attribute": ["value_1", "value_2"]}
)Returns updated KeycloakAdmin::ClientAuthzResourceRepresentation
note: for scopes, use {name: scope.name} to reference the scope object
KeycloakAdmin.realm("realm_a")
.authz_resources(client.id)
.update(resource.id,
{
"name": "Dummy Resource",
"type": "type",
"owner_managed_access": true,
"display_name": "display_name",
"attributes": {"a":["b","c"]},
"uris": [ "/resource_1/*" , "/resource_1/" ],
"scopes":[
{name: scope_1.name},
{name: scope_2.name}
],
"icon_uri": "https://icon.url"
})Returns array of KeycloakAdmin::ClientAuthzResourceRepresentation
KeycloakAdmin.realm("realm_a").authz_resources(client.id).find_by("Dummy Resource", "", "", "", "")or
KeycloakAdmin.realm("realm_a").authz_resources(client.id).find_by("", "type", "", "", "")Returns KeycloakAdmin::ClientAuthzResourceRepresentation
KeycloakAdmin.realm("realm_a").authz_resources(client.id).get(resource.id)KeycloakAdmin.realm("realm_a").authz_resources(client.id).delete(resource.id)Note: for the moment only role policies are supported.
Returns added KeycloakAdmin::ClientAuthzPolicyRepresentation
KeycloakAdmin.realm("realm_a")
.authz_policies(client.id, 'role')
.create!("Policy 1",
"description",
"role",
"POSITIVE",
"UNANIMOUS",
true,
[{id: realm_role.id, required: true}]
)Returns array of KeycloakAdmin::ClientAuthzPolicyRepresentation
KeycloakAdmin.realm("realm_a").authz_policies(client.id, 'role').find_by("Policy 1", "role") Returns KeycloakAdmin::ClientAuthzPolicyRepresentation
KeycloakAdmin.realm("realm_a").authz_policies(client.id, 'role').get(policy.id) KeycloakAdmin.realm("realm_a").authz_policies(client.id, 'role').delete(policy.id)Returns added KeycloakAdmin::ClientAuthzPermissionRepresentation
KeycloakAdmin.realm("realm_a")
.authz_permissions(client.id, :resource)
.create!("Dummy Resource Permission",
"resource description",
"UNANIMOUS",
"POSITIVE",
[resource.id],
[policy.id],
nil,
""
)Returns added KeycloakAdmin::ClientAuthzPermissionRepresentation
KeycloakAdmin.realm("realm_a")
.authz_permissions(client.id, :scope)
.create!("Dummy Scope Permission",
"scope description",
"UNANIMOUS",
"POSITIVE",
[resource.id],
[policy.id],
[scope_1.id, scope_2.id],
""
) Return array of KeycloakAdmin::ClientAuthzPermissionRepresentation
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, "", resource.id).listReturn array of KeycloakAdmin::ClientAuthzPermissionRepresentation
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, 'resource').listReturn array of KeycloakAdmin::ClientAuthzPermissionRepresentation
authz_permissions(client.id, 'scope').list.sizeReturn array of KeycloakAdmin::ClientAuthzPermissionRepresentation
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, "resource").find_by(resource_permission.name, nil)or
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, "resource").find_by(resource_permission.name, nil)or
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, "resource").find_by(resource_permission.name, resource.id)or
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, "scope").find_by(scope_permission.name, resource.id)or
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, "scope").find_by(scope_permission.name, resource.id, "POST_1")or
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, "resource").find_by(nil, resource.id)or
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, "scope").find_by(nil, resource.id)or
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, "scope").find_by(nil, resource.id, "POST_1")or
KeycloakAdmin.realm("realm_a").authz_permissions(client.id, "scope").find_by(scope_permission.name, nil)KeycloakAdmin.realm("realm_a").authz_permissions(client.id, 'scope').delete(scope.id) KeycloakAdmin.realm("realm_a").authz_permissions(client.id, 'resource').delete(resource_permission.id)Returns an array of KeycloakAdmin::ClientScopeRepresentation.
KeycloakAdmin.realm("a_realm").client_scopes.listReturns an instance of KeycloakAdmin::ClientScopeRepresentation.
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
KeycloakAdmin.realm("a_realm").client_scopes.get(client_scope_id)Returns an array of KeycloakAdmin::ClientScopeRepresentation whose names contain the given substring.
KeycloakAdmin.realm("a_realm").client_scopes.search("my-scope")Takes scope_representation of type KeycloakAdmin::ClientScopeRepresentation. Returns true on success.
scope = KeycloakAdmin::ClientScopeRepresentation.new
scope.name = "my-scope"
scope.description = "My custom scope"
scope.protocol = "openid-connect"
scope.attributes = { "display.on.consent.screen" => "true", "include.in.token.scope" => "true" }
KeycloakAdmin.realm("a_realm").client_scopes.create!(scope)Takes scope_representation of type KeycloakAdmin::ClientScopeRepresentation (must include its id). Returns true on success.
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
scope = KeycloakAdmin.realm("a_realm").client_scopes.get(client_scope_id)
scope.description = "Updated description"
KeycloakAdmin.realm("a_realm").client_scopes.save(scope)client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
KeycloakAdmin.realm("a_realm").client_scopes.delete(client_scope_id)Protocol mappers allow you to transform tokens and assertions. The following operations are available on the protocol mappers of a given client scope.
Returns an array of KeycloakAdmin::ProtocolMapperRepresentation.
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).listReturns an instance of KeycloakAdmin::ProtocolMapperRepresentation.
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
mapper_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).get(mapper_id)Takes mapper_representation of type KeycloakAdmin::ProtocolMapperRepresentation. Returns true on success.
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
mapper = KeycloakAdmin::ProtocolMapperRepresentation.new
mapper.name = "my-mapper"
mapper.protocol = "openid-connect"
mapper.protocolMapper = "oidc-usermodel-attribute-mapper"
mapper.config = { "user.attribute" => "locale", "claim.name" => "locale", "jsonType.label" => "String", "id.token.claim" => "true", "access.token.claim" => "true", "userinfo.token.claim" => "true" }
KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).create!(mapper)Takes mapper_representation of type KeycloakAdmin::ProtocolMapperRepresentation (must include its id). Returns true on success.
client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
mapper = KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).get(mapper_id)
mapper.config["claim.name"] = "updated_claim"
KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).save(mapper)client_scope_id = "7686af34-204c-4515-8122-78d19febbf6e"
mapper_id = "95985b21-d884-4bbd-b852-cb8cd365afc2"
KeycloakAdmin.realm("a_realm").client_scope_protocol_mappers(client_scope_id).delete(mapper_id)Unit tests need nothing but the gems:
$ bundle install
$ bundle exec rake spec
Integration tests run against a real Keycloak. docker-compose.yml boots one configured like the one CI uses, so a green run locally means the same thing as a green run in CI:
$ docker compose up -d
$ bundle exec rake integration
To reproduce a failure specific to an older Keycloak, pin image: in docker-compose.yml to that version and start over with docker compose down -v && docker compose up -d. The suite is exercised in CI against Keycloak 19.0, 21.0, 23.0, 25.0 and 26.7.0.
Everything can also run inside a container:
$ docker build . -t keycloak-admin:test
$ docker run -v `pwd`:/usr/src/app/ keycloak-admin:test rspec spec
Releases are published to RubyGems by GitHub Actions
(.github/workflows/release.yml), through Trusted Publishing:
no API key is stored in this repository, the workflow exchanges a short-lived GitHub OIDC token for a
scoped RubyGems credential.
- Update
KeycloakAdmin::VERSIONinlib/keycloak-admin/version.rband theCHANGELOG.md - Commit and push these changes to
main - Tag the commit and push the tag:
$ git tag -a v2.0.3 -m "Version 2.0.3"
$ git push origin v2.0.3