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

/Server/bkr/server/pools.py

https://github.com/beaker-project/beaker
Python | 479 lines | 423 code | 7 blank | 49 comment | 8 complexity | f04be05402746cb37c5557cd8650dfbe MD5 | raw file
Possible License(s): GPL-2.0, CC-BY-SA-3.0
  1. # This program is free software; you can redistribute it and/or modify
  2. # it under the terms of the GNU General Public License as published by
  3. # the Free Software Foundation; either version 2 of the License, or
  4. # (at your option) any later version.
  5. import re
  6. from flask import jsonify, request
  7. from bkr.server import identity
  8. from bkr.server.app import app
  9. from bkr.server.model import System, SystemPool, SystemAccessPolicy, \
  10. SystemAccessPolicyRule, User, Group, SystemPermission, Activity
  11. from bkr.server.flask_util import auth_required, \
  12. convert_internal_errors, read_json_request, BadRequest400, \
  13. Forbidden403, MethodNotAllowed405, NotFound404, Conflict409, \
  14. UnsupportedMediaType415, request_wants_json, render_tg_template, \
  15. json_collection
  16. from bkr.server.util import absolute_url
  17. from sqlalchemy.orm import contains_eager
  18. from sqlalchemy.orm.exc import NoResultFound
  19. from bkr.server.database import session
  20. from bkr.server.systems import _get_system_by_FQDN, _edit_access_policy_rules
  21. import datetime
  22. from bkr.server.bexceptions import DatabaseLookupError
  23. @app.route('/pools/', methods=['GET'])
  24. def get_pools():
  25. """
  26. Returns a pageable JSON collection of system pools in Beaker.
  27. Refer to :ref:`pageable-json-collections`.
  28. The following fields are supported for filtering and sorting:
  29. ``id``
  30. ID of the pool.
  31. ``name``
  32. Name of the pool.
  33. ``owner.user_name``
  34. Username of the pool owner (if the pool is owned by a user rather than
  35. by a group).
  36. ``owner.group_name``
  37. Name of the pool's owning group (if the pool is owned by a group rather
  38. than by a user).
  39. """
  40. query = SystemPool.query.order_by(SystemPool.name)
  41. # join User and Group for sorting/filtering and also for eager loading
  42. query = query\
  43. .outerjoin(SystemPool.owning_user)\
  44. .options(contains_eager(SystemPool.owning_user))\
  45. .outerjoin(SystemPool.owning_group)\
  46. .options(contains_eager(SystemPool.owning_group))
  47. json_result = json_collection(query, columns={
  48. 'id': SystemPool.id,
  49. 'name': SystemPool.name,
  50. 'owner.user_name': User.user_name,
  51. 'owner.group_name': Group.group_name,
  52. })
  53. if request_wants_json():
  54. return jsonify(json_result)
  55. return render_tg_template('bkr.server.templates.backgrid', {
  56. 'title': u'Pools',
  57. 'grid_collection_type': 'SystemPools',
  58. 'grid_collection_data': json_result,
  59. 'grid_collection_url': request.path,
  60. 'grid_view_type': 'PoolsView',
  61. 'grid_add_label': 'Create',
  62. 'grid_add_view_type': 'PoolCreateModal' if not identity.current.anonymous else 'null',
  63. })
  64. _typeahead_split_pattern = re.compile(r'[-\s]+')
  65. @app.route('/pools/+typeahead')
  66. def pools_typeahead():
  67. if 'q' in request.args:
  68. pools = SystemPool.query.filter(SystemPool.name.like('%%%s%%' % request.args['q']))
  69. else:
  70. pools = SystemPool.query
  71. data = [{'name': pool.name, 'tokens': _typeahead_split_pattern.split(pool.name.strip())}
  72. for pool in pools.values(SystemPool.name)]
  73. return jsonify(data=data)
  74. def _get_pool_by_name(pool_name, lockmode=False):
  75. """Get system pool by name, reporting HTTP 404 if the system pool is not found"""
  76. try:
  77. return SystemPool.by_name(pool_name, lockmode)
  78. except NoResultFound:
  79. raise NotFound404('System pool %s does not exist' % pool_name)
  80. @app.route('/pools/<pool_name>/', methods=['GET'])
  81. def get_pool(pool_name):
  82. """
  83. Provides detailed information about a system pool in JSON format.
  84. :param pool_name: System pool's name.
  85. """
  86. pool = _get_pool_by_name(pool_name)
  87. if request_wants_json():
  88. return jsonify(pool.__json__())
  89. return render_tg_template('bkr.server.templates.system_pool', {
  90. 'title': pool.name,
  91. 'system_pool': pool,
  92. })
  93. def _get_owner(data):
  94. if data is None:
  95. data = {}
  96. user_name = data.get('user_name')
  97. group_name = data.get('group_name')
  98. if user_name and group_name:
  99. raise Forbidden403('System pool can have either an user or a group as owner')
  100. if user_name:
  101. owner = User.by_user_name(user_name)
  102. if owner is None:
  103. raise BadRequest400('No such user %s' % user_name)
  104. if owner.removed:
  105. raise BadRequest400('System pool cannot be owned by deleted user %s' % owner.user_name)
  106. owner_type = 'user'
  107. if group_name:
  108. try:
  109. owner = Group.by_name(group_name)
  110. except NoResultFound:
  111. raise BadRequest400('No such group %r' % group_name)
  112. owner_type = 'group'
  113. return owner, owner_type
  114. @app.route('/pools/', methods=['POST'])
  115. @auth_required
  116. def create_pool():
  117. """
  118. Creates a new system pool in Beaker. The request must be
  119. :mimetype:`application/x-www-form-urlencoded` or
  120. :mimetype:`application/json`.
  121. :jsonparam string name: Name for the system pool.
  122. :jsonparam string description: Description of the system pool.
  123. :jsonparam object owner: JSON object containing a ``user_name`` key or
  124. ``group_name`` key identifying the owner for the system pool.
  125. :status 201: The system pool was successfully created.
  126. """
  127. owner = None
  128. description = None
  129. u = identity.current.user
  130. if request.json:
  131. if 'name' not in request.json:
  132. raise BadRequest400('Missing pool name key')
  133. new_name = request.json['name']
  134. if 'owner' in request.json:
  135. owner = request.json['owner']
  136. if 'description' in request.json:
  137. description = request.json['description']
  138. elif request.form:
  139. if 'name' not in request.form:
  140. raise BadRequest400('Missing pool name parameter')
  141. new_name = request.form['name']
  142. if 'owner' in request.form:
  143. owner = request.form['owner']
  144. if 'description' in request.form:
  145. description = request.form['description']
  146. else:
  147. raise UnsupportedMediaType415
  148. with convert_internal_errors():
  149. if SystemPool.query.filter(SystemPool.name == new_name).count() != 0:
  150. raise Conflict409('System pool with name %r already exists' % new_name)
  151. pool = SystemPool(name=new_name, description=description)
  152. session.add(pool)
  153. if owner:
  154. owner, owner_type = _get_owner(owner)
  155. if owner_type == 'user':
  156. pool.owning_user = owner
  157. else:
  158. pool.owning_group = owner
  159. else:
  160. pool.owning_user = u
  161. # new systems pool are visible to everybody by default
  162. pool.access_policy = SystemAccessPolicy()
  163. pool.access_policy.add_rule(SystemPermission.view, everybody=True)
  164. pool.record_activity(user=u, service=u'HTTP',
  165. action=u'Created', field=u'Pool',
  166. new=unicode(pool))
  167. response = jsonify(pool.__json__())
  168. response.status_code = 201
  169. response.headers.add('Location', absolute_url(pool.href))
  170. return response
  171. @app.route('/pools/<pool_name>/', methods=['PATCH'])
  172. @auth_required
  173. def update_pool(pool_name):
  174. """
  175. Updates attributes of an existing system pool. The request body must be a JSON
  176. object containing one or more of the following keys.
  177. :param pool_name: System pool's name.
  178. :jsonparam string name: New name for the system pool.
  179. :jsonparam string description: Description of the system pool.
  180. :jsonparam object owner: JSON object containing a ``user_name`` key or
  181. ``group_name`` key identifying the new owner for the system pool.
  182. :status 200: System pool was updated.
  183. :status 400: Invalid data was given.
  184. """
  185. pool = _get_pool_by_name(pool_name)
  186. if not pool.can_edit(identity.current.user):
  187. raise Forbidden403('Cannot edit system pool')
  188. data = read_json_request(request)
  189. # helper for recording activity below
  190. def record_activity(field, old, new, action=u'Changed'):
  191. pool.record_activity(user=identity.current.user, service=u'HTTP',
  192. action=action, field=field, old=old, new=new)
  193. with convert_internal_errors():
  194. renamed = False
  195. if 'name' in data:
  196. new_name = data['name']
  197. if new_name != pool.name:
  198. if SystemPool.query.filter(SystemPool.name == new_name).count():
  199. raise Conflict409('System pool %s already exists' % new_name)
  200. record_activity(u'Name', pool.name, new_name)
  201. pool.name = new_name
  202. renamed = True
  203. if 'description' in data:
  204. new_description = data['description']
  205. if new_description != pool.description:
  206. record_activity(u'Description', pool.description, new_description)
  207. pool.description = new_description
  208. if 'owner' in data:
  209. new_owner, owner_type = _get_owner(data['owner'])
  210. if owner_type == 'user':
  211. pool.change_owner(user=new_owner)
  212. else:
  213. pool.change_owner(group=new_owner)
  214. response = jsonify(pool.__json__())
  215. if renamed:
  216. response.headers.add('Location', absolute_url(pool.href))
  217. return response
  218. # For compat only. Separate function so that it doesn't appear in the docs.
  219. @app.route('/pools/<pool_name>/', methods=['POST'])
  220. def update_system_pool_post(pool_name):
  221. return update_pool(pool_name)
  222. @app.route('/pools/<pool_name>/systems/', methods=['POST'])
  223. @auth_required
  224. def add_system_to_pool(pool_name):
  225. """
  226. Add a system to a system pool
  227. :param pool_name: System pool's name.
  228. :jsonparam fqdn: System's fully-qualified domain name.
  229. """
  230. u = identity.current.user
  231. data = read_json_request(request)
  232. pool = _get_pool_by_name(pool_name, lockmode='update')
  233. if 'fqdn' not in data:
  234. raise BadRequest400('System FQDN not specified')
  235. try:
  236. system = System.by_fqdn(data['fqdn'], u)
  237. except DatabaseLookupError:
  238. raise BadRequest400("System '%s' does not exist" % data['fqdn'])
  239. if not pool in system.pools:
  240. if pool.can_edit(u) and system.can_edit(u):
  241. system.record_activity(user=u, service=u'HTTP',
  242. action=u'Added', field=u'Pool',
  243. old=None,
  244. new=unicode(pool))
  245. system.pools.append(pool)
  246. system.date_modified = datetime.datetime.utcnow()
  247. pool.record_activity(user=u, service=u'HTTP',
  248. action=u'Added', field=u'System', old=None,
  249. new=unicode(system))
  250. else:
  251. if not pool.can_edit(u):
  252. raise Forbidden403('You do not have permission to '
  253. 'add systems to pool %s' % pool.name)
  254. if not system.can_edit(u):
  255. raise Forbidden403('You do not have permission to '
  256. 'modify system %s' % system.fqdn)
  257. return '', 204
  258. @app.route('/pools/<pool_name>/systems/', methods=['DELETE'])
  259. @auth_required
  260. def remove_system_from_pool(pool_name):
  261. """
  262. Remove a system from a system pool
  263. :param pool_name: System pool's name.
  264. :queryparam fqdn: System's fully-qualified domain name
  265. """
  266. if 'fqdn' not in request.args:
  267. raise MethodNotAllowed405
  268. fqdn = request.args['fqdn']
  269. system = _get_system_by_FQDN(fqdn)
  270. u = identity.current.user
  271. pool = _get_pool_by_name(pool_name, lockmode='update')
  272. if pool in system.pools:
  273. if pool.can_edit(u) or system.can_edit(u):
  274. if system.active_access_policy == pool.access_policy:
  275. system.active_access_policy = system.custom_access_policy
  276. system.record_activity(user=u, service=u'HTTP',
  277. field=u'Active Access Policy',
  278. action=u'Changed',
  279. old = pool.access_policy,
  280. new = system.custom_access_policy)
  281. system.pools.remove(pool)
  282. system.record_activity(user=u, service=u'HTTP',
  283. action=u'Removed', field=u'Pool', old=unicode(pool), new=None)
  284. system.date_modified = datetime.datetime.utcnow()
  285. pool.record_activity(user=u, service=u'HTTP',
  286. action=u'Removed', field=u'System', old=unicode(system), new=None)
  287. else:
  288. raise Forbidden403('You do not have permission to modify system %s'
  289. 'or remove systems from pool %s' % (system.fqdn, pool.name))
  290. else:
  291. raise BadRequest400('System %s is not in pool %s' % (system.fqdn, pool.name))
  292. return '', 204
  293. @app.route('/pools/<pool_name>/access-policy/', methods=['GET'])
  294. def get_access_policy(pool_name):
  295. """
  296. Get access policy for pool
  297. :param pool_name: System pool's name.
  298. """
  299. pool = _get_pool_by_name(pool_name)
  300. rules = pool.access_policy.rules
  301. return jsonify({
  302. 'id': pool.access_policy.id,
  303. 'rules': [
  304. {'id': rule.id,
  305. 'user': rule.user.user_name if rule.user else None,
  306. 'group': rule.group.group_name if rule.group else None,
  307. 'everybody': rule.everybody,
  308. 'permission': unicode(rule.permission)}
  309. for rule in rules],
  310. 'possible_permissions': [
  311. {'value': unicode(permission),
  312. 'label': unicode(permission.label)}
  313. for permission in SystemPermission],
  314. })
  315. @app.route('/pools/<pool_name>/access-policy/', methods=['POST', 'PUT'])
  316. @auth_required
  317. def save_access_policy(pool_name):
  318. """
  319. Updates the access policy for a system pool.
  320. :param pool_name: System pool's name.
  321. :jsonparam array rules: List of rules to include in the new policy. This
  322. replaces all existing rules in the policy. Each rule is a JSON object
  323. with ``user``, ``group``, and ``everybody`` keys.
  324. """
  325. pool = _get_pool_by_name(pool_name)
  326. if not pool.can_edit_policy(identity.current.user):
  327. raise Forbidden403('Cannot edit system pool policy')
  328. data = read_json_request(request)
  329. _edit_access_policy_rules(pool, pool.access_policy, data['rules'])
  330. return jsonify(pool.access_policy.__json__())
  331. @app.route('/pools/<pool_name>/access-policy/rules/', methods=['POST'])
  332. @auth_required
  333. def add_access_policy_rule(pool_name):
  334. """
  335. Adds a new rule to the access policy for a system pool. Each rule in the policy
  336. grants a permission to a single user, a group of users, or to everybody.
  337. See :ref:`system-access-policies-api` for a description of the expected JSON parameters.
  338. :param pool_name: System pool's name.
  339. """
  340. pool = _get_pool_by_name(pool_name)
  341. if not pool.can_edit_policy(identity.current.user):
  342. raise Forbidden403('Cannot edit system pool policy')
  343. policy = pool.access_policy
  344. rule = read_json_request(request)
  345. if rule.get('user', None):
  346. user = User.by_user_name(rule['user'])
  347. if not user:
  348. raise BadRequest400("User '%s' does not exist" % rule['user'])
  349. if user.removed:
  350. raise BadRequest400('Cannot add deleted user %s to access policy' % user.user_name)
  351. else:
  352. user = None
  353. if rule.get('group', None):
  354. try:
  355. group = Group.by_name(rule['group'])
  356. except NoResultFound:
  357. raise BadRequest400("Group '%s' does not exist" % rule['group'])
  358. else:
  359. group = None
  360. try:
  361. permission = SystemPermission.from_string(rule['permission'])
  362. except ValueError:
  363. raise BadRequest400('Invalid permission')
  364. new_rule = policy.add_rule(user=user, group=group,
  365. everybody=rule['everybody'],
  366. permission=permission)
  367. pool.record_activity(user=identity.current.user, service=u'HTTP',
  368. field=u'Access Policy Rule', action=u'Added',
  369. new=repr(new_rule))
  370. return '', 204
  371. @app.route('/pools/<pool_name>/access-policy/rules/', methods=['DELETE'])
  372. @auth_required
  373. def delete_access_policy_rules(pool_name):
  374. """
  375. Deletes one or more matching rules from a system pool's access policy.
  376. See :ref:`system-access-policies-api` for description of the expected query parameters
  377. :param pool_name: System pool's name.
  378. """
  379. pool = _get_pool_by_name(pool_name)
  380. if not pool.can_edit_policy(identity.current.user):
  381. raise Forbidden403('Cannot edit system policy')
  382. policy = pool.access_policy
  383. query = SystemAccessPolicyRule.query.filter(SystemAccessPolicyRule.policy == policy)
  384. if 'permission' in request.args:
  385. query = query.filter(SystemAccessPolicyRule.permission.in_(
  386. request.args.getlist('permission', type=SystemPermission.from_string)))
  387. else:
  388. raise MethodNotAllowed405
  389. if 'user' in request.args:
  390. query = query.join(SystemAccessPolicyRule.user)\
  391. .filter(User.user_name.in_(request.args.getlist('user')))
  392. elif 'group' in request.args:
  393. query = query.join(SystemAccessPolicyRule.group)\
  394. .filter(Group.group_name.in_(request.args.getlist('group')))
  395. elif 'everybody' in request.args:
  396. query = query.filter(SystemAccessPolicyRule.everybody)
  397. else:
  398. raise MethodNotAllowed405
  399. for rule in query:
  400. rule.record_deletion(service=u'HTTP')
  401. session.delete(rule)
  402. return '', 204
  403. @app.route('/pools/<pool_name>/', methods=['DELETE'])
  404. @auth_required
  405. def delete_pool(pool_name):
  406. """
  407. Deletes a system pool
  408. :param pool_name: System pool's name
  409. """
  410. pool = _get_pool_by_name(pool_name, lockmode='update')
  411. u = identity.current.user
  412. if not pool.can_edit(u):
  413. raise Forbidden403('Cannot delete pool %s' % pool_name)
  414. systems = System.query.filter(System.pools.contains(pool))
  415. System.record_bulk_activity(systems, user=identity.current.user,
  416. service=u'HTTP', action=u'Removed',
  417. field=u'Pool',
  418. old=unicode(pool),
  419. new=None)
  420. # Since we are deleting the pool, we will have to change the active
  421. # access policy for all systems using the pool's policy to their
  422. # custom policy
  423. systems = System.query.filter(System.active_access_policy == pool.access_policy)
  424. for system in systems:
  425. system.active_access_policy = system.custom_access_policy
  426. System.record_bulk_activity(systems, user=identity.current.user,
  427. service=u'HTTP',
  428. field=u'Active Access Policy', action=u'Changed',
  429. old = 'Pool policy: %s' % pool_name,
  430. new = 'Custom access policy')
  431. session.delete(pool)
  432. activity = Activity(u, u'HTTP', u'Deleted', u'Pool', pool_name)
  433. session.add(activity)
  434. return '', 204