Skip to content

Commit 69cf7fc

Browse files
authored
Merge pull request #9 from stackhpc/upstream/2025.1-2026-01-05
Synchronise 2025.1 with upstream
2 parents 9e0942b + cd84d0b commit 69cf7fc

File tree

24 files changed

+439
-55
lines changed

24 files changed

+439
-55
lines changed

doc/source/getting-started/policy_mapping.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ identity:delete_application_credential DELETE /v3/users/{use
245245
identity:get_access_rule GET /v3/users/{user_id}/access_rules/{access_rule_id}
246246
identity:list_access_rules GET /v3/users/{user_id}/access_rules
247247
identity:delete_access_rule DELETE /v3/users/{user_id}/access_rules/{access_rule_id}
248+
identity:s3tokens_validate POST /v3/s3tokens
249+
identity:ec2tokens_validate POST /v3/es2tokens
248250

249251
========================================================= ===
250252

keystone/api/_shared/authentication.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ def _check_and_set_default_scoping(auth_info, auth_context):
5656

5757
default_project_id = user_ref.get('default_project_id')
5858
if not default_project_id:
59-
# User has no default project. He shall get an unscoped token.
59+
# User has no default project. They shall get an unscoped token.
60+
msg = (
61+
"User %(user_id)s doesn't have a default project. "
62+
" The token will be unscoped rather than scoped to a project."
63+
)
64+
LOG.debug(msg, {'user_id': user_ref['id']})
6065
return
6166

6267
# make sure user's default project is legit before scoping to it
@@ -178,6 +183,13 @@ def authenticate(auth_info, auth_context):
178183
resp_method_names = resp.response_data.pop('method_names', [])
179184
auth_context['method_names'].extend(resp_method_names)
180185
auth_context.update(resp.response_data or {})
186+
# NOTE(gtema): When trying to get token from
187+
# application_credential based token we need to restore
188+
# application_credential_id to prevent escaping its bounds.
189+
if "application_credential_id" in resp:
190+
auth_context["application_credential_id"] = resp[
191+
"application_credential_id"
192+
]
181193
elif resp.response_body:
182194
auth_response['methods'].append(method_name)
183195
auth_response[method_name] = resp.response_body
@@ -226,7 +238,14 @@ def authenticate_for_token(auth=None):
226238
app_cred_id = None
227239
if 'application_credential' in method_names:
228240
token_auth = auth_info.auth['identity']
229-
app_cred_id = token_auth['application_credential']['id']
241+
if "application_credential" in token_auth:
242+
app_cred_id = token_auth['application_credential']['id']
243+
elif "application_credential_id" in auth_context:
244+
app_cred_id = auth_context["application_credential_id"]
245+
else:
246+
raise exception.MissingApplicationCredentialId(
247+
user_id=auth_context['user_id']
248+
)
230249

231250
# Do MFA Rule Validation for the user
232251
if not core.UserMFARulesValidator.check_auth_methods_against_rules(

keystone/api/ec2tokens.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from keystone.api._shared import EC2_S3_Resource
2323
from keystone.api._shared import json_home_relations
24+
from keystone.common import rbac_enforcer
2425
from keystone.common import render_token
2526
from keystone.common import utils
2627
from keystone import exception
@@ -30,6 +31,9 @@
3031
CRED_TYPE_EC2 = 'ec2'
3132

3233

34+
ENFORCER = rbac_enforcer.RBACEnforcer
35+
36+
3337
class EC2TokensResource(EC2_S3_Resource.ResourceBase):
3438
@staticmethod
3539
def _check_signature(creds_ref, credentials):
@@ -57,12 +61,14 @@ def _check_signature(creds_ref, credentials):
5761
else:
5862
raise exception.Unauthorized(_('EC2 signature not supplied.'))
5963

60-
@ks_flask.unenforced_api
6164
def post(self):
6265
"""Authenticate ec2 token.
6366
6467
POST /v3/ec2tokens
6568
"""
69+
# Enforce RBAC in the same way as S3 tokens
70+
ENFORCER.enforce_call(action='identity:ec2tokens_validate')
71+
6672
token = self.handle_authenticate()
6773
token_reference = render_token.render_token_response_from_model(token)
6874
resp_body = jsonutils.dumps(token_reference)

keystone/api/s3tokens.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222

2323
from keystone.api._shared import EC2_S3_Resource
2424
from keystone.api._shared import json_home_relations
25+
from keystone.common import rbac_enforcer
2526
from keystone.common import render_token
2627
from keystone.common import utils
2728
from keystone import exception
2829
from keystone.i18n import _
2930
from keystone.server import flask as ks_flask
3031

32+
ENFORCER = rbac_enforcer.RBACEnforcer
33+
3134

3235
def _calculate_signature_v1(string_to_sign, secret_key):
3336
"""Calculate a v1 signature.
@@ -96,12 +99,14 @@ def _check_signature(creds_ref, credentials):
9699
message=_('Credential signature mismatch')
97100
)
98101

99-
@ks_flask.unenforced_api
100102
def post(self):
101103
"""Authenticate s3token.
102104
103105
POST /v3/s3tokens
104106
"""
107+
# Use standard Keystone policy enforcement for s3tokens access
108+
ENFORCER.enforce_call(action='identity:s3tokens_validate')
109+
105110
token = self.handle_authenticate()
106111
token_reference = render_token.render_token_response_from_model(token)
107112
resp_body = jsonutils.dumps(token_reference)

keystone/api/validation/__init__.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@
1616
import typing as ty
1717

1818
import flask
19+
from oslo_log import log
1920
from oslo_serialization import jsonutils
2021

2122
from keystone.api.validation import validators
23+
import keystone.conf
24+
from keystone import exception
25+
26+
CONF = keystone.conf.CONF
27+
LOG = log.getLogger(__name__)
2228

2329

2430
def validated(cls):
@@ -138,26 +144,40 @@ def add_validator(func):
138144
def wrapper(*args, **kwargs):
139145
response = func(*args, **kwargs)
140146

141-
if schema is not None:
142-
# In Flask it is not uncommon that the method return a tuple of
143-
# body and the status code. In the runtime Keystone only return
144-
# a body, but some of the used testtools do return a tuple.
145-
if isinstance(response, tuple):
146-
_body = response[0]
147-
else:
148-
_body = response
149-
150-
# NOTE(stephenfin): If our response is an object, we need to
151-
# serializer and deserialize to convert e.g. date-time
152-
# to strings
153-
_body = jsonutils.dump_as_bytes(_body)
154-
155-
if _body == b"":
156-
body = None
147+
if CONF.api.response_validation == 'ignore':
148+
# don't waste our time checking anything if we're ignoring
149+
# schema errors
150+
return response
151+
152+
if schema is None:
153+
return response
154+
155+
# In Flask it is not uncommon that the method return a tuple of
156+
# body and the status code. In the runtime Keystone only return
157+
# a body, but some of the used testtools do return a tuple.
158+
if isinstance(response, tuple):
159+
_body = response[0]
160+
else:
161+
_body = response
162+
163+
# NOTE(stephenfin): If our response is an object, we need to
164+
# serializer and deserialize to convert e.g. date-time
165+
# to strings
166+
_body = jsonutils.dump_as_bytes(_body)
167+
168+
if _body == b'':
169+
body = None
170+
else:
171+
body = jsonutils.loads(_body)
172+
173+
try:
174+
_schema_validator(schema, body, args, kwargs, is_body=True)
175+
except exception.SchemaValidationError:
176+
if CONF.api.response_validation == 'warn':
177+
LOG.exception('Schema failed to validate')
157178
else:
158-
body = jsonutils.loads(_body)
179+
raise
159180

160-
_schema_validator(schema, body, args, kwargs, is_body=True)
161181
return response
162182

163183
wrapper._response_body_schema = schema

keystone/auth/plugins/token.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414

1515
import flask
1616
from oslo_log import log
17+
import typing as ty
1718

1819
from keystone.auth.plugins import base
1920
from keystone.auth.plugins import mapped
2021
from keystone.common import provider_api
2122
import keystone.conf
2223
from keystone import exception
2324
from keystone.i18n import _
25+
from keystone.models.token_model import TokenModel
2426

2527
LOG = log.getLogger(__name__)
2628

@@ -49,13 +51,12 @@ def authenticate(self, auth_payload):
4951
# for re-scoping and we want to maintain the values. Most
5052
# AuthMethodHandlers do no such thing and this is not required.
5153
response_data.setdefault('method_names', []).extend(token.methods)
52-
5354
return base.AuthHandlerResponse(
5455
status=True, response_body=None, response_data=response_data
5556
)
5657

5758

58-
def token_authenticate(token):
59+
def token_authenticate(token: TokenModel) -> dict[str, ty.Any]:
5960
response_data = {}
6061
try:
6162
# Do not allow tokens used for delegation to
@@ -88,6 +89,20 @@ def token_authenticate(token):
8889
'or domain-scoped token is not allowed.'
8990
)
9091
)
92+
elif token.application_credential:
93+
# NOTE(gtema): when getting token from token (initially issued by
94+
# application credential) it is necessary to ensure scope is not
95+
# requested.
96+
if project_scoped or domain_scoped:
97+
raise exception.ForbiddenAction(
98+
action=_(
99+
"Using an application credential token to create a "
100+
"project-scoped or domain-scoped token is not allowed."
101+
)
102+
)
103+
response_data["application_credential_id"] = (
104+
token.application_credential["id"]
105+
)
91106

92107
if not CONF.token.allow_rescope_scoped_token:
93108
# Do not allow conversion from scoped tokens.

keystone/common/policies/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from keystone.common.policies import domain
2323
from keystone.common.policies import domain_config
2424
from keystone.common.policies import ec2_credential
25+
from keystone.common.policies import ec2tokens
2526
from keystone.common.policies import endpoint
2627
from keystone.common.policies import endpoint_group
2728
from keystone.common.policies import grant
@@ -40,6 +41,7 @@
4041
from keystone.common.policies import revoke_event
4142
from keystone.common.policies import role
4243
from keystone.common.policies import role_assignment
44+
from keystone.common.policies import s3tokens
4345
from keystone.common.policies import service
4446
from keystone.common.policies import service_provider
4547
from keystone.common.policies import token
@@ -78,6 +80,8 @@ def list_rules():
7880
revoke_event.list_rules(),
7981
role.list_rules(),
8082
role_assignment.list_rules(),
83+
s3tokens.list_rules(),
84+
ec2tokens.list_rules(),
8185
service.list_rules(),
8286
service_provider.list_rules(),
8387
token_revocation.list_rules(),
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
from oslo_policy import policy
14+
15+
from keystone.common.policies import base
16+
17+
# Align EC2 tokens API with S3 tokens: require admin or service users
18+
ADMIN_OR_SERVICE = 'rule:service_or_admin'
19+
20+
21+
ec2tokens_policies = [
22+
policy.DocumentedRuleDefault(
23+
name=base.IDENTITY % 'ec2tokens_validate',
24+
check_str=ADMIN_OR_SERVICE,
25+
scope_types=['system', 'domain', 'project'],
26+
description='Validate EC2 credentials and create a Keystone token. '
27+
'Restricted to service users or administrators.',
28+
operations=[{'path': '/v3/ec2tokens', 'method': 'POST'}],
29+
)
30+
]
31+
32+
33+
def list_rules():
34+
return ec2tokens_policies
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
from oslo_policy import policy
14+
15+
from keystone.common.policies import base
16+
17+
# S3 tokens API requires service authentication to prevent presigned URL exploitation
18+
# This policy restricts access to service users or administrators only
19+
ADMIN_OR_SERVICE = 'rule:service_or_admin'
20+
21+
s3tokens_policies = [
22+
policy.DocumentedRuleDefault(
23+
name=base.IDENTITY % 's3tokens_validate',
24+
check_str=ADMIN_OR_SERVICE,
25+
scope_types=['system', 'domain', 'project'],
26+
description='Validate S3 credentials and create a Keystone token. '
27+
'Restricted to service users or administrators to prevent '
28+
'exploitation via presigned URLs.',
29+
operations=[{'path': '/v3/s3tokens', 'method': 'POST'}],
30+
)
31+
]
32+
33+
34+
def list_rules():
35+
return s3tokens_policies

keystone/conf/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from oslo_middleware import cors
2020
from osprofiler import opts as profiler
2121

22+
from keystone.conf import api
2223
from keystone.conf import application_credential
2324
from keystone.conf import assignment
2425
from keystone.conf import auth
@@ -54,8 +55,8 @@
5455

5556
CONF = cfg.CONF
5657

57-
5858
conf_modules = [
59+
api,
5960
application_credential,
6061
assignment,
6162
auth,
@@ -90,7 +91,6 @@
9091
wsgi,
9192
]
9293

93-
9494
oslo_messaging.set_transport_defaults(control_exchange='keystone')
9595

9696

0 commit comments

Comments
 (0)