diff --git a/keystone/api/auth.py b/keystone/api/auth.py index 96c17f1301..d399df433c 100644 --- a/keystone/api/auth.py +++ b/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) diff --git a/keystone/common/authorization.py b/keystone/common/authorization.py index a15c9eaac3..b236367b8b 100644 --- a/keystone/common/authorization.py +++ b/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' diff --git a/keystone/common/rbac_enforcer/enforcer.py b/keystone/common/rbac_enforcer/enforcer.py index defae6c754..cb5034b068 100644 --- a/keystone/common/rbac_enforcer/enforcer.py +++ b/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] = {} diff --git a/keystone/common/render_token.py b/keystone/common/render_token.py index 9363639114..320260b1f5 100644 --- a/keystone/common/render_token.py +++ b/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 diff --git a/keystone/models/token_model.py b/keystone/models/token_model.py index 7f190286a7..077f138b22 100644 --- a/keystone/models/token_model.py +++ b/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. diff --git a/keystone/server/flask/request_processing/middleware/auth_context.py b/keystone/server/flask/request_processing/middleware/auth_context.py index 56f1df397c..ae6947a459 100644 --- a/keystone/server/flask/request_processing/middleware/auth_context.py +++ b/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 diff --git a/keystone/tests/unit/test_v3_auth.py b/keystone/tests/unit/test_v3_auth.py index 8ac266c1ba..926c102862 100644 --- a/keystone/tests/unit/test_v3_auth.py +++ b/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) diff --git a/keystone/token/provider.py b/keystone/token/provider.py index c455885da1..e3c45a3033 100644 --- a/keystone/token/provider.py +++ b/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, diff --git a/lower-constraints.txt b/lower-constraints.txt index 8c78350ef5..2e061fea6d 100644 --- a/lower-constraints.txt +++ b/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 diff --git a/requirements.txt b/requirements.txt index d33a948748..36a0cdc68c 100644 --- a/requirements.txt +++ b/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