Browse Source

Check policy when handling a HTTP request

* Add default policy for handling the create request.
* Allow it to be accessed only by nova service.
* Remove unused code copied from cinder.

Change-Id: Ieaa407f27c6774d1fd17850a9571de5554360bae
Grzegorz Grasza 3 months ago
parent
commit
462305315c

+ 1
- 0
.gitignore View File

@@ -1,5 +1,6 @@
1 1
 build
2 2
 files/join.conf
3
+files/policy.yaml.sample
3 4
 tools/lintstack.head.py
4 5
 *.pyc
5 6
 *.egg-info/

+ 3
- 0
files/policy-generator.conf View File

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

+ 4
- 98
novajoin/context.py View File

@@ -22,8 +22,6 @@ import copy
22 22
 from oslo_config import cfg
23 23
 from oslo_context import context
24 24
 from oslo_log import log as logging
25
-from oslo_utils import timeutils
26
-import six
27 25
 
28 26
 from novajoin import policy
29 27
 
@@ -39,54 +37,9 @@ class RequestContext(context.RequestContext):
39 37
     Represents the user taking a given action within the system.
40 38
 
41 39
     """
42
-    def __init__(self, user_id, project_id, is_admin=None, read_deleted="no",
43
-                 roles=None, project_name=None, remote_address=None,
44
-                 timestamp=None, request_id=None, auth_token=None,
45
-                 overwrite=True, quota_class=None, service_catalog=None,
46
-                 domain=None, user_domain=None, project_domain=None,
47
-                 **kwargs):
48
-        """Initialize RequestContext.
49
-
50
-        :param read_deleted: 'no' indicates deleted records are hidden, 'yes'
51
-            indicates deleted records are visible, 'only' indicates that
52
-            *only* deleted records are visible.
53
-
54
-        :param overwrite: Set to False to ensure that the greenthread local
55
-            copy of the index is not overwritten.
56
-
57
-        :param kwargs: Extra arguments that might be present, but we ignore
58
-            because they possibly came in from older rpc messages.
59
-        """
60
-
61
-        super(RequestContext, self).__init__(auth_token=auth_token,
62
-                                             user=user_id,
63
-                                             tenant=project_id,
64
-                                             domain=domain,
65
-                                             user_domain=user_domain,
66
-                                             project_domain=project_domain,
67
-                                             is_admin=is_admin,
68
-                                             request_id=request_id,
69
-                                             overwrite=overwrite,
70
-                                             roles=roles)
71
-        self.project_name = project_name
72
-        self.read_deleted = read_deleted
73
-        self.remote_address = remote_address
74
-        if not timestamp:
75
-            timestamp = timeutils.utcnow()
76
-        elif isinstance(timestamp, six.string_types):
77
-            timestamp = timeutils.parse_isotime(timestamp)
78
-        self.timestamp = timestamp
79
-        self.quota_class = quota_class
80
-
81
-        if service_catalog:
82
-            # Only include required parts of service_catalog
83
-            self.service_catalog = [s for s in service_catalog
84
-                                    if s.get('type') in
85
-                                    ('identity', 'compute', 'object-store',
86
-                                     'image')]
87
-        else:
88
-            # if list is empty or none
89
-            self.service_catalog = []
40
+    def __init__(self, *args, **kwargs):
41
+
42
+        super(RequestContext, self).__init__(*args, **kwargs)
90 43
 
91 44
         # We need to have RequestContext attributes defined
92 45
         # when policy.check_is_admin invokes request logging
@@ -96,39 +49,14 @@ class RequestContext(context.RequestContext):
96 49
         elif self.is_admin and 'admin' not in self.roles:
97 50
             self.roles.append('admin')
98 51
 
99
-    def _get_read_deleted(self):
100
-        return self._read_deleted
101
-
102
-    def _set_read_deleted(self, read_deleted):
103
-        if read_deleted not in ('no', 'yes', 'only'):
104
-            raise ValueError("read_deleted can only be one of 'no', "
105
-                             "'yes' or 'only', not %r") % read_deleted
106
-        self._read_deleted = read_deleted
107
-
108
-    def _del_read_deleted(self):
109
-        del self._read_deleted
110
-
111
-    read_deleted = property(_get_read_deleted, _set_read_deleted,
112
-                            _del_read_deleted)
113
-
114 52
     def to_dict(self):
115 53
         result = super(RequestContext, self).to_dict()
116 54
         result['user_id'] = self.user_id
55
+        result['user_name'] = self.user_name
117 56
         result['project_id'] = self.project_id
118 57
         result['project_name'] = self.project_name
119
-        result['domain'] = self.domain
120
-        result['read_deleted'] = self.read_deleted
121
-        result['remote_address'] = self.remote_address
122
-        result['timestamp'] = self.timestamp.isoformat()
123
-        result['quota_class'] = self.quota_class
124
-        result['service_catalog'] = self.service_catalog
125
-        result['request_id'] = self.request_id
126 58
         return result
127 59
 
128
-    @classmethod
129
-    def from_dict(cls, values):
130
-        return cls(**values)
131
-
132 60
     def elevated(self, read_deleted=None, overwrite=False):
133 61
         """Return a version of this context with admin flag set."""
134 62
         context = self.deepcopy()
@@ -144,25 +72,3 @@ class RequestContext(context.RequestContext):
144 72
 
145 73
     def deepcopy(self):
146 74
         return copy.deepcopy(self)
147
-
148
-    # NOTE(sirp): the openstack/common version of RequestContext uses
149
-    # tenant/user whereas the Cinder version uses project_id/user_id.
150
-    # NOTE(adrienverge): The Cinder version of RequestContext now uses
151
-    # tenant/user internally, so it is compatible with context-aware code from
152
-    # openstack/common. We still need this shim for the rest of Cinder's
153
-    # code.
154
-    @property
155
-    def project_id(self):
156
-        return self.tenant
157
-
158
-    @project_id.setter
159
-    def project_id(self, value):
160
-        self.tenant = value
161
-
162
-    @property
163
-    def user_id(self):
164
-        return self.user
165
-
166
-    @user_id.setter
167
-    def user_id(self, value):
168
-        self.user = value

+ 0
- 4
novajoin/exception.py View File

@@ -131,10 +131,6 @@ class GlanceConnectionFailed(JoinException):
131 131
     message = "Connection to glance failed: %(reason)s"
132 132
 
133 133
 
134
-class ImageLimitExceeded(JoinException):
135
-    message = "Image quota exceeded"
136
-
137
-
138 134
 class ImageNotAuthorized(JoinException):
139 135
     message = "Not authorized for image %(image_id)s."
140 136
 

+ 7
- 1
novajoin/join.py View File

@@ -25,6 +25,7 @@ from novajoin.glance import get_default_image_service
25 25
 from novajoin.ipa import IPAClient
26 26
 from novajoin import keystone_client
27 27
 from novajoin.nova import get_instance
28
+from novajoin import policy
28 29
 from novajoin import util
29 30
 
30 31
 
@@ -112,6 +113,12 @@ class JoinController(Controller):
112 113
             LOG.error('No body in create request')
113 114
             raise base.Fault(webob.exc.HTTPBadRequest())
114 115
 
116
+        context = req.environ.get('novajoin.context')
117
+        try:
118
+            policy.authorize_action(context, 'join:create')
119
+        except exception.PolicyNotAuthorized:
120
+            raise base.Fault(webob.exc.HTTPForbidden())
121
+
115 122
         instance_id = body.get('instance-id')
116 123
         image_id = body.get('image-id')
117 124
         project_id = body.get('project-id')
@@ -139,7 +146,6 @@ class JoinController(Controller):
139 146
         if enroll.lower() != 'true':
140 147
             LOG.debug('IPA enrollment not requested in instance creation')
141 148
 
142
-        context = req.environ.get('novajoin.context')
143 149
         image_service = get_default_image_service()
144 150
         image_metadata = {}
145 151
         try:

+ 4
- 44
novajoin/middleware/auth.py View File

@@ -19,8 +19,6 @@ Simplified Common Auth Middleware from cinder.
19 19
 
20 20
 from oslo_config import cfg
21 21
 from oslo_log import log as logging
22
-from oslo_middleware import request_id
23
-from oslo_serialization import jsonutils
24 22
 import webob.dec
25 23
 import webob.exc
26 24
 
@@ -53,48 +51,10 @@ class JoinKeystoneContext(novajoin.base.Middleware):
53 51
 
54 52
     @webob.dec.wsgify(RequestClass=novajoin.base.Request)
55 53
     def __call__(self, req):
56
-        user_id = req.headers.get('X_USER')
57
-        user_id = req.headers.get('X_USER_ID', user_id)
58
-        if user_id is None:
59
-            LOG.debug("Neither X_USER_ID nor X_USER found in request")
54
+        try:
55
+            ctx = context.RequestContext.from_environ(req.environ)
56
+        except KeyError:
57
+            LOG.debug("Keystone middleware headers not found in request!")
60 58
             return webob.exc.HTTPUnauthorized()
61
-        # get the roles
62
-        roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')]
63
-        if 'X_TENANT_ID' in req.headers:
64
-            # This is the new header since Keystone went to ID/Name
65
-            project_id = req.headers['X_TENANT_ID']
66
-        else:
67
-            # This is for legacy compatibility
68
-            project_id = req.headers['X_TENANT']
69
-
70
-        project_name = req.headers.get('X_TENANT_NAME')
71
-
72
-        req_id = req.environ.get(request_id.ENV_REQUEST_ID)
73
-
74
-        # Get the auth token
75
-        auth_token = req.headers.get('X_AUTH_TOKEN',
76
-                                     req.headers.get('X_STORAGE_TOKEN'))
77
-
78
-        # Build a context, including the auth_token...
79
-        remote_address = req.remote_addr
80
-
81
-        service_catalog = None
82
-        if req.headers.get('X_SERVICE_CATALOG') is not None:
83
-            try:
84
-                catalog_header = req.headers.get('X_SERVICE_CATALOG')
85
-                service_catalog = jsonutils.loads(catalog_header)
86
-            except ValueError:
87
-                raise webob.exc.HTTPInternalServerError(
88
-                    explanation='Invalid service catalog json.')
89
-
90
-        ctx = context.RequestContext(user_id,
91
-                                     project_id,
92
-                                     project_name=project_name,
93
-                                     roles=roles,
94
-                                     auth_token=auth_token,
95
-                                     remote_address=remote_address,
96
-                                     service_catalog=service_catalog,
97
-                                     request_id=req_id)
98
-
99 59
         req.environ['novajoin.context'] = ctx
100 60
         return self.application

+ 42
- 20
novajoin/policy.py View File

@@ -16,58 +16,81 @@
16 16
 """Policy Engine"""
17 17
 
18 18
 
19
-from oslo_config import cfg
20 19
 from oslo_policy import opts as policy_opts
21 20
 from oslo_policy import policy
22 21
 
22
+from novajoin import config
23 23
 from novajoin import exception
24 24
 
25
-CONF = cfg.CONF
26
-policy_opts.set_defaults(cfg.CONF, 'policy.json')
25
+CONF = config.CONF
26
+policy_opts.set_defaults(CONF, 'policy.json')
27 27
 
28 28
 _ENFORCER = None
29 29
 
30
-
31
-def init():
30
+# We have only one endpoint, so there is not a lot of default rules
31
+_RULES = [
32
+    policy.RuleDefault(
33
+        'context_is_admin', 'role:admin',
34
+        "Decides what is required for the 'is_admin:True' check to succeed."),
35
+    policy.RuleDefault(
36
+        'service_role', 'role:service',
37
+        "service role"),
38
+    policy.RuleDefault(
39
+        'compute_service_user', 'user_name:nova and rule:service_role',
40
+        "This is usualy the nova service user, which calls the novajoin API, "
41
+        "configured in [vendordata_dynamic_auth] in nova.conf."),
42
+    policy.DocumentedRuleDefault(
43
+        'join:create', 'rule:compute_service_user',
44
+        'Generate the OTP, register it with IPA',
45
+        [{'path': '/', 'method': 'POST'}]
46
+    )
47
+]
48
+
49
+
50
+def list_rules():
51
+    return _RULES
52
+
53
+
54
+def get_enforcer():
32 55
     global _ENFORCER  # pylint: disable=global-statement
33 56
     if not _ENFORCER:
34 57
         _ENFORCER = policy.Enforcer(CONF)
58
+        _ENFORCER.register_defaults(list_rules())
59
+    return _ENFORCER
35 60
 
36 61
 
37
-def enforce_action(context, action):
62
+def authorize_action(context, action):
38 63
     """Checks that the action can be done by the given context.
