File: keystone-bug-2119646-stable-2024.1.patch

package info (click to toggle)
keystone 2%3A22.0.2-0%2Bdeb12u1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm-proposed-updates
  • size: 13,460 kB
  • sloc: python: 107,361; pascal: 2,207; sh: 813; xml: 335; makefile: 215
file content (383 lines) | stat: -rw-r--r-- 15,692 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
From 8c246cac15b2d770f26eeafee51e09f66cb0ad79 Mon Sep 17 00:00:00 2001
From: Grzegorz Grasza <xek@redhat.com>
Date: Fri, 19 Sep 2025 14:02:18 +0200
Subject: [PATCH] Add service user authentication to ec2 and s3 endpoints

Add a policy to enforce authentication with a user in the service
group. This maintains AWS compatibility with the added security
layer.

Closes-Bug: 2119646
Change-Id: Ic84b84247e05f29874e2c5636a033aaedd4de83c
(cherry picked from commit 69d299eab04a1e1bab25eb89e0fdf7f0106b8ee5)
Signed-off-by: Jeremy Stanley <fungi@yuggoth.org>
---
 keystone/api/ec2tokens.py                    |  8 ++-
 keystone/api/s3tokens.py                     |  7 ++-
 keystone/common/policies/__init__.py         |  4 ++
 keystone/common/policies/ec2tokens.py        | 37 ++++++++++++++
 keystone/common/policies/s3tokens.py         | 35 ++++++++++++++
 keystone/tests/unit/test_contrib_ec2_core.py | 30 ++++++++++--
 keystone/tests/unit/test_contrib_s3_core.py  | 51 ++++++++++++++------
 keystone/tests/unit/test_v3_credential.py    | 22 ++++++---
 8 files changed, 166 insertions(+), 28 deletions(-)
 create mode 100644 keystone/common/policies/ec2tokens.py
 create mode 100644 keystone/common/policies/s3tokens.py

Index: keystone/keystone/api/ec2tokens.py
===================================================================
--- keystone.orig/keystone/api/ec2tokens.py
+++ keystone/keystone/api/ec2tokens.py
@@ -21,6 +21,7 @@ from oslo_serialization import jsonutils
 
 from keystone.api._shared import EC2_S3_Resource
 from keystone.api._shared import json_home_relations
+from keystone.common import rbac_enforcer
 from keystone.common import render_token
 from keystone.common import utils
 from keystone import exception
@@ -31,6 +32,9 @@ from keystone.server import flask as ks_
 CRED_TYPE_EC2 = 'ec2'
 
 
+ENFORCER = rbac_enforcer.RBACEnforcer
+
+
 class EC2TokensResource(EC2_S3_Resource.ResourceBase):
     @staticmethod
     def _check_signature(creds_ref, credentials):
