diff --git a/cinder/api/contrib/quotas.py b/cinder/api/contrib/quotas.py index 0af8496ea28..fd60b129f09 100644 --- a/cinder/api/contrib/quotas.py +++ b/cinder/api/contrib/quotas.py @@ -85,6 +85,10 @@ class QuotaSetsController(wsgi.Controller): :param parent_id: The parent id of the project in which the user want to perform an update or delete operation. """ + if context_project.is_admin_project: + # The calling project has admin privileges and should be able + # to operate on all quotas. + return if context_project.parent_id and parent_id != context_project.id: msg = _("Update and delete quota operations can only be made " "by an admin of immediate parent or by the CLOUD admin.") @@ -105,15 +109,20 @@ class QuotaSetsController(wsgi.Controller): def _authorize_show(self, context_project, target_project): """Checks if show is allowed in the current hierarchy. - With hierarchical projects, are allowed to perform quota show operation - users with admin role in, at least, one of the following projects: the - current project; the immediate parent project; or the root project. + With hierarchical projects, users are allowed to perform a quota show + operation if they have the cloud admin role or if they belong to at + least one of the following projects: the target project, its immediate + parent project, or the root project of its hierarchy. :param context_project: The project in which the user is scoped to. :param target_project: The project in which the user wants to perform a show operation. """ + if context_project.is_admin_project: + # The calling project has admin privileges and should be able + # to view all quotas. + return if target_project.parent_id: if target_project.id != context_project.id: if not self._is_descendant(target_project.id, @@ -170,7 +179,8 @@ class QuotaSetsController(wsgi.Controller): target_project = quota_utils.get_project_hierarchy( context, target_project_id) context_project = quota_utils.get_project_hierarchy( - context, context.project_id, subtree_as_ids=True) + context, context.project_id, subtree_as_ids=True, + is_admin_project=context.is_admin) self._authorize_show(context_project, target_project) @@ -238,7 +248,8 @@ class QuotaSetsController(wsgi.Controller): # Get the children of the project which the token is scoped to # in order to know if the target_project is in its hierarchy. context_project = quota_utils.get_project_hierarchy( - context, context.project_id, subtree_as_ids=True) + context, context.project_id, subtree_as_ids=True, + is_admin_project=context.is_admin) self._authorize_update_or_delete(context_project, target_project.id, parent_id) diff --git a/cinder/quota_utils.py b/cinder/quota_utils.py index 6abfb7f8dcd..82ec29ac6e8 100644 --- a/cinder/quota_utils.py +++ b/cinder/quota_utils.py @@ -38,12 +38,14 @@ class GenericProjectInfo(object): def __init__(self, project_id, project_keystone_api_version, project_parent_id=None, project_subtree=None, - project_parent_tree=None): + project_parent_tree=None, + is_admin_project=False): self.id = project_id self.keystone_api_version = project_keystone_api_version self.parent_id = project_parent_id self.subtree = project_subtree self.parents = project_parent_tree + self.is_admin_project = is_admin_project def get_volume_type_reservation(ctxt, volume, type_id, @@ -90,7 +92,7 @@ def _filter_domain_id_from_parents(domain_id, tree): def get_project_hierarchy(context, project_id, subtree_as_ids=False, - parents_as_ids=False): + parents_as_ids=False, is_admin_project=False): """A Helper method to get the project hierarchy. Along with hierarchical multitenancy in keystone API v3, projects can be @@ -118,6 +120,8 @@ def get_project_hierarchy(context, project_id, subtree_as_ids=False, if parents_as_ids: generic_project.parents = _filter_domain_id_from_parents( project.domain_id, project.parents) + + generic_project.is_admin_project = is_admin_project except exceptions.NotFound: msg = (_("Tenant ID: %s does not exist.") % project_id) raise webob.exc.HTTPNotFound(explanation=msg) diff --git a/cinder/tests/unit/api/contrib/test_quotas.py b/cinder/tests/unit/api/contrib/test_quotas.py index b38c4b60f9b..602938a1e29 100644 --- a/cinder/tests/unit/api/contrib/test_quotas.py +++ b/cinder/tests/unit/api/contrib/test_quotas.py @@ -80,11 +80,13 @@ class QuotaSetsControllerTestBase(test.TestCase): class FakeProject(object): - def __init__(self, id=fake.PROJECT_ID, parent_id=None): + def __init__(self, id=fake.PROJECT_ID, parent_id=None, + is_admin_project=False): self.id = id self.parent_id = parent_id self.subtree = None self.parents = None + self.is_admin_project = is_admin_project def setUp(self): super(QuotaSetsControllerTestBase, self).setUp() @@ -148,7 +150,7 @@ class QuotaSetsControllerTestBase(test.TestCase): self.C.id: self.C, self.D.id: self.D} def _get_project(self, context, id, subtree_as_ids=False, - parents_as_ids=False): + parents_as_ids=False, is_admin_project=False): return self.project_by_id.get(id, self.FakeProject()) def _create_fake_quota_usages(self, usage_map): @@ -616,6 +618,15 @@ class QuotaSetsControllerNestedQuotasTest(QuotaSetsControllerTestBase): expected = make_subproject_body(tenant_id=self.D.id) self.assertDictMatch(expected, result) + def test_subproject_show_not_in_hierarchy_admin_context(self): + E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None, + is_admin_project=True) + self.project_by_id[E.id] = E + self.req.environ['cinder.context'].project_id = E.id + result = self.controller.show(self.req, self.B.id) + expected = make_subproject_body(tenant_id=self.B.id) + self.assertDictMatch(expected, result) + def test_subproject_show_target_project_equals_to_context_project( self): self.req.environ['cinder.context'].project_id = self.B.id @@ -653,6 +664,27 @@ class QuotaSetsControllerNestedQuotasTest(QuotaSetsControllerTestBase): self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, self.req, F.id, body) + def test_update_subproject_not_in_hierarchy_admin_context(self): + E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None, + is_admin_project=True) + self.project_by_id[E.id] = E + self.req.environ['cinder.context'].project_id = E.id + body = make_body(gigabytes=2000, snapshots=15, + volumes=5, backups=5, tenant_id=None) + # Update the project A quota, not in the project hierarchy + # of E but it will be allowed because E is the cloud admin. + result = self.controller.update(self.req, self.A.id, body) + self.assertDictMatch(body, result) + # Update the quota of B to be equal to its parent A. + result = self.controller.update(self.req, self.B.id, body) + self.assertDictMatch(body, result) + # Remove the admin role from project E + E.is_admin_project = False + # Now updating the quota of B will fail, because it is not + # a member of E's hierarchy and E is no longer a cloud admin. + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.update, self.req, self.B.id, body) + def test_update_subproject(self): # Update the project A quota. self.req.environ['cinder.context'].project_id = self.A.id diff --git a/releasenotes/notes/allow-admin-quota-operations-c1c2236711224023.yaml b/releasenotes/notes/allow-admin-quota-operations-c1c2236711224023.yaml new file mode 100644 index 00000000000..0a36494a2c8 --- /dev/null +++ b/releasenotes/notes/allow-admin-quota-operations-c1c2236711224023.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - Projects with the admin role are now allowed to operate + on the quotas of all other projects.