request.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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 UserNotActive
  6. from tracim.exceptions import ContentNotFound
  7. from tracim.exceptions import InvalidUserId
  8. from tracim.exceptions import InvalidWorkspaceId
  9. from tracim.exceptions import InvalidContentId
  10. from tracim.exceptions import InvalidCommentId
  11. from tracim.exceptions import ContentNotFoundInTracimRequest
  12. from tracim.exceptions import WorkspaceNotFoundInTracimRequest
  13. from tracim.exceptions import UserNotFoundInTracimRequest
  14. from tracim.exceptions import UserDoesNotExist
  15. from tracim.exceptions import WorkspaceNotFound
  16. from tracim.exceptions import ImmutableAttribute
  17. from tracim.models.contents import ContentTypeLegacy as ContentType
  18. from tracim.lib.core.content import ContentApi
  19. from tracim.lib.core.user import UserApi
  20. from tracim.lib.core.workspace import WorkspaceApi
  21. from tracim.lib.utils.authorization import JSONDecodeError
  22. from tracim.models import User
  23. from tracim.models.data import Workspace
  24. from tracim.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. config=request.registry.settings['CFG']
  210. )
  211. comment = api.get_one(
  212. comment_id,
  213. content_type=ContentType.Comment,
  214. workspace=workspace,
  215. parent=content,
  216. )
  217. except NoResultFound as exc:
  218. raise ContentNotFound(
  219. 'Comment {} does not exist '
  220. 'or is not visible for this user'.format(comment_id)
  221. ) from exc
  222. return comment
  223. def _get_current_content(
  224. self,
  225. user: User,
  226. workspace: Workspace,
  227. request: 'TracimRequest'
  228. ) -> Content:
  229. """
  230. Get current content from request
  231. :param user: User who want to check the workspace
  232. :param workspace: Workspace of the content
  233. :param request: pyramid request
  234. :return: current content
  235. """
  236. content_id = ''
  237. try:
  238. if 'content_id' in request.matchdict:
  239. content_id_str = request.matchdict['content_id']
  240. if not isinstance(content_id_str, str) or not content_id_str.isdecimal(): # nopep8
  241. raise InvalidContentId('content_id is not a correct integer') # nopep8
  242. content_id = int(request.matchdict['content_id'])
  243. if not content_id:
  244. raise ContentNotFoundInTracimRequest('No content_id property found in request') # nopep8
  245. api = ContentApi(
  246. current_user=user,
  247. session=request.dbsession,
  248. config=request.registry.settings['CFG']
  249. )
  250. content = api.get_one(content_id=content_id, workspace=workspace, content_type=ContentType.Any) # nopep8
  251. except NoResultFound as exc:
  252. raise ContentNotFound(
  253. 'Content {} does not exist '
  254. 'or is not visible for this user'.format(content_id)
  255. ) from exc
  256. return content
  257. def _get_candidate_user(
  258. self,
  259. request: 'TracimRequest',
  260. ) -> User:
  261. """
  262. Get candidate user
  263. :param request: pyramid request
  264. :return: user found from header/body
  265. """
  266. app_config = request.registry.settings['CFG']
  267. uapi = UserApi(None, session=request.dbsession, config=app_config)
  268. login = ''
  269. try:
  270. login = None
  271. if 'user_id' in request.matchdict:
  272. user_id_str = request.matchdict['user_id']
  273. if not isinstance(user_id_str, str) or not user_id_str.isdecimal():
  274. raise InvalidUserId('user_id is not a correct integer') # nopep8
  275. login = int(request.matchdict['user_id'])
  276. if not login:
  277. raise UserNotFoundInTracimRequest('You request a candidate user but the context not permit to found one') # nopep8
  278. user = uapi.get_one(login)
  279. except UserNotFoundInTracimRequest as exc:
  280. raise UserDoesNotExist('User {} not found'.format(login)) from exc
  281. return user
  282. def _get_auth_safe_user(
  283. self,
  284. request: 'TracimRequest',
  285. ) -> User:
  286. """
  287. Get current pyramid authenticated user from request
  288. :param request: pyramid request
  289. :return: current authenticated user
  290. """
  291. app_config = request.registry.settings['CFG']
  292. uapi = UserApi(None, session=request.dbsession, config=app_config)
  293. login = ''
  294. try:
  295. login = request.authenticated_userid
  296. if not login:
  297. raise UserNotFoundInTracimRequest('You request a current user but the context not permit to found one') # nopep8
  298. user = uapi.get_one_by_email(login)
  299. if not user.is_active:
  300. raise UserNotActive('User {} is not active'.format(login))
  301. except (UserDoesNotExist, UserNotFoundInTracimRequest) as exc:
  302. raise NotAuthenticated('User {} not found'.format(login)) from exc
  303. return user
  304. def _get_current_workspace(
  305. self,
  306. user: User,
  307. request: 'TracimRequest'
  308. ) -> Workspace:
  309. """
  310. Get current workspace from request
  311. :param user: User who want to check the workspace
  312. :param request: pyramid request
  313. :return: current workspace
  314. """
  315. workspace_id = ''
  316. try:
  317. if 'workspace_id' in request.matchdict:
  318. workspace_id_str = request.matchdict['workspace_id']
  319. if not isinstance(workspace_id_str, str) or not workspace_id_str.isdecimal(): # nopep8
  320. raise InvalidWorkspaceId('workspace_id is not a correct integer') # nopep8
  321. workspace_id = int(request.matchdict['workspace_id'])
  322. if not workspace_id:
  323. raise WorkspaceNotFoundInTracimRequest('No workspace_id property found in request') # nopep8
  324. wapi = WorkspaceApi(
  325. current_user=user,
  326. session=request.dbsession,
  327. config=request.registry.settings['CFG']
  328. )
  329. workspace = wapi.get_one(workspace_id)
  330. except NoResultFound as exc:
  331. raise WorkspaceNotFound(
  332. 'Workspace {} does not exist '
  333. 'or is not visible for this user'.format(workspace_id)
  334. ) from exc
  335. return workspace
  336. def _get_candidate_workspace(
  337. self,
  338. user: User,
  339. request: 'TracimRequest'
  340. ) -> Workspace:
  341. """
  342. Get current workspace from request
  343. :param user: User who want to check the workspace
  344. :param request: pyramid request
  345. :return: current workspace
  346. """
  347. workspace_id = ''
  348. try:
  349. if 'new_workspace_id' in request.json_body:
  350. workspace_id = request.json_body['new_workspace_id']
  351. if not isinstance(workspace_id, int):
  352. if workspace_id.isdecimal():
  353. workspace_id = int(workspace_id)
  354. else:
  355. raise InvalidWorkspaceId('workspace_id is not a correct integer') # nopep8
  356. if not workspace_id:
  357. raise WorkspaceNotFoundInTracimRequest('No new_workspace_id property found in body') # nopep8
  358. wapi = WorkspaceApi(
  359. current_user=user,
  360. session=request.dbsession,
  361. config=request.registry.settings['CFG']
  362. )
  363. workspace = wapi.get_one(workspace_id)
  364. except JSONDecodeError as exc:
  365. raise WorkspaceNotFound('Invalid JSON content') from exc
  366. except NoResultFound as exc:
  367. raise WorkspaceNotFound(
  368. 'Workspace {} does not exist '
  369. 'or is not visible for this user'.format(workspace_id)
  370. ) from exc
  371. return workspace