From 855396299771a290f2de9918a91ced4925d98238 Mon Sep 17 00:00:00 2001
From: Goutham Pacha Ravi <gouthampravi@gmail.com>
Date: Thu, 25 Feb 2021 23:41:57 -0800
Subject: [PATCH] Clean up some policy code

oslo policy handles the mapping of
credentials from a context object to values
that oslo policy cares about. This mapping
includes some deprecations and compatibility
handling code that we must take advantage of [1].
So, stop mapping context to policy values
on our end, and rely on oslo.policy handling
this for us.

enforce and authorize methods in policy.py
do the same thing, but with a subtle
difference. The "enforce" method doesn't
raise errors when unregistered policies are
invoked - this shouldn't ever be the case
for any policies written/maintained within
manila - however, we support API extensions
and don't dictate what must be done there. So
add docstrings to clarify that we shouldn't
invoke enforce, ever.

Also handle InvalidPolicyScope exceptions
and raise the oslo.policy library version
since some test enhancements have been
committed in the latest version.

[1] https://opendev.org/openstack/oslo.policy/src/commit/d3185debdbb7aa254ad4ce2acc38c9a34908e44a/oslo_policy/policy.py#L1077-L1096

Change-Id: I069bf7143d6ff66b3dcdc34c9b52d48f5808481b
Signed-off-by: Goutham Pacha Ravi <gouthampravi@gmail.com>
---
 lower-constraints.txt       |  2 +-
 manila/api/extensions.py    |  9 +++----
 manila/policy.py            | 50 +++++++++++++++++++++++--------------
 manila/tests/test_policy.py | 35 ++++++++++++++++++++++++++
 requirements.txt            |  2 +-
 5 files changed, 71 insertions(+), 27 deletions(-)

diff --git a/lower-constraints.txt b/lower-constraints.txt
index 57f22de724..7486b72fbd 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -64,7 +64,7 @@ oslo.i18n==5.0.1
 oslo.log==4.4.0
 oslo.messaging==12.5.0
 oslo.middleware==4.1.1
-oslo.policy==3.6.0
+oslo.policy==3.6.2
 oslo.reports==2.2.0
 oslo.rootwrap==6.2.0
 oslo.serialization==4.0.1
diff --git a/manila/api/extensions.py b/manila/api/extensions.py
index bea35bec94..6bbdca258e 100644
--- a/manila/api/extensions.py
+++ b/manila/api/extensions.py
@@ -22,10 +22,9 @@ from oslo_utils import importutils
 import webob.dec
 import webob.exc
 
-import manila.api.openstack
 from manila.api.openstack import wsgi
 from manila import exception
-import manila.policy
+from manila import policy
 
 CONF = cfg.CONF
 LOG = log.getLogger(__name__)
@@ -332,12 +331,10 @@ def load_standard_extensions(ext_mgr, logger, path, package, ext_list=None):
 
 def extension_authorizer(api_name, extension_name):
     def authorize(context, target=None, action=None):
-        if target is None:
-            target = {'project_id': context.project_id,
-                      'user_id': context.user_id}
+        target = target or policy.default_target(context)
         if action is None:
             act = '%s_extension:%s' % (api_name, extension_name)
         else:
             act = '%s_extension:%s:%s' % (api_name, extension_name, action)
-        manila.policy.enforce(context, act, target)
+        policy.enforce(context, act, target)
     return authorize
diff --git a/manila/policy.py b/manila/policy.py
index 05ac7be479..f0501999a0 100644
--- a/manila/policy.py
+++ b/manila/policy.py
@@ -69,6 +69,10 @@ def init(rules=None, use_conf=True):
 def enforce(context, action, target, do_raise=True):
     """Verifies that the action is valid on the target in this context.
 
+       **IMPORTANT** ONLY for use in API extensions. This method ignores
+       unregistered rules and applies a default rule on them; there should
+       be no unregistered rules in first party manila APIs.
+
        :param context: manila context
        :param action: string representing the action to be checked,
            this should be colon separated for clarity.
@@ -88,12 +92,15 @@ def enforce(context, action, target, do_raise=True):
     """
     init()
 
-    return _ENFORCER.enforce(action,
-                             target,
-                             context.to_policy_values(),
-                             do_raise=do_raise,
-                             exc=exception.PolicyNotAuthorized,
-                             action=action)
+    try:
+        return _ENFORCER.enforce(action,
+                                 target,
+                                 context,
+                                 do_raise=do_raise,
+                                 exc=exception.PolicyNotAuthorized,
+                                 action=action)
+    except policy.InvalidScope:
+        raise exception.PolicyNotAuthorized(action=action)
 
 
 def set_rules(rules, overwrite=True, use_conf=False):
