users.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. from __future__ import annotations
  2. import random
  3. import string
  4. from flask import current_app
  5. from flask_security import current_user, SQLAlchemySessionUserDatastore
  6. from flask_security.recoverable import update_password
  7. from email_validator import (
  8. validate_email,
  9. EmailNotValidError,
  10. EmailUndeliverableError,
  11. )
  12. from email_validator.deliverability import validate_email_deliverability
  13. from flask_security.utils import hash_password
  14. from werkzeug.exceptions import NotFound
  15. from sqlalchemy import select, delete
  16. from flexmeasures.data import db
  17. from flexmeasures.data.models.data_sources import DataSource
  18. from flexmeasures.data.models.audit_log import AuditLog
  19. from flexmeasures.data.models.user import User, Role, Account
  20. from flexmeasures.utils.time_utils import server_now
  21. class InvalidFlexMeasuresUser(Exception):
  22. pass
  23. def get_user(id: str) -> User:
  24. """Get a user, raise if not found."""
  25. user: User = db.session.get(User, int(id))
  26. if user is None:
  27. raise NotFound
  28. return user
  29. def find_user_by_email(user_email: str, keep_in_session: bool = True) -> User:
  30. user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role)
  31. user = user_datastore.find_user(email=user_email)
  32. if not keep_in_session:
  33. # we might need this object persistent across requests
  34. db.session.expunge(user)
  35. return user
  36. def create_user( # noqa: C901
  37. password: str = None,
  38. user_roles: dict[str, str] | list[dict[str, str]] | str | list[str] | None = None,
  39. check_email_deliverability: bool = True,
  40. account_name: str | None = None,
  41. **kwargs,
  42. ) -> User:
  43. """
  44. Convenience wrapper to create a new User object.
  45. It hashes the password.
  46. In addition to the user, this function can create
  47. - new Role objects (if user roles do not already exist)
  48. - an Account object (if it does not exist yet)
  49. - a new DataSource object that corresponds to the user
  50. Remember to commit the session after calling this function!
  51. """
  52. # Check necessary input explicitly before anything happens
  53. if password is None or password == "":
  54. raise InvalidFlexMeasuresUser("No password provided.")
  55. if "email" not in kwargs:
  56. raise InvalidFlexMeasuresUser("No email address provided.")
  57. email = kwargs.pop("email").strip()
  58. try:
  59. email_info = validate_email(email, check_deliverability=False)
  60. # The mx check talks to the SMTP server. During testing, we skip it because it
  61. # takes a bit of time and without internet connection it fails.
  62. if check_email_deliverability and not current_app.testing:
  63. try:
  64. validate_email_deliverability(
  65. email_info.domain, email_info["domain_i18n"]
  66. )
  67. except EmailUndeliverableError as eue:
  68. raise InvalidFlexMeasuresUser(
  69. "The email address %s does not seem to be deliverable: %s"
  70. % (email, str(eue))
  71. )
  72. except EmailNotValidError as enve:
  73. raise InvalidFlexMeasuresUser(
  74. "%s is not a valid email address: %s" % (email, str(enve))
  75. )
  76. if "username" not in kwargs:
  77. username = email.split("@")[0]
  78. else:
  79. username = kwargs.pop("username").strip()
  80. # Check integrity explicitly before anything happens
  81. existing_user_by_email = db.session.execute(
  82. select(User).filter_by(email=email)
  83. ).scalar_one_or_none()
  84. if existing_user_by_email is not None:
  85. raise InvalidFlexMeasuresUser("User with email %s already exists." % email)
  86. existing_user_by_username = db.session.execute(
  87. select(User).filter_by(username=username)
  88. ).scalar_one_or_none()
  89. if existing_user_by_username is not None:
  90. raise InvalidFlexMeasuresUser(
  91. "User with username %s already exists." % username
  92. )
  93. # check if we can link/create an account
  94. if account_name is None:
  95. raise InvalidFlexMeasuresUser(
  96. "Cannot create user without knowing the name of the account which this user is associated with."
  97. )
  98. account = db.session.execute(
  99. select(Account).filter_by(name=account_name)
  100. ).scalar_one_or_none()
  101. active_user_id, active_user_name = None, None
  102. if hasattr(current_user, "id"):
  103. active_user_id, active_user_name = current_user.id, current_user.username
  104. if account is None:
  105. print(f"Creating account {account_name} ...")
  106. account = Account(name=account_name)
  107. db.session.add(account)
  108. db.session.flush()
  109. account_audit_log = AuditLog(
  110. event_datetime=server_now(),
  111. event=f"Account {account_name} created",
  112. active_user_id=active_user_id,
  113. active_user_name=active_user_name,
  114. affected_account_id=account.id,
  115. )
  116. db.session.add(account_audit_log)
  117. user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role)
  118. kwargs.update(password=hash_password(password), email=email, username=username)
  119. user = user_datastore.create_user(**kwargs)
  120. user.account = account
  121. # add roles to user (creating new roles if necessary)
  122. if user_roles:
  123. if not isinstance(user_roles, list):
  124. user_roles = [user_roles] # type: ignore
  125. for user_role in user_roles:
  126. if isinstance(user_role, dict):
  127. role = user_datastore.find_role(user_role["name"])
  128. else:
  129. role = user_datastore.find_role(user_role)
  130. if role is None:
  131. if isinstance(user_role, dict):
  132. role = user_datastore.create_role(**user_role)
  133. else:
  134. role = user_datastore.create_role(name=user_role)
  135. user_datastore.add_role_to_user(user, role)
  136. # create data source
  137. db.session.add(DataSource(user=user))
  138. db.session.flush()
  139. user_audit_log = AuditLog(
  140. event_datetime=server_now(),
  141. event=f"User {user.username} created",
  142. active_user_id=active_user_id,
  143. active_user_name=active_user_name,
  144. affected_user_id=user.id,
  145. affected_account_id=account.id,
  146. )
  147. db.session.add(user_audit_log)
  148. return user
  149. def set_random_password(user: User):
  150. """
  151. Randomise a user's password.
  152. Remember to commit the session after calling this function!
  153. """
  154. new_random_password = "".join(
  155. [random.choice(string.ascii_lowercase) for _ in range(24)]
  156. )
  157. update_password(user, new_random_password)
  158. active_user_id, active_user_name = None, None
  159. if hasattr(current_user, "id"):
  160. active_user_id, active_user_name = current_user.id, current_user.username
  161. user_audit_log = AuditLog(
  162. event_datetime=server_now(),
  163. event=f"Password reset for user {user.username}",
  164. active_user_id=active_user_id,
  165. active_user_name=active_user_name,
  166. affected_user_id=user.id,
  167. )
  168. db.session.add(user_audit_log)
  169. def remove_cookie_and_token_access(user: User):
  170. """
  171. Remove access of current cookies and auth tokens for a user.
  172. This might be useful if you feel their password, cookie or tokens
  173. are compromised. in the former case, you can also call `set_random_password`.
  174. Remember to commit the session after calling this function!
  175. """
  176. user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role)
  177. user_datastore.reset_user_access(user)
  178. def delete_user(user: User):
  179. """
  180. Delete the user (and also his assets and power measurements!).
  181. Deleting oneself is not allowed.
  182. Remember to commit the session after calling this function!
  183. """
  184. if hasattr(current_user, "id") and user.id == current_user.id:
  185. raise Exception("You cannot delete yourself.")
  186. user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role)
  187. user_datastore.delete_user(user)
  188. db.session.execute(delete(User).filter_by(id=user.id))
  189. current_app.logger.info("Deleted %s." % user)
  190. active_user_id, active_user_name = None, None
  191. if hasattr(current_user, "id"):
  192. active_user_id, active_user_name = current_user.id, current_user.username
  193. user_audit_log = AuditLog(
  194. event_datetime=server_now(),
  195. event=f"User {user.username} deleted",
  196. active_user_id=active_user_id,
  197. active_user_name=active_user_name,
  198. affected_user_id=None, # add the audit log record even if the user is gone
  199. affected_account_id=user.account_id,
  200. )
  201. db.session.add(user_audit_log)