error_handling.py 4.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. """
  2. Auth error handling.
  3. Beware: There is a historical confusion of naming between authentication and authorization.
  4. Names of Responses have to be kept as they were called in original W3 protocols.
  5. See explanation below.
  6. """
  7. from __future__ import annotations
  8. from typing import Callable
  9. from flask import request, jsonify, current_app
  10. from flexmeasures.utils.error_utils import log_error
  11. # "Unauthorized"
  12. # "The request requires user authentication. The response MUST include a WWW-Authenticate header field."
  13. # So this essentially means the user needs to authenticate!
  14. # For the historical confusion between "authorize" and "authenticate" in this status' name,
  15. # see https://robertlathanh.com/2012/06/http-status-codes-401-unauthorized-and-403-forbidden-for-authentication-and-authorization-and-oauth/
  16. UNAUTH_STATUS_CODE = 401
  17. UNAUTH_ERROR_CLASS = "Unauthorized"
  18. UNAUTH_ERROR_STATUS = (
  19. "UNAUTHORIZED" # keeping the historical name intact for protocol consistency.
  20. )
  21. UNAUTH_MSG = (
  22. "You could not be properly authenticated for this content or functionality."
  23. )
  24. # "Forbidden"
  25. # "The server understood the request, but is refusing to fulfil it. Authorization will not help and the request SHOULD NOT be repeated."
  26. # So this is the real authorization status!
  27. # Preferably to be used when the user is logged in but is not authorized for the resource.
  28. # Advice: a not logged-in user should preferably see a 404 NotFound.
  29. FORBIDDEN_STATUS_CODE = 403
  30. FORBIDDEN_ERROR_CLASS = "InvalidSender"
  31. FORBIDDEN_ERROR_STATUS = "INVALID_SENDER"
  32. FORBIDDEN_MSG = "You cannot be authorized for this content or functionality."
  33. def unauthorized_handler_e(e):
  34. """Swallow error. Useful for classical Flask error handler registration."""
  35. log_error(e, str(e))
  36. return unauthorized_handler()
  37. def unauthorized_handler(func: Callable | None = None, params: list | None = None):
  38. """
  39. Handler for authorization problems.
  40. :param func: the Flask-Security-Too decorator, if relevant, and params are its parameters.
  41. We respond with json if the request doesn't say otherwise.
  42. Also, other FlexMeasures packages can define that they want to wrap JSON responses
  43. and/or render HTML error pages (for non-JSON requests) in custom ways ―
  44. by registering unauthorized_handler_api & unauthorized_handler_html, respectively.
  45. """
  46. if request.is_json or request.content_type is None:
  47. if hasattr(current_app, "unauthorized_handler_api"):
  48. return current_app.unauthorized_handler_api(params)
  49. response = jsonify(dict(message=FORBIDDEN_MSG, status=FORBIDDEN_ERROR_STATUS))
  50. response.status_code = FORBIDDEN_STATUS_CODE
  51. return response
  52. if hasattr(current_app, "unauthorized_handler_html"):
  53. return current_app.unauthorized_handler_html()
  54. return "%s:%s" % (FORBIDDEN_ERROR_CLASS, FORBIDDEN_MSG), FORBIDDEN_STATUS_CODE
  55. def unauthenticated_handler_e(e):
  56. """Swallow error. Useful for classical Flask error handler registration."""
  57. log_error(e, str(e))
  58. return unauthenticated_handler()
  59. def unauthenticated_handler(
  60. mechanisms: list | None = None, headers: dict | None = None
  61. ):
  62. """
  63. Handler for authentication problems.
  64. :param mechanisms: a list of which authentication mechanisms were tried.
  65. :param headers: a dict of headers to return.
  66. We respond with json if the request doesn't say otherwise.
  67. Also, other FlexMeasures packages can define that they want to wrap JSON responses
  68. and/or render HTML error pages (for non-JSON requests) in custom ways ―
  69. by registering unauthenticated_handler_api & unauthenticated_handler_html, respectively.
  70. """
  71. if request.is_json or request.content_type is None:
  72. if hasattr(current_app, "unauthenticated_handler_api"):
  73. return current_app.unauthenticated_handler_api(None, [])
  74. response = jsonify(dict(message=UNAUTH_MSG, status=UNAUTH_ERROR_STATUS))
  75. response.status_code = UNAUTH_STATUS_CODE
  76. if headers is not None:
  77. response.headers.update(headers)
  78. return response
  79. if hasattr(current_app, "unauthenticated_handler_html"):
  80. return current_app.unauthenticated_handler_html()
  81. return "%s:%s" % (UNAUTH_ERROR_CLASS, UNAUTH_MSG), UNAUTH_STATUS_CODE