A multipurpose python flask API server and administration SPA
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.

208 lines
5.6 KiB

  1. """Middleware to handle authentication."""
  2. import base64
  3. import binascii
  4. from enum import Enum
  5. from functools import wraps
  6. from typing import Optional, Callable, Any
  7. from flask import request, Response, g, json
  8. from werkzeug.datastructures import Authorization
  9. from werkzeug.http import bytes_to_wsgi, wsgi_to_bytes
  10. from corvus.api.model import APIMessage
  11. from corvus.service import (
  12. authentication_service,
  13. user_service,
  14. user_token_service
  15. )
  16. from corvus.service.role_service import ROLES, Role
  17. from corvus.service import transformation_service
  18. class Auth(Enum):
  19. """Authentication scheme definitions."""
  20. TOKEN = 'TOKEN'
  21. BASIC = 'BASIC'
  22. NONE = 'NONE'
  23. def authenticate_with_password(
  24. name: Optional[str], password: Optional[str]) -> bool:
  25. """
  26. Authenticate a username and a password.
  27. :param name:
  28. :param password:
  29. :return:
  30. """
  31. if name is None or password is None:
  32. return False
  33. user = user_service.find_by_name(name)
  34. if user is not None \
  35. and authentication_service.is_valid_password(user, password):
  36. g.user = user
  37. return True
  38. return False
  39. def authenticate_with_token(name: Optional[str], token: Optional[str]) -> bool:
  40. """
  41. Authenticate a username and a token.
  42. :param name:
  43. :param token:
  44. :return:
  45. """
  46. if name is None or token is None:
  47. return False
  48. user = user_service.find_by_name(name)
  49. if user is not None:
  50. user_token = user_token_service.find_by_user_and_token(user, token)
  51. if user is not None \
  52. and user_token_service.is_valid_token(user_token):
  53. g.user = user
  54. g.user_token = user_token
  55. return True
  56. return False
  57. def authentication_failed(auth_type: str) -> Response:
  58. """
  59. Return a correct response for failed authentication.
  60. :param auth_type:
  61. :return:
  62. """
  63. return Response(
  64. status=401,
  65. headers={
  66. 'WWW-Authenticate': '%s realm="Login Required"' % auth_type
  67. })
  68. def authorization_failed(required_role: str) -> Response:
  69. """Return a correct response for failed authorization."""
  70. return Response(
  71. status=401,
  72. response=json.dumps({
  73. 'message': '{} role not present'.format(required_role)
  74. }),
  75. content_type='application/json'
  76. )
  77. def parse_token_header(
  78. header_value: Optional[str]) -> Optional[Authorization]:
  79. """
  80. Parse the Authorization: Token header for the username and token.
  81. :param header_value:
  82. :return:
  83. """
  84. if not header_value:
  85. return None
  86. auth_info = wsgi_to_bytes(header_value)
  87. try:
  88. username, token = base64.b64decode(auth_info).split(b':', 1)
  89. except binascii.Error:
  90. return None
  91. return Authorization('token', {'username': bytes_to_wsgi(username),
  92. 'password': bytes_to_wsgi(token)})
  93. def require_basic_auth(func: Callable) -> Callable:
  94. """
  95. Decorate require inline basic auth.
  96. :param func:
  97. :return:
  98. """
  99. @wraps(func)
  100. def decorate(*args: list, **kwargs: dict) -> Any:
  101. """
  102. Authenticate with a password.
  103. :param args:
  104. :param kwargs:
  105. :return:
  106. """
  107. auth = request.authorization
  108. if auth and authenticate_with_password(auth.username, auth.password):
  109. return func(*args, **kwargs)
  110. return authentication_failed('Basic')
  111. return decorate
  112. def require_token_auth(func: Callable) -> Callable:
  113. """
  114. Decorate require inline token auth.
  115. :param func:
  116. :return:
  117. """
  118. @wraps(func)
  119. def decorate(*args: list, **kwargs: dict) -> Any:
  120. """
  121. Authenticate with a token.
  122. :param args:
  123. :param kwargs:
  124. :return:
  125. """
  126. token = parse_token_header(
  127. request.headers.get('X-Auth-Token'))
  128. if token and authenticate_with_token(token.username, token.password):
  129. return func(*args, **kwargs)
  130. return authentication_failed('Token')
  131. return decorate
  132. def require_role(required_role: Role) -> Callable:
  133. """
  134. Decorate require a user role.
  135. :param required_role:
  136. :return:
  137. """
  138. def required_role_decorator(func: Callable) -> Callable:
  139. """Decorate the function."""
  140. @wraps(func)
  141. def decorate(*args: list, **kwargs: dict) -> Any:
  142. """Require a user role."""
  143. if g.user.role in ROLES.find_roles_in_hierarchy(required_role):
  144. return func(*args, **kwargs)
  145. return authorization_failed(required_role.value)
  146. return decorate
  147. return required_role_decorator
  148. def require(required_auth: Auth, required_role: Role) -> Callable:
  149. """
  150. Decorate require Auth and Role.
  151. :param required_auth:
  152. :param required_role:
  153. :return:
  154. """
  155. def require_decorator(func: Callable) -> Callable:
  156. @wraps(func)
  157. def decorate(*args: list, **kwargs: dict) -> Any:
  158. decorated = require_role(required_role)(func)
  159. if required_auth == Auth.BASIC:
  160. decorated = require_basic_auth(decorated)
  161. elif required_auth == Auth.TOKEN:
  162. decorated = require_token_auth(decorated)
  163. else:
  164. return Response(
  165. response=transformation_service.serialize_model(
  166. APIMessage(
  167. message="Unexpected Server Error",
  168. success=False
  169. )),
  170. status=500)
  171. return decorated(*args, **kwargs)
  172. return decorate
  173. return require_decorator