From c04db3a6ff8ef39ba9af6a1026ab49a539bdf5e3 Mon Sep 17 00:00:00 2001
From: Nate Potter <nathaniel.potter@intel.com>
Date: Mon, 13 Mar 2017 17:40:54 +0100
Subject: [PATCH] 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.

Closes-bug: #1597045
Change-Id: Ifc0f1c5d06e7ca3aa7d9cf267a40f2a6b5cfc078
(cherry picked from commit a0a04f4332a609e854f2e67e3e9e9b723197b584)
---
 cinder/api/contrib/quotas.py                       | 14 ++++++++++++--
 cinder/tests/unit/api/contrib/test_quotas.py       | 22 +++++++++++++++++-----
 ...ow-admin-quota-operations-c1c2236711224023.yaml |  4 ++++
 3 files changed, 33 insertions(+), 7 deletions(-)
 create mode 100644 releasenotes/notes/allow-admin-quota-operations-c1c2236711224023.yaml

diff --git a/cinder/api/contrib/quotas.py b/cinder/api/contrib/quotas.py
index 0749507..e426459 100644
--- a/cinder/api/contrib/quotas.py
+++ b/cinder/api/contrib/quotas.py
@@ -62,11 +62,12 @@ class QuotaSetsController(wsgi.Controller):
         """Abstraction layer for Keystone V2 and V3 project objects"""
 
         def __init__(self, project_id, project_keystone_api_version,
-                     project_parent_id=None, project_subtree=None):
+                     project_parent_id=None, project_subtree=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.is_admin_project = is_admin_project
 
     def _format_quota_set(self, project_id, quota_set):
         """Convert the quota object to a result dict."""
@@ -147,6 +148,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.")
@@ -176,6 +181,10 @@ class QuotaSetsController(wsgi.Controller):
         :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 operate on all quotas.
+            return
         if target_project.parent_id:
             if target_project.id != context_project.id:
                 if not self._is_descendant(target_project.id,
@@ -213,7 +222,8 @@ class QuotaSetsController(wsgi.Controller):
         """
         try:
             keystone = self._keystone_client(context)
-            generic_project = self.GenericProjectInfo(id, keystone.version)
+            generic_project = self.GenericProjectInfo(id, keystone.version,
+                                                      is_admin_project=context.is_admin)
             if keystone.version == 'v3':
                 project = keystone.projects.get(id,
                                                 subtree_as_ids=subtree_as_ids)
diff --git a/cinder/tests/unit/api/contrib/test_quotas.py b/cinder/tests/unit/api/contrib/test_quotas.py
index c1b6656..8a60a9d 100644
--- a/cinder/tests/unit/api/contrib/test_quotas.py
+++ b/cinder/tests/unit/api/contrib/test_quotas.py
@@ -78,10 +78,11 @@ class QuotaSetsControllerTest(test.TestCase):
 
     class FakeProject(object):
 
-        def __init__(self, id='foo', parent_id=None):
+        def __init__(self, id='foo', parent_id=None, is_admin_project=False):
             self.id = id
             self.parent_id = parent_id
             self.subtree = None
+            self.is_admin_project = is_admin_project
 
     def setUp(self):
         super(QuotaSetsControllerTest, self).setUp()
@@ -125,7 +126,7 @@ class QuotaSetsControllerTest(test.TestCase):
         self.project_by_id = {self.A.id: self.A, self.B.id: self.B,
                               self.C.id: self.C, self.D.id: self.D}
 
-    def _get_project(self, context, id, subtree_as_ids=False):
+    def _get_project(self, context, id, subtree_as_ids=False, is_admin_project=False):
         return self.project_by_id.get(id, self.FakeProject())
 
     @mock.patch('keystoneclient.client.Client')
@@ -143,7 +144,7 @@ class QuotaSetsControllerTest(test.TestCase):
         keystoneclient = ksclient_class.return_value
         keystoneclient.version = 'v2.0'
         expected_project = self.controller.GenericProjectInfo(
-            context.project_id, 'v2.0')
+            context.project_id, 'v2.0', is_admin_project=True)
         project = self.controller._get_project(context, context.project_id)
         self.assertEqual(expected_project.__dict__, project.__dict__)
 
@@ -156,7 +157,7 @@ class QuotaSetsControllerTest(test.TestCase):
         del returned_project.subtree
         keystoneclient.projects.get.return_value = returned_project
         expected_project = self.controller.GenericProjectInfo(
-            context.project_id, 'v3', 'bar')
+            context.project_id, 'v3', 'bar', is_admin_project=True)
         project = self.controller._get_project(context, context.project_id)
         self.assertEqual(expected_project.__dict__, project.__dict__)
 
@@ -170,7 +171,7 @@ class QuotaSetsControllerTest(test.TestCase):
         returned_project.subtree = subtree_dict
         keystoneclient.projects.get.return_value = returned_project
         expected_project = self.controller.GenericProjectInfo(
-            context.project_id, 'v3', 'bar', subtree_dict)
+            context.project_id, 'v3', 'bar', subtree_dict, True)
         project = self.controller._get_project(context, context.project_id,
                                                subtree_as_ids=True)
         keystoneclient.projects.get.assert_called_once_with(
@@ -222,6 +223,17 @@ class QuotaSetsControllerTest(test.TestCase):
         expected = make_subproject_body(tenant_id=self.D.id)
         self.assertDictMatch(result, expected)
 
+    def test_subproject_show_not_in_hierarchy_admin_context(self):
+        self.controller._get_project = mock.Mock()
+        self.controller._get_project.side_effect = self._get_project
+        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.controller._get_project = mock.Mock()
         self.controller._get_project.side_effect = self._get_project
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 0000000..0a36494
--- /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.
-- 
2.6.4

