diff --git a/CHANGELOG.md b/CHANGELOG.md
index a14d8be..c8891db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,3 @@
-
Changelog
============
@@ -6,7 +5,7 @@ All notable changes to this project will be documented in this file.
## [0.5.0] - 2017-08-21
-* Basic functions for Keycloak API (well_know, token, userinfo, logout, certs,
+* Basic functions for Keycloak API (well_know, token, userinfo, logout, certs,
entitlement, instropect)
## [0.6.0] - 2017-08-23
@@ -39,4 +38,8 @@ entitlement, instropect)
* Add groups functions
* Add Admin Tasks for user and client role management
* Function to trigger user sync from provider
-* Optional parameter: verify
\ No newline at end of file
+
+## [0.12.1] - 2018-08-04
+
+* Add get_idps
+* Rework group functions
diff --git a/README.md b/README.md
index ec9b7cd..6a8c9cc 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,9 @@ The documentation for python-keycloak is available on [readthedocs](http://pytho
* [Marcos Pereira](marcospereira.mpj@gmail.com)
* [Martin Devlin](martin.devlin@pearson.com)
* [Shon T. Urbas](shon.urbas@gmail.com>)
+* [Markus Spanier]()
+* [Remco Kranenburg]()
+* [Remco Kranenburg]()
## Usage
@@ -203,8 +206,21 @@ groups = keycloak_admin.get_groups()
group = keycloak_admin.get_group(group_id='group_id')
# Get group by name
-group = keycloak_admin.get_group_by_name(name_or_path='group_id', search_in_subgroups=True)
+group = keycloak_admin.get_group_by_path(path='/group/subgroup', search_in_subgroups=True)
# Function to trigger user sync from provider
sync_users(storage_id="storage_di", action="action")
+
+# Get client role id from name
+role_id = keycloak_admin.get_client_role_id(client_id=client_id, role_name="test")
+
+# Get all roles for the realm or client
+realm_roles = keycloak_admin.get_roles()
+
+# Assign client role to user. Note that BOTH role_name and role_id appear to be required.
+keycloak_admin.assign_client_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test")
+
+# Get all ID Providers
+idps = keycloak_admin.get_idps()
+
```
diff --git a/docs/source/conf.py b/docs/source/conf.py
index ef6b6fe..654da5b 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -60,9 +60,9 @@ author = 'Marcos Pereira'
# built documents.
#
# The short X.Y version.
-version = '0.12.0'
+version = '0.12.1'
# The full version, including alpha/beta/rc tags.
-release = '0.12.0'
+release = '0.12.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/docs/source/index.rst b/docs/source/index.rst
index eea1c71..87a72cc 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -43,6 +43,7 @@ python-keycloak depends on:
* Python 3
* `requests `_
* `python-jose `_
+* `simplejson `_
Tests Dependencies
------------------
@@ -81,7 +82,8 @@ Main methods::
keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/",
client_id="example_client",
realm_name="example_realm",
- client_secret_key="secret")
+ client_secret_key="secret",
+ verify=True)
# Get WellKnow
config_well_know = keycloak_openid.well_know()
@@ -216,6 +218,12 @@ Main methods::
# Create client role
keycloak_admin.create_client_role(client_id, "test")
+ # Get client role id from name
+ role_id = keycloak_admin.get_client_role_id(client_id=client_id, role_name="test")
+
+ # Get all roles for the realm or client
+ realm_roles = keycloak_admin.get_roles()
+
# Assign client role to user. Note that BOTH role_name and role_id appear to be required.
keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test")
@@ -228,8 +236,8 @@ Main methods::
# Get group
group = keycloak_admin.get_group(group_id='group_id')
- # Get group by name
- group = keycloak_admin.get_group_by_name(name_or_path='group_id', search_in_subgroups=True)
+ # Get group by path
+ group = keycloak_admin.get_group_by_path(path='/group/subgroup', search_in_subgroups=True)
# Function to trigger user sync from provider
sync_users(storage_id="storage_di", action="action")
diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py
index 27d8b14..300c5f7 100644
--- a/keycloak/exceptions.py
+++ b/keycloak/exceptions.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
import requests
+from simplejson import JSONDecodeError
class KeycloakError(Exception):
@@ -67,16 +68,20 @@ class KeycloakInvalidTokenError(KeycloakOperationError):
pass
-def raise_error_from_response(response, error, expected_code=200):
+def raise_error_from_response(response, error, expected_code=200, skip_exists=False):
if expected_code == response.status_code:
if expected_code == requests.codes.no_content:
return {}
+
try:
return response.json()
- except ValueError:
+ except JSONDecodeError as e:
return response.content
+ if skip_exists and response.status_code == 409:
+ return {"Already exists"}
+
try:
message = response.json()['message']
except (KeyError, ValueError):
diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py
index 17e2c12..8f2dbc2 100644
--- a/keycloak/keycloak_admin.py
+++ b/keycloak/keycloak_admin.py
@@ -123,6 +123,19 @@ class KeycloakAdmin:
data_raw = self.connection.raw_get(URL_ADMIN_USERS.format(**params_path), **query)
return raise_error_from_response(data_raw, KeycloakGetError)
+ def get_idps(self):
+ """
+ Returns a list of ID Providers,
+
+ IdentityProviderRepresentation
+ https://www.keycloak.org/docs-api/3.3/rest-api/index.html#_identityproviderrepresentation
+
+ :return: array IdentityProviderRepresentation
+ """
+ params_path = {"realm-name": self.realm_name}
+ data_raw = self.connection.raw_get(URL_ADMIN_IDPS.format(**params_path))
+ return raise_error_from_response(data_raw, KeycloakGetError)
+
def create_user(self, payload):
"""
Create a new user Username must be unique
@@ -135,6 +148,12 @@ class KeycloakAdmin:
:return: UserRepresentation
"""
params_path = {"realm-name": self.realm_name}
+
+ exists = self.get_user_id(username=payload['username'])
+
+ if exists is not None:
+ return str(exists)
+
data_raw = self.connection.raw_post(URL_ADMIN_USERS.format(**params_path),
data=json.dumps(payload))
return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
@@ -333,7 +352,28 @@ class KeycloakAdmin:
data_raw = self.connection.raw_get(URL_ADMIN_GROUP.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
- def get_group_by_name(self, name_or_path, search_in_subgroups=False):
+ def get_subgroups(self, group, path):
+ """
+ Utility function to iterate through nested group structures
+
+ GroupRepresentation
+ http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
+
+ :param name: group (GroupRepresentation)
+ :param path: group path (string)
+
+ :return: Keycloak server response (GroupRepresentation)
+ """
+
+ for subgroup in group["subGroups"]:
+ if subgroup['path'] == path:
+ return subgroup
+ elif subgroup["subGroups"]:
+ for subgroup in group["subGroups"]:
+ return self.get_subgroups(subgroup, path)
+ return None
+
+ def get_group_by_path(self, path, search_in_subgroups=False):
"""
Get group id based on name or path.
A straight name or path match with a top-level group will return first.
@@ -342,7 +382,6 @@ class KeycloakAdmin:
GroupRepresentation
http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
- :param name: group name
:param path: group path
:param search_in_subgroups: True if want search in the subgroups
:return: Keycloak server response (GroupRepresentation)
@@ -352,48 +391,48 @@ class KeycloakAdmin:
# TODO: Review this code is necessary
for group in groups:
- if group['name'] == name_or_path or group['path'] == name_or_path:
+ if group['path'] == path:
return group
elif search_in_subgroups and group["subGroups"]:
- for subgroup in group["subGroups"]:
- if subgroup['name'] == name_or_path or subgroup['path'] == name_or_path:
- return subgroup
-
+ res = self.get_subgroups(group, path)
+ if res != None:
+ return res
return None
- def create_group(self, name=None, client_roles={}, realm_roles=[], sub_groups=[], path=None, parent=None):
+ def create_group(self, payload, parent=None, skip_exists=False):
"""
- Create a group in the Realm
+ Creates a group in the Realm
+
+ :param payload: GroupRepresentation
+ :param parent: parent group's id. Required to create a sub-group.
GroupRepresentation
http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
- :param name: group name
- :param client_roles: (Dict) Client roles to include in groupp # Not demonstrated to work
- :param realm_roles: (List) Realm roles to include in group # Not demonstrated to work
- :param sub_groups: (List) Subgroups to include in groupp # Not demonstrated to work
- :param path: group path
- :param parent: parent group's id. Required to create a sub-group.
-
- :return: Keycloak server response (GroupRepresentation)
+ :return: Http response
"""
+ name = payload['name']
+ path = payload['path']
+ exists = None
+
+ if name is None and path is not None:
+ path="/" + name
+
+ elif path is not None:
+ exists = self.get_group_by_path(path=path, search_in_subgroups=True)
- data = {"name": name or path,
- "path": path,
- "clientRoles": client_roles,
- "realmRoles": realm_roles,
- "subGroups": sub_groups}
+ if exists is not None:
+ return str(exists)
if parent is None:
- params_path = {"realm-name": self.realm_name}
- data_raw = self.connection.raw_post(URL_ADMIN_GROUPS.format(**params_path),
- data=json.dumps(data))
+ params_path = {"realm-name": self.realm_name}
+ data_raw = self.connection.raw_post(URL_ADMIN_GROUPS.format(**params_path),
+ data=json.dumps(payload))
else:
- params_path = {"realm-name": self.realm_name, "id": parent}
- data_raw = self.connection.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path),
- data=json.dumps(data))
-
- return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
+ params_path = {"realm-name": self.realm_name, "id": parent,}
+ data_raw = self.connection.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path),
+ data=json.dumps(payload))
+ return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists)
def group_set_permissions(self, group_id, enabled=True):
"""
@@ -496,7 +535,7 @@ class KeycloakAdmin:
return None
- def create_client(self, payload):
+ def create_client(self, payload, skip_exists=False):
"""
Create a client
@@ -509,7 +548,7 @@ class KeycloakAdmin:
params_path = {"realm-name": self.realm_name}
data_raw = self.connection.raw_post(URL_ADMIN_CLIENTS.format(**params_path),
data=json.dumps(payload))
- return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
+ return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists)
def delete_client(self, client_id):
"""
@@ -591,7 +630,7 @@ class KeycloakAdmin:
role = self.get_client_role(client_id, role_name)
return role.get("id")
- def create_client_role(self, payload):
+ def create_client_role(self, payload, skip_exists=False):
"""
Create a client role
@@ -605,7 +644,7 @@ class KeycloakAdmin:
params_path = {"realm-name": self.realm_name, "id": self.client_id}
data_raw = self.connection.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path),
data=json.dumps(payload))
- return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
+ return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists)
def delete_client_role(self, role_name):
"""
diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py
index ce593da..5897e10 100644
--- a/keycloak/urls_patterns.py
+++ b/keycloak/urls_patterns.py
@@ -35,6 +35,7 @@ URL_ADMIN_RESET_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password"
URL_ADMIN_GET_SESSIONS = "admin/realms/{realm-name}/users/{id}/sessions"
URL_ADMIN_USER_CLIENT_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}"
URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}"
+URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password"
URL_ADMIN_SERVER_INFO = "admin/serverinfo"
@@ -51,3 +52,5 @@ URL_ADMIN_CLIENT_ROLE = "admin/realms/{realm-name}/clients/{id}/roles/{role-name
URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles"
URL_ADMIN_USER_STORAGE = "admin/realms/{realm-name}/user-storage/{id}/sync"
+
+URL_ADMIN_IDPS = "admin/realms/{realm}/identity-provider/instances"
diff --git a/requirements.txt b/requirements.txt
index 70d3ca7..f03af1b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
requests>=2.18.4
httmock>=1.2.5
python-jose>=1.4.0
+simplejson
diff --git a/setup.py b/setup.py
index e3c8d2d..03ac4f1 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ from setuptools import setup
setup(
name='python-keycloak',
- version='0.11.1',
+ version='0.12.1',
url='https://bitbucket.org/agriness/python-keycloak',
license='GNU General Public License - V3',
author='Marcos Pereira',
@@ -12,7 +12,7 @@ setup(
keywords='keycloak openid',
description=u'python-keycloak is a Python package providing access to the Keycloak API.',
packages=['keycloak', 'keycloak.authorization', 'keycloak.tests'],
- install_requires=['requests==2.18.4', 'httmock==1.2.5', 'python-jose==1.4.0'],
+ install_requires=['requests==2.18.4', 'httmock==1.2.5', 'python-jose==1.4.0', 'simplejson'],
classifiers=[
'Programming Language :: Python :: 3',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',