From 50c7c3fd336fb2e3a3e19ad8d584138f43b84e7c Mon Sep 17 00:00:00 2001 From: Martin Devlin Date: Wed, 6 Dec 2017 16:07:55 +0000 Subject: [PATCH 1/5] Add groups functions --- README.md | 5 +- keycloak/exceptions.py | 5 +- keycloak/keycloak_admin.py | 279 ++++++++++++++++++++++++++++++++----- keycloak/urls_patterns.py | 7 + 4 files changed, 258 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index a80448a..6f2b8d7 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Python Keycloak ==================== +For review- see https://bitbucket.org/agriness/python-keycloak + **python-keycloak** is a Python package providing access to the Keycloak API. ## Installation @@ -51,8 +53,7 @@ from keycloak import KeycloakOpenID keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", client_id="example_client", realm_name="example_realm", - client_secret_key="secret", - verify=True) + client_secret_key="secret") # Get WellKnow config_well_know = keycloak_openid.well_know() diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index 95be231..300c5f7 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -68,7 +68,7 @@ class KeycloakInvalidTokenError(KeycloakOperationError): pass -def raise_error_from_response(response, error, expected_code=200): +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: @@ -79,6 +79,9 @@ def raise_error_from_response(response, error, expected_code=200): except JSONDecodeError as e: return response.content + if skip_exists and response.status_code == 409: + return {"Already exists"} + try: message = response.json()['message'] except (KeyError, ValueError): diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c24dbc3..542fd7c 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -15,9 +15,12 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +# Unless otherwise stated in the comments, "id", in e.g. user_id, refers to the internal Keycloak server ID, usually a uuid string + from .urls_patterns import URL_ADMIN_USERS_COUNT, URL_ADMIN_USER, URL_ADMIN_USER_CONSENTS, \ URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_GET_SESSIONS, \ - URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENTS, URL_ADMIN_CLIENT, URL_ADMIN_CLIENT_ROLES, URL_ADMIN_REALM_ROLES, URL_ADMIN_USER_CLIENT_ROLES + URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENTS, URL_ADMIN_CLIENT, URL_ADMIN_CLIENT_ROLES, URL_ADMIN_REALM_ROLES, URL_ADMIN_USER_CLIENT_ROLES, \ + URL_ADMIN_GROUP, URL_ADMIN_GROUPS, URL_ADMIN_GROUP_CHILD, URL_ADMIN_USER_GROUP, URL_ADMIN_USER_PASSWORD, URL_ADMIN_GROUP_PERMISSIONS from .keycloak_openid import KeycloakOpenID from .exceptions import raise_error_from_response, KeycloakGetError @@ -106,14 +109,14 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_USERS.format(**params_path), **query) return raise_error_from_response(data_raw, KeycloakGetError) - def create_user(self, username, email='', firstName='', lastName='', emailVerified=False, enabled=True): + def create_user(self, username, email='', firstName='', lastName='', emailVerified=False, enabled=True, password=None, passwordTemp=False, skip_exists=False): """ Create a new user Username must be unique UserRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation - :param payload: UserRepresentation + :param data: Http response """ data={} @@ -124,9 +127,29 @@ class KeycloakAdmin: data["emailVerified"]=emailVerified data["enabled"]=enabled params_path = {"realm-name": self.realm_name} + + exists = self.get_user_id(username=username) + + if exists is not None: + return str(exists) + data_raw = self.connection.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(data)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + create_resp = raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + + if password is not None: + user_id = self.get_user_id(username) + data={} + data["value"]=password + data["type"]="password" + data["temporary"]=passwordTemp + + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.connection.raw_put(URL_ADMIN_USER_PASSWORD.format(**params_path), + data=json.dumps(data)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + else: + return create_resp def users_count(self): """ @@ -148,16 +171,16 @@ class KeycloakAdmin: clientId in UserRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation - :return: user_id (uuid as string) + :return: user_id """ params_path = {"realm-name": self.realm_name, "username": username} data_raw = self.connection.raw_get(URL_ADMIN_USERS.format(**params_path)) data_content = raise_error_from_response(data_raw, KeycloakGetError) for user in data_content: - thisusername = json.dumps(user["username"]).strip('"') - if thisusername == username: - return json.dumps(user["id"]).strip('"') + thisusername = json.dumps(user["username"]).strip('"') + if thisusername == username: + return json.dumps(user["id"]).strip('"') return None @@ -175,12 +198,12 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def update_user(self, user_id, username, email='', firstName='', lastName='', emailVerified=False, enabled=True): + def update_user(self, user_id, username, email='', firstName='', lastName='', emailVerified=False, enabled=True, password=None, passwordTemp=False): """ Update the user :param user_id: User id - :param payload: UserRepresentation + :param data: UserRepresentation :return: Http response """ @@ -191,11 +214,24 @@ class KeycloakAdmin: data["lastName"]=lastName data["emailVerified"]=emailVerified data["enabled"]=enabled - params_path = {"realm-name": self.realm_name} params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.connection.raw_put(URL_ADMIN_USER.format(**params_path), data=json.dumps(data)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + update_resp = raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + if password is not None: + user_id = self.get_user_id(username) + data={} + data["value"]=password + data["type"]="password" + data["temporary"]=passwordTemp + + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.connection.raw_put(URL_ADMIN_USER_PASSWORD.format(**params_path), + data=json.dumps(data)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + else: + return update_resp def delete_user(self, user_id): """ @@ -261,7 +297,7 @@ class KeycloakAdmin: """ Get sessions associated with the user - :param user_id: User id + :param user_id: id of user UserSessionRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation @@ -284,6 +320,176 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_SERVER_INFO) return raise_error_from_response(data_raw, KeycloakGetError) + def get_groups(self): + """ + Get 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 + + :return: array GroupRepresentation + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_ADMIN_GROUPS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_group(self, group_id): + """ + Get group by id. Returns full group details + + GroupRepresentation + http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + + :return: array GroupRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.connection.raw_get(URL_ADMIN_GROUP.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_group_id(self, name=None, path=None, parent=None): + """ + Get group id based on name or path. + A straight name or path match with a top-level group will return first. + Subgroups are traversed, the first to match path (or name with path) is returned. + + :param name: group name + :param path: group path + :param parent: parent group's id. Required to find a sub-group below level 1. + + GroupRepresentation + http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + + :return: GroupID (string) + """ + if parent is not None: + params_path = {"realm-name": self.realm_name, "id": parent} + data_raw = self.connection.raw_get(URL_ADMIN_GROUP.format(**params_path)) + res = raise_error_from_response(data_raw, KeycloakGetError) + data_content = [] + data_content.append(res) + else: + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_ADMIN_GROUPS.format(**params_path)) + data_content = raise_error_from_response(data_raw, KeycloakGetError) + + for group in data_content: + thisgroupname = json.dumps(group["name"]).strip('"') + thisgrouppath = json.dumps(group["path"]).strip('"') + if (thisgroupname == name and name is not None) or (thisgrouppath == path and path is not None): + return json.dumps(group["id"]).strip('"') + for subgroup in group["subGroups"]: + thisgrouppath = json.dumps(subgroup["path"]).strip('"') + if (thisgrouppath == path and path is not None) or (thisgrouppath == name and name is not None): + return json.dumps(subgroup["id"]).strip('"') + return None + + def create_group(self, name=None, client_roles={}, realm_roles=[], sub_groups=[], path=None, parent=None, skip_exists=False): + """ + Creates a group in the Realm + + :param name: group name + :param client_roles (map): Client roles to include in groupp # Not demonstrated to work + :param realm_roles (array): Realm roles to include in group # Not demonstrated to work + :param sub_groups (array): Subgroups to include in groupp # Not demonstrated to work + :param path: group path + :param parent: parent group's id. Required to create a sub-group. + + GroupRepresentation + http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + + :return: Http response + """ + if name is None and path is not None: + name=path + + data={} + data["name"]=name + data["path"]=path + data["clientRoles"]=client_roles + data["realmRoles"]=realm_roles + data["subGroups"]=sub_groups + + if name is not None: + exists = self.get_group_id(name=name, parent=parent) + elif path is not None: + exists = self.get_group_id(path=path, parent=parent) + + if exists is not None: + return str(exists) + + if parent is None: + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_post(URL_ADMIN_GROUPS.format(**params_path), + data=json.dumps(data)) + else: + params_path = {"realm-name": self.realm_name, "id": parent,} + data_raw = self.connection.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path), + data=json.dumps(data)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + + def group_set_permissions(self, group_id, enabled=True): + """ + Enable/Disable permissions for a group. Cannot delete group if disabled + + :param group_id: id of group + :param enabled: boolean + + :return: {} + """ + data={} + data["enabled"]=enabled + + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.connection.raw_put(URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), + data=json.dumps(data)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def group_user_add(self, user_id, group_id): + """ + 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: {} + """ + data={} + data["realm"]=self.realm_name + data["userId"]=user_id + data["groupId"]=group_id + + params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} + data_raw = self.connection.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), + data=json.dumps(data)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def group_user_remove(self, user_id, group_id): + """ + 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 + + :return: {} + """ + params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} + data_raw = self.connection.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_group(self, group_id): + """ + Deletes a group in the Realm + + :param group_id: id of group to delete + + :return: Http response + """ + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.connection.raw_delete(URL_ADMIN_GROUP.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def get_clients(self): """ Get clients belonging to the realm Returns a list of clients belonging to the realm @@ -302,8 +508,7 @@ class KeycloakAdmin: Get internal keycloak client id from client-id. This is required for further actions against this client. - :param client_id_name: - clientId in ClientRepresentation + :param client_id_name: name in ClientRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation :return: client_id (uuid as string) @@ -313,9 +518,9 @@ class KeycloakAdmin: data_content = raise_error_from_response(data_raw, KeycloakGetError) for client in data_content: - client_id = json.dumps(client["clientId"]).strip('"') - if client_id == client_id_name: - return json.dumps(client["id"]).strip('"') + client_id = json.dumps(client["clientId"]).strip('"') + if client_id == client_id_name: + return json.dumps(client["id"]).strip('"') return None @@ -326,7 +531,7 @@ class KeycloakAdmin: ClientRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation - :param client_id: id of client (not client-id) + :param client_id: id of client (not client-id) :return: ClientRepresentation """ @@ -334,11 +539,15 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def create_client(self, name, client_id, redirect_urls, protocol="openid-connect", public_client=True, direct_access_grants=True): + def create_client(self, name, client_id, redirect_uris, protocol="openid-connect", public_client=True, direct_access_grants=True, skip_exists=False): """ Create a client - :param name: name of client, payload (ClientRepresentation) + :param name: name of client + :param client_id: (oauth client-id) + :param redirect_uris: Valid edirect URIs + :param redirect urls + :param protocol: openid-connect or saml ClientRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation @@ -347,14 +556,14 @@ class KeycloakAdmin: data={} data["name"]=name data["clientId"]=client_id - data["redirectUris"]=redirect_urls + data["redirectUris"]=redirect_uris data["protocol"]=protocol data["publicClient"]=public_client data["directAccessGrantsEnabled"]=direct_access_grants params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(data)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) def delete_client(self, client_id): """ @@ -363,7 +572,7 @@ class KeycloakAdmin: ClientRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation - :param client_id: id of client (not client-id) + :param client_id: keycloak client id (not oauth client-id) :return: ClientRepresentation """ @@ -375,11 +584,11 @@ class KeycloakAdmin: """ Get all roles for the client + :param client_id: id of client (not client-id) + RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation - :param client_id: id of client (not client-id) - :return: RoleRepresentation """ params_path = {"realm-name": self.realm_name, "id": client_id} @@ -391,11 +600,11 @@ class KeycloakAdmin: Get client role id This is required for further actions with this role. + :param client_id: id of client (not client-id), role_name: name of role + RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation - :param client_id: id of client (not client-id), role_name: name of role - :return: role_id """ params_path = {"realm-name": self.realm_name, "id": client_id} @@ -403,9 +612,9 @@ class KeycloakAdmin: data_content = raise_error_from_response(data_raw, KeycloakGetError) for role in data_content: - this_role_name = json.dumps(role["name"]).strip('"') - if this_role_name == role_name: - return json.dumps(role["id"]).strip('"') + this_role_name = json.dumps(role["name"]).strip('"') + if this_role_name == role_name: + return json.dumps(role["id"]).strip('"') return None @@ -422,11 +631,11 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def create_client_role(self, client_id, role_name): + def create_client_role(self, client_id, role_name, skip_exists=False): """ Create a client role - :param client_id: id of client (not client-id), payload (RoleRepresentation) + :param client_id: id of client (not client-id), role_name: name of role RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation @@ -438,13 +647,13 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.connection.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(data)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) def delete_client_role(self, client_id, role_name): """ Create a client role - :param client_id: id of client (not client-id), payload (RoleRepresentation) + :param client_id: id of client (not client-id), role_name: name of role RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 33c04e9..c9dc8a9 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -34,9 +34,16 @@ URL_ADMIN_SEND_VERIFY_EMAIL = "admin/realms/{realm-name}/users/{id}/send-verify- 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_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" +URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" URL_ADMIN_SERVER_INFO = "admin/serverinfo" +URL_ADMIN_GROUPS = "admin/realms/{realm-name}/groups" +URL_ADMIN_GROUP = "admin/realms/{realm-name}/groups/{id}" +URL_ADMIN_GROUP_CHILD = "admin/realms/{realm-name}/groups/{id}/children" +URL_ADMIN_GROUP_PERMISSIONS = "admin/realms/{realm-name}/groups/{id}/management/permissions" + URL_ADMIN_CLIENTS = "admin/realms/{realm-name}/clients" URL_ADMIN_CLIENT = "admin/realms/{realm-name}/clients/{id}" URL_ADMIN_CLIENT_ROLES = "admin/realms/{realm-name}/clients/{id}/roles" From c71e521c5613b36a27f39e1420f35e72361fc9ce Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 23 Jan 2018 23:16:19 -0200 Subject: [PATCH 2/5] Refactoring all services for client and client-roles. --- keycloak/keycloak_admin.py | 101 +++++++++++++++---------------------- keycloak/urls_patterns.py | 2 +- 2 files changed, 42 insertions(+), 61 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index e581fab..276985a 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -23,7 +23,7 @@ from .urls_patterns import \ URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_GET_SESSIONS, \ URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENTS, URL_ADMIN_CLIENT, URL_ADMIN_CLIENT_ROLES, URL_ADMIN_REALM_ROLES, \ URL_ADMIN_USER_CLIENT_ROLES, URL_ADMIN_GROUP, URL_ADMIN_GROUPS, URL_ADMIN_GROUP_CHILD, URL_ADMIN_USER_GROUP,\ - URL_ADMIN_USER_PASSWORD, URL_ADMIN_GROUP_PERMISSIONS + URL_ADMIN_GROUP_PERMISSIONS from .keycloak_openid import KeycloakOpenID @@ -46,7 +46,8 @@ class KeycloakAdmin: self._realm_name = realm_name # Get token Admin - keycloak_openid = KeycloakOpenID(server_url=server_url, client_id=client_id, realm_name=realm_name, verify=verify) + keycloak_openid = KeycloakOpenID(server_url=server_url, client_id=client_id, realm_name=realm_name, + verify=verify) self._token = keycloak_openid.token(username, password) self._connection = ConnectionManager(base_url=server_url, @@ -217,7 +218,7 @@ class KeycloakAdmin: """ payload = {"type": "password", "temporary": temporary, "value": password} params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.connection.raw_put(URL_ADMIN_USER_PASSWORD.format(**params_path), + data_raw = self.connection.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=200) @@ -481,24 +482,22 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_CLIENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_client_id(self, client_id_name): + def get_client_id(self, client_name): """ Get internal keycloak client id from client-id. This is required for further actions against this client. - :param client_id_name: name in ClientRepresentation + :param client_name: name in ClientRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation :return: client_id (uuid as string) """ - params_path = {"realm-name": self.realm_name, "clientId": client_id_name} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENTS.format(**params_path)) - data_content = raise_error_from_response(data_raw, KeycloakGetError) - for client in data_content: - client_id = json.dumps(client["clientId"]).strip('"') - if client_id == client_id_name: - return json.dumps(client["id"]).strip('"') + clients = self.get_clients() + + for client in clients: + if client_name == client['name']: + return client["id"] return None @@ -517,31 +516,20 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def create_client(self, name, client_id, redirect_uris, protocol="openid-connect", public_client=True, - direct_access_grants=True): + def create_client(self, payload): """ Create a client - :param name: name of client - :param client_id: (oauth client-id) - :param redirect_uris: Valid edirect URIs - :param redirect urls - :param protocol: openid-connect or saml + :param payload: ClientRepresentation - ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + :return: UserRepresentation + + ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation """ - data={} - data["name"]=name - data["clientId"]=client_id - data["redirectUris"]=redirect_uris - data["protocol"]=protocol - data["publicClient"]=public_client - data["directAccessGrantsEnabled"]=direct_access_grants params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_post(URL_ADMIN_CLIENTS.format(**params_path), - data=json.dumps(data)) + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) def delete_client(self, client_id): @@ -576,24 +564,22 @@ class KeycloakAdmin: def get_client_role_id(self, client_id, role_name): """ - Get client role id + Get client role id by name This is required for further actions with this role. - :param client_id: id of client (not client-id), role_name: name of role + :param client_id: id of client (not client-id) + :param role_name: role’s name (not id!) RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation :return: role_id """ - params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ROLES.format(**params_path)) - data_content = raise_error_from_response(data_raw, KeycloakGetError) + roles = self.get_client_roles(client_id) - for role in data_content: - this_role_name = json.dumps(role["name"]).strip('"') - if this_role_name == role_name: - return json.dumps(role["id"]).strip('"') + for role in roles: + if roles['name'] == role_name: + return role["id"] return None @@ -610,54 +596,49 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def create_client_role(self, client_id, role_name, skip_exists=False): + def create_client_role(self, payload): """ Create a client role - :param client_id: id of client (not client-id), role_name: name of role + :param payload: id of client (not client-id), role_name: name of role RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation """ - data={} - data["name"]=role_name - data["clientRole"]=True - params_path = {"realm-name": self.realm_name, "id": client_id} + params_path = {"realm-name": self.realm_name, "id": self.client_id} data_raw = self.connection.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path), - data=json.dumps(data)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) - def delete_client_role(self, client_id, role_name): + def delete_client_role(self, role_name): """ Create a client role - :param client_id: id of client (not client-id), role_name: name of role + :param role_name: role’s name (not id!) RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation """ - data={} - data["name"]=role_name - data["clientRole"]=True - params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_delete(URL_ADMIN_CLIENT_ROLES.format(**params_path) + "/" + role_name, - data=json.dumps(data)) + params_path = {"realm-name": self.realm_name, "id": self.client_id, "role-name": role_name} + data_raw = self.connection.raw_delete(URL_ADMIN_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) - def assign_client_role(self, user_id, client_id, role_id, role_name): + def assign_client_role(self, user_id, client_id, roles): """ Assign a client role to a user - :param client_id: id of client (not client-id), user_id: id of user, client_id: id of client containing role, - role_id: client role id, role_name: client role name) + :param client_id: id of client (not client-id) + :param user_id: id of user + :param client_id: id of client containing role, + :param roles: roles list or role (use RoleRepresentation) + + :return """ - payload=[{}] - payload[0]["id"]=role_id - payload[0]["name"]=role_name + payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.connection.raw_post(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index c9dc8a9..23b0d3b 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -35,7 +35,6 @@ 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_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" -URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" URL_ADMIN_SERVER_INFO = "admin/serverinfo" @@ -47,5 +46,6 @@ URL_ADMIN_GROUP_PERMISSIONS = "admin/realms/{realm-name}/groups/{id}/management/ URL_ADMIN_CLIENTS = "admin/realms/{realm-name}/clients" URL_ADMIN_CLIENT = "admin/realms/{realm-name}/clients/{id}" URL_ADMIN_CLIENT_ROLES = "admin/realms/{realm-name}/clients/{id}/roles" +URL_ADMIN_CLIENT_ROLES = "/admin/realms/{realm}/clients/{id}/roles/{role-name}" URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" From 2c176e6f8a7b7a02f810d2cfae28df0209f873fd Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 25 Jan 2018 19:11:20 -0200 Subject: [PATCH 3/5] Refactoring code: groups and client. --- README.md | 51 ++++++-- keycloak/keycloak_admin.py | 236 +++++++++++++++++------------------- keycloak/keycloak_openid.py | 20 +++ keycloak/urls_patterns.py | 2 +- 4 files changed, 173 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index d4d715b..a6d2811 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,9 @@ token = keycloak_openid.token("user", "password") # Get Userinfo userinfo = keycloak_openid.userinfo(token['access_token']) +# Refresh token +token = keycloak_openid.refresh_token(token['refresh_token']) + # Logout keycloak_openid.logout(token['refresh_token']) @@ -111,7 +114,18 @@ new_user = keycloak_admin.create_user({"email": "example@example.com", "firstName": "Example", "lastName": "Example", "realmRoles": ["user_default", ], - "attributes": {"example": "1,2,3,3,"}}) + "attributes": {"example": "1,2,3,3,"}}) + + +# Add user and set password +new_user = keycloak_admin.create_user({"email": "example@example.com", + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example", + "credentials": [{"value": "secret","type": "password",}], + "realmRoles": ["user_default", ], + "attributes": {"example": "1,2,3,3,"}}) # User counter count_users = keycloak_admin.users_count() @@ -128,6 +142,9 @@ user = keycloak_admin.get_user("user-id-keycloak") # Update User 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) # Delete User response = keycloak_admin.delete_user(user_id="user-id-keycloak") @@ -155,20 +172,36 @@ clients = keycloak_admin.get_clients() 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) +client = keycloak_admin.get_client(client_id="client_id") + +# Get all roles for the realm or client +realm_roles = keycloak_admin.get_realm_roles() # Get all roles for the client -client_roles = keycloak_admin.get_client_role(client_id=client_id) +client_roles = keycloak_admin.get_client_roles(client_id="client_id") -# Create client role -keycloak_admin.create_client_role(client_id, "test") +# Get client role +role = keycloak_admin.get_client_role(client_id="client_id", role_name="role_name") +# Warning: Deprecated # Get client role id from name -role_id = keycloak_admin.get_client_role_id(client_id=client_id, role_name="test") +role_id = keycloak_admin.get_client_role_id(client_id="client_id", role_name="test") -# Get all roles for the realm or client -realm_roles = keycloak_admin.get_roles() +# Create client role +keycloak_admin.create_client_role(client_id, "test") # Assign client role to user. Note that BOTH role_name and role_id appear to be required. -keycloak_admin.assign_client_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test") +keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test") + +# Create new group +group = keycloak_admin.create_group(name="Example Group") + +# Get all groups +groups = keycloak_admin.get_groups() + +# Get group +group = keycloak_admin.get_group(group_id='group_id') + +# Get group by name +group = keycloak_admin.get_group_by_name(name_or_path='group_id', search_in_subgroups=True) ``` diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 276985a..8de92be 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -17,7 +17,7 @@ # Unless otherwise stated in the comments, "id", in e.g. user_id, refers to the # internal Keycloak server ID, usually a uuid string - +from keycloak.urls_patterns import URL_ADMIN_CLIENT_ROLE from .urls_patterns import \ URL_ADMIN_USERS_COUNT, URL_ADMIN_USER, URL_ADMIN_USER_CONSENTS, \ URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_GET_SESSIONS, \ @@ -145,10 +145,11 @@ class KeycloakAdmin: Get internal keycloak user id from username This is required for further actions against this user. - :param username: - clientId in UserRepresentation + UserRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + :param username: id in UserRepresentation + :return: user_id """ params_path = {"realm-name": self.realm_name, "username": username} @@ -156,8 +157,8 @@ class KeycloakAdmin: data_content = raise_error_from_response(data_raw, KeycloakGetError) for user in data_content: - thisusername = json.dumps(user["username"]).strip('"') - if thisusername == username: + this_use_rname = json.dumps(user["username"]).strip('"') + if this_use_rname == username: return json.dumps(user["id"]).strip('"') return None @@ -220,7 +221,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.connection.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=200) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def consents_user(self, user_id): """ @@ -317,94 +318,73 @@ class KeycloakAdmin: GroupRepresentation http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation - :return: array GroupRepresentation + :return: Keycloak server response (GroupRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.connection.raw_get(URL_ADMIN_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_group_id(self, name=None, path=None, parent=None): + def get_group_by_name(self, name_or_path, search_in_subgroups=False): """ Get group id based on name or path. A straight name or path match with a top-level group will return first. Subgroups are traversed, the first to match path (or name with path) is returned. - :param name: group name - :param path: group path - :param parent: parent group's id. Required to find a sub-group below level 1. - GroupRepresentation http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation - :return: GroupID (string) + :param name: group name + :param path: group path + :param search_in_subgroups: True if want search in the subgroups + :return: Keycloak server response (GroupRepresentation) """ - if parent is not None: - params_path = {"realm-name": self.realm_name, "id": parent} - data_raw = self.connection.raw_get(URL_ADMIN_GROUP.format(**params_path)) - res = raise_error_from_response(data_raw, KeycloakGetError) - data_content = [] - data_content.append(res) - else: - params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_ADMIN_GROUPS.format(**params_path)) - data_content = raise_error_from_response(data_raw, KeycloakGetError) - for group in data_content: - thisgroupname = json.dumps(group["name"]).strip('"') - thisgrouppath = json.dumps(group["path"]).strip('"') - if (thisgroupname == name and name is not None) or (thisgrouppath == path and path is not None): - return json.dumps(group["id"]).strip('"') - for subgroup in group["subGroups"]: - thisgrouppath = json.dumps(subgroup["path"]).strip('"') + groups = self.get_groups() - if (thisgrouppath == path and path is not None) or (thisgrouppath == name and name is not None): - return json.dumps(subgroup["id"]).strip('"') + # TODO: Review this code is necessary + for group in groups: + if group['name'] == name_or_path or group['path'] == name_or_path: + return group + elif search_in_subgroups and group["subGroups"]: + for subgroup in group["subGroups"]: + if subgroup['name'] == name_or_path or subgroup['path'] == name_or_path: + return subgroup return None - def create_group(self, name=None, client_roles={}, realm_roles=[], sub_groups=[], path=None, parent=None, skip_exists=False): + def create_group(self, name=None, client_roles={}, realm_roles=[], sub_groups=[], path=None, parent=None): """ - Creates a group in the Realm + Create a group in the Realm + + GroupRepresentation + http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation :param name: group name - :param client_roles (map): Client roles to include in groupp # Not demonstrated to work - :param realm_roles (array): Realm roles to include in group # Not demonstrated to work - :param sub_groups (array): Subgroups to include in groupp # Not demonstrated to work + :param client_roles: (Dict) Client roles to include in groupp # Not demonstrated to work + :param realm_roles: (List) Realm roles to include in group # Not demonstrated to work + :param sub_groups: (List) Subgroups to include in groupp # Not demonstrated to work :param path: group path :param parent: parent group's id. Required to create a sub-group. - GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation - - :return: Http response + :return: Keycloak server response (GroupRepresentation) """ - if name is None and path is not None: - name=path - - data={} - data["name"]=name - data["path"]=path - data["clientRoles"]=client_roles - data["realmRoles"]=realm_roles - data["subGroups"]=sub_groups - if name is not None: - exists = self.get_group_id(name=name, parent=parent) - elif path is not None: - exists = self.get_group_id(path=path, parent=parent) - - if exists is not None: - return str(exists) + data = {"name": name or path, + "path": path, + "clientRoles": client_roles, + "realmRoles": realm_roles, + "subGroups": sub_groups} if parent is None: - params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_post(URL_ADMIN_GROUPS.format(**params_path), - data=json.dumps(data)) + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_post(URL_ADMIN_GROUPS.format(**params_path), + data=json.dumps(data)) else: - params_path = {"realm-name": self.realm_name, "id": parent,} - data_raw = self.connection.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path), - data=json.dumps(data)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + params_path = {"realm-name": self.realm_name, "id": parent} + data_raw = self.connection.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path), + data=json.dumps(data)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) def group_set_permissions(self, group_id, enabled=True): """ @@ -412,15 +392,12 @@ class KeycloakAdmin: :param group_id: id of group :param enabled: boolean - - :return: {} + :return: Keycloak server response """ - data={} - data["enabled"]=enabled params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.connection.raw_put(URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), - data=json.dumps(data)) + data=json.dumps({"enabled": enabled})) return raise_error_from_response(data_raw, KeycloakGetError) def group_user_add(self, user_id, group_id): @@ -430,17 +407,11 @@ class KeycloakAdmin: :param group_id: id of group :param user_id: id of user :param group_id: id of group to add to - - :return: {} + :return: Keycloak server response """ - data={} - data["realm"]=self.realm_name - data["userId"]=user_id - data["groupId"]=group_id params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} - data_raw = self.connection.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), - data=json.dumps(data)) + data_raw = self.connection.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def group_user_remove(self, user_id, group_id): @@ -450,9 +421,9 @@ class KeycloakAdmin: :param group_id: id of group :param user_id: id of user :param group_id: id of group to add to - - :return: {} + :return: Keycloak server response """ + params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.connection.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) @@ -462,9 +433,9 @@ class KeycloakAdmin: Deletes a group in the Realm :param group_id: id of group to delete - - :return: Http response + :return: Keycloak server response """ + params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.connection.raw_delete(URL_ADMIN_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) @@ -476,12 +447,28 @@ class KeycloakAdmin: ClientRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation - :return: ClientRepresentation + :return: Keycloak server response (ClientRepresentation) """ + params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_ADMIN_CLIENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_client(self, client_id): + """ + Get representation of the client + + ClientRepresentation + http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + + :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.connection.raw_get(URL_ADMIN_CLIENT.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_client_id(self, client_name): """ Get internal keycloak client id from client-id. @@ -489,7 +476,6 @@ class KeycloakAdmin: :param client_name: name in ClientRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation - :return: client_id (uuid as string) """ @@ -501,32 +487,16 @@ class KeycloakAdmin: return None - def get_client(self, client_id): - """ - Get representation of the client - - ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation - - :param client_id: id of client (not client-id) - - :return: ClientRepresentation - """ - params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) - def create_client(self, payload): """ Create a client - :param payload: ClientRepresentation - - :return: UserRepresentation - ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + :param payload: ClientRepresentation + :return: Keycloak server response (UserRepresentation) """ + params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) @@ -540,13 +510,27 @@ class KeycloakAdmin: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation :param client_id: keycloak client id (not oauth client-id) - - :return: ClientRepresentation + :return: Keycloak server response (ClientRepresentation) """ + params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.connection.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def get_realm_roles(self): + """ + Get all roles for the realm or client + + RoleRepresentation + http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + + :return: Keycloak server response (RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_client_roles(self, client_id): """ Get all roles for the client @@ -556,13 +540,14 @@ class KeycloakAdmin: RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation - :return: RoleRepresentation + :return: Keycloak server response (RoleRepresentation) """ + params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_client_role_id(self, client_id, role_name): + def get_client_role(self, client_id, role_name): """ Get client role id by name This is required for further actions with this role. @@ -575,37 +560,39 @@ class KeycloakAdmin: :return: role_id """ - roles = self.get_client_roles(client_id) + params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} + data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ROLE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) - for role in roles: - if roles['name'] == role_name: - return role["id"] + def get_client_role_id(self, client_id, role_name): + """ + Warning: Deprecated - return None + Get client role id by name + This is required for further actions with this role. - def get_roles(self): - """ - Get all roles for the realm or client + :param client_id: id of client (not client-id) + :param role_name: role’s name (not id!) RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation - :return: RoleRepresentation + :return: role_id """ - params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) + role = self.get_client_role(client_id, role_name) + return role.get("id") def create_client_role(self, payload): """ Create a client role - :param payload: id of client (not client-id), role_name: name of role - RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + :param payload: id of client (not client-id), role_name: name of role + :return: Keycloak server response (RoleRepresentation) """ + params_path = {"realm-name": self.realm_name, "id": self.client_id} data_raw = self.connection.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) @@ -615,14 +602,13 @@ class KeycloakAdmin: """ Create a client role - :param role_name: role’s name (not id!) - RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + :param role_name: role’s name (not id!) """ params_path = {"realm-name": self.realm_name, "id": self.client_id, "role-name": role_name} - data_raw = self.connection.raw_delete(URL_ADMIN_CLIENT_ROLES.format(**params_path)) + data_raw = self.connection.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def assign_client_role(self, user_id, client_id, roles): @@ -633,9 +619,7 @@ class KeycloakAdmin: :param user_id: id of user :param client_id: id of client containing role, :param roles: roles list or role (use RoleRepresentation) - - :return - + :return Keycloak server response """ payload = roles if isinstance(roles, list) else [roles] diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index f19c0e0..0f7b446 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -165,6 +165,26 @@ class KeycloakOpenID: data=payload) return raise_error_from_response(data_raw, KeycloakGetError) + def refresh_token(self, refresh_token, grant_type=["refresh_token"]): + """ + The token endpoint is used to obtain tokens. Tokens can either be obtained by + exchanging an authorization code or by supplying credentials directly depending on + what flow is used. The token endpoint is also used to obtain new access tokens + when they expire. + + http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint + + :param refresh_token: + :param grant_type: + :return: + """ + params_path = {"realm-name": self.realm_name} + payload = {"client_id": self.client_id, "grant_type": grant_type, "refresh_token": refresh_token} + payload = self._add_secret_key(payload) + data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), + data=payload) + return raise_error_from_response(data_raw, KeycloakGetError) + def userinfo(self, token): """ The userinfo endpoint returns standard claims about the authenticated user, diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 23b0d3b..2be99f2 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -46,6 +46,6 @@ URL_ADMIN_GROUP_PERMISSIONS = "admin/realms/{realm-name}/groups/{id}/management/ URL_ADMIN_CLIENTS = "admin/realms/{realm-name}/clients" URL_ADMIN_CLIENT = "admin/realms/{realm-name}/clients/{id}" URL_ADMIN_CLIENT_ROLES = "admin/realms/{realm-name}/clients/{id}/roles" -URL_ADMIN_CLIENT_ROLES = "/admin/realms/{realm}/clients/{id}/roles/{role-name}" +URL_ADMIN_CLIENT_ROLE = "admin/realms/{realm-name}/clients/{id}/roles/{role-name}" URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" From f2eec08519217b0cedd5304ae82da457247172c4 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 25 Jan 2018 20:14:39 -0200 Subject: [PATCH 4/5] Added groups functions and client functions. Added Changedlog --- CHANGELOG.md | 41 ++++++++++++++++++++++++++ README.md | 3 ++ docs/source/conf.py | 4 +-- docs/source/index.rst | 60 +++++++++++++++++++++++++++++++------- keycloak/keycloak_admin.py | 7 +++++ 5 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8b90ae2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ + +Changelog +============ + +All notable changes to this project will be documented in this file. + +## [0.5.0] - 2017-08-21 + +* Basic functions for Keycloak API (well_know, token, userinfo, logout, certs, +entitlement, instropect) + +## [0.6.0] - 2017-08-23 + +* Added load authorization settings + +## [0.7.0] - 2017-08-23 + +* Added polices + +## [0.8.0] - 2017-08-23 + +* Added permissions + +## [0.9.0] - 2017-09-05 + +* Added functions for Admin Keycloak API + +## [0.10.0] - 2017-10-23 + +* Updated libraries versions +* Updated Docs + +## [0.11.0] - 2017-12-12 + +* Changed Instropect RPT + +## [0.12.0] - 2018-01-25 + +* Add groups functions +* Add Admin Tasks for user and client role management +* Function to trigger user sync from provider \ No newline at end of file diff --git a/README.md b/README.md index a6d2811..ec9b7cd 100644 --- a/README.md +++ b/README.md @@ -204,4 +204,7 @@ group = keycloak_admin.get_group(group_id='group_id') # Get group by name group = keycloak_admin.get_group_by_name(name_or_path='group_id', search_in_subgroups=True) + +# Function to trigger user sync from provider +sync_users(storage_id="storage_di", action="action") ``` diff --git a/docs/source/conf.py b/docs/source/conf.py index c8f9890..ef6b6fe 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.11.1' +version = '0.12.0' # The full version, including alpha/beta/rc tags. -release = '0.11.1' +release = '0.12.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/index.rst b/docs/source/index.rst index 9ce5594..eea1c71 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -73,14 +73,15 @@ Usage Main methods:: + # KEYCLOAK OPENID + from keycloak import KeycloakOpenID # Configure client keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", client_id="example_client", realm_name="example_realm", - client_secret_key="secret", - verify=True) + client_secret_key="secret") # Get WellKnow config_well_know = keycloak_openid.well_know() @@ -91,6 +92,9 @@ Main methods:: # Get Userinfo userinfo = keycloak_openid.userinfo(token['access_token']) + # Refresh token + token = keycloak_openid.refresh_token(token['refresh_token']) + # Logout keycloak_openid.logout(token['refresh_token']) @@ -138,12 +142,26 @@ Main methods:: "realmRoles": ["user_default", ], "attributes": {"example": "1,2,3,3,"}}) + + # Add user and set password + new_user = keycloak_admin.create_user({"email": "example@example.com", + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example", + "credentials": [{"value": "secret","type": "password",}], + "realmRoles": ["user_default", ], + "attributes": {"example": "1,2,3,3,"}}) + # User counter count_users = keycloak_admin.users_count() # Get users Returns a list of users, filtered according to query parameters users = keycloak_admin.get_users({}) + # Get user ID from name + user-id-keycloak = keycloak_admin.get_user_id("example@example.com") + # Get User user = keycloak_admin.get_user("user-id-keycloak") @@ -151,6 +169,9 @@ Main methods:: 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) + # Delete User response = keycloak_admin.delete_user(user_id="user-id-keycloak") @@ -177,19 +198,38 @@ Main methods:: client_id=keycloak_admin.get_client_id("my-client") # Get representation of the client - id of client (not client-id) - client_roles = keycloak_admin.get_client_role(client_id=client_id) + client = keycloak_admin.get_client(client_id="client_id") + + # Get all roles for the realm or client + realm_roles = keycloak_admin.get_realm_roles() # Get all roles for the client - client_roles = keycloak_admin.get_client_role(client_id=client_id) + client_roles = keycloak_admin.get_client_roles(client_id="client_id") - # Create client role - keycloak_admin.create_client_role(client_id, "test") + # Get client role + role = keycloak_admin.get_client_role(client_id="client_id", role_name="role_name") + # Warning: Deprecated # Get client role id from name - role_id = keycloak_admin.get_client_role_id(client_id=client_id, role_name="test") + role_id = keycloak_admin.get_client_role_id(client_id="client_id", role_name="test") - # Get all roles for the realm or client - realm_roles = keycloak_admin.get_roles() + # Create client role + keycloak_admin.create_client_role(client_id, "test") # Assign client role to user. Note that BOTH role_name and role_id appear to be required. - keycloak_admin.assign_client_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test") + keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test") + + # Create new group + group = keycloak_admin.create_group(name="Example Group") + + # Get all groups + groups = keycloak_admin.get_groups() + + # Get group + group = keycloak_admin.get_group(group_id='group_id') + + # Get group by name + group = keycloak_admin.get_group_by_name(name_or_path='group_id', search_in_subgroups=True) + + # Function to trigger user sync from provider + sync_users(storage_id="storage_di", action="action") diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 4deb79c..068f29b 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -629,6 +629,13 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def sync_users(self, storage_id, action): + """ + Function to trigger user sync from provider + + :param storage_id: + :param action: + :return: + """ data = {'action': action} params_query = {"action": action} From 82d90e861280bfe17cc23d822874f1fbd213d8d8 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 25 Jan 2018 20:23:53 -0200 Subject: [PATCH 5/5] Updated docks --- CHANGELOG.md | 3 ++- keycloak/keycloak_admin.py | 11 ++++++++++- keycloak/keycloak_openid.py | 10 +++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b90ae2..a14d8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,4 +38,5 @@ entitlement, instropect) * Add groups functions * Add Admin Tasks for user and client role management -* Function to trigger user sync from provider \ No newline at end of file +* Function to trigger user sync from provider +* Optional parameter: verify \ No newline at end of file diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 068f29b..17e2c12 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -39,7 +39,16 @@ import json class KeycloakAdmin: - def __init__(self, server_url, verify, username, password, realm_name='master', client_id='admin-cli'): + def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True): + """ + + :param server_url: Keycloak server url + :param username: admin username + :param password: admin password + :param realm_name: realm name + :param client_id: client id + :param verify: True if want check connection SSL + """ self._username = username self._password = password self._client_id = client_id diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 0f7b446..f1dcde4 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -34,7 +34,15 @@ import json class KeycloakOpenID: - def __init__(self, server_url, verify, client_id, realm_name, client_secret_key=None): + def __init__(self, server_url, realm_name, client_id, client_secret_key=None, verify=True): + """ + + :param server_url: Keycloak server url + :param client_id: client id + :param realm_name: realm name + :param client_secret_key: client secret key + :param verify: True if want check connection SSL + """ self._client_id = client_id self._client_secret_key = client_secret_key self._realm_name = realm_name