Browse Source

Fixed merge

pull/265/head
Marcos Pereira Jr 3 years ago
parent
commit
ea47d0052b
  1. 1
      Pipfile
  2. 36
      Pipfile.lock
  3. 24
      README.md
  4. 0
      bin/deploy.sh
  5. 4
      docs/source/conf.py
  6. 9
      keycloak/connection.py
  7. 250
      keycloak/keycloak_admin.py
  8. 2
      keycloak/keycloak_openid.py
  9. 2
      keycloak/tests/test_connection.py
  10. 12
      keycloak/urls_patterns.py
  11. 3
      requirements.txt
  12. 2
      setup.py

1
Pipfile

@ -7,6 +7,7 @@ name = "pypi"
requests = ">=2.20.0" requests = ">=2.20.0"
httmock = ">=1.2.5" httmock = ">=1.2.5"
python-jose = ">=1.4.0" python-jose = ">=1.4.0"
urllib3 = ">=1.26.5"
[dev-packages] [dev-packages]

36
Pipfile.lock

@ -18,10 +18,10 @@
"default": { "default": {
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
], ],
"version": "==2019.9.11"
"version": "==2020.12.5"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@ -32,17 +32,16 @@
}, },
"ecdsa": { "ecdsa": {
"hashes": [ "hashes": [
"sha256:163c80b064a763ea733870feb96f9dd9b92216cfcacd374837af18e4e8ec3d4d",
"sha256:9814e700890991abeceeb2242586024d4758c8fc18445b194a49bd62d85861db"
"sha256:881fa5e12bb992972d3d1b3d4dfbe149ab76a89f13da02daa5ea1ec7dea6e747",
"sha256:cfc046a2ddd425adbd1a78b3c46f0d1325c657811c0f45ecc3a0a6236c1e50ff"
], ],
"index": "pypi",
"version": "==0.13.3"
"version": "==0.16.1"
}, },
"future": { "future": {
"hashes": [ "hashes": [
"sha256:6142ef79e2416e432931d527452a1cab3aa4a754a0a53d25b2589f79e1106f34"
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
], ],
"version": "==0.18.0"
"version": "==0.18.2"
}, },
"httmock": { "httmock": {
"hashes": [ "hashes": [
@ -60,10 +59,10 @@
}, },
"pyasn1": { "pyasn1": {
"hashes": [ "hashes": [
"sha256:62cdade8b5530f0b185e09855dd422bc05c0bbff6b72ff61381c09dac7befd8c",
"sha256:a9495356ca1d66ed197a0f72b41eb1823cf7ea8b5bd07191673e8147aecf8604"
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
], ],
"version": "==0.4.7"
"version": "==0.4.8"
}, },
"python-jose": { "python-jose": {
"hashes": [ "hashes": [
@ -83,17 +82,18 @@
}, },
"rsa": { "rsa": {
"hashes": [ "hashes": [
"sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66",
"sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487"
"sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4",
"sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b"
], ],
"version": "==4.0"
"index": "pypi",
"version": "==4.7"
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
], ],
"version": "==1.12.0"
"version": "==1.15.0"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [

24
README.md

@ -54,6 +54,7 @@ The documentation for python-keycloak is available on [readthedocs](http://pytho
* [Josha Inglis](https://bitbucket.org/joshainglis/) * [Josha Inglis](https://bitbucket.org/joshainglis/)
* [Alex](https://bitbucket.org/alex_zel/) * [Alex](https://bitbucket.org/alex_zel/)
* [Ewan Jone](https://bitbucket.org/kisamoto/) * [Ewan Jone](https://bitbucket.org/kisamoto/)
* [Lukas Martini](https://github.com/lutoma)
## Usage ## Usage
@ -97,7 +98,7 @@ token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['ac
token_info = keycloak_openid.introspect(token['access_token']) token_info = keycloak_openid.introspect(token['access_token'])
# Decode Token # Decode Token
KEYCLOAK_PUBLIC_KEY = keycloak_openid.public_key()
KEYCLOAK_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" + keycloak_openid.public_key() + "\n-----END PUBLIC KEY-----"
options = {"verify_signature": True, "verify_aud": True, "verify_exp": True} options = {"verify_signature": True, "verify_aud": True, "verify_exp": True}
token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options)
@ -126,6 +127,15 @@ new_user = keycloak_admin.create_user({"email": "example@example.com",
"firstName": "Example", "firstName": "Example",
"lastName": "Example"}) "lastName": "Example"})
# Add user and raise exception if username already exists
# exist_ok currently defaults to True for backwards compatibility reasons
new_user = keycloak_admin.create_user({"email": "example@example.com",
"username": "example@example.com",
"enabled": True,
"firstName": "Example",
"lastName": "Example"},
exist_ok=False)
# Add user and set password # Add user and set password
new_user = keycloak_admin.create_user({"email": "example@example.com", new_user = keycloak_admin.create_user({"email": "example@example.com",
"username": "example@example.com", "username": "example@example.com",
@ -134,6 +144,16 @@ new_user = keycloak_admin.create_user({"email": "example@example.com",
"lastName": "Example", "lastName": "Example",
"credentials": [{"value": "secret","type": "password",}]}) "credentials": [{"value": "secret","type": "password",}]})
# Add user and specify a locale
new_user = keycloak_admin.create_user({"email": "example@example.fr",
"username": "example@example.fr",
"enabled": True,
"firstName": "Example",
"lastName": "Example",
"attributes": {
"locale": ["fr"]
})
# User counter # User counter
count_users = keycloak_admin.users_count() count_users = keycloak_admin.users_count()
@ -195,7 +215,7 @@ role = keycloak_admin.get_client_role(client_id="client_id", role_name="role_nam
role_id = keycloak_admin.get_client_role_id(client_id="client_id", role_name="test") role_id = keycloak_admin.get_client_role_id(client_id="client_id", role_name="test")
# Create client role # Create client role
keycloak_admin.create_client_role(client_id='client_id', {'name': 'roleName', 'clientRole': True})
keycloak_admin.create_client_role(client_role_id='client_id', {'name': 'roleName', 'clientRole': True})
# Assign client role to user. Note that BOTH role_name and role_id appear to be required. # Assign client role to user. Note that BOTH role_name and role_id appear to be required.
keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test") keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test")

0
bin/deploy.sh

4
docs/source/conf.py

@ -60,9 +60,9 @@ author = 'Marcos Pereira'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '0.23.0'
version = '0.26.1'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '0.23.0'
release = '0.26.1'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.

9
keycloak/connection.py

@ -54,12 +54,15 @@ class ConnectionManager(object):
for protocol in ('https://', 'http://'): for protocol in ('https://', 'http://'):
adapter = HTTPAdapter(max_retries=1) adapter = HTTPAdapter(max_retries=1)
# adds POST to retry whitelist # adds POST to retry whitelist
method_whitelist = set(adapter.max_retries.method_whitelist)
method_whitelist.add('POST')
adapter.max_retries.method_whitelist = frozenset(method_whitelist)
allowed_methods = set(adapter.max_retries.allowed_methods)
allowed_methods.add('POST')
adapter.max_retries.allowed_methods = frozenset(allowed_methods)
self._s.mount(protocol, adapter) self._s.mount(protocol, adapter)
def __del__(self):
self._s.close()
@property @property
def base_url(self): def base_url(self):
""" Return base url in use for requests to the server. """ """ Return base url in use for requests to the server. """

250
keycloak/keycloak_admin.py

@ -33,9 +33,9 @@ from .connection import ConnectionManager
from .exceptions import raise_error_from_response, KeycloakGetError from .exceptions import raise_error_from_response, KeycloakGetError
from .keycloak_openid import KeycloakOpenID from .keycloak_openid import KeycloakOpenID
from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \
URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES,\
URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES, \
URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \
URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_GROUPS_CLIENT_ROLES, \
URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \
URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, URL_ADMIN_USER_GROUP, URL_ADMIN_REALM_ROLES, URL_ADMIN_GROUP_CHILD, \ URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, URL_ADMIN_USER_GROUP, URL_ADMIN_REALM_ROLES, URL_ADMIN_GROUP_CHILD, \
URL_ADMIN_USER_CONSENTS, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_CLIENT, URL_ADMIN_USER, URL_ADMIN_CLIENT_ROLE, \ URL_ADMIN_USER_CONSENTS, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_CLIENT, URL_ADMIN_USER, URL_ADMIN_CLIENT_ROLE, \
URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \ URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \
@ -47,7 +47,9 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC
URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \
URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \
URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \
URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER
URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \
URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS, \
URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT
class KeycloakAdmin: class KeycloakAdmin:
@ -77,7 +79,7 @@ class KeycloakAdmin:
:param realm_name: realm name :param realm_name: realm name
:param client_id: client id :param client_id: client id
:param verify: True if want check connection SSL :param verify: True if want check connection SSL
:param client_secret_key: client secret key
:param client_secret_key: client secret key (optional, required only for access type confidential)
:param custom_headers: dict of custom header to pass to each HTML request :param custom_headers: dict of custom header to pass to each HTML request
:param user_realm_name: The realm name of the user, if different from realm_name :param user_realm_name: The realm name of the user, if different from realm_name
:param auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete'] :param auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete']
@ -223,9 +225,18 @@ class KeycloakAdmin:
if not partial_results: if not partial_results:
break break
results.extend(partial_results) results.extend(partial_results)
if len(partial_results) < query['max']:
break
page += 1 page += 1
return results return results
def __fetch_paginated(self, url, query=None):
query = query or {}
return raise_error_from_response(
self.raw_get(url, **query),
KeycloakGetError)
def import_realm(self, payload): def import_realm(self, payload):
""" """
Import a new realm from a RealmRepresentation. Realm name must be unique. Import a new realm from a RealmRepresentation. Realm name must be unique.
@ -242,6 +253,22 @@ class KeycloakAdmin:
data=json.dumps(payload)) data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201])
def export_realm(self, export_clients=False, export_groups_and_role=False):
"""
Export the realm configurations in the json format
RealmRepresentation
https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_partialexport
:param export-clients: Skip if not want to export realm clients
:param export-groups-and-roles: Skip if not want to export realm groups and roles
:return: realm configurations JSON
"""
params_path = {"realm-name": self.realm_name, "export-clients": export_clients, "export-groups-and-roles": export_groups_and_role }
data_raw = self.raw_post(URL_ADMIN_REALM_EXPORT.format(**params_path), data="")
return raise_error_from_response(data_raw, KeycloakGetError)
def get_realms(self): def get_realms(self):
""" """
Lists all realms in Keycloak deployment Lists all realms in Keycloak deployment
@ -307,8 +334,14 @@ class KeycloakAdmin:
:param query: Query parameters (optional) :param query: Query parameters (optional)
:return: users list :return: users list
""" """
query = query or {}
params_path = {"realm-name": self.realm_name} params_path = {"realm-name": self.realm_name}
return self.__fetch_all(URL_ADMIN_USERS.format(**params_path), query)
url = URL_ADMIN_USERS.format(**params_path)
if "first" in query or "max" in query:
return self.__fetch_paginated(url, query)
return self.__fetch_all(url, query)
def create_idp(self, payload): def create_idp(self, payload):
""" """
@ -362,7 +395,7 @@ class KeycloakAdmin:
data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def create_user(self, payload):
def create_user(self, payload, exist_ok=True):
""" """
Create a new user. Username must be unique Create a new user. Username must be unique
@ -370,11 +403,13 @@ class KeycloakAdmin:
https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation
:param payload: UserRepresentation :param payload: UserRepresentation
:param exist_ok: If False, raise KeycloakGetError if username already exists. Otherwise, return existing user ID.
:return: UserRepresentation :return: UserRepresentation
""" """
params_path = {"realm-name": self.realm_name} params_path = {"realm-name": self.realm_name}
if exist_ok:
exists = self.get_user_id(username=payload['username']) exists = self.get_user_id(username=payload['username'])
if exists is not None: if exists is not None:
@ -408,9 +443,9 @@ class KeycloakAdmin:
:return: user_id :return: user_id
""" """
users = self.get_users(query={"search": username})
return next((user["id"] for user in users if user["username"] == username), None)
lower_user_name = username.lower()
users = self.get_users(query={"search": lower_user_name})
return next((user["id"] for user in users if user["username"] == lower_user_name), None)
def get_user(self, user_id): def get_user(self, user_id):
""" """
@ -485,6 +520,19 @@ class KeycloakAdmin:
data=json.dumps(payload)) data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def logout(self, user_id):
"""
Logs out user.
https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_logout
:param user_id: User id
:return:
"""
params_path = {"realm-name": self.realm_name, "id": user_id}
data_raw = self.raw_post(URL_ADMIN_USER_LOGOUT.format(**params_path), data="")
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def consents_user(self, user_id): def consents_user(self, user_id):
""" """
Get consents granted by the user Get consents granted by the user
@ -537,7 +585,7 @@ class KeycloakAdmin:
params_path = {"realm-name": self.realm_name, "id": user_id} params_path = {"realm-name": self.realm_name, "id": user_id}
params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri} params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri}
data_raw = self.raw_put(URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), data_raw = self.raw_put(URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path),
data=payload, **params_query)
data=json.dumps(payload), **params_query)
return raise_error_from_response(data_raw, KeycloakGetError) return raise_error_from_response(data_raw, KeycloakGetError)
def send_verify_email(self, user_id, client_id=None, redirect_uri=None): def send_verify_email(self, user_id, client_id=None, redirect_uri=None):
@ -584,7 +632,7 @@ class KeycloakAdmin:
data_raw = self.raw_get(URL_ADMIN_SERVER_INFO) data_raw = self.raw_get(URL_ADMIN_SERVER_INFO)
return raise_error_from_response(data_raw, KeycloakGetError) return raise_error_from_response(data_raw, KeycloakGetError)
def get_groups(self):
def get_groups(self, query=None):
""" """
Returns a list of groups belonging to the realm Returns a list of groups belonging to the realm
@ -593,8 +641,14 @@ class KeycloakAdmin:
:return: array GroupRepresentation :return: array GroupRepresentation
""" """
query = query or {}
params_path = {"realm-name": self.realm_name} params_path = {"realm-name": self.realm_name}
return self.__fetch_all(URL_ADMIN_GROUPS.format(**params_path))
url = URL_ADMIN_USERS.format(**params_path)
if "first" in query or "max" in query:
return self.__fetch_paginated(url, query)
return self.__fetch_all(url, query)
def get_group(self, group_id): def get_group(self, group_id):
""" """
@ -646,7 +700,12 @@ class KeycloakAdmin:
:return: Keycloak server response (UserRepresentation) :return: Keycloak server response (UserRepresentation)
""" """
params_path = {"realm-name": self.realm_name, "id": group_id} params_path = {"realm-name": self.realm_name, "id": group_id}
return self.__fetch_all(URL_ADMIN_GROUP_MEMBERS.format(**params_path), query)
url = URL_ADMIN_USERS.format(**params_path)
if "first" in query or "max" in query:
return self.__fetch_paginated(url, query)
return self.__fetch_all(url, query)
def get_group_by_path(self, path, search_in_subgroups=False): def get_group_by_path(self, path, search_in_subgroups=False):
""" """
@ -1014,6 +1073,22 @@ class KeycloakAdmin:
data=json.dumps(payload)) data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists)
def add_composite_client_roles_to_role(self, client_role_id, role_name, roles):
"""
Add composite roles to client role
:param client_role_id: id of client (not client-id)
:param role_name: The name of the role
:param roles: roles list or role (use RoleRepresentation) to be updated
:return Keycloak server response
"""
payload = roles if isinstance(roles, list) else [roles]
params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name}
data_raw = self.raw_post(URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path),
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def delete_client_role(self, client_role_id, role_name): def delete_client_role(self, client_role_id, role_name):
""" """
Delete a client role Delete a client role
@ -1220,7 +1295,7 @@ class KeycloakAdmin:
:return: Keycloak server response (array RoleRepresentation) :return: Keycloak server response (array RoleRepresentation)
""" """
params_path = {"realm-name": self.realm_name, "id": group_id} params_path = {"realm-name": self.realm_name, "id": group_id}
data_raw = self.raw_get(URL_ADMIN_GET_GROUPS_REALM_ROLES.format(**params_path))
data_raw = self.raw_get(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError) return raise_error_from_response(data_raw, KeycloakGetError)
def assign_group_client_roles(self, group_id, client_id, roles): def assign_group_client_roles(self, group_id, client_id, roles):
@ -1359,7 +1434,7 @@ class KeycloakAdmin:
params_path = {"realm-name": self.realm_name} params_path = {"realm-name": self.realm_name}
data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path),
data=payload)
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists)
def copy_authentication_flow(self, payload, flow_alias): def copy_authentication_flow(self, payload, flow_alias):
@ -1373,7 +1448,7 @@ class KeycloakAdmin:
params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias}
data_raw = self.raw_post(URL_ADMIN_FLOWS_COPY.format(**params_path), data_raw = self.raw_post(URL_ADMIN_FLOWS_COPY.format(**params_path),
data=payload)
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201])
def get_authentication_flow_executions(self, flow_alias): def get_authentication_flow_executions(self, flow_alias):
@ -1401,7 +1476,7 @@ class KeycloakAdmin:
params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias}
data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path),
data=payload)
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def create_authentication_flow_execution(self, payload, flow_alias): def create_authentication_flow_execution(self, payload, flow_alias):
@ -1418,7 +1493,7 @@ class KeycloakAdmin:
params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias}
data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION.format(**params_path), data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION.format(**params_path),
data=payload)
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201])
def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False):
@ -1436,9 +1511,50 @@ class KeycloakAdmin:
params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias}
data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path),
data=payload)
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists)
def get_authenticator_config(self, config_id):
"""
Get authenticator configuration. Returns all configuration details.
:param config_id: Authenticator config id
:return: Response(json)
"""
params_path = {"realm-name": self.realm_name, "id": config_id}
data_raw = self.raw_get(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
def update_authenticator_config(self, payload, config_id):
"""
Update an authenticator configuration.
AuthenticatorConfigRepresentation
https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticatorconfigrepresentation
:param payload: AuthenticatorConfigRepresentation
:param config_id: Authenticator config id
:return: Response(json)
"""
params_path = {"realm-name": self.realm_name, "id": config_id}
data_raw = self.raw_put(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path),
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def delete_authenticator_config(self, config_id):
"""
Delete a authenticator configuration.
https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authentication_management_resource
:param config_id: Authenticator config id
:return: Keycloak server Response
"""
params_path = {"realm-name": self.realm_name, "id": config_id}
data_raw = self.raw_delete(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def sync_users(self, storage_id, action): def sync_users(self, storage_id, action):
""" """
Function to trigger user sync from provider Function to trigger user sync from provider
@ -1496,6 +1612,22 @@ class KeycloakAdmin:
data=json.dumps(payload)) data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists)
def update_client_scope(self, client_scope_id, payload):
"""
Update a client scope
ClientScopeRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_client_scopes_resource
:param client_scope_id: The id of the client scope
:param payload: ClientScopeRepresentation
:return: Keycloak server response (ClientScopeRepresentation)
"""
params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id}
data_raw = self.raw_put(URL_ADMIN_CLIENT_SCOPE.format(**params_path),
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def add_mapper_to_client_scope(self, client_scope_id, payload): def add_mapper_to_client_scope(self, client_scope_id, payload):
""" """
Add a mapper to a client scope Add a mapper to a client scope
@ -1531,6 +1663,26 @@ class KeycloakAdmin:
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, payload):
"""
Update an existing protocol mapper in a client scope
https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_protocol_mappers_resource
:param client_scope_id: The id of the client scope
:param protocol_mapper_id: The id of the protocol mapper which exists in the client scope
and should to be updated
:param payload: ProtocolMapperRepresentation
:return: Keycloak server Response
"""
params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id,
"protocol-mapper-id": protocol_mapper_id}
data_raw = self.raw_put(
URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def add_mapper_to_client(self, client_id, payload): def add_mapper_to_client(self, client_id, payload):
""" """
Add a mapper to a client Add a mapper to a client
@ -1664,6 +1816,34 @@ class KeycloakAdmin:
data=None) data=None)
return raise_error_from_response(data_raw, KeycloakGetError) return raise_error_from_response(data_raw, KeycloakGetError)
def get_events(self, query=None):
"""
Return a list of events, filtered according to query parameters
EventRepresentation array
https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_eventrepresentation
:return: events list
"""
params_path = {"realm-name": self.realm_name}
data_raw = self.raw_get(URL_ADMIN_EVENTS.format(**params_path),
data=None, **query)
return raise_error_from_response(data_raw, KeycloakGetError)
def set_events(self, payload):
"""
Set realm events configuration
RealmEventsConfigRepresentation
https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmeventsconfigrepresentation
:return: Http response
"""
params_path = {"realm-name": self.realm_name}
data_raw = self.raw_put(URL_ADMIN_EVENTS.format(**params_path),
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])
def raw_get(self, *args, **kwargs): def raw_get(self, *args, **kwargs):
""" """
Calls connection.raw_get. Calls connection.raw_get.
@ -1717,14 +1897,17 @@ class KeycloakAdmin:
return r return r
def get_token(self): def get_token(self):
token_realm_name = 'master' if self.client_secret_key else self.user_realm_name or self.realm_name
self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id,
realm_name=self.user_realm_name or self.realm_name, verify=self.verify,
realm_name=token_realm_name, verify=self.verify,
client_secret_key=self.client_secret_key, client_secret_key=self.client_secret_key,
custom_headers=self.custom_headers) custom_headers=self.custom_headers)
grant_type = ["password"] grant_type = ["password"]
if self.client_secret_key: if self.client_secret_key:
grant_type = ["client_credentials"] grant_type = ["client_credentials"]
if self.user_realm_name:
self.realm_name = self.user_realm_name
self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type) self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type)
@ -1751,3 +1934,30 @@ class KeycloakAdmin:
else: else:
raise raise
self.connection.add_param_headers('Authorization', 'Bearer ' + self.token.get('access_token')) self.connection.add_param_headers('Authorization', 'Bearer ' + self.token.get('access_token'))
def get_client_all_sessions(self, client_id):
"""
Get sessions associated with the client
:param client_id: id of client
UserSessionRepresentation
http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation
:return: UserSessionRepresentation
"""
params_path = {"realm-name": self.realm_name, "id": client_id}
data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
def delete_user_realm_role(self, user_id, payload):
"""
Delete realm-level role mappings
DELETE admin/realms/{realm-name}/users/{id}/role-mappings/realm
"""
params_path = {"realm-name": self.realm_name, "id": str(user_id) }
data_raw = self.connection.raw_delete(URL_ADMIN_DELETE_USER_ROLE.format(**params_path),
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204])

2
keycloak/keycloak_openid.py

@ -189,7 +189,7 @@ class KeycloakOpenID:
payload = {"username": username, "password": password, payload = {"username": username, "password": password,
"client_id": self.client_id, "grant_type": grant_type, "client_id": self.client_id, "grant_type": grant_type,
"code": code, "redirect_uri": redirect_uri} "code": code, "redirect_uri": redirect_uri}
if payload:
if extra:
payload.update(extra) payload.update(extra)
if totp: if totp:

2
keycloak/tests/test_connection.py

@ -156,6 +156,7 @@ class TestConnection(unittest.TestCase):
with mock.patch.object(KeycloakOpenID, "__init__", return_value=None) as mock_keycloak_open_id: with mock.patch.object(KeycloakOpenID, "__init__", return_value=None) as mock_keycloak_open_id:
with mock.patch("keycloak.keycloak_openid.KeycloakOpenID.token", return_value=fake_token): with mock.patch("keycloak.keycloak_openid.KeycloakOpenID.token", return_value=fake_token):
with mock.patch("keycloak.connection.ConnectionManager.__init__", return_value=None) as mock_connection_manager: with mock.patch("keycloak.connection.ConnectionManager.__init__", return_value=None) as mock_connection_manager:
with mock.patch("keycloak.connection.ConnectionManager.__del__", return_value=None) as mock_connection_manager_delete:
server_url = "https://localhost/auth/" server_url = "https://localhost/auth/"
username = "admin" username = "admin"
password = "secret" password = "secret"
@ -187,3 +188,4 @@ class TestConnection(unittest.TestCase):
headers=expected_header, headers=expected_header,
timeout=60, timeout=60,
verify=False) verify=False)
mock_connection_manager_delete.assert_called_once_with()

