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.

795 lines
29 KiB

  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. """Keycloak UMA module.
  24. The module contains a UMA compatible client for keycloak:
  25. https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-federated-authz-2.0.html
  26. """
  27. import json
  28. from typing import Iterable
  29. from urllib.parse import quote_plus
  30. from async_property import async_property
  31. from .connection import ConnectionManager
  32. from .exceptions import (
  33. KeycloakDeleteError,
  34. KeycloakGetError,
  35. KeycloakPostError,
  36. KeycloakPutError,
  37. raise_error_from_response,
  38. )
  39. from .openid_connection import KeycloakOpenIDConnection
  40. from .uma_permissions import UMAPermission
  41. from .urls_patterns import URL_UMA_WELL_KNOWN
  42. class KeycloakUMA:
  43. """Keycloak UMA client.
  44. :param connection: OpenID connection manager
  45. """
  46. def __init__(self, connection: KeycloakOpenIDConnection):
  47. """Init method.
  48. :param connection: OpenID connection manager
  49. :type connection: KeycloakOpenIDConnection
  50. """
  51. self.connection = connection
  52. self._well_known = None
  53. def _fetch_well_known(self):
  54. params_path = {"realm-name": self.connection.realm_name}
  55. data_raw = self.connection.raw_get(URL_UMA_WELL_KNOWN.format(**params_path))
  56. return raise_error_from_response(data_raw, KeycloakGetError)
  57. @staticmethod
  58. def format_url(url, **kwargs):
  59. """Substitute url path parameters.
  60. Given a parameterized url string, returns the string after url encoding and substituting
  61. the given params. For example,
  62. `format_url("https://myserver/{my_resource}/{id}", my_resource="hello world", id="myid")`
  63. would produce `https://myserver/hello+world/myid`.
  64. :param url: url string to format
  65. :type url: str
  66. :param kwargs: dict containing kwargs to substitute
  67. :type kwargs: dict
  68. :return: formatted string
  69. :rtype: str
  70. """
  71. return url.format(**{k: quote_plus(v) for k, v in kwargs.items()})
  72. @staticmethod
  73. async def a_format_url(url, **kwargs):
  74. """Substitute url path parameters.
  75. Given a parameterized url string, returns the string after url encoding and substituting
  76. the given params. For example,
  77. `format_url("https://myserver/{my_resource}/{id}", my_resource="hello world", id="myid")`
  78. would produce `https://myserver/hello+world/myid`.
  79. :param url: url string to format
  80. :type url: str
  81. :param kwargs: dict containing kwargs to substitute
  82. :type kwargs: dict
  83. :return: formatted string
  84. :rtype: str
  85. """
  86. return url.format(**{k: quote_plus(v) for k, v in kwargs.items()})
  87. @property
  88. def uma_well_known(self):
  89. """Get the well_known UMA2 config.
  90. :returns: It lists endpoints and other configuration options relevant
  91. :rtype: dict
  92. """
  93. # per instance cache
  94. if not self._well_known:
  95. self._well_known = self._fetch_well_known()
  96. return self._well_known
  97. @async_property
  98. async def a_uma_well_known(self):
  99. """Get the well_known UMA2 config async.
  100. :returns: It lists endpoints and other configuration options relevant
  101. :rtype: dict
  102. """
  103. if not self._well_known:
  104. self._well_known = await self.a__fetch_well_known()
  105. return self._well_known
  106. def resource_set_create(self, payload):
  107. """Create a resource set.
  108. Spec
  109. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#rfc.section.2.2.1
  110. ResourceRepresentation
  111. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_resourcerepresentation
  112. :param payload: ResourceRepresentation
  113. :type payload: dict
  114. :return: ResourceRepresentation with the _id property assigned
  115. :rtype: dict
  116. """
  117. data_raw = self.connection.raw_post(
  118. self.uma_well_known["resource_registration_endpoint"], data=json.dumps(payload)
  119. )
  120. return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201])
  121. def resource_set_update(self, resource_id, payload):
  122. """Update a resource set.
  123. Spec
  124. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#update-resource-set
  125. ResourceRepresentation
  126. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_resourcerepresentation
  127. :param resource_id: id of the resource
  128. :type resource_id: str
  129. :param payload: ResourceRepresentation
  130. :type payload: dict
  131. :return: Response dict (empty)
  132. :rtype: dict
  133. """
  134. url = self.format_url(
  135. self.uma_well_known["resource_registration_endpoint"] + "/{id}", id=resource_id
  136. )
  137. data_raw = self.connection.raw_put(url, data=json.dumps(payload))
  138. return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204])
  139. def resource_set_read(self, resource_id):
  140. """Read a resource set.
  141. Spec
  142. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#read-resource-set
  143. ResourceRepresentation
  144. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_resourcerepresentation
  145. :param resource_id: id of the resource
  146. :type resource_id: str
  147. :return: ResourceRepresentation
  148. :rtype: dict
  149. """
  150. url = self.format_url(
  151. self.uma_well_known["resource_registration_endpoint"] + "/{id}", id=resource_id
  152. )
  153. data_raw = self.connection.raw_get(url)
  154. return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200])
  155. def resource_set_delete(self, resource_id):
  156. """Delete a resource set.
  157. Spec
  158. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#delete-resource-set
  159. :param resource_id: id of the resource
  160. :type resource_id: str
  161. :return: Response dict (empty)
  162. :rtype: dict
  163. """
  164. url = self.format_url(
  165. self.uma_well_known["resource_registration_endpoint"] + "/{id}", id=resource_id
  166. )
  167. data_raw = self.connection.raw_delete(url)
  168. return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204])
  169. def resource_set_list_ids(
  170. self,
  171. name: str = "",
  172. exact_name: bool = False,
  173. uri: str = "",
  174. owner: str = "",
  175. resource_type: str = "",
  176. scope: str = "",
  177. matchingUri: bool = False,
  178. first: int = 0,
  179. maximum: int = -1,
  180. ):
  181. """Query for list of resource set ids.
  182. Spec
  183. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#list-resource-sets
  184. :param name: query resource name
  185. :type name: str
  186. :param exact_name: query exact match for resource name
  187. :type exact_name: bool
  188. :param uri: query resource uri
  189. :type uri: str
  190. :param owner: query resource owner
  191. :type owner: str
  192. :param resource_type: query resource type
  193. :type resource_type: str
  194. :param scope: query resource scope
  195. :type scope: str
  196. :param matchingUri: enable URI matching
  197. :type matchingUri: bool
  198. :param first: index of first matching resource to return
  199. :type first: int
  200. :param maximum: maximum number of resources to return (-1 for all)
  201. :type maximum: int
  202. :return: List of ids
  203. :rtype: List[str]
  204. """
  205. query = dict()
  206. if name:
  207. query["name"] = name
  208. if exact_name:
  209. query["exactName"] = "true"
  210. if uri:
  211. query["uri"] = uri
  212. if owner:
  213. query["owner"] = owner
  214. if resource_type:
  215. query["type"] = resource_type
  216. if scope:
  217. query["scope"] = scope
  218. if matchingUri:
  219. query["matchingUri"] = "true"
  220. if first > 0:
  221. query["first"] = first
  222. if maximum >= 0:
  223. query["max"] = maximum
  224. data_raw = self.connection.raw_get(
  225. self.uma_well_known["resource_registration_endpoint"], **query
  226. )
  227. return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200])
  228. def resource_set_list(self):
  229. """List all resource sets.
  230. Spec
  231. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#list-resource-sets
  232. ResourceRepresentation
  233. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_resourcerepresentation
  234. :yields: Iterator over a list of ResourceRepresentations
  235. :rtype: Iterator[dict]
  236. """
  237. for resource_id in self.resource_set_list_ids():
  238. resource = self.resource_set_read(resource_id)
  239. yield resource
  240. def permission_ticket_create(self, permissions: Iterable[UMAPermission]):
  241. """Create a permission ticket.
  242. :param permissions: Iterable of uma permissions to validate the token against
  243. :type permissions: Iterable[UMAPermission]
  244. :returns: Keycloak decision
  245. :rtype: boolean
  246. :raises KeycloakPostError: In case permission resource not found
  247. """
  248. resources = dict()
  249. for permission in permissions:
  250. resource_id = getattr(permission, "resource_id", None)
  251. if resource_id is None:
  252. resource_ids = self.resource_set_list_ids(
  253. exact_name=True, name=permission.resource, first=0, maximum=1
  254. )
  255. if not resource_ids:
  256. raise KeycloakPostError("Invalid resource specified")
  257. setattr(permission, "resource_id", resource_ids[0])
  258. resources.setdefault(resource_id, set())
  259. if permission.scope:
  260. resources[resource_id].add(permission.scope)
  261. payload = [
  262. {"resource_id": resource_id, "resource_scopes": list(scopes)}
  263. for resource_id, scopes in resources.items()
  264. ]
  265. data_raw = self.connection.raw_post(
  266. self.uma_well_known["permission_endpoint"], data=json.dumps(payload)
  267. )
  268. return raise_error_from_response(data_raw, KeycloakPostError)
  269. def permissions_check(self, token, permissions: Iterable[UMAPermission]):
  270. """Check UMA permissions by user token with requested permissions.
  271. The token endpoint is used to check UMA permissions from Keycloak. It can only be
  272. invoked by confidential clients.
  273. https://www.keycloak.org/docs/latest/authorization_services/#_service_authorization_api
  274. :param token: user token
  275. :type token: str
  276. :param permissions: Iterable of uma permissions to validate the token against
  277. :type permissions: Iterable[UMAPermission]
  278. :returns: Keycloak decision
  279. :rtype: boolean
  280. """
  281. payload = {
  282. "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket",
  283. "permission": ",".join(str(permission) for permission in permissions),
  284. "response_mode": "decision",
  285. "audience": self.connection.client_id,
  286. }
  287. # Everyone always has the null set of permissions
  288. # However keycloak cannot evaluate the null set
  289. if len(payload["permission"]) == 0:
  290. return True
  291. connection = ConnectionManager(self.connection.base_url)
  292. connection.add_param_headers("Authorization", "Bearer " + token)
  293. connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
  294. data_raw = connection.raw_post(self.uma_well_known["token_endpoint"], data=payload)
  295. try:
  296. data = raise_error_from_response(data_raw, KeycloakPostError)
  297. except KeycloakPostError:
  298. return False
  299. return data.get("result", False)
  300. def policy_resource_create(self, resource_id, payload):
  301. """Create permission policy for resource.
  302. Supports name, description, scopes, roles, groups, clients
  303. https://www.keycloak.org/docs/latest/authorization_services/#associating-a-permission-with-a-resource
  304. :param resource_id: _id of resource
  305. :type resource_id: str
  306. :param payload: permission configuration
  307. :type payload: dict
  308. :return: PermissionRepresentation
  309. :rtype: dict
  310. """
  311. data_raw = self.connection.raw_post(
  312. self.uma_well_known["policy_endpoint"] + f"/{resource_id}", data=json.dumps(payload)
  313. )
  314. return raise_error_from_response(data_raw, KeycloakPostError)
  315. def policy_update(self, policy_id, payload):
  316. """Update permission policy.
  317. https://www.keycloak.org/docs/latest/authorization_services/#associating-a-permission-with-a-resource
  318. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_policyrepresentation
  319. :param policy_id: id of policy permission
  320. :type policy_id: str
  321. :param payload: policy permission configuration
  322. :type payload: dict
  323. :return: PermissionRepresentation
  324. :rtype: dict
  325. """
  326. data_raw = self.connection.raw_put(
  327. self.uma_well_known["policy_endpoint"] + f"/{policy_id}", data=json.dumps(payload)
  328. )
  329. return raise_error_from_response(data_raw, KeycloakPutError)
  330. def policy_delete(self, policy_id):
  331. """Delete permission policy.
  332. https://www.keycloak.org/docs/latest/authorization_services/#removing-a-permission
  333. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_policyrepresentation
  334. :param policy_id: id of permission policy
  335. :type policy_id: str
  336. :return: PermissionRepresentation
  337. :rtype: dict
  338. """
  339. data_raw = self.connection.raw_delete(
  340. self.uma_well_known["policy_endpoint"] + f"/{policy_id}"
  341. )
  342. return raise_error_from_response(data_raw, KeycloakDeleteError)
  343. def policy_query(
  344. self,
  345. resource: str = "",
  346. name: str = "",
  347. scope: str = "",
  348. first: int = 0,
  349. maximum: int = -1,
  350. ):
  351. """Query permission policies.
  352. https://www.keycloak.org/docs/latest/authorization_services/#querying-permission
  353. :param resource: query resource id
  354. :type resource: str
  355. :param name: query resource name
  356. :type name: str
  357. :param scope: query resource scope
  358. :type scope: str
  359. :param first: index of first matching resource to return
  360. :type first: int
  361. :param maximum: maximum number of resources to return (-1 for all)
  362. :type maximum: int
  363. :return: List of ids
  364. :return: List of ids
  365. :rtype: List[str]
  366. """
  367. query = dict()
  368. if name:
  369. query["name"] = name
  370. if resource:
  371. query["resource"] = resource
  372. if scope:
  373. query["scope"] = scope
  374. if first > 0:
  375. query["first"] = first
  376. if maximum >= 0:
  377. query["max"] = maximum
  378. data_raw = self.connection.raw_get(self.uma_well_known["policy_endpoint"], **query)
  379. return raise_error_from_response(data_raw, KeycloakGetError)
  380. async def a__fetch_well_known(self):
  381. """Get the well_known UMA2 config async.
  382. :returns: It lists endpoints and other configuration options relevant
  383. :rtype: dict
  384. """
  385. params_path = {"realm-name": self.connection.realm_name}
  386. data_raw = await self.connection.a_raw_get(URL_UMA_WELL_KNOWN.format(**params_path))
  387. return raise_error_from_response(data_raw, KeycloakGetError)
  388. async def a_resource_set_create(self, payload):
  389. """Create a resource set asynchronously.
  390. Spec
  391. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#rfc.section.2.2.1
  392. ResourceRepresentation
  393. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_resourcerepresentation
  394. :param payload: ResourceRepresentation
  395. :type payload: dict
  396. :return: ResourceRepresentation with the _id property assigned
  397. :rtype: dict
  398. """
  399. data_raw = await self.connection.a_raw_post(
  400. (await self.a_uma_well_known)["resource_registration_endpoint"],
  401. data=json.dumps(payload),
  402. )
  403. return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201])
  404. async def a_resource_set_update(self, resource_id, payload):
  405. """Update a resource set asynchronously.
  406. Spec
  407. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#update-resource-set
  408. ResourceRepresentation
  409. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_resourcerepresentation
  410. :param resource_id: id of the resource
  411. :type resource_id: str
  412. :param payload: ResourceRepresentation
  413. :type payload: dict
  414. :return: Response dict (empty)
  415. :rtype: dict
  416. """
  417. url = self.format_url(
  418. (await self.a_uma_well_known)["resource_registration_endpoint"] + "/{id}",
  419. id=resource_id,
  420. )
  421. data_raw = await self.connection.a_raw_put(url, data=json.dumps(payload))
  422. return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204])
  423. async def a_resource_set_read(self, resource_id):
  424. """Read a resource set asynchronously.
  425. Spec
  426. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#read-resource-set
  427. ResourceRepresentation
  428. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_resourcerepresentation
  429. :param resource_id: id of the resource
  430. :type resource_id: str
  431. :return: ResourceRepresentation
  432. :rtype: dict
  433. """
  434. url = self.format_url(
  435. (await self.a_uma_well_known)["resource_registration_endpoint"] + "/{id}",
  436. id=resource_id,
  437. )
  438. data_raw = await self.connection.a_raw_get(url)
  439. return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200])
  440. async def a_resource_set_delete(self, resource_id):
  441. """Delete a resource set asynchronously.
  442. Spec
  443. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#delete-resource-set
  444. :param resource_id: id of the resource
  445. :type resource_id: str
  446. :return: Response dict (empty)
  447. :rtype: dict
  448. """
  449. url = self.format_url(
  450. (await self.a_uma_well_known)["resource_registration_endpoint"] + "/{id}",
  451. id=resource_id,
  452. )
  453. data_raw = await self.connection.a_raw_delete(url)
  454. return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204])
  455. async def a_resource_set_list_ids(
  456. self,
  457. name: str = "",
  458. exact_name: bool = False,
  459. uri: str = "",
  460. owner: str = "",
  461. resource_type: str = "",
  462. scope: str = "",
  463. matchingUri: bool = False,
  464. first: int = 0,
  465. maximum: int = -1,
  466. ):
  467. """Query for list of resource set ids asynchronously.
  468. Spec
  469. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#list-resource-sets
  470. :param name: query resource name
  471. :type name: str
  472. :param exact_name: query exact match for resource name
  473. :type exact_name: bool
  474. :param uri: query resource uri
  475. :type uri: str
  476. :param owner: query resource owner
  477. :type owner: str
  478. :param resource_type: query resource type
  479. :type resource_type: str
  480. :param scope: query resource scope
  481. :type scope: str
  482. :param first: index of first matching resource to return
  483. :param matchingUri: enable URI matching
  484. :type matchingUri: bool
  485. :type first: int
  486. :param maximum: maximum number of resources to return (-1 for all)
  487. :type maximum: int
  488. :return: List of ids
  489. :rtype: List[str]
  490. """
  491. query = dict()
  492. if name:
  493. query["name"] = name
  494. if exact_name:
  495. query["exactName"] = "true"
  496. if uri:
  497. query["uri"] = uri
  498. if owner:
  499. query["owner"] = owner
  500. if resource_type:
  501. query["type"] = resource_type
  502. if scope:
  503. query["scope"] = scope
  504. if matchingUri:
  505. query["matchingUri"] = "true"
  506. if first > 0:
  507. query["first"] = first
  508. if maximum >= 0:
  509. query["max"] = maximum
  510. data_raw = await self.connection.a_raw_get(
  511. (await self.a_uma_well_known)["resource_registration_endpoint"], **query
  512. )
  513. return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200])
  514. async def a_resource_set_list(self):
  515. """List all resource sets asynchronously.
  516. Spec
  517. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#list-resource-sets
  518. ResourceRepresentation
  519. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_resourcerepresentation
  520. :yields: Iterator over a list of ResourceRepresentations
  521. :rtype: Iterator[dict]
  522. """
  523. for resource_id in await self.a_resource_set_list_ids():
  524. resource = await self.a_resource_set_read(resource_id)
  525. yield resource
  526. async def a_permission_ticket_create(self, permissions: Iterable[UMAPermission]):
  527. """Create a permission ticket asynchronously.
  528. :param permissions: Iterable of uma permissions to validate the token against
  529. :type permissions: Iterable[UMAPermission]
  530. :returns: Keycloak decision
  531. :rtype: boolean
  532. :raises KeycloakPostError: In case permission resource not found
  533. """
  534. resources = dict()
  535. for permission in permissions:
  536. resource_id = getattr(permission, "resource_id", None)
  537. if resource_id is None:
  538. resource_ids = await self.a_resource_set_list_ids(
  539. exact_name=True, name=permission.resource, first=0, maximum=1
  540. )
  541. if not resource_ids:
  542. raise KeycloakPostError("Invalid resource specified")
  543. setattr(permission, "resource_id", resource_ids[0])
  544. resources.setdefault(resource_id, set())
  545. if permission.scope:
  546. resources[resource_id].add(permission.scope)
  547. payload = [
  548. {"resource_id": resource_id, "resource_scopes": list(scopes)}
  549. for resource_id, scopes in resources.items()
  550. ]
  551. data_raw = await self.connection.a_raw_post(
  552. (await self.a_uma_well_known)["permission_endpoint"], data=json.dumps(payload)
  553. )
  554. return raise_error_from_response(data_raw, KeycloakPostError)
  555. async def a_permissions_check(self, token, permissions: Iterable[UMAPermission]):
  556. """Check UMA permissions by user token with requested permissions asynchronously.
  557. The token endpoint is used to check UMA permissions from Keycloak. It can only be
  558. invoked by confidential clients.
  559. https://www.keycloak.org/docs/latest/authorization_services/#_service_authorization_api
  560. :param token: user token
  561. :type token: str
  562. :param permissions: Iterable of uma permissions to validate the token against
  563. :type permissions: Iterable[UMAPermission]
  564. :returns: Keycloak decision
  565. :rtype: boolean
  566. """
  567. payload = {
  568. "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket",
  569. "permission": ",".join(str(permission) for permission in permissions),
  570. "response_mode": "decision",
  571. "audience": self.connection.client_id,
  572. }
  573. # Everyone always has the null set of permissions
  574. # However keycloak cannot evaluate the null set
  575. if len(payload["permission"]) == 0:
  576. return True
  577. connection = ConnectionManager(self.connection.base_url)
  578. connection.add_param_headers("Authorization", "Bearer " + token)
  579. connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
  580. data_raw = await connection.a_raw_post(
  581. (await self.a_uma_well_known)["token_endpoint"], data=payload
  582. )
  583. try:
  584. data = raise_error_from_response(data_raw, KeycloakPostError)
  585. except KeycloakPostError:
  586. return False
  587. return data.get("result", False)
  588. async def a_policy_resource_create(self, resource_id, payload):
  589. """Create permission policy for resource asynchronously.
  590. Supports name, description, scopes, roles, groups, clients
  591. https://www.keycloak.org/docs/latest/authorization_services/#associating-a-permission-with-a-resource
  592. :param resource_id: _id of resource
  593. :type resource_id: str
  594. :param payload: permission configuration
  595. :type payload: dict
  596. :return: PermissionRepresentation
  597. :rtype: dict
  598. """
  599. data_raw = await self.connection.a_raw_post(
  600. (await self.a_uma_well_known)["policy_endpoint"] + f"/{resource_id}",
  601. data=json.dumps(payload),
  602. )
  603. return raise_error_from_response(data_raw, KeycloakPostError)
  604. async def a_policy_update(self, policy_id, payload):
  605. """Update permission policy asynchronously.
  606. https://www.keycloak.org/docs/latest/authorization_services/#associating-a-permission-with-a-resource
  607. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_policyrepresentation
  608. :param policy_id: id of policy permission
  609. :type policy_id: str
  610. :param payload: policy permission configuration
  611. :type payload: dict
  612. :return: PermissionRepresentation
  613. :rtype: dict
  614. """
  615. data_raw = await self.connection.a_raw_put(
  616. (await self.a_uma_well_known)["policy_endpoint"] + f"/{policy_id}",
  617. data=json.dumps(payload),
  618. )
  619. return raise_error_from_response(data_raw, KeycloakPutError)
  620. async def a_policy_delete(self, policy_id):
  621. """Delete permission policy asynchronously.
  622. https://www.keycloak.org/docs/latest/authorization_services/#removing-a-permission
  623. https://www.keycloak.org/docs-api/24.0.2/rest-api/index.html#_policyrepresentation
  624. :param policy_id: id of permission policy
  625. :type policy_id: str
  626. :return: PermissionRepresentation
  627. :rtype: dict
  628. """
  629. data_raw = await self.connection.a_raw_delete(
  630. (await self.a_uma_well_known)["policy_endpoint"] + f"/{policy_id}"
  631. )
  632. return raise_error_from_response(data_raw, KeycloakDeleteError)
  633. async def a_policy_query(
  634. self,
  635. resource: str = "",
  636. name: str = "",
  637. scope: str = "",
  638. first: int = 0,
  639. maximum: int = -1,
  640. ):
  641. """Query permission policies asynchronously.
  642. https://www.keycloak.org/docs/latest/authorization_services/#querying-permission
  643. :param resource: query resource id
  644. :type resource: str
  645. :param name: query resource name
  646. :type name: str
  647. :param scope: query resource scope
  648. :type scope: str
  649. :param first: index of first matching resource to return
  650. :type first: int
  651. :param maximum: maximum number of resources to return (-1 for all)
  652. :type maximum: int
  653. :return: List of ids
  654. :return: List of ids
  655. :rtype: List[str]
  656. """
  657. query = dict()
  658. if name:
  659. query["name"] = name
  660. if resource:
  661. query["resource"] = resource
  662. if scope:
  663. query["scope"] = scope
  664. if first > 0:
  665. query["first"] = first
  666. if maximum >= 0:
  667. query["max"] = maximum
  668. data_raw = await self.connection.a_raw_get(
  669. (await self.a_uma_well_known)["policy_endpoint"], **query
  670. )
  671. return raise_error_from_response(data_raw, KeycloakGetError)