Keycloak API
Python module to automate Keycloak or Red Hat Single Sign-On (RHSSO) configuration.
How To Install
pip install kcapi
Testing
To run the test you would need a Keycloak instance, you can run one locally or in the cloud then you just have to follow this steps:
python3.10 -m venv .venv
source .venv/bin/activate
pip install requests
# Setup SSO server - go to https://developers.redhat.com/developer-sandbox/get-started,
# launch sandbox environment, +Add, select some "Red Hat Single Sign-On..." template.
export KC_USER=admin
export KC_PASSWORD=admin_password
export KC_REALM=myrealm # do not use master realm, it cannot be removed
export KC_ENDPOINT=https://my-first-sso-me-me-dev.apps.sandbox.x8i5.p1.openshiftapps.com
python -m unittest
API
OpenID
This class takes care of OpenID login using password owner credentials flow.
Constructor
from rhsso import OpenID
oid_client = OpenID({
"client_id": "admin-cli",
"username": USER,
"password": PASSWORD,
"grant_type":"password",
"realm" : "master"
}, endpoint)
- client_id: Client Identifier in Keycloak.
- username: Login username for the Realm.
- password: Login password for the Realm.
- grant_type: The grant type you want to use (usually
password
). - endpoint: A Keycloak or RHSSO URL endpoint, something like:
https://my_keycloak.com
.
Methods
getToken
This will initiate a session with the Keycloak server and will return a OpenID token back.
oid_client.getToken()
createAdminClient
This static method should be used in order to access the master Realm in Keycloak.
oidc = OpenID.createAdminClient(self.USER, self.PASSWORD)
oidc.getToken()
Keycloak
This class builds all the Keycloak configuration REST resources by using REST conventions we can target the majority of Keycloak services.
Constructor
kc = Keycloak(token, self.ENDPOINT)
The constructor takes two parameters:
- token: A token with enough priviledge to perform the desired operation.
- endpoint: A Keycloak or RHSSO URL endpoint, something like:
https://my_keycloak.com
.
Methods
build
This methods build a REST client (capabilities detailed below) targeting a specific Keycloak REST resource.
groups = kc.build('groups', 'my_realm')
state = groups.create({"name": "DC"}).isOk()
In this example we build the 'groups' API for my_realm
Realm.
Supported Resources
Here is a quick list of supported resources:
As long as you find a REST endpoint that follow the standard you can use this method to build a client around it, an example of this is the non well documented components
endpoint.
admin
Similar to the build
method but the client points to the master
realm, allowing us operation such as realm creation.
main_realm = kc.admin()
main_realm.create({"enabled": "true", "id": my_realm, "realm": my_realm})
REST API
When you use the build
or admin
methods you will get back a REST class pointing to the Keycloak resource, keep in mind that this class don't check that the resource is valid, this is done to keep it flexible and to make it easy to adapt to new Keycloak REST API changes in the future.
Usage
In order to create one you need to build
method we have used before:
batman = {
"enabled":'true',
"attributes":{},
"username":"batman",
"firstName":"Bruce",
"lastName":"Wayne",
"emailVerified":""
}
users = kc.build('users', 'DC')
state = users.create(batman).isOk()
Methods
Following the example above lets see the methods we have starting with the usual CRUD methods:
create
This method POST
a dictionary into any given resource:
batman = {
"enabled":'true',
"attributes":{},
"username":"batman",
"firstName":"Bruce",
"lastName":"Wayne",
"emailVerified":""
}
state = users.create(batman).isOk()
- dictionary: Dictionary with the fields we want to POST to the server.
update
This method performs a PUT
on the resource.
batman_update = {
"firstName":"Bruno",
"emailVerified": True
}
id = 'bf81a9d9-811f-4807-bd69-3d74eecbe9f4'
state = users.update(id, batman_update).isOk()
- id: Id of the resource in Keycloak.
- dictionary: Dictionary representing the updated fields.
remove
This method sends a DELETE
to the pointed resource.
batman_update = {
"firstName":"Bruno",
"emailVerified": True
}
id = 'bf81a9d9-811f-4807-bd69-3d74eecbe9f4'
state = users.remove(id).isOk()
- id: Id of the resource in Keycloak.
- dictionary: Dictionary representing the updated fields.
get
Send a GET
request to retrieve a specific Keycloak resource.
id = 'bf81a9d9-811f-4807-bd69-3d74eecbe9f4'
user = users.get(id).response()
all
Return all objects of a particular resource type.
users = kc.build('users', 'DC')
user_list = users.all()
findFirst
Finds a resource by passing an arbitrary key/value pair.
users = kc.build('users', 'DC')
users.findFirst({"key":"username", "value": 'batman'})
exist
Check if a resource matching the provided id
exists:
users = kc.build('users', 'DC')
id = 'bf81a9d9-811f-4807-bd69-3d74eecbe9f4'
users.exists(id)
existByKV
Check if a resource matching the provided key/value pair, exists.
users = kc.build('users', 'DC')
users.existByKV("username", 'batman')
ResponseHandler
Each CRUD method returns a ResponseHandler
class with the following methods.
Methods
response
returns the requests response object.
users.update(id, batman_update).response().status_code
isOk
Return True
if the request complete successfully otherwise it will raise an exception.
state = users.update(id, batman_update).isOk()
verify
Does the same as isOk
but it allow you to chain more methods.
batman_update = {
"firstName":"Bruno",
"emailVerified": True
}
id = 'bf81a9d9-811f-4807-bd69-3d74eecbe9f4'
cookies = users.update(id, batman_update).verify().response().cookies
Specialisations
Realms
KeycloakCaches
This class handles the Keycloak caches.
Instantiation
realms = kc.build('realms', 'my_realm')
caches = realms.caches(self.REALM)
clearUserCache
This method tells Keycloak to clear the user cache.
caches.clearUserCache()
clearRealmCache
This method tells Keycloak to clear the realm cache.
caches.clearRealmCache()
clearKeyCache
This method tells Keycloak to clear the external public key cache for clients and identity providers.
caches.clearKeyCache()
For more information on how this caches works follow this link.
Users
updateCredentials
Update user credentials.
user_credentials = {
'temporary': False,
'value':'12345'
}
state = users.updateCredentials(user_info, user_credentials).isOk() # Updated user password.
Where:
- temporary: Boolean where if
True
provide a temporary password just for the first login. - value: String with the password.
joinGroup
Add a user into a existing group.
First we need a group:
def createDCGroup():
group = kc.build('groups', 'heroes')
return group.create({"name": "DC"}).isOk()
Then we can join the group the following way:
createDCGroup()
users = kc.build('users', 'heroes')
user = {"key": "username", "value": "batman"}
group = {"key": "name", "value": "DC"}
users.joinGroup(user, group).isOk()
The API works by matching the first occurrence between the provided key/value
for the two resources (User and Group), this can help in various situation for example if we want to target the user by uuid
.
Using uuid
as user identifier.
createDCGroup()
users = kc.build('users', 'heroes')
user = {"key": "uuid", "value": "23e4567-e89b-..."}
group = {"key": "name", "value": "DC"}
users.joinGroup(user, group).isOk()
Or we want to use the group id
:
user = {"key": "uuid", "value": "23e4567-e89b-..."}
group = {"key": "id", "value": "f8d91722-a1f0-45e..."}
users.joinGroup(user, group).isOk()
If the field criteria don't return a unique value, the first entry in the list will be used.
leaveGroup
Remove a user from a group.
createDCGroup()
users = kc.build('users', 'heroes')
user = {"key": "username", "value": "batman"}
group = {"key": "uuid", "value": "123e4567-e89b-..."}
users.leaveGroup(user, group).isOk()
user = {"key": "uuid", "value": "12d3-a456-4"}
group = {"key": "id", "value": "123e4567-e89b-..."}
users.leaveGroup(user, group).isOk()
The same rules for key/value
discussed above also applies here.
Groups
To manage the relationship between realm level roles and groups, we can use the RealmsRolesMapping.
To get an instance of this class you need to instantiate the group
resource class:
groups = kc.build('groups', 'heroes')
And use the method realmRoles
passing a valid group dictionary:
realmsRoles = groups.realmRoles({"key":"name", "value":'DC'})
Then we get a class with following methods:
add
Add a list of existing roles to a group.
def makeRoles(self):
roles = kc.build('roles', self.realm)
lvl1 = roles.create({"name": "level-1"}).isOk()
lvl2 = roles.create({"name": "level-2"}).isOk()
return lvl1 and lvl2
if makeRoles():
realmsRoles = groups.realmRoles({"key":"name", "value":'DC'})
realmsRoles.add(["level-1", "level-2"])
remove
Remove a list of associated roles from a group.
realmsRoles = groups.realmRoles({"key":"name", "value":'DC'})
realmsRoles.remove(["level-1", "level-2"])
Roles
composite
In Keycloak we can map roles to other roles, this method allow you to do just that.
role_watch = self.kc.build('roles', 'my-realm').find('watch')
added = role_watch.add_composite('view')