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.

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