request.py 14KB

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