diff options
author | Ruan HE <ruan.he@orange.com> | 2016-06-09 08:12:34 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@172.30.200.206> | 2016-06-09 08:12:34 +0000 |
commit | 4bc079a2664f9a407e332291f34d174625a9d5ea (patch) | |
tree | 7481cd5d0a9b3ce37c44c797a1e0d39881221cbe /keystone-moon/keystone/assignment/controllers.py | |
parent | 2f179c5790fbbf6144205d3c6e5089e6eb5f048a (diff) | |
parent | 2e7b4f2027a1147ca28301e4f88adf8274b39a1f (diff) |
Merge "Update Keystone core to Mitaka."
Diffstat (limited to 'keystone-moon/keystone/assignment/controllers.py')
-rw-r--r-- | keystone-moon/keystone/assignment/controllers.py | 383 |
1 files changed, 348 insertions, 35 deletions
diff --git a/keystone-moon/keystone/assignment/controllers.py b/keystone-moon/keystone/assignment/controllers.py index bbaf9437..1b163013 100644 --- a/keystone-moon/keystone/assignment/controllers.py +++ b/keystone-moon/keystone/assignment/controllers.py @@ -27,6 +27,7 @@ from keystone.common import controller from keystone.common import dependency from keystone.common import utils from keystone.common import validation +from keystone.common import wsgi from keystone import exception from keystone.i18n import _ from keystone import notifications @@ -40,7 +41,7 @@ LOG = log.getLogger(__name__) class TenantAssignment(controller.V2Controller): """The V2 Project APIs that are processing assignments.""" - @controller.v2_deprecated + @controller.v2_auth_deprecated def get_projects_for_token(self, context, **kw): """Get valid tenants for token based on token used to authenticate. @@ -138,6 +139,11 @@ class RoleAssignmentV2(controller.V2Controller): """ self.assert_admin(context) + # NOTE(davechen): Router without project id is defined, + # but we don't plan on implementing this. + if tenant_id is None: + raise exception.NotImplemented( + message=_('User roles not supported: tenant_id required')) roles = self.assignment_api.get_roles_for_user_and_project( user_id, tenant_id) return {'roles': [self.role_api.get_role(x) @@ -261,7 +267,7 @@ class ProjectAssignmentV3(controller.V3Controller): super(ProjectAssignmentV3, self).__init__() self.get_member_from_driver = self.resource_api.get_project - @controller.filterprotected('enabled', 'name') + @controller.filterprotected('domain_id', 'enabled', 'name') def list_user_projects(self, context, filters, user_id): hints = ProjectAssignmentV3.build_driver_hints(context, filters) refs = self.assignment_api.list_projects_for_user(user_id, @@ -271,7 +277,19 @@ class ProjectAssignmentV3(controller.V3Controller): @dependency.requires('role_api') class RoleV3(controller.V3Controller): - """The V3 Role CRUD APIs.""" + """The V3 Role CRUD APIs. + + To ease complexity (and hence risk) in writing the policy rules for the + role APIs, we create separate policy actions for roles that are domain + specific, as opposed to those that are global. In order to achieve this + each of the role API methods has a wrapper method that checks to see if the + role is global or domain specific. + + NOTE (henry-nash): If this separate global vs scoped policy action pattern + becomes repeated for other entities, we should consider encapsulating this + into a specialized router class. + + """ collection_name = 'roles' member_name = 'role' @@ -280,9 +298,104 @@ class RoleV3(controller.V3Controller): super(RoleV3, self).__init__() self.get_member_from_driver = self.role_api.get_role + def _is_domain_role(self, role): + return role.get('domain_id') is not None + + def _is_domain_role_target(self, role_id): + try: + role = self.role_api.get_role(role_id) + except exception.RoleNotFound: + # We hide this error since we have not yet carried out a policy + # check - and it maybe that the caller isn't authorized to make + # this call. If so, we want that error to be raised instead. + return False + return self._is_domain_role(role) + + def create_role_wrapper(self, context, role): + if self._is_domain_role(role): + return self.create_domain_role(context, role=role) + else: + return self.create_role(context, role=role) + @controller.protected() @validation.validated(schema.role_create, 'role') def create_role(self, context, role): + return self._create_role(context, role) + + @controller.protected() + @validation.validated(schema.role_create, 'role') + def create_domain_role(self, context, role): + return self._create_role(context, role) + + def list_roles_wrapper(self, context): + # If there is no domain_id filter defined, then we only want to return + # global roles, so we set the domain_id filter to None. + params = context['query_string'] + if 'domain_id' not in params: + context['query_string']['domain_id'] = None + + if context['query_string']['domain_id'] is not None: + return self.list_domain_roles(context) + else: + return self.list_roles(context) + + @controller.filterprotected('name', 'domain_id') + def list_roles(self, context, filters): + return self._list_roles(context, filters) + + @controller.filterprotected('name', 'domain_id') + def list_domain_roles(self, context, filters): + return self._list_roles(context, filters) + + def get_role_wrapper(self, context, role_id): + if self._is_domain_role_target(role_id): + return self.get_domain_role(context, role_id=role_id) + else: + return self.get_role(context, role_id=role_id) + + @controller.protected() + def get_role(self, context, role_id): + return self._get_role(context, role_id) + + @controller.protected() + def get_domain_role(self, context, role_id): + return self._get_role(context, role_id) + + def update_role_wrapper(self, context, role_id, role): + # Since we don't allow you change whether a role is global or domain + # specific, we can ignore the new update attributes and just look at + # the existing role. + if self._is_domain_role_target(role_id): + return self.update_domain_role( + context, role_id=role_id, role=role) + else: + return self.update_role(context, role_id=role_id, role=role) + + @controller.protected() + @validation.validated(schema.role_update, 'role') + def update_role(self, context, role_id, role): + return self._update_role(context, role_id, role) + + @controller.protected() + @validation.validated(schema.role_update, 'role') + def update_domain_role(self, context, role_id, role): + return self._update_role(context, role_id, role) + + def delete_role_wrapper(self, context, role_id): + if self._is_domain_role_target(role_id): + return self.delete_domain_role(context, role_id=role_id) + else: + return self.delete_role(context, role_id=role_id) + + @controller.protected() + def delete_role(self, context, role_id): + return self._delete_role(context, role_id) + + @controller.protected() + def delete_domain_role(self, context, role_id): + return self._delete_role(context, role_id) + + def _create_role(self, context, role): if role['name'] == CONF.member_role_name: # Use the configured member role ID when creating the configured # member role name. This avoids the potential of creating a @@ -297,32 +410,146 @@ class RoleV3(controller.V3Controller): ref = self.role_api.create_role(ref['id'], ref, initiator) return RoleV3.wrap_member(context, ref) - @controller.filterprotected('name') - def list_roles(self, context, filters): + def _list_roles(self, context, filters): hints = RoleV3.build_driver_hints(context, filters) refs = self.role_api.list_roles( hints=hints) return RoleV3.wrap_collection(context, refs, hints=hints) - @controller.protected() - def get_role(self, context, role_id): + def _get_role(self, context, role_id): ref = self.role_api.get_role(role_id) return RoleV3.wrap_member(context, ref) - @controller.protected() - @validation.validated(schema.role_update, 'role') - def update_role(self, context, role_id, role): + def _update_role(self, context, role_id, role): self._require_matching_id(role_id, role) initiator = notifications._get_request_audit_info(context) ref = self.role_api.update_role(role_id, role, initiator) return RoleV3.wrap_member(context, ref) - @controller.protected() - def delete_role(self, context, role_id): + def _delete_role(self, context, role_id): initiator = notifications._get_request_audit_info(context) self.role_api.delete_role(role_id, initiator) +@dependency.requires('role_api') +class ImpliedRolesV3(controller.V3Controller): + """The V3 ImpliedRoles CRD APIs. There is no Update.""" + + def _prior_role_stanza(self, endpoint, prior_role_id, prior_role_name): + return { + "id": prior_role_id, + "links": { + "self": endpoint + "/v3/roles/" + prior_role_id + }, + "name": prior_role_name + } + + def _implied_role_stanza(self, endpoint, implied_role): + implied_id = implied_role['id'] + implied_response = { + "id": implied_id, + "links": { + "self": endpoint + "/v3/roles/" + implied_id + }, + "name": implied_role['name'] + } + return implied_response + + def _populate_prior_role_response(self, endpoint, prior_id): + prior_role = self.role_api.get_role(prior_id) + response = { + "role_inference": { + "prior_role": self._prior_role_stanza( + endpoint, prior_id, prior_role['name']) + } + } + return response + + def _populate_implied_roles_response(self, endpoint, + prior_id, implied_ids): + response = self._populate_prior_role_response(endpoint, prior_id) + response["role_inference"]['implies'] = [] + for implied_id in implied_ids: + implied_role = self.role_api.get_role(implied_id) + implied_response = self._implied_role_stanza( + endpoint, implied_role) + response["role_inference"]['implies'].append(implied_response) + return response + + def _populate_implied_role_response(self, endpoint, prior_id, implied_id): + response = self._populate_prior_role_response(endpoint, prior_id) + implied_role = self.role_api.get_role(implied_id) + stanza = self._implied_role_stanza(endpoint, implied_role) + response["role_inference"]['implies'] = stanza + return response + + @controller.protected() + def get_implied_role(self, context, prior_role_id, implied_role_id): + ref = self.role_api.get_implied_role(prior_role_id, implied_role_id) + + prior_id = ref['prior_role_id'] + implied_id = ref['implied_role_id'] + endpoint = super(controller.V3Controller, ImpliedRolesV3).base_url( + context, 'public') + response = self._populate_implied_role_response( + endpoint, prior_id, implied_id) + return response + + @controller.protected() + def check_implied_role(self, context, prior_role_id, implied_role_id): + self.role_api.get_implied_role(prior_role_id, implied_role_id) + + @controller.protected() + def create_implied_role(self, context, prior_role_id, implied_role_id): + self.role_api.create_implied_role(prior_role_id, implied_role_id) + return wsgi.render_response( + self.get_implied_role(context, prior_role_id, implied_role_id), + status=(201, 'Created')) + + @controller.protected() + def delete_implied_role(self, context, prior_role_id, implied_role_id): + self.role_api.delete_implied_role(prior_role_id, implied_role_id) + + @controller.protected() + def list_implied_roles(self, context, prior_role_id): + ref = self.role_api.list_implied_roles(prior_role_id) + implied_ids = [r['implied_role_id'] for r in ref] + endpoint = super(controller.V3Controller, ImpliedRolesV3).base_url( + context, 'public') + + results = self._populate_implied_roles_response( + endpoint, prior_role_id, implied_ids) + + return results + + @controller.protected() + def list_role_inference_rules(self, context): + refs = self.role_api.list_role_inference_rules() + role_dict = {role_ref['id']: role_ref + for role_ref in self.role_api.list_roles()} + + rules = dict() + endpoint = super(controller.V3Controller, ImpliedRolesV3).base_url( + context, 'public') + + for ref in refs: + implied_role_id = ref['implied_role_id'] + prior_role_id = ref['prior_role_id'] + implied = rules.get(prior_role_id, []) + implied.append(self._implied_role_stanza( + endpoint, role_dict[implied_role_id])) + rules[prior_role_id] = implied + + inferences = [] + for prior_id, implied in rules.items(): + prior_response = self._prior_role_stanza( + endpoint, prior_id, role_dict[prior_id]['name']) + inferences.append({'prior_role': prior_response, + 'implies': implied}) + results = {'role_inferences': inferences} + return results + + @dependency.requires('assignment_api', 'identity_api', 'resource_api', 'role_api') class GrantAssignmentV3(controller.V3Controller): @@ -475,6 +702,13 @@ class RoleAssignmentV3(controller.V3Controller): 'role_id': role_id, 'indirect': {'project_id': parent_id}} + or, for a role that was implied by a prior role: + + {'user_id': user_id, + 'project_id': project_id, + 'role_id': role_id, + 'indirect': {'role_id': prior role_id}} + It is possible to deduce if a role assignment came from group membership if it has both 'user_id' in the main body of the dict and 'group_id' in the 'indirect' subdict, as well as it is possible to @@ -505,13 +739,19 @@ class RoleAssignmentV3(controller.V3Controller): } """ - formatted_entity = {'links': {}} inherited_assignment = entity.get('inherited_to_projects') if 'project_id' in entity: - formatted_entity['scope'] = ( - {'project': {'id': entity['project_id']}}) + if 'project_name' in entity: + formatted_entity['scope'] = {'project': { + 'id': entity['project_id'], + 'name': entity['project_name'], + 'domain': {'id': entity['project_domain_id'], + 'name': entity['project_domain_name']}}} + else: + formatted_entity['scope'] = { + 'project': {'id': entity['project_id']}} if 'domain_id' in entity.get('indirect', {}): inherited_assignment = True @@ -524,12 +764,24 @@ class RoleAssignmentV3(controller.V3Controller): else: formatted_link = '/projects/%s' % entity['project_id'] elif 'domain_id' in entity: - formatted_entity['scope'] = {'domain': {'id': entity['domain_id']}} + if 'domain_name' in entity: + formatted_entity['scope'] = { + 'domain': {'id': entity['domain_id'], + 'name': entity['domain_name']}} + else: + formatted_entity['scope'] = { + 'domain': {'id': entity['domain_id']}} formatted_link = '/domains/%s' % entity['domain_id'] if 'user_id' in entity: - formatted_entity['user'] = {'id': entity['user_id']} - + if 'user_name' in entity: + formatted_entity['user'] = { + 'id': entity['user_id'], + 'name': entity['user_name'], + 'domain': {'id': entity['user_domain_id'], + 'name': entity['user_domain_name']}} + else: + formatted_entity['user'] = {'id': entity['user_id']} if 'group_id' in entity.get('indirect', {}): membership_url = ( self.base_url(context, '/groups/%s/users/%s' % ( @@ -539,11 +791,31 @@ class RoleAssignmentV3(controller.V3Controller): else: formatted_link += '/users/%s' % entity['user_id'] elif 'group_id' in entity: - formatted_entity['group'] = {'id': entity['group_id']} + if 'group_name' in entity: + formatted_entity['group'] = { + 'id': entity['group_id'], + 'name': entity['group_name'], + 'domain': {'id': entity['group_domain_id'], + 'name': entity['group_domain_name']}} + else: + formatted_entity['group'] = {'id': entity['group_id']} formatted_link += '/groups/%s' % entity['group_id'] - formatted_entity['role'] = {'id': entity['role_id']} - formatted_link += '/roles/%s' % entity['role_id'] + if 'role_name' in entity: + formatted_entity['role'] = {'id': entity['role_id'], + 'name': entity['role_name']} + else: + formatted_entity['role'] = {'id': entity['role_id']} + prior_role_link = '' + if 'role_id' in entity.get('indirect', {}): + formatted_link += '/roles/%s' % entity['indirect']['role_id'] + prior_role_link = ( + '/prior_role/%(prior)s/implies/%(implied)s' % { + 'prior': entity['role_id'], + 'implied': entity['indirect']['role_id'] + }) + else: + formatted_link += '/roles/%s' % entity['role_id'] if inherited_assignment: formatted_entity['scope']['OS-INHERIT:inherited_to'] = ( @@ -553,6 +825,9 @@ class RoleAssignmentV3(controller.V3Controller): formatted_entity['links']['assignment'] = self.base_url(context, formatted_link) + if prior_role_link: + formatted_entity['links']['prior_role'] = ( + self.base_url(context, prior_role_link)) return formatted_entity @@ -586,10 +861,7 @@ class RoleAssignmentV3(controller.V3Controller): msg = _('Specify a user or group, not both') raise exception.ValidationError(msg) - @controller.filterprotected('group.id', 'role.id', - 'scope.domain.id', 'scope.project.id', - 'scope.OS-INHERIT:inherited_to', 'user.id') - def list_role_assignments(self, context, filters): + def _list_role_assignments(self, context, filters, include_subtree=False): """List role assignments to user and groups on domains and projects. Return a list of all existing role assignments in the system, filtered @@ -615,6 +887,8 @@ class RoleAssignmentV3(controller.V3Controller): params = context['query_string'] effective = 'effective' in params and ( self.query_filter_is_true(params['effective'])) + include_names = ('include_names' in params and + self.query_filter_is_true(params['include_names'])) if 'scope.OS-INHERIT:inherited_to' in params: inherited = ( @@ -640,20 +914,59 @@ class RoleAssignmentV3(controller.V3Controller): group_id=params.get('group.id'), domain_id=params.get('scope.domain.id'), project_id=params.get('scope.project.id'), - inherited=inherited, effective=effective) + include_subtree=include_subtree, + inherited=inherited, effective=effective, + include_names=include_names) formatted_refs = [self._format_entity(context, ref) for ref in refs] return self.wrap_collection(context, formatted_refs) - @controller.protected() - def get_role_assignment(self, context): - raise exception.NotImplemented() + @controller.filterprotected('group.id', 'role.id', + 'scope.domain.id', 'scope.project.id', + 'scope.OS-INHERIT:inherited_to', 'user.id') + def list_role_assignments(self, context, filters): + return self._list_role_assignments(context, filters) - @controller.protected() - def update_role_assignment(self, context): - raise exception.NotImplemented() + def _check_list_tree_protection(self, context, protection_info): + """Check protection for list assignment for tree API. - @controller.protected() - def delete_role_assignment(self, context): - raise exception.NotImplemented() + The policy rule might want to inspect the domain of any project filter + so if one is defined, then load the project ref and pass it to the + check protection method. + + """ + ref = {} + for filter, value in protection_info['filter_attr'].items(): + if filter == 'scope.project.id' and value: + ref['project'] = self.resource_api.get_project(value) + + self.check_protection(context, protection_info, ref) + + @controller.filterprotected('group.id', 'role.id', + 'scope.domain.id', 'scope.project.id', + 'scope.OS-INHERIT:inherited_to', 'user.id', + callback=_check_list_tree_protection) + def list_role_assignments_for_tree(self, context, filters): + if not context['query_string'].get('scope.project.id'): + msg = _('scope.project.id must be specified if include_subtree ' + 'is also specified') + raise exception.ValidationError(message=msg) + return self._list_role_assignments(context, filters, + include_subtree=True) + + def list_role_assignments_wrapper(self, context): + """Main entry point from router for list role assignments. + + Since we want different policy file rules to be applicable based on + whether there the include_subtree query parameter is part of the API + call, this method checks for this and then calls the appropriate + protected entry point. + + """ + params = context['query_string'] + if 'include_subtree' in params and ( + self.query_filter_is_true(params['include_subtree'])): + return self.list_role_assignments_for_tree(context) + else: + return self.list_role_assignments(context) |