From 9080c79a4e8c97b8c44d2013c189f5b8d69abe31 Mon Sep 17 00:00:00 2001 From: Guillaume Troupel Date: Fri, 27 Sep 2019 15:02:36 +0200 Subject: [PATCH 01/62] return user id on user creation --- keycloak/keycloak_admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 9fdc040..5e4e5ef 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -235,7 +235,9 @@ class KeycloakAdmin: data_raw = self.connection.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + _last_slash_idx = data_raw.headers['Location'].rindex('/') + return data_raw.headers['Location'][_last_slash_idx + 1:] def users_count(self): """ From 0f8e7f6a7cdc15d6664d7b1cbdd8e7c02c800cf1 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Wed, 11 Dec 2019 00:55:12 -0200 Subject: [PATCH 02/62] Set version --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index ced1647..eb0cb2c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ author = 'Marcos Pereira' # built documents. # # The short X.Y version. -version = '0.17.6' +version = '0.18.0' # The full version, including alpha/beta/rc tags. -release = '0.17.6' +release = '0.18.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 3183221..77d210b 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open("README.md", "r") as fh: setup( name='python-keycloak', - version='0.17.6', + version='0.18.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 69d29968eea72e02ffcc3b6ed652c13cafd9556a Mon Sep 17 00:00:00 2001 From: e6646 Date: Tue, 24 Dec 2019 15:57:56 -0600 Subject: [PATCH 03/62] Updated documentation and links --- keycloak/keycloak_admin.py | 150 ++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 69 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index a42bd57..d1bf1cd 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -72,6 +72,7 @@ class KeycloakAdmin: :param verify: True if want check connection SSL :param client_secret_key: client secret key :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 auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete'] """ self.server_url = server_url @@ -224,7 +225,7 @@ class KeycloakAdmin: Import a new realm from a RealmRepresentation. Realm name must be unique. RealmRepresentation - https://www.keycloak.org/docs-api/4.4/rest-api/index.html#_realmrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation :param payload: RealmRepresentation @@ -248,10 +249,11 @@ class KeycloakAdmin: """ Create a realm - ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_realmrepresentation + RealmRepresentation: + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation - :param skip_exists: Skip if Realm already exist. :param payload: RealmRepresentation + :param skip_exists: Skip if Realm already exist. :return: Keycloak server response (RealmRepresentation) """ @@ -262,8 +264,12 @@ class KeycloakAdmin: def get_users(self, query=None): """ - Get users Returns a list of users, filtered according to query parameters + Return a list of users, filtered according to query parameters + UserRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + + :param query: Query parameters (optional) :return: users list """ params_path = {"realm-name": self.realm_name} @@ -274,7 +280,7 @@ class KeycloakAdmin: Returns a list of ID Providers, IdentityProviderRepresentation - https://www.keycloak.org/docs-api/3.3/rest-api/index.html#_identityproviderrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityproviderrepresentation :return: array IdentityProviderRepresentation """ @@ -284,10 +290,10 @@ class KeycloakAdmin: def create_user(self, payload): """ - Create a new user Username must be unique + Create a new user. Username must be unique UserRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation :param payload: UserRepresentation @@ -320,7 +326,7 @@ class KeycloakAdmin: This is required for further actions against this user. UserRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation :param username: id in UserRepresentation @@ -336,7 +342,8 @@ class KeycloakAdmin: :param user_id: User id - UserRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + UserRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation :return: UserRepresentation """ @@ -346,7 +353,7 @@ class KeycloakAdmin: def get_user_groups(self, user_id): """ - Get user groups Returns a list of groups of which the user is a member + Returns a list of groups of which the user is a member :param user_id: User id @@ -387,8 +394,8 @@ class KeycloakAdmin: Set up a password for the user. If temporary is True, the user will have to reset the temporary password next time they log in. - http://www.keycloak.org/docs-api/3.2/rest-api/#_users_resource - http://www.keycloak.org/docs-api/3.2/rest-api/#_credentialrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_users_resource + https://www.keycloak.org/docs-api/8.0/rest-api/#_credentialrepresentation :param user_id: User id :param password: New password @@ -416,14 +423,14 @@ class KeycloakAdmin: def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None): """ - Send a update account email to the user An email contains a + Send an update account email to the user. An email contains a link the user can click to perform a set of required actions. - :param user_id: - :param payload: - :param client_id: - :param lifespan: - :param redirect_uri: + :param user_id: User id + :param payload: A list of actions for the user to complete + :param client_id: Client id (optional) + :param lifespan: Number of seconds after which the generated token expires (optional) + :param redirect_uri: The redirect uri (optional) :return: """ @@ -439,8 +446,8 @@ class KeycloakAdmin: link the user can click to perform a set of required actions. :param user_id: User id - :param client_id: Client id - :param redirect_uri: Redirect uri + :param client_id: Client id (optional) + :param redirect_uri: Redirect uri (optional) :return: """ @@ -457,7 +464,7 @@ class KeycloakAdmin: :param user_id: id of user UserSessionRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_usersessionrepresentation :return: UserSessionRepresentation """ @@ -470,7 +477,7 @@ class KeycloakAdmin: Get themes, social providers, auth providers, and event listeners available on this server ServerInfoRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_serverinforepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_serverinforepresentation :return: ServerInfoRepresentation """ @@ -479,10 +486,10 @@ class KeycloakAdmin: def get_groups(self): """ - Get groups belonging to the realm. Returns a list of groups belonging to the realm + Returns a list of groups belonging to the realm GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :return: array GroupRepresentation """ @@ -494,8 +501,9 @@ class KeycloakAdmin: Get group by id. Returns full group details GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + :param group_id: The group id :return: Keycloak server response (GroupRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} @@ -507,7 +515,7 @@ class KeycloakAdmin: Utility function to iterate through nested group structures GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :param name: group (GroupRepresentation) :param path: group path (string) @@ -531,8 +539,10 @@ class KeycloakAdmin: Get members by group id. Returns group members GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_userrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_userrepresentation + :param group_id: The group id + :param query: Additional query parameters (see https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getmembers) :return: Keycloak server response (UserRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} @@ -545,7 +555,7 @@ class KeycloakAdmin: Subgroups are traversed, the first to match path (or name with path) is returned. GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :param path: group path :param search_in_subgroups: True if want search in the subgroups @@ -573,9 +583,10 @@ class KeycloakAdmin: :param payload: GroupRepresentation :param parent: parent group's id. Required to create a sub-group. + :param skip_exists: If true then do not raise an error if it already exists GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :return: Http response """ @@ -599,7 +610,7 @@ class KeycloakAdmin: :param payload: GroupRepresentation with updated information. GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :return: Http response """ @@ -627,7 +638,6 @@ class KeycloakAdmin: """ Add user to group (user_id and group_id) - :param group_id: id of group :param user_id: id of user :param group_id: id of group to add to :return: Keycloak server response @@ -641,9 +651,8 @@ class KeycloakAdmin: """ Remove user from group (user_id and group_id) - :param group_id: id of group :param user_id: id of user - :param group_id: id of group to add to + :param group_id: id of group to remove from :return: Keycloak server response """ @@ -665,10 +674,10 @@ class KeycloakAdmin: def get_clients(self): """ - Get clients belonging to the realm Returns a list of clients belonging to the realm + Returns a list of clients belonging to the realm ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response (ClientRepresentation) """ @@ -682,7 +691,7 @@ class KeycloakAdmin: Get representation of the client ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) @@ -698,7 +707,7 @@ class KeycloakAdmin: This is required for further actions against this client. :param client_name: name in ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: client_id (uuid as string) """ @@ -715,7 +724,7 @@ class KeycloakAdmin: Get authorization json from client. :param client_id: id in ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ @@ -728,7 +737,7 @@ class KeycloakAdmin: Get resources from client. :param client_id: id in ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ @@ -740,9 +749,9 @@ class KeycloakAdmin: """ Create a client - ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + ClientRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation - :param skip_exists: Skip if client already exist. + :param skip_exists: If true then do not raise an error if client already exists :param payload: ClientRepresentation :return: Keycloak server response (UserRepresentation) """ @@ -771,7 +780,7 @@ class KeycloakAdmin: Get representation of the client ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :param client_id: keycloak client id (not oauth client-id) :return: Keycloak server response (ClientRepresentation) @@ -786,7 +795,7 @@ class KeycloakAdmin: Get all roles for the realm or client RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) """ @@ -802,7 +811,7 @@ class KeycloakAdmin: :param client_id: id of client (not client-id) RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) """ @@ -820,7 +829,7 @@ class KeycloakAdmin: :param role_name: role’s name (not id!) RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: role_id """ @@ -839,7 +848,7 @@ class KeycloakAdmin: :param role_name: role’s name (not id!) RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: role_id """ @@ -851,10 +860,11 @@ class KeycloakAdmin: Create a client role RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) :param payload: RoleRepresentation + :param skip_exists: If true then do not raise an error if client role already exists :return: Keycloak server response (RoleRepresentation) """ @@ -865,10 +875,10 @@ class KeycloakAdmin: def delete_client_role(self, client_role_id, role_name): """ - Create a client role + Delete a client role RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) :param role_name: role’s name (not id!) @@ -881,9 +891,8 @@ class KeycloakAdmin: """ Assign a client role 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 client_id: id of client (not client-id) :param roles: roles list or role (use RoleRepresentation) :return Keycloak server response """ @@ -898,8 +907,8 @@ class KeycloakAdmin: """ Create a new role for the realm or client - :param realm: realm name (not id) - :param rep: RoleRepresentation https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_rolerepresentation + :param payload: The role (use RoleRepresentation) + :param skip_exists: If true then do not raise an error if realm role already exists :return Keycloak server response """ @@ -913,9 +922,8 @@ class KeycloakAdmin: """ 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 client_id: id of client containing role (not client-id) :param roles: roles list or role (use RoleRepresentation) :return Keycloak server response """ @@ -930,8 +938,8 @@ class KeycloakAdmin: """ Get all client roles for a user. - :param client_id: id of client (not client-id) :param user_id: id of user + :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES, user_id, client_id) @@ -940,8 +948,8 @@ class KeycloakAdmin: """ Get available client role-mappings for a user. - :param client_id: id of client (not client-id) :param user_id: id of user + :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id) @@ -950,8 +958,8 @@ class KeycloakAdmin: """ Get composite client role-mappings for a user. - :param client_id: id of client (not client-id) :param user_id: id of user + :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id) @@ -965,9 +973,8 @@ class KeycloakAdmin: """ Delete client roles from 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 client_id: id of client containing role (not client-id) :param roles: roles list or role to delete (use RoleRepresentation) :return: Keycloak server response """ @@ -982,7 +989,7 @@ class KeycloakAdmin: Get authentication flows. Returns all flow details AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation :return: Keycloak server response (AuthenticationFlowRepresentation) """ @@ -995,9 +1002,10 @@ class KeycloakAdmin: Create a new authentication flow AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation :param payload: AuthenticationFlowRepresentation + :param skip_exists: If true then do not raise an error if authentication flow already exists :return: Keycloak server response (RoleRepresentation) """ @@ -1010,6 +1018,7 @@ class KeycloakAdmin: """ Get authentication flow executions. Returns all execution steps + :param flow_alias: the flow alias :return: Response(json) """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} @@ -1021,9 +1030,10 @@ class KeycloakAdmin: Update an authentication flow execution AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation :param payload: AuthenticationExecutionInfoRepresentation + :param flow_alias: The flow alias :return: Keycloak server response """ @@ -1036,8 +1046,8 @@ class KeycloakAdmin: """ Function to trigger user sync from provider - :param storage_id: - :param action: + :param storage_id: The id of the user storage provider + :param action: Action can be "triggerFullSync" or "triggerChangedUsersSync" :return: """ data = {'action': action} @@ -1051,7 +1061,7 @@ class KeycloakAdmin: def get_client_scopes(self): """ Get representation of the client scopes for the realm where we are connected to - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientscopes + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes :return: Keycloak server response Array of (ClientScopeRepresentation) """ @@ -1063,8 +1073,9 @@ class KeycloakAdmin: def get_client_scope(self, client_scope_id): """ Get representation of the client scopes for the realm where we are connected to - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientscopes + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + :param client_scope_id: The id of the client scope :return: Keycloak server response (ClientScopeRepresentation) """ @@ -1076,8 +1087,9 @@ class KeycloakAdmin: def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_create_mapper + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_create_mapper + :param client_scope_id: The id of the client scope :param payload: ProtocolMapperRepresentation :return: Keycloak server Response """ @@ -1093,7 +1105,7 @@ class KeycloakAdmin: """ Get representation of the client secrets - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientsecret + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientsecret :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) From 8436dbc19d4b7de0f7557b4e5013f1730150b4fd Mon Sep 17 00:00:00 2001 From: Josh Bode Date: Thu, 16 Jan 2020 12:16:19 +1100 Subject: [PATCH 04/62] Set session `auth` to trivial function Sets the session to not add "basic" auth if entry for keycloak host exists in users `.netrc` (`requests` causes `Authorization: Basic ...` header to be added if an entry exists in `.netrc` by default) see: https://requests.readthedocs.io/en/master/api/#requests.Session.auth --- keycloak/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/keycloak/connection.py b/keycloak/connection.py index 6f32439..451082c 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -47,6 +47,7 @@ class ConnectionManager(object): self._timeout = timeout self._verify = verify self._s = requests.Session() + self._s.auth = lambda x: x # don't let requests add auth headers # retry once to reset connection with Keycloak after tomcat's ConnectionTimeout # see https://github.com/marcospereirampj/python-keycloak/issues/36 From fa9f4520d5eb9f290d062095e7d445117057a531 Mon Sep 17 00:00:00 2001 From: rike e Date: Tue, 4 Feb 2020 09:30:04 +0100 Subject: [PATCH 05/62] include data in raw_delete request data was missing in raw_delete request: requests with json bodies like in "delete_client_roles_of_user" won't work --- keycloak/connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keycloak/connection.py b/keycloak/connection.py index 6f32439..65e8a0c 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -198,11 +198,12 @@ class ConnectionManager(object): raise KeycloakConnectionError( "Can't connect to server (%s)" % e) - def raw_delete(self, path, **kwargs): + def raw_delete(self, path, data, **kwargs): """ Submit delete request to the path. :arg path (str): Path for request. + data (dict): Payload for request. :return Response the request. :exception @@ -211,6 +212,7 @@ class ConnectionManager(object): try: return self._s.delete(urljoin(self.base_url, path), params=kwargs, + data=data, headers=self.headers, timeout=self.timeout, verify=self.verify) From d743e43065344c7e1f96461a7f4197000715e161 Mon Sep 17 00:00:00 2001 From: twsl <45483159+twsI@users.noreply.github.com> Date: Fri, 14 Feb 2020 00:12:00 +0100 Subject: [PATCH 06/62] Added public key method --- README.md | 2 +- keycloak/keycloak_openid.py | 14 +++++++++++++- keycloak/urls_patterns.py | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f39f86..d3b71e5 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['ac token_info = keycloak_openid.introspect(token['access_token'])) # Decode Token -KEYCLOAK_PUBLIC_KEY = "secret" +KEYCLOAK_PUBLIC_KEY = keycloak_openid.public_key() options = {"verify_signature": True, "verify_aud": True, "exp": True} token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index b196a85..2d12678 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -30,6 +30,7 @@ from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError, \ KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError from .urls_patterns import ( + URL_REALM, URL_AUTH, URL_TOKEN, URL_USERINFO, @@ -263,8 +264,19 @@ class KeycloakOpenID: :return: """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) + data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + + def public_key(self): + """ + The public key is exposed by the realm page directly. + + :return: + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError)['public_key'] + def entitlement(self, token, resource_server_id): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index fad3455..e3f4d95 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -22,6 +22,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # OPENID URLS +URL_REALM = "realms/{realm-name}" URL_WELL_KNOWN = "realms/{realm-name}/.well-known/openid-configuration" URL_TOKEN = "realms/{realm-name}/protocol/openid-connect/token" URL_USERINFO = "realms/{realm-name}/protocol/openid-connect/userinfo" From 51b7f29dd40642d18392eb541de42820c11ed584 Mon Sep 17 00:00:00 2001 From: twsl <45483159+twsI@users.noreply.github.com> Date: Fri, 14 Feb 2020 00:29:13 +0100 Subject: [PATCH 07/62] Fixed mixed up urls --- keycloak/keycloak_openid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 2d12678..c39dbf6 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -264,7 +264,7 @@ class KeycloakOpenID: :return: """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) + data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def public_key(self): @@ -274,7 +274,7 @@ class KeycloakOpenID: :return: """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) + data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError)['public_key'] From f2c32eb5c4820c1c223e9e21302851d4287ca5c9 Mon Sep 17 00:00:00 2001 From: Hari Yerramsetty Date: Sun, 16 Feb 2020 00:22:19 -0500 Subject: [PATCH 08/62] Documentation: Add client_secret_key while instantiating KeyCloakAdmin() --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5f39f86..a752abb 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", username='example-admin', password='secret', realm_name="example_realm", + client_secret_key="client-secret", verify=True) # Add user From 294df15e304bc0c0ae72f751e22831eeac4bdd51 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 18 Feb 2020 21:55:17 -0300 Subject: [PATCH 09/62] Release 0.19.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index eb0cb2c..8614386 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ author = 'Marcos Pereira' # built documents. # # The short X.Y version. -version = '0.18.0' +version = '0.19.0' # The full version, including alpha/beta/rc tags. -release = '0.18.0' +release = '0.19.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 77d210b..b89cd31 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open("README.md", "r") as fh: setup( name='python-keycloak', - version='0.18.0', + version='0.19.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 7dff2e2b759e6a7009b41db4011bacc9d40fa2b2 Mon Sep 17 00:00:00 2001 From: Paolo Romolini Date: Mon, 24 Feb 2020 09:30:56 +0100 Subject: [PATCH 10/62] Add group realm roles delete, get and add Signed-off-by: Paolo Romolini Signed-off-by: Tamara Noncentini --- keycloak/keycloak_admin.py | 43 ++++++++++++++++++++++++++++++++++++++ keycloak/urls_patterns.py | 2 ++ 2 files changed, 45 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6d7bd20..089416a 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -28,6 +28,8 @@ import json from builtins import isinstance from typing import List, Iterable +from keycloak.urls_patterns import URL_ADMIN_GROUPS_REALM_ROLES, \ + URL_ADMIN_GET_GROUPS_REALM_ROLES from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID @@ -936,6 +938,47 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def assign_group_realm_roles(self, group_id, roles): + """ + Assign realm roles to a group + + :param group_id: id of groupp + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.raw_post(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_group_realm_roles(self, group_id, roles): + """ + Delete realm roles of a group + + :param group_id: id of group + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.raw_delete(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def get_group_realm_roles(self, group_id): + """ + Get all realm roles for a group. + + :param user_id: id of the group + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.raw_get(URL_ADMIN_GET_GROUPS_REALM_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_client_roles_of_user(self, user_id, client_id): """ Get all client roles for a user. diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index e3f4d95..19ca28f 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -43,6 +43,8 @@ URL_ADMIN_RESET_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" 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_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_GET_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings" 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_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" From d47131d5ab9172f7a9d6830b4a10c116481ddbdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Thu, 5 Mar 2020 12:27:41 +0100 Subject: [PATCH 11/62] make data-parameter optional Fixing issue https://github.com/marcospereirampj/python-keycloak/issues/65 --- keycloak/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/connection.py b/keycloak/connection.py index 4841792..12903a0 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -199,7 +199,7 @@ class ConnectionManager(object): raise KeycloakConnectionError( "Can't connect to server (%s)" % e) - def raw_delete(self, path, data, **kwargs): + def raw_delete(self, path, data={}, **kwargs): """ Submit delete request to the path. :arg From 429761b520e0af0871073d5365bdf2e672ec4d8a Mon Sep 17 00:00:00 2001 From: Joshua Adelman Date: Thu, 5 Mar 2020 11:28:33 -0500 Subject: [PATCH 12/62] add MANIFEST.in to package license file --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1aba38f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE From 505d0e5772ee690de0b0947389073f36180c59f8 Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Fri, 13 Mar 2020 16:53:57 +0100 Subject: [PATCH 13/62] Add generate_client_secrets --- keycloak/keycloak_admin.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6d7bd20..634019c 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1103,6 +1103,20 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + def generate_client_secrets(self, client_id): + """ + + Generate a new secret for the client + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_regeneratesecret + + :param client_id: id of client (not client-id) + :return: Keycloak server response (ClientRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_post(URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_client_secrets(self, client_id): """ From 43a7a3943b3256a9dacf6dd93c0bc077e4e4fa52 Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Fri, 13 Mar 2020 17:05:03 +0100 Subject: [PATCH 14/62] Add support for Client Credentials Grant in KeycloakAdmin --- docs/source/index.rst | 8 ++++++++ keycloak/keycloak_admin.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index af697da..597cfb2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -158,6 +158,14 @@ Main methods:: # realm_name="example_realm", # verify=True, # custom_headers={'CustomHeader': 'value'}) + # + # You can also authenticate with client_id and client_secret + #keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", + # client_id="example_client", + # client_secret_key="secret", + # realm_name="example_realm", + # verify=True, + # custom_headers={'CustomHeader': 'value'}) # Add user new_user = keycloak_admin.create_user({"email": "example@example.com", diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6d7bd20..c96d397 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -60,7 +60,7 @@ class KeycloakAdmin: _custom_headers = None _user_realm_name = None - def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True, + def __init__(self, server_url, username=None, password=None, realm_name='master', client_id='admin-cli', verify=True, client_secret_key=None, custom_headers=None, user_realm_name=None, auto_refresh_token=None): """ From 4315d90d0be73139dc4f839cc9ce7e940cc3d4d3 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Sat, 11 Apr 2020 03:11:12 -0300 Subject: [PATCH 15/62] Release 0.20.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 8614386..ba92885 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ author = 'Marcos Pereira' # built documents. # # The short X.Y version. -version = '0.19.0' +version = '0.20.0' # The full version, including alpha/beta/rc tags. -release = '0.19.0' +release = '0.20.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index b89cd31..a0a550b 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open("README.md", "r") as fh: setup( name='python-keycloak', - version='0.19.0', + version='0.20.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 488b4fa8e1f11c0e13804c84dd54d11622762eb6 Mon Sep 17 00:00:00 2001 From: Matthew DuCharme Date: Sat, 11 Apr 2020 15:21:18 -0400 Subject: [PATCH 16/62] Added routes for deleting and updating realms --- keycloak/keycloak_admin.py | 32 +++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 0bcf28f..8e66d9e 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -42,7 +42,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES + URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM class KeycloakAdmin: @@ -263,6 +263,36 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + def update_realm(self, realm_name, payload): + """ + Update a realm. This wil only update top level attributes and will ignore any user, + role, or client information in the payload. + + RealmRepresentation: + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + + :param realm_name: Realm name (not the realm id) + :param payload: RealmRepresentation + :return: Http response + """ + + params_path = {"realm-name": realm_name} + data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_realm(self, realm_name): + """ + Delete a realm + + :param realm_name: Realm name (not the realm id) + :return: Http response + """ + + params_path = {"realm-name": realm_name} + data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def get_users(self, query=None): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 19ca28f..d49a5a9 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -75,6 +75,7 @@ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER = URL_ADMIN_CLIENT_SCOPE + "/protocol-mappers URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" URL_ADMIN_REALMS = "admin/realms" +URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" From e03a1ba9bec4ad9fb7c13db9c5c2030a03ec2b9b Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Thu, 30 Apr 2020 16:51:45 +0200 Subject: [PATCH 17/62] feat: add components --- docs/source/index.rst | 20 +++++++++++ keycloak/keycloak_admin.py | 74 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 3 ++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 597cfb2..aeb8744 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -276,3 +276,23 @@ Main methods:: # Function to trigger user sync from provider sync_users(storage_id="storage_di", action="action") + + # Rotate RSA realm keys + # List existing rsa keys + components = keycloak_admin.get_components(query={"parent":"example_realm", "type":"org.keycloak.keys.KeyProvider"}) + components_rsa_generated = list(filter(lambda component: component["provider-id"] == "rsa-generated")) + + # Create a new one + keycloak_admin.create_component({"name":"rsa-generated","providerId":"rsa-generated","providerType":"org.keycloak.keys.KeyProvider","parentId":"example_realm","config":{"priority":["100"],"enabled":["true"],"active":["true"],"algorithm":["RS256"],"keySize":["2048"]}}) + + for component in components_rsa_generated: + component_details = keycloak_admin.get_component(component['id']) + + # Delete inactive keys + if component_details['config']['active'] == ["false"]: + keycloak_admin.delete_component(component['id']) + + # Make previous keys inactive + else: + component_details['config']['active'] = ["false"] + keycloak_admin.update_component(component['id'], component_details) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 0bcf28f..c0d8ab8 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -42,7 +42,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES + URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT class KeycloakAdmin: @@ -1174,6 +1174,78 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_CLIENT_SECRETS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_components(self, query=None): + """ + Return a list of components, filtered according to query parameters + + ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :param query: Query parameters (optional) + :return: components list + """ + params_path = {"realm-name": self.realm_name} + return self.__fetch_all(URL_ADMIN_COMPONENTS.format(**params_path), query) + + def create_component(self, payload): + """ + Create a new component. + + ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :param payload: ComponentRepresentation + + :return: UserRepresentation + """ + params_path = {"realm-name": self.realm_name} + + data_raw = self.raw_post(URL_ADMIN_COMPONENTS.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + + def get_component(self, component_id): + """ + Get representation of the component + + :param component_id: Component id + + ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :return: ComponentRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": component_id} + data_raw = self.raw_get(URL_ADMIN_COMPONENT.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def update_component(self, component_id, payload): + """ + Update the component + + :param component_id: Component id + :param payload: ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :return: Http response + """ + params_path = {"realm-name": self.realm_name, "id": component_id} + data_raw = self.raw_put(URL_ADMIN_COMPONENT.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_component(self, component_id): + """ + Delete the component + + :param component_id: Component id + + :return: Http response + """ + params_path = {"realm-name": self.realm_name, "id": component_id} + data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def raw_get(self, *args, **kwargs): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 19ca28f..809e1f1 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -79,3 +79,6 @@ URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" + +URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" +URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/component/{component-id}" From 77d5325c07ed2f7b103c758afb17ae2a789c000a Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Thu, 30 Apr 2020 19:01:15 +0200 Subject: [PATCH 18/62] fixes + add get_keys --- docs/source/index.rst | 26 +++++++++++--------------- keycloak/keycloak_admin.py | 30 +++++++++++++++++++++++------- keycloak/urls_patterns.py | 3 ++- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index aeb8744..2b41d35 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -277,22 +277,18 @@ Main methods:: # Function to trigger user sync from provider sync_users(storage_id="storage_di", action="action") - # Rotate RSA realm keys - # List existing rsa keys - components = keycloak_admin.get_components(query={"parent":"example_realm", "type":"org.keycloak.keys.KeyProvider"}) - components_rsa_generated = list(filter(lambda component: component["provider-id"] == "rsa-generated")) + # List public RSA keys + components = keycloak_admin.keys - # Create a new one - keycloak_admin.create_component({"name":"rsa-generated","providerId":"rsa-generated","providerType":"org.keycloak.keys.KeyProvider","parentId":"example_realm","config":{"priority":["100"],"enabled":["true"],"active":["true"],"algorithm":["RS256"],"keySize":["2048"]}}) + # List all keys + components = keycloak_admin.get_components(query={"parent":"example_realm", "type":"org.keycloak.keys.KeyProvider"}) - for component in components_rsa_generated: - component_details = keycloak_admin.get_component(component['id']) + # Create a new RSA key + component = keycloak_admin.create_component({"name":"rsa-generated","providerId":"rsa-generated","providerType":"org.keycloak.keys.KeyProvider","parentId":"example_realm","config":{"priority":["100"],"enabled":["true"],"active":["true"],"algorithm":["RS256"],"keySize":["2048"]}}) - # Delete inactive keys - if component_details['config']['active'] == ["false"]: - keycloak_admin.delete_component(component['id']) + # Update the key + component_details['config']['active'] = ["false"] + keycloak_admin.update_component(component['id']) - # Make previous keys inactive - else: - component_details['config']['active'] = ["false"] - keycloak_admin.update_component(component['id'], component_details) + # Delete the key + keycloak_admin.delete_component(component['id']) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c0d8ab8..36dca80 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -41,8 +41,8 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, + URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS class KeycloakAdmin: @@ -1185,7 +1185,9 @@ class KeycloakAdmin: :return: components list """ params_path = {"realm-name": self.realm_name} - return self.__fetch_all(URL_ADMIN_COMPONENTS.format(**params_path), query) + data_raw = self.raw_get(URL_ADMIN_COMPONENTS.format(**params_path), + data=None, **query) + return raise_error_from_response(data_raw, KeycloakGetError) def create_component(self, payload): """ @@ -1202,7 +1204,7 @@ class KeycloakAdmin: data_raw = self.raw_post(URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) def get_component(self, component_id): """ @@ -1215,7 +1217,7 @@ class KeycloakAdmin: :return: ComponentRepresentation """ - params_path = {"realm-name": self.realm_name, "id": component_id} + params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_get(URL_ADMIN_COMPONENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) @@ -1229,7 +1231,7 @@ class KeycloakAdmin: :return: Http response """ - params_path = {"realm-name": self.realm_name, "id": component_id} + params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_put(URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) @@ -1242,10 +1244,24 @@ class KeycloakAdmin: :return: Http response """ - params_path = {"realm-name": self.realm_name, "id": component_id} + params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def get_keys(self): + """ + Return a list of keys, filtered according to query parameters + + KeysMetadataRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_key_resource + + :return: keys list + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get(URL_ADMIN_KEYS.format(**params_path), + data=None) + return raise_error_from_response(data_raw, KeycloakGetError) + def raw_get(self, *args, **kwargs): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 809e1f1..8cf764b 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -81,4 +81,5 @@ URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" -URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/component/{component-id}" +URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" +URL_ADMIN_KEYS = "admin/realms/{realm-name}/components/keys" From b1ef1d3dfda04301f64466fbd959ed4892607766 Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Sun, 3 May 2020 22:25:47 +0200 Subject: [PATCH 19/62] fix indent --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 36dca80..ae66d53 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -41,7 +41,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS From 24fc50fae89924e3e1b0c69399bbd90f2b15e977 Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Sun, 3 May 2020 22:31:06 +0200 Subject: [PATCH 20/62] fix keys url --- keycloak/urls_patterns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 8cf764b..95fc345 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -82,4 +82,4 @@ URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{fl URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" -URL_ADMIN_KEYS = "admin/realms/{realm-name}/components/keys" +URL_ADMIN_KEYS = "admin/realms/{realm-name}/keys" From 4f7069b6754edc854d8c62945554df92989d15e9 Mon Sep 17 00:00:00 2001 From: Shady Nawara Date: Thu, 7 May 2020 04:47:59 -0500 Subject: [PATCH 21/62] Modified update_client & create_realm_role to use keycloak_admin raw_put/post Modified update_client & create_realm_role to use the keycloak_admin raw_put/post instead of the connection's raw_put/post directly to allow for token refresh --- 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 0bcf28f..f13d889 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -775,7 +775,7 @@ class KeycloakAdmin: :return: Http response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_put(URL_ADMIN_CLIENT.format(**params_path), + data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) @@ -917,7 +917,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), + data_raw = self.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) From 50b65c06a2af1e374351511ab7724f589eb1f367 Mon Sep 17 00:00:00 2001 From: Paolo Romolini Date: Fri, 8 May 2020 15:39:23 +0200 Subject: [PATCH 22/62] Add update and delete role by name Add 'Update a role' and 'Delete a role' by name and without passing the client parameter --- keycloak/keycloak_admin.py | 26 +++++++++++++++++++++++++- keycloak/urls_patterns.py | 2 ++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 0bcf28f..7b87b45 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -29,7 +29,7 @@ from builtins import isinstance from typing import List, Iterable from keycloak.urls_patterns import URL_ADMIN_GROUPS_REALM_ROLES, \ - URL_ADMIN_GET_GROUPS_REALM_ROLES + URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID @@ -921,6 +921,30 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + def update_realm_role(self, role_name, payload): + """ + Update a role for the realm by name + :param role_name: The name of the role to be updated + :param payload: The role (use RoleRepresentation) + :return Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.connection.raw_put(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_realm_role(self, role_name): + """ + Delete a role for the realm by name + :param payload: The role name {'role-name':'name-of-the-role'} + :return Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.connection.raw_delete( + URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def assign_realm_roles(self, user_id, client_id, roles): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 19ca28f..8cb88b2 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -76,6 +76,8 @@ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER = URL_ADMIN_CLIENT_SCOPE + "/protocol-mappers URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" URL_ADMIN_REALMS = "admin/realms" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" +URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" + URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" From 1f5e9ad2e4d48abaec74c14a7de53a80cf5efafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Thu, 4 Jun 2020 15:13:34 +0200 Subject: [PATCH 23/62] fixing issue #91 and be backwards compatible with older keycloak versions --- keycloak/exceptions.py | 9 ++++--- keycloak/keycloak_admin.py | 50 ++++++++++++++++++------------------- keycloak/keycloak_openid.py | 2 +- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index a3894e7..381d9af 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -73,9 +73,12 @@ class KeycloakInvalidTokenError(KeycloakOperationError): pass -def raise_error_from_response(response, error, expected_code=200, skip_exists=False): - if expected_code == response.status_code: - if expected_code == requests.codes.no_content: +def raise_error_from_response(response, error, expected_codes=None, skip_exists=False): + if expected_codes is None: + expected_codes = [200, 201, 204] + + if response.status_code in expected_codes: + if response.status_code == requests.codes.no_content: return {} try: diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 0bcf28f..d05ca9f 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -236,7 +236,7 @@ class KeycloakAdmin: data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_realms(self): """ @@ -261,7 +261,7 @@ class KeycloakAdmin: data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def get_users(self, query=None): @@ -310,7 +310,7 @@ class KeycloakAdmin: data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) - raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) _last_slash_idx = data_raw.headers['Location'].rindex('/') return data_raw.headers['Location'][_last_slash_idx + 1:] @@ -379,7 +379,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_user(self, user_id): """ @@ -391,7 +391,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_delete(URL_ADMIN_USER.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def set_user_password(self, user_id, password, temporary=True): """ @@ -411,7 +411,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def consents_user(self, user_id): """ @@ -604,7 +604,7 @@ class KeycloakAdmin: data_raw = self.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def update_group(self, group_id, payload): """ @@ -622,7 +622,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): """ @@ -649,7 +649,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def group_user_remove(self, user_id, group_id): """ @@ -662,7 +662,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_group(self, group_id): """ @@ -674,7 +674,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete(URL_ADMIN_GROUP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_clients(self): """ @@ -763,7 +763,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def update_client(self, client_id, payload): """ @@ -777,7 +777,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.connection.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_client(self, client_id): """ @@ -792,7 +792,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_realm_roles(self): """ @@ -875,7 +875,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_role_id} data_raw = self.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def delete_client_role(self, client_role_id, role_name): """ @@ -889,7 +889,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} data_raw = self.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def assign_client_role(self, user_id, client_id, roles): """ @@ -905,7 +905,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_post(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def create_realm_role(self, payload, skip_exists=False): """ @@ -919,7 +919,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def assign_realm_roles(self, user_id, client_id, roles): @@ -936,7 +936,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_post(URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def assign_group_realm_roles(self, group_id, roles): """ @@ -951,7 +951,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_post(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_group_realm_roles(self, group_id, roles): """ @@ -966,7 +966,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_group_realm_roles(self, group_id): """ @@ -1027,7 +1027,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_delete(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_authentication_flows(self): """ @@ -1057,7 +1057,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def get_authentication_flow_executions(self, flow_alias): """ @@ -1085,7 +1085,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def sync_users(self, storage_id, action): """ @@ -1144,7 +1144,7 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def generate_client_secrets(self, client_id): """ diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index c39dbf6..9ab42f0 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -251,7 +251,7 @@ class KeycloakOpenID: data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def certs(self): """ From c135a4ebb3f97b187f80ab5f940fc7076d852fcf Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 30 Jun 2020 00:11:22 -0300 Subject: [PATCH 24/62] Fixed linter. --- keycloak/keycloak_admin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c3ef9a9..6293d5b 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -293,7 +293,6 @@ class KeycloakAdmin: data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) - def get_users(self, query=None): """ Return a list of users, filtered according to query parameters @@ -1182,7 +1181,6 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope @@ -1316,7 +1314,6 @@ class KeycloakAdmin: data=None) return raise_error_from_response(data_raw, KeycloakGetError) - def raw_get(self, *args, **kwargs): """ Calls connection.raw_get. From 3273ea9f001442b3ff081bcfddcf25c3767085bf Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 30 Jun 2020 00:12:35 -0300 Subject: [PATCH 25/62] Release version 0.21.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index ba92885..b09b16b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ author = 'Marcos Pereira' # built documents. # # The short X.Y version. -version = '0.20.0' +version = '0.21.0' # The full version, including alpha/beta/rc tags. -release = '0.20.0' +release = '0.21.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index a0a550b..acf6187 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open("README.md", "r") as fh: setup( name='python-keycloak', - version='0.20.0', + version='0.21.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From a1877c5236e3b8f9697555e6ce95dd5fbe8effb5 Mon Sep 17 00:00:00 2001 From: Hari Yerramsetty Date: Fri, 10 Jul 2020 18:31:36 -0400 Subject: [PATCH 26/62] Update README.md set_user_password is an instance method KeycloakAdmin class --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26b9e03..03b4f64 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ response = keycloak_admin.update_user(user_id="user-id-keycloak", payload={'firstName': 'Example Update'}) # Update User Password -response = set_user_password(user_id="user-id-keycloak", password="secret", temporary=True) +response = keycloak_admin.set_user_password(user_id="user-id-keycloak", password="secret", temporary=True) # Delete User response = keycloak_admin.delete_user(user_id="user-id-keycloak") From 0c26781a0993e18b4667943a7151d0719edd90cc Mon Sep 17 00:00:00 2001 From: Evgeni Enchev Date: Thu, 16 Jul 2020 20:20:41 +0300 Subject: [PATCH 27/62] add client-level operations for group roles --- keycloak/keycloak_admin.py | 47 ++++++++++++++++++++++++++++++++++++++ keycloak/urls_patterns.py | 1 + 2 files changed, 48 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6293d5b..44d69f5 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1032,6 +1032,53 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_GET_GROUPS_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def assign_group_client_roles(self, group_id, client_id, roles): + """ + Assign client roles to a group + + :param group_id: id of group + :param client_id: id of client (not client-id) + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + data_raw = self.raw_post(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_group_client_roles(self, group_id, client_id, roles): + """ + Delete client roles of a group + + :param group_id: id of group + :param client_id: id of client (not client-id) + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + data_raw = self.raw_get(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_group_client_roles(self, group_id, client_id, roles): + """ + Get client roles of a group + + :param group_id: id of group + :param client_id: id of client (not client-id) + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + data_raw = self.raw_delete(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def get_client_roles_of_user(self, user_id, client_id): """ Get all client roles for a user. diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index f08a422..247e6c9 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -45,6 +45,7 @@ URL_ADMIN_USER_CLIENT_ROLES = "admin/realms/{realm-name}/users/{id}/role-mapping 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_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_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_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" From 19af6bcedab663ab22c379811011ecf056635024 Mon Sep 17 00:00:00 2001 From: Evgeni Enchev Date: Thu, 16 Jul 2020 20:24:52 +0300 Subject: [PATCH 28/62] improve comment --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 44d69f5..27423c4 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1070,7 +1070,7 @@ class KeycloakAdmin: :param group_id: id of group :param client_id: id of client (not client-id) :param roles: roles list or role (use GroupRoleRepresentation) - :return Keycloak server response + :return Keycloak server response (array RoleRepresentation) """ payload = roles if isinstance(roles, list) else [roles] From 68dea8051a32be3ad222a5bf30922f9d99270edb Mon Sep 17 00:00:00 2001 From: Gemini Lasswell Date: Tue, 21 Jul 2020 15:57:45 -0700 Subject: [PATCH 29/62] Handle 'Token is not active' error in refresh_token If a token is acquired but not used before it times out, Keycloak will return a 400 response with the message 'Token is not active'. Change refresh_token to handle this error by getting a new token. --- keycloak/keycloak_admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6293d5b..2211151 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1397,7 +1397,8 @@ 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: + 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 From 0baf5116477bbccd2a143e7156182ce70707501c Mon Sep 17 00:00:00 2001 From: Jonek Date: Tue, 28 Jul 2020 12:51:44 +0200 Subject: [PATCH 30/62] Fix broken link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 03b4f64..98af23d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ For review- see https://github.com/marcospereirampj/python-keycloak python-keycloak depends on: * Python 3 -* [requests](http://docs.python-requests.org/en/master/) +* [requests](https://requests.readthedocs.io) * [python-jose](http://python-jose.readthedocs.io/en/latest/) ### Tests Dependencies From b98e1533774f1d4fb9145e7904d98bd45cd0609b Mon Sep 17 00:00:00 2001 From: giordyb Date: Mon, 3 Aug 2020 09:59:43 +0200 Subject: [PATCH 31/62] Update keycloak_admin.py added capabilities to add and get user's social login --- keycloak/keycloak_admin.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6293d5b..232c5d3 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -42,7 +42,8 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS + 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 class KeycloakAdmin: @@ -454,6 +455,29 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_user_social_logins(self, user_id): + """ + Returns a list of federated identities/social logins of which the user has been associated with + :param user_id: User id + :return: federated identities list + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_FEDERATED_IDENTITIES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def add_user_social_login(self, user_id, provider_id, provider_userid, provider_username): + + """ + Add a federated identity / social login provider to the user + :param user_id: User id + :param provider: Social login provider id + :param realm: realm name + :return: + """ + payload = {"identityProvider": provider_id, "userId": provider_userid, "userName": provider_username} + 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 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 707637bfd6e68c7eeec40d693855497a6f08848a Mon Sep 17 00:00:00 2001 From: giordyb Date: Mon, 3 Aug 2020 10:00:46 +0200 Subject: [PATCH 32/62] Update urls_patterns.py added url patterns for social login --- keycloak/urls_patterns.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index f08a422..bb22af7 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -86,3 +86,6 @@ URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{fl URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" 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_IDENTITY = "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}" From 3838ed036ba2e13dc6f5774fb7bded77bcbd015d Mon Sep 17 00:00:00 2001 From: biagio Date: Mon, 3 Aug 2020 10:08:43 +0200 Subject: [PATCH 33/62] fix comma --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 232c5d3..86e321f 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -42,7 +42,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS \ + 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 From 03522bb5904d48933f115b878a0c2d3c4d9d4e94 Mon Sep 17 00:00:00 2001 From: biagio Date: Mon, 3 Aug 2020 10:15:08 +0200 Subject: [PATCH 34/62] added description --- keycloak/keycloak_admin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 86e321f..58e01e2 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -470,8 +470,9 @@ class KeycloakAdmin: """ Add a federated identity / social login provider to the user :param user_id: User id - :param provider: Social login provider id - :param realm: realm name + :param provider_id: Social login provider id + :param provider_userid: userid specified by the provider + :param provider_username: username specified by the provider :return: """ payload = {"identityProvider": provider_id, "userId": provider_userid, "userName": provider_username} From 3a45fa5d4e55b4cb3685db6726a819f8972f3d64 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Sun, 16 Aug 2020 12:09:16 -0300 Subject: [PATCH 35/62] Release version 0.22.0 --- docs/source/conf.py | 4 ++-- keycloak/keycloak_admin.py | 21 ++++++++++----------- setup.py | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b09b16b..96f7777 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ author = 'Marcos Pereira' # built documents. # # The short X.Y version. -version = '0.21.0' +version = '0.22.0' # The full version, including alpha/beta/rc tags. -release = '0.21.0' +release = '0.22.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 2f0b03a..ae443b6 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -29,7 +29,7 @@ from builtins import isinstance from typing import List, Iterable from keycloak.urls_patterns import URL_ADMIN_GROUPS_REALM_ROLES, \ - URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME + URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID @@ -194,7 +194,6 @@ class KeycloakAdmin: self._auto_refresh_token = value - def __fetch_all(self, url, query=None): '''Wrapper function to paginate GET requests @@ -280,7 +279,7 @@ class KeycloakAdmin: params_path = {"realm-name": realm_name} data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_realm(self, realm_name): """ @@ -292,7 +291,7 @@ class KeycloakAdmin: params_path = {"realm-name": realm_name} data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_users(self, query=None): """ @@ -986,7 +985,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.connection.raw_put(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_realm_role(self, role_name): """ @@ -998,7 +997,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.connection.raw_delete( URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def assign_realm_roles(self, user_id, client_id, roles): """ @@ -1071,7 +1070,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_post(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_group_client_roles(self, group_id, client_id, roles): """ @@ -1102,7 +1101,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_delete(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_client_roles_of_user(self, user_id, client_id): """ @@ -1328,7 +1327,7 @@ class KeycloakAdmin: data_raw = self.raw_post(URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_component(self, component_id): """ @@ -1358,7 +1357,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_put(URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_component(self, component_id): """ @@ -1370,7 +1369,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_keys(self): """ diff --git a/setup.py b/setup.py index acf6187..9467d53 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open("README.md", "r") as fh: setup( name='python-keycloak', - version='0.21.0', + version='0.22.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 7f0ed66803808c58fe2a21e37a6f498e281c4cd4 Mon Sep 17 00:00:00 2001 From: ggallard Date: Wed, 19 Aug 2020 18:31:04 -0300 Subject: [PATCH 36/62] Added create_client_scope --- keycloak/keycloak_admin.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6..7603a88 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1252,6 +1252,22 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def create_client_scope(self, payload, skip_exists=False): + """ + Create a client scope + + ClientScopeRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + + :param payload: ClientScopeRepresentation + :param skip_exists: If true then do not raise an error if client scope already exists + :return: Keycloak server response (ClientScopeRepresentation) + """ + + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_post(URL_ADMIN_CLIENT_SCOPES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope From a02195cfd696322de836b45497cfe3f790c04979 Mon Sep 17 00:00:00 2001 From: ggallard Date: Thu, 20 Aug 2020 17:17:49 -0300 Subject: [PATCH 37/62] fixed for updated parameter on raise_error_from_response() --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 7603a88..d265a54 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1266,7 +1266,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def add_mapper_to_client_scope(self, client_scope_id, payload): """ From f7a358b034bc73f3966dce98cd25daddc05ea1d5 Mon Sep 17 00:00:00 2001 From: ggallard Date: Thu, 20 Aug 2020 18:38:20 -0300 Subject: [PATCH 38/62] added get_client_service_account_user --- keycloak/keycloak_admin.py | 15 ++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index d265a54..82d8323 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -43,7 +43,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ 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_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER class KeycloakAdmin: @@ -802,6 +802,19 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path)) return data_raw + def get_client_service_account_user(self, client_id): + """ + Get service account user from client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: UserRepresentation + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get(URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def create_client(self, payload, skip_exists=False): """ Create a client diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8..3369d40 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -68,6 +68,7 @@ URL_ADMIN_CLIENT_ROLES = URL_ADMIN_CLIENT + "/roles" URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" 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_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" URL_ADMIN_CLIENT_SCOPES = "admin/realms/{realm-name}/client-scopes" From 7e72aec7071142683dc6b6ba9a71b3de1c1a192a Mon Sep 17 00:00:00 2001 From: Manish Verma Date: Thu, 27 Aug 2020 14:19:29 +0530 Subject: [PATCH 39/62] Added code for fetching role users from role in realm and client --- keycloak/keycloak_admin.py | 25 ++++++++++++++++++++++++- keycloak/urls_patterns.py | 2 ++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6..6857d98 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -43,7 +43,8 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ 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_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ + URL_ADMIN_REALM_ROLES_MEMBERS class KeycloakAdmin: @@ -861,6 +862,16 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_realm_role_members(self, role_name, **query): + """ + Get role members of realm by role name. + :param role_name: Name of the role. + :param query: Additional Query parameters (see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_roles_resource) + :return: Keycloak Server Response (UserRepresentation) + """ + params_path = {"realm-name": self.realm_name, "role-name":role_name} + return self.__fetch_all(URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query) + def get_client_roles(self, client_id): """ Get all roles for the client @@ -960,6 +971,18 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_client_role_members(self, client_id, role_name, **query): + """ + Get members by client role . + :param client_id: The client id + :param role_name: the name of role to be queried. + :param query: Additional query parameters ( see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clients_resource) + :return: Keycloak server response (UserRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id":client_id, "role-name":role_name} + return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path) , query) + + def create_realm_role(self, payload, skip_exists=False): """ Create a new role for the realm or client diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8..bd8653d 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -66,6 +66,7 @@ URL_ADMIN_CLIENT = URL_ADMIN_CLIENTS + "/{id}" URL_ADMIN_CLIENT_SECRETS= URL_ADMIN_CLIENT + "/client-secret" URL_ADMIN_CLIENT_ROLES = URL_ADMIN_CLIENT + "/roles" URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" +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_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" @@ -75,6 +76,7 @@ URL_ADMIN_CLIENT_SCOPE = URL_ADMIN_CLIENT_SCOPES + "/{scope-id}" URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER = URL_ADMIN_CLIENT_SCOPE + "/protocol-mappers/models" URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" +URL_ADMIN_REALM_ROLES_MEMBERS = URL_ADMIN_REALM_ROLES + "/{role-name}/users" URL_ADMIN_REALMS = "admin/realms" URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" From 59c3851264ab78df8826c05e55e54a7e9e3ddb14 Mon Sep 17 00:00:00 2001 From: domste Date: Thu, 27 Aug 2020 14:25:00 +0200 Subject: [PATCH 40/62] add deprecation exception on entitlement call --- keycloak/keycloak_openid.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 9ab42f0..b4d60fd 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -28,7 +28,8 @@ from jose import jwt from .authorization import Authorization from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError, \ - KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError + KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError, + KeycloakDeprecationError from .urls_patterns import ( URL_REALM, URL_AUTH, @@ -291,6 +292,9 @@ class KeycloakOpenID: self.connection.add_param_headers("Authorization", "Bearer " + token) params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id} data_raw = self.connection.raw_get(URL_ENTITLEMENT.format(**params_path)) + + if data_raw.status_code == 404: + return raise_error_from_response(data_raw, KeycloakDeprecationError) return raise_error_from_response(data_raw, KeycloakGetError) From 4da5bed75e27703d160443b7181e22a29c2d72bb Mon Sep 17 00:00:00 2001 From: domste Date: Thu, 27 Aug 2020 14:27:16 +0200 Subject: [PATCH 41/62] added KeycloakDeprecationException add exception to indicate a deprecation case --- keycloak/exceptions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index 381d9af..67da62a 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -53,6 +53,9 @@ class KeycloakOperationError(KeycloakError): pass +class KeycloakDeprecationError(KeycloakError): + pass + class KeycloakGetError(KeycloakOperationError): pass From 2aedfec220c47f62cbabc771a43d38ad5e34df3f Mon Sep 17 00:00:00 2001 From: Abhijeet Sharma Date: Fri, 4 Sep 2020 16:49:31 +0530 Subject: [PATCH 42/62] Add Missing Methods --- keycloak/keycloak_admin.py | 31 ++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6..71964db 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -40,7 +40,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \ URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ - URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ + URL_ADMIN_IDP_MAPPERS, URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ 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 @@ -306,6 +306,35 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} return self.__fetch_all(URL_ADMIN_USERS.format(**params_path), query) + def create_idp(self, payload): + """ + Create an ID Provider, + + IdentityProviderRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityproviderrepresentation + + :param: payload: IdentityProviderRepresentation + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + def add_mapper_to_idp(self, idp_alias, payload): + """ + Create an ID Provider, + + IdentityProviderRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityprovidermapperrepresentation + + :param: idp_alias: alias for Idp to add mapper in + :param: payload: IdentityProviderMapperRepresentation + """ + params_path = {"realm-name": self.realm_name, "idp-alias": idp_alias} + data_raw = self.raw_post(URL_ADMIN_IDP_MAPPERS.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def get_idps(self): """ Returns a list of ID Providers, diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8..2c6fcfc 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -78,6 +78,7 @@ URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" URL_ADMIN_REALMS = "admin/realms" URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" +URL_ADMIN_IDP_MAPPERS = "admin/realms/{realm-name}/identity-provider/instances/{idp-alias}/mappers" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" From 454a8e81a42f715b8ec6f304e2e4dfb51b032994 Mon Sep 17 00:00:00 2001 From: Attila Csoma Date: Fri, 4 Sep 2020 22:06:54 +0200 Subject: [PATCH 43/62] Add installation provider download --- keycloak/keycloak_admin.py | 20 +++++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6..52c8465 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -41,7 +41,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ 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 @@ -847,6 +847,24 @@ class KeycloakAdmin: data_raw = self.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_client_installation_provider(self, client_id, provider_id): + """ + Get content for given installation provider + + Related documentation: + https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_clients_resource + + Possible provider_id list available in the ServerInfoRepresentation#clientInstallations + https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_serverinforepresentation + + :param client_id: Client id + :param provider_id: provider id to specify response format + """ + + params_path = {"realm-name": self.realm_name, "id": client_id, "provider-id": provider_id} + data_raw = self.raw_get(URL_ADMIN_CLIENT_INSTALLATION_PROVIDER.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + def get_realm_roles(self): """ Get all roles for the realm or client diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8..f6d7ef5 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -69,6 +69,7 @@ URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" 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_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" +URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" URL_ADMIN_CLIENT_SCOPES = "admin/realms/{realm-name}/client-scopes" URL_ADMIN_CLIENT_SCOPE = URL_ADMIN_CLIENT_SCOPES + "/{scope-id}" From f59dc97466fb58fb1aede31359bc51e5f9199b47 Mon Sep 17 00:00:00 2001 From: Abhijeet Sharma Date: Tue, 8 Sep 2020 14:33:25 +0530 Subject: [PATCH 44/62] Added delete_idp method --- keycloak/keycloak_admin.py | 12 +++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 71964db..a03ef92 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -39,7 +39,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC 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_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ - URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ + URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, URL_ADMIN_IDP, \ URL_ADMIN_IDP_MAPPERS, URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ @@ -348,6 +348,16 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_IDPS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def delete_idp(self, idp_alias): + """ + Deletes ID Provider, + + :param: idp_alias: idp alias name + """ + params_path = {"realm-name": self.realm_name, "alias": idp_alias} + data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def create_user(self, payload): """ Create a new user. Username must be unique diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 2c6fcfc..11187c1 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -79,6 +79,7 @@ URL_ADMIN_REALMS = "admin/realms" URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_IDP_MAPPERS = "admin/realms/{realm-name}/identity-provider/instances/{idp-alias}/mappers" +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}" From 0c0bec4e6668fcc44e6c25c654c4d7402027c803 Mon Sep 17 00:00:00 2001 From: andres Date: Wed, 23 Sep 2020 12:57:16 +0200 Subject: [PATCH 45/62] Added get_realm_roles_of_user --- keycloak/keycloak_admin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6..36c3cb8 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1015,6 +1015,11 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_realm_roles_of_user(self, user_id): + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def assign_group_realm_roles(self, group_id, roles): """ Assign realm roles to a group From c17fdaa3d8522a39a5c812d8e40e60d09ca367f5 Mon Sep 17 00:00:00 2001 From: andres Date: Wed, 23 Sep 2020 12:57:54 +0200 Subject: [PATCH 46/62] Added get_realm_roles_of_user --- keycloak/keycloak_admin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 36c3cb8..40fa977 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1016,6 +1016,13 @@ class KeycloakAdmin: 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. + + :param user_id: id of user + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) From 28a2f4eb11f4a943d382b847ddde4d7864755315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Weing=C3=A4rtner?= Date: Wed, 23 Sep 2020 22:14:26 -0300 Subject: [PATCH 47/62] Create add mapper to SP Client and delete mapper from scope methods --- keycloak/keycloak_admin.py | 38 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 2 ++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6..bfa6e55 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -43,7 +43,8 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ 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_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, \ + URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS class KeycloakAdmin: @@ -1269,6 +1270,41 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): + """ + Delete a mapper from a client scope + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_delete_mapper + + :param client_scope_id: The id of the client scope + :param payload: ProtocolMapperRepresentation + :return: Keycloak server Response + """ + + params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id, + "protocol-mapper-id": protocol_mppaer_id} + + data_raw = self.raw_delete( + URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) + + 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 + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_create_mapper + + :param client_id: The id of the client + :param payload: ProtocolMapperRepresentation + :return: Keycloak server Response + """ + + 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)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def generate_client_secrets(self, client_id): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8..bc7fd72 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -69,10 +69,12 @@ URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" 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_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" +URL_ADMIN_CLIENT_PROTOCOL_MAPPER = URL_ADMIN_CLIENT + "/protocol-mappers/models" URL_ADMIN_CLIENT_SCOPES = "admin/realms/{realm-name}/client-scopes" URL_ADMIN_CLIENT_SCOPE = URL_ADMIN_CLIENT_SCOPES + "/{scope-id}" URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER = URL_ADMIN_CLIENT_SCOPE + "/protocol-mappers/models" +URL_ADMIN_CLIENT_SCOPES_MAPPERS = URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER + "/{protocol-mapper-id}" URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" URL_ADMIN_REALMS = "admin/realms" From 6757373d29b8d0776e3ab9207c82421e0d4e8acf Mon Sep 17 00:00:00 2001 From: Bob van Luijt Date: Wed, 30 Sep 2020 15:59:22 +0200 Subject: [PATCH 48/62] Added user_realm_name to the docs --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 98af23d..4204c56 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,8 @@ from keycloak import KeycloakAdmin keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", username='example-admin', password='secret', - realm_name="example_realm", + realm_name="master", + user_realm_name="only_if_other_realm_than_master", client_secret_key="client-secret", verify=True) From 80729ae3ccf0cf98ceef423f690ee0c60deaf624 Mon Sep 17 00:00:00 2001 From: Bergiu Date: Thu, 1 Oct 2020 13:53:14 +0200 Subject: [PATCH 49/62] fixed #104 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98af23d..aaae5b4 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ count_users = keycloak_admin.users_count() users = keycloak_admin.get_users({}) # Get user ID from name -user-id-keycloak = keycloak_admin.get_user_id("example@example.com") +user_id_keycloak = keycloak_admin.get_user_id("example@example.com") # Get User user = keycloak_admin.get_user("user-id-keycloak") @@ -175,7 +175,7 @@ server_info = keycloak_admin.get_server_info() clients = keycloak_admin.get_clients() # Get client - id (not client-id) from client by name -client_id=keycloak_admin.get_client_id("my-client") +client_id = keycloak_admin.get_client_id("my-client") # Get representation of the client - id of client (not client-id) client = keycloak_admin.get_client(client_id="client_id") From 171d12a675590914d708c98ff66dafb067fe79bb Mon Sep 17 00:00:00 2001 From: "A. Shpak" Date: Tue, 20 Oct 2020 20:07:57 +0300 Subject: [PATCH 50/62] fix wrong param in example from readme --- README.md | 2 +- docs/source/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98af23d..ea354b4 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ token_info = keycloak_openid.introspect(token['access_token'])) # Decode Token KEYCLOAK_PUBLIC_KEY = keycloak_openid.public_key() -options = {"verify_signature": True, "verify_aud": True, "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) # Get permissions by token diff --git a/docs/source/index.rst b/docs/source/index.rst index 2b41d35..0cd6e2f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -132,7 +132,7 @@ Main methods:: # Decode Token KEYCLOAK_PUBLIC_KEY = "secret" - options = {"verify_signature": True, "verify_aud": True, "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) # Get permissions by token From 89225eff12b5e92b29b12f782accce6f8255c7d0 Mon Sep 17 00:00:00 2001 From: rpisani5c <55719539+rpisani5c@users.noreply.github.com> Date: Tue, 27 Oct 2020 12:33:44 -0500 Subject: [PATCH 51/62] Update keycloak_admin.py Swapped the names for get_group_client_roles & delete_group_client_roles respectively. Updated usage, removed the roles object build for the get_ method. --- keycloak/keycloak_admin.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6..53ddab4 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1072,9 +1072,9 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def delete_group_client_roles(self, group_id, client_id, roles): + def get_group_client_roles(self, group_id, client_id): """ - Delete client roles of a group + Get client roles of a group :param group_id: id of group :param client_id: id of client (not client-id) @@ -1082,14 +1082,13 @@ class KeycloakAdmin: :return Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_get(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_group_client_roles(self, group_id, client_id, roles): + def delete_group_client_roles(self, group_id, client_id, roles): """ - Get client roles of a group + Delete client roles of a group :param group_id: id of group :param client_id: id of client (not client-id) From 70b9efeaa30017208bd1b641637ff051142f6b21 Mon Sep 17 00:00:00 2001 From: rpisani5c <55719539+rpisani5c@users.noreply.github.com> Date: Tue, 27 Oct 2020 12:36:42 -0500 Subject: [PATCH 52/62] Update keycloak_admin.py Removed the roles param definition on the get_group_client_roles method. --- keycloak/keycloak_admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 53ddab4..6356cfe 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1078,7 +1078,6 @@ class KeycloakAdmin: :param group_id: id of group :param client_id: id of client (not client-id) - :param roles: roles list or role (use GroupRoleRepresentation) :return Keycloak server response """ From 42ee704d58287008b4e31c384510f13e02abb0ff Mon Sep 17 00:00:00 2001 From: PGijsbers Date: Wed, 28 Oct 2020 12:29:21 +0200 Subject: [PATCH 53/62] Remove excess parenthesis --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 98af23d..c808845 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['ac token_type_hint="requesting_party_token")) # Introspect Token -token_info = keycloak_openid.introspect(token['access_token'])) +token_info = keycloak_openid.introspect(token['access_token']) # Decode Token KEYCLOAK_PUBLIC_KEY = keycloak_openid.public_key() From 876640616b91d3983d203f8e4771dfd4f43a72bd Mon Sep 17 00:00:00 2001 From: faffeldt Date: Fri, 6 Nov 2020 10:13:52 +0100 Subject: [PATCH 54/62] Adds create_authentication_flow_subflow admin method --- keycloak/keycloak_admin.py | 20 +++++++++++++++++++- keycloak/urls_patterns.py | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6..f546a1e 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -43,7 +43,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ 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_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_FLOWS_SUBFLOWS class KeycloakAdmin: @@ -1211,6 +1211,24 @@ class KeycloakAdmin: data=payload) 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 + + AuthenticationFlowRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + + :param payload: AuthenticationFlowRepresentation + :param flow_alias: The flow alias + :param skip_exists: If true then do not raise an error if authentication flow already exists + :return: Keycloak server response (RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} + data_raw = self.raw_post(URL_ADMIN_FLOWS_SUBFLOWS.format(**params_path), + data=payload) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def sync_users(self, storage_id, action): """ Function to trigger user sync from provider diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8..447d28e 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -80,9 +80,9 @@ URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" - URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" +URL_ADMIN_FLOWS_SUBFLOWS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" From 39aee83585b977c4c16512c9c8feaf16f7d85722 Mon Sep 17 00:00:00 2001 From: faffeldt Date: Fri, 6 Nov 2020 10:42:37 +0100 Subject: [PATCH 55/62] Adds create_authentication_flow_execution admin method --- keycloak/keycloak_admin.py | 22 ++++++++++++++++++++-- keycloak/urls_patterns.py | 3 ++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f546a1e..b0c9917 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -43,7 +43,8 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ 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_FLOWS_SUBFLOWS + URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, \ + URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW class KeycloakAdmin: @@ -1210,6 +1211,23 @@ class KeycloakAdmin: data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def create_authentication_flow_execution(self, payload, flow_alias): + """ + Create an authentication flow execution + + AuthenticationExecutionInfoRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + + :param payload: AuthenticationExecutionInfoRepresentation + :param flow_alias: The flow alias + :return: Keycloak server response + """ + + 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=payload) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): """ @@ -1225,7 +1243,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_post(URL_ADMIN_FLOWS_SUBFLOWS.format(**params_path), + data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), data=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 447d28e..ea10e45 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -82,7 +82,8 @@ URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" -URL_ADMIN_FLOWS_SUBFLOWS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" +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_COMPONENTS = "admin/realms/{realm-name}/components" URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" From 8dfdddc5c64c5b8c4a42d4002ca3468727d38a8f Mon Sep 17 00:00:00 2001 From: faffeldt Date: Fri, 6 Nov 2020 11:47:03 +0100 Subject: [PATCH 56/62] Adds copy_authentication_flow admin method --- 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 b0c9917..726bfa8 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -44,7 +44,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ 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_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW + URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY class KeycloakAdmin: @@ -1184,6 +1184,21 @@ class KeycloakAdmin: data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def copy_authentication_flow(self, payload, flow_alias): + """ + Copy existing authentication flow under a new name. The new name is given as 'newName' attribute of the passed payload. + + :param payload: JSON containing 'newName' attribute + :param flow_alias: the flow alias + :return: Keycloak server response (RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} + data_raw = self.raw_post(URL_ADMIN_FLOWS_COPY.format(**params_path), + data=payload) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + 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 ea10e45..a7bc39c 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -81,6 +81,7 @@ URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" +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_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" From 6c62286adefdf9b6293b3ee23fbfcf4740db09df Mon Sep 17 00:00:00 2001 From: faffeldt Date: Fri, 6 Nov 2020 11:55:31 +0100 Subject: [PATCH 57/62] Adds get_authentication_flow_for_id admin method --- keycloak/keycloak_admin.py | 18 ++++++++++++++++-- keycloak/urls_patterns.py | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 726bfa8..a8c17f2 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -44,7 +44,8 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURC URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ 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_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 class KeycloakAdmin: @@ -1166,6 +1167,20 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(URL_ADMIN_FLOWS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + + def get_authentication_flow_for_id(self, flow_id): + """ + Get one authentication flow by it's id/alias. Returns all flow details + + AuthenticationFlowRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + + :param flow_id: the id of a flow NOT it's alias + :return: Keycloak server response (AuthenticationFlowRepresentation) + """ + params_path = {"realm-name": self.realm_name, "flow-id": flow_id} + data_raw = self.raw_get(URL_ADMIN_FLOWS_ALIAS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow(self, payload, skip_exists=False): """ @@ -1198,7 +1213,6 @@ class KeycloakAdmin: data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) - 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 a7bc39c..2ea23fd 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -81,6 +81,7 @@ URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" 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_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" From 4624c7b2dce0869dc4d6b2396086f82eca420a5b Mon Sep 17 00:00:00 2001 From: Tamara Nocentini Date: Wed, 11 Nov 2020 14:32:18 +0100 Subject: [PATCH 58/62] Manage composite realm roles of the realm role Signed-off-by: Tamara Nocentini Signed-off-by: Paolo Romolini --- keycloak/keycloak_admin.py | 52 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6..81201cd 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -29,7 +29,10 @@ from builtins import isinstance from typing import List, Iterable from keycloak.urls_patterns import URL_ADMIN_GROUPS_REALM_ROLES, \ - URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES + URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ + URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, \ + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE + from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID @@ -999,6 +1002,53 @@ class KeycloakAdmin: URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def add_composite_realm_roles_to_role(self, role_name, roles): + """ + Add composite roles to the role + + :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, "role-name": role_name} + data_raw = self.raw_post( + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, + expected_codes=[204]) + + def remove_composite_realm_roles_to_role(self, role_name, roles): + """ + Remove composite roles from the role + + :param role_name: The name of the role + :param roles: roles list or role (use RoleRepresentation) to be removed + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_delete( + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, + expected_codes=[204]) + + def get_composite_realm_roles_of_role(self, role_name): + """ + Get composite roles of the role + + :param role_name: The name of the role + :return Keycloak server response (array RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_get( + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def assign_realm_roles(self, user_id, client_id, roles): """ Assign realm roles to a user diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8..78cf34c 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -79,6 +79,7 @@ URL_ADMIN_REALMS = "admin/realms" URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" 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_FLOWS = "admin/realms/{realm-name}/authentication/flows" From 05b3a276543341b647f16b82fcc83d9c0bbae217 Mon Sep 17 00:00:00 2001 From: Paolo Romolini Date: Fri, 13 Nov 2020 16:11:28 +0100 Subject: [PATCH 59/62] Add "Get a role by name" API Signed-off-by: Paolo Romolini --- keycloak/keycloak_admin.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 81201cd..f98ca84 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -977,6 +977,19 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def get_realm_role(self, role_name): + """ + Get realm role by role name + :param role_name: role's name, not id! + + RoleRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + :return: role_id + """ + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_get(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def update_realm_role(self, role_name, payload): """ Update a role for the realm by name From d046ce1d68efa6a1e884108b6385dd8a0f94ca01 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 19 Nov 2020 16:57:16 -0300 Subject: [PATCH 60/62] Fixed IndentationError --- keycloak/keycloak_openid.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index b4d60fd..0f801ea 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -28,8 +28,7 @@ from jose import jwt from .authorization import Authorization from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError, \ - KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError, - KeycloakDeprecationError + KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError, KeycloakDeprecationError from .urls_patterns import ( URL_REALM, URL_AUTH, From 9bfcd1f88b077497930115841b96355a56363a70 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 19 Nov 2020 17:18:11 -0300 Subject: [PATCH 61/62] Fixed imports. --- keycloak/keycloak_admin.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index dd433fc..f88d00d 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -26,25 +26,22 @@ import json from builtins import isinstance -from typing import List, Iterable - -from keycloak.urls_patterns import URL_ADMIN_GROUPS_REALM_ROLES, \ - URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ - URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, \ - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE +from typing import Iterable 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_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, \ + 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_GET_GROUPS_REALM_ROLES, 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_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_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, URL_ADMIN_IDP, \ URL_ADMIN_IDP_MAPPERS, URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ 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, \ From 2f13f6cdf1e7ca47cc43ca0c43c39ce01dc3ddf9 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 19 Nov 2020 17:30:36 -0300 Subject: [PATCH 62/62] Release 0.23.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 96f7777..637a63b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ author = 'Marcos Pereira' # built documents. # # The short X.Y version. -version = '0.22.0' +version = '0.23.0' # The full version, including alpha/beta/rc tags. -release = '0.22.0' +release = '0.23.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 9467d53..78b8d96 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open("README.md", "r") as fh: setup( name='python-keycloak', - version='0.22.0', + version='0.23.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira',