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.

638 lines
24 KiB

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
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
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2017 Marcos Pereira <marcospereira.mpj@gmail.com>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU Lesser General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU Lesser General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. # Unless otherwise stated in the comments, "id", in e.g. user_id, refers to the
  18. # internal Keycloak server ID, usually a uuid string
  19. from keycloak.urls_patterns import URL_ADMIN_CLIENT_ROLE
  20. from .urls_patterns import \
  21. URL_ADMIN_USERS_COUNT, URL_ADMIN_USER, URL_ADMIN_USER_CONSENTS, \
  22. URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_GET_SESSIONS, \
  23. URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENTS, URL_ADMIN_CLIENT, URL_ADMIN_CLIENT_ROLES, URL_ADMIN_REALM_ROLES, \
  24. URL_ADMIN_GROUP, URL_ADMIN_GROUPS, URL_ADMIN_GROUP_CHILD, URL_ADMIN_USER_GROUP,\
  25. URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_USER_CLIENT_ROLES, URL_ADMIN_USER_STORAGE
  26. from .keycloak_openid import KeycloakOpenID
  27. from .exceptions import raise_error_from_response, KeycloakGetError
  28. from .urls_patterns import (
  29. URL_ADMIN_USERS,
  30. )
  31. from .connection import ConnectionManager
  32. import json
  33. class KeycloakAdmin:
  34. def __init__(self, server_url, verify, username, password, realm_name='master', client_id='admin-cli'):
  35. self._username = username
  36. self._password = password
  37. self._client_id = client_id
  38. self._realm_name = realm_name
  39. # Get token Admin
  40. keycloak_openid = KeycloakOpenID(server_url=server_url, client_id=client_id, realm_name=realm_name,
  41. verify=verify)
  42. self._token = keycloak_openid.token(username, password)
  43. self._connection = ConnectionManager(base_url=server_url,
  44. headers={'Authorization': 'Bearer ' + self.token.get('access_token'),
  45. 'Content-Type': 'application/json'},
  46. timeout=60,
  47. verify=verify)
  48. @property
  49. def realm_name(self):
  50. return self._realm_name
  51. @realm_name.setter
  52. def realm_name(self, value):
  53. self._realm_name = value
  54. @property
  55. def connection(self):
  56. return self._connection
  57. @connection.setter
  58. def connection(self, value):
  59. self._connection = value
  60. @property
  61. def client_id(self):
  62. return self._client_id
  63. @client_id.setter
  64. def client_id(self, value):
  65. self._client_id = value
  66. @property
  67. def username(self):
  68. return self._username
  69. @username.setter
  70. def username(self, value):
  71. self._username = value
  72. @property
  73. def password(self):
  74. return self._password
  75. @password.setter
  76. def password(self, value):
  77. self._password = value
  78. @property
  79. def token(self):
  80. return self._token
  81. @token.setter
  82. def token(self, value):
  83. self._token = value
  84. def get_users(self, query=None):
  85. """
  86. Get users Returns a list of users, filtered according to query parameters
  87. :return: users list
  88. """
  89. params_path = {"realm-name": self.realm_name}
  90. data_raw = self.connection.raw_get(URL_ADMIN_USERS.format(**params_path), **query)
  91. return raise_error_from_response(data_raw, KeycloakGetError)
  92. def create_user(self, payload):
  93. """
  94. Create a new user Username must be unique
  95. UserRepresentation
  96. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation
  97. :param payload: UserRepresentation
  98. :return: UserRepresentation
  99. """
  100. params_path = {"realm-name": self.realm_name}
  101. data_raw = self.connection.raw_post(URL_ADMIN_USERS.format(**params_path),
  102. data=json.dumps(payload))
  103. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
  104. def users_count(self):
  105. """
  106. User counter
  107. :return: counter
  108. """
  109. params_path = {"realm-name": self.realm_name}
  110. data_raw = self.connection.raw_get(URL_ADMIN_USERS_COUNT.format(**params_path))
  111. return raise_error_from_response(data_raw, KeycloakGetError)
  112. def get_user_id(self, username):
  113. """
  114. Get internal keycloak user id from username
  115. This is required for further actions against this user.
  116. UserRepresentation
  117. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation
  118. :param username: id in UserRepresentation
  119. :return: user_id
  120. """
  121. params_path = {"realm-name": self.realm_name, "username": username}
  122. data_raw = self.connection.raw_get(URL_ADMIN_USERS.format(**params_path))
  123. data_content = raise_error_from_response(data_raw, KeycloakGetError)
  124. for user in data_content:
  125. this_use_rname = json.dumps(user["username"]).strip('"')
  126. if this_use_rname == username:
  127. return json.dumps(user["id"]).strip('"')
  128. return None
  129. def get_user(self, user_id):
  130. """
  131. Get representation of the user
  132. :param user_id: User id
  133. UserRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation
  134. :return: UserRepresentation
  135. """
  136. params_path = {"realm-name": self.realm_name, "id": user_id}
  137. data_raw = self.connection.raw_get(URL_ADMIN_USER.format(**params_path))
  138. return raise_error_from_response(data_raw, KeycloakGetError)
  139. def update_user(self, user_id, payload):
  140. """
  141. Update the user
  142. :param user_id: User id
  143. :param payload: UserRepresentation
  144. :return: Http response
  145. """
  146. params_path = {"realm-name": self.realm_name, "id": user_id}
  147. data_raw = self.connection.raw_put(URL_ADMIN_USER.format(**params_path),
  148. data=json.dumps(payload))
  149. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  150. def delete_user(self, user_id):
  151. """
  152. Delete the user
  153. :param user_id: User id
  154. :return: Http response
  155. """
  156. params_path = {"realm-name": self.realm_name, "id": user_id}
  157. data_raw = self.connection.raw_delete(URL_ADMIN_USER.format(**params_path))
  158. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  159. def set_user_password(self, user_id, password, temporary=True):
  160. """
  161. Set up a password for the user. If temporary is True, the user will have to reset
  162. the temporary password next time they log in.
  163. http://www.keycloak.org/docs-api/3.2/rest-api/#_users_resource
  164. http://www.keycloak.org/docs-api/3.2/rest-api/#_credentialrepresentation
  165. :param user_id: User id
  166. :param password: New password
  167. :param temporary: True if password is temporary
  168. :return:
  169. """
  170. payload = {"type": "password", "temporary": temporary, "value": password}
  171. params_path = {"realm-name": self.realm_name, "id": user_id}
  172. data_raw = self.connection.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path),
  173. data=json.dumps(payload))
  174. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  175. def consents_user(self, user_id):
  176. """
  177. Get consents granted by the user
  178. :param user_id: User id
  179. :return: consents
  180. """
  181. params_path = {"realm-name": self.realm_name, "id": user_id}
  182. data_raw = self.connection.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path))
  183. return raise_error_from_response(data_raw, KeycloakGetError)
  184. def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None):
  185. """
  186. Send a update account email to the user An email contains a
  187. link the user can click to perform a set of required actions.
  188. :param user_id:
  189. :param payload:
  190. :param client_id:
  191. :param lifespan:
  192. :param redirect_uri:
  193. :return:
  194. """
  195. params_path = {"realm-name": self.realm_name, "id": user_id}
  196. params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri}
  197. data_raw = self.connection.raw_put(URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path),
  198. data=payload, **params_query)
  199. return raise_error_from_response(data_raw, KeycloakGetError)
  200. def send_verify_email(self, user_id, client_id=None, redirect_uri=None):
  201. """
  202. Send a update account email to the user An email contains a
  203. link the user can click to perform a set of required actions.
  204. :param user_id: User id
  205. :param client_id: Client id
  206. :param redirect_uri: Redirect uri
  207. :return:
  208. """
  209. params_path = {"realm-name": self.realm_name, "id": user_id}
  210. params_query = {"client_id": client_id, "redirect_uri": redirect_uri}
  211. data_raw = self.connection.raw_put(URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path),
  212. data={}, **params_query)
  213. return raise_error_from_response(data_raw, KeycloakGetError)
  214. def get_sessions(self, user_id):
  215. """
  216. Get sessions associated with the user
  217. :param user_id: id of user
  218. UserSessionRepresentation
  219. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation
  220. :return: UserSessionRepresentation
  221. """
  222. params_path = {"realm-name": self.realm_name, "id": user_id}
  223. data_raw = self.connection.raw_get(URL_ADMIN_GET_SESSIONS.format(**params_path))
  224. return raise_error_from_response(data_raw, KeycloakGetError)
  225. def get_server_info(self):
  226. """
  227. Get themes, social providers, auth providers, and event listeners available on this server
  228. ServerInfoRepresentation
  229. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_serverinforepresentation
  230. :return: ServerInfoRepresentation
  231. """
  232. data_raw = self.connection.raw_get(URL_ADMIN_SERVER_INFO)
  233. return raise_error_from_response(data_raw, KeycloakGetError)
  234. def get_groups(self):
  235. """
  236. Get groups belonging to the realm. Returns a list of groups belonging to the realm
  237. GroupRepresentation
  238. http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
  239. :return: array GroupRepresentation
  240. """
  241. params_path = {"realm-name": self.realm_name}
  242. data_raw = self.connection.raw_get(URL_ADMIN_GROUPS.format(**params_path))
  243. return raise_error_from_response(data_raw, KeycloakGetError)
  244. def get_group(self, group_id):
  245. """
  246. Get group by id. Returns full group details
  247. GroupRepresentation
  248. http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
  249. :return: Keycloak server response (GroupRepresentation)
  250. """
  251. params_path = {"realm-name": self.realm_name, "id": group_id}
  252. data_raw = self.connection.raw_get(URL_ADMIN_GROUP.format(**params_path))
  253. return raise_error_from_response(data_raw, KeycloakGetError)
  254. def get_group_by_name(self, name_or_path, search_in_subgroups=False):
  255. """
  256. Get group id based on name or path.
  257. A straight name or path match with a top-level group will return first.
  258. Subgroups are traversed, the first to match path (or name with path) is returned.
  259. GroupRepresentation
  260. http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
  261. :param name: group name
  262. :param path: group path
  263. :param search_in_subgroups: True if want search in the subgroups
  264. :return: Keycloak server response (GroupRepresentation)
  265. """
  266. groups = self.get_groups()
  267. # TODO: Review this code is necessary
  268. for group in groups:
  269. if group['name'] == name_or_path or group['path'] == name_or_path:
  270. return group
  271. elif search_in_subgroups and group["subGroups"]:
  272. for subgroup in group["subGroups"]:
  273. if subgroup['name'] == name_or_path or subgroup['path'] == name_or_path:
  274. return subgroup
  275. return None
  276. def create_group(self, name=None, client_roles={}, realm_roles=[], sub_groups=[], path=None, parent=None):
  277. """
  278. Create a group in the Realm
  279. GroupRepresentation
  280. http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation
  281. :param name: group name
  282. :param client_roles: (Dict) Client roles to include in groupp # Not demonstrated to work
  283. :param realm_roles: (List) Realm roles to include in group # Not demonstrated to work
  284. :param sub_groups: (List) Subgroups to include in groupp # Not demonstrated to work
  285. :param path: group path
  286. :param parent: parent group's id. Required to create a sub-group.
  287. :return: Keycloak server response (GroupRepresentation)
  288. """
  289. data = {"name": name or path,
  290. "path": path,
  291. "clientRoles": client_roles,
  292. "realmRoles": realm_roles,
  293. "subGroups": sub_groups}
  294. if parent is None:
  295. params_path = {"realm-name": self.realm_name}
  296. data_raw = self.connection.raw_post(URL_ADMIN_GROUPS.format(**params_path),
  297. data=json.dumps(data))
  298. else:
  299. params_path = {"realm-name": self.realm_name, "id": parent}
  300. data_raw = self.connection.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path),
  301. data=json.dumps(data))
  302. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
  303. def group_set_permissions(self, group_id, enabled=True):
  304. """
  305. Enable/Disable permissions for a group. Cannot delete group if disabled
  306. :param group_id: id of group
  307. :param enabled: boolean
  308. :return: Keycloak server response
  309. """
  310. params_path = {"realm-name": self.realm_name, "id": group_id}
  311. data_raw = self.connection.raw_put(URL_ADMIN_GROUP_PERMISSIONS.format(**params_path),
  312. data=json.dumps({"enabled": enabled}))
  313. return raise_error_from_response(data_raw, KeycloakGetError)
  314. def group_user_add(self, user_id, group_id):
  315. """
  316. Add user to group (user_id and group_id)
  317. :param group_id: id of group
  318. :param user_id: id of user
  319. :param group_id: id of group to add to
  320. :return: Keycloak server response
  321. """
  322. params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id}
  323. data_raw = self.connection.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None)
  324. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  325. def group_user_remove(self, user_id, group_id):
  326. """
  327. Remove user from group (user_id and group_id)
  328. :param group_id: id of group
  329. :param user_id: id of user
  330. :param group_id: id of group to add to
  331. :return: Keycloak server response
  332. """
  333. params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id}
  334. data_raw = self.connection.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path))
  335. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  336. def delete_group(self, group_id):
  337. """
  338. Deletes a group in the Realm
  339. :param group_id: id of group to delete
  340. :return: Keycloak server response
  341. """
  342. params_path = {"realm-name": self.realm_name, "id": group_id}
  343. data_raw = self.connection.raw_delete(URL_ADMIN_GROUP.format(**params_path))
  344. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  345. def get_clients(self):
  346. """
  347. Get clients belonging to the realm Returns a list of clients belonging to the realm
  348. ClientRepresentation
  349. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  350. :return: Keycloak server response (ClientRepresentation)
  351. """
  352. params_path = {"realm-name": self.realm_name}
  353. data_raw = self.connection.raw_get(URL_ADMIN_CLIENTS.format(**params_path))
  354. return raise_error_from_response(data_raw, KeycloakGetError)
  355. def get_client(self, client_id):
  356. """
  357. Get representation of the client
  358. ClientRepresentation
  359. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  360. :param client_id: id of client (not client-id)
  361. :return: Keycloak server response (ClientRepresentation)
  362. """
  363. params_path = {"realm-name": self.realm_name, "id": client_id}
  364. data_raw = self.connection.raw_get(URL_ADMIN_CLIENT.format(**params_path))
  365. return raise_error_from_response(data_raw, KeycloakGetError)
  366. def get_client_id(self, client_name):
  367. """
  368. Get internal keycloak client id from client-id.
  369. This is required for further actions against this client.
  370. :param client_name: name in ClientRepresentation
  371. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  372. :return: client_id (uuid as string)
  373. """
  374. clients = self.get_clients()
  375. for client in clients:
  376. if client_name == client['name']:
  377. return client["id"]
  378. return None
  379. def create_client(self, payload):
  380. """
  381. Create a client
  382. ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  383. :param payload: ClientRepresentation
  384. :return: Keycloak server response (UserRepresentation)
  385. """
  386. params_path = {"realm-name": self.realm_name}
  387. data_raw = self.connection.raw_post(URL_ADMIN_CLIENTS.format(**params_path),
  388. data=json.dumps(payload))
  389. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
  390. def delete_client(self, client_id):
  391. """
  392. Get representation of the client
  393. ClientRepresentation
  394. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation
  395. :param client_id: keycloak client id (not oauth client-id)
  396. :return: Keycloak server response (ClientRepresentation)
  397. """
  398. params_path = {"realm-name": self.realm_name, "id": client_id}
  399. data_raw = self.connection.raw_delete(URL_ADMIN_CLIENT.format(**params_path))
  400. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  401. def get_realm_roles(self):
  402. """
  403. Get all roles for the realm or client
  404. RoleRepresentation
  405. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  406. :return: Keycloak server response (RoleRepresentation)
  407. """
  408. params_path = {"realm-name": self.realm_name}
  409. data_raw = self.connection.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path))
  410. return raise_error_from_response(data_raw, KeycloakGetError)
  411. def get_client_roles(self, client_id):
  412. """
  413. Get all roles for the client
  414. :param client_id: id of client (not client-id)
  415. RoleRepresentation
  416. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  417. :return: Keycloak server response (RoleRepresentation)
  418. """
  419. params_path = {"realm-name": self.realm_name, "id": client_id}
  420. data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ROLES.format(**params_path))
  421. return raise_error_from_response(data_raw, KeycloakGetError)
  422. def get_client_role(self, client_id, role_name):
  423. """
  424. Get client role id by name
  425. This is required for further actions with this role.
  426. :param client_id: id of client (not client-id)
  427. :param role_name: roles name (not id!)
  428. RoleRepresentation
  429. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  430. :return: role_id
  431. """
  432. params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name}
  433. data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ROLE.format(**params_path))
  434. return raise_error_from_response(data_raw, KeycloakGetError)
  435. def get_client_role_id(self, client_id, role_name):
  436. """
  437. Warning: Deprecated
  438. Get client role id by name
  439. This is required for further actions with this role.
  440. :param client_id: id of client (not client-id)
  441. :param role_name: roles name (not id!)
  442. RoleRepresentation
  443. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  444. :return: role_id
  445. """
  446. role = self.get_client_role(client_id, role_name)
  447. return role.get("id")
  448. def create_client_role(self, payload):
  449. """
  450. Create a client role
  451. RoleRepresentation
  452. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  453. :param payload: id of client (not client-id), role_name: name of role
  454. :return: Keycloak server response (RoleRepresentation)
  455. """
  456. params_path = {"realm-name": self.realm_name, "id": self.client_id}
  457. data_raw = self.connection.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path),
  458. data=json.dumps(payload))
  459. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201)
  460. def delete_client_role(self, role_name):
  461. """
  462. Create a client role
  463. RoleRepresentation
  464. http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation
  465. :param role_name: roles name (not id!)
  466. """
  467. params_path = {"realm-name": self.realm_name, "id": self.client_id, "role-name": role_name}
  468. data_raw = self.connection.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path))
  469. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  470. def assign_client_role(self, user_id, client_id, roles):
  471. """
  472. Assign a client role to a user
  473. :param client_id: id of client (not client-id)
  474. :param user_id: id of user
  475. :param client_id: id of client containing role,
  476. :param roles: roles list or role (use RoleRepresentation)
  477. :return Keycloak server response
  478. """
  479. payload = roles if isinstance(roles, list) else [roles]
  480. params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id}
  481. data_raw = self.connection.raw_post(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path),
  482. data=json.dumps(payload))
  483. return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204)
  484. def sync_users(self, storage_id, action):
  485. data = {'action': action}
  486. params_query = {"action": action}
  487. params_path = {"realm-name": self.realm_name, "id": storage_id}
  488. data_raw = self.connection.raw_post(URL_ADMIN_USER_STORAGE.format(**params_path),
  489. data=json.dumps(data), **params_query)
  490. return raise_error_from_response(data_raw, KeycloakGetError)