Browse Source

Add access rules to token validation

This change adds application credential access rules to the token model
and ensures that only clients (that is, keystonemiddleware) that support
access rule enforcement are allowed to validate tokens containing
access rules.

Depends-on: https://review.openstack.org/633369

bp whitelist-extension-for-app-creds

Change-Id: I301651369cf03e06550bc29eb534506674e56a1f
changes/93/631993/32
Colleen Murphy 3 years ago
parent
commit
049d9bcbe4
  1. 5
      keystone/api/auth.py
  2. 4
      keystone/common/authorization.py
  3. 5
      keystone/common/rbac_enforcer/enforcer.py
  4. 4
      keystone/common/render_token.py
  5. 3
      keystone/models/token_model.py
  6. 13
      keystone/server/flask/request_processing/middleware/auth_context.py
  7. 52
      keystone/tests/unit/test_v3_auth.py
  8. 23
      keystone/token/provider.py
  9. 2
      lower-constraints.txt
  10. 2
      requirements.txt

5
keystone/api/auth.py

@ -286,12 +286,15 @@ class AuthTokenResource(_AuthFederationWebSSOBase):
token_id = flask.request.headers.get(
authorization.SUBJECT_TOKEN_HEADER)
access_rules_support = flask.request.headers.get(
authorization.ACCESS_RULES_HEADER)
allow_expired = strutils.bool_from_string(
flask.request.args.get('allow_expired'))
window_secs = CONF.token.allow_expired_window if allow_expired else 0
include_catalog = 'nocatalog' not in flask.request.args
token = PROVIDERS.token_provider_api.validate_token(
token_id, window_seconds=window_secs)
token_id, window_seconds=window_secs,
access_rules_support=access_rules_support)
token_resp = render_token.render_token_response_from_model(
token, include_catalog=include_catalog)
resp_body = jsonutils.dumps(token_resp)

4
keystone/common/authorization.py

@ -32,3 +32,7 @@ SUBJECT_TOKEN_HEADER = 'X-Subject-Token' # nosec
# Environment variable used to convey the Keystone auth context,
# the user credential used for policy enforcement.
AUTH_CONTEXT_ENV = 'KEYSTONE_AUTH_CONTEXT'
# Header set by versions of keystonemiddleware that understand application
# credential access rules
ACCESS_RULES_HEADER = 'OpenStack-Identity-Access-Rules'

5
keystone/common/rbac_enforcer/enforcer.py

@ -193,6 +193,8 @@ class RBACEnforcer(object):
# of the auth paths.
target = 'token'
subject_token = flask.request.headers.get('X-Subject-Token')
access_rules_support = flask.request.headers.get(
authorization.ACCESS_RULES_HEADER)
if subject_token is not None:
allow_expired = (strutils.bool_from_string(
flask.request.args.get('allow_expired', False),
@ -201,7 +203,8 @@ class RBACEnforcer(object):
window_seconds = CONF.token.allow_expired_window
token = PROVIDER_APIS.token_provider_api.validate_token(
subject_token,
window_seconds=window_seconds
window_seconds=window_seconds,
access_rules_support=access_rules_support
)
# TODO(morgan): Expand extracted data from the subject token.
ret_dict[target] = {}

4
keystone/common/render_token.py

@ -138,5 +138,9 @@ def render_token_response_from_model(token, include_catalog=True):
)
restricted = not token.application_credential['unrestricted']
token_reference['token'][key]['restricted'] = restricted
if token.application_credential.get('access_rules'):
token_reference['token'][key]['access_rules'] = (
token.application_credential['access_rules']
)
return token_reference

3
keystone/models/token_model.py

@ -29,6 +29,9 @@ PROVIDERS = provider_api.ProviderAPIs
V3 = 'v3.0'
VERSIONS = frozenset([V3])
# minimum access rules support
ACCESS_RULES_MIN_VERSION = 1.0
class TokenModel(object):
"""An object that represents a token emitted by keystone.

13
keystone/server/flask/request_processing/middleware/auth_context.py

@ -55,6 +55,9 @@ LOG = log.getLogger(__name__)
JSON_ENCODE_CONTENT_TYPES = set(['application/json',
'application/json-home'])
# minimum access rules support
ACCESS_RULES_MIN_VERSION = token_model.ACCESS_RULES_MIN_VERSION
def best_match_language(req):
"""Determine the best available locale.
@ -236,12 +239,14 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
kwargs_to_fetch_token = True
def __init__(self, app):
super(AuthContextMiddleware, self).__init__(app, log=LOG)
super(AuthContextMiddleware, self).__init__(app, log=LOG,
service_type='identity')
self.token = None
def fetch_token(self, token, **kwargs):
try:
self.token = self.token_provider_api.validate_token(token)
self.token = self.token_provider_api.validate_token(
token, access_rules_support=ACCESS_RULES_MIN_VERSION)
return render_token.render_token_response_from_model(self.token)
except exception.TokenNotFound:
raise auth_token.InvalidToken(_('Could not find token'))
@ -419,7 +424,9 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
# do not, and should not, use. This adds them in to the context.
if not self.token:
self.token = PROVIDERS.token_provider_api.validate_token(
request.user_token
request.user_token,
access_rules_support=request.headers.get(
authorization.ACCESS_RULES_HEADER)
)
self._keystone_specific_values(self.token, request_context)
request_context.auth_token = request.user_token

52
keystone/tests/unit/test_v3_auth.py

@ -5583,7 +5583,7 @@ class ApplicationCredentialAuth(test_v3.RestfulTestCase):
self.auth_plugin_config_override(
methods=['application_credential', 'password', 'token'])
def _make_app_cred(self, expires=None):
def _make_app_cred(self, expires=None, access_rules=None):
roles = [{'id': self.role_id}]
data = {
'id': uuid.uuid4().hex,
@ -5596,8 +5596,19 @@ class ApplicationCredentialAuth(test_v3.RestfulTestCase):
}
if expires:
data['expires_at'] = expires
if access_rules:
data['access_rules'] = access_rules
return data
def _validate_token(self, token, headers=None, expected_status=http_client.OK):
path = '/v3/auth/tokens'
headers = headers or {}
headers.update({'X-Auth-Token': token, 'X-Subject-Token': token})
with self.test_client() as c:
resp = c.get(path, headers=headers,
expected_status_code=expected_status)
return resp
def test_valid_application_credential_succeeds(self):
app_cred = self._make_app_cred()
app_cred_ref = self.app_cred_api.create_application_credential(
@ -5780,3 +5791,42 @@ class ApplicationCredentialAuth(test_v3.RestfulTestCase):
project_id=new_project['id'])
self.v3_create_token(app_cred_auth,
expected_status=http_client.UNAUTHORIZED)
def test_application_credential_with_access_rules(self):
access_rules = [
{
'id': uuid.uuid4().hex,
'path': '/v2.1/servers',
'method': 'POST',
'service': uuid.uuid4().hex,
}
]
app_cred = self._make_app_cred(access_rules=access_rules)
app_cred_ref = self.app_cred_api.create_application_credential(
app_cred)
auth_data = self.build_authentication_request(
app_cred_id=app_cred_ref['id'], secret=app_cred_ref['secret'])
resp = self.v3_create_token(auth_data,
expected_status=http_client.CREATED)
token = resp.headers.get('X-Subject-Token')
headers = {'OpenStack-Identity-Access-Rules': '1.0'}
self._validate_token(token, headers=headers)
def test_application_credential_access_rules_without_header_fails(self):
access_rules = [
{
'id': uuid.uuid4().hex,
'path': '/v2.1/servers',
'method': 'POST',
'service': uuid.uuid4().hex,
}
]
app_cred = self._make_app_cred(access_rules=access_rules)
app_cred_ref = self.app_cred_api.create_application_credential(
app_cred)
auth_data = self.build_authentication_request(
app_cred_id=app_cred_ref['id'], secret=app_cred_ref['secret'])
resp = self.v3_create_token(auth_data,
expected_status=http_client.CREATED)
token = resp.headers.get('X-Subject-Token')
self._validate_token(token, expected_status=http_client.NOT_FOUND)

23
keystone/token/provider.py

@ -51,6 +51,9 @@ UnsupportedTokenVersionException = exception.UnsupportedTokenVersionException
V3 = token_model.V3
VERSIONS = token_model.VERSIONS
# minimum access rules support
ACCESS_RULES_MIN_VERSION = token_model.ACCESS_RULES_MIN_VERSION
def default_expire_time():
"""Determine when a fresh token should expire.
@ -135,13 +138,15 @@ class Manager(manager.Manager):
def check_revocation(self, token):
return self.check_revocation_v3(token)
def validate_token(self, token_id, window_seconds=0):
def validate_token(self, token_id, window_seconds=0,
access_rules_support=None):
if not token_id:
raise exception.TokenNotFound(_('No token in the request'))
try:
token = self._validate_token(token_id)
self._is_valid_token(token, window_seconds=window_seconds)
self._validate_token_access_rules(token, access_rules_support)
return token
except exception.Unauthorized as e:
LOG.debug('Unable to validate token: %s', e)
@ -199,6 +204,22 @@ class Manager(manager.Manager):
else:
raise exception.TokenNotFound(_('Failed to validate token'))
def _validate_token_access_rules(self, token, access_rules_support=None):
if token.application_credential_id:
app_cred_api = PROVIDERS.application_credential_api
app_cred = app_cred_api.get_application_credential(
token.application_credential_id)
if (app_cred.get('access_rules') is not None and
(not access_rules_support or
(float(access_rules_support) < ACCESS_RULES_MIN_VERSION))):
LOG.exception('Attempted to use application credential'
' access rules with a middleware that does not'
' understand them. You must upgrade'
' keystonemiddleware on all services that'
' accept application credentials as an'
' authentication method.')
raise exception.TokenNotFound(_('Failed to validate token'))
def issue_token(self, user_id, method_names, expires_at=None,
system=None, project_id=None, domain_id=None,
auth_context=None, trust_id=None, app_cred_id=None,

2
lower-constraints.txt

@ -16,7 +16,7 @@ hacking==1.1.0
iso8601==0.1.12
jsonschema==2.6.0
keystoneauth1==3.4.0
keystonemiddleware==5.1.0
keystonemiddleware==7.0.0
ldappool===2.3.1
lxml==3.4.1
mock==2.0.0

2
requirements.txt

@ -17,7 +17,7 @@ sqlalchemy-migrate>=0.11.0 # Apache-2.0
stevedore>=1.20.0 # Apache-2.0
passlib>=1.7.0 # BSD
python-keystoneclient>=3.8.0 # Apache-2.0
keystonemiddleware>=5.1.0 # Apache-2.0
keystonemiddleware>=7.0.0 # Apache-2.0
bcrypt>=3.1.3 # Apache-2.0
scrypt>=0.8.0 # BSD
oslo.cache>=1.26.0 # Apache-2.0

Loading…
Cancel
Save