39 64
 
40
-    Applies a check to ensure the context's project_id and user_id can be
41
-    applied to the given action using the policy enforcement api.
65
+    Applies a check to ensure the context's project_id, user_id and others
66
+    can be applied to the given action using the policy enforcement api.
42 67
     """
43 68
 
44
-    return enforce(context, action, {'project_id': context.project_id,
45
-                                     'user_id': context.user_id})
69
+    return authorize(context, action, context.to_dict())
46 70
 
47 71
 
48
-def enforce(context, action, target):
72
+def authorize(context, action, target):
49 73
     """Verifies that the action is valid on the target in this context.
50 74
 
51
-       :param context: cinder context
75
+       :param context: novajoin context
52 76
        :param action: string representing the action to be checked
53 77
            this should be colon separated for clarity.
54 78
            i.e. ``compute:create_instance``,
55 79
            ``compute:attach_volume``,
56 80
            ``volume:attach_volume``
57 81
 
58
-       :param object: dictionary representing the object of the action
82
+       :param target: dictionary representing the object of the action
59 83
            for object creation this should be a dictionary representing the
60 84
            location of the object e.g. ``{'project_id': context.project_id}``
61 85
 
62 86
        :raises PolicyNotAuthorized: if verification fails.
63 87
 
64 88
     """
65
-    init()
66 89
 
67
-    return _ENFORCER.enforce(action, target, context.to_dict(),
68
-                             do_raise=True,
69
-                             exc=exception.PolicyNotAuthorized,
70
-                             action=action)
90
+    return get_enforcer().authorize(action, target, context.to_dict(),
91
+                                    do_raise=True,
92
+                                    exc=exception.PolicyNotAuthorized,
93
+                                    action=action)
71 94
 
72 95
 
73 96
 def check_is_admin(roles, context=None):
@@ -76,7 +99,6 @@ def check_is_admin(roles, context=None):
76 99
        Can use roles or user_id from context to determine if user is admin.
77 100
        In a multi-domain configuration, roles alone may not be sufficient.
78 101
     """
79
-    init()
80 102
 
81 103
     # include project_id on target to avoid KeyError if context_is_admin
82 104
     # policy definition is missing, and default admin_or_owner rule
@@ -90,4 +112,4 @@ def check_is_admin(roles, context=None):
90 112
                        'user_id': context.user_id
91 113
                        }
92 114
 
93
-    return _ENFORCER.enforce('context_is_admin', target, credentials)
115
+    return get_enforcer().authorize('context_is_admin', target, credentials)

+ 15
- 4
novajoin/tests/unit/api/fakes.py View File

@@ -34,11 +34,22 @@ class HTTPRequest(webob.Request):
34 34
             if 'v1' in args[0]:
35 35
                 kwargs['base_url'] = 'http://localhost/v1'
36 36
         use_admin_context = kwargs.pop('use_admin_context', False)
37
+        use_nova_service_context = kwargs.pop('use_nova_context', True)
37 38
         version = kwargs.pop('version', '1.0')
38 39
         out = base.Request.blank(*args, **kwargs)
39
-        out.environ['cinder.context'] = FakeRequestContext(
40
-            fake.USER_ID,
41
-            fake.PROJECT_ID,
42
-            is_admin=use_admin_context)
40
+        if use_nova_service_context:
41
+            out.environ['novajoin.context'] = FakeRequestContext(
42
+                user_id=fake.USER_ID,
43
+                user_name='nova',
44
+                roles=['service'],
45
+                project_id=fake.PROJECT_ID,
46
+                is_admin=use_admin_context)
47
+        else:
48
+            out.environ['novajoin.context'] = FakeRequestContext(
49
+                user_id=fake.USER_ID,
50
+                user_name='not_nova',
51
+                roles=['not_service'],
52
+                project_id=fake.PROJECT_ID,
53
+                is_admin=use_admin_context)
43 54
         out.api_version_request = Join(version)
44 55
         return out

+ 17
- 0
novajoin/tests/unit/api/v1/test_api.py View File

@@ -18,6 +18,7 @@ from oslo_serialization import jsonutils
18 18
 from testtools.matchers import MatchesRegex
19 19
 
20 20
 from novajoin.base import Fault
21
+from novajoin import config
21 22
 from novajoin import join
22 23
 from novajoin import test
23 24
 from novajoin.tests.unit.api import fakes
@@ -36,6 +37,7 @@ class JoinTest(test.TestCase):
36 37
 
37 38
     def setUp(self):
38 39
         self.join_controller = join.JoinController()
40
+        config.CONF([])
39 41
         super(JoinTest, self).setUp()
40 42
 
41 43
     def test_no_body(self):
@@ -53,6 +55,21 @@ class JoinTest(test.TestCase):
53 55
         else:
54 56
             assert(False)
55 57
 
58
+    def test_unauthorized(self):
59
+        body = {'test': 'test'}
60
+        req = fakes.HTTPRequest.blank('/v1/', use_nova_context=False)
61
+        req.method = 'POST'
62
+        req.content_type = "application/json"
63
+
64
+        # Not using assertRaises because the exception is wrapped as
65
+        # a Fault
66
+        try:
67
+            self.join_controller.create(req, body)
68
+        except Fault as fault:
69
+            assert fault.status_int == 403
70
+        else:
71
+            assert(False)
72
+
56 73
     def test_no_instanceid(self):