@@ -60,12 +64,14 @@ class EC2TokensResource(EC2_S3_Resource.
             raise exception.Unauthorized(
                 _('EC2 signature not supplied.'))
 
-    @ks_flask.unenforced_api
     def post(self):
         """Authenticate ec2 token.
 
         POST /v3/ec2tokens
         """
+        # Enforce RBAC in the same way as S3 tokens
+        ENFORCER.enforce_call(action='identity:ec2tokens_validate')
+
         token = self.handle_authenticate()
         token_reference = render_token.render_token_response_from_model(token)
         resp_body = jsonutils.dumps(token_reference)
Index: keystone/keystone/api/s3tokens.py
===================================================================
--- keystone.orig/keystone/api/s3tokens.py
+++ keystone/keystone/api/s3tokens.py
@@ -22,12 +22,15 @@ from oslo_serialization import jsonutils
 
 from keystone.api._shared import EC2_S3_Resource
 from keystone.api._shared import json_home_relations
+from keystone.common import rbac_enforcer
 from keystone.common import render_token
 from keystone.common import utils
 from keystone import exception
 from keystone.i18n import _
 from keystone.server import flask as ks_flask
 
+ENFORCER = rbac_enforcer.RBACEnforcer
+
 
 def _calculate_signature_v1(string_to_sign, secret_key):
     """Calculate a v1 signature.
@@ -90,12 +93,14 @@ class S3Resource(EC2_S3_Resource.Resourc
             raise exception.Unauthorized(
                 message=_('Credential signature mismatch'))
 
-    @ks_flask.unenforced_api
     def post(self):
         """Authenticate s3token.
 
         POST /v3/s3tokens
         """
+        # Use standard Keystone policy enforcement for s3tokens access
+        ENFORCER.enforce_call(action='identity:s3tokens_validate')
+
         token = self.handle_authenticate()
         token_reference = render_token.render_token_response_from_model(token)
         resp_body = jsonutils.dumps(token_reference)
Index: keystone/keystone/common/policies/__init__.py
===================================================================
--- keystone.orig/keystone/common/policies/__init__.py
+++ keystone/keystone/common/policies/__init__.py
@@ -22,6 +22,7 @@ from keystone.common.policies import cre
 from keystone.common.policies import domain
 from keystone.common.policies import domain_config
 from keystone.common.policies import ec2_credential
+from keystone.common.policies import ec2tokens
 from keystone.common.policies import endpoint
 from keystone.common.policies import endpoint_group
 from keystone.common.policies import grant
@@ -40,6 +41,7 @@ from keystone.common.policies import reg
 from keystone.common.policies import revoke_event
 from keystone.common.policies import role
 from keystone.common.policies import role_assignment
+from keystone.common.policies import s3tokens
 from keystone.common.policies import service
 from keystone.common.policies import service_provider
 from keystone.common.policies import token
@@ -78,6 +80,8 @@ def list_rules():
         revoke_event.list_rules(),
         role.list_rules(),
         role_assignment.list_rules(),
+        s3tokens.list_rules(),
+        ec2tokens.list_rules(),
         service.list_rules(),
         service_provider.list_rules(),
         token_revocation.list_rules(),
Index: keystone/keystone/common/policies/ec2tokens.py
===================================================================
--- /dev/null
+++ keystone/keystone/common/policies/ec2tokens.py
@@ -0,0 +1,37 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_policy import policy
+
+from keystone.common.policies import base
+
+# Align EC2 tokens API with S3 tokens: require admin or service users
+ADMIN_OR_SERVICE = 'rule:service_or_admin'
+
+
+ec2tokens_policies = [
+    policy.DocumentedRuleDefault(
+        name=base.IDENTITY % 'ec2tokens_validate',
+        check_str=ADMIN_OR_SERVICE,
+        scope_types=['system', 'domain', 'project'],
+        description='Validate EC2 credentials and create a Keystone token. '
+        'Restricted to service users or administrators.',
+        operations=[{'path': '/v3/ec2tokens', 'method': 'POST'}],
+    )
+]
+
+
+def list_rules():
+    return ec2tokens_policies
+
+
+
Index: keystone/keystone/common/policies/s3tokens.py
===================================================================
--- /dev/null
+++ keystone/keystone/common/policies/s3tokens.py
@@ -0,0 +1,35 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_policy import policy
+
+from keystone.common.policies import base
+
+# S3 tokens API requires service authentication to prevent presigned URL exploitation
+# This policy restricts access to service users or administrators only
+ADMIN_OR_SERVICE = 'rule:service_or_admin'
+
+s3tokens_policies = [
+    policy.DocumentedRuleDefault(
+        name=base.IDENTITY % 's3tokens_validate',
+        check_str=ADMIN_OR_SERVICE,
+        scope_types=['system', 'domain', 'project'],
+        description='Validate S3 credentials and create a Keystone token. '
+        'Restricted to service users or administrators to prevent '
+        'exploitation via presigned URLs.',
+        operations=[{'path': '/v3/s3tokens', 'method': 'POST'}],
+    )
+]
+
+
+def list_rules():
+    return s3tokens_policies
Index: keystone/keystone/tests/unit/test_contrib_ec2_core.py
===================================================================
--- keystone.orig/keystone/tests/unit/test_contrib_ec2_core.py
+++ keystone/keystone/tests/unit/test_contrib_ec2_core.py
@@ -44,7 +44,7 @@ class EC2ContribCoreV3(test_v3.RestfulTe
         self.assertEqual(http.client.METHOD_NOT_ALLOWED,
                          resp.status_code)
 
-    def test_valid_authentication_response_with_proper_secret(self):
+    def _test_valid_authentication_response_with_proper_secret(self, **kwargs):
         signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
         timestamp = utils.isotime(timeutils.utcnow())
         credentials = {
@@ -56,15 +56,39 @@ class EC2ContribCoreV3(test_v3.RestfulTe
             'params': {
                 'SignatureVersion': '2',
                 'Action': 'Test',
-                'Timestamp': timestamp
+                'Timestamp': timestamp,
             },
         }
         credentials['signature'] = signer.generate(credentials)
+        # Authenticate as system admin by default unless overridden via kwargs
+        token = None
+        if 'noauth' in kwargs and kwargs['noauth']:
+            token = None
+        else:
+            PROVIDERS.assignment_api.create_system_grant_for_user(
+                self.user_id, self.role_id
+            )
+            token = self.get_system_scoped_token()
+
+        expected_status = kwargs.get('expected_status', http.client.OK)
         resp = self.post(
             '/ec2tokens',
             body={'credentials': credentials},
-            expected_status=http.client.OK)
-        self.assertValidProjectScopedTokenResponse(resp, self.user)
+            expected_status=expected_status,
+            token=token,
+            noauth=kwargs.get('noauth'),
+        )
+        if expected_status == http.client.OK:
+            self.assertValidProjectScopedTokenResponse(resp, self.user)
+
+    def test_valid_authentication_response_with_proper_secret(self):
+        self._test_valid_authentication_response_with_proper_secret()
+
+    def test_valid_authentication_response_with_proper_secret_noauth(self):
+        # ec2 endpoint now enforces RBAC; unauthenticated should be denied
+        self._test_valid_authentication_response_with_proper_secret(
+            expected_status=http.client.UNAUTHORIZED, noauth=True
+        )
 
     def test_valid_authentication_response_with_signature_v4(self):
         signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
Index: keystone/keystone/tests/unit/test_contrib_s3_core.py
===================================================================
--- keystone.orig/keystone/tests/unit/test_contrib_s3_core.py
+++ keystone/keystone/tests/unit/test_contrib_s3_core.py
@@ -40,26 +40,45 @@ class S3ContribCore(test_v3.RestfulTestC
             self.credential['id'], self.credential)
 
     def test_http_get_method_not_allowed(self):
-        resp = self.get('/s3tokens',
-                        expected_status=http.client.METHOD_NOT_ALLOWED,
-                        convert=False)
-        self.assertEqual(http.client.METHOD_NOT_ALLOWED,
-                         resp.status_code)
+        resp = self.get(
+            '/s3tokens',
+            expected_status=http.client.METHOD_NOT_ALLOWED,
+            convert=False,
+        )
+        self.assertEqual(http.client.METHOD_NOT_ALLOWED, resp.status_code)
 
-    def test_good_response(self):
+    def _test_good_response(self, expected_status=http.client.OK, **kwargs):
         sts = 'string to sign'  # opaque string from swift3
-        sig = hmac.new(self.cred_blob['secret'].encode('ascii'),
-                       sts.encode('ascii'), hashlib.sha1).digest()
+        sig = hmac.new(
+            self.cred_blob['secret'].encode('ascii'),
+            sts.encode('ascii'),
+            hashlib.sha1,
+        ).digest()
         resp = self.post(
             '/s3tokens',
-            body={'credentials': {
-                'access': self.cred_blob['access'],
-                'signature': base64.b64encode(sig).strip(),
-                'token': base64.b64encode(sts.encode('ascii')).strip(),
-            }},
-            expected_status=http.client.OK)
-        self.assertValidProjectScopedTokenResponse(resp, self.user,
-                                                   forbid_token_id=True)
+            body={
+                'credentials': {
+                    'access': self.cred_blob['access'],
+                    'signature': base64.b64encode(sig).strip(),
+                    'token': base64.b64encode(sts.encode('ascii')).strip(),
+                }
+            },
+            expected_status=expected_status,
+            **kwargs,
+        )
+        if expected_status == http.client.OK:
+            self.assertValidProjectScopedTokenResponse(
+                resp, self.user, forbid_token_id=True
+            )
+        else:
+            self.assertValidErrorResponse(resp)
+
+    def test_good_response(self):
+        self._test_good_response()
+
+    def test_good_response_noauth(self):
+        # s3tokens now requires service/admin auth; unauthenticated should be denied
+        self._test_good_response(http.client.UNAUTHORIZED, noauth=True)
 
     def test_bad_request(self):
         self.post(
Index: keystone/keystone/tests/unit/test_v3_credential.py
===================================================================
--- keystone.orig/keystone/tests/unit/test_v3_credential.py
+++ keystone/keystone/tests/unit/test_v3_credential.py
@@ -79,16 +79,24 @@ class CredentialBaseTestCase(test_v3.Res
 
         # Now make a request to validate the signed dummy request via the
         # ec2tokens API.  This proves the v3 ec2 credentials actually work.
-        sig_ref = {'access': access,
-                   'signature': signature,
-                   'host': 'foo',
-                   'verb': 'GET',
-                   'path': '/bar',
-                   'params': params}
+        sig_ref = {
+            'access': access,
+            'signature': signature,
+            'host': 'foo',
+            'verb': 'GET',
+            'path': '/bar',
+            'params': params,
+        }
+        PROVIDERS.assignment_api.create_system_grant_for_user(
+            self.user_id, self.role_id
+        )
+        token = self.get_system_scoped_token()
         r = self.post(
             '/ec2tokens',
             body={'ec2Credentials': sig_ref},
-            expected_status=http.client.OK)
+            expected_status=http.client.OK,
+            token=token,
+        )
         self.assertValidTokenResponse(r)
         return r.result['token']
 
Index: keystone/doc/source/getting-started/policy_mapping.rst
===================================================================
--- keystone.orig/doc/source/getting-started/policy_mapping.rst
+++ keystone/doc/source/getting-started/policy_mapping.rst
@@ -246,6 +246,9 @@ identity:get_access_rule
 identity:list_access_rules                                 GET /v3/users/{user_id}/access_rules
 identity:delete_access_rule                                DELETE /v3/users/{user_id}/access_rules/{access_rule_id}
 
+identity:ec2tokens_validate                                POST /v3/ec2tokens
+identity:s3tokens_validate                                 POST /v3/s3tokens
+
 =========================================================  ===
 
 .. _grant_resources: