Merge "Add access rules to token validation"
This commit is contained in:
commit
63551bca59
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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] = {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…
Reference in New Issue