57 74
         body = {"metadata": {"ipa_enroll": "True"},
58 75
                 "image-id": fake.IMAGE_ID,

+ 6
- 0
setup.cfg View File

@@ -73,3 +73,9 @@ oslo.config.opts.defaults =
73 73
 console_scripts =
74 74
     novajoin-server = novajoin.wsgi:main
75 75
     novajoin-notify = novajoin.notifications:main
76
+
77
+oslo.policy.policies =
78
+    novajoin = novajoin.policy:list_rules
79
+
80
+oslo.policy.enforcer =
81
+    novajoin = novajoin.policy:get_enforcer

+ 6
- 0
tox.ini View File

@@ -65,6 +65,7 @@ commands = sphinx-build -W -b html doc/source doc/build/html
65 65
 
66 66
 [testenv:genconfig]
67 67
 sitepackages = False
68
+basepython = python3
68 69
 envdir = {toxworkdir}/pep8
69 70
 commands = oslo-config-generator --config-file=files/novajoin-config-generator.conf
70 71
 
@@ -86,3 +87,8 @@ setenv =
86 87
 commands =
87 88
     /usr/bin/find . -type f -name "*.py[c|o]" -delete
88 89
     stestr run --slowest {posargs}
90
+
91
+[testenv:genpolicy]
92
+basepython = python3
93
+envdir = {toxworkdir}/pep8
94
+commands = oslopolicy-sample-generator --config-file files/policy-generator.conf

Loading…
Cancel
Save