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.

275 lines
8.5 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. """User-managed access permissions module."""
  24. from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError
  25. class UMAPermission:
  26. """A class to conveniently assemble permissions.
  27. The class itself is callable, and will return the assembled permission.
  28. Usage example:
  29. >>> r = Resource("Users")
  30. >>> s = Scope("delete")
  31. >>> permission = r(s)
  32. >>> print(permission)
  33. 'Users#delete'
  34. :param permission: Permission
  35. :type permission: UMAPermission
  36. :param resource: Resource
  37. :type resource: str
  38. :param scope: Scope
  39. :type scope: str
  40. """
  41. def __init__(self, permission=None, resource="", scope=""):
  42. """Init method.
  43. :param permission: Permission
  44. :type permission: UMAPermission
  45. :param resource: Resource
  46. :type resource: str
  47. :param scope: Scope
  48. :type scope: str
  49. :raises PermissionDefinitionError: In case bad permission definition
  50. """
  51. self.resource = resource
  52. self.scope = scope
  53. if permission:
  54. if not isinstance(permission, UMAPermission):
  55. raise PermissionDefinitionError(
  56. "can't determine if '{}' is a resource or scope".format(permission)
  57. )
  58. if permission.resource:
  59. self.resource = str(permission.resource)
  60. if permission.scope:
  61. self.scope = str(permission.scope)
  62. def __str__(self):
  63. """Str method.
  64. :returns: String representation
  65. :rtype: str
  66. """
  67. scope = self.scope
  68. if scope:
  69. scope = "#" + scope
  70. return "{}{}".format(self.resource, scope)
  71. def __eq__(self, __o: object) -> bool:
  72. """Eq method.
  73. :param __o: The other object
  74. :type __o: object
  75. :returns: Equality boolean
  76. :rtype: bool
  77. """
  78. return str(self) == str(__o)
  79. def __repr__(self) -> str:
  80. """Repr method.
  81. :returns: The object representation
  82. :rtype: str
  83. """
  84. return self.__str__()
  85. def __hash__(self) -> int:
  86. """Hash method.
  87. :returns: Hash of the object
  88. :rtype: int
  89. """
  90. return hash(str(self))
  91. def __call__(self, permission=None, resource="", scope="") -> "UMAPermission":
  92. """Call method.
  93. :param permission: Permission
  94. :type permission: UMAPermission
  95. :param resource: Resource
  96. :type resource: str
  97. :param scope: Scope
  98. :type scope: str
  99. :returns: The combined UMA permission
  100. :rtype: UMAPermission
  101. :raises PermissionDefinitionError: In case bad permission definition
  102. """
  103. result_resource = self.resource
  104. result_scope = self.scope
  105. if resource:
  106. result_resource = str(resource)
  107. if scope:
  108. result_scope = str(scope)
  109. if permission:
  110. if not isinstance(permission, UMAPermission):
  111. raise PermissionDefinitionError(
  112. "can't determine if '{}' is a resource or scope".format(permission)
  113. )
  114. if permission.resource:
  115. result_resource = str(permission.resource)
  116. if permission.scope:
  117. result_scope = str(permission.scope)
  118. return UMAPermission(resource=result_resource, scope=result_scope)
  119. class Resource(UMAPermission):
  120. """A UMAPermission Resource class to conveniently assemble permissions.
  121. The class itself is callable, and will return the assembled permission.
  122. :param resource: Resource
  123. :type resource: str
  124. """
  125. def __init__(self, resource):
  126. """Init method.
  127. :param resource: Resource
  128. :type resource: str
  129. """
  130. super().__init__(resource=resource)
  131. class Scope(UMAPermission):
  132. """A UMAPermission Scope class to conveniently assemble permissions.
  133. The class itself is callable, and will return the assembled permission.
  134. :param scope: Scope
  135. :type scope: str
  136. """
  137. def __init__(self, scope):
  138. """Init method.
  139. :param scope: Scope
  140. :type scope: str
  141. """
  142. super().__init__(scope=scope)
  143. class AuthStatus:
  144. """A class that represents the authorization/login status of a user associated with a token.
  145. This has to evaluate to True if and only if the user is properly authorized
  146. for the requested resource.
  147. :param is_logged_in: Is logged in indicator
  148. :type is_logged_in: bool
  149. :param is_authorized: Is authorized indicator
  150. :type is_authorized: bool
  151. :param missing_permissions: Missing permissions
  152. :type missing_permissions: set
  153. """
  154. def __init__(self, is_logged_in, is_authorized, missing_permissions):
  155. """Init method.
  156. :param is_logged_in: Is logged in indicator
  157. :type is_logged_in: bool
  158. :param is_authorized: Is authorized indicator
  159. :type is_authorized: bool
  160. :param missing_permissions: Missing permissions
  161. :type missing_permissions: set
  162. """
  163. self.is_logged_in = is_logged_in
  164. self.is_authorized = is_authorized
  165. self.missing_permissions = missing_permissions
  166. def __bool__(self):
  167. """Bool method.
  168. :returns: Boolean representation
  169. :rtype: bool
  170. """
  171. return self.is_authorized
  172. def __repr__(self):
  173. """Repr method.
  174. :returns: The object representation
  175. :rtype: str
  176. """
  177. return (
  178. f"AuthStatus("
  179. f"is_authorized={self.is_authorized}, "
  180. f"is_logged_in={self.is_logged_in}, "
  181. f"missing_permissions={self.missing_permissions})"
  182. )
  183. def build_permission_param(permissions):
  184. """Transform permissions to a set, so they are usable for requests.
  185. :param permissions: Permissions
  186. :type permissions: str | Iterable[str] | dict[str, str] | dict[str, Iterabble[str]]
  187. :returns: Permission parameters
  188. :rtype: set
  189. :raises KeycloakPermissionFormatError: In case of bad permission format
  190. """
  191. if permissions is None or permissions == "":
  192. return set()
  193. if isinstance(permissions, str):
  194. return set((permissions,))
  195. if isinstance(permissions, UMAPermission):
  196. return set((str(permissions),))
  197. try: # treat as dictionary of permissions
  198. result = set()
  199. for resource, scopes in permissions.items():
  200. if scopes is None:
  201. result.add(resource)
  202. elif isinstance(scopes, str):
  203. result.add("{}#{}".format(resource, scopes))
  204. else:
  205. try:
  206. for scope in scopes:
  207. if not isinstance(scope, str):
  208. raise KeycloakPermissionFormatError(
  209. "misbuilt permission {}".format(permissions)
  210. )
  211. result.add("{}#{}".format(resource, scope))
  212. except TypeError:
  213. raise KeycloakPermissionFormatError(
  214. "misbuilt permission {}".format(permissions)
  215. )
  216. return result
  217. except AttributeError:
  218. pass
  219. result = set()
  220. for permission in permissions:
  221. if not isinstance(permission, (str, UMAPermission)):
  222. raise KeycloakPermissionFormatError("misbuilt permission {}".format(permissions))
  223. result.add(str(permission))
  224. return result