request.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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.app_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. config=request.registry.settings['CFG']
  210. )
  211. comment = api.get_one(
  212. comment_id,
  213. content_type=CONTENT_TYPES.Comment.slug,
  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=CONTENT_TYPES.Any_SLUG) # 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