app_utils.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. """
  2. Utils for serving the FlexMeasures app
  3. """
  4. from __future__ import annotations
  5. import os
  6. import sys
  7. import click
  8. from flask import Flask, current_app, redirect
  9. from flask.cli import FlaskGroup, with_appcontext
  10. from flask_security import current_user
  11. import sentry_sdk
  12. from sentry_sdk.integrations.flask import FlaskIntegration
  13. from sentry_sdk.integrations.rq import RqIntegration
  14. from pkg_resources import get_distribution
  15. from flexmeasures.app import create as create_app
  16. @click.group(cls=FlaskGroup, create_app=create_app)
  17. @with_appcontext
  18. def flexmeasures_cli():
  19. """
  20. Management scripts for the FlexMeasures platform.
  21. """
  22. # We use @app_context above, so things from the app setup are initialised
  23. # only once! This is crucial for Sentry, for example.
  24. pass
  25. def init_sentry(app: Flask):
  26. """
  27. Configure Sentry.
  28. We need the app to read the Sentry DSN from configuration, and also
  29. to send some additional meta information.
  30. """
  31. sentry_dsn = app.config.get("SENTRY_DSN")
  32. if not sentry_dsn:
  33. app.logger.info(
  34. "[FLEXMEASURES] No SENTRY_DSN setting found, so initialising Sentry cannot happen ..."
  35. )
  36. return
  37. app.logger.info("[FLEXMEASURES] Initialising Sentry ...")
  38. sentry_sdk.init(
  39. dsn=sentry_dsn,
  40. integrations=[FlaskIntegration(), RqIntegration()],
  41. debug=app.debug,
  42. release=f"flexmeasures@{get_distribution('flexmeasures').version}",
  43. send_default_pii=True, # user data (current user id, email address, username) is attached to the event.
  44. environment=app.config.get("FLEXMEASURES_ENV"),
  45. **app.config["FLEXMEASURES_SENTRY_CONFIG"],
  46. )
  47. sentry_sdk.set_tag("mode", app.config.get("FLEXMEASURES_MODE"))
  48. sentry_sdk.set_tag("platform-name", app.config.get("FLEXMEASURES_PLATFORM_NAME"))
  49. def set_secret_key(app, filename="secret_key"):
  50. """Set the SECRET_KEY or exit.
  51. We first check if it is already in the config.
  52. Then we look for it in environment var SECRET_KEY.
  53. Finally, we look for `filename` in the app's instance directory.
  54. If nothing is found, we print instructions
  55. to create the secret and then exit.
  56. """
  57. secret_key = app.config.get("SECRET_KEY", None)
  58. if secret_key is not None:
  59. return
  60. secret_key = os.environ.get("SECRET_KEY", None)
  61. if secret_key is not None:
  62. app.config["SECRET_KEY"] = secret_key
  63. return
  64. filename = os.path.join(app.instance_path, filename)
  65. try:
  66. app.config["SECRET_KEY"] = open(filename, "rb").read()
  67. except IOError:
  68. app.logger.error(
  69. """
  70. Error: No secret key set.
  71. You can add the SECRET_KEY setting to your conf file (this example works only on Unix):
  72. echo "SECRET_KEY=\"`python3 -c 'import secrets; print(secrets.token_hex(24))'`\"" >> ~/.flexmeasures.cfg
  73. OR you can add an env var:
  74. export SECRET_KEY=xxxxxxxxxxxxxxx
  75. (on windows, use "set" instead of "export")
  76. OR you can create a secret key file (this example works only on Unix):
  77. mkdir -p %s
  78. head -c 24 /dev/urandom > %s
  79. You can also use Python to create a good secret:
  80. python -c "import secrets; print(secrets.token_urlsafe())"
  81. """
  82. % (os.path.dirname(filename), filename)
  83. )
  84. sys.exit(2)
  85. def root_dispatcher():
  86. """
  87. Re-routes to root views fitting for the current user,
  88. depending on the FLEXMEASURES_ROOT_VIEW setting.
  89. """
  90. default_root_view = "/dashboard"
  91. root_view = default_root_view
  92. configs = current_app.config.get("FLEXMEASURES_ROOT_VIEW", [])
  93. root_view = find_first_applicable_config_entry(configs, "FLEXMEASURES_ROOT_VIEW")
  94. if root_view in ("", "/", None):
  95. root_view = default_root_view
  96. if not root_view.startswith("/"):
  97. root_view = f"/{root_view}"
  98. current_app.logger.info(f"Redirecting root view to {root_view} ...")
  99. return redirect(root_view)
  100. def find_first_applicable_config_entry(
  101. configs: list, setting_name: str, app: Flask | None = None
  102. ) -> str | None:
  103. if app is None:
  104. app = current_app
  105. if isinstance(configs, str):
  106. configs = [configs] # ignore: type
  107. for config in configs:
  108. entry = parse_config_entry_by_account_roles(config, setting_name, app)
  109. if entry is not None:
  110. return entry
  111. return None
  112. def parse_config_entry_by_account_roles(
  113. config: str | tuple[str, list[str]],
  114. setting_name: str,
  115. app: Flask | None = None,
  116. ) -> str | None:
  117. """
  118. Parse a config entry (which can be a string, e.g. "dashboard" or a tuple, e.g. ("dashboard", ["MDC"])).
  119. In the latter case, return the first item (a string) only if the current user's account roles match with the
  120. list of roles in the second item. Otherwise, return None.
  121. """
  122. if app is None:
  123. app = current_app
  124. if isinstance(config, str):
  125. return config
  126. elif isinstance(config, tuple) and len(config) == 2:
  127. entry, account_role_names = config
  128. if not isinstance(entry, str):
  129. app.logger.warning(
  130. f"View name setting '{entry}' in {setting_name} is not a string. Ignoring ..."
  131. )
  132. return None
  133. if not isinstance(account_role_names, list):
  134. app.logger.warning(
  135. f"Role names setting '{account_role_names}' in {setting_name} is not a list. Ignoring ..."
  136. )
  137. return None
  138. if not hasattr(current_user, "account"):
  139. # e.g. AnonymousUser
  140. return None
  141. for account_role_name in account_role_names:
  142. if account_role_name in [
  143. role.name for role in current_user.account.account_roles
  144. ]:
  145. return entry
  146. else:
  147. app.logger.warning(
  148. f"Setting '{config}' in {setting_name} is neither a string nor two-part tuple. Ignoring ..."
  149. )
  150. return None