Allow admin project to operate on all quotas

Currently when operating on quotas in a nested
hierarchical environment, the calling project context
must be a member of the same hierarchy as the target
project in order to be authorized, even if it has the
admin role. This is a change in behavior for users who
are used to being able to view and modify any project
quotas as admin without needing a token scoped to the
target project's hierarchy.

This patch allows projects with the cloud admin role to
be able to operate on all project quotas regardless of
its place in project hierarchy.

Change-Id: Ifc0f1c5d06e7ca3aa7d9cf267a40f2a6b5cfc078
Closes-bug: #1597045
This commit is contained in:
Nate Potter 2016-06-29 19:59:46 +00:00
parent 55cb9e8747
commit a0a04f4332
4 changed files with 60 additions and 9 deletions

View File

@ -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)

View File

@ -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)

View File

@ -81,11 +81,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()
@ -149,7 +151,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):
@ -606,6 +608,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
@ -643,6 +654,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

View File

@ -0,0 +1,4 @@
---
fixes:
- Projects with the admin role are now allowed to operate
on the quotas of all other projects.