diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1aba38f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE diff --git a/README.md b/README.md index 67a51d1..8a53e50 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ For review- see https://github.com/marcospereirampj/python-keycloak python-keycloak depends on: * Python 3 -* [requests](http://docs.python-requests.org/en/master/) +* [requests](https://requests.readthedocs.io) * [python-jose](http://python-jose.readthedocs.io/en/latest/) ### Tests Dependencies @@ -94,11 +94,11 @@ token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['ac token_type_hint="requesting_party_token")) # Introspect Token -token_info = keycloak_openid.introspect(token['access_token'])) +token_info = keycloak_openid.introspect(token['access_token']) # Decode Token -KEYCLOAK_PUBLIC_KEY = "secret" -options = {"verify_signature": True, "verify_aud": True, "exp": True} +KEYCLOAK_PUBLIC_KEY = keycloak_openid.public_key() +options = {"verify_signature": True, "verify_aud": True, "verify_exp": True} token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) # Get permissions by token @@ -114,7 +114,9 @@ from keycloak import KeycloakAdmin keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", username='example-admin', password='secret', - realm_name="example_realm", + realm_name="master", + user_realm_name="only_if_other_realm_than_master", + client_secret_key="client-secret", verify=True) # Add user @@ -139,7 +141,7 @@ count_users = keycloak_admin.users_count() users = keycloak_admin.get_users({}) # Get user ID from name -user-id-keycloak = keycloak_admin.get_user_id("example@example.com") +user_id_keycloak = keycloak_admin.get_user_id("example@example.com") # Get User user = keycloak_admin.get_user("user-id-keycloak") @@ -149,7 +151,7 @@ response = keycloak_admin.update_user(user_id="user-id-keycloak", payload={'firstName': 'Example Update'}) # Update User Password -response = set_user_password(user_id="user-id-keycloak", password="secret", temporary=True) +response = keycloak_admin.set_user_password(user_id="user-id-keycloak", password="secret", temporary=True) # Delete User response = keycloak_admin.delete_user(user_id="user-id-keycloak") @@ -174,7 +176,7 @@ server_info = keycloak_admin.get_server_info() clients = keycloak_admin.get_clients() # Get client - id (not client-id) from client by name -client_id=keycloak_admin.get_client_id("my-client") +client_id = keycloak_admin.get_client_id("my-client") # Get representation of the client - id of client (not client-id) client = keycloak_admin.get_client(client_id="client_id") diff --git a/docs/source/conf.py b/docs/source/conf.py index ced1647..637a63b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ author = 'Marcos Pereira' # built documents. # # The short X.Y version. -version = '0.17.6' +version = '0.23.0' # The full version, including alpha/beta/rc tags. -release = '0.17.6' +release = '0.23.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 af697da..0cd6e2f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -132,7 +132,7 @@ Main methods:: # Decode Token KEYCLOAK_PUBLIC_KEY = "secret" - options = {"verify_signature": True, "verify_aud": True, "exp": True} + options = {"verify_signature": True, "verify_aud": True, "verify_exp": True} token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) # Get permissions by token @@ -158,6 +158,14 @@ Main methods:: # realm_name="example_realm", # verify=True, # custom_headers={'CustomHeader': 'value'}) + # + # You can also authenticate with client_id and client_secret + #keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", + # client_id="example_client", + # client_secret_key="secret", + # realm_name="example_realm", + # verify=True, + # custom_headers={'CustomHeader': 'value'}) # Add user new_user = keycloak_admin.create_user({"email": "example@example.com", @@ -268,3 +276,19 @@ Main methods:: # Function to trigger user sync from provider sync_users(storage_id="storage_di", action="action") + + # List public RSA keys + components = keycloak_admin.keys + + # List all keys + components = keycloak_admin.get_components(query={"parent":"example_realm", "type":"org.keycloak.keys.KeyProvider"}) + + # Create a new RSA key + component = keycloak_admin.create_component({"name":"rsa-generated","providerId":"rsa-generated","providerType":"org.keycloak.keys.KeyProvider","parentId":"example_realm","config":{"priority":["100"],"enabled":["true"],"active":["true"],"algorithm":["RS256"],"keySize":["2048"]}}) + + # Update the key + component_details['config']['active'] = ["false"] + keycloak_admin.update_component(component['id']) + + # Delete the key + keycloak_admin.delete_component(component['id']) diff --git a/keycloak/connection.py b/keycloak/connection.py index 6f32439..12903a0 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -47,6 +47,7 @@ class ConnectionManager(object): self._timeout = timeout self._verify = verify self._s = requests.Session() + self._s.auth = lambda x: x # don't let requests add auth headers # retry once to reset connection with Keycloak after tomcat's ConnectionTimeout # see https://github.com/marcospereirampj/python-keycloak/issues/36 @@ -198,11 +199,12 @@ class ConnectionManager(object): raise KeycloakConnectionError( "Can't connect to server (%s)" % e) - def raw_delete(self, path, **kwargs): + def raw_delete(self, path, data={}, **kwargs): """ Submit delete request to the path. :arg path (str): Path for request. + data (dict): Payload for request. :return Response the request. :exception @@ -211,6 +213,7 @@ class ConnectionManager(object): try: return self._s.delete(urljoin(self.base_url, path), params=kwargs, + data=data, headers=self.headers, timeout=self.timeout, verify=self.verify) diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index a3894e7..67da62a 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -53,6 +53,9 @@ class KeycloakOperationError(KeycloakError): pass +class KeycloakDeprecationError(KeycloakError): + pass + class KeycloakGetError(KeycloakOperationError): pass @@ -73,9 +76,12 @@ class KeycloakInvalidTokenError(KeycloakOperationError): pass -def raise_error_from_response(response, error, expected_code=200, skip_exists=False): - if expected_code == response.status_code: - if expected_code == requests.codes.no_content: +def raise_error_from_response(response, error, expected_codes=None, skip_exists=False): + if expected_codes is None: + expected_codes = [200, 201, 204] + + if response.status_code in expected_codes: + if response.status_code == requests.codes.no_content: return {} try: diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f0b3c52..e257cc0 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -26,21 +26,27 @@ import json from builtins import isinstance -from typing import List, Iterable +from typing import Iterable from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ - URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, \ + URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES,\ + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ + URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_GROUPS_CLIENT_ROLES, \ URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, URL_ADMIN_USER_GROUP, URL_ADMIN_REALM_ROLES, URL_ADMIN_GROUP_CHILD, \ URL_ADMIN_USER_CONSENTS, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_CLIENT, URL_ADMIN_USER, URL_ADMIN_CLIENT_ROLE, \ URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \ URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ - URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ - URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ + URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, URL_ADMIN_IDP, \ + URL_ADMIN_IDP_MAPPERS, URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES + URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ + URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ + URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ + URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ + URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER class KeycloakAdmin: @@ -60,7 +66,7 @@ class KeycloakAdmin: _custom_headers = None _user_realm_name = None - def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True, + def __init__(self, server_url, username=None, password=None, realm_name='master', client_id='admin-cli', verify=True, client_secret_key=None, custom_headers=None, user_realm_name=None, auto_refresh_token=None): """ @@ -72,6 +78,7 @@ class KeycloakAdmin: :param verify: True if want check connection SSL :param client_secret_key: client secret key :param custom_headers: dict of custom header to pass to each HTML request + :param user_realm_name: The realm name of the user, if different from realm_name :param auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete'] """ self.server_url = server_url @@ -190,7 +197,6 @@ class KeycloakAdmin: self._auto_refresh_token = value - def __fetch_all(self, url, query=None): '''Wrapper function to paginate GET requests @@ -224,7 +230,7 @@ class KeycloakAdmin: Import a new realm from a RealmRepresentation. Realm name must be unique. RealmRepresentation - https://www.keycloak.org/docs-api/4.4/rest-api/index.html#_realmrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation :param payload: RealmRepresentation @@ -233,7 +239,7 @@ class KeycloakAdmin: data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_realms(self): """ @@ -248,33 +254,96 @@ class KeycloakAdmin: """ Create a realm - ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_realmrepresentation + RealmRepresentation: + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation - :param skip_exists: Skip if Realm already exist. :param payload: RealmRepresentation + :param skip_exists: Skip if Realm already exist. :return: Keycloak server response (RealmRepresentation) """ data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + + def update_realm(self, realm_name, payload): + """ + Update a realm. This wil only update top level attributes and will ignore any user, + role, or client information in the payload. + + RealmRepresentation: + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + + :param realm_name: Realm name (not the realm id) + :param payload: RealmRepresentation + :return: Http response + """ + params_path = {"realm-name": realm_name} + data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def delete_realm(self, realm_name): + """ + Delete a realm + + :param realm_name: Realm name (not the realm id) + :return: Http response + """ + + params_path = {"realm-name": realm_name} + data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_users(self, query=None): """ - Get users Returns a list of users, filtered according to query parameters + Return a list of users, filtered according to query parameters + UserRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + + :param query: Query parameters (optional) :return: users list """ params_path = {"realm-name": self.realm_name} return self.__fetch_all(URL_ADMIN_USERS.format(**params_path), query) + def create_idp(self, payload): + """ + Create an ID Provider, + + IdentityProviderRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityproviderrepresentation + + :param: payload: IdentityProviderRepresentation + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + def add_mapper_to_idp(self, idp_alias, payload): + """ + Create an ID Provider, + + IdentityProviderRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityprovidermapperrepresentation + + :param: idp_alias: alias for Idp to add mapper in + :param: payload: IdentityProviderMapperRepresentation + """ + params_path = {"realm-name": self.realm_name, "idp-alias": idp_alias} + data_raw = self.raw_post(URL_ADMIN_IDP_MAPPERS.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def get_idps(self): """ Returns a list of ID Providers, IdentityProviderRepresentation - https://www.keycloak.org/docs-api/3.3/rest-api/index.html#_identityproviderrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityproviderrepresentation :return: array IdentityProviderRepresentation """ @@ -282,12 +351,22 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_IDPS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def delete_idp(self, idp_alias): + """ + Deletes ID Provider, + + :param: idp_alias: idp alias name + """ + params_path = {"realm-name": self.realm_name, "alias": idp_alias} + data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def create_user(self, payload): """ - Create a new user Username must be unique + Create a new user. Username must be unique UserRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation :param payload: UserRepresentation @@ -302,7 +381,9 @@ class KeycloakAdmin: data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + _last_slash_idx = data_raw.headers['Location'].rindex('/') + return data_raw.headers['Location'][_last_slash_idx + 1:] def users_count(self): """ @@ -320,7 +401,7 @@ class KeycloakAdmin: This is required for further actions against this user. UserRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation :param username: id in UserRepresentation @@ -336,7 +417,8 @@ class KeycloakAdmin: :param user_id: User id - UserRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + UserRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation :return: UserRepresentation """ @@ -346,7 +428,7 @@ class KeycloakAdmin: def get_user_groups(self, user_id): """ - Get user groups Returns a list of groups of which the user is a member + Returns a list of groups of which the user is a member :param user_id: User id @@ -368,7 +450,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_user(self, user_id): """ @@ -380,15 +462,15 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_delete(URL_ADMIN_USER.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def set_user_password(self, user_id, password, temporary=True): """ Set up a password for the user. If temporary is True, the user will have to reset the temporary password next time they log in. - http://www.keycloak.org/docs-api/3.2/rest-api/#_users_resource - http://www.keycloak.org/docs-api/3.2/rest-api/#_credentialrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_users_resource + https://www.keycloak.org/docs-api/8.0/rest-api/#_credentialrepresentation :param user_id: User id :param password: New password @@ -400,7 +482,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def consents_user(self, user_id): """ @@ -414,16 +496,40 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_user_social_logins(self, user_id): + """ + Returns a list of federated identities/social logins of which the user has been associated with + :param user_id: User id + :return: federated identities list + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_FEDERATED_IDENTITIES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def add_user_social_login(self, user_id, provider_id, provider_userid, provider_username): + + """ + Add a federated identity / social login provider to the user + :param user_id: User id + :param provider_id: Social login provider id + :param provider_userid: userid specified by the provider + :param provider_username: username specified by the provider + :return: + """ + payload = {"identityProvider": provider_id, "userId": provider_userid, "userName": provider_username} + params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} + data_raw = self.raw_post(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload)) + def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None): """ - Send a update account email to the user An email contains a + Send an update account email to the user. An email contains a link the user can click to perform a set of required actions. - :param user_id: - :param payload: - :param client_id: - :param lifespan: - :param redirect_uri: + :param user_id: User id + :param payload: A list of actions for the user to complete + :param client_id: Client id (optional) + :param lifespan: Number of seconds after which the generated token expires (optional) + :param redirect_uri: The redirect uri (optional) :return: """ @@ -439,8 +545,8 @@ class KeycloakAdmin: link the user can click to perform a set of required actions. :param user_id: User id - :param client_id: Client id - :param redirect_uri: Redirect uri + :param client_id: Client id (optional) + :param redirect_uri: Redirect uri (optional) :return: """ @@ -457,7 +563,7 @@ class KeycloakAdmin: :param user_id: id of user UserSessionRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_usersessionrepresentation :return: UserSessionRepresentation """ @@ -470,7 +576,7 @@ class KeycloakAdmin: Get themes, social providers, auth providers, and event listeners available on this server ServerInfoRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_serverinforepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_serverinforepresentation :return: ServerInfoRepresentation """ @@ -479,10 +585,10 @@ class KeycloakAdmin: def get_groups(self): """ - Get groups belonging to the realm. Returns a list of groups belonging to the realm + Returns a list of groups belonging to the realm GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :return: array GroupRepresentation """ @@ -494,8 +600,9 @@ class KeycloakAdmin: Get group by id. Returns full group details GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + :param group_id: The group id :return: Keycloak server response (GroupRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} @@ -507,7 +614,7 @@ class KeycloakAdmin: Utility function to iterate through nested group structures GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :param name: group (GroupRepresentation) :param path: group path (string) @@ -531,8 +638,10 @@ class KeycloakAdmin: Get members by group id. Returns group members GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_userrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_userrepresentation + :param group_id: The group id + :param query: Additional query parameters (see https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getmembers) :return: Keycloak server response (UserRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} @@ -545,7 +654,7 @@ class KeycloakAdmin: Subgroups are traversed, the first to match path (or name with path) is returned. GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :param path: group path :param search_in_subgroups: True if want search in the subgroups @@ -573,9 +682,10 @@ class KeycloakAdmin: :param payload: GroupRepresentation :param parent: parent group's id. Required to create a sub-group. + :param skip_exists: If true then do not raise an error if it already exists GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :return: Http response """ @@ -589,7 +699,7 @@ class KeycloakAdmin: data_raw = self.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def update_group(self, group_id, payload): """ @@ -599,7 +709,7 @@ class KeycloakAdmin: :param payload: GroupRepresentation with updated information. GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :return: Http response """ @@ -607,7 +717,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): """ @@ -627,7 +737,6 @@ class KeycloakAdmin: """ Add user to group (user_id and group_id) - :param group_id: id of group :param user_id: id of user :param group_id: id of group to add to :return: Keycloak server response @@ -635,21 +744,20 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def group_user_remove(self, user_id, group_id): """ Remove user from group (user_id and group_id) - :param group_id: id of group :param user_id: id of user - :param group_id: id of group to add to + :param group_id: id of group to remove from :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_group(self, group_id): """ @@ -661,14 +769,14 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete(URL_ADMIN_GROUP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_clients(self): """ - Get clients belonging to the realm Returns a list of clients belonging to the realm + Returns a list of clients belonging to the realm ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response (ClientRepresentation) """ @@ -682,7 +790,7 @@ class KeycloakAdmin: Get representation of the client ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) @@ -698,7 +806,7 @@ class KeycloakAdmin: This is required for further actions against this client. :param client_name: name in ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: client_id (uuid as string) """ @@ -715,7 +823,7 @@ class KeycloakAdmin: Get authorization json from client. :param client_id: id in ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ @@ -728,7 +836,7 @@ class KeycloakAdmin: Get resources from client. :param client_id: id in ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ @@ -736,13 +844,26 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path)) return data_raw + def get_client_service_account_user(self, client_id): + """ + Get service account user from client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: UserRepresentation + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get(URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def create_client(self, payload, skip_exists=False): """ Create a client - ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + ClientRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation - :param skip_exists: Skip if client already exist. + :param skip_exists: If true then do not raise an error if client already exists :param payload: ClientRepresentation :return: Keycloak server response (UserRepresentation) """ @@ -750,7 +871,7 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def update_client(self, client_id, payload): """ @@ -762,16 +883,16 @@ class KeycloakAdmin: :return: Http response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_put(URL_ADMIN_CLIENT.format(**params_path), + data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_client(self, client_id): """ Get representation of the client ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :param client_id: keycloak client id (not oauth client-id) :return: Keycloak server response (ClientRepresentation) @@ -779,14 +900,32 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def get_client_installation_provider(self, client_id, provider_id): + """ + Get content for given installation provider + + Related documentation: + https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_clients_resource + + Possible provider_id list available in the ServerInfoRepresentation#clientInstallations + https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_serverinforepresentation + + :param client_id: Client id + :param provider_id: provider id to specify response format + """ + + params_path = {"realm-name": self.realm_name, "id": client_id, "provider-id": provider_id} + data_raw = self.raw_get(URL_ADMIN_CLIENT_INSTALLATION_PROVIDER.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def get_realm_roles(self): """ Get all roles for the realm or client RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) """ @@ -795,6 +934,16 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_realm_role_members(self, role_name, **query): + """ + Get role members of realm by role name. + :param role_name: Name of the role. + :param query: Additional Query parameters (see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_roles_resource) + :return: Keycloak Server Response (UserRepresentation) + """ + params_path = {"realm-name": self.realm_name, "role-name":role_name} + return self.__fetch_all(URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query) + def get_client_roles(self, client_id): """ Get all roles for the client @@ -802,7 +951,7 @@ class KeycloakAdmin: :param client_id: id of client (not client-id) RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) """ @@ -820,7 +969,7 @@ class KeycloakAdmin: :param role_name: role’s name (not id!) RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: role_id """ @@ -839,7 +988,7 @@ class KeycloakAdmin: :param role_name: role’s name (not id!) RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: role_id """ @@ -851,39 +1000,39 @@ class KeycloakAdmin: Create a client role RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) :param payload: RoleRepresentation + :param skip_exists: If true then do not raise an error if client role already exists :return: Keycloak server response (RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_role_id} data_raw = self.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def delete_client_role(self, client_role_id, role_name): """ - Create a client role + Delete a client role RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) :param role_name: role’s name (not id!) """ params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} data_raw = self.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def assign_client_role(self, user_id, client_id, roles): """ Assign a client role to a user - :param client_id: id of client (not client-id) :param user_id: id of user - :param client_id: id of client containing role, + :param client_id: id of client (not client-id) :param roles: roles list or role (use RoleRepresentation) :return Keycloak server response """ @@ -892,22 +1041,118 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_post(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def get_client_role_members(self, client_id, role_name, **query): + """ + Get members by client role . + :param client_id: The client id + :param role_name: the name of role to be queried. + :param query: Additional query parameters ( see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clients_resource) + :return: Keycloak server response (UserRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id":client_id, "role-name":role_name} + return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path) , query) + def create_realm_role(self, payload, skip_exists=False): """ Create a new role for the realm or client - :param realm: realm name (not id) - :param rep: RoleRepresentation https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_rolerepresentation + :param payload: The role (use RoleRepresentation) + :param skip_exists: If true then do not raise an error if realm role already exists :return Keycloak server response """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), + data_raw = self.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + + def get_realm_role(self, role_name): + """ + Get realm role by role name + :param role_name: role's name, not id! + + RoleRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + :return: role_id + """ + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_get(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def update_realm_role(self, role_name, payload): + """ + Update a role for the realm by name + :param role_name: The name of the role to be updated + :param payload: The role (use RoleRepresentation) + :return Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.connection.raw_put(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def delete_realm_role(self, role_name): + """ + Delete a role for the realm by name + :param payload: The role name {'role-name':'name-of-the-role'} + :return Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.connection.raw_delete( + URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def add_composite_realm_roles_to_role(self, role_name, roles): + """ + Add composite roles to the role + + :param role_name: The name of the role + :param roles: roles list or role (use RoleRepresentation) to be updated + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_post( + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, + expected_codes=[204]) + + def remove_composite_realm_roles_to_role(self, role_name, roles): + """ + Remove composite roles from the role + + :param role_name: The name of the role + :param roles: roles list or role (use RoleRepresentation) to be removed + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_delete( + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, + expected_codes=[204]) + + def get_composite_realm_roles_of_role(self, role_name): + """ + Get composite roles of the role + + :param role_name: The name of the role + :return Keycloak server response (array RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_get( + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) def assign_realm_roles(self, user_id, roles): """ @@ -922,14 +1167,112 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_post(URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def get_realm_roles_of_user(self, user_id): + """ + Get all realm roles for a user. + + :param user_id: id of user + :return: Keycloak server response (array RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def assign_group_realm_roles(self, group_id, roles): + """ + Assign realm roles to a group + + :param group_id: id of groupp + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.raw_post(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def delete_group_realm_roles(self, group_id, roles): + """ + Delete realm roles of a group + + :param group_id: id of group + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.raw_delete(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def get_group_realm_roles(self, group_id): + """ + Get all realm roles for a group. + + :param user_id: id of the group + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.raw_get(URL_ADMIN_GET_GROUPS_REALM_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def assign_group_client_roles(self, group_id, client_id, roles): + """ + Assign client roles to a group + + :param group_id: id of group + :param client_id: id of client (not client-id) + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + data_raw = self.raw_post(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def get_group_client_roles(self, group_id, client_id): + """ + Get client roles of a group + + :param group_id: id of group + :param client_id: id of client (not client-id) + :return Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + data_raw = self.raw_get(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def delete_group_client_roles(self, group_id, client_id, roles): + """ + Delete client roles of a group + + :param group_id: id of group + :param client_id: id of client (not client-id) + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response (array RoleRepresentation) + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + data_raw = self.raw_delete(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_client_roles_of_user(self, user_id, client_id): """ Get all client roles for a user. - :param client_id: id of client (not client-id) :param user_id: id of user + :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES, user_id, client_id) @@ -938,8 +1281,8 @@ class KeycloakAdmin: """ Get available client role-mappings for a user. - :param client_id: id of client (not client-id) :param user_id: id of user + :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id) @@ -948,8 +1291,8 @@ class KeycloakAdmin: """ Get composite client role-mappings for a user. - :param client_id: id of client (not client-id) :param user_id: id of user + :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id) @@ -963,9 +1306,8 @@ class KeycloakAdmin: """ Delete client roles from a user. - :param client_id: id of client (not client-id) :param user_id: id of user - :param client_id: id of client containing role, + :param client_id: id of client containing role (not client-id) :param roles: roles list or role to delete (use RoleRepresentation) :return: Keycloak server response """ @@ -973,41 +1315,71 @@ class KeycloakAdmin: params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_delete(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_authentication_flows(self): """ Get authentication flows. Returns all flow details AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation :return: Keycloak server response (AuthenticationFlowRepresentation) """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(URL_ADMIN_FLOWS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + + def get_authentication_flow_for_id(self, flow_id): + """ + Get one authentication flow by it's id/alias. Returns all flow details + + AuthenticationFlowRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + + :param flow_id: the id of a flow NOT it's alias + :return: Keycloak server response (AuthenticationFlowRepresentation) + """ + params_path = {"realm-name": self.realm_name, "flow-id": flow_id} + data_raw = self.raw_get(URL_ADMIN_FLOWS_ALIAS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow(self, payload, skip_exists=False): """ Create a new authentication flow AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation :param payload: AuthenticationFlowRepresentation + :param skip_exists: If true then do not raise an error if authentication flow already exists :return: Keycloak server response (RoleRepresentation) """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + + def copy_authentication_flow(self, payload, flow_alias): + """ + Copy existing authentication flow under a new name. The new name is given as 'newName' attribute of the passed payload. + + :param payload: JSON containing 'newName' attribute + :param flow_alias: the flow alias + :return: Keycloak server response (RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} + data_raw = self.raw_post(URL_ADMIN_FLOWS_COPY.format(**params_path), + data=payload) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_authentication_flow_executions(self, flow_alias): """ Get authentication flow executions. Returns all execution steps + :param flow_alias: the flow alias :return: Response(json) """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} @@ -1019,23 +1391,59 @@ class KeycloakAdmin: Update an authentication flow execution AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation :param payload: AuthenticationExecutionInfoRepresentation + :param flow_alias: The flow alias :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def create_authentication_flow_execution(self, payload, flow_alias): + """ + Create an authentication flow execution + + AuthenticationExecutionInfoRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + + :param payload: AuthenticationExecutionInfoRepresentation + :param flow_alias: The flow alias + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} + data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION.format(**params_path), + data=payload) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): + """ + Create a new sub authentication flow for a given authentication flow + + AuthenticationFlowRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + + :param payload: AuthenticationFlowRepresentation + :param flow_alias: The flow alias + :param skip_exists: If true then do not raise an error if authentication flow already exists + :return: Keycloak server response (RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} + data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), + data=payload) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def sync_users(self, storage_id, action): """ Function to trigger user sync from provider - :param storage_id: - :param action: + :param storage_id: The id of the user storage provider + :param action: Action can be "triggerFullSync" or "triggerChangedUsersSync" :return: """ data = {'action': action} @@ -1049,7 +1457,7 @@ class KeycloakAdmin: def get_client_scopes(self): """ Get representation of the client scopes for the realm where we are connected to - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientscopes + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes :return: Keycloak server response Array of (ClientScopeRepresentation) """ @@ -1061,8 +1469,9 @@ class KeycloakAdmin: def get_client_scope(self, client_scope_id): """ Get representation of the client scopes for the realm where we are connected to - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientscopes + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + :param client_scope_id: The id of the client scope :return: Keycloak server response (ClientScopeRepresentation) """ @@ -1070,12 +1479,28 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def create_client_scope(self, payload, skip_exists=False): + """ + Create a client scope + + ClientScopeRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + + :param payload: ClientScopeRepresentation + :param skip_exists: If true then do not raise an error if client scope already exists + :return: Keycloak server response (ClientScopeRepresentation) + """ + + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_post(URL_ADMIN_CLIENT_SCOPES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_create_mapper + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_create_mapper + :param client_scope_id: The id of the client scope :param payload: ProtocolMapperRepresentation :return: Keycloak server Response """ @@ -1085,13 +1510,62 @@ class KeycloakAdmin: data_raw = self.raw_post( URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): + """ + Delete a mapper from a client scope + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_delete_mapper + + :param client_scope_id: The id of the client scope + :param payload: ProtocolMapperRepresentation + :return: Keycloak server Response + """ + + params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id, + "protocol-mapper-id": protocol_mppaer_id} + + data_raw = self.raw_delete( + URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def add_mapper_to_client(self, client_id, payload): + """ + Add a mapper to a client + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_create_mapper + + :param client_id: The id of the client + :param payload: ProtocolMapperRepresentation + :return: Keycloak server Response + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + + data_raw = self.raw_post( + URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + def generate_client_secrets(self, client_id): + """ + + Generate a new secret for the client + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_regeneratesecret + + :param client_id: id of client (not client-id) + :return: Keycloak server response (ClientRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_post(URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None) + return raise_error_from_response(data_raw, KeycloakGetError) def get_client_secrets(self, client_id): """ Get representation of the client secrets - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientsecret + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientsecret :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) @@ -1101,6 +1575,93 @@ class KeycloakAdmin: data_raw = self.raw_get(URL_ADMIN_CLIENT_SECRETS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_components(self, query=None): + """ + Return a list of components, filtered according to query parameters + + ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :param query: Query parameters (optional) + :return: components list + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get(URL_ADMIN_COMPONENTS.format(**params_path), + data=None, **query) + return raise_error_from_response(data_raw, KeycloakGetError) + + def create_component(self, payload): + """ + Create a new component. + + ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :param payload: ComponentRepresentation + + :return: UserRepresentation + """ + params_path = {"realm-name": self.realm_name} + + data_raw = self.raw_post(URL_ADMIN_COMPONENTS.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + def get_component(self, component_id): + """ + Get representation of the component + + :param component_id: Component id + + ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :return: ComponentRepresentation + """ + params_path = {"realm-name": self.realm_name, "component-id": component_id} + data_raw = self.raw_get(URL_ADMIN_COMPONENT.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def update_component(self, component_id, payload): + """ + Update the component + + :param component_id: Component id + :param payload: ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :return: Http response + """ + params_path = {"realm-name": self.realm_name, "component-id": component_id} + data_raw = self.raw_put(URL_ADMIN_COMPONENT.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def delete_component(self, component_id): + """ + Delete the component + + :param component_id: Component id + + :return: Http response + """ + params_path = {"realm-name": self.realm_name, "component-id": component_id} + data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def get_keys(self): + """ + Return a list of keys, filtered according to query parameters + + KeysMetadataRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_key_resource + + :return: keys list + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get(URL_ADMIN_KEYS.format(**params_path), + data=None) + return raise_error_from_response(data_raw, KeycloakGetError) def raw_get(self, *args, **kwargs): """ @@ -1185,7 +1746,8 @@ class KeycloakAdmin: try: self.token = self.keycloak_openid.refresh_token(refresh_token) except KeycloakGetError as e: - if e.response_code == 400 and b'Refresh token expired' in e.response_body: + if e.response_code == 400 and (b'Refresh token expired' in e.response_body or + b'Token is not active' in e.response_body): self.get_token() else: raise diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index b196a85..0f801ea 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -28,8 +28,9 @@ from jose import jwt from .authorization import Authorization from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError, \ - KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError + KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError, KeycloakDeprecationError from .urls_patterns import ( + URL_REALM, URL_AUTH, URL_TOKEN, URL_USERINFO, @@ -250,7 +251,7 @@ class KeycloakOpenID: data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def certs(self): """ @@ -265,6 +266,17 @@ class KeycloakOpenID: params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + + def public_key(self): + """ + The public key is exposed by the realm page directly. + + :return: + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError)['public_key'] + def entitlement(self, token, resource_server_id): """ @@ -279,6 +291,9 @@ class KeycloakOpenID: self.connection.add_param_headers("Authorization", "Bearer " + token) params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id} data_raw = self.connection.raw_get(URL_ENTITLEMENT.format(**params_path)) + + if data_raw.status_code == 404: + return raise_error_from_response(data_raw, KeycloakDeprecationError) return raise_error_from_response(data_raw, KeycloakGetError) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index fad3455..14410d8 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -22,6 +22,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # OPENID URLS +URL_REALM = "realms/{realm-name}" URL_WELL_KNOWN = "realms/{realm-name}/.well-known/openid-configuration" URL_TOKEN = "realms/{realm-name}/protocol/openid-connect/token" URL_USERINFO = "realms/{realm-name}/protocol/openid-connect/userinfo" @@ -42,6 +43,9 @@ URL_ADMIN_RESET_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" URL_ADMIN_GET_SESSIONS = "admin/realms/{realm-name}/users/{id}/sessions" URL_ADMIN_USER_CLIENT_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}" URL_ADMIN_USER_REALM_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" +URL_ADMIN_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/realm" +URL_ADMIN_GET_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings" +URL_ADMIN_GROUPS_CLIENT_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/clients/{client-id}" URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available" URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/composite" URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" @@ -62,17 +66,39 @@ URL_ADMIN_CLIENT = URL_ADMIN_CLIENTS + "/{id}" URL_ADMIN_CLIENT_SECRETS= URL_ADMIN_CLIENT + "/client-secret" URL_ADMIN_CLIENT_ROLES = URL_ADMIN_CLIENT + "/roles" URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" +URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" +URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" +URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" +URL_ADMIN_CLIENT_PROTOCOL_MAPPER = URL_ADMIN_CLIENT + "/protocol-mappers/models" URL_ADMIN_CLIENT_SCOPES = "admin/realms/{realm-name}/client-scopes" URL_ADMIN_CLIENT_SCOPE = URL_ADMIN_CLIENT_SCOPES + "/{scope-id}" URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER = URL_ADMIN_CLIENT_SCOPE + "/protocol-mappers/models" +URL_ADMIN_CLIENT_SCOPES_MAPPERS = URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER + "/{protocol-mapper-id}" URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" +URL_ADMIN_REALM_ROLES_MEMBERS = URL_ADMIN_REALM_ROLES + "/{role-name}/users" URL_ADMIN_REALMS = "admin/realms" +URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" +URL_ADMIN_IDP_MAPPERS = "admin/realms/{realm-name}/identity-provider/instances/{idp-alias}/mappers" +URL_ADMIN_IDP = "admin/realms//{realm-name}/identity-provider/instances/{alias}" +URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" +URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = "admin/realms/{realm-name}/roles/{role-name}/composites" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" +URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" +URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" +URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" +URL_ADMIN_FLOWS_EXECUTIONS_FLOW = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" + +URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" +URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" +URL_ADMIN_KEYS = "admin/realms/{realm-name}/keys" + +URL_ADMIN_USER_FEDERATED_IDENTITIES = "admin/realms/{realm-name}/users/{id}/federated-identity" +URL_ADMIN_USER_FEDERATED_IDENTITY = "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}" diff --git a/setup.py b/setup.py index 3183221..78b8d96 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open("README.md", "r") as fh: setup( name='python-keycloak', - version='0.17.6', + version='0.23.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira',