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.

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