OpenStack Orchestration (Heat)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

472 lines
18KB

  1. #
  2. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  3. # not use this file except in compliance with the License. You may obtain
  4. # a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software
  9. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  10. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  11. # License for the specific language governing permissions and limitations
  12. # under the License.
  13. from heat.common import exception
  14. from heat.common.i18n import _
  15. from heat.engine import constraints
  16. from heat.engine import properties
  17. from heat.engine import resource
  18. from heat.engine import support
  19. class KeystoneRoleAssignmentMixin(object):
  20. """Implements role assignments between user/groups and project/domain.
  21. For example::
  22. heat_template_version: 2013-05-23
  23. parameters:
  24. ... Group or User parameters
  25. group_role:
  26. type: string
  27. description: role
  28. group_role_domain:
  29. type: string
  30. description: group role domain
  31. group_role_project:
  32. type: string
  33. description: group role project
  34. resources:
  35. admin_group:
  36. type: OS::Keystone::Group OR OS::Keystone::User
  37. properties:
  38. ... Group or User properties
  39. roles:
  40. - role: {get_param: group_role}
  41. domain: {get_param: group_role_domain}
  42. - role: {get_param: group_role}
  43. project: {get_param: group_role_project}
  44. """
  45. PROPERTIES = (
  46. ROLES
  47. ) = (
  48. 'roles'
  49. )
  50. _ROLES_MAPPING_PROPERTIES = (
  51. ROLE, DOMAIN, PROJECT
  52. ) = (
  53. 'role', 'domain', 'project'
  54. )
  55. mixin_properties_schema = {
  56. ROLES: properties.Schema(
  57. properties.Schema.LIST,
  58. _('List of role assignments.'),
  59. schema=properties.Schema(
  60. properties.Schema.MAP,
  61. _('Map between role with either project or domain.'),
  62. schema={
  63. ROLE: properties.Schema(
  64. properties.Schema.STRING,
  65. _('Keystone role.'),
  66. required=True,
  67. constraints=([constraints.
  68. CustomConstraint('keystone.role')])
  69. ),
  70. PROJECT: properties.Schema(
  71. properties.Schema.STRING,
  72. _('Keystone project.'),
  73. constraints=([constraints.
  74. CustomConstraint('keystone.project')])
  75. ),
  76. DOMAIN: properties.Schema(
  77. properties.Schema.STRING,
  78. _('Keystone domain.'),
  79. constraints=([constraints.
  80. CustomConstraint('keystone.domain')])
  81. ),
  82. }
  83. ),
  84. update_allowed=True
  85. )
  86. }
  87. def _add_role_assignments_to_group(self, group_id, role_assignments):
  88. for role_assignment in self._normalize_to_id(role_assignments):
  89. if role_assignment.get(self.PROJECT) is not None:
  90. self.client().roles.grant(
  91. role=role_assignment.get(self.ROLE),
  92. project=role_assignment.get(self.PROJECT),
  93. group=group_id
  94. )
  95. elif role_assignment.get(self.DOMAIN) is not None:
  96. self.client().roles.grant(
  97. role=role_assignment.get(self.ROLE),
  98. domain=role_assignment.get(self.DOMAIN),
  99. group=group_id
  100. )
  101. def _add_role_assignments_to_user(self, user_id, role_assignments):
  102. for role_assignment in self._normalize_to_id(role_assignments):
  103. if role_assignment.get(self.PROJECT) is not None:
  104. self.client().roles.grant(
  105. role=role_assignment.get(self.ROLE),
  106. project=role_assignment.get(self.PROJECT),
  107. user=user_id
  108. )
  109. elif role_assignment.get(self.DOMAIN) is not None:
  110. self.client().roles.grant(
  111. role=role_assignment.get(self.ROLE),
  112. domain=role_assignment.get(self.DOMAIN),
  113. user=user_id
  114. )
  115. def _remove_role_assignments_from_group(self, group_id, role_assignments,
  116. current_assignments):
  117. for role_assignment in self._normalize_to_id(role_assignments):
  118. if role_assignment in current_assignments:
  119. if role_assignment.get(self.PROJECT) is not None:
  120. self.client().roles.revoke(
  121. role=role_assignment.get(self.ROLE),
  122. project=role_assignment.get(self.PROJECT),
  123. group=group_id
  124. )
  125. elif role_assignment.get(self.DOMAIN) is not None:
  126. self.client().roles.revoke(
  127. role=role_assignment.get(self.ROLE),
  128. domain=role_assignment.get(self.DOMAIN),
  129. group=group_id
  130. )
  131. def _remove_role_assignments_from_user(self, user_id, role_assignments,
  132. current_assignments):
  133. for role_assignment in self._normalize_to_id(role_assignments):
  134. if role_assignment in current_assignments:
  135. if role_assignment.get(self.PROJECT) is not None:
  136. self.client().roles.revoke(
  137. role=role_assignment.get(self.ROLE),
  138. project=role_assignment.get(self.PROJECT),
  139. user=user_id
  140. )
  141. elif role_assignment.get(self.DOMAIN) is not None:
  142. self.client().roles.revoke(
  143. role=role_assignment.get(self.ROLE),
  144. domain=role_assignment.get(self.DOMAIN),
  145. user=user_id
  146. )
  147. def _normalize_to_id(self, role_assignment_prps):
  148. role_assignments = []
  149. if role_assignment_prps is None:
  150. return role_assignments
  151. for role_assignment in role_assignment_prps:
  152. role = role_assignment.get(self.ROLE)
  153. project = role_assignment.get(self.PROJECT)
  154. domain = role_assignment.get(self.DOMAIN)
  155. role_assignments.append({
  156. self.ROLE: self.client_plugin().get_role_id(role),
  157. self.PROJECT: (self.client_plugin().
  158. get_project_id(project)) if project else None,
  159. self.DOMAIN: (self.client_plugin().
  160. get_domain_id(domain)) if domain else None
  161. })
  162. return role_assignments
  163. def _find_differences(self, updated_prps, stored_prps):
  164. updated_role_project_assignments = []
  165. updated_role_domain_assignments = []
  166. # Split the properties into two set of role assignments
  167. # (project, domain) from updated properties
  168. for role_assignment in updated_prps or []:
  169. if role_assignment.get(self.PROJECT) is not None:
  170. updated_role_project_assignments.append(
  171. '%s:%s' % (
  172. role_assignment[self.ROLE],
  173. role_assignment[self.PROJECT]))
  174. elif (role_assignment.get(self.DOMAIN)
  175. is not None):
  176. updated_role_domain_assignments.append(
  177. '%s:%s' % (role_assignment[self.ROLE],
  178. role_assignment[self.DOMAIN]))
  179. stored_role_project_assignments = []
  180. stored_role_domain_assignments = []
  181. # Split the properties into two set of role assignments
  182. # (project, domain) from updated properties
  183. for role_assignment in (stored_prps or []):
  184. if role_assignment.get(self.PROJECT) is not None:
  185. stored_role_project_assignments.append(
  186. '%s:%s' % (
  187. role_assignment[self.ROLE],
  188. role_assignment[self.PROJECT]))
  189. elif (role_assignment.get(self.DOMAIN)
  190. is not None):
  191. stored_role_domain_assignments.append(
  192. '%s:%s' % (role_assignment[self.ROLE],
  193. role_assignment[self.DOMAIN]))
  194. new_role_assignments = []
  195. removed_role_assignments = []
  196. # NOTE: finding the diff of list of strings is easier by using 'set'
  197. # so properties are converted to string in above sections
  198. # New items
  199. for item in (set(updated_role_project_assignments) -
  200. set(stored_role_project_assignments)):
  201. new_role_assignments.append(
  202. {self.ROLE: item[:item.find(':')],
  203. self.PROJECT: item[item.find(':') + 1:]}
  204. )
  205. for item in (set(updated_role_domain_assignments) -
  206. set(stored_role_domain_assignments)):
  207. new_role_assignments.append(
  208. {self.ROLE: item[:item.find(':')],
  209. self.DOMAIN: item[item.find(':') + 1:]}
  210. )
  211. # Old items
  212. for item in (set(stored_role_project_assignments) -
  213. set(updated_role_project_assignments)):
  214. removed_role_assignments.append(
  215. {self.ROLE: item[:item.find(':')],
  216. self.PROJECT: item[item.find(':') + 1:]}
  217. )
  218. for item in (set(stored_role_domain_assignments) -
  219. set(updated_role_domain_assignments)):
  220. removed_role_assignments.append(
  221. {self.ROLE: item[:item.find(':')],
  222. self.DOMAIN: item[item.find(':') + 1:]}
  223. )
  224. return new_role_assignments, removed_role_assignments
  225. def create_assignment(self, user_id=None, group_id=None):
  226. if self.properties.get(self.ROLES) is not None:
  227. if user_id is not None:
  228. self._add_role_assignments_to_user(
  229. user_id,
  230. self.properties.get(self.ROLES))
  231. elif group_id is not None:
  232. self._add_role_assignments_to_group(
  233. group_id,
  234. self.properties.get(self.ROLES))
  235. def update_assignment(self, prop_diff, user_id=None, group_id=None):
  236. # if there is no change do not update
  237. if self.ROLES in prop_diff:
  238. (new_role_assignments,
  239. removed_role_assignments) = self._find_differences(
  240. prop_diff.get(self.ROLES),
  241. self.properties[self.ROLES])
  242. if len(new_role_assignments) > 0:
  243. if user_id is not None:
  244. self._add_role_assignments_to_user(
  245. user_id,
  246. new_role_assignments)
  247. elif group_id is not None:
  248. self._add_role_assignments_to_group(
  249. group_id,
  250. new_role_assignments)
  251. if len(removed_role_assignments) > 0:
  252. current_assignments = self.parse_list_assignments(
  253. user_id=user_id, group_id=group_id)
  254. if user_id is not None:
  255. self._remove_role_assignments_from_user(
  256. user_id,
  257. removed_role_assignments,
  258. current_assignments)
  259. elif group_id is not None:
  260. self._remove_role_assignments_from_group(
  261. group_id,
  262. removed_role_assignments,
  263. current_assignments)
  264. def delete_assignment(self, user_id=None, group_id=None):
  265. if self.properties[self.ROLES] is not None:
  266. current_assignments = self.parse_list_assignments(
  267. user_id=user_id, group_id=group_id)
  268. if user_id is not None:
  269. self._remove_role_assignments_from_user(
  270. user_id,
  271. (self.properties[self.ROLES]),
  272. current_assignments)
  273. elif group_id is not None:
  274. self._remove_role_assignments_from_group(
  275. group_id,
  276. (self.properties[self.ROLES]),
  277. current_assignments)
  278. def validate_assignment_properties(self):
  279. if self.properties.get(self.ROLES) is not None:
  280. for role_assignment in self.properties.get(self.ROLES):
  281. project = role_assignment.get(self.PROJECT)
  282. domain = role_assignment.get(self.DOMAIN)
  283. if project is not None and domain is not None:
  284. raise exception.ResourcePropertyConflict(self.PROJECT,
  285. self.DOMAIN)
  286. if project is None and domain is None:
  287. msg = _('Either project or domain must be specified for'
  288. ' role %s') % role_assignment.get(self.ROLE)
  289. raise exception.StackValidationFailed(message=msg)
  290. def parse_list_assignments(self, user_id=None, group_id=None):
  291. """Method used for get_live_state implementation in other resources."""
  292. assignments = []
  293. roles = []
  294. if user_id is not None:
  295. assignments = self.client().role_assignments.list(user=user_id)
  296. elif group_id is not None:
  297. assignments = self.client().role_assignments.list(group=group_id)
  298. for assignment in assignments:
  299. values = assignment.to_dict()
  300. if not values.get('role') or not values.get('role').get('id'):
  301. continue
  302. role = {
  303. self.ROLE: values['role']['id'],
  304. self.DOMAIN: (values.get('scope') and
  305. values['scope'].get('domain') and
  306. values['scope'].get('domain').get('id')),
  307. self.PROJECT: (values.get('scope') and
  308. values['scope'].get('project') and
  309. values['scope'].get('project').get('id')),
  310. }
  311. roles.append(role)
  312. return roles
  313. class KeystoneUserRoleAssignment(resource.Resource,
  314. KeystoneRoleAssignmentMixin):
  315. """Resource for granting roles to a user.
  316. Resource for specifying users and their's roles.
  317. """
  318. support_status = support.SupportStatus(
  319. version='5.0.0',
  320. message=_('Supported versions: keystone v3'))
  321. default_client_name = 'keystone'
  322. PROPERTIES = (
  323. USER,
  324. ) = (
  325. 'user',
  326. )
  327. properties_schema = {
  328. USER: properties.Schema(
  329. properties.Schema.STRING,
  330. _('Name or id of keystone user.'),
  331. required=True,
  332. update_allowed=True,
  333. constraints=[constraints.CustomConstraint('keystone.user')]
  334. )
  335. }
  336. properties_schema.update(
  337. KeystoneRoleAssignmentMixin.mixin_properties_schema)
  338. def client(self):
  339. return super(KeystoneUserRoleAssignment, self).client().client
  340. @property
  341. def user_id(self):
  342. try:
  343. return self.client_plugin().get_user_id(
  344. self.properties.get(self.USER))
  345. except Exception as ex:
  346. self.client_plugin().ignore_not_found(ex)
  347. return None
  348. def handle_create(self):
  349. self.create_assignment(user_id=self.user_id)
  350. def handle_update(self, json_snippet, tmpl_diff, prop_diff):
  351. self.update_assignment(user_id=self.user_id, prop_diff=prop_diff)
  352. def handle_delete(self):
  353. with self.client_plugin().ignore_not_found:
  354. self.delete_assignment(user_id=self.user_id)
  355. def validate(self):
  356. super(KeystoneUserRoleAssignment, self).validate()
  357. self.validate_assignment_properties()
  358. class KeystoneGroupRoleAssignment(resource.Resource,
  359. KeystoneRoleAssignmentMixin):
  360. """Resource for granting roles to a group.
  361. Resource for specifying groups and their's roles.
  362. """
  363. support_status = support.SupportStatus(
  364. version='5.0.0',
  365. message=_('Supported versions: keystone v3'))
  366. default_client_name = 'keystone'
  367. PROPERTIES = (
  368. GROUP,
  369. ) = (
  370. 'group',
  371. )
  372. properties_schema = {
  373. GROUP: properties.Schema(
  374. properties.Schema.STRING,
  375. _('Name or id of keystone group.'),
  376. required=True,
  377. update_allowed=True,
  378. constraints=[constraints.CustomConstraint('keystone.group')]
  379. )
  380. }
  381. properties_schema.update(
  382. KeystoneRoleAssignmentMixin.mixin_properties_schema)
  383. def client(self):
  384. return super(KeystoneGroupRoleAssignment, self).client().client
  385. @property
  386. def group_id(self):
  387. try:
  388. return self.client_plugin().get_group_id(
  389. self.properties.get(self.GROUP))
  390. except Exception as ex:
  391. self.client_plugin().ignore_not_found(ex)
  392. return None
  393. def handle_create(self):
  394. self.create_assignment(group_id=self.group_id)
  395. def handle_update(self, json_snippet, tmpl_diff, prop_diff):
  396. self.update_assignment(group_id=self.group_id, prop_diff=prop_diff)
  397. def handle_delete(self):
  398. with self.client_plugin().ignore_not_found:
  399. self.delete_assignment(group_id=self.group_id)
  400. def validate(self):
  401. super(KeystoneGroupRoleAssignment, self).validate()
  402. self.validate_assignment_properties()
  403. def resource_mapping():
  404. return {
  405. 'OS::Keystone::UserRoleAssignment': KeystoneUserRoleAssignment,
  406. 'OS::Keystone::GroupRoleAssignment': KeystoneGroupRoleAssignment
  407. }