Browse Source

[policy in code] Add support for share instance export location resource

This is the basic patch which consits of the framework
code for default policy in code feature as well as
share instance export location resource.

Partial-Implements: blueprint policy-in-code
Change-Id: Iedde7a4a674a60e760b47d5eb2973f42d79226d8
zhongjun 1 year ago
parent
commit
b21c3d68a4

+ 3
- 0
etc/manila/manila-policy-generator.conf View File

@@ -0,0 +1,3 @@
1
+[DEFAULT]
2
+output_file = etc/manila/policy.yaml.sample
3
+namespace = manila

+ 0
- 8
etc/manila/policy.json View File

@@ -1,10 +1,4 @@
1 1
 {
2
-    "context_is_admin": "role:admin",
3
-    "admin_or_owner": "is_admin:True or project_id:%(project_id)s",
4
-    "default": "rule:admin_or_owner",
5
-
6
-    "admin_api": "is_admin:True",
7
-
8 2
     "availability_zone:index": "rule:default",
9 3
 
10 4
     "quota_set:update": "rule:admin_api",
@@ -50,8 +44,6 @@
50 44
     "share_instance:show": "rule:admin_api",
51 45
     "share_instance:force_delete": "rule:admin_api",
52 46
     "share_instance:reset_status": "rule:admin_api",
53
-    "share_instance_export_location:index": "rule:admin_api",
54
-    "share_instance_export_location:show": "rule:admin_api",
55 47
 
56 48
     "share:create_snapshot": "rule:default",
57 49
     "share:delete_snapshot": "rule:default",

+ 6
- 1
manila/context.py View File

@@ -72,7 +72,7 @@ class RequestContext(context.RequestContext):
72 72
         self.project_id = self.tenant
73 73
 
74 74
         if self.is_admin is None:
75
-            self.is_admin = policy.check_is_admin(self.roles)
75
+            self.is_admin = policy.check_is_admin(self)
76 76
         elif self.is_admin and 'admin' not in self.roles:
77 77
             self.roles.append('admin')
78 78
         self.read_deleted = read_deleted
@@ -135,6 +135,11 @@ class RequestContext(context.RequestContext):
135 135
 
136 136
         return ctx
137 137
 
138
+    def to_policy_values(self):
139
+        policy = super(RequestContext, self).to_policy_values()
140
+        policy['is_admin'] = self.is_admin
141
+        return policy
142
+
138 143
 
139 144
 def get_admin_context(read_deleted="no"):
140 145
     return RequestContext(user_id=None,

+ 27
- 0
manila/policies/__init__.py View File

@@ -0,0 +1,27 @@
1
+# Copyright (c) 2017 Huawei Technologies Co., Ltd.
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+
17
+import itertools
18
+
19
+from manila.policies import base
20
+from manila.policies import share_instance_export_location
21
+
22
+
23
+def list_rules():
24
+    return itertools.chain(
25
+        base.list_rules(),
26
+        share_instance_export_location.list_rules(),
27
+    )

+ 32
- 0
manila/policies/base.py View File

@@ -0,0 +1,32 @@
1
+# Copyright (c) 2017 Huawei Technologies Co., Ltd.
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+from oslo_policy import policy
17
+
18
+RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner'
19
+RULE_ADMIN_API = 'rule:admin_api'
20
+
21
+rules = [
22
+    policy.RuleDefault(name='context_is_admin', check_str='role:admin'),
23
+    policy.RuleDefault(
24
+        name='admin_or_owner',
25
+        check_str='is_admin:True or project_id:%(project_id)s'),
26
+    policy.RuleDefault(name='default', check_str=RULE_ADMIN_OR_OWNER),
27
+    policy.RuleDefault(name='admin_api', check_str='is_admin:True'),
28
+]
29
+
30
+
31
+def list_rules():
32
+    return rules

+ 51
- 0
manila/policies/share_instance_export_location.py View File

@@ -0,0 +1,51 @@
1
+# Copyright (c) 2017 Huawei Technologies Co., Ltd.
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+from oslo_policy import policy
17
+
18
+from manila.policies import base
19
+
20
+
21
+BASE_POLICY_NAME = 'share_instance_export_location:%s'
22
+
23
+
24
+share_export_location_policies = [
25
+    policy.DocumentedRuleDefault(
26
+        name=BASE_POLICY_NAME % 'index',
27
+        check_str=base.RULE_ADMIN_API,
28
+        description='Return data about the requested export location.',
29
+        operations=[
30
+            {
31
+                'method': 'POST',
32
+                'path': ('/share_instances/{share_instance_id}/'
33
+                         'export_locations'),
34
+            }
35
+        ]),
36
+    policy.DocumentedRuleDefault(
37
+        name=BASE_POLICY_NAME % 'show',
38
+        check_str=base.RULE_ADMIN_API,
39
+        description='Return data about the requested export location.',
40
+        operations=[
41
+            {
42
+                'method': 'GET',
43
+                'path': ('/share_instances/{share_instance_id}/'
44
+                         'export_locations/{export_location_id}'),
45
+            }
46
+        ]),
47
+]
48
+
49
+
50
+def list_rules():
51
+    return share_export_location_policies

+ 119
- 18
manila/policy.py View File

@@ -16,13 +16,18 @@
16 16
 """Policy Engine For Manila"""
17 17
 
18 18
 import functools
19
+import sys
19 20
 
20 21
 from oslo_config import cfg
22
+from oslo_log import log as logging
21 23
 from oslo_policy import policy
24
+from oslo_utils import excutils
22 25
 
23 26
 from manila import exception
27
+from manila import policies
24 28
 
25 29
 CONF = cfg.CONF
30
+LOG = logging.getLogger(__name__)
26 31
 _ENFORCER = None
27 32
 
28 33
 
@@ -33,13 +38,24 @@ def reset():
33 38
         _ENFORCER = None
34 39
 
35 40
 
36
-def init(policy_path=None):
41
+def init(rules=None, use_conf=True):
42
+    """Init an Enforcer class.
43
+
44
+        :param policy_file: Custom policy file to use, if none is specified,
45
+                          `CONF.policy_file` will be used.
46
+        :param rules: Default dictionary / Rules to use. It will be
47
+                    considered just in the first instantiation.
48
+        :param default_rule: Default rule to use, CONF.default_rule will
49
+                           be used if none is specified.
50
+        :param use_conf: Whether to load rules from config file.
51
+    """
52
+
37 53
     global _ENFORCER
38 54
     if not _ENFORCER:
39
-        _ENFORCER = policy.Enforcer(CONF)
40
-        if policy_path:
41
-            _ENFORCER.policy_path = policy_path
42
-    _ENFORCER.load_rules()
55
+        _ENFORCER = policy.Enforcer(CONF,
56
+                                    rules=rules,
57
+                                    use_conf=use_conf)
58
+        register_rules(_ENFORCER)
43 59
 
44 60
 
45 61
 def enforce(context, action, target, do_raise=True):
@@ -48,9 +64,7 @@ def enforce(context, action, target, do_raise=True):
48 64
        :param context: manila context
49 65
        :param action: string representing the action to be checked,
50 66
            this should be colon separated for clarity.
51
-           i.e. ``compute:create_instance``,
52
-           ``compute:attach_volume``,
53
-           ``volume:attach_volume``
67
+           i.e. ``share:create``,
54 68
        :param target: dictionary representing the object of the action
55 69
            for object creation, this should be a dictionary representing the
56 70
            location of the object e.g. ``{'project_id': context.project_id}``
@@ -76,19 +90,101 @@ def enforce(context, action, target, do_raise=True):
76 90
     return _ENFORCER.enforce(action, target, context, **extra)
77 91
 
78 92
 
79
-def check_is_admin(roles):
80
-    """Whether or not roles contain 'admin' role according to policy setting.
93
+def set_rules(rules, overwrite=True, use_conf=False):
94
+    """Set rules based on the provided dict of rules.
95
+
96
+       :param rules: New rules to use. It should be an instance of dict.
97
+       :param overwrite: Whether to overwrite current rules or update them
98
+                         with the new rules.
99
+       :param use_conf: Whether to reload rules from config file.
100
+    """
101
+
102
+    init(use_conf=False)
103
+    _ENFORCER.set_rules(rules, overwrite, use_conf)
104
+
105
+
106
+def get_rules():
107
+    if _ENFORCER:
108
+        return _ENFORCER.rules
109
+
110
+
111
+def register_rules(enforcer):
112
+    enforcer.register_defaults(policies.list_rules())
113
+
114
+
115
+def get_enforcer():
116
+    # This method is for use by oslopolicy CLI scripts. Those scripts need the
117
+    # 'output-file' and 'namespace' options, but having those in sys.argv means
118
+    # loading the Manila config options will fail as those are not expected to
119
+    # be present. So we pass in an arg list with those stripped out.
120
+    conf_args = []
121
+    # Start at 1 because cfg.CONF expects the equivalent of sys.argv[1:]
122
+    i = 1
123
+    while i < len(sys.argv):
124
+        if sys.argv[i].strip('-') in ['namespace', 'output-file']:
125
+            i += 2
126
+            continue
127
+        conf_args.append(sys.argv[i])
128
+        i += 1
129
+
130
+    cfg.CONF(conf_args, project='manila')
131
+    init()
132
+    return _ENFORCER
133
+
134
+
135
+def authorize(context, action, target, do_raise=True, exc=None):
136
+    """Verifies that the action is valid on the target in this context.
137
+
138
+       :param context: manila context
139
+       :param action: string representing the action to be checked
140
+           this should be colon separated for clarity.
141
+           i.e. ``share:create``,
142
+       :param target: dictionary representing the object of the action
143
+           for object creation this should be a dictionary representing the
144
+           location of the object e.g. ``{'project_id': context.project_id}``
145
+       :param do_raise: if True (the default), raises PolicyNotAuthorized;
146
+           if False, returns False
147
+       :param exc: Class of the exception to raise if the check fails.
148
+                   Any remaining arguments passed to :meth:`authorize` (both
149
+                   positional and keyword arguments) will be passed to
150
+                   the exception class. If not specified,
151
+                   :class:`PolicyNotAuthorized` will be used.
152
+
153
+       :raises manila.exception.PolicyNotAuthorized: if verification fails
154
+           and do_raise is True. Or if 'exc' is specified it will raise an
155
+           exception of that type.
156
+
157
+       :return: returns a non-False value (not necessarily "True") if
158
+           authorized, and the exact value False if not authorized and
159
+           do_raise is False.
160
+    """
161
+    init()
162
+    credentials = context.to_policy_values()
163
+    if not exc:
164
+        exc = exception.PolicyNotAuthorized
165
+    try:
166
+        result = _ENFORCER.authorize(action, target, credentials,
167
+                                     do_raise=do_raise, exc=exc, action=action)
168
+    except policy.PolicyNotRegistered:
169
+        with excutils.save_and_reraise_exception():
170
+            LOG.exception('Policy not registered')
171
+    except Exception:
172
+        with excutils.save_and_reraise_exception():
173
+            LOG.debug('Policy check for %(action)s failed with credentials '
174
+                      '%(credentials)s',
175
+                      {'action': action, 'credentials': credentials})
176
+    return result
177
+
178
+
179
+def check_is_admin(context):
180
+    """Whether or not user is admin according to policy setting.
81 181
 
82 182
     """
83 183
     init()
84 184
 
85
-    # include project_id on target to avoid KeyError if context_is_admin
86
-    # policy definition is missing, and default admin_or_owner rule
87
-    # attempts to apply.  Since our credentials dict does not include a
88
-    # project_id, this target can never match as a generic rule.
89
-    target = {'project_id': ''}
90
-    credentials = {'roles': roles}
91
-    return _ENFORCER.enforce("context_is_admin", target, credentials)
185
+    credentials = context.to_policy_values()
186
+    target = credentials
187
+    return _ENFORCER.authorize('context_is_admin', target, credentials)
92 188
 
93 189
 
94 190
 def wrap_check_policy(resource):
@@ -110,4 +206,9 @@ def check_policy(context, resource, action, target_obj=None):
110 206
     }
111 207
     target.update(target_obj or {})
112 208
     _action = '%s:%s' % (resource, action)
113
-    enforce(context, _action, target)
209
+    # The else branch will be deleted after all policy in code patches
210
+    # be merged.
211
+    if resource in ('share_instance_export_location', ):
212
+        authorize(context, _action, target)
213
+    else:
214
+        enforce(context, _action, target)

+ 0
- 2
manila/tests/policy.json View File

@@ -59,8 +59,6 @@
59 59
     "share_instance:show": "rule:admin_api",
60 60
     "share_instance:force_delete": "rule:admin_api",
61 61
     "share_instance:reset_status": "rule:admin_api",
62
-    "share_instance_export_location:index": "rule:admin_api",
63
-    "share_instance_export_location:show": "rule:admin_api",
64 62
 
65 63
     "share_snapshot:force_delete": "rule:admin_api",
66 64
     "share_snapshot:reset_status": "rule:admin_api",

+ 47
- 82
manila/tests/test_policy.py View File

@@ -15,8 +15,6 @@
15 15
 
16 16
 """Test of Policy Engine For Manila."""
17 17
 
18
-import os.path
19
-
20 18
 from oslo_config import cfg
21 19
 from oslo_policy import policy as common_policy
22 20
 
@@ -24,113 +22,80 @@ from manila import context
24 22
 from manila import exception
25 23
 from manila import policy
26 24
 from manila import test
27
-from manila import utils
28 25
 
29 26
 CONF = cfg.CONF
30 27
 
31 28
 
32
-class PolicyFileTestCase(test.TestCase):
33
-
34
-    def setUp(self):
35
-        super(PolicyFileTestCase, self).setUp()
36
-        # since is_admin is defined by policy, create context before reset
37
-        self.context = context.RequestContext('fake', 'fake')
38
-        policy.reset()
39
-        self.target = {}
40
-
41
-    def test_modified_policy_reloads(self):
42
-        with utils.tempdir() as tmpdir:
43
-            tmpfilename = os.path.join(tmpdir, 'policy')
44
-            CONF.set_override('policy_file', tmpfilename, group='oslo_policy')
45
-            action = "example:test"
46
-            with open(tmpfilename, "w") as policyfile:
47
-                policyfile.write("""{"example:test": []}""")
48
-            policy.init(tmpfilename)
49
-            policy.enforce(self.context, action, self.target)
50
-            with open(tmpfilename, "w") as policyfile:
51
-                policyfile.write("""{"example:test": ["false:false"]}""")
52
-            # NOTE(vish): reset stored policy cache so we don't have to
53
-            # sleep(1)
54
-            policy._ENFORCER.load_rules(True)
55
-            self.assertRaises(
56
-                exception.PolicyNotAuthorized,
57
-                policy.enforce,
58
-                self.context,
59
-                action,
60
-                self.target,
61
-            )
62
-
63
-
64 29
 class PolicyTestCase(test.TestCase):
65 30
     def setUp(self):
66 31
         super(PolicyTestCase, self).setUp()
32
+        rules = [
33
+            common_policy.RuleDefault("true", '@'),
34
+            common_policy.RuleDefault("test:allowed", '@'),
35
+            common_policy.RuleDefault("test:denied", "!"),
36
+            common_policy.RuleDefault("test:my_file",
37
+                                      "role:compute_admin or "
38
+                                      "project_id:%(project_id)s"),
39
+            common_policy.RuleDefault("test:early_and_fail", "! and @"),
40
+            common_policy.RuleDefault("test:early_or_success", "@ or !"),
41
+            common_policy.RuleDefault("test:lowercase_admin",
42
+                                      "role:admin"),
43
+            common_policy.RuleDefault("test:uppercase_admin",
44
+                                      "role:ADMIN"),
45
+        ]
67 46
         policy.reset()
68 47
         policy.init()
69
-        self.rules = {
70
-            "true": [],
71
-            "example:allowed": [],
72
-            "example:denied": [["false:false"]],
73
-            "example:get_http": [["http:http://www.example.com"]],
74
-            "example:my_file": [["role:compute_admin"],
75
-                                ["project_id:%(project_id)s"]],
76
-            "example:early_and_fail": [["false:false", "rule:true"]],
77
-            "example:early_or_success": [["rule:true"], ["false:false"]],
78
-            "example:lowercase_admin": [["role:admin"], ["role:sysadmin"]],
79
-            "example:uppercase_admin": [["role:ADMIN"], ["role:sysadmin"]],
80
-        }
81
-        self._set_rules()
48
+        # before a policy rule can be used, its default has to be registered.
49
+        policy._ENFORCER.register_defaults(rules)
82 50
         self.context = context.RequestContext('fake', 'fake', roles=['member'])
83 51
         self.target = {}
52
+        self.addCleanup(policy.reset)
84 53
 
85
-    def tearDown(self):
86
-        policy.reset()
87
-        super(PolicyTestCase, self).tearDown()
88
-
89
-    def _set_rules(self):
90
-        these_rules = common_policy.Rules.from_dict(self.rules)
91
-        policy._ENFORCER.set_rules(these_rules)
92
-
93
-    def test_enforce_nonexistent_action_throws(self):
94
-        action = "example:noexist"
95
-        self.assertRaises(exception.PolicyNotAuthorized, policy.enforce,
54
+    def test_authorize_nonexistent_action_throws(self):
55
+        action = "test:noexist"
56
+        self.assertRaises(common_policy.PolicyNotRegistered, policy.authorize,
96 57
                           self.context, action, self.target)
97 58
 
98
-    def test_enforce_bad_action_throws(self):
99
-        action = "example:denied"
100
-        self.assertRaises(exception.PolicyNotAuthorized, policy.enforce,
59
+    def test_authorize_bad_action_throws(self):
60
+        action = "test:denied"
61
+        self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
101 62
                           self.context, action, self.target)
102 63
 
103
-    def test_enforce_good_action(self):
104
-        action = "example:allowed"
105
-        policy.enforce(self.context, action, self.target)
64
+    def test_authorize_bad_action_noraise(self):
65
+        action = "test:denied"
66
+        result = policy.authorize(self.context, action, self.target, False)
67
+        self.assertFalse(result)
106 68
 
107
-    def test_templatized_enforcement(self):
69
+    def test_authorize_good_action(self):
70
+        action = "test:allowed"
71
+        result = policy.authorize(self.context, action, self.target)
72
+        self.assertTrue(result)
73
+
74
+    def test_templatized_authorization(self):
108 75
         target_mine = {'project_id': 'fake'}
109 76
         target_not_mine = {'project_id': 'another'}
110
-        action = "example:my_file"
111
-        policy.enforce(self.context, action, target_mine)
112
-        self.assertRaises(exception.PolicyNotAuthorized, policy.enforce,
77
+        action = "test:my_file"
78
+        policy.authorize(self.context, action, target_mine)
79
+        self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
113 80
                           self.context, action, target_not_mine)
114 81
 
115
-    def test_early_AND_enforcement(self):
116
-        action = "example:early_and_fail"
117
-        self.assertRaises(exception.PolicyNotAuthorized, policy.enforce,
82
+    def test_early_AND_authorization(self):
83
+        action = "test:early_and_fail"
84
+        self.assertRaises(exception.PolicyNotAuthorized, policy.authorize,
118 85
                           self.context, action, self.target)
119 86
 
120
-    def test_early_OR_enforcement(self):
121
-        action = "example:early_or_success"
122
-        policy.enforce(self.context, action, self.target)
87
+    def test_early_OR_authorization(self):
88
+        action = "test:early_or_success"
89
+        policy.authorize(self.context, action, self.target)
123 90
 
124 91
     def test_ignore_case_role_check(self):
125
-        lowercase_action = "example:lowercase_admin"
126
-        uppercase_action = "example:uppercase_admin"
127
-        # NOTE(dprince) we mix case in the Admin role here to ensure
128
-        # case is ignored
92
+        lowercase_action = "test:lowercase_admin"
93
+        uppercase_action = "test:uppercase_admin"
129 94
         admin_context = context.RequestContext('admin',
130 95
                                                'fake',
131 96
                                                roles=['AdMiN'])
132
-        policy.enforce(admin_context, lowercase_action, self.target)
133
-        policy.enforce(admin_context, uppercase_action, self.target)
97
+        policy.authorize(admin_context, lowercase_action, self.target)
98
+        policy.authorize(admin_context, uppercase_action, self.target)
134 99
 
135 100
 
136 101
 class DefaultPolicyTestCase(test.TestCase):
@@ -214,6 +179,6 @@ class ContextIsAdminPolicyTestCase(test.TestCase):
214 179
         }
215 180
         self._set_rules(rules, CONF.oslo_policy.policy_default_rule)
216 181
         ctx = context.RequestContext('fake', 'fake')
217
-        self.assertFalse(ctx.is_admin)
182
+        self.assertTrue(ctx.is_admin)
218 183
         ctx = context.RequestContext('fake', 'fake', roles=['admin'])
219 184
         self.assertTrue(ctx.is_admin)

+ 8
- 0
setup.cfg View File

@@ -71,6 +71,14 @@ oslo.config.opts =
71 71
     manila = manila.opts:list_opts
72 72
 oslo.config.opts.defaults =
73 73
     manila = manila.common.config:set_middleware_defaults
74
+oslo.policy.enforcer =
75
+    manila = manila.policy:get_enforcer
76
+oslo.policy.policies =
77
+    # The sample policies will be ordered by entry point and then by list
78
+    # returned from that entry point. If more control is desired split out each
79
+    # list_rules method into a separate entry point rather than using the
80
+    # aggregate method.
81
+    manila = manila.policies:list_rules
74 82
 manila.share.drivers.dell_emc.plugins =
75 83
     vnx = manila.share.drivers.dell_emc.plugins.vnx.connection:VNXStorageConnection
76 84
     unity = manila.share.drivers.dell_emc.plugins.unity.connection:UnityStorageConnection

+ 3
- 0
tox.ini View File

@@ -58,6 +58,9 @@ whitelist_externals = bash
58 58
 commands =
59 59
   oslo-config-generator --config-file etc/oslo-config-generator/manila.conf
60 60
 
61
+[testenv:genpolicy]
62
+commands = oslopolicy-sample-generator --config-file=etc/manila/manila-policy-generator.conf
63
+
61 64
 [testenv:venv]
62 65
 commands = {posargs}
63 66
 

Loading…
Cancel
Save