diff --git a/CHANGELOG.md b/CHANGELOG.md index a14d8be..c8891db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,3 @@ - Changelog ============ @@ -6,7 +5,7 @@ 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, +* Basic functions for Keycloak API (well_know, token, userinfo, logout, certs, entitlement, instropect) ## [0.6.0] - 2017-08-23 @@ -39,4 +38,8 @@ entitlement, instropect) * 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 + +## [0.12.1] - 2018-08-04 + +* Add get_idps +* Rework group functions diff --git a/README.md b/README.md index ec9b7cd..188306c 100644 --- a/README.md +++ b/README.md @@ -203,8 +203,21 @@ groups = keycloak_admin.get_groups() 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) +group = keycloak_admin.get_group_by_path(path='/group/subgroup', search_in_subgroups=True) # Function to trigger user sync from provider sync_users(storage_id="storage_di", action="action") + +# Get client role id from name +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() + +# 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") + +# Get all ID Providers +idps = keycloak_admin.get_idps() + ``` diff --git a/docs/source/conf.py b/docs/source/conf.py index ef6b6fe..654da5b 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.12.0' +version = '0.12.1' # The full version, including alpha/beta/rc tags. -release = '0.12.0' +release = '0.12.1' # 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 eea1c71..87a72cc 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -43,6 +43,7 @@ python-keycloak depends on: * Python 3 * `requests `_ * `python-jose `_ +* `simplejson `_ Tests Dependencies ------------------ @@ -81,7 +82,8 @@ Main methods:: keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", client_id="example_client", realm_name="example_realm", - client_secret_key="secret") + client_secret_key="secret", + verify=True) # Get WellKnow config_well_know = keycloak_openid.well_know() @@ -216,6 +218,12 @@ Main methods:: # Create client role keycloak_admin.create_client_role(client_id, "test") + # Get client role id from name + 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() + # 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") @@ -228,8 +236,8 @@ Main methods:: # 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) + # Get group by path + group = keycloak_admin.get_group_by_path(path='/group/subgroup', search_in_subgroups=True) # Function to trigger user sync from provider sync_users(storage_id="storage_di", action="action") diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index 27d8b14..300c5f7 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import requests +from simplejson import JSONDecodeError class KeycloakError(Exception): @@ -67,16 +68,20 @@ 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: return {} + try: return response.json() - except ValueError: + 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 17e2c12..8f2dbc2 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -123,6 +123,19 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_USERS.format(**params_path), **query) return raise_error_from_response(data_raw, KeycloakGetError) + def get_idps(self): + """ + Returns a list of ID Providers, + + IdentityProviderRepresentation + https://www.keycloak.org/docs-api/3.3/rest-api/index.html#_identityproviderrepresentation + + :return: array IdentityProviderRepresentation + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_ADMIN_IDPS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def create_user(self, payload): """ Create a new user Username must be unique @@ -135,6 +148,12 @@ class KeycloakAdmin: :return: UserRepresentation """ params_path = {"realm-name": self.realm_name} + + exists = self.get_user_id(username=payload['username']) + + if exists is not None: + return str(exists) + 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) @@ -333,7 +352,28 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_group_by_name(self, name_or_path, search_in_subgroups=False): + def get_subgroups(self, group, path): + """ + Utility function to iterate through nested group structures + + GroupRepresentation + http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + + :param name: group (GroupRepresentation) + :param path: group path (string) + + :return: Keycloak server response (GroupRepresentation) + """ + + for subgroup in group["subGroups"]: + if subgroup['path'] == path: + return subgroup + elif subgroup["subGroups"]: + for subgroup in group["subGroups"]: + return self.get_subgroups(subgroup, path) + return None + + def get_group_by_path(self, 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. @@ -342,7 +382,6 @@ class KeycloakAdmin: 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) @@ -352,48 +391,48 @@ class KeycloakAdmin: # TODO: Review this code is necessary for group in groups: - if group['name'] == name_or_path or group['path'] == name_or_path: + if group['path'] == 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 - + res = self.get_subgroups(group, path) + if res != None: + return res return None - def create_group(self, name=None, client_roles={}, realm_roles=[], sub_groups=[], path=None, parent=None): + def create_group(self, payload, parent=None, skip_exists=False): """ - Create a group in the Realm + Creates a group in the Realm + + :param payload: GroupRepresentation + :param parent: parent group's id. Required to create a sub-group. 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) + :return: Http response """ + name = payload['name'] + path = payload['path'] + exists = None + + if name is None and path is not None: + path="/" + name + + elif path is not None: + exists = self.get_group_by_path(path=path, search_in_subgroups=True) - data = {"name": name or path, - "path": path, - "clientRoles": client_roles, - "realmRoles": realm_roles, - "subGroups": sub_groups} + 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)) + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_post(URL_ADMIN_GROUPS.format(**params_path), + data=json.dumps(payload)) 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) + 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(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) def group_set_permissions(self, group_id, enabled=True): """ @@ -496,7 +535,7 @@ class KeycloakAdmin: return None - def create_client(self, payload): + def create_client(self, payload, skip_exists=False): """ Create a client @@ -509,7 +548,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_post(URL_ADMIN_CLIENTS.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_code=201, skip_exists=skip_exists) def delete_client(self, client_id): """ @@ -591,7 +630,7 @@ class KeycloakAdmin: role = self.get_client_role(client_id, role_name) return role.get("id") - def create_client_role(self, payload): + def create_client_role(self, payload, skip_exists=False): """ Create a client role @@ -605,7 +644,7 @@ class KeycloakAdmin: 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)) - 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, role_name): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index ce593da..5897e10 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -35,6 +35,7 @@ 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" @@ -51,3 +52,5 @@ URL_ADMIN_CLIENT_ROLE = "admin/realms/{realm-name}/clients/{id}/roles/{role-name URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" URL_ADMIN_USER_STORAGE = "admin/realms/{realm-name}/user-storage/{id}/sync" + +URL_ADMIN_IDPS = "admin/realms/{realm}/identity-provider/instances" diff --git a/requirements.txt b/requirements.txt index d6eafdf..386a0c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests==2.18.4 httmock==1.2.5 python-jose==1.4.0 +simplejson \ No newline at end of file diff --git a/setup.py b/setup.py index e3c8d2d..03ac4f1 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup setup( name='python-keycloak', - version='0.11.1', + version='0.12.1', url='https://bitbucket.org/agriness/python-keycloak', license='GNU General Public License - V3', author='Marcos Pereira', @@ -12,7 +12,7 @@ setup( keywords='keycloak openid', description=u'python-keycloak is a Python package providing access to the Keycloak API.', packages=['keycloak', 'keycloak.authorization', 'keycloak.tests'], - install_requires=['requests==2.18.4', 'httmock==1.2.5', 'python-jose==1.4.0'], + install_requires=['requests==2.18.4', 'httmock==1.2.5', 'python-jose==1.4.0', 'simplejson'], classifiers=[ 'Programming Language :: Python :: 3', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',