12
keycloak/urls_patterns.py

@ -44,13 +44,13 @@ URL_ADMIN_GET_SESSIONS = "admin/realms/{realm-name}/users/{id}/sessions"
URL_ADMIN_USER_CLIENT_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}" URL_ADMIN_USER_CLIENT_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}"
URL_ADMIN_USER_REALM_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" URL_ADMIN_USER_REALM_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/realm"
URL_ADMIN_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/realm" URL_ADMIN_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/realm"
URL_ADMIN_GET_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings"
URL_ADMIN_GROUPS_CLIENT_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/clients/{client-id}" URL_ADMIN_GROUPS_CLIENT_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/clients/{client-id}"
URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available" URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available"
URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/composite" URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/composite"
URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}"
URL_ADMIN_USER_GROUPS = "admin/realms/{realm-name}/users/{id}/groups" URL_ADMIN_USER_GROUPS = "admin/realms/{realm-name}/users/{id}/groups"
URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password"
URL_ADMIN_USER_LOGOUT = "admin/realms/{realm-name}/users/{id}/logout"
URL_ADMIN_USER_STORAGE = "admin/realms/{realm-name}/user-storage/{id}/sync" URL_ADMIN_USER_STORAGE = "admin/realms/{realm-name}/user-storage/{id}/sync"
URL_ADMIN_SERVER_INFO = "admin/serverinfo" URL_ADMIN_SERVER_INFO = "admin/serverinfo"
@ -63,9 +63,11 @@ URL_ADMIN_GROUP_MEMBERS = "admin/realms/{realm-name}/groups/{id}/members"
URL_ADMIN_CLIENTS = "admin/realms/{realm-name}/clients" URL_ADMIN_CLIENTS = "admin/realms/{realm-name}/clients"
URL_ADMIN_CLIENT = URL_ADMIN_CLIENTS + "/{id}" URL_ADMIN_CLIENT = URL_ADMIN_CLIENTS + "/{id}"
URL_ADMIN_CLIENT_SECRETS= URL_ADMIN_CLIENT + "/client-secret"
URL_ADMIN_CLIENT_ALL_SESSIONS = URL_ADMIN_CLIENT + "/user-sessions"
URL_ADMIN_CLIENT_SECRETS = URL_ADMIN_CLIENT + "/client-secret"
URL_ADMIN_CLIENT_ROLES = URL_ADMIN_CLIENT + "/roles" URL_ADMIN_CLIENT_ROLES = URL_ADMIN_CLIENT + "/roles"
URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}"
URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE = URL_ADMIN_CLIENT_ROLE + "/composites"
URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users"
URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings"
URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource"
@ -88,6 +90,7 @@ URL_ADMIN_IDP_MAPPERS = "admin/realms/{realm-name}/identity-provider/instances/{
URL_ADMIN_IDP = "admin/realms//{realm-name}/identity-provider/instances/{alias}" URL_ADMIN_IDP = "admin/realms//{realm-name}/identity-provider/instances/{alias}"
URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}"
URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = "admin/realms/{realm-name}/roles/{role-name}/composites" URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = "admin/realms/{realm-name}/roles/{role-name}/composites"
URL_ADMIN_REALM_EXPORT = "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&exportGroupsAndRoles={export-groups-and-roles}"
URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows"
URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}"
@ -95,6 +98,7 @@ URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-ali
URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions"
URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution"
URL_ADMIN_FLOWS_EXECUTIONS_FLOW = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" URL_ADMIN_FLOWS_EXECUTIONS_FLOW = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow"
URL_ADMIN_AUTHENTICATOR_CONFIG = "admin/realms/{realm-name}/authentication/config/{id}"
URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components"
URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}"
@ -102,3 +106,7 @@ URL_ADMIN_KEYS = "admin/realms/{realm-name}/keys"
URL_ADMIN_USER_FEDERATED_IDENTITIES = "admin/realms/{realm-name}/users/{id}/federated-identity" URL_ADMIN_USER_FEDERATED_IDENTITIES = "admin/realms/{realm-name}/users/{id}/federated-identity"
URL_ADMIN_USER_FEDERATED_IDENTITY = "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}" URL_ADMIN_USER_FEDERATED_IDENTITY = "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}"
URL_ADMIN_EVENTS = 'admin/realms/{realm-name}/events'
URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm"

3
requirements.txt

@ -2,3 +2,6 @@ requests>=2.20.0
httmock>=1.2.5 httmock>=1.2.5
python-jose>=1.4.0 python-jose>=1.4.0
twine==1.13.0 twine==1.13.0
jose~=1.0.0
setuptools~=54.2.0
urllib3>=1.26.5

2
setup.py

@ -7,7 +7,7 @@ with open("README.md", "r") as fh:
setup( setup(
name='python-keycloak', name='python-keycloak',
version='0.23.0',
version='0.26.1',
url='https://github.com/marcospereirampj/python-keycloak', url='https://github.com/marcospereirampj/python-keycloak',
license='The MIT License', license='The MIT License',
author='Marcos Pereira', author='Marcos Pereira',

Loading…
Cancel
Save