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.

214 lines
7.0 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 assembly 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. self.resource = resource
  44. self.scope = scope
  45. if permission:
  46. if not isinstance(permission, UMAPermission):
  47. raise PermissionDefinitionError(
  48. "can't determine if '{}' is a resource or scope".format(permission)
  49. )
  50. if permission.resource:
  51. self.resource = str(permission.resource)
  52. if permission.scope:
  53. self.scope = str(permission.scope)
  54. def __str__(self):
  55. """Str method."""
  56. scope = self.scope
  57. if scope:
  58. scope = "#" + scope
  59. return "{}{}".format(self.resource, scope)
  60. def __eq__(self, __o: object) -> bool:
  61. """Eq method."""
  62. return str(self) == str(__o)
  63. def __repr__(self) -> str:
  64. """Repr method."""
  65. return self.__str__()
  66. def __hash__(self) -> int:
  67. """Hash method."""
  68. return hash(str(self))
  69. def __call__(self, permission=None, resource="", scope="") -> object:
  70. """Call method."""
  71. result_resource = self.resource
  72. result_scope = self.scope
  73. if resource:
  74. result_resource = str(resource)
  75. if scope:
  76. result_scope = str(scope)
  77. if permission:
  78. if not isinstance(permission, UMAPermission):
  79. raise PermissionDefinitionError(
  80. "can't determine if '{}' is a resource or scope".format(permission)
  81. )
  82. if permission.resource:
  83. result_resource = str(permission.resource)
  84. if permission.scope:
  85. result_scope = str(permission.scope)
  86. return UMAPermission(resource=result_resource, scope=result_scope)
  87. class Resource(UMAPermission):
  88. """An UMAPermission Resource class to conveniently assembly permissions.
  89. The class itself is callable, and will return the assembled permission.
  90. :param resource: Resource
  91. :type resource: str
  92. """
  93. def __init__(self, resource):
  94. """Init method."""
  95. super().__init__(resource=resource)
  96. class Scope(UMAPermission):
  97. """An UMAPermission Scope class to conveniently assembly permissions.
  98. The class itself is callable, and will return the assembled permission.
  99. :param scope: Scope
  100. :type scope: str
  101. """
  102. def __init__(self, scope):
  103. """Init method."""
  104. super().__init__(scope=scope)
  105. class AuthStatus:
  106. """A class that represents the authorization/login status of a user associated with a token.
  107. This has to evaluate to True if and only if the user is properly authorized
  108. for the requested resource.
  109. :param is_logged_in: Is logged in indicator
  110. :type is_logged_in: bool
  111. :param is_authorized: Is authorized indicator
  112. :type is_authorized: bool
  113. :param missing_permissions: Missing permissions
  114. :type missing_permissions: set
  115. """
  116. def __init__(self, is_logged_in, is_authorized, missing_permissions):
  117. """Init method."""
  118. self.is_logged_in = is_logged_in
  119. self.is_authorized = is_authorized
  120. self.missing_permissions = missing_permissions
  121. def __bool__(self):
  122. """Bool method."""
  123. return self.is_authorized
  124. def __repr__(self):
  125. """Repr method."""
  126. return (
  127. f"AuthStatus("
  128. f"is_authorized={self.is_authorized}, "
  129. f"is_logged_in={self.is_logged_in}, "
  130. f"missing_permissions={self.missing_permissions})"
  131. )
  132. def build_permission_param(permissions):
  133. """Transform permissions to a set, so they are usable for requests.
  134. :param permissions: either str (resource#scope),
  135. iterable[str] (resource#scope),
  136. dict[str,str] (resource: scope),
  137. dict[str,iterable[str]] (resource: scopes)
  138. :return: result bool
  139. """
  140. if permissions is None or permissions == "":
  141. return set()
  142. if isinstance(permissions, str):
  143. return set((permissions,))
  144. if isinstance(permissions, UMAPermission):
  145. return set((str(permissions),))
  146. try: # treat as dictionary of permissions
  147. result = set()
  148. for resource, scopes in permissions.items():
  149. print(f"resource={resource}scopes={scopes}")
  150. if scopes is None:
  151. result.add(resource)
  152. elif isinstance(scopes, str):
  153. result.add("{}#{}".format(resource, scopes))
  154. else:
  155. try:
  156. for scope in scopes:
  157. if not isinstance(scope, str):
  158. raise KeycloakPermissionFormatError(
  159. "misbuilt permission {}".format(permissions)
  160. )
  161. result.add("{}#{}".format(resource, scope))
  162. except TypeError:
  163. raise KeycloakPermissionFormatError(
  164. "misbuilt permission {}".format(permissions)
  165. )
  166. return result
  167. except AttributeError:
  168. pass
  169. result = set()
  170. for permission in permissions:
  171. if not isinstance(permission, (str, UMAPermission)):
  172. raise KeycloakPermissionFormatError("misbuilt permission {}".format(permissions))
  173. result.add(str(permission))
  174. return result