@@ -165,32 +172,41 @@ def authorize(context, action, target, do_raise=True, exc=None):
            do_raise is False.
     """
     init()
-    credentials = context.to_policy_values()
     if not exc:
         exc = exception.PolicyNotAuthorized
+    target = target or default_target(context)
+
     try:
-        result = _ENFORCER.authorize(action, target, credentials,
+        result = _ENFORCER.authorize(action, target, context,
                                      do_raise=do_raise, exc=exc, action=action)
     except policy.PolicyNotRegistered:
         with excutils.save_and_reraise_exception():
             LOG.exception('Policy not registered')
+    except policy.InvalidScope:
+        raise exception.PolicyNotAuthorized(action=action)
     except Exception:
         with excutils.save_and_reraise_exception():
+            msg_args = {
+                'action': action,
+                'credentials': context.to_policy_values(),
+            }
             LOG.debug('Policy check for %(action)s failed with credentials '
-                      '%(credentials)s',
-                      {'action': action, 'credentials': credentials})
+                      '%(credentials)s', msg_args)
     return result
 
 
+def default_target(context):
+    return {'project_id': context.project_id, 'user_id': context.user_id}
+
+
 def check_is_admin(context):
     """Whether or not user is admin according to policy setting.
 
     """
     init()
-
-    credentials = context.to_policy_values()
-    target = credentials
-    return _ENFORCER.authorize('context_is_admin', target, credentials)
+    # the target is user-self
+    target = default_target(context)
+    return _ENFORCER.authorize('context_is_admin', target, context)
 
 
 def wrap_check_policy(resource):
@@ -206,10 +222,6 @@ def wrap_check_policy(resource):
 
 
 def check_policy(context, resource, action, target_obj=None, do_raise=True):
-    target = {
-        'project_id': context.project_id,
-        'user_id': context.user_id,
-    }
-    target.update(target_obj or {})
+    target = target_obj or default_target(context)
     _action = '%s:%s' % (resource, action)
     return authorize(context, _action, target, do_raise=do_raise)
diff --git a/manila/tests/test_policy.py b/manila/tests/test_policy.py
index a0d8bb4569..cab315f41d 100644
--- a/manila/tests/test_policy.py
+++ b/manila/tests/test_policy.py
@@ -15,6 +15,7 @@
 
 """Test of Policy Engine For Manila."""
 
+import ddt
 from oslo_config import cfg
 from oslo_policy import policy as common_policy
 
@@ -26,6 +27,7 @@ from manila import test
 CONF = cfg.CONF
 
 
+@ddt.ddt
 class PolicyTestCase(test.TestCase):
     def setUp(self):
         super(PolicyTestCase, self).setUp()
@@ -97,8 +99,41 @@ class PolicyTestCase(test.TestCase):
         policy.authorize(admin_context, lowercase_action, self.target)
         policy.authorize(admin_context, uppercase_action, self.target)
 
+    @ddt.data('enforce', 'authorize')
+    def test_authorize_properly_handles_invalid_scope_exception(self, method):
+        self.fixture.config(enforce_scope=True, group='oslo_policy')
+        project_context = context.RequestContext(project_id='fake-project-id',
+                                                 roles=['bar'])
+        policy.reset()
+        policy.init()
+        rule = common_policy.RuleDefault('foo', 'role:bar',
+                                         scope_types=['system'])
+        policy._ENFORCER.register_defaults([rule])
+
+        self.assertRaises(exception.PolicyNotAuthorized,
+                          getattr(policy, method),
+                          project_context, 'foo', {})
+
+    @ddt.data('enforce', 'authorize')
+    def test_authorize_does_not_raise_forbidden(self, method):
+        self.fixture.config(enforce_scope=False, group='oslo_policy')
+        project_context = context.RequestContext(project_id='fake-project-id',
+                                                 roles=['bar'])
+        policy.reset()
+        policy.init()
+        rule = common_policy.RuleDefault('foo', 'role:bar',
+                                         scope_types=['system'])
+        policy._ENFORCER.register_defaults([rule])
+
+        self.assertTrue(getattr(policy, method)(project_context, 'foo', {}))
+
 
 class DefaultPolicyTestCase(test.TestCase):
+    """This test case calls into the "enforce" method in policy
+
+    enforce() in contrast with authorize() allows "default" rules to apply
+    to policies that have not been registered.
+    """
 
     def setUp(self):
         super(DefaultPolicyTestCase, self).setUp()
diff --git a/requirements.txt b/requirements.txt
index b9b71ca5e1..638607c8bc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,7 +17,7 @@ oslo.i18n>=5.0.1 # Apache-2.0
 oslo.log>=4.4.0 # Apache-2.0
 oslo.messaging>=12.5.0 # Apache-2.0
 oslo.middleware>=4.1.1 # Apache-2.0
-oslo.policy>=3.6.0 # Apache-2.0
+oslo.policy>=3.6.2 # Apache-2.0
 oslo.reports>=2.2.0 # Apache-2.0
 oslo.rootwrap>=6.2.0 # Apache-2.0
 oslo.serialization>=4.0.1 # Apache-2.0