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.

200 lines
7.3 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 urllib.parse import quote_plus
  29. from .exceptions import (
  30. KeycloakDeleteError,
  31. KeycloakGetError,
  32. KeycloakPostError,
  33. KeycloakPutError,
  34. raise_error_from_response,
  35. )
  36. from .openid_connection import KeycloakOpenIDConnection
  37. from .urls_patterns import URL_UMA_WELL_KNOWN
  38. class KeycloakUMA:
  39. """Keycloak UMA client.
  40. :param connection: OpenID connection manager
  41. """
  42. def __init__(self, connection: KeycloakOpenIDConnection):
  43. """Init method.
  44. :param connection: OpenID connection manager
  45. :type connection: KeycloakOpenIDConnection
  46. """
  47. self.connection = connection
  48. custom_headers = self.connection.custom_headers or {}
  49. custom_headers.update({"Content-Type": "application/json"})
  50. self.connection.custom_headers = custom_headers
  51. self._well_known = None
  52. def _fetch_well_known(self):
  53. params_path = {"realm-name": self.connection.realm_name}
  54. data_raw = self.connection.raw_get(URL_UMA_WELL_KNOWN.format(**params_path))
  55. return raise_error_from_response(data_raw, KeycloakGetError)
  56. @staticmethod
  57. def format_url(url, **kwargs):
  58. """Substitute url path parameters.
  59. Given a parameterized url string, returns the string after url encoding and substituting
  60. the given params. For example,
  61. `format_url("https://myserver/{my_resource}/{id}", my_resource="hello world", id="myid")`
  62. would produce `https://myserver/hello+world/myid`.
  63. :param url: url string to format
  64. :type url: str
  65. :param kwargs: dict containing kwargs to substitute
  66. :type kwargs: dict
  67. :return: formatted string
  68. :rtype: str
  69. """
  70. return url.format(**{k: quote_plus(v) for k, v in kwargs.items()})
  71. @property
  72. def uma_well_known(self):
  73. """Get the well_known UMA2 config.
  74. :returns: It lists endpoints and other configuration options relevant
  75. :rtype: dict
  76. """
  77. # per instance cache
  78. if not self._well_known:
  79. self._well_known = self._fetch_well_known()
  80. return self._well_known
  81. def resource_set_create(self, payload):
  82. """Create a resource set.
  83. Spec
  84. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#rfc.section.2.2.1
  85. ResourceRepresentation
  86. https://www.keycloak.org/docs-api/20.0.0/rest-api/index.html#_resourcerepresentation
  87. :param payload: ResourceRepresentation
  88. :type payload: dict
  89. :return: ResourceRepresentation with the _id property assigned
  90. :rtype: dict
  91. """
  92. data_raw = self.connection.raw_post(
  93. self.uma_well_known["resource_registration_endpoint"], data=json.dumps(payload)
  94. )
  95. return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201])
  96. def resource_set_update(self, resource_id, payload):
  97. """Update a resource set.
  98. Spec
  99. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#update-resource-set
  100. ResourceRepresentation
  101. https://www.keycloak.org/docs-api/20.0.0/rest-api/index.html#_resourcerepresentation
  102. :param resource_id: id of the resource
  103. :type resource_id: str
  104. :param payload: ResourceRepresentation
  105. :type payload: dict
  106. :return: Response dict (empty)
  107. :rtype: dict
  108. """
  109. url = self.format_url(
  110. self.uma_well_known["resource_registration_endpoint"] + "/{id}", id=resource_id
  111. )
  112. data_raw = self.connection.raw_put(url, data=json.dumps(payload))
  113. return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204])
  114. def resource_set_read(self, resource_id):
  115. """Read a resource set.
  116. Spec
  117. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#read-resource-set
  118. ResourceRepresentation
  119. https://www.keycloak.org/docs-api/20.0.0/rest-api/index.html#_resourcerepresentation
  120. :param resource_id: id of the resource
  121. :type resource_id: str
  122. :return: ResourceRepresentation
  123. :rtype: dict
  124. """
  125. url = self.format_url(
  126. self.uma_well_known["resource_registration_endpoint"] + "/{id}", id=resource_id
  127. )
  128. data_raw = self.connection.raw_get(url)
  129. return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200])
  130. def resource_set_delete(self, resource_id):
  131. """Delete a resource set.
  132. Spec
  133. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#delete-resource-set
  134. :param resource_id: id of the resource
  135. :type resource_id: str
  136. :return: Response dict (empty)
  137. :rtype: dict
  138. """
  139. url = self.format_url(
  140. self.uma_well_known["resource_registration_endpoint"] + "/{id}", id=resource_id
  141. )
  142. data_raw = self.connection.raw_delete(url)
  143. return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204])
  144. def resource_set_list_ids(self):
  145. """List all resource set ids.
  146. Spec
  147. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#list-resource-sets
  148. :return: List of ids
  149. :rtype: List[str]
  150. """
  151. data_raw = self.connection.raw_get(self.uma_well_known["resource_registration_endpoint"])
  152. return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200])
  153. def resource_set_list(self):
  154. """List all resource sets.
  155. Spec
  156. https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#list-resource-sets
  157. ResourceRepresentation
  158. https://www.keycloak.org/docs-api/20.0.0/rest-api/index.html#_resourcerepresentation
  159. :yields: Iterator over a list of ResourceRepresentations
  160. :rtype: Iterator[dict]
  161. """
  162. for resource_id in self.resource_set_list_ids():
  163. resource = self.resource_set_read(resource_id)
  164. yield resource