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
This commit is contained in:
parent
67682dcd07
commit
049d9bcbe4
|
@ -286,12 +286,15 @@ class AuthTokenResource(_AuthFederationWebSSOBase):
|
||||||
|
|
||||||
token_id = flask.request.headers.get(
|
token_id = flask.request.headers.get(
|
||||||
authorization.SUBJECT_TOKEN_HEADER)
|
authorization.SUBJECT_TOKEN_HEADER)
|
||||||
|
access_rules_support = flask.request.headers.get(
|
||||||
|
authorization.ACCESS_RULES_HEADER)
|
||||||
allow_expired = strutils.bool_from_string(
|
allow_expired = strutils.bool_from_string(
|
||||||
flask.request.args.get('allow_expired'))
|
flask.request.args.get('allow_expired'))
|
||||||
window_secs = CONF.token.allow_expired_window if allow_expired else 0
|
window_secs = CONF.token.allow_expired_window if allow_expired else 0
|
||||||
include_catalog = 'nocatalog' not in flask.request.args
|
include_catalog = 'nocatalog' not in flask.request.args
|
||||||
token = PROVIDERS.token_provider_api.validate_token(
|
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_resp = render_token.render_token_response_from_model(
|
||||||
token, include_catalog=include_catalog)
|
token, include_catalog=include_catalog)
|
||||||
resp_body = jsonutils.dumps(token_resp)
|
resp_body = jsonutils.dumps(token_resp)
|
||||||
|
|
|
@ -32,3 +32,7 @@ SUBJECT_TOKEN_HEADER = 'X-Subject-Token' # nosec
|
||||||
# Environment variable used to convey the Keystone auth context,
|
# Environment variable used to convey the Keystone auth context,
|
||||||
# the user credential used for policy enforcement.
|
# the user credential used for policy enforcement.
|
||||||
AUTH_CONTEXT_ENV = 'KEYSTONE_AUTH_CONTEXT'
|
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'
|
||||||
|
|
|
@ -193,6 +193,8 @@ class RBACEnforcer(object):
|
||||||
# of the auth paths.
|
# of the auth paths.
|
||||||
target = 'token'
|
target = 'token'
|
||||||
subject_token = flask.request.headers.get('X-Subject-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:
|
if subject_token is not None:
|
||||||
allow_expired = (strutils.bool_from_string(
|
allow_expired = (strutils.bool_from_string(
|
||||||
flask.request.args.get('allow_expired', False),
|
flask.request.args.get('allow_expired', False),
|
||||||
|
@ -201,7 +203,8 @@ class RBACEnforcer(object):
|
||||||
window_seconds = CONF.token.allow_expired_window
|
window_seconds = CONF.token.allow_expired_window
|
||||||
token = PROVIDER_APIS.token_provider_api.validate_token(
|
token = PROVIDER_APIS.token_provider_api.validate_token(
|
||||||
subject_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.
|
# TODO(morgan): Expand extracted data from the subject token.
|
||||||
ret_dict[target] = {}
|
ret_dict[target] = {}
|
||||||
|
|
|
@ -138,5 +138,9 @@ def render_token_response_from_model(token, include_catalog=True):
|
||||||
)
|
)
|
||||||
restricted = not token.application_credential['unrestricted']
|
restricted = not token.application_credential['unrestricted']
|
||||||
token_reference['token'][key]['restricted'] = restricted
|
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
|
return token_reference
|
||||||
|
|
|
@ -29,6 +29,9 @@ PROVIDERS = provider_api.ProviderAPIs
|
||||||
V3 = 'v3.0'
|
V3 = 'v3.0'
|
||||||
VERSIONS = frozenset([V3])
|
VERSIONS = frozenset([V3])
|
||||||
|
|
||||||
|
# minimum access rules support
|
||||||
|
ACCESS_RULES_MIN_VERSION = 1.0
|
||||||
|
|
||||||
|
|
||||||
class TokenModel(object):
|
class TokenModel(object):
|
||||||
"""An object that represents a token emitted by keystone.
|
"""An object that represents a token emitted by keystone.
|
||||||
|
|
|
@ -55,6 +55,9 @@ LOG = log.getLogger(__name__)
|
||||||
JSON_ENCODE_CONTENT_TYPES = set(['application/json',
|
JSON_ENCODE_CONTENT_TYPES = set(['application/json',
|
||||||
'application/json-home'])
|
'application/json-home'])
|
||||||
|
|
||||||
|
# minimum access rules support
|
||||||
|
ACCESS_RULES_MIN_VERSION = token_model.ACCESS_RULES_MIN_VERSION
|
||||||
|
|
||||||
|
|
||||||
def best_match_language(req):
|
def best_match_language(req):
|
||||||
"""Determine the best available locale.
|
"""Determine the best available locale.
|
||||||
|
@ -236,12 +239,14 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
|
||||||
kwargs_to_fetch_token = True
|
kwargs_to_fetch_token = True
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
super(AuthContextMiddleware, self).__init__(app, log=LOG)
|
super(AuthContextMiddleware, self).__init__(app, log=LOG,
|
||||||
|
service_type='identity')
|
||||||
self.token = None
|
self.token = None
|
||||||
|
|
||||||
def fetch_token(self, token, **kwargs):
|
def fetch_token(self, token, **kwargs):
|
||||||
try:
|
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)
|
return render_token.render_token_response_from_model(self.token)
|
||||||
except exception.TokenNotFound:
|
except exception.TokenNotFound:
|
||||||
raise auth_token.InvalidToken(_('Could not find token'))
|
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.
|
# do not, and should not, use. This adds them in to the context.
|
||||||
if not self.token:
|
if not self.token:
|
||||||
self.token = PROVIDERS.token_provider_api.validate_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)
|
self._keystone_specific_values(self.token, request_context)
|
||||||
request_context.auth_token = request.user_token
|
request_context.auth_token = request.user_token
|
||||||
|
|
|
@ -5583,7 +5583,7 @@ class ApplicationCredentialAuth(test_v3.RestfulTestCase):
|
||||||
self.auth_plugin_config_override(
|
self.auth_plugin_config_override(
|
||||||
methods=['application_credential', 'password', 'token'])
|
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}]
|
roles = [{'id': self.role_id}]
|
||||||
data = {
|
data = {
|
||||||
'id': uuid.uuid4().hex,
|
'id': uuid.uuid4().hex,
|
||||||
|
@ -5596,8 +5596,19 @@ class ApplicationCredentialAuth(test_v3.RestfulTestCase):
|
||||||
}
|
}
|
||||||
if expires:
|
if expires:
|
||||||
data['expires_at'] = expires
|
data['expires_at'] = expires
|
||||||
|
if access_rules:
|
||||||
|
data['access_rules'] = access_rules
|
||||||
return data
|
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):
|
def test_valid_application_credential_succeeds(self):
|
||||||
app_cred = self._make_app_cred()
|
app_cred = self._make_app_cred()
|
||||||
app_cred_ref = self.app_cred_api.create_application_credential(
|
app_cred_ref = self.app_cred_api.create_application_credential(
|
||||||
|
@ -5780,3 +5791,42 @@ class ApplicationCredentialAuth(test_v3.RestfulTestCase):
|
||||||
project_id=new_project['id'])
|
project_id=new_project['id'])
|
||||||
self.v3_create_token(app_cred_auth,
|
self.v3_create_token(app_cred_auth,
|
||||||
expected_status=http_client.UNAUTHORIZED)
|
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)
|
||||||
|
|
|
@ -51,6 +51,9 @@ UnsupportedTokenVersionException = exception.UnsupportedTokenVersionException
|
||||||
V3 = token_model.V3
|
V3 = token_model.V3
|
||||||
VERSIONS = token_model.VERSIONS
|
VERSIONS = token_model.VERSIONS
|
||||||
|
|
||||||
|
# minimum access rules support
|
||||||
|
ACCESS_RULES_MIN_VERSION = token_model.ACCESS_RULES_MIN_VERSION
|
||||||
|
|
||||||
|
|
||||||
def default_expire_time():
|
def default_expire_time():
|
||||||
"""Determine when a fresh token should expire.
|
"""Determine when a fresh token should expire.
|
||||||
|
@ -135,13 +138,15 @@ class Manager(manager.Manager):
|
||||||
def check_revocation(self, token):
|
def check_revocation(self, token):
|
||||||
return self.check_revocation_v3(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:
|
if not token_id:
|
||||||
raise exception.TokenNotFound(_('No token in the request'))
|
raise exception.TokenNotFound(_('No token in the request'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
token = self._validate_token(token_id)
|
token = self._validate_token(token_id)
|
||||||
self._is_valid_token(token, window_seconds=window_seconds)
|
self._is_valid_token(token, window_seconds=window_seconds)
|
||||||
|
self._validate_token_access_rules(token, access_rules_support)
|
||||||
return token
|
return token
|
||||||
except exception.Unauthorized as e:
|
except exception.Unauthorized as e:
|
||||||
LOG.debug('Unable to validate token: %s', e)
|
LOG.debug('Unable to validate token: %s', e)
|
||||||
|
@ -199,6 +204,22 @@ class Manager(manager.Manager):
|
||||||
else:
|
else:
|
||||||
raise exception.TokenNotFound(_('Failed to validate token'))
|
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,
|
def issue_token(self, user_id, method_names, expires_at=None,
|
||||||
system=None, project_id=None, domain_id=None,
|
system=None, project_id=None, domain_id=None,
|
||||||
auth_context=None, trust_id=None, app_cred_id=None,
|
auth_context=None, trust_id=None, app_cred_id=None,
|
||||||
|
|
|
@ -16,7 +16,7 @@ hacking==1.1.0
|
||||||
iso8601==0.1.12
|
iso8601==0.1.12
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
keystoneauth1==3.4.0
|
keystoneauth1==3.4.0
|
||||||
keystonemiddleware==5.1.0
|
keystonemiddleware==7.0.0
|
||||||
ldappool===2.3.1
|
ldappool===2.3.1
|
||||||
lxml==3.4.1
|
lxml==3.4.1
|
||||||
mock==2.0.0
|
mock==2.0.0
|
||||||
|
|
|
@ -17,7 +17,7 @@ sqlalchemy-migrate>=0.11.0 # Apache-2.0
|
||||||
stevedore>=1.20.0 # Apache-2.0
|
stevedore>=1.20.0 # Apache-2.0
|
||||||
passlib>=1.7.0 # BSD
|
passlib>=1.7.0 # BSD
|
||||||
python-keystoneclient>=3.8.0 # Apache-2.0
|
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
|
bcrypt>=3.1.3 # Apache-2.0
|
||||||
scrypt>=0.8.0 # BSD
|
scrypt>=0.8.0 # BSD
|
||||||
oslo.cache>=1.26.0 # Apache-2.0
|
oslo.cache>=1.26.0 # Apache-2.0
|
||||||
|
|
Loading…
Reference in New Issue