user.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. # -*- coding: utf-8 -*-
  2. from smtplib import SMTPException
  3. import transaction
  4. import typing as typing
  5. from sqlalchemy import or_
  6. from sqlalchemy.orm import Session
  7. from sqlalchemy.orm import Query
  8. from sqlalchemy.orm.exc import NoResultFound
  9. from tracim_backend.config import CFG
  10. from tracim_backend.models.auth import User
  11. from tracim_backend.models.auth import Group
  12. from tracim_backend.exceptions import NoUserSetted
  13. from tracim_backend.exceptions import EmailAlreadyExistInDb
  14. from tracim_backend.exceptions import TooShortAutocompleteString
  15. from tracim_backend.exceptions import PasswordDoNotMatch
  16. from tracim_backend.exceptions import EmailValidationFailed
  17. from tracim_backend.exceptions import UserDoesNotExist
  18. from tracim_backend.exceptions import WrongUserPassword
  19. from tracim_backend.exceptions import AuthenticationFailed
  20. from tracim_backend.exceptions import NotificationNotSend
  21. from tracim_backend.exceptions import UserNotActive
  22. from tracim_backend.models.context_models import UserInContext
  23. from tracim_backend.lib.mail_notifier.notifier import get_email_manager
  24. from tracim_backend.models.context_models import TypeUser
  25. from tracim_backend.models.data import UserRoleInWorkspace
  26. class UserApi(object):
  27. def __init__(
  28. self,
  29. current_user: typing.Optional[User],
  30. session: Session,
  31. config: CFG,
  32. show_deleted: bool = False,
  33. ) -> None:
  34. self._session = session
  35. self._user = current_user
  36. self._config = config
  37. self._show_deleted = show_deleted
  38. def _base_query(self):
  39. query = self._session.query(User)
  40. if not self._show_deleted:
  41. query = query.filter(User.is_deleted == False)
  42. return query
  43. def get_user_with_context(self, user: User) -> UserInContext:
  44. """
  45. Return UserInContext object from User
  46. """
  47. user = UserInContext(
  48. user=user,
  49. dbsession=self._session,
  50. config=self._config,
  51. )
  52. return user
  53. # Getters
  54. def get_one(self, user_id: int) -> User:
  55. """
  56. Get one user by user id
  57. """
  58. try:
  59. user = self._base_query().filter(User.user_id == user_id).one()
  60. except NoResultFound as exc:
  61. raise UserDoesNotExist('User "{}" not found in database'.format(user_id)) from exc # nopep8
  62. return user
  63. def get_one_by_email(self, email: str) -> User:
  64. """
  65. Get one user by email
  66. :param email: Email of the user
  67. :return: one user
  68. """
  69. try:
  70. user = self._base_query().filter(User.email == email).one()
  71. except NoResultFound as exc:
  72. raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc # nopep8
  73. return user
  74. def get_one_by_public_name(self, public_name: str) -> User:
  75. """
  76. Get one user by public_name
  77. """
  78. try:
  79. user = self._base_query().filter(User.display_name == public_name).one()
  80. except NoResultFound as exc:
  81. raise UserDoesNotExist('User "{}" not found in database'.format(public_name)) from exc # nopep8
  82. return user
  83. # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
  84. def get_one_by_id(self, id: int) -> User:
  85. return self.get_one(user_id=id)
  86. def get_current_user(self) -> User:
  87. """
  88. Get current_user
  89. """
  90. if not self._user:
  91. raise UserDoesNotExist('There is no current user')
  92. return self._user
  93. def _get_all_query(self) -> Query:
  94. return self._session.query(User).order_by(User.display_name)
  95. def get_all(self) -> typing.Iterable[User]:
  96. return self._get_all_query().all()
  97. def get_known_user(
  98. self,
  99. acp: str,
  100. ) -> typing.Iterable[User]:
  101. """
  102. Return list of know user by current UserApi user.
  103. :param acp: autocomplete filter by name/email
  104. :return: List of found users
  105. """
  106. if len(acp) < 2:
  107. raise TooShortAutocompleteString(
  108. '"{acp}" is a too short string, acp string need to have more than one character'.format(acp=acp) # nopep8
  109. )
  110. query = self._get_all_query()
  111. query = query.filter(or_(User.display_name.ilike('%{}%'.format(acp)), User.email.ilike('%{}%'.format(acp)))) # nopep8
  112. # INFO - G.M - 2018-07-27 - if user is set and is simple user, we
  113. # should show only user in same workspace as user
  114. if self._user and self._user.profile.id <= Group.TIM_USER:
  115. user_workspaces_id_query = self._session.\
  116. query(UserRoleInWorkspace.workspace_id).\
  117. distinct(UserRoleInWorkspace.workspace_id).\
  118. filter(UserRoleInWorkspace.user_id == self._user.user_id)
  119. users_in_workspaces = self._session.\
  120. query(UserRoleInWorkspace.user_id).\
  121. distinct(UserRoleInWorkspace.user_id).\
  122. filter(UserRoleInWorkspace.workspace_id.in_(user_workspaces_id_query.subquery())).subquery() # nopep8
  123. query = query.filter(User.user_id.in_(users_in_workspaces))
  124. return query.all()
  125. def find(
  126. self,
  127. user_id: int=None,
  128. email: str=None,
  129. public_name: str=None
  130. ) -> typing.Tuple[TypeUser, User]:
  131. """
  132. Find existing user from all theses params.
  133. Check is made in this order: user_id, email, public_name
  134. If no user found raise UserDoesNotExist exception
  135. """
  136. user = None
  137. if user_id:
  138. try:
  139. user = self.get_one(user_id)
  140. return TypeUser.USER_ID, user
  141. except UserDoesNotExist:
  142. pass
  143. if email:
  144. try:
  145. user = self.get_one_by_email(email)
  146. return TypeUser.EMAIL, user
  147. except UserDoesNotExist:
  148. pass
  149. if public_name:
  150. try:
  151. user = self.get_one_by_public_name(public_name)
  152. return TypeUser.PUBLIC_NAME, user
  153. except UserDoesNotExist:
  154. pass
  155. raise UserDoesNotExist('User not found with any of given params.')
  156. # Check methods
  157. def user_with_email_exists(self, email: str) -> bool:
  158. try:
  159. self.get_one_by_email(email)
  160. return True
  161. # TODO - G.M - 09-04-2018 - Better exception
  162. except:
  163. return False
  164. def authenticate_user(self, email: str, password: str) -> User:
  165. """
  166. Authenticate user with email and password, raise AuthenticationFailed
  167. if uncorrect.
  168. :param email: email of the user
  169. :param password: cleartext password of the user
  170. :return: User who was authenticated.
  171. """
  172. try:
  173. user = self.get_one_by_email(email)
  174. if not user.is_active:
  175. raise UserNotActive('User "{}" is not active'.format(email))
  176. if user.validate_password(password):
  177. return user
  178. else:
  179. raise WrongUserPassword('User "{}" password is incorrect'.format(email)) # nopep8
  180. except (WrongUserPassword, UserDoesNotExist) as exc:
  181. raise AuthenticationFailed('User "{}" authentication failed'.format(email)) from exc # nopep8
  182. # Actions
  183. def set_password(
  184. self,
  185. user: User,
  186. loggedin_user_password: str,
  187. new_password: str,
  188. new_password2: str,
  189. do_save: bool=True
  190. ):
  191. """
  192. Set User password if loggedin user password is correct
  193. and both new_password are the same.
  194. :param user: User who need password changed
  195. :param loggedin_user_password: cleartext password of logged user (not
  196. same as user)
  197. :param new_password: new password for user
  198. :param new_password2: should be same as new_password
  199. :param do_save: should we save new user password ?
  200. :return:
  201. """
  202. if not self._user:
  203. raise NoUserSetted('Current User should be set in UserApi to use this method') # nopep8
  204. if not self._user.validate_password(loggedin_user_password): # nopep8
  205. raise WrongUserPassword(
  206. 'Wrong password for authenticated user {}'. format(self._user.user_id) # nopep8
  207. )
  208. if new_password != new_password2:
  209. raise PasswordDoNotMatch('Passwords given are different')
  210. self.update(
  211. user=user,
  212. password=new_password,
  213. do_save=do_save,
  214. )
  215. if do_save:
  216. # TODO - G.M - 2018-07-24 - Check why commit is needed here
  217. self.save(user)
  218. return user
  219. def set_email(
  220. self,
  221. user: User,
  222. loggedin_user_password: str,
  223. email: str,
  224. do_save: bool = True
  225. ):
  226. """
  227. Set email address of user if loggedin user password is correct
  228. :param user: User who need email changed
  229. :param loggedin_user_password: cleartext password of logged user (not
  230. same as user)
  231. :param email:
  232. :param do_save:
  233. :return:
  234. """
  235. if not self._user:
  236. raise NoUserSetted('Current User should be set in UserApi to use this method') # nopep8
  237. if not self._user.validate_password(loggedin_user_password): # nopep8
  238. raise WrongUserPassword(
  239. 'Wrong password for authenticated user {}'. format(self._user.user_id) # nopep8
  240. )
  241. self.update(
  242. user=user,
  243. email=email,
  244. do_save=do_save,
  245. )
  246. return user
  247. def _check_email(self, email: str) -> bool:
  248. """
  249. Check if email is completely ok to be used in user db table
  250. """
  251. is_email_correct = self._check_email_correctness(email)
  252. if not is_email_correct:
  253. raise EmailValidationFailed(
  254. 'Email given form {} is uncorrect'.format(email)) # nopep8
  255. email_already_exist_in_db = self.check_email_already_in_db(email)
  256. if email_already_exist_in_db:
  257. raise EmailAlreadyExistInDb(
  258. 'Email given {} already exist, please choose something else'.format(email) # nopep8
  259. )
  260. return True
  261. def check_email_already_in_db(self, email: str) -> bool:
  262. """
  263. Verify if given email does not already exist in db
  264. """
  265. return self._session.query(User.email).filter(User.email==email).count() != 0 # nopep8
  266. def _check_email_correctness(self, email: str) -> bool:
  267. """
  268. Verify if given email is correct:
  269. - check format
  270. - futur active check for email ? (dns based ?)
  271. """
  272. # TODO - G.M - 2018-07-05 - find a better way to check email
  273. if not email:
  274. return False
  275. email = email.split('@')
  276. if len(email) != 2:
  277. return False
  278. return True
  279. def update(
  280. self,
  281. user: User,
  282. name: str=None,
  283. email: str=None,
  284. password: str=None,
  285. timezone: str=None,
  286. groups: typing.Optional[typing.List[Group]]=None,
  287. do_save=True,
  288. ) -> User:
  289. if name is not None:
  290. user.display_name = name
  291. if email is not None and email != user.email:
  292. self._check_email(email)
  293. user.email = email
  294. if password is not None:
  295. user.password = password
  296. if timezone is not None:
  297. user.timezone = timezone
  298. if groups is not None:
  299. # INFO - G.M - 2018-07-18 - Delete old groups
  300. for group in user.groups:
  301. if group not in groups:
  302. user.groups.remove(group)
  303. # INFO - G.M - 2018-07-18 - add new groups
  304. for group in groups:
  305. if group not in user.groups:
  306. user.groups.append(group)
  307. if do_save:
  308. self.save(user)
  309. return user
  310. def create_user(
  311. self,
  312. email,
  313. password: str = None,
  314. name: str = None,
  315. timezone: str = '',
  316. groups=[],
  317. do_save: bool=True,
  318. do_notify: bool=True,
  319. ) -> User:
  320. new_user = self.create_minimal_user(email, groups, save_now=False)
  321. self.update(
  322. user=new_user,
  323. name=name,
  324. email=email,
  325. password=password,
  326. timezone=timezone,
  327. do_save=False,
  328. )
  329. if do_notify:
  330. try:
  331. email_manager = get_email_manager(self._config, self._session)
  332. email_manager.notify_created_account(
  333. new_user,
  334. password=password
  335. )
  336. except SMTPException as e:
  337. raise NotificationNotSend()
  338. if do_save:
  339. self.save(new_user)
  340. return new_user
  341. def create_minimal_user(
  342. self,
  343. email,
  344. groups=[],
  345. save_now=False
  346. ) -> User:
  347. """Previous create_user method"""
  348. self._check_email(email)
  349. user = User()
  350. user.email = email
  351. user.display_name = email.split('@')[0]
  352. for group in groups:
  353. user.groups.append(group)
  354. self._session.add(user)
  355. if save_now:
  356. self._session.flush()
  357. return user
  358. def enable(self, user: User, do_save=False):
  359. user.is_active = True
  360. if do_save:
  361. self.save(user)
  362. def disable(self, user:User, do_save=False):
  363. user.is_active = False
  364. if do_save:
  365. self.save(user)
  366. def delete(self, user: User, do_save=False):
  367. user.is_deleted = True
  368. if do_save:
  369. self.save(user)
  370. def undelete(self, user: User, do_save=False):
  371. user.is_deleted = False
  372. if do_save:
  373. self.save(user)
  374. def save(self, user: User):
  375. self._session.flush()
  376. def execute_created_user_actions(self, created_user: User) -> None:
  377. """
  378. Execute actions when user just been created
  379. :return:
  380. """
  381. # NOTE: Cyclic import
  382. # TODO - G.M - 28-03-2018 - [Calendar] Reenable Calendar stuff
  383. #from tracim.lib.calendar import CalendarManager
  384. #from tracim.model.organisational import UserCalendar
  385. # TODO - G.M - 04-04-2018 - [auth]
  386. # Check if this is already needed with
  387. # new auth system
  388. created_user.ensure_auth_token(
  389. session=self._session,
  390. validity_seconds=self._config.USER_AUTH_TOKEN_VALIDITY
  391. )
  392. # Ensure database is up-to-date
  393. self._session.flush()
  394. transaction.commit()
  395. # TODO - G.M - 28-03-2018 - [Calendar] Reenable Calendar stuff
  396. # calendar_manager = CalendarManager(created_user)
  397. # calendar_manager.create_then_remove_fake_event(
  398. # calendar_class=UserCalendar,
  399. # related_object_id=created_user.user_id,
  400. # )