You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1196 lines
44 KiB

7 years ago
6 years ago
6 years ago
6 years ago
7 years ago
6 years ago
7 years ago
7 years ago
7 years ago
6 years ago
6 years ago
6 years ago
7 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
  1. # -*- coding: utf-8 -*-
  2. #
  3. # The MIT License (MIT)
  4. #
  5. # Copyright (C) 2017 Marcos Pereira <marcospereira.mpj@gmail.com>
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy of
  8. # this software and associated documentation files (the "Software"), to deal in
  9. # the Software without restriction, including without limitation the rights to
  10. # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  11. # the Software, and to permit persons to whom the Software is furnished to do so,
  12. # subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in all
  15. # copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  19. # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  20. # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  21. # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  22. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. # Unless otherwise stated in the comments, "id", in e.g. user_id, refers to the
  24. # internal Keycloak server ID, usually a uuid string
  25. import json
  26. from builtins import isinstance
  27. from typing import List, Iterable
  28. from .connection import ConnectionManager
  29. from .exceptions import raise_error_from_response, KeycloakGetError
  30. from .keycloak_openid import KeycloakOpenID
  31. from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \
  32. URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, \
  33. URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, URL_ADMIN_USER_GROUP, URL_ADMIN_REALM_ROLES, URL_ADMIN_GROUP_CHILD, \
  34. URL_ADMIN_USER_CONSENTS, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_CLIENT, URL_ADMIN_USER, URL_ADMIN_CLIENT_ROLE, \
  35. URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \
  36. URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \
  37. URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \
  38. URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \
  39. URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \
  40. URL_ADMIN_USER_REALM_ROLES
  41. class KeycloakAdmin:
  42. PAGE_SIZE = 100
  43. _server_url = None
  44. _username = None
  45. _password = None
  46. _realm_name = None
  47. _client_id = None
  48. _verify = None
  49. _client_secret_key = None
  50. _auto_refresh_token = None
  51. _connection = None
  52. _token = None
  53. _custom_headers = None
  54. _user_realm_name = None
  55. def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True,
  56. client_secret_key=None, custom_headers=None, user_realm_name=None, auto_refresh_token=None):
  57. """
  58. :param server_url: Keycloak server url
  59. :param username: admin username
  60. :param password: admin password
  61. :param realm_name: realm name
  62. :param client_id: client id
  63. :param verify: True if want check connection SSL
  64. :param client_secret_key: client secret key
  65. :param custom_headers: dict of custom header to pass to each HTML request
  66. :param auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete']
  67. """
  68. self.server_url = server_url
  69. self.username = username
  70. self.password = password
  71. self.realm_name = realm_name
  72. self.client_id = client_id
  73. self.verify = verify
  74. self.client_secret_key = client_secret_key
  75. self.auto_refresh_token = auto_refresh_token or []
  76. self.user_realm_name = user_realm_name
  77. self.custom_headers = custom_headers
  78. # Get token Admin
  79. self.get_token()
  80. @property
  81. def server_url(self):
  82. return self._server_url
  83. @server_url.setter
  84. def server_url(self, value):
  85. self._server_url = value
  86. @property
  87. def realm_name(self):
  88. return self._realm_name
  89. @realm_name.setter
  90. def realm_name(self, value):
  91. self._realm_name = value
  92. @property
  93. def connection(self):
  94. return self._connection
  95. @connection.setter
  96. def connection(self, value):
  97. self._connection = value
  98. @property
  99. def client_id(self):
  100. return self._client_id
  101. @client_id.setter
  102. def client_id(self, value):
  103. self._client_id = value
  104. @property
  105. def client_secret_key(self):
  106. return self._client_secret_key
  107. @client_secret_key.setter
  108. def client_secret_key(self, value):
  109. self._client_secret_key = value
  110. @property
  111. def verify(self):
  112. return self._verify
  113. @verify.setter
  114. def verify(self, value):
  115. self._verify = value
  116. @property
  117. def username(self):
  118. return self._username
  119. @username.setter
  120. def username(self, value):
  121. self._username = value
  122. @property
  123. def password(self):
  124. return self._password
  125. @password.setter
  126. def password(self, value):
  127. self._password = value
  128. @property
  129. def token(self):
  130. return self._token
  131. @token.setter
  132. def token(self, value):
  133. self._token = value
  134. @property
  135. def auto_refresh_token(self):
  136. return self._auto_refresh_token
  137. @property
  138. def user_realm_name(self):
  139. return self._user_realm_name
  140. @user_realm_name.setter
  141. def user_realm_name(self, value):
  142. self._user_realm_name = value
  143. @property
  144. def custom_headers(self):
  145. return self._custom_headers
  146. @custom_headers.setter
  147. def custom_headers(self, value):
  148. self._custom_headers = value
  149. @auto_refresh_token.setter
  150. def auto_refresh_token(self, value):
  151. allowed_methods = {'get', 'post', 'put', 'delete'}
  152. if not isinstance(value, Iterable):
  153. raise TypeError('Expected a list of strings among {allowed}'.format(allowed=allowed_methods))
  154. if not all(method in allowed_methods for method in value):
  155. raise TypeError('Unexpected method in auto_refresh_token, accepted methods are {allowed}'.format(allowed=allowed_methods))
  156. self._auto_refresh_token = value
  157. def __fetch_all(self, url, query=None):
  158. '''Wrapper function to paginate GET requests
  159. :param url: The url on which the query is executed
  160. :param query: Existing query parameters (optional)
  161. :return: Combined results of paginated queries
  162. '''
  163. results = []
  164. # initalize query if it was called with None
  165. if not query:
  166. query = {}
  167. page = 0
  168. query['max'] = self.PAGE_SIZE
  169. # fetch until we can
  170. while True:
  171. query['first'] = page*self.PAGE_SIZE
  172. partial_results = raise_error_from_response(
  173. self.raw_get(url, **query),
  174. KeycloakGetError)
  175. if not partial_results:
  176. break
  177. results.extend(partial_results)
  178. page += 1
  179. return results
  180. def import_realm(self, payload):
  181. """
  182. Import a new realm from a RealmRepresentation. Realm name must be unique.
  183. RealmRepresentation
  184. https://www.keycloak.org/docs-api/4.4/rest-api/index.html#_realmrepresentation
  185. :param payload: RealmRepresentation
  186. :return: RealmRepresentation
  187. """
  188. data_raw = self.raw_post(URL_ADMIN_REALMS,
  189. data=json.dumps(payload))
  190. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
  191. def get_realms(self):
  192. """
  193. Lists all realms in Keycloak deployment
  194. :return: realms list
  195. """
  196. data_raw = self.raw_get(URL_ADMIN_REALMS)
  197. return raise_error_from_response(data_raw, KeycloakGetError)
  198. def create_realm(self, payload, skip_exists=False):
  199. """
  200. Create a realm
  201. ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_realmrepresentation
  202. :param skip_exists: Skip if Realm already exist.
  203. :param payload: RealmRepresentation
  204. :return: Keycloak server response (RealmRepresentation)
  205. """
  206. data_raw = self.raw_post(URL_ADMIN_REALMS,
  207. data=json.dumps(payload))
  208. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists)
  209. def get_users(self, query=None):
  210. """
  211. Get users Returns a list of users, filtered according to query parameters
  212. :return: users list
  213. """
  214. params_path = {"realm-name": self.realm_name}
  215. return self.__fetch_all(URL_ADMIN_USERS.format(**params_path), query)
  216. def get_idps(self):
  217. """
  218. Returns a list of ID Providers,
  219. IdentityProviderRepresentation
  220. https://www.keycloak.org/docs-api/3.3/rest-api/index.html#_identityproviderrepresentation
  221. :return: array IdentityProviderRepresentation
  222. """
  223. params_path = {"realm-name": self.realm_name}
  224. data_raw = self.raw_get(URL_ADMIN_IDPS.format(**params_path))
  225. return raise_error_from_response(data_raw, KeycloakGetError)
  226. def create_user(self, payload):
  227. """
  228. Create a new user Username must be unique
  229. UserRepresentation
  230. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation
  231. :param payload: UserRepresentation
  232. :return: UserRepresentation
  233. """
  234. params_path = {"realm-name": self.realm_name}
  235. exists = self.get_user_id(username=payload['username'])
  236. if exists is not None:
  237. return str(exists)
  238. data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path),
  239. data=json.dumps(payload))
  240. raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
  241. _last_slash_idx = data_raw.headers['Location'].rindex('/')
  242. return data_raw.headers['Location'][_last_slash_idx + 1:]
  243. def users_count(self):
  244. """
  245. User counter
  246. :return: counter
  247. """
  248. params_path = {"realm-name": self.realm_name}
  249. data_raw = self.raw_get(URL_ADMIN_USERS_COUNT.format(**params_path))
  250. return raise_error_from_response(data_raw, KeycloakGetError)
  251. def get_user_id(self, username):
  252. """
  253. Get internal keycloak user id from username
  254. This is required for further actions against this user.
  255. UserRepresentation
  256. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation
  257. :param username: id in UserRepresentation
  258. :return: user_id
  259. """
  260. users = self.get_users(query={"search": username})
  261. return next((user["id"] for user in users if user["username"] == username), None)
  262. def get_user(self, user_id):
  263. """
  264. Get representation of the user
  265. :param user_id: User id
  266. UserRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation
  267. :return: UserRepresentation
  268. """
  269. params_path = {"realm-name": self.realm_name, "id": user_id}
  270. data_raw = self.raw_get(URL_ADMIN_USER.format(**params_path))
  271. return raise_error_from_response(data_raw, KeycloakGetError)
  272. def get_user_groups(self, user_id):
  273. """
  274. Get user groups Returns a list of groups of which the user is a member
  275. :param user_id: User id
  276. :return: user groups list
  277. """
  278. params_path = {"realm-name": self.realm_name, "id": user_id}
  279. data_raw = self.raw_get(URL_ADMIN_USER_GROUPS.format(**params_path))
  280. return raise_error_from_response(data_raw, KeycloakGetError)
  281. def update_user(self, user_id, payload):
  282. """
  283. Update the user
  284. :param user_id: User id
  285. :param payload: UserRepresentation
  286. :return: Http response
  287. """
  288. params_path = {"realm-name": self.realm_name, "id": user_id}
  289. data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path),
  290. data=json.dumps(payload))
  291. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  292. def delete_user(self, user_id):
  293. """
  294. Delete the user
  295. :param user_id: User id
  296. :return: Http response
  297. """
  298. params_path = {"realm-name": self.realm_name, "id": user_id}
  299. data_raw = self.raw_delete(URL_ADMIN_USER.format(**params_path))
  300. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  301. def set_user_password(self, user_id, password, temporary=True):
  302. """
  303. Set up a password for the user. If temporary is True, the user will have to reset
  304. the temporary password next time they log in.
  305. http://www.keycloak.org/docs-api/3.2/rest-api/#_users_resource
  306. http://www.keycloak.org/docs-api/3.2/rest-api/#_credentialrepresentation
  307. :param user_id: User id
  308. :param password: New password
  309. :param temporary: True if password is temporary
  310. :return:
  311. """
  312. payload = {"type": "password", "temporary": temporary, "value": password}
  313. params_path = {"realm-name": self.realm_name, "id": user_id}
  314. data_raw = self.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path),
  315. data=json.dumps(payload))
  316. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  317. def consents_user(self, user_id):
  318. """
  319. Get consents granted by the user
  320. :param user_id: User id
  321. :return: consents
  322. """
  323. params_path = {"realm-name": self.realm_name, "id": user_id}
  324. data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path))
  325. return raise_error_from_response(data_raw, KeycloakGetError)
  326. def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None):
  327. """
  328. Send a update account email to the user An email contains a
  329. link the user can click to perform a set of required actions.
  330. :param user_id:
  331. :param payload:
  332. :param client_id:
  333. :param lifespan:
  334. :param redirect_uri:
  335. :return:
  336. """
  337. params_path = {"realm-name": self.realm_name, "id": user_id}
  338. params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri}
  339. data_raw = self.raw_put(URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path),
  340. data=payload, **params_query)
  341. return raise_error_from_response(data_raw, KeycloakGetError)
  342. def send_verify_email(self, user_id, client_id=None, redirect_uri=None):
  343. """
  344. Send a update account email to the user An email contains a
  345. link the user can click to perform a set of required actions.
  346. :param user_id: User id
  347. :param client_id: Client id
  348. :param redirect_uri: Redirect uri
  349. :return:
  350. """
  351. params_path = {"realm-name": self.realm_name, "id": user_id}
  352. params_query = {"client_id": client_id, "redirect_uri": redirect_uri}
  353. data_raw = self.raw_put(URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path),
  354. data={}, **params_query)
  355. return raise_error_from_response(data_raw, KeycloakGetError)
  356. def get_sessions(self, user_id):
  357. """
  358. Get sessions associated with the user
  359. :param user_id: id of user
  360. UserSessionRepresentation
  361. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation
  362. :return: UserSessionRepresentation
  363. """
  364. params_path = {"realm-name": self.realm_name, "id": user_id}
  365. data_raw = self.raw_get(URL_ADMIN_GET_SESSIONS.format(**params_path))
  366. return raise_error_from_response(data_raw, KeycloakGetError)
  367. def get_server_info(self):
  368. """
  369. Get themes, social providers, auth providers, and event listeners available on this server
  370. ServerInfoRepresentation
  371. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_serverinforepresentation
  372. :return: ServerInfoRepresentation
  373. """
  374. data_raw = self.raw_get(URL_ADMIN_SERVER_INFO)
  375. return raise_error_from_response(data_raw, KeycloakGetError)
  376. def get_groups(self):
  377. """
  378. Get groups belonging to the realm. Returns a list of groups belonging to the realm
  379. GroupRepresentation
  380. http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
  381. :return: array GroupRepresentation
  382. """
  383. params_path = {"realm-name": self.realm_name}
  384. return self.__fetch_all(URL_ADMIN_GROUPS.format(**params_path))
  385. def get_group(self, group_id):
  386. """
  387. Get group by id. Returns full group details
  388. GroupRepresentation
  389. http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
  390. :return: Keycloak server response (GroupRepresentation)
  391. """
  392. params_path = {"realm-name": self.realm_name, "id": group_id}
  393. data_raw = self.raw_get(URL_ADMIN_GROUP.format(**params_path))
  394. return raise_error_from_response(data_raw, KeycloakGetError)
  395. def get_subgroups(self, group, path):
  396. """
  397. Utility function to iterate through nested group structures
  398. GroupRepresentation
  399. http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
  400. :param name: group (GroupRepresentation)
  401. :param path: group path (string)
  402. :return: Keycloak server response (GroupRepresentation)
  403. """
  404. for subgroup in group["subGroups"]:
  405. if subgroup['path'] == path:
  406. return subgroup
  407. elif subgroup["subGroups"]:
  408. for subgroup in group["subGroups"]:
  409. result = self.get_subgroups(subgroup, path)
  410. if result:
  411. return result
  412. # went through the tree without hits
  413. return None
  414. def get_group_members(self, group_id, **query):
  415. """
  416. Get members by group id. Returns group members
  417. GroupRepresentation
  418. http://www.keycloak.org/docs-api/3.2/rest-api/#_userrepresentation
  419. :return: Keycloak server response (UserRepresentation)
  420. """
  421. params_path = {"realm-name": self.realm_name, "id": group_id}
  422. return self.__fetch_all(URL_ADMIN_GROUP_MEMBERS.format(**params_path), query)
  423. def get_group_by_path(self, path, search_in_subgroups=False):
  424. """
  425. Get group id based on name or path.
  426. A straight name or path match with a top-level group will return first.
  427. Subgroups are traversed, the first to match path (or name with path) is returned.
  428. GroupRepresentation
  429. http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
  430. :param path: group path
  431. :param search_in_subgroups: True if want search in the subgroups
  432. :return: Keycloak server response (GroupRepresentation)
  433. """
  434. groups = self.get_groups()
  435. # TODO: Review this code is necessary
  436. for group in groups:
  437. if group['path'] == path:
  438. return group
  439. elif search_in_subgroups and group["subGroups"]:
  440. for group in group["subGroups"]:
  441. if group['path'] == path:
  442. return group
  443. res = self.get_subgroups(group, path)
  444. if res != None:
  445. return res
  446. return None
  447. def create_group(self, payload, parent=None, skip_exists=False):
  448. """
  449. Creates a group in the Realm
  450. :param payload: GroupRepresentation
  451. :param parent: parent group's id. Required to create a sub-group.
  452. GroupRepresentation
  453. http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
  454. :return: Http response
  455. """
  456. if parent is None:
  457. params_path = {"realm-name": self.realm_name}
  458. data_raw = self.raw_post(URL_ADMIN_GROUPS.format(**params_path),
  459. data=json.dumps(payload))
  460. else:
  461. params_path = {"realm-name": self.realm_name, "id": parent, }
  462. data_raw = self.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path),
  463. data=json.dumps(payload))
  464. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists)
  465. def update_group(self, group_id, payload):
  466. """
  467. Update group, ignores subgroups.
  468. :param group_id: id of group
  469. :param payload: GroupRepresentation with updated information.
  470. GroupRepresentation
  471. http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
  472. :return: Http response
  473. """
  474. params_path = {"realm-name": self.realm_name, "id": group_id}
  475. data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path),
  476. data=json.dumps(payload))
  477. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  478. def group_set_permissions(self, group_id, enabled=True):
  479. """
  480. Enable/Disable permissions for a group. Cannot delete group if disabled
  481. :param group_id: id of group
  482. :param enabled: boolean
  483. :return: Keycloak server response
  484. """
  485. params_path = {"realm-name": self.realm_name, "id": group_id}
  486. data_raw = self.raw_put(URL_ADMIN_GROUP_PERMISSIONS.format(**params_path),
  487. data=json.dumps({"enabled": enabled}))
  488. return raise_error_from_response(data_raw, KeycloakGetError)
  489. def group_user_add(self, user_id, group_id):
  490. """
  491. Add user to group (user_id and group_id)
  492. :param group_id: id of group
  493. :param user_id: id of user
  494. :param group_id: id of group to add to
  495. :return: Keycloak server response
  496. """
  497. params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id}
  498. data_raw = self.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None)
  499. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  500. def group_user_remove(self, user_id, group_id):
  501. """
  502. Remove user from group (user_id and group_id)
  503. :param group_id: id of group
  504. :param user_id: id of user
  505. :param group_id: id of group to add to
  506. :return: Keycloak server response
  507. """
  508. params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id}
  509. data_raw = self.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path))
  510. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  511. def delete_group(self, group_id):
  512. """
  513. Deletes a group in the Realm
  514. :param group_id: id of group to delete
  515. :return: Keycloak server response
  516. """
  517. params_path = {"realm-name": self.realm_name, "id": group_id}
  518. data_raw = self.raw_delete(URL_ADMIN_GROUP.format(**params_path))
  519. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  520. def get_clients(self):
  521. """
  522. Get clients belonging to the realm Returns a list of clients belonging to the realm
  523. ClientRepresentation
  524. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  525. :return: Keycloak server response (ClientRepresentation)
  526. """
  527. params_path = {"realm-name": self.realm_name}
  528. data_raw = self.raw_get(URL_ADMIN_CLIENTS.format(**params_path))
  529. return raise_error_from_response(data_raw, KeycloakGetError)
  530. def get_client(self, client_id):
  531. """
  532. Get representation of the client
  533. ClientRepresentation
  534. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  535. :param client_id: id of client (not client-id)
  536. :return: Keycloak server response (ClientRepresentation)
  537. """
  538. params_path = {"realm-name": self.realm_name, "id": client_id}
  539. data_raw = self.raw_get(URL_ADMIN_CLIENT.format(**params_path))
  540. return raise_error_from_response(data_raw, KeycloakGetError)
  541. def get_client_id(self, client_name):
  542. """
  543. Get internal keycloak client id from client-id.
  544. This is required for further actions against this client.
  545. :param client_name: name in ClientRepresentation
  546. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  547. :return: client_id (uuid as string)
  548. """
  549. clients = self.get_clients()
  550. for client in clients:
  551. if client_name == client.get('name') or client_name == client.get('clientId'):
  552. return client["id"]
  553. return None
  554. def get_client_authz_settings(self, client_id):
  555. """
  556. Get authorization json from client.
  557. :param client_id: id in ClientRepresentation
  558. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  559. :return: Keycloak server response
  560. """
  561. params_path = {"realm-name": self.realm_name, "id": client_id}
  562. data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path))
  563. return data_raw
  564. def get_client_authz_resources(self, client_id):
  565. """
  566. Get resources from client.
  567. :param client_id: id in ClientRepresentation
  568. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  569. :return: Keycloak server response
  570. """
  571. params_path = {"realm-name": self.realm_name, "id": client_id}
  572. data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path))
  573. return data_raw
  574. def create_client(self, payload, skip_exists=False):
  575. """
  576. Create a client
  577. ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  578. :param skip_exists: Skip if client already exist.
  579. :param payload: ClientRepresentation
  580. :return: Keycloak server response (UserRepresentation)
  581. """
  582. params_path = {"realm-name": self.realm_name}
  583. data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path),
  584. data=json.dumps(payload))
  585. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists)
  586. def update_client(self, client_id, payload):
  587. """
  588. Update a client
  589. :param client_id: Client id
  590. :param payload: ClientRepresentation
  591. :return: Http response
  592. """
  593. params_path = {"realm-name": self.realm_name, "id": client_id}
  594. data_raw = self.connection.raw_put(URL_ADMIN_CLIENT.format(**params_path),
  595. data=json.dumps(payload))
  596. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  597. def delete_client(self, client_id):
  598. """
  599. Get representation of the client
  600. ClientRepresentation
  601. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  602. :param client_id: keycloak client id (not oauth client-id)
  603. :return: Keycloak server response (ClientRepresentation)
  604. """
  605. params_path = {"realm-name": self.realm_name, "id": client_id}
  606. data_raw = self.raw_delete(URL_ADMIN_CLIENT.format(**params_path))
  607. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  608. def get_realm_roles(self):
  609. """
  610. Get all roles for the realm or client
  611. RoleRepresentation
  612. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  613. :return: Keycloak server response (RoleRepresentation)
  614. """
  615. params_path = {"realm-name": self.realm_name}
  616. data_raw = self.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path))
  617. return raise_error_from_response(data_raw, KeycloakGetError)
  618. def get_client_roles(self, client_id):
  619. """
  620. Get all roles for the client
  621. :param client_id: id of client (not client-id)
  622. RoleRepresentation
  623. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  624. :return: Keycloak server response (RoleRepresentation)
  625. """
  626. params_path = {"realm-name": self.realm_name, "id": client_id}
  627. data_raw = self.raw_get(URL_ADMIN_CLIENT_ROLES.format(**params_path))
  628. return raise_error_from_response(data_raw, KeycloakGetError)
  629. def get_client_role(self, client_id, role_name):
  630. """
  631. Get client role id by name
  632. This is required for further actions with this role.
  633. :param client_id: id of client (not client-id)
  634. :param role_name: roles name (not id!)
  635. RoleRepresentation
  636. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  637. :return: role_id
  638. """
  639. params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name}
  640. data_raw = self.raw_get(URL_ADMIN_CLIENT_ROLE.format(**params_path))
  641. return raise_error_from_response(data_raw, KeycloakGetError)
  642. def get_client_role_id(self, client_id, role_name):
  643. """
  644. Warning: Deprecated
  645. Get client role id by name
  646. This is required for further actions with this role.
  647. :param client_id: id of client (not client-id)
  648. :param role_name: roles name (not id!)
  649. RoleRepresentation
  650. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  651. :return: role_id
  652. """
  653. role = self.get_client_role(client_id, role_name)
  654. return role.get("id")
  655. def create_client_role(self, client_role_id, payload, skip_exists=False):
  656. """
  657. Create a client role
  658. RoleRepresentation
  659. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  660. :param client_role_id: id of client (not client-id)
  661. :param payload: RoleRepresentation
  662. :return: Keycloak server response (RoleRepresentation)
  663. """
  664. params_path = {"realm-name": self.realm_name, "id": client_role_id}
  665. data_raw = self.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path),
  666. data=json.dumps(payload))
  667. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists)
  668. def delete_client_role(self, client_role_id, role_name):
  669. """
  670. Create a client role
  671. RoleRepresentation
  672. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  673. :param client_role_id: id of client (not client-id)
  674. :param role_name: roles name (not id!)
  675. """
  676. params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name}
  677. data_raw = self.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path))
  678. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  679. def assign_client_role(self, user_id, client_id, roles):
  680. """
  681. Assign a client role to a user
  682. :param client_id: id of client (not client-id)
  683. :param user_id: id of user
  684. :param client_id: id of client containing role,
  685. :param roles: roles list or role (use RoleRepresentation)
  686. :return Keycloak server response
  687. """
  688. payload = roles if isinstance(roles, list) else [roles]
  689. params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id}
  690. data_raw = self.raw_post(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path),
  691. data=json.dumps(payload))
  692. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  693. def create_realm_role(self, payload, skip_exists=False):
  694. """
  695. Create a new role for the realm or client
  696. :param realm: realm name (not id)
  697. :param rep: RoleRepresentation https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_rolerepresentation
  698. :return Keycloak server response
  699. """
  700. params_path = {"realm-name": self.realm_name}
  701. data_raw = self.connection.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path),
  702. data=json.dumps(payload))
  703. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists)
  704. def assign_realm_roles(self, user_id, client_id, roles):
  705. """
  706. Assign realm roles to a user
  707. :param client_id: id of client (not client-id)
  708. :param user_id: id of user
  709. :param client_id: id of client containing role,
  710. :param roles: roles list or role (use RoleRepresentation)
  711. :return Keycloak server response
  712. """
  713. payload = roles if isinstance(roles, list) else [roles]
  714. params_path = {"realm-name": self.realm_name, "id": user_id}
  715. data_raw = self.raw_post(URL_ADMIN_USER_REALM_ROLES.format(**params_path),
  716. data=json.dumps(payload))
  717. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  718. def get_client_roles_of_user(self, user_id, client_id):
  719. """
  720. Get all client roles for a user.
  721. :param client_id: id of client (not client-id)
  722. :param user_id: id of user
  723. :return: Keycloak server response (array RoleRepresentation)
  724. """
  725. return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES, user_id, client_id)
  726. def get_available_client_roles_of_user(self, user_id, client_id):
  727. """
  728. Get available client role-mappings for a user.
  729. :param client_id: id of client (not client-id)
  730. :param user_id: id of user
  731. :return: Keycloak server response (array RoleRepresentation)
  732. """
  733. return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id)
  734. def get_composite_client_roles_of_user(self, user_id, client_id):
  735. """
  736. Get composite client role-mappings for a user.
  737. :param client_id: id of client (not client-id)
  738. :param user_id: id of user
  739. :return: Keycloak server response (array RoleRepresentation)
  740. """
  741. return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id)
  742. def _get_client_roles_of_user(self, client_level_role_mapping_url, user_id, client_id):
  743. params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id}
  744. data_raw = self.raw_get(client_level_role_mapping_url.format(**params_path))
  745. return raise_error_from_response(data_raw, KeycloakGetError)
  746. def delete_client_roles_of_user(self, user_id, client_id, roles):
  747. """
  748. Delete client roles from a user.
  749. :param client_id: id of client (not client-id)
  750. :param user_id: id of user
  751. :param client_id: id of client containing role,
  752. :param roles: roles list or role to delete (use RoleRepresentation)
  753. :return: Keycloak server response
  754. """
  755. payload = roles if isinstance(roles, list) else [roles]
  756. params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id}
  757. data_raw = self.raw_delete(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path),
  758. data=json.dumps(payload))
  759. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  760. def get_authentication_flows(self):
  761. """
  762. Get authentication flows. Returns all flow details
  763. AuthenticationFlowRepresentation
  764. https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationflowrepresentation
  765. :return: Keycloak server response (AuthenticationFlowRepresentation)
  766. """
  767. params_path = {"realm-name": self.realm_name}
  768. data_raw = self.raw_get(URL_ADMIN_FLOWS.format(**params_path))
  769. return raise_error_from_response(data_raw, KeycloakGetError)
  770. def create_authentication_flow(self, payload, skip_exists=False):
  771. """
  772. Create a new authentication flow
  773. AuthenticationFlowRepresentation
  774. https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationflowrepresentation
  775. :param payload: AuthenticationFlowRepresentation
  776. :return: Keycloak server response (RoleRepresentation)
  777. """
  778. params_path = {"realm-name": self.realm_name}
  779. data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path),
  780. data=payload)
  781. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists)
  782. def get_authentication_flow_executions(self, flow_alias):
  783. """
  784. Get authentication flow executions. Returns all execution steps
  785. :return: Response(json)
  786. """
  787. params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias}
  788. data_raw = self.raw_get(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path))
  789. return raise_error_from_response(data_raw, KeycloakGetError)
  790. def update_authentication_flow_executions(self, payload, flow_alias):
  791. """
  792. Update an authentication flow execution
  793. AuthenticationExecutionInfoRepresentation
  794. https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationexecutioninforepresentation
  795. :param payload: AuthenticationExecutionInfoRepresentation
  796. :return: Keycloak server response
  797. """
  798. params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias}
  799. data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path),
  800. data=payload)
  801. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  802. def sync_users(self, storage_id, action):
  803. """
  804. Function to trigger user sync from provider
  805. :param storage_id:
  806. :param action:
  807. :return:
  808. """
  809. data = {'action': action}
  810. params_query = {"action": action}
  811. params_path = {"realm-name": self.realm_name, "id": storage_id}
  812. data_raw = self.raw_post(URL_ADMIN_USER_STORAGE.format(**params_path),
  813. data=json.dumps(data), **params_query)
  814. return raise_error_from_response(data_raw, KeycloakGetError)
  815. def get_client_scopes(self):
  816. """
  817. Get representation of the client scopes for the realm where we are connected to
  818. https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientscopes
  819. :return: Keycloak server response Array of (ClientScopeRepresentation)
  820. """
  821. params_path = {"realm-name": self.realm_name}
  822. data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPES.format(**params_path))
  823. return raise_error_from_response(data_raw, KeycloakGetError)
  824. def get_client_scope(self, client_scope_id):
  825. """
  826. Get representation of the client scopes for the realm where we are connected to
  827. https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientscopes
  828. :return: Keycloak server response (ClientScopeRepresentation)
  829. """
  830. params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id}
  831. data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPE.format(**params_path))
  832. return raise_error_from_response(data_raw, KeycloakGetError)
  833. def add_mapper_to_client_scope(self, client_scope_id, payload):
  834. """
  835. Add a mapper to a client scope
  836. https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_create_mapper
  837. :param payload: ProtocolMapperRepresentation
  838. :return: Keycloak server Response
  839. """
  840. params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id}
  841. data_raw = self.raw_post(
  842. URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload))
  843. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
  844. def get_client_secrets(self, client_id):
  845. """
  846. Get representation of the client secrets
  847. https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientsecret
  848. :param client_id: id of client (not client-id)
  849. :return: Keycloak server response (ClientRepresentation)
  850. """
  851. params_path = {"realm-name": self.realm_name, "id": client_id}
  852. data_raw = self.raw_get(URL_ADMIN_CLIENT_SECRETS.format(**params_path))
  853. return raise_error_from_response(data_raw, KeycloakGetError)
  854. def raw_get(self, *args, **kwargs):
  855. """
  856. Calls connection.raw_get.
  857. If auto_refresh is set for *get* and *access_token* is expired, it will refresh the token
  858. and try *get* once more.
  859. """
  860. r = self.connection.raw_get(*args, **kwargs)
  861. if 'get' in self.auto_refresh_token and r.status_code == 401:
  862. self.refresh_token()
  863. return self.connection.raw_get(*args, **kwargs)
  864. return r
  865. def raw_post(self, *args, **kwargs):
  866. """
  867. Calls connection.raw_post.
  868. If auto_refresh is set for *post* and *access_token* is expired, it will refresh the token
  869. and try *post* once more.
  870. """
  871. r = self.connection.raw_post(*args, **kwargs)
  872. if 'post' in self.auto_refresh_token and r.status_code == 401:
  873. self.refresh_token()
  874. return self.connection.raw_post(*args, **kwargs)
  875. return r
  876. def raw_put(self, *args, **kwargs):
  877. """
  878. Calls connection.raw_put.
  879. If auto_refresh is set for *put* and *access_token* is expired, it will refresh the token
  880. and try *put* once more.
  881. """
  882. r = self.connection.raw_put(*args, **kwargs)
  883. if 'put' in self.auto_refresh_token and r.status_code == 401:
  884. self.refresh_token()
  885. return self.connection.raw_put(*args, **kwargs)
  886. return r
  887. def raw_delete(self, *args, **kwargs):
  888. """
  889. Calls connection.raw_delete.
  890. If auto_refresh is set for *delete* and *access_token* is expired, it will refresh the token
  891. and try *delete* once more.
  892. """
  893. r = self.connection.raw_delete(*args, **kwargs)
  894. if 'delete' in self.auto_refresh_token and r.status_code == 401:
  895. self.refresh_token()
  896. return self.connection.raw_delete(*args, **kwargs)
  897. return r
  898. def get_token(self):
  899. self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id,
  900. realm_name=self.user_realm_name or self.realm_name, verify=self.verify,
  901. client_secret_key=self.client_secret_key,
  902. custom_headers=self.custom_headers)
  903. grant_type = ["password"]
  904. if self.client_secret_key:
  905. grant_type = ["client_credentials"]
  906. self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type)
  907. headers = {
  908. 'Authorization': 'Bearer ' + self.token.get('access_token'),
  909. 'Content-Type': 'application/json'
  910. }
  911. if self.custom_headers is not None:
  912. # merge custom headers to main headers
  913. headers.update(self.custom_headers)
  914. self._connection = ConnectionManager(base_url=self.server_url,
  915. headers=headers,
  916. timeout=60,
  917. verify=self.verify)
  918. def refresh_token(self):
  919. refresh_token = self.token.get('refresh_token')
  920. try:
  921. self.token = self.keycloak_openid.refresh_token(refresh_token)
  922. except KeycloakGetError as e:
  923. if e.response_code == 400 and b'Refresh token expired' in e.response_body:
  924. self.get_token()
  925. else:
  926. raise
  927. self.connection.add_param_headers('Authorization', 'Bearer ' + self.token.get('access_token'))