From 65721b6c405d1e6283f7d9975ae26d10bb503b48 Mon Sep 17 00:00:00 2001 From: David-Lor <17401854+David-Lor@users.noreply.github.com> Date: Sun, 6 Sep 2020 19:10:05 +0200 Subject: [PATCH 01/28] Remove unused client_id arg from assign_realm_roles method --- keycloak/keycloak_admin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index a42bd57..f0b3c52 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -909,13 +909,11 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) - def assign_realm_roles(self, user_id, client_id, roles): + def assign_realm_roles(self, user_id, roles): """ Assign realm roles to a user - :param client_id: id of client (not client-id) :param user_id: id of user - :param client_id: id of client containing role, :param roles: roles list or role (use RoleRepresentation) :return Keycloak server response """ From 5a3b13f256629963a687960f9da2bcc2c355c0e2 Mon Sep 17 00:00:00 2001 From: David-Lor <17401854+David-Lor@users.noreply.github.com> Date: Sun, 6 Sep 2020 19:10:29 +0200 Subject: [PATCH 02/28] docs: add example to assign realm roles to user in README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 5f39f86..67a51d1 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,10 @@ realm_roles = keycloak_admin.get_roles() # 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") +# Assign realm roles to user +keycloak_admin.assign_realm_roles(user_id=user_id, roles=realm_roles) + + # Get all ID Providers idps = keycloak_admin.get_idps() From b293f0170a2ea58ef9e14593387ddf92eedfd9ff Mon Sep 17 00:00:00 2001 From: BartoszCki Date: Wed, 21 Oct 2020 22:40:27 +0200 Subject: [PATCH 03/28] Fetch users/groups/group members with pagination --- keycloak/keycloak_admin.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6..b168d86 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -222,6 +222,13 @@ class KeycloakAdmin: page += 1 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): """ Import a new realm from a RealmRepresentation. Realm name must be unique. @@ -303,8 +310,14 @@ class KeycloakAdmin: :param query: Query parameters (optional) :return: users list """ + query = query or {} 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 get_idps(self): """ @@ -541,7 +554,7 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_SERVER_INFO) 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 @@ -550,8 +563,14 @@ class KeycloakAdmin: :return: array GroupRepresentation """ + query = query or {} 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): """ @@ -603,7 +622,12 @@ class KeycloakAdmin: :return: Keycloak server response (UserRepresentation) """ 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): """ From fa9f1741af02012d70c0dab1830503b9c225e978 Mon Sep 17 00:00:00 2001 From: Ilya Glotov Date: Fri, 4 Dec 2020 14:09:28 +0300 Subject: [PATCH 04/28] Use the same request headers and cookies --- keycloak/keycloak_admin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f88d00d..e5a55cb 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -26,6 +26,7 @@ import json from builtins import isinstance +from copy import deepcopy from typing import Iterable from .connection import ConnectionManager @@ -1737,10 +1738,8 @@ class KeycloakAdmin: # merge custom headers to main headers headers.update(self.custom_headers) - self._connection = ConnectionManager(base_url=self.server_url, - headers=headers, - timeout=60, - verify=self.verify) + self._connection = deepcopy(self.keycloak_openid.connection()) + self._connection._headers.update(headers) def refresh_token(self): refresh_token = self.token.get('refresh_token') From 4b6b076f55f5c9940de068b91e6dae208d39928c Mon Sep 17 00:00:00 2001 From: Joerg Schaarschmidt Date: Wed, 9 Jun 2021 18:11:23 +0200 Subject: [PATCH 05/28] add delete_realm_roles_of_user function --- docs/source/index.rst | 3 +++ keycloak/keycloak_admin.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index 0cd6e2f..0f61b50 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -262,6 +262,9 @@ Main methods:: # Assign realm roles to user. Note that BOTH role_name and role_id appear to be required. keycloak_admin.assign_realm_roles(client_id="client_id", user_id="user_id", roles=[{"roles_representation"}]) + # Delete realm roles of user. Note that BOTH role_name and role_id appear to be required. + keycloak_admin.deletes_realm_roles_of_user(client_id="client_id", user_id="user_id", roles=[{"roles_representation"}]) + # Create new group group = keycloak_admin.create_group(name="Example Group") diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ffb5968..34c1919 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1189,6 +1189,22 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def delete_realm_roles_of_user(self, user_id, client_id, roles): + """ + Deletes realm roles of a user + + :param user_id: id of user + :param client_id: id of client containing role (not client-id) + :param roles: roles list or role (use RoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_delete(URL_ADMIN_USER_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_realm_roles_of_user(self, user_id): """ Get all realm roles for a user. From 93b9991dc84f5a62a2eabd0c348afd5a1292ab25 Mon Sep 17 00:00:00 2001 From: Joerg Schaarschmidt Date: Wed, 9 Jun 2021 18:34:20 +0200 Subject: [PATCH 06/28] Remove unused client_id from delete_realm_roles_of users --- docs/source/index.rst | 2 +- keycloak/keycloak_admin.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 0f61b50..3b3007b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -263,7 +263,7 @@ Main methods:: keycloak_admin.assign_realm_roles(client_id="client_id", user_id="user_id", roles=[{"roles_representation"}]) # Delete realm roles of user. Note that BOTH role_name and role_id appear to be required. - keycloak_admin.deletes_realm_roles_of_user(client_id="client_id", user_id="user_id", roles=[{"roles_representation"}]) + keycloak_admin.deletes_realm_roles_of_user(user_id="user_id", roles=[{"roles_representation"}]) # Create new group group = keycloak_admin.create_group(name="Example Group") diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 34c1919..2afd1e2 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1189,12 +1189,11 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def delete_realm_roles_of_user(self, user_id, client_id, roles): + def delete_realm_roles_of_user(self, user_id, roles): """ Deletes realm roles of a user :param user_id: id of user - :param client_id: id of client containing role (not client-id) :param roles: roles list or role (use RoleRepresentation) :return Keycloak server response """ From 50354e61086c69a6cf7118bee56e15c35668c2c3 Mon Sep 17 00:00:00 2001 From: Md Minhazul Haque Date: Tue, 31 Aug 2021 05:25:40 +0600 Subject: [PATCH 07/28] Add feature to list and delete user credentials --- README.md | 9 ++++++++ keycloak/keycloak_admin.py | 47 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 2 ++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da88a9b..2a2a2cd 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,15 @@ response = keycloak_admin.update_user(user_id="user-id-keycloak", # Update User Password response = keycloak_admin.set_user_password(user_id="user-id-keycloak", password="secret", temporary=True) + +# Get User Credentials +credentials = keycloak_admin.get_credentials(user_id='user_id') + +# Get User Credential by ID +credential = keycloak_admin.get_credential(user_id='user_id', credential_id='credential_id') + +# Delete User Credential +response = keycloak_admin.delete_credential(user_id='user_id', credential_id='credential_id') # Delete User response = keycloak_admin.delete_user(user_id="user-id-keycloak") diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be1..5ad0212 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -48,7 +48,8 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC 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_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 + URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, \ + URL_ADMIN_USER_CREDENTIALS, URL_ADMIN_USER_CREDENTIAL class KeycloakAdmin: @@ -506,6 +507,50 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_credentials(self, user_id): + """ + Returns a list of credential belonging to the user. + + CredentialRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation + + :param: user_id: user id + :return: Keycloak server response (CredentialRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_CREDENTIALS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_credential(self, user_id, credential_id): + """ + Get credential of the user. + + CredentialRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation + + :param: user_id: user id + :param: credential_id: credential id + :return: Keycloak server response (ClientRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": user_id, "credential_id": credential_id} + data_raw = self.raw_get(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def delete_credential(self, user_id, credential_id): + """ + Delete credential of the user. + + CredentialRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation + + :param: user_id: user id + :param: credential_id: credential id + :return: Keycloak server response (ClientRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": user_id, "credential_id": credential_id} + data_raw = self.raw_delete(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def logout(self, user_id): """ Logs out user. diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 1332586..ac0b16c 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -50,6 +50,8 @@ URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/ro 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_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" +URL_ADMIN_USER_CREDENTIALS = "admin/realms/{realm-name}/users/{id}/credentials" +URL_ADMIN_USER_CREDENTIAL = "admin/realms/{realm-name}/users/{id}/credentials/{credential_id}" URL_ADMIN_USER_LOGOUT = "admin/realms/{realm-name}/users/{id}/logout" URL_ADMIN_USER_STORAGE = "admin/realms/{realm-name}/user-storage/{id}/sync" From 5b202e71cb76f0e05f8820ccd5e8a56dc2d37bf4 Mon Sep 17 00:00:00 2001 From: Nicklas Sedlock Date: Fri, 3 Sep 2021 13:10:12 +0200 Subject: [PATCH 08/28] fix: handle refresh_token error "Session not active" --- keycloak/keycloak_admin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be1..18e3fb7 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1906,8 +1906,12 @@ class KeycloakAdmin: try: self.token = self.keycloak_openid.refresh_token(refresh_token) except KeycloakGetError as e: - if e.response_code == 400 and (b'Refresh token expired' in e.response_body or - b'Token is not active' in e.response_body): + list_errors = [ + b'Refresh token expired', + b'Token is not active', + b'Session not active' + ] + if e.response_code == 400 and any(err in e.response_body for err in list_errors): self.get_token() else: raise From 69d60d430a35cc2c01e3658b81287926310e2f46 Mon Sep 17 00:00:00 2001 From: Ankur Saxena Date: Thu, 16 Sep 2021 11:20:51 -0400 Subject: [PATCH 09/28] Add remove and update methods for client protocol mappers --- keycloak/keycloak_admin.py | 42 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 3 ++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be1..188c6e7 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1672,10 +1672,50 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload)) + URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + def update_client_mapper(self, client_id, mapper_id, payload): + """ + Update client mapper + :param client_id: The id of the client + :param client_mapper_id: The id of the mapper to be deleted + :param payload: ProtocolMapperRepresentation + :return: Keycloak server response + """ + + params_path = { + "realm-name": self.realm_name, + "id": self.client_id, + "protocol-mapper-id": mapper_id, + } + data_raw = self.raw_put( + URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def remove_client_mapper(self, client_id, client_mapper_id): + """ + Removes a mapper from the client + https://www.keycloak.org/docs-api/15.0/rest-api/index.html#_protocol_mappers_resource + :param client_id: The id of the client + :param client_mapper_id: The id of the mapper to be deleted + :return: Keycloak server response + """ + + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "protocol-mapper-id": mapper_id + } + + data_raw = self.raw_delete( + URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def generate_client_secrets(self, client_id): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 1332586..fc396e9 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -74,7 +74,8 @@ URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/re URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" -URL_ADMIN_CLIENT_PROTOCOL_MAPPER = URL_ADMIN_CLIENT + "/protocol-mappers/models" +URL_ADMIN_CLIENT_PROTOCOL_MAPPERS = URL_ADMIN_CLIENT + "/protocol-mappers/models" +URL_ADMIN_CLIENT_PROTOCOL_MAPPER = URL_ADMIN_CLIENT_PROTOCOL_MAPPERS + "/{protocol-mapper-id}" URL_ADMIN_CLIENT_SCOPES = "admin/realms/{realm-name}/client-scopes" URL_ADMIN_CLIENT_SCOPE = URL_ADMIN_CLIENT_SCOPES + "/{scope-id}" From d0894d4352a982c4c100938d301797a0719281ed Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Fri, 1 Oct 2021 12:58:54 +0300 Subject: [PATCH 10/28] Don't force realm name when using secret key Using other realms can be useful, for example, to manage realm users with restricted rights. --- keycloak/keycloak_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be1..c934a7b 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1873,7 +1873,7 @@ class KeycloakAdmin: return r def get_token(self): - token_realm_name = 'master' if self.client_secret_key else self.user_realm_name or self.realm_name + token_realm_name = self.user_realm_name or self.realm_name self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, realm_name=token_realm_name, verify=self.verify, client_secret_key=self.client_secret_key, @@ -1938,4 +1938,4 @@ class KeycloakAdmin: 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]) \ No newline at end of file + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) From a37bf45c77164d9635fec91ae099103d37cee6ab Mon Sep 17 00:00:00 2001 From: Tobias Henkel Date: Fri, 8 Oct 2021 19:01:29 +0200 Subject: [PATCH 11/28] Add delete_user_social_login This makes it possible to delete federated identities without having to use raw requests. --- keycloak/keycloak_admin.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be1..464ef18 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -555,6 +555,18 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} data_raw = self.raw_post(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload)) + def delete_user_social_login(self, user_id, provider_id): + + """ + Delete a federated identity / social login provider from the user + :param user_id: User id + :param provider_id: Social login provider id + :return: + """ + params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} + data_raw = self.raw_delete(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None): """ Send an update account email to the user. An email contains a From 9e7d0d2ec5829ecd384f8fd2a3e5c7ca88db52ef Mon Sep 17 00:00:00 2001 From: Leandro de Souza Date: Wed, 13 Oct 2021 11:13:31 -0300 Subject: [PATCH 12/28] Added optional proxies for requests calls --- docs/source/index.rst | 9 +++++++++ keycloak/connection.py | 6 +++++- keycloak/keycloak_openid.py | 6 ++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 0cd6e2f..7347d0e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -100,6 +100,15 @@ Main methods:: # verify=True, # custom_headers={'CustomHeader': 'value'}) + # Optionally, you can pass proxies as well that will be used in all HTTP calls. See requests documentation for more details_ + # `requests-proxies `_. + # keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", + # client_id="example_client", + # realm_name="example_realm", + # client_secret_key="secret", + # verify=True, + # proxies={'http': 'http://10.10.1.10:3128', 'https': 'http://10.10.1.10:1080'}) + # Get WellKnow config_well_know = keycloak_openid.well_know() diff --git a/keycloak/connection.py b/keycloak/connection.py index 7d5ed2f..1ceae02 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -39,9 +39,10 @@ class ConnectionManager(object): headers (dict): The header parameters of the requests to the server. timeout (int): Timeout to use for requests to the server. verify (bool): Verify server SSL. + proxies (dict): The proxies servers requests is sent by. """ - def __init__(self, base_url, headers={}, timeout=60, verify=True): + def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): self._base_url = base_url self._headers = headers self._timeout = timeout @@ -59,6 +60,9 @@ class ConnectionManager(object): adapter.max_retries.allowed_methods = frozenset(allowed_methods) self._s.mount(protocol, adapter) + + if proxies: + self._s.proxies = proxies def __del__(self): self._s.close() diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 197dd26..1d6ed28 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -44,7 +44,7 @@ from .urls_patterns import ( class KeycloakOpenID: - def __init__(self, server_url, realm_name, client_id, client_secret_key=None, verify=True, custom_headers=None): + def __init__(self, server_url, realm_name, client_id, client_secret_key=None, verify=True, custom_headers=None, proxies=None): """ :param server_url: Keycloak server url @@ -53,6 +53,7 @@ class KeycloakOpenID: :param client_secret_key: client secret key :param verify: True if want check connection SSL :param custom_headers: dict of custom header to pass to each HTML request + :param proxies: dict of proxies to sent the request by. """ self._client_id = client_id self._client_secret_key = client_secret_key @@ -64,7 +65,8 @@ class KeycloakOpenID: self._connection = ConnectionManager(base_url=server_url, headers=headers, timeout=60, - verify=verify) + verify=verify, + proxies=proxies) self._authorization = Authorization() From fc079d8fb73a889ade397818f3e32a49a57a59dd Mon Sep 17 00:00:00 2001 From: Leandro de Souza Date: Wed, 13 Oct 2021 11:28:53 -0300 Subject: [PATCH 13/28] Using session.proxies.update method instead of seting it as an dict --- keycloak/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/connection.py b/keycloak/connection.py index 1ceae02..bdecfce 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -62,7 +62,7 @@ class ConnectionManager(object): self._s.mount(protocol, adapter) if proxies: - self._s.proxies = proxies + self._s.proxies.update(proxies) def __del__(self): self._s.close() From fd0577c7303f2cfe2620fc06d3b037e3fb9d99aa Mon Sep 17 00:00:00 2001 From: Aarno Aukia Date: Thu, 21 Oct 2021 21:11:39 +0300 Subject: [PATCH 14/28] add_group needs a dict of GroupRepresentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da88a9b..fa6c4a3 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,7 @@ keycloak_admin.delete_client_roles_of_user(client_id="client_id", user_id="user_ keycloak_admin.delete_client_roles_of_user(client_id="client_id", user_id="user_id", roles=[{"id": "role-id_1"}, {"id": "role-id_2"}]) # Create new group -group = keycloak_admin.create_group(name="Example Group") +group = keycloak_admin.create_group({"name": "Example Group"}) # Get all groups groups = keycloak_admin.get_groups() From 3b93754d27d9aa7d76213131e79055fda568563e Mon Sep 17 00:00:00 2001 From: lcgkm Date: Sun, 7 Nov 2021 10:59:28 +0800 Subject: [PATCH 15/28] Add new AuthZ API support NOTE: These are private API, will be changed later. --- keycloak/keycloak_admin.py | 65 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 3 ++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be1..371897b 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -32,6 +32,7 @@ from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ + URL_ADMIN_CLIENT_AUTHZ_POLICIES, URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION, \ 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_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ @@ -866,6 +867,22 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path)) return data_raw + def create_client_authz_resource(self, client_id, payload, skip_exists=False): + """ + Create resources of client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, + "id": client_id} + + data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def get_client_authz_resources(self, client_id): """ Get resources from client. @@ -877,7 +894,53 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path)) - return data_raw + return raise_error_from_response(data_raw, KeycloakGetError) + + def create_client_authz_role_based_policy(self, client_id, payload, skip_exists=False): + """ + Create role-based policy of client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, + "id": client_id} + + data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + + def create_client_authz_role_based_permission(self, client_id, payload, skip_exists=False): + """ + Create role-based permission of client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, + "id": client_id} + + data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + + def get_client_authz_policies(self, client_id): + """ + Get policies from client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + params_query = {"first": 0, "max": 20, "permission": False} + data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path), **params_query) + return raise_error_from_response(data_raw, KeycloakGetError) def get_client_service_account_user(self, client_id): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 1332586..9122116 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -71,6 +71,9 @@ URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE = URL_ADMIN_CLIENT_ROLE + "/composi 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_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" +URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT + "/authz/resource-server/policy" +URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = URL_ADMIN_CLIENT_AUTHZ_POLICIES + "/role" +URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION = URL_ADMIN_CLIENT + "/authz/resource-server/permission/resource" URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" From 656de1c4669ce60600f33eb07ce5fadcab4a1885 Mon Sep 17 00:00:00 2001 From: lcgkm Date: Sun, 7 Nov 2021 11:27:33 +0800 Subject: [PATCH 16/28] Add more comments --- keycloak/keycloak_admin.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 371897b..d5b6df8 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -873,6 +873,9 @@ class KeycloakAdmin: :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :param payload: ResourceRepresentation + https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_resourcerepresentation + :return: Keycloak server response """ @@ -902,6 +905,9 @@ class KeycloakAdmin: :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :param payload: PolicyRepresentation + https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_policyrepresentation + :return: Keycloak server response """ @@ -918,6 +924,20 @@ class KeycloakAdmin: :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :param payload: No Document + payload example: + payload={ + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "name": "Permission-Name", + "resources": [ + resource_id + ], + "policies": [ + policy_id + ] + :return: Keycloak server response """ @@ -934,6 +954,9 @@ class KeycloakAdmin: :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :param payload: PolicyRepresentation + https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_policyrepresentation + :return: Keycloak server response """ From 10c212cce5e07609fef89783925bf6bac2c9059e Mon Sep 17 00:00:00 2001 From: lcgkm Date: Sun, 7 Nov 2021 11:40:18 +0800 Subject: [PATCH 17/28] Refine comments --- keycloak/keycloak_admin.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index d5b6df8..2453d5c 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -905,8 +905,19 @@ class KeycloakAdmin: :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation - :param payload: PolicyRepresentation - https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_policyrepresentation + :param payload: No Document + payload example: + payload={ + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "name": "Policy-1", + "roles": [ + { + "id": id + } + ] + } :return: Keycloak server response """ @@ -924,7 +935,8 @@ class KeycloakAdmin: :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation - :param payload: No Document + :param payload: PolicyRepresentation + https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_policyrepresentation payload example: payload={ "type": "resource", From 80bb7a56b58557a8dc30521b257dbc2ada124a6a Mon Sep 17 00:00:00 2001 From: lcgkm Date: Sun, 7 Nov 2021 11:53:27 +0800 Subject: [PATCH 18/28] Fix invalid permission type A permission associates the object being protected and the policies that must be evaluated to decide whether access should be granted. Permissions can be created to protect two main types of objects: 1. Resources 2. Scopes --- keycloak/keycloak_admin.py | 8 ++++---- keycloak/urls_patterns.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 2453d5c..69da3d8 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -32,7 +32,7 @@ from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ - URL_ADMIN_CLIENT_AUTHZ_POLICIES, URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION, \ + URL_ADMIN_CLIENT_AUTHZ_POLICIES, URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION, \ 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_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ @@ -929,9 +929,9 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) - def create_client_authz_role_based_permission(self, client_id, payload, skip_exists=False): + def create_client_authz_resource_based_permission(self, client_id, payload, skip_exists=False): """ - Create role-based permission of client. + Create resource-based permission of client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation @@ -956,7 +956,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION.format(**params_path), + data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 9122116..b58c4bb 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -73,7 +73,8 @@ URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/set URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT + "/authz/resource-server/policy" URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = URL_ADMIN_CLIENT_AUTHZ_POLICIES + "/role" -URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION = URL_ADMIN_CLIENT + "/authz/resource-server/permission/resource" +URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS = URL_ADMIN_CLIENT + "/authz/resource-server/permission" +URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION = URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS + "/resource" URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" From 7ee625d063580f95c09b7eefe5865bae8d025214 Mon Sep 17 00:00:00 2001 From: Ryan Gard Date: Mon, 15 Nov 2021 09:28:03 -0800 Subject: [PATCH 19/28] Enable Keycloak Admin for Non-master Realms Allow the 'KeycloakAdmin' class to instantiate against non-master realms using an Authorization header for a non-admin user that is granted permissions to inspect or manage Keycloak admin resources. Example: kca = KeycloakAdmin( 'https://auth.keycloak.local/auth/', realm_name='my-realm', client_id='admin', custom_headers=auth_headers ) --- keycloak/keycloak_admin.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be1..8c31e95 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -254,7 +254,7 @@ class KeycloakAdmin: :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 } @@ -1885,12 +1885,16 @@ class KeycloakAdmin: 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) + if self.username and self.password: + self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type) - headers = { - 'Authorization': 'Bearer ' + self.token.get('access_token'), - 'Content-Type': 'application/json' - } + headers = { + 'Authorization': 'Bearer ' + self.token.get('access_token'), + 'Content-Type': 'application/json' + } + else: + self._token = None + headers = {} if self.custom_headers is not None: # merge custom headers to main headers From a6ad87d62c62bc3ab7bac5bf1ebd935a3a314488 Mon Sep 17 00:00:00 2001 From: robsonyeg Date: Wed, 17 Nov 2021 14:51:37 -0700 Subject: [PATCH 20/28] fix invalid credential error and refresh_code not exist error --- keycloak/keycloak_admin.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be1..5dad457 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1873,7 +1873,14 @@ class KeycloakAdmin: return r def get_token(self): - token_realm_name = 'master' if self.client_secret_key else self.user_realm_name or self.realm_name + # token_realm_name = 'master' if self.client_secret_key else self.user_realm_name or self.realm_name + if self.user_realm_name: + token_realm_name = self.user_realm_name + elif self.realm_name: + token_realm_name = self.realm_name + else: + token_realm_name = "master" + self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, realm_name=token_realm_name, verify=self.verify, client_secret_key=self.client_secret_key, @@ -1902,15 +1909,19 @@ class KeycloakAdmin: verify=self.verify) def refresh_token(self): - refresh_token = self.token.get('refresh_token') - try: - self.token = self.keycloak_openid.refresh_token(refresh_token) - except KeycloakGetError as e: - if e.response_code == 400 and (b'Refresh token expired' in e.response_body or - b'Token is not active' in e.response_body): - self.get_token() - else: - raise + refresh_token = self.token.get('refresh_token', None) + if refresh_token is None: + self.get_token() + else: + try: + self.token = self.keycloak_openid.refresh_token(refresh_token) + except KeycloakGetError as e: + if e.response_code == 400 and (b'Refresh token expired' in e.response_body or + b'Token is not active' in e.response_body): + self.get_token() + else: + raise + self.connection.add_param_headers('Authorization', 'Bearer ' + self.token.get('access_token')) def get_client_all_sessions(self, client_id): From c538d8fb3232b84ba5e033b919e636116b8ed84a Mon Sep 17 00:00:00 2001 From: ggallard Date: Sat, 20 Nov 2021 16:24:57 -0300 Subject: [PATCH 21/28] added get/add/delete default (default/optional) client scopes --- keycloak/keycloak_admin.py | 76 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 5 +++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be1..f5576bd 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -48,7 +48,9 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC 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_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 + URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, \ + URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES, URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE, \ + URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES, URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE class KeycloakAdmin: @@ -1659,6 +1661,78 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_default_default_client_scopes(self): + """ + Return list of default default client scopes + + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + + def delete_default_default_client_scope(self, scope_id): + """ + Delete default default client scope + + :param scope_id: default default client scope id + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": scope_id} + data_raw = self.raw_delete(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + + def add_default_default_client_scope(self, scope_id): + """ + Add default default client scope + + :param scope_id: default default client scope id + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": scope_id} + payload = {"realm": self.realm_name, "clientScopeId": scope_id} + data_raw = self.raw_put(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + + def get_default_optional_client_scopes(self): + """ + Return list of default optional client scopes + + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + + def delete_default_optional_client_scope(self, scope_id): + """ + Delete default optional client scope + + :param scope_id: default optional client scope id + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": scope_id} + data_raw = self.raw_delete(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + + def add_default_optional_client_scope(self, scope_id): + """ + Add default optional client scope + + :param scope_id: default optional client scope id + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": scope_id} + payload = {"realm": self.realm_name, "clientScopeId": scope_id} + data_raw = self.raw_put(URL_ADMIN_DEFAULT_OPTIONAL_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(self, client_id, payload): """ Add a mapper to a client diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 1332586..bbc5fe0 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -92,6 +92,11 @@ 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_EXPORT = "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&exportGroupsAndRoles={export-groups-and-roles}" +URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES = URL_ADMIN_REALM + "/default-default-client-scopes" +URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE = URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES + "/{id}" +URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES = URL_ADMIN_REALM + "/default-optional-client-scopes" +URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE = URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES + "/{id}" + URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" From 76b6798429b5cec876b1b4e66d1d7337e75859cf Mon Sep 17 00:00:00 2001 From: ggallard Date: Tue, 23 Nov 2021 13:21:15 -0300 Subject: [PATCH 22/28] added get/delete authentication_flow_execution(execution_id) --- keycloak/keycloak_admin.py | 31 +++++++++++++++++++++++++++++-- keycloak/urls_patterns.py | 1 + 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be1..d7dfc71 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -48,8 +48,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC 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_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 - + URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, URL_ADMIN_FLOWS_EXECUTION class KeycloakAdmin: @@ -1455,6 +1454,20 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_authentication_flow_execution(self, execution_id): + """ + Get authentication flow execution. + + AuthenticationExecutionInfoRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + + :param execution_id: the execution ID + :return: Response(json) + """ + params_path = {"realm-name": self.realm_name, "id": execution_id} + data_raw = self.raw_get(URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def create_authentication_flow_execution(self, payload, flow_alias): """ Create an authentication flow execution @@ -1472,6 +1485,20 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def delete_authentication_flow_execution(self, execution_id): + """ + Delete authentication flow execution + + AuthenticationExecutionInfoRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + + :param execution_id: keycloak client id (not oauth client-id) + :return: Keycloak server response (json) + """ + params_path = {"realm-name": self.realm_name, "id": execution_id} + data_raw = self.raw_delete(URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): """ Create a new sub authentication flow for a given authentication flow diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 1332586..148fe4f 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -96,6 +96,7 @@ URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" +URL_ADMIN_FLOWS_EXECUTION = "admin/realms/{realm-name}/authentication/executions/{id}" 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_AUTHENTICATOR_CONFIG = "admin/realms/{realm-name}/authentication/config/{id}" From 95c4a7ee2dce930e086e83fca85bc4774e47df88 Mon Sep 17 00:00:00 2001 From: ggallard Date: Tue, 23 Nov 2021 13:22:46 -0300 Subject: [PATCH 23/28] added delete_authentication_flow(flow_id) --- keycloak/keycloak_admin.py | 17 ++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index d7dfc71..fc96e87 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -48,7 +48,8 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC 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_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, URL_ADMIN_FLOWS_EXECUTION + URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, URL_ADMIN_FLOWS_EXECUTION, \ + URL_ADMIN_FLOW class KeycloakAdmin: @@ -1426,6 +1427,20 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def delete_authentication_flow(self, flow_id): + """ + Delete authentication flow + + AuthenticationInfoRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationinforepresentation + + :param flow_id: authentication flow id + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": flow_id} + data_raw = self.raw_delete(URL_ADMIN_FLOW.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_authentication_flow_executions(self, flow_alias): """ Get authentication flow executions. Returns all execution steps diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 148fe4f..1fcdb59 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -93,6 +93,7 @@ URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = "admin/realms/{realm-name}/roles/{r 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_FLOW = URL_ADMIN_FLOWS + "/{id}" URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" From 9777a85fa09ec8339ca3b68d1f2a58ce433a4a6f Mon Sep 17 00:00:00 2001 From: ggallard Date: Tue, 23 Nov 2021 13:25:12 -0300 Subject: [PATCH 24/28] fixed typo in URL pattern --- keycloak/keycloak_admin.py | 4 ++-- keycloak/urls_patterns.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index fc96e87..19c83a5 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -45,7 +45,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ 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_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ + URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ 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, URL_ADMIN_FLOWS_EXECUTION, \ @@ -1496,7 +1496,7 @@ class KeycloakAdmin: """ 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_EXECUTION.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 1fcdb59..f89fd7c 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -98,7 +98,7 @@ URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" URL_ADMIN_FLOWS_EXECUTION = "admin/realms/{realm-name}/authentication/executions/{id}" -URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" +URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION = "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_AUTHENTICATOR_CONFIG = "admin/realms/{realm-name}/authentication/config/{id}" From 2dcf68fcef691ffc43ad45d110cb1fea6d9d675e Mon Sep 17 00:00:00 2001 From: Matthew Martin Date: Sat, 4 Dec 2021 21:30:45 -0500 Subject: [PATCH 25/28] always publish wheels --- bin/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/deploy.sh b/bin/deploy.sh index e4b4d02..4c45a77 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -9,5 +9,5 @@ username=${PYPI_USERNAME} password=${PYPI_PASSWORD} EOF -python setup.py sdist +python setup.py sdist bdist_wheel --universal twine upload dist/* From 58c4426a4540fe1c067391986482a3f1c0d4be3f Mon Sep 17 00:00:00 2001 From: Matthew Martin Date: Sat, 4 Dec 2021 21:54:15 -0500 Subject: [PATCH 26/28] pure, not universal. --- bin/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/deploy.sh b/bin/deploy.sh index 4c45a77..9086dec 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -9,5 +9,5 @@ username=${PYPI_USERNAME} password=${PYPI_PASSWORD} EOF -python setup.py sdist bdist_wheel --universal +python setup.py sdist bdist_wheel twine upload dist/* From 4ba3007e412997365f3e53523c821fd36939cc38 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Jr Date: Mon, 14 Feb 2022 12:04:40 -0300 Subject: [PATCH 27/28] Reverted connection deepcopy on KeycloakAdmin --- keycloak/keycloak_admin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f3ddbcd..4f7356f 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1920,8 +1920,10 @@ class KeycloakAdmin: # merge custom headers to main headers headers.update(self.custom_headers) - self._connection = deepcopy(self.keycloak_openid.connection()) - self._connection._headers.update(headers) + self._connection = ConnectionManager(base_url=self.server_url, + headers=headers, + timeout=60, + verify=self.verify) def refresh_token(self): refresh_token = self.token.get('refresh_token') From 28c82d10510426ea6be79450595c8e8d55fd77a8 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Jr Date: Mon, 14 Feb 2022 12:08:02 -0300 Subject: [PATCH 28/28] Removed unsed import. --- keycloak/keycloak_admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 4f7356f..ab3ef20 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -26,7 +26,6 @@ import json from builtins import isinstance -from copy import deepcopy from typing import Iterable from .connection import ConnectionManager