request.py 14KB

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