diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a14d8be --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,42 @@ + +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 +* Optional parameter: verify \ No newline at end of file diff --git a/README.md b/README.md index 239436c..ec9b7cd 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 @@ -53,8 +55,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() @@ -65,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']) @@ -110,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() @@ -127,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") @@ -154,20 +172,39 @@ 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) + +# 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 1de4fdf..17e2c12 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -15,10 +15,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from .urls_patterns import URL_ADMIN_USERS_COUNT, URL_ADMIN_USER, URL_ADMIN_USER_CONSENTS, \ +# 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, \ 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_USER_STORAGE + URL_ADMIN_GROUP, URL_ADMIN_GROUPS, URL_ADMIN_GROUP_CHILD, URL_ADMIN_USER_GROUP,\ + URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_USER_CLIENT_ROLES, URL_ADMIN_USER_STORAGE + from .keycloak_openid import KeycloakOpenID from .exceptions import raise_error_from_response, KeycloakGetError @@ -33,14 +39,24 @@ 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 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, @@ -138,20 +154,21 @@ 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 - :return: user_id (uuid as string) + :param username: id in UserRepresentation + + :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('"') + this_use_rname = json.dumps(user["username"]).strip('"') + if this_use_rname == username: + return json.dumps(user["id"]).strip('"') return None @@ -195,6 +212,26 @@ class KeycloakAdmin: data_raw = self.connection.raw_delete(URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def set_user_password(self, user_id, password, temporary=True): + """ + 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 + + :param user_id: User id + :param password: New password + :param temporary: True if password is temporary + + :return: + """ + 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_RESET_PASSWORD.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def consents_user(self, user_id): """ Get consents granted by the user @@ -247,7 +284,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 @@ -270,41 +307,162 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_SERVER_INFO) return raise_error_from_response(data_raw, KeycloakGetError) - def get_clients(self): + def get_groups(self): """ - Get clients belonging to the realm Returns a list of clients belonging to the realm + Get groups belonging to the realm. Returns a list of groups belonging to the realm - ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + GroupRepresentation + http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation - :return: ClientRepresentation + :return: array GroupRepresentation """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENTS.format(**params_path)) + data_raw = self.connection.raw_get(URL_ADMIN_GROUPS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_client_id(self, client_id_name): + def get_group(self, group_id): """ - Get internal keycloak client id from client-id. - This is required for further actions against this client. + Get group by id. Returns full group details - :param client_id_name: - clientId in ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + GroupRepresentation + http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation - :return: client_id (uuid as string) + :return: Keycloak server response (GroupRepresentation) """ - 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) + 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) - for client in data_content: - client_id = json.dumps(client["clientId"]).strip('"') - if client_id == client_id_name: - return json.dumps(client["id"]).strip('"') + 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. + + GroupRepresentation + http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + + :param name: group name + :param path: group path + :param search_in_subgroups: True if want search in the subgroups + :return: Keycloak server response (GroupRepresentation) + """ + + groups = self.get_groups() + + # 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): + """ + 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: (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. + + :return: Keycloak server response (GroupRepresentation) + """ + + 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)) + 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) + + 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: Keycloak server response + """ + + 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({"enabled": enabled})) + 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: Keycloak server response + """ + + 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=None) + 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: 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) + + def delete_group(self, group_id): + """ + Deletes a group in the Realm + + :param group_id: id of group to delete + :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) + + def get_clients(self): + """ + Get 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 + + :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 @@ -312,34 +470,45 @@ 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) - - :return: 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 create_client(self, name, client_id, redirect_urls, protocol="openid-connect", public_client=True, direct_access_grants=True): + def get_client_id(self, client_name): """ - Create a client - - :param name: name of client, payload (ClientRepresentation) + Get internal keycloak client id from client-id. + This is required for further actions against this client. - 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) + """ + + clients = self.get_clients() + for client in clients: + if client_name == client['name']: + return client["id"] + + return None + + def create_client(self, payload): """ - data={} - data["name"]=name - data["clientId"]=client_id - data["redirectUris"]=redirect_urls - data["protocol"]=protocol - data["publicClient"]=public_client - data["directAccessGrantsEnabled"]=direct_access_grants + Create a client + + 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(data)) + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) def delete_client(self, client_id): @@ -349,121 +518,137 @@ 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) - - :return: ClientRepresentation + :param client_id: keycloak client id (not oauth client-id) + :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_client_roles(self, client_id): + def get_realm_roles(self): """ - Get all roles for the client + 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 + :param client_id: id of client (not client-id) - :return: RoleRepresentation + 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, "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 + Get client role id by name This is required for further actions with this 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 - :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} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ROLES.format(**params_path)) - data_content = raise_error_from_response(data_raw, KeycloakGetError) + 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 data_content: - this_role_name = json.dumps(role["name"]).strip('"') - if this_role_name == role_name: - return json.dumps(role["id"]).strip('"') + 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, client_id, role_name): + def create_client_role(self, payload): """ Create a client role - :param client_id: id of client (not client-id), payload (RoleRepresentation) - 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) """ - 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)) + 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), payload (RoleRepresentation) - RoleRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + :param role_name: role’s name (not id!) """ - 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_ROLE.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 Keycloak server response """ - 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)) 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_path = {"realm-name": self.realm_name, "id": storage_id} params_query = {"action": action} + + params_path = {"realm-name": self.realm_name, "id": storage_id} data_raw = self.connection.raw_post(URL_ADMIN_USER_STORAGE.format(**params_path), data=json.dumps(data), **params_query) return raise_error_from_response(data_raw, KeycloakGetError) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index f19c0e0..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 @@ -165,6 +173,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 24e9a59..ce593da 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -34,12 +34,19 @@ 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_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" +URL_ADMIN_CLIENT_ROLE = "admin/realms/{realm-name}/clients/{id}/roles/{role-name}" URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles"