PageRenderTime 50ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/paasmaker/pacemaker/controller/workspace.py

https://bitbucket.org/paasmaker/paasmaker
Python | 403 lines | 389 code | 6 blank | 8 comment | 0 complexity | 81e733e5f8b03b720098e40472628f9d MD5 | raw file
  1. #
  2. # Paasmaker - Platform as a Service
  3. #
  4. # This Source Code Form is subject to the terms of the Mozilla Public
  5. # License, v. 2.0. If a copy of the MPL was not distributed with this
  6. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  7. #
  8. import unittest
  9. import uuid
  10. import logging
  11. import json
  12. import re
  13. import paasmaker
  14. from paasmaker.common.controller import BaseController, BaseControllerTest
  15. from paasmaker.common.core import constants
  16. import colander
  17. import tornado
  18. import tornado.testing
  19. import sqlalchemy.exc
  20. logger = logging.getLogger(__name__)
  21. logger.addHandler(logging.NullHandler())
  22. VALID_IDENTIFIER = re.compile(r"^[-A-Za-z0-9.]{1,}$")
  23. class WorkspaceSchema(colander.MappingSchema):
  24. name = colander.SchemaNode(colander.String(),
  25. title="Workspace Name",
  26. description="The name of this workspace.",
  27. validator=colander.Length(min=2))
  28. # TODO: Put proper validation on this.
  29. stub = colander.SchemaNode(colander.String(),
  30. title="Workspace stub",
  31. description="A short, URL friendly name for the workspace.",
  32. validator=colander.Regex(VALID_IDENTIFIER, "Workspace stub must match " + VALID_IDENTIFIER.pattern))
  33. tags = colander.SchemaNode(colander.Mapping(unknown='preserve'),
  34. title="Workspace Tags",
  35. description="A set of tags for this workspace.",
  36. missing={},
  37. default={})
  38. serialised_tags = colander.SchemaNode(colander.String(),
  39. title="Workspace Tags JSON",
  40. description="JSON-encoded version of the tags for this workspace; takes precedence over tags if set",
  41. missing="",
  42. default="")
  43. class WorkspaceBaseController(BaseController):
  44. def _get_workspace(self, workspace_id=None):
  45. workspace = None
  46. if workspace_id:
  47. # Find and load the workspace.
  48. workspace = self.session.query(
  49. paasmaker.model.Workspace
  50. ).get(int(workspace_id))
  51. if not workspace:
  52. raise tornado.web.HTTPError(404, "No such workspace.")
  53. self.add_data('workspace', workspace)
  54. return workspace
  55. class WorkspaceEditController(WorkspaceBaseController):
  56. AUTH_METHODS = [BaseController.SUPER, BaseController.USER]
  57. def _default_workspace(self):
  58. workspace = paasmaker.model.Workspace()
  59. workspace.name = ''
  60. return workspace
  61. def get(self, workspace_id=None):
  62. workspace = self._get_workspace(workspace_id)
  63. if not workspace:
  64. workspace = self._default_workspace()
  65. self.require_permission(constants.PERMISSION.WORKSPACE_VIEW, workspace=workspace)
  66. self.add_data('workspace', workspace)
  67. # Workaround for the fact that this controller is used for two things:
  68. # /workspace/1 in HTML format shows the edit page
  69. # /workspace/1?format=json is the API call for details about the workspace
  70. # The former requires WORKSPACE_EDIT and the latter only needs WORKSPACE_VIEW.
  71. # TODO: the former should probably be /workspace/1/edit
  72. if self.has_permission(constants.PERMISSION.WORKSPACE_EDIT):
  73. self.client_side_render()
  74. else:
  75. self.render("api/apionly.html")
  76. def post(self, workspace_id=None):
  77. workspace = self._get_workspace(workspace_id)
  78. self.require_permission(constants.PERMISSION.WORKSPACE_EDIT, workspace=workspace)
  79. valid_data = self.validate_data(WorkspaceSchema())
  80. if not workspace:
  81. workspace = self._default_workspace()
  82. workspace.name = self.params['name']
  83. workspace.tags = self.params['tags']
  84. workspace.stub = self.params['stub']
  85. if len(self.params['serialised_tags']) > 0:
  86. workspace.tags = json.loads(self.params['serialised_tags'])
  87. if valid_data:
  88. self.session.add(workspace)
  89. try:
  90. self.session.commit()
  91. self.session.refresh(workspace)
  92. except sqlalchemy.exc.IntegrityError, ex:
  93. self.session.rollback()
  94. self.reload_current_user()
  95. if 'stub' in str(ex):
  96. valid_data = False
  97. self.add_error('The workspace stub is not unique.')
  98. else:
  99. raise ex
  100. if valid_data:
  101. self.add_data('workspace', workspace)
  102. self.action_success(None, '/workspace/' + str(workspace.id) + '/applications')
  103. else:
  104. self.add_data('workspace', workspace)
  105. self.render("workspace/edit.html")
  106. @staticmethod
  107. def get_routes(configuration):
  108. routes = []
  109. routes.append((r"/workspace/create", WorkspaceEditController, configuration))
  110. routes.append((r"/workspace/(\d+)", WorkspaceEditController, configuration))
  111. return routes
  112. class WorkspaceListController(BaseController):
  113. AUTH_METHODS = [BaseController.SUPER, BaseController.USER]
  114. def get(self):
  115. # This helper fetches only the workspaces the logged in user
  116. # has permissions to access.
  117. workspaces = self._my_workspace_list()
  118. self._paginate('workspaces', workspaces)
  119. self.client_side_render()
  120. @staticmethod
  121. def get_routes(configuration):
  122. routes = []
  123. routes.append((r"/workspace/list", WorkspaceListController, configuration))
  124. return routes
  125. class WorkspaceDeleteController(WorkspaceBaseController):
  126. AUTH_METHODS = [BaseController.SUPER, BaseController.USER]
  127. def post(self, workspace_id=None):
  128. workspace = self._get_workspace(workspace_id)
  129. self.require_permission(constants.PERMISSION.WORKSPACE_DELETE, workspace=workspace)
  130. if not workspace.can_delete:
  131. raise tornado.web.HTTPError(400, "Can't delete this workspace, as it still has applications.")
  132. # NOTE: When deleting, SQLAlchemy will cascade to remove any WorkspaceUserRole records
  133. # that point to this workspace. However, the flat table will be out of date.
  134. self.session.delete(workspace)
  135. self.session.commit()
  136. # Update the flat permissions table.
  137. paasmaker.model.WorkspaceUserRoleFlat.build_flat_table(self.session)
  138. self.add_data('success', True)
  139. self.client_side_render()
  140. @staticmethod
  141. def get_routes(configuration):
  142. routes = []
  143. routes.append((r"/workspace/(\d+)/delete", WorkspaceDeleteController, configuration))
  144. return routes
  145. class WorkspaceEditControllerTest(BaseControllerTest):
  146. config_modules = ['pacemaker']
  147. def get_app(self):
  148. self.late_init_configuration(self.io_loop)
  149. routes = WorkspaceEditController.get_routes({'configuration': self.configuration})
  150. routes.extend(WorkspaceListController.get_routes({'configuration': self.configuration}))
  151. application = tornado.web.Application(routes, **self.configuration.get_tornado_configuration())
  152. return application
  153. def test_create(self):
  154. # Create the workspace.
  155. request = paasmaker.common.api.workspace.WorkspaceCreateAPIRequest(self.configuration)
  156. request.set_superkey_auth()
  157. request.set_workspace_name('Test workspace')
  158. request.set_workspace_stub('test')
  159. request.send(self.stop)
  160. response = self.wait()
  161. self.failIf(not response.success)
  162. self.assertEquals(len(response.errors), 0, "There were errors.")
  163. self.assertEquals(len(response.warnings), 0, "There were warnings.")
  164. self.assertTrue(response.data.has_key('workspace'), "Missing workspace object in return data.")
  165. self.assertTrue(response.data['workspace'].has_key('id'), "Missing ID in return data.")
  166. self.assertTrue(response.data['workspace'].has_key('name'), "Missing name in return data.")
  167. # Try to create again with a duplicate stub.
  168. request = paasmaker.common.api.workspace.WorkspaceCreateAPIRequest(self.configuration)
  169. request.set_superkey_auth()
  170. request.set_workspace_name('Test workspace')
  171. request.set_workspace_stub('test')
  172. request.send(self.stop)
  173. response = self.wait()
  174. self.failIf(response.success)
  175. self.assertTrue(len(response.errors) > 0, "There were not errors.")
  176. self.assertIn('The workspace stub is not unique.', response.errors[0], "Wrong error message.")
  177. def test_create_fail(self):
  178. # Send through some bogus data.
  179. request = paasmaker.common.api.workspace.WorkspaceCreateAPIRequest(self.configuration)
  180. request.set_superkey_auth()
  181. request.set_workspace_name('a')
  182. request.send(self.stop)
  183. response = self.wait()
  184. self.failIf(response.success)
  185. input_errors = response.data['input_errors']
  186. self.assertTrue(input_errors.has_key('name'), "Missing error on name attribute.")
  187. def test_edit(self):
  188. # Create the workspace.
  189. request = paasmaker.common.api.workspace.WorkspaceCreateAPIRequest(self.configuration)
  190. request.set_superkey_auth()
  191. request.set_workspace_name('Test workspace')
  192. request.set_workspace_stub('test')
  193. request.send(self.stop)
  194. response = self.wait()
  195. self.failIf(not response.success)
  196. workspace_id = response.data['workspace']['id']
  197. # Set up the request.
  198. request = paasmaker.common.api.workspace.WorkspaceEditAPIRequest(self.configuration)
  199. request.set_superkey_auth()
  200. # This loads the workspace data from the server.
  201. request.load(workspace_id, self.stop, self.stop)
  202. load_response = self.wait()
  203. # Now attempt to change the workspace.
  204. request.set_workspace_name('Test Altered workspace')
  205. # Send it along!
  206. request.send(self.stop)
  207. response = self.wait()
  208. self.failIf(not response.success)
  209. self.assertEquals(response.data['workspace']['name'], 'Test Altered workspace', 'Name was not updated.')
  210. # Load up the workspace separately and confirm.
  211. self.configuration.get_database_session(self.stop, None)
  212. session = self.wait()
  213. workspace = session.query(paasmaker.model.Workspace).get(workspace_id)
  214. self.assertEquals(workspace.name, 'Test Altered workspace', 'Name was not updated.')
  215. # Create a second workspace.
  216. request = paasmaker.common.api.workspace.WorkspaceCreateAPIRequest(self.configuration)
  217. request.set_superkey_auth()
  218. request.set_workspace_name('Test workspace 2')
  219. request.set_workspace_stub('test2')
  220. request.send(self.stop)
  221. response = self.wait()
  222. self.failIf(not response.success)
  223. workspace_two_id = response.data['workspace']['id']
  224. # Try to edit the second workspace to have the same stub as the first.
  225. request = paasmaker.common.api.workspace.WorkspaceEditAPIRequest(self.configuration)
  226. request.set_superkey_auth()
  227. # This loads the workspace data from the server.
  228. request.load(workspace_two_id, self.stop, self.stop)
  229. load_response = self.wait()
  230. # Now attempt to change the workspace.
  231. request.set_workspace_stub('test')
  232. # Send it along!
  233. request.send(self.stop)
  234. response = self.wait()
  235. self.failIf(response.success)
  236. self.assertIn('The workspace stub is not unique.', response.errors[0], "Wrong error message.")
  237. def test_edit_fail(self):
  238. # Create the workspace.
  239. request = paasmaker.common.api.workspace.WorkspaceCreateAPIRequest(self.configuration)
  240. request.set_superkey_auth()
  241. request.set_workspace_name('Test workspace')
  242. request.set_workspace_stub('test')
  243. request.send(self.stop)
  244. response = self.wait()
  245. self.failIf(not response.success)
  246. workspace_id = response.data['workspace']['id']
  247. # Set up the request.
  248. request = paasmaker.common.api.workspace.WorkspaceEditAPIRequest(self.configuration)
  249. request.set_superkey_auth()
  250. # This loads the workspace data from the server.
  251. request.load(workspace_id, self.stop, self.stop)
  252. load_response = self.wait()
  253. # Now attempt to change the workspace.
  254. request.set_workspace_name('a')
  255. # Send it along!
  256. request.send(self.stop)
  257. response = self.wait()
  258. self.failIf(response.success)
  259. input_errors = response.data['input_errors']
  260. self.assertTrue(input_errors.has_key('name'), "Missing error on name attribute.")
  261. def test_list(self):
  262. # Create the workspace.
  263. request = paasmaker.common.api.workspace.WorkspaceCreateAPIRequest(self.configuration)
  264. request.set_superkey_auth()
  265. request.set_workspace_name('Test workspace')
  266. request.set_workspace_stub('test')
  267. request.send(self.stop)
  268. response = self.wait()
  269. self.failIf(not response.success)
  270. request = paasmaker.common.api.workspace.WorkspaceListAPIRequest(self.configuration)
  271. request.set_superkey_auth()
  272. request.send(self.stop)
  273. response = self.wait()
  274. self.failIf(not response.success)
  275. self.assertTrue(response.data.has_key('workspaces'), "Missing workspaces list.")
  276. self.assertEquals(len(response.data['workspaces']), 1, "Not enough workspaces returned.")
  277. self.assertEquals(response.data['workspaces'][0]['name'], 'Test workspace', "Returned workspace is not as expected.")
  278. # Create a second workspace.
  279. request = paasmaker.common.api.workspace.WorkspaceCreateAPIRequest(self.configuration)
  280. request.set_superkey_auth()
  281. request.set_workspace_name('Second Workspace')
  282. request.set_workspace_stub('test-two')
  283. request.send(self.stop)
  284. response = self.wait()
  285. self.failIf(not response.success)
  286. second_workspace_id = int(response.data['workspace']['id'])
  287. request = paasmaker.common.api.workspace.WorkspaceListAPIRequest(self.configuration)
  288. request.set_superkey_auth()
  289. request.send(self.stop)
  290. response = self.wait()
  291. self.failIf(not response.success)
  292. self.assertTrue(response.data.has_key('workspaces'), "Missing workspaces list.")
  293. self.assertEquals(len(response.data['workspaces']), 2, "Not enough workspaces returned.")
  294. # Now, create a user and assign them permission only to view the second workspace.
  295. self.configuration.get_database_session(self.stop, None)
  296. session = self.wait()
  297. user = paasmaker.model.User()
  298. user.login = 'username'
  299. user.email = 'username@example.com'
  300. user.password = 'testtest'
  301. role = paasmaker.model.Role()
  302. role.name = 'Workspace Level'
  303. role.add_permission(constants.PERMISSION.WORKSPACE_VIEW)
  304. session.add(user)
  305. session.add(role)
  306. workspace = session.query(paasmaker.model.Workspace).get(second_workspace_id)
  307. wu = paasmaker.model.WorkspaceUserRole()
  308. wu.workspace = workspace
  309. wu.user = user
  310. wu.role = role
  311. session.add(wu)
  312. session.commit()
  313. paasmaker.model.WorkspaceUserRoleFlat.build_flat_table(session)
  314. session.refresh(user)
  315. # Fetch the workspace list as that user.
  316. request = paasmaker.common.api.workspace.WorkspaceListAPIRequest(self.configuration)
  317. request.set_auth(user.apikey)
  318. request.send(self.stop)
  319. response = self.wait()
  320. self.failIf(not response.success)
  321. self.assertTrue(response.data.has_key('workspaces'), "Missing workspaces list.")
  322. self.assertEquals(len(response.data['workspaces']), 1, "Not enough workspaces returned.")
  323. self.assertEquals(response.data['workspaces'][0]['name'], 'Second Workspace', "Returned workspace is not as expected.")