From bae47f0761af52209995d1f99650451ba4a773a8 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 5 Sep 2017 11:34:00 -0300 Subject: [PATCH 1/5] Methods: list users, get user, create user. --- .gitignore | 1 + keycloak/__init__.py | 350 +--------------------------------- keycloak/keycloak_admin.py | 144 ++++++++++++++ keycloak/keycloak_openid.py | 365 ++++++++++++++++++++++++++++++++++++ keycloak/urls_patterns.py | 8 +- 5 files changed, 518 insertions(+), 350 deletions(-) create mode 100644 keycloak/keycloak_admin.py create mode 100644 keycloak/keycloak_openid.py diff --git a/.gitignore b/.gitignore index 3f4c507..7ea9902 100644 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,5 @@ ENV/ .idea/ main.py +main2.py s3air-authz-config.json \ No newline at end of file diff --git a/keycloak/__init__.py b/keycloak/__init__.py index aec53e7..cf1f955 100644 --- a/keycloak/__init__.py +++ b/keycloak/__init__.py @@ -14,352 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from .authorization import Authorization -from .exceptions import raise_error_from_response, KeycloakGetError, KeycloakSecretNotFound, \ - KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError -from .urls_patterns import ( - URL_AUTH, - URL_TOKEN, - URL_USERINFO, - URL_WELL_KNOWN, - URL_LOGOUT, - URL_CERTS, - URL_ENTITLEMENT, - URL_INTROSPECT -) -from .connection import ConnectionManager -from jose import jwt -import json - - -class Keycloak: - - def __init__(self, server_url, client_id, realm_name, client_secret_key=None): - self._client_id = client_id - self._client_secret_key = client_secret_key - self._realm_name = realm_name - - self._connection = ConnectionManager(base_url=server_url, - headers={}, - timeout=60) - - self._authorization = Authorization() - - @property - def client_id(self): - return self._client_id - - @client_id.setter - def client_id(self, value): - self._client_id = value - - @property - def client_secret_key(self): - return self._client_secret_key - - @client_secret_key.setter - def client_secret_key(self, value): - self._client_secret_key = value - - @property - def realm_name(self): - return self._realm_name - - @realm_name.setter - def realm_name(self, value): - self._realm_name = value - - @property - def connection(self): - return self._connection - - @connection.setter - def connection(self, value): - self._connection = value - - @property - def authorization(self): - return self._authorization - - @authorization.setter - def authorization(self, value): - self._authorization = value - - def _add_secret_key(self, payload): - """ - Add secret key if exist. - - :param payload: - :return: - """ - if self.client_secret_key: - payload.update({"client_secret": self.client_secret_key}) - - return payload - - def _build_name_role(self, role): - """ - - :param role: - :return: - """ - return self.client_id + "/" + role - - def _token_info(self, token, method_token_info, **kwargs): - """ - - :param token: - :param method_token_info: - :param kwargs: - :return: - """ - if method_token_info == 'introspect': - token_info = self.introspect(token) - else: - token_info = self.decode_token(token, **kwargs) - - return token_info - - def well_know(self): - """ The most important endpoint to understand is the well-known configuration - endpoint. It lists endpoints and other configuration options relevant to - the OpenID Connect implementation in Keycloak. - - :return It lists endpoints and other configuration options relevant. - """ - - params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_WELL_KNOWN.format(**params_path)) - - return raise_error_from_response(data_raw, KeycloakGetError) - - def auth_url(self, redirect_uri): - """ - - http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint - - :return: - """ - return NotImplemented - - def token(self, username, password, grant_type=["password"]): - """ - 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 username: - :param password: - :param grant_type: - :return: - """ - params_path = {"realm-name": self.realm_name} - payload = {"username": username, "password": password, - "client_id": self.client_id, "grant_type": grant_type} - - 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, - and is protected by a bearer token. - - http://openid.net/specs/openid-connect-core-1_0.html#UserInfo - - :param token: - :return: - """ - - self.connection.add_param_headers("Authorization", "Bearer " + token) - params_path = {"realm-name": self.realm_name} - - data_raw = self.connection.raw_get(URL_USERINFO.format(**params_path)) - - return raise_error_from_response(data_raw, KeycloakGetError) - - def logout(self, refresh_token): - """ - The logout endpoint logs out the authenticated user. - :param refresh_token: - :return: - """ - params_path = {"realm-name": self.realm_name} - payload = {"client_id": self.client_id, "refresh_token": refresh_token} - - payload = self._add_secret_key(payload) - data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), - data=payload) - - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) - - def certs(self): - """ - The certificate endpoint returns the public keys enabled by the realm, encoded as a - JSON Web Key (JWK). Depending on the realm settings there can be one or more keys enabled - for verifying tokens. - - https://tools.ietf.org/html/rfc7517 - - :return: - """ - params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) - - def entitlement(self, token, resource_server_id): - """ - Client applications can use a specific endpoint to obtain a special security token - called a requesting party token (RPT). This token consists of all the entitlements - (or permissions) for a user as a result of the evaluation of the permissions and authorization - policies associated with the resources being requested. With an RPT, client applications can - gain access to protected resources at the resource server. - - :return: - """ - 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)) - - return raise_error_from_response(data_raw, KeycloakGetError) - - def introspect(self, token, rpt=None, token_type_hint=None): - """ - The introspection endpoint is used to retrieve the active state of a token. It is can only be - invoked by confidential clients. - - https://tools.ietf.org/html/rfc7662 - - :param token: - :param rpt: - :param token_type_hint: - - :return: - """ - params_path = {"realm-name": self.realm_name} - - payload = {"client_id": self.client_id, "token": token} - - if token_type_hint == 'requesting_party_token': - if rpt: - payload.update({"token": rpt, "token_type_hint": token_type_hint}) - self.connection.add_param_headers("Authorization", "Bearer " + token) - else: - raise KeycloakRPTNotFound("Can't found RPT.") - - payload = self._add_secret_key(payload) - - data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), - data=payload) - - return raise_error_from_response(data_raw, KeycloakGetError) - - def decode_token(self, token, key, algorithms=['RS256'], **kwargs): - """ - A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data - structure that represents a cryptographic key. This specification - also defines a JWK Set JSON data structure that represents a set of - JWKs. Cryptographic algorithms and identifiers for use with this - specification are described in the separate JSON Web Algorithms (JWA) - specification and IANA registries established by that specification. - - https://tools.ietf.org/html/rfc7517 - - :param token: - :param key: - :param algorithms: - :return: - """ - - return jwt.decode(token, key, algorithms=algorithms, - audience=self.client_id, **kwargs) - - def load_authorization_config(self, path): - """ - Load Keycloak settings (authorization) - - :param path: settings file (json) - :return: - """ - authorization_file = open(path, 'r') - authorization_json = json.loads(authorization_file.read()) - self.authorization.load_config(authorization_json) - authorization_file.close() - - def get_policies(self, token, method_token_info='introspect', **kwargs): - """ - Get policies by user token - - :param token: user token - :return: policies list - """ - - if not self.authorization.policies: - raise KeycloakAuthorizationConfigError( - "Keycloak settings not found. Load Authorization Keycloak settings." - ) - - token_info = self._token_info(token, method_token_info, **kwargs) - - if method_token_info == 'introspect' and not token_info['active']: - raise KeycloakInvalidTokenError( - "Token expired or invalid." - ) - - user_resources = token_info['resource_access'].get(self.client_id) - - if not user_resources: - return None - - policies = [] - - for policy_name, policy in self.authorization.policies.items(): - for role in user_resources['roles']: - if self._build_name_role(role) in policy.roles: - policies.append(policy) - - return list(set(policies)) - - def get_permissions(self, token, method_token_info='introspect', **kwargs): - """ - Get permission by user token - - :param token: user token - :param method_token_info: Decode token method - :param kwargs: parameters for decode - :return: permissions list - """ - - if not self.authorization.policies: - raise KeycloakAuthorizationConfigError( - "Keycloak settings not found. Load Authorization Keycloak settings." - ) - - token_info = self._token_info(token, method_token_info, **kwargs) - - if method_token_info == 'introspect' and not token_info['active']: - raise KeycloakInvalidTokenError( - "Token expired or invalid." - ) - - user_resources = token_info['resource_access'].get(self.client_id) - - if not user_resources: - return None - - permissions = [] - - for policy_name, policy in self.authorization.policies.items(): - for role in user_resources['roles']: - if self._build_name_role(role) in policy.roles: - permissions += policy.permissions - - return list(set(permissions)) - - +from .keycloak_openid import * +from .keycloak_admin import * diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py new file mode 100644 index 0000000..2c3a9d3 --- /dev/null +++ b/keycloak/keycloak_admin.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Marcos Pereira +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +from keycloak.urls_patterns import URL_ADMIN_USERS_COUNT, URL_ADMIN_USER +from .keycloak_openid import KeycloakOpenID + +from .exceptions import raise_error_from_response, KeycloakGetError, KeycloakSecretNotFound, \ + KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError + +from .urls_patterns import ( + URL_ADMIN_USERS, +) + +from .connection import ConnectionManager +from jose import jwt +import json + + +class KeycloakAdmin: + + def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli'): + self._username = username + self._password = password + self._client_id = client_id + self._realm_name = realm_name + + # Get token Admin + keycloak_openid = KeycloakOpenID(server_url, client_id, realm_name) + self._token = keycloak_openid.token(username, password) + + self._connection = ConnectionManager(base_url=server_url, + headers={'Authorization': 'Bearer ' + self.token.get('access_token'), + 'Content-Type': 'application/json'}, + timeout=60) + + @property + def realm_name(self): + return self._realm_name + + @realm_name.setter + def realm_name(self, value): + self._realm_name = value + + @property + def connection(self): + return self._connection + + @connection.setter + def connection(self, value): + self._connection = value + + @property + def client_id(self): + return self._client_id + + @client_id.setter + def client_id(self, value): + self._client_id = value + + @property + def username(self): + return self._username + + @username.setter + def username(self, value): + self._username = value + + @property + def password(self): + return self._password + + @password.setter + def password(self, value): + self._password = value + + @property + def token(self): + return self._token + + @token.setter + def token(self, value): + self._token = value + + def list_users(self, query=None): + """ + Get users Returns a list of users, filtered according to query parameters + + :return: users list + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_ADMIN_USERS.format(**params_path), **query) + return raise_error_from_response(data_raw, KeycloakGetError) + + def create_user(self, payload): + """ + Create a new user Username must be unique + + UserRepresentation + http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + + :return: UserRepresentation + """ + params_path = {"realm-name": self.realm_name} + 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) + + def count_users(self): + """ + User counter + + :return: counter + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_ADMIN_USERS_COUNT.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_user(self, user_id): + """ + Get representation of the user + + UserRepresentation + http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + + :return: UserRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.connection.raw_get(URL_ADMIN_USER.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py new file mode 100644 index 0000000..669d5e0 --- /dev/null +++ b/keycloak/keycloak_openid.py @@ -0,0 +1,365 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Marcos Pereira +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +from .authorization import Authorization +from .exceptions import raise_error_from_response, KeycloakGetError, \ + KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError +from .urls_patterns import ( + URL_TOKEN, + URL_USERINFO, + URL_WELL_KNOWN, + URL_LOGOUT, + URL_CERTS, + URL_ENTITLEMENT, + URL_INTROSPECT +) +from .connection import ConnectionManager +from jose import jwt +import json + + +class KeycloakOpenID: + + def __init__(self, server_url, client_id, realm_name, client_secret_key=None): + self._client_id = client_id + self._client_secret_key = client_secret_key + self._realm_name = realm_name + + self._connection = ConnectionManager(base_url=server_url, + headers={}, + timeout=60) + + self._authorization = Authorization() + + @property + def client_id(self): + return self._client_id + + @client_id.setter + def client_id(self, value): + self._client_id = value + + @property + def client_secret_key(self): + return self._client_secret_key + + @client_secret_key.setter + def client_secret_key(self, value): + self._client_secret_key = value + + @property + def realm_name(self): + return self._realm_name + + @realm_name.setter + def realm_name(self, value): + self._realm_name = value + + @property + def connection(self): + return self._connection + + @connection.setter + def connection(self, value): + self._connection = value + + @property + def authorization(self): + return self._authorization + + @authorization.setter + def authorization(self, value): + self._authorization = value + + def _add_secret_key(self, payload): + """ + Add secret key if exist. + + :param payload: + :return: + """ + if self.client_secret_key: + payload.update({"client_secret": self.client_secret_key}) + + return payload + + def _build_name_role(self, role): + """ + + :param role: + :return: + """ + return self.client_id + "/" + role + + def _token_info(self, token, method_token_info, **kwargs): + """ + + :param token: + :param method_token_info: + :param kwargs: + :return: + """ + if method_token_info == 'introspect': + token_info = self.introspect(token) + else: + token_info = self.decode_token(token, **kwargs) + + return token_info + + def well_know(self): + """ The most important endpoint to understand is the well-known configuration + endpoint. It lists endpoints and other configuration options relevant to + the OpenID Connect implementation in Keycloak. + + :return It lists endpoints and other configuration options relevant. + """ + + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_WELL_KNOWN.format(**params_path)) + + return raise_error_from_response(data_raw, KeycloakGetError) + + def auth_url(self, redirect_uri): + """ + + http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint + + :return: + """ + return NotImplemented + + def token(self, username, password, grant_type=["password"]): + """ + 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 username: + :param password: + :param grant_type: + :return: + """ + params_path = {"realm-name": self.realm_name} + payload = {"username": username, "password": password, + "client_id": self.client_id, "grant_type": grant_type} + + 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, + and is protected by a bearer token. + + http://openid.net/specs/openid-connect-core-1_0.html#UserInfo + + :param token: + :return: + """ + + self.connection.add_param_headers("Authorization", "Bearer " + token) + params_path = {"realm-name": self.realm_name} + + data_raw = self.connection.raw_get(URL_USERINFO.format(**params_path)) + + return raise_error_from_response(data_raw, KeycloakGetError) + + def logout(self, refresh_token): + """ + The logout endpoint logs out the authenticated user. + :param refresh_token: + :return: + """ + params_path = {"realm-name": self.realm_name} + payload = {"client_id": self.client_id, "refresh_token": refresh_token} + + payload = self._add_secret_key(payload) + data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), + data=payload) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def certs(self): + """ + The certificate endpoint returns the public keys enabled by the realm, encoded as a + JSON Web Key (JWK). Depending on the realm settings there can be one or more keys enabled + for verifying tokens. + + https://tools.ietf.org/html/rfc7517 + + :return: + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def entitlement(self, token, resource_server_id): + """ + Client applications can use a specific endpoint to obtain a special security token + called a requesting party token (RPT). This token consists of all the entitlements + (or permissions) for a user as a result of the evaluation of the permissions and authorization + policies associated with the resources being requested. With an RPT, client applications can + gain access to protected resources at the resource server. + + :return: + """ + 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)) + + return raise_error_from_response(data_raw, KeycloakGetError) + + def introspect(self, token, rpt=None, token_type_hint=None): + """ + The introspection endpoint is used to retrieve the active state of a token. It is can only be + invoked by confidential clients. + + https://tools.ietf.org/html/rfc7662 + + :param token: + :param rpt: + :param token_type_hint: + + :return: + """ + params_path = {"realm-name": self.realm_name} + + payload = {"client_id": self.client_id, "token": token} + + if token_type_hint == 'requesting_party_token': + if rpt: + payload.update({"token": rpt, "token_type_hint": token_type_hint}) + self.connection.add_param_headers("Authorization", "Bearer " + token) + else: + raise KeycloakRPTNotFound("Can't found RPT.") + + payload = self._add_secret_key(payload) + + data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), + data=payload) + + return raise_error_from_response(data_raw, KeycloakGetError) + + def decode_token(self, token, key, algorithms=['RS256'], **kwargs): + """ + A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data + structure that represents a cryptographic key. This specification + also defines a JWK Set JSON data structure that represents a set of + JWKs. Cryptographic algorithms and identifiers for use with this + specification are described in the separate JSON Web Algorithms (JWA) + specification and IANA registries established by that specification. + + https://tools.ietf.org/html/rfc7517 + + :param token: + :param key: + :param algorithms: + :return: + """ + + return jwt.decode(token, key, algorithms=algorithms, + audience=self.client_id, **kwargs) + + def load_authorization_config(self, path): + """ + Load Keycloak settings (authorization) + + :param path: settings file (json) + :return: + """ + authorization_file = open(path, 'r') + authorization_json = json.loads(authorization_file.read()) + self.authorization.load_config(authorization_json) + authorization_file.close() + + def get_policies(self, token, method_token_info='introspect', **kwargs): + """ + Get policies by user token + + :param token: user token + :return: policies list + """ + + if not self.authorization.policies: + raise KeycloakAuthorizationConfigError( + "Keycloak settings not found. Load Authorization Keycloak settings." + ) + + token_info = self._token_info(token, method_token_info, **kwargs) + + if method_token_info == 'introspect' and not token_info['active']: + raise KeycloakInvalidTokenError( + "Token expired or invalid." + ) + + user_resources = token_info['resource_access'].get(self.client_id) + + if not user_resources: + return None + + policies = [] + + for policy_name, policy in self.authorization.policies.items(): + for role in user_resources['roles']: + if self._build_name_role(role) in policy.roles: + policies.append(policy) + + return list(set(policies)) + + def get_permissions(self, token, method_token_info='introspect', **kwargs): + """ + Get permission by user token + + :param token: user token + :param method_token_info: Decode token method + :param kwargs: parameters for decode + :return: permissions list + """ + + if not self.authorization.policies: + raise KeycloakAuthorizationConfigError( + "Keycloak settings not found. Load Authorization Keycloak settings." + ) + + token_info = self._token_info(token, method_token_info, **kwargs) + + if method_token_info == 'introspect' and not token_info['active']: + raise KeycloakInvalidTokenError( + "Token expired or invalid." + ) + + user_resources = token_info['resource_access'].get(self.client_id) + + if not user_resources: + return None + + permissions = [] + + for policy_name, policy in self.authorization.policies.items(): + for role in user_resources['roles']: + if self._build_name_role(role) in policy.roles: + permissions += policy.permissions + + return list(set(permissions)) + + + diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index d43b006..bfbf563 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -15,12 +15,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +# OPENID URLS URL_WELL_KNOWN = "realms/{realm-name}/.well-known/openid-configuration" -URL_AUTH = "realms/{realm-name}/protocol/openid-connect/auth" URL_TOKEN = "realms/{realm-name}/protocol/openid-connect/token" URL_USERINFO = "realms/{realm-name}/protocol/openid-connect/userinfo" URL_LOGOUT = "realms/{realm-name}/protocol/openid-connect/logout" URL_CERTS = "realms/{realm-name}/protocol/openid-connect/certs" URL_INTROSPECT = "realms/{realm-name}/protocol/openid-connect/token/introspect" - URL_ENTITLEMENT = "realms/{realm-name}/authz/entitlement/{resource-server-id}" + +# ADMIN URLS +URL_ADMIN_USERS = "admin/realms/{realm-name}/users" +URL_ADMIN_USERS_COUNT = "admin/realms/{realm-name}/users/count" +URL_ADMIN_USER = "admin/realms/{realm-name}/users/{id}" From bd42f20289b6b00a730a514c1afb0c7ef07a60e0 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 5 Sep 2017 11:45:05 -0300 Subject: [PATCH 2/5] Added HTTP Delete. --- keycloak/connection.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/keycloak/connection.py b/keycloak/connection.py index d38d2ab..808edb6 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -162,3 +162,22 @@ class ConnectionManager(object): except Exception as e: raise KeycloakConnectionError( "Can't connect to server (%s)" % e) + + def raw_delete(self, path, **kwargs): + """ Submit delete request to the path. + + :arg + path (str): Path for request. + :return + Response the request. + :exception + HttpError: Can't connect to server. + """ + try: + return requests.delete(urljoin(self.base_url, path), + params=kwargs, + headers=self.headers, + timeout=self.timeout) + except Exception as e: + raise KeycloakConnectionError( + "Can't connect to server (%s)" % e) From 153bedbb71abcb05e4a31bdb73295538effc5daa Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 5 Sep 2017 15:49:26 -0300 Subject: [PATCH 3/5] added send_verify_email, reset_password, send_update_account, server_info, get_sessions, get_clients. --- keycloak/keycloak_admin.py | 141 ++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 10 +++ 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 2c3a9d3..75de538 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -14,7 +14,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from keycloak.urls_patterns import URL_ADMIN_USERS_COUNT, URL_ADMIN_USER +from keycloak.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 from .keycloak_openid import KeycloakOpenID from .exceptions import raise_error_from_response, KeycloakGetError, KeycloakSecretNotFound, \ @@ -111,6 +113,8 @@ class KeycloakAdmin: UserRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + :param payload: UserRepresentation + :return: UserRepresentation """ params_path = {"realm-name": self.realm_name} @@ -132,8 +136,9 @@ class KeycloakAdmin: """ Get representation of the user - UserRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + :param user_id: User id + + UserRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation :return: UserRepresentation """ @@ -141,4 +146,134 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def update_user(self, user_id, payload): + """ + Update the user + + :param user_id: User id + :param payload: UserRepresentation + + :return: Http response + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.connection.raw_put(URL_ADMIN_USER.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_user(self, user_id): + """ + Delete the user + + :param user_id: User id + + :return: Http response + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.connection.raw_delete(URL_ADMIN_USER.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def consents_user(self, user_id): + """ + Get consents granted by the user + + :param user_id: User id + + :return: consents + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.connection.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + 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 + 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: + + :return: + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri} + data_raw = self.connection.raw_put(URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), + data=payload, **params_query) + return raise_error_from_response(data_raw, KeycloakGetError) + + def reset_password(self, user_id, password): + """ + Set up a temporary password for the user User will have to reset the + temporary password next time they log in. + + :param user_id: User id + :param password: A Temporary password + + :return: + """ + 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({'pass': password})) + return raise_error_from_response(data_raw, KeycloakGetError) + + def send_verify_email(self, user_id, client_id=None, redirect_uri=None): + """ + Send a 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: User id + :param client_id: Client id + :param redirect_uri: Redirect uri + + :return: + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + params_query = {"client_id": client_id, "redirect_uri": redirect_uri} + data_raw = self.connection.raw_put(URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), + data={}, **params_query) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_sessions(self, user_id): + """ + Get sessions associated with the user + + :param user_id: User id + + UserSessionRepresentation + http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation + + :return: UserSessionRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.connection.raw_get(URL_ADMIN_GET_SESSIONS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_server_info(self): + """ + Get themes, social providers, auth providers, and event listeners available on this server + + :param user_id: User id + + ServerInfoRepresentation + http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_serverinforepresentation + + :return: ServerInfoRepresentation + """ + data_raw = self.connection.raw_get(URL_ADMIN_SERVER_INFO) + return raise_error_from_response(data_raw, KeycloakGetError) + + 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: 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) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index bfbf563..240eca0 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -28,3 +28,13 @@ URL_ENTITLEMENT = "realms/{realm-name}/authz/entitlement/{resource-server-id}" URL_ADMIN_USERS = "admin/realms/{realm-name}/users" URL_ADMIN_USERS_COUNT = "admin/realms/{realm-name}/users/count" URL_ADMIN_USER = "admin/realms/{realm-name}/users/{id}" +URL_ADMIN_USER_CONSENTS = "admin/realms/{realm-name}/users/{id}/consents" +URL_ADMIN_SEND_UPDATE_ACCOUNT = "admin/realms/{realm-name}/users/{id}/execute-actions-email" +URL_ADMIN_SEND_VERIFY_EMAIL = "admin/realms/{realm-name}/users/{id}/send-verify-email" +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_SERVER_INFO = "admin/serverinfo" + +URL_ADMIN_CLIENTS = "admin/realms/{realm-name}/clients" + + From df7e0e1132080bc5ebbfff52a32a82855e59c7f1 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 5 Sep 2017 16:45:16 -0300 Subject: [PATCH 4/5] Updated docs. --- README.md | 95 ++++++++++++++++++++++++++++++------- docs/source/index.rst | 96 +++++++++++++++++++++++++++++++------- keycloak/keycloak_admin.py | 73 +++++++++++++++++++---------- keycloak/urls_patterns.py | 4 ++ 4 files changed, 212 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 89b7096..064dc0e 100644 --- a/README.md +++ b/README.md @@ -44,49 +44,112 @@ The documentation for python-keycloak is available on [readthedocs](http://pytho ## Usage ```python -from keycloak import Keycloak +from keycloak import KeycloakOpenID # Configure client -keycloak = Keycloak(server_url="http://localhost:8080/auth/", +keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", client_id="example_client", realm_name="example_realm", client_secret_key="secret") # Get WellKnow -config_well_know = keycloak.well_know() +config_well_know = keycloak_openid.well_know() # Get Token -token = keycloak.token("user", "password") +token = keycloak_openid.token("user", "password") # Get Userinfo -userinfo = keycloak.userinfo(token['access_token']) +userinfo = keycloak_openid.userinfo(token['access_token']) # Logout -keycloak.logout(token['refresh_token']) +keycloak_openid.logout(token['refresh_token']) # Get Certs -certs = keycloak.certs() +certs = keycloak_openid.certs() # Get RPT (Entitlement) -token = keycloak.token("user", "password") -rpt = keycloak.entitlement(token['access_token'], "resource_id") +token = keycloak_openid.token("user", "password") +rpt = keycloak_openid.entitlement(token['access_token'], "resource_id") # Instropect RPT -token_rpt_info = keycloak.instropect(keycloak.instropect(token['access_token'], rpt=rpt['rpt'], +token_rpt_info = keycloak_openid.instropect(keycloak_openid.instropect(token['access_token'], rpt=rpt['rpt'], token_type_hint="requesting_party_token")) # Introspect Token -token_info = keycloak.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} -token_info = keycloak.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) +token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) # Get permissions by token -token = keycloak.token("user", "password") -keycloak.load_authorization_config("example-authz-config.json") -policies = keycloak.get_policies(token['access_token'], method_token_info='decode', key=KEYCLOAK_PUBLIC_KEY) -permissions = keycloak.get_permissions(token['access_token'], method_token_info='introspect') +token = keycloak_openid.token("user", "password") +keycloak_openid.load_authorization_config("example-authz-config.json") +policies = keycloak_openid.get_policies(token['access_token'], method_token_info='decode', key=KEYCLOAK_PUBLIC_KEY) +permissions = keycloak_openid.get_permissions(token['access_token'], method_token_info='introspect') + +# KEYCLOAK ADMIN + +from keycloak import KeycloakAdmin + +keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", + username='example-admin', + password='secret', + realm_name="example_realm") + +# Add user +new_user = keycloak_admin.create_user({"email": "example@example.com", + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example", + "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 +user = keycloak_admin.get_user("user-id-keycloak") + +# Update User +response = keycloak_admin.update_user(user_id="user-id-keycloak", + payload={'firstName': 'Example Update'}) + +# Delete User +response = keycloak_admin.delete_user(user_id="user-id-keycloak") + +# Get consents granted by the user +consents = keycloak_admin.consents_user(user_id="user-id-keycloak") + +# Send User Action +response = keycloak_admin.send_update_account(user_id="user-id-keycloak", + payload=json.dumps(['UPDATE_PASSWORD'])) + +# Send Verify Email +response = keycloak_admin.send_verify_email(user_id="user-id-keycloak") + +# Get sessions associated with the user +sessions = keycloak_admin.get_sessions(user_id="user-id-keycloak") + +# Get themes, social providers, auth providers, and event listeners available on this server +server_info = keycloak_admin.get_server_info() + +# Get clients belonging to the realm Returns a list of clients belonging to the realm +clients = keycloak_admin.get_clients() + +# Get representation of the client - id of client (not client-id) +client = keycloak_admin.get_client(client_id='id-client') + +# Get all roles for the client +client_roles = keycloak_admin.get_client_role(client_id='id-client') + + +# Get all roles for the realm or client +realm_roles = keycloak_admin.get_roles() ``` \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 1c6be6c..8abc8d9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -71,48 +71,112 @@ Usage Main methods:: - from keycloak import Keycloak + from keycloak import KeycloakOpenID # Configure client - keycloak = Keycloak(server_url="http://localhost:8080/auth/", + keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", client_id="example_client", realm_name="example_realm", client_secret_key="secret") # Get WellKnow - config_well_know = keycloak.well_know() + config_well_know = keycloak_openid.well_know() # Get Token - token = keycloak.token("user", "password") + token = keycloak_openid.token("user", "password") # Get Userinfo - userinfo = keycloak.userinfo(token['access_token']) + userinfo = keycloak_openid.userinfo(token['access_token']) # Logout - keycloak.logout(token['refresh_token']) + keycloak_openid.logout(token['refresh_token']) # Get Certs - certs = keycloak.certs() + certs = keycloak_openid.certs() # Get RPT (Entitlement) - token = keycloak.token("user", "password") - rpt = keycloak.entitlement(token['access_token'], "resource_id") + token = keycloak_openid.token("user", "password") + rpt = keycloak_openid.entitlement(token['access_token'], "resource_id") # Instropect RPT - token_rpt_info = keycloak.instropect(keycloak.instropect(token['access_token'], rpt=rpt['rpt'], + token_rpt_info = keycloak_openid.instropect(keycloak_openid.instropect(token['access_token'], rpt=rpt['rpt'], token_type_hint="requesting_party_token")) # Introspect Token - token_info = keycloak.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} - token_info = keycloak.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) + token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) # Get permissions by token - token = keycloak.token("user", "password") - keycloak.load_authorization_config("example-authz-config.json") - policies = keycloak.get_policies(token['access_token'], method_token_info='decode', key=KEYCLOAK_PUBLIC_KEY) - permissions = keycloak.get_permissions(token['access_token'], method_token_info='introspect') + token = keycloak_openid.token("user", "password") + keycloak_openid.load_authorization_config("example-authz-config.json") + policies = keycloak_openid.get_policies(token['access_token'], method_token_info='decode', key=KEYCLOAK_PUBLIC_KEY) + permissions = keycloak_openid.get_permissions(token['access_token'], method_token_info='introspect') + + # KEYCLOAK ADMIN + + from keycloak import KeycloakAdmin + + keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", + username='example-admin', + password='secret', + realm_name="example_realm") + + # Add user + new_user = keycloak_admin.create_user({"email": "example@example.com", + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example", + "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 + user = keycloak_admin.get_user("user-id-keycloak") + + # Update User + response = keycloak_admin.update_user(user_id="user-id-keycloak", + payload={'firstName': 'Example Update'}) + + # Delete User + response = keycloak_admin.delete_user(user_id="user-id-keycloak") + + # Get consents granted by the user + consents = keycloak_admin.consents_user(user_id="user-id-keycloak") + + # Send User Action + response = keycloak_admin.send_update_account(user_id="user-id-keycloak", + payload=json.dumps(['UPDATE_PASSWORD'])) + + # Send Verify Email + response = keycloak_admin.send_verify_email(user_id="user-id-keycloak") + + # Get sessions associated with the user + sessions = keycloak_admin.get_sessions(user_id="user-id-keycloak") + + # Get themes, social providers, auth providers, and event listeners available on this server + server_info = keycloak_admin.get_server_info() + + # Get clients belonging to the realm Returns a list of clients belonging to the realm + clients = keycloak_admin.get_clients() + + # Get representation of the client - id of client (not client-id) + client = keycloak_admin.get_client(client_id='id-client') + + # Get all roles for the client + client_roles = keycloak_admin.get_client_role(client_id='id-client') + + + # Get all roles for the realm or client + realm_roles = keycloak_admin.get_roles() + diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 75de538..1f75c6c 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -14,20 +14,19 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from keycloak.urls_patterns import URL_ADMIN_USERS_COUNT, URL_ADMIN_USER, URL_ADMIN_USER_CONSENTS, \ + +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_SERVER_INFO, URL_ADMIN_CLIENTS, URL_ADMIN_CLIENT, URL_ADMIN_CLIENT_ROLES, URL_ADMIN_REALM_ROLES from .keycloak_openid import KeycloakOpenID -from .exceptions import raise_error_from_response, KeycloakGetError, KeycloakSecretNotFound, \ - KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError +from .exceptions import raise_error_from_response, KeycloakGetError from .urls_patterns import ( URL_ADMIN_USERS, ) from .connection import ConnectionManager -from jose import jwt import json @@ -96,7 +95,7 @@ class KeycloakAdmin: def token(self, value): self._token = value - def list_users(self, query=None): + def get_users(self, query=None): """ Get users Returns a list of users, filtered according to query parameters @@ -122,7 +121,7 @@ class KeycloakAdmin: data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) - def count_users(self): + def users_count(self): """ User counter @@ -203,21 +202,6 @@ class KeycloakAdmin: data=payload, **params_query) return raise_error_from_response(data_raw, KeycloakGetError) - def reset_password(self, user_id, password): - """ - Set up a temporary password for the user User will have to reset the - temporary password next time they log in. - - :param user_id: User id - :param password: A Temporary password - - :return: - """ - 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({'pass': password})) - return raise_error_from_response(data_raw, KeycloakGetError) - def send_verify_email(self, user_id, client_id=None, redirect_uri=None): """ Send a update account email to the user An email contains a @@ -254,8 +238,6 @@ class KeycloakAdmin: """ Get themes, social providers, auth providers, and event listeners available on this server - :param user_id: User id - ServerInfoRepresentation http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_serverinforepresentation @@ -277,3 +259,46 @@ class KeycloakAdmin: data_raw = self.connection.raw_get(URL_ADMIN_CLIENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_client(self, client_id): + """ + Get representation of the client + + ClientRepresentation + http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + + :param client_id: id of client (not client-id) + + :return: ClientRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.connection.raw_get(URL_ADMIN_CLIENT.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_client_role(self, client_id): + """ + Get all roles for the client + + RoleRepresentation + http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + + :param client_id: id of client (not client-id) + + :return: RoleRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_roles(self): + """ + Get all roles for the realm or client + + RoleRepresentation + http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + + :return: 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) + diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 240eca0..6ffab2a 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -36,5 +36,9 @@ URL_ADMIN_GET_SESSIONS = "admin/realms/{realm-name}/users/{id}/sessions" URL_ADMIN_SERVER_INFO = "admin/serverinfo" 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_REALM_ROLES = "admin/realms/{realm-name}/roles" From e7f465289cd8f69ffc552b23b389180e1d74005e Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 5 Sep 2017 16:46:58 -0300 Subject: [PATCH 5/5] Updated version. --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b631bc1..b354c21 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.8.2' +version = '0.9.0' # The full version, including alpha/beta/rc tags. -release = '0.8.2' +release = '0.9.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index cb82636..8a06a58 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup setup( name='python-keycloak', - version='0.8.4', + version='0.9.0', url='https://bitbucket.org/agriness/python-keycloak', license='GNU General Public License - V3', author='Marcos Pereira',