An ebook/comic library service and web client
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.

165 lines
4.4 KiB

  1. """Middleware to handle authentication."""
  2. import base64
  3. import binascii
  4. from functools import wraps
  5. from typing import Optional, Callable, Any
  6. from flask import request, Response, g, json
  7. from werkzeug.datastructures import Authorization
  8. from werkzeug.http import bytes_to_wsgi, wsgi_to_bytes
  9. from atheneum.service import (
  10. authentication_service,
  11. user_service,
  12. user_token_service
  13. )
  14. from atheneum.service.role_service import ROLES, Role
  15. def authenticate_with_password(name: str, password: str) -> bool:
  16. """
  17. Authenticate a username and a password.
  18. :param name:
  19. :param password:
  20. :return:
  21. """
  22. user = user_service.find_by_name(name)
  23. if user is not None \
  24. and authentication_service.is_valid_password(user, password):
  25. g.user = user
  26. return True
  27. return False
  28. def authenticate_with_token(name: str, token: str) -> bool:
  29. """
  30. Authenticate a username and a token.
  31. :param name:
  32. :param token:
  33. :return:
  34. """
  35. user = user_service.find_by_name(name)
  36. if user is not None:
  37. user_token = user_token_service.find_by_user_and_token(user, token)
  38. if user is not None \
  39. and authentication_service.is_valid_token(user_token):
  40. g.user = user
  41. g.user_token = user_token
  42. return True
  43. return False
  44. def authentication_failed(auth_type: str) -> Response:
  45. """
  46. Return a correct response for failed authentication.
  47. :param auth_type:
  48. :return:
  49. """
  50. return Response(
  51. status=401,
  52. headers={
  53. 'WWW-Authenticate': '%s realm="Login Required"' % auth_type
  54. })
  55. def authorization_failed(required_role: str) -> Response:
  56. """Return a correct response for failed authorization."""
  57. return Response(
  58. status=401,
  59. response=json.dumps({
  60. 'message': '{} role not present'.format(required_role)
  61. }),
  62. content_type='application/json'
  63. )
  64. def parse_token_header(
  65. header_value: str) -> Optional[Authorization]:
  66. """
  67. Parse the Authorization: Bearer header for the username and token.
  68. :param header_value:
  69. :return:
  70. """
  71. if not header_value:
  72. return None
  73. value = wsgi_to_bytes(header_value)
  74. try:
  75. auth_type, auth_info = value.split(None, 1)
  76. auth_type = auth_type.lower()
  77. except ValueError:
  78. return None
  79. if auth_type == b'bearer':
  80. try:
  81. username, token = base64.b64decode(auth_info).split(b':', 1)
  82. except binascii.Error:
  83. return None
  84. return Authorization('bearer', {'username': bytes_to_wsgi(username),
  85. 'password': bytes_to_wsgi(token)})
  86. return None
  87. def require_basic_auth(func: Callable) -> Callable:
  88. """
  89. Decorate require inline basic auth.
  90. :param func:
  91. :return:
  92. """
  93. @wraps(func)
  94. def decorate(*args: list, **kwargs: dict) -> Any:
  95. """
  96. Authenticate with a password.
  97. :param args:
  98. :param kwargs:
  99. :return:
  100. """
  101. auth = request.authorization
  102. if auth and authenticate_with_password(auth.username, auth.password):
  103. return func(*args, **kwargs)
  104. return authentication_failed('Basic')
  105. return decorate
  106. def require_token_auth(func: Callable) -> Callable:
  107. """
  108. Decorate require inline token auth.
  109. :param func:
  110. :return:
  111. """
  112. @wraps(func)
  113. def decorate(*args: list, **kwargs: dict) -> Any:
  114. """
  115. Authenticate with a token.
  116. :param args:
  117. :param kwargs:
  118. :return:
  119. """
  120. token = parse_token_header(
  121. request.headers.get('Authorization', None))
  122. if token and authenticate_with_token(token.username, token.password):
  123. return func(*args, **kwargs)
  124. return authentication_failed('Bearer')
  125. return decorate
  126. def require_role(required_role: Role) -> Callable:
  127. """Decorate require user role."""
  128. def required_role_decorator(func: Callable) -> Callable:
  129. """Decorate the function."""
  130. @wraps(func)
  131. def decorate(*args: list, **kwargs: dict) -> Any:
  132. """Require a user role."""
  133. if g.user.role in ROLES.find_roles_in_hierarchy(required_role):
  134. return func(*args, **kwargs)
  135. return authorization_failed(required_role.value)
  136. return decorate
  137. return required_role_decorator