Merge "Add access rules to token validation"

This commit is contained in:
Zuul 2019-09-15 06:33:27 +00:00 committed by Gerrit Code Review
commit 63551bca59
10 changed files with 104 additions and 9 deletions

View File

@ -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)

View File

@ -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'

View File

@ -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] = {}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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