request.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. # -*- coding: utf-8 -*-
  2. from pyramid.request import Request
  3. from sqlalchemy.orm.exc import NoResultFound
  4. from tracim_backend.exceptions import NotAuthenticated
  5. from tracim_backend.exceptions import UserNotActive
  6. from tracim_backend.exceptions import ContentNotFound
  7. from tracim_backend.exceptions import InvalidUserId
  8. from tracim_backend.exceptions import InvalidWorkspaceId
  9. from tracim_backend.exceptions import InvalidContentId
  10. from tracim_backend.exceptions import InvalidCommentId
  11. from tracim_backend.exceptions import ContentNotFoundInTracimRequest
  12. from tracim_backend.exceptions import WorkspaceNotFoundInTracimRequest
  13. from tracim_backend.exceptions import UserNotFoundInTracimRequest
  14. from tracim_backend.exceptions import UserDoesNotExist
  15. from tracim_backend.exceptions import WorkspaceNotFound
  16. from tracim_backend.exceptions import ImmutableAttribute
  17. from tracim_backend.models.contents import CONTENT_TYPES
  18. from tracim_backend.lib.core.content import ContentApi
  19. from tracim_backend.lib.core.user import UserApi
  20. from tracim_backend.lib.core.workspace import WorkspaceApi
  21. from tracim_backend.lib.utils.authorization import JSONDecodeError
  22. from tracim_backend.models import User
  23. from tracim_backend.models.data import Workspace
  24. from tracim_backend.models.data import Content
  25. class TracimRequest(Request):
  26. """
  27. Request with tracim specific params/methods
  28. """
  29. def __init__(
  30. self,
  31. environ,
  32. charset=None,
  33. unicode_errors=None,
  34. decode_param_names=None,
  35. **kw
  36. ):
  37. super().__init__(
  38. environ,
  39. charset,
  40. unicode_errors,
  41. decode_param_names,
  42. **kw
  43. )
  44. # Current comment, found in request path
  45. self._current_comment = None # type: Content
  46. # Current content, found in request path
  47. self._current_content = None # type: Content
  48. # Current workspace, found in request path
  49. self._current_workspace = None # type: Workspace
  50. # Candidate workspace found in request body
  51. self._candidate_workspace = None # type: Workspace
  52. # Authenticated user
  53. self._current_user = None # type: User
  54. # User found from request headers, content, distinct from authenticated
  55. # user
  56. self._candidate_user = None # type: User
  57. # INFO - G.M - 18-05-2018 - Close db at the end of the request
  58. self.add_finished_callback(self._cleanup)
  59. @property
  60. def current_workspace(self) -> Workspace:
  61. """
  62. Get current workspace of the request according to authentification and
  63. request headers (to retrieve workspace). Setted by default value the
  64. first time if not configured.
  65. :return: Workspace of the request
  66. """
  67. if self._current_workspace is None:
  68. self._current_workspace = self._get_current_workspace(self.current_user, self) # nopep8
  69. return self._current_workspace
  70. @current_workspace.setter
  71. def current_workspace(self, workspace: Workspace) -> None:
  72. """
  73. Setting current_workspace
  74. :param workspace:
  75. :return:
  76. """
  77. if self._current_workspace is not None:
  78. raise ImmutableAttribute(
  79. "Can't modify already setted current_workspace"
  80. )
  81. self._current_workspace = workspace
  82. @property
  83. def current_user(self) -> User:
  84. """
  85. Get user from authentication mecanism.
  86. """
  87. if self._current_user is None:
  88. self.current_user = self._get_auth_safe_user(self)
  89. return self._current_user
  90. @current_user.setter
  91. def current_user(self, user: User) -> None:
  92. if self._current_user is not None:
  93. raise ImmutableAttribute(
  94. "Can't modify already setted current_user"
  95. )
  96. self._current_user = user
  97. @property
  98. def current_content(self) -> Content:
  99. """
  100. Get current content from path
  101. """
  102. if self._current_content is None:
  103. self._current_content = self._get_current_content(
  104. self.current_user,
  105. self.current_workspace,
  106. self
  107. )
  108. return self._current_content
  109. @current_content.setter
  110. def current_content(self, content: Content) -> None:
  111. if self._current_content is not None:
  112. raise ImmutableAttribute(
  113. "Can't modify already setted current_content"
  114. )
  115. self._current_content = content
  116. @property
  117. def current_comment(self) -> Content:
  118. """
  119. Get current comment from path
  120. """
  121. if self._current_comment is None:
  122. self._current_comment = self._get_current_comment(
  123. self.current_user,
  124. self.current_workspace,
  125. self.current_content,
  126. self
  127. )
  128. return self._current_comment
  129. @current_comment.setter
  130. def current_comment(self, content: Content) -> None:
  131. if self._current_comment is not None:
  132. raise ImmutableAttribute(
  133. "Can't modify already setted current_content"
  134. )
  135. self._current_comment = content
  136. # TODO - G.M - 24-05-2018 - Find a better naming for this ?
  137. @property
  138. def candidate_user(self) -> User:
  139. """
  140. Get user from headers/body request. This user is not
  141. the one found by authentication mecanism. This user
  142. can help user to know about who one page is about in
  143. a similar way as current_workspace.
  144. """
  145. if self._candidate_user is None:
  146. self.candidate_user = self._get_candidate_user(self)
  147. return self._candidate_user
  148. @property
  149. def candidate_workspace(self) -> Workspace:
  150. """
  151. Get workspace from headers/body request. This workspace is not
  152. the one found from path. Its the one from json body.
  153. """
  154. if self._candidate_workspace is None:
  155. self._candidate_workspace = self._get_candidate_workspace(
  156. self.current_user,
  157. self
  158. )
  159. return self._candidate_workspace
  160. def _cleanup(self, request: 'TracimRequest') -> None:
  161. """
  162. Close dbsession at the end of the request in order to avoid exception
  163. about not properly closed session or "object created in another thread"
  164. issue
  165. see https://github.com/tracim/tracim_backend/issues/62
  166. :param request: same as self, request
  167. :return: nothing.
  168. """
  169. self._current_user = None
  170. self._current_workspace = None
  171. self.dbsession.close()
  172. @candidate_user.setter
  173. def candidate_user(self, user: User) -> None:
  174. if self._candidate_user is not None:
  175. raise ImmutableAttribute(
  176. "Can't modify already setted candidate_user"
  177. )
  178. self._candidate_user = user
  179. ###
  180. # Utils for TracimRequest
  181. ###
  182. def _get_current_comment(
  183. self,
  184. user: User,
  185. workspace: Workspace,
  186. content: Content,
  187. request: 'TracimRequest'
  188. ) -> Content:
  189. """
  190. Get current content from request
  191. :param user: User who want to check the workspace
  192. :param workspace: Workspace of the content
  193. :param content: comment is related to this content
  194. :param request: pyramid request
  195. :return: current content
  196. """
  197. comment_id = ''
  198. try:
  199. if 'comment_id' in request.matchdict:
  200. comment_id_str = request.matchdict['content_id']
  201. if not isinstance(comment_id_str, str) or not comment_id_str.isdecimal(): # nopep8
  202. raise InvalidCommentId('comment_id is not a correct integer') # nopep8
  203. comment_id = int(request.matchdict['comment_id'])
  204. if not comment_id:
  205. raise ContentNotFoundInTracimRequest('No comment_id property found in request') # nopep8
  206. api = ContentApi(
  207. current_user=user,
  208. session=request.dbsession,
  209. show_deleted=True,
  210. show_archived=True,
  211. config=request.registry.settings['CFG']
  212. )
  213. comment = api.get_one(
  214. comment_id,
  215. content_type=CONTENT_TYPES.Comment.slug,
  216. workspace=workspace,
  217. parent=content,
  218. )
  219. except NoResultFound as exc:
  220. raise ContentNotFound(
  221. 'Comment {} does not exist '
  222. 'or is not visible for this user'.format(comment_id)
  223. ) from exc
  224. return comment
  225. def _get_current_content(
  226. self,
  227. user: User,
  228. workspace: Workspace,
  229. request: 'TracimRequest'
  230. ) -> Content:
  231. """
  232. Get current content from request
  233. :param user: User who want to check the workspace
  234. :param workspace: Workspace of the content
  235. :param request: pyramid request
  236. :return: current content
  237. """
  238. content_id = ''
  239. try:
  240. if 'content_id' in request.matchdict:
  241. content_id_str = request.matchdict['content_id']
  242. if not isinstance(content_id_str, str) or not content_id_str.isdecimal(): # nopep8
  243. raise InvalidContentId('content_id is not a correct integer') # nopep8
  244. content_id = int(request.matchdict['content_id'])
  245. if not content_id:
  246. raise ContentNotFoundInTracimRequest('No content_id property found in request') # nopep8
  247. api = ContentApi(
  248. current_user=user,
  249. show_deleted=True,
  250. show_archived=True,
  251. session=request.dbsession,
  252. config=request.registry.settings['CFG']
  253. )
  254. content = api.get_one(content_id=content_id, workspace=workspace, content_type=CONTENT_TYPES.Any_SLUG) # nopep8
  255. except NoResultFound as exc:
  256. raise ContentNotFound(
  257. 'Content {} does not exist '
  258. 'or is not visible for this user'.format(content_id)
  259. ) from exc
  260. return content
  261. def _get_candidate_user(
  262. self,
  263. request: 'TracimRequest',
  264. ) -> User:
  265. """
  266. Get candidate user
  267. :param request: pyramid request
  268. :return: user found from header/body
  269. """
  270. app_config = request.registry.settings['CFG']
  271. uapi = UserApi(None, show_deleted=True, session=request.dbsession, config=app_config)
  272. login = ''
  273. try:
  274. login = None
  275. if 'user_id' in request.matchdict:
  276. user_id_str = request.matchdict['user_id']
  277. if not isinstance(user_id_str, str) or not user_id_str.isdecimal():
  278. raise InvalidUserId('user_id is not a correct integer') # nopep8
  279. login = int(request.matchdict['user_id'])
  280. if not login:
  281. raise UserNotFoundInTracimRequest('You request a candidate user but the context not permit to found one') # nopep8
  282. user = uapi.get_one(login)
  283. except UserNotFoundInTracimRequest as exc:
  284. raise UserDoesNotExist('User {} not found'.format(login)) from exc
  285. return user
  286. def _get_auth_safe_user(
  287. self,
  288. request: 'TracimRequest',
  289. ) -> User:
  290. """
  291. Get current pyramid authenticated user from request
  292. :param request: pyramid request
  293. :return: current authenticated user
  294. """
  295. app_config = request.registry.settings['CFG']
  296. uapi = UserApi(None, session=request.dbsession, config=app_config)
  297. login = ''
  298. try:
  299. login = request.authenticated_userid
  300. if not login:
  301. raise UserNotFoundInTracimRequest('You request a current user but the context not permit to found one') # nopep8
  302. user = uapi.get_one_by_email(login)
  303. if not user.is_active:
  304. raise UserNotActive('User {} is not active'.format(login))
  305. except (UserDoesNotExist, UserNotFoundInTracimRequest) as exc:
  306. raise NotAuthenticated('User {} not found'.format(login)) from exc
  307. return user
  308. def _get_current_workspace(
  309. self,
  310. user: User,
  311. request: 'TracimRequest'
  312. ) -> Workspace:
  313. """
  314. Get current workspace from request
  315. :param user: User who want to check the workspace
  316. :param request: pyramid request
  317. :return: current workspace
  318. """
  319. workspace_id = ''
  320. try:
  321. if 'workspace_id' in request.matchdict:
  322. workspace_id_str = request.matchdict['workspace_id']
  323. if not isinstance(workspace_id_str, str) or not workspace_id_str.isdecimal(): # nopep8
  324. raise InvalidWorkspaceId('workspace_id is not a correct integer') # nopep8
  325. workspace_id = int(request.matchdict['workspace_id'])
  326. if not workspace_id:
  327. raise WorkspaceNotFoundInTracimRequest('No workspace_id property found in request') # nopep8
  328. wapi = WorkspaceApi(
  329. current_user=user,
  330. session=request.dbsession,
  331. config=request.registry.settings['CFG'],
  332. show_deleted=True,
  333. )
  334. workspace = wapi.get_one(workspace_id)
  335. except NoResultFound as exc:
  336. raise WorkspaceNotFound(
  337. 'Workspace {} does not exist '
  338. 'or is not visible for this user'.format(workspace_id)
  339. ) from exc
  340. return workspace
  341. def _get_candidate_workspace(
  342. self,
  343. user: User,
  344. request: 'TracimRequest'
  345. ) -> Workspace:
  346. """
  347. Get current workspace from request
  348. :param user: User who want to check the workspace
  349. :param request: pyramid request
  350. :return: current workspace
  351. """
  352. workspace_id = ''
  353. try:
  354. if 'new_workspace_id' in request.json_body:
  355. workspace_id = request.json_body['new_workspace_id']
  356. if not isinstance(workspace_id, int):
  357. if workspace_id.isdecimal():
  358. workspace_id = int(workspace_id)
  359. else:
  360. raise InvalidWorkspaceId('workspace_id is not a correct integer') # nopep8
  361. if not workspace_id:
  362. raise WorkspaceNotFoundInTracimRequest('No new_workspace_id property found in body') # nopep8
  363. wapi = WorkspaceApi(
  364. current_user=user,
  365. session=request.dbsession,
  366. config=request.registry.settings['CFG'],
  367. show_deleted=True,
  368. )
  369. workspace = wapi.get_one(workspace_id)
  370. except JSONDecodeError as exc:
  371. raise WorkspaceNotFound('Invalid JSON content') from exc
  372. except NoResultFound as exc:
  373. raise WorkspaceNotFound(
  374. 'Workspace {} does not exist '
  375. 'or is not visible for this user'.format(workspace_id)
  376. ) from exc
  377. return workspace