From d97832e8e826e37171b727072c720a9b589998dd Mon Sep 17 00:00:00 2001 From: morgan fainberg Date: Tue, 18 Sep 2018 10:54:59 -0700 Subject: [PATCH] Convert auth to flask native dispatching Convert the /auth paths to flask native dispatching. A minor change to additional_urls was implemented to ensure all urls are added at once instead of individually (causing an over- write issue within flask as a single resource may only have a single set of URL mappings). Alternate URLs now support adding alternate JSON Home rel links. This is to support the case of OS-FEDERATION auth routes moving to /auth. The old JSON Home entries must exist but reference the new paths. This port includes the following test changes (needed due to the way flask handles requests and the way requests are passed through the auth system): * Implemented keystone.common.render_token (module) containing render_token_response_from_model and use it instead of keystone.common.controller.render_token_response_from_model. Minor differences occur in render_token_response_from_model in the keystone.common.render_token module, this is simply for referencing data from flask instead of the request object. * Test cases have been modified to no longer rely on the auth controller(s) directly * Test cases now use "make_request" as a context manager since authenticate/authenticate_for_token directly reference the flask contexts and must have an explicit context pushed. * Test cases no longer pass request objects into methods such as authenticate/authenticate_for_token or similar methods on the auth plugins * Test cases for federation reference the token model now where possible instead of the rendered token response. Rendered token responses are generated where needed. * Auth Plugin Configuration is done in test core as well. This is because Auth controller does not exist. NOTE: This is a massive change, but must of these changes were now easily uncoupled because of how far reaching auth is. Change-Id: I636928102875760726cc3493775a2be48e774fd7 Partial-Bug: #1776504 --- keystone/api/__init__.py | 3 + keystone/api/_shared/authentication.py | 243 ++++++++ keystone/api/_shared/json_home_relations.py | 5 + keystone/api/auth.py | 578 ++++++++++++++++++ keystone/api/endpoints.py | 3 +- keystone/api/groups.py | 5 +- keystone/api/os_federation.py | 109 +--- keystone/api/os_oauth1.py | 4 +- keystone/application_credential/core.py | 2 +- keystone/auth/__init__.py | 2 - keystone/auth/controllers.py | 454 -------------- keystone/auth/core.py | 13 +- .../auth/plugins/application_credential.py | 3 +- keystone/auth/plugins/base.py | 4 +- keystone/auth/plugins/external.py | 34 +- keystone/auth/plugins/mapped.py | 31 +- keystone/auth/plugins/oauth1.py | 14 +- keystone/auth/plugins/password.py | 3 +- keystone/auth/plugins/token.py | 14 +- keystone/auth/plugins/totp.py | 2 +- keystone/auth/routers.py | 77 --- keystone/common/rbac_enforcer/enforcer.py | 6 +- keystone/common/render_token.py | 145 +++++ keystone/contrib/ec2/controllers.py | 3 +- keystone/federation/controllers.py | 226 ------- keystone/federation/routers.py | 95 --- keystone/federation/utils.py | 7 +- keystone/identity/controllers.py | 2 +- keystone/identity/core.py | 17 +- keystone/middleware/auth.py | 4 +- keystone/notifications.py | 35 +- keystone/server/flask/__init__.py | 3 +- keystone/server/flask/application.py | 9 +- keystone/server/flask/common.py | 82 +-- keystone/server/flask/core.py | 2 + .../application_credential/test_backends.py | 6 +- .../tests/unit/common/test_notifications.py | 83 +-- .../unit/contrib/federation/test_utils.py | 44 +- keystone/tests/unit/core.py | 20 +- .../identity/shadow_users/test_backend.py | 16 +- .../tests/unit/identity/test_backend_sql.py | 420 ++++++------- keystone/tests/unit/identity/test_backends.py | 86 ++- .../tests/unit/server/test_keystone_flask.py | 19 +- keystone/tests/unit/test_auth_plugin.py | 52 +- keystone/tests/unit/test_backend_ldap.py | 26 +- keystone/tests/unit/test_backend_ldap_pool.py | 26 +- keystone/tests/unit/test_cli.py | 16 +- .../tests/unit/test_ldap_pool_livetest.py | 13 +- keystone/tests/unit/test_v3.py | 13 - keystone/tests/unit/test_v3_federation.py | 438 ++++++------- 50 files changed, 1801 insertions(+), 1716 deletions(-) create mode 100644 keystone/api/_shared/authentication.py create mode 100644 keystone/api/auth.py delete mode 100644 keystone/auth/controllers.py delete mode 100644 keystone/auth/routers.py create mode 100644 keystone/common/render_token.py delete mode 100644 keystone/federation/controllers.py delete mode 100644 keystone/federation/routers.py diff --git a/keystone/api/__init__.py b/keystone/api/__init__.py index 046f098cb6..7b5f476fa0 100644 --- a/keystone/api/__init__.py +++ b/keystone/api/__init__.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from keystone.api import auth from keystone.api import credentials from keystone.api import discovery from keystone.api import domains @@ -33,6 +34,7 @@ from keystone.api import system from keystone.api import trusts __all__ = ( + 'auth', 'discovery', 'credentials', 'domains', @@ -58,6 +60,7 @@ __all__ = ( __apis__ = ( discovery, + auth, credentials, domains, endpoints, diff --git a/keystone/api/_shared/authentication.py b/keystone/api/_shared/authentication.py new file mode 100644 index 0000000000..10370fcc18 --- /dev/null +++ b/keystone/api/_shared/authentication.py @@ -0,0 +1,243 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# Shared code for Authentication flows. This module is where actual auth +# happens. The code here is shared between Federation and Auth. + +# TODO(morgan): Deprecate all auth flows in /v3/OS-FEDERATION, merge this code +# into keystone.api.auth. For now this is the best place for the code to +# exist. + +import flask +from oslo_log import log +import six + +from keystone.auth import core +from keystone.common import provider_api +from keystone import exception +from keystone.federation import constants +from keystone.i18n import _ + + +LOG = log.getLogger(__name__) +PROVIDERS = provider_api.ProviderAPIs + + +def _check_and_set_default_scoping(auth_info, auth_context): + (domain_id, project_id, trust, unscoped, system) = ( + auth_info.get_scope() + ) + if trust: + project_id = trust['project_id'] + if system or domain_id or project_id or trust: + # scope is specified + return + + # Skip scoping when unscoped federated token is being issued + if constants.IDENTITY_PROVIDER in auth_context: + return + + # Do not scope if request is for explicitly unscoped token + if unscoped is not None: + return + + # fill in default_project_id if it is available + try: + user_ref = PROVIDERS.identity_api.get_user(auth_context['user_id']) + except exception.UserNotFound as e: + LOG.warning(six.text_type(e)) + raise exception.Unauthorized(e) + + default_project_id = user_ref.get('default_project_id') + if not default_project_id: + # User has no default project. He shall get an unscoped token. + return + + # make sure user's default project is legit before scoping to it + try: + default_project_ref = PROVIDERS.resource_api.get_project( + default_project_id) + default_project_domain_ref = PROVIDERS.resource_api.get_domain( + default_project_ref['domain_id']) + if (default_project_ref.get('enabled', True) and + default_project_domain_ref.get('enabled', True)): + if PROVIDERS.assignment_api.get_roles_for_user_and_project( + user_ref['id'], default_project_id): + auth_info.set_scope(project_id=default_project_id) + else: + msg = ("User %(user_id)s doesn't have access to" + " default project %(project_id)s. The token" + " will be unscoped rather than scoped to the" + " project.") + LOG.debug(msg, + {'user_id': user_ref['id'], + 'project_id': default_project_id}) + else: + msg = ("User %(user_id)s's default project %(project_id)s" + " is disabled. The token will be unscoped rather" + " than scoped to the project.") + LOG.debug(msg, + {'user_id': user_ref['id'], + 'project_id': default_project_id}) + except (exception.ProjectNotFound, exception.DomainNotFound): + # default project or default project domain doesn't exist, + # will issue unscoped token instead + msg = ("User %(user_id)s's default project %(project_id)s not" + " found. The token will be unscoped rather than" + " scoped to the project.") + LOG.debug(msg, {'user_id': user_ref['id'], + 'project_id': default_project_id}) + + +def authenticate(auth_info, auth_context): + """Authenticate user.""" + # NOTE(notmorgan): This is not super pythonic, but we lean on the + # __setitem__ method in auth_context to handle edge cases and security + # of the attributes set by the plugins. This check to ensure + # `auth_context` is an instance of AuthContext is extra insurance and + # will prevent regressions. + + if not isinstance(auth_context, core.AuthContext): + LOG.error( + '`auth_context` passed to the Auth controller ' + '`authenticate` method is not of type ' + '`keystone.auth.core.AuthContext`. For security ' + 'purposes this is required. This is likely a programming ' + 'error. Received object of type `%s`', type(auth_context)) + raise exception.Unauthorized( + _('Cannot Authenticate due to internal error.')) + # The 'external' method allows any 'REMOTE_USER' based authentication + # In some cases the server can set REMOTE_USER as '' instead of + # dropping it, so this must be filtered out + if flask.request.remote_user: + try: + external = core.get_auth_method('external') + resp = external.authenticate(auth_info) + if resp and resp.status: + # NOTE(notmorgan): ``external`` plugin cannot be multi-step + # it is either a plain success/fail. + auth_context.setdefault( + 'method_names', []).insert(0, 'external') + # NOTE(notmorgan): All updates to auth_context is handled + # here in the .authenticate method. + auth_context.update(resp.response_data or {}) + + except exception.AuthMethodNotSupported: + # This will happen there is no 'external' plugin registered + # and the container is performing authentication. + # The 'kerberos' and 'saml' methods will be used this way. + # In those cases, it is correct to not register an + # 'external' plugin; if there is both an 'external' and a + # 'kerberos' plugin, it would run the check on identity twice. + LOG.debug("No 'external' plugin is registered.") + except exception.Unauthorized: + # If external fails then continue and attempt to determine + # user identity using remaining auth methods + LOG.debug("Authorization failed for 'external' auth method.") + + # need to aggregate the results in case two or more methods + # are specified + auth_response = {'methods': []} + for method_name in auth_info.get_method_names(): + method = core.get_auth_method(method_name) + resp = method.authenticate(auth_info.get_method_data(method_name)) + if resp: + if resp.status: + auth_context.setdefault( + 'method_names', []).insert(0, method_name) + # NOTE(notmorgan): All updates to auth_context is handled + # here in the .authenticate method. If the auth attempt was + # not successful do not update the auth_context + resp_method_names = resp.response_data.pop( + 'method_names', []) + auth_context['method_names'].extend(resp_method_names) + auth_context.update(resp.response_data or {}) + elif resp.response_body: + auth_response['methods'].append(method_name) + auth_response[method_name] = resp.response_body + + if auth_response["methods"]: + # authentication continuation required + raise exception.AdditionalAuthRequired(auth_response) + + if 'user_id' not in auth_context: + msg = 'User not found by auth plugin; authentication failed' + tr_msg = _('User not found by auth plugin; authentication failed') + LOG.warning(msg) + raise exception.Unauthorized(tr_msg) + + +def authenticate_for_token(auth=None): + """Authenticate user and issue a token.""" + try: + auth_info = core.AuthInfo.create(auth=auth) + auth_context = core.AuthContext(method_names=[], + bind={}) + authenticate(auth_info, auth_context) + if auth_context.get('access_token_id'): + auth_info.set_scope(None, auth_context['project_id'], None) + _check_and_set_default_scoping(auth_info, auth_context) + (domain_id, project_id, trust, unscoped, system) = ( + auth_info.get_scope() + ) + trust_id = trust.get('id') if trust else None + + # NOTE(notmorgan): only methods that actually run and succeed will + # be in the auth_context['method_names'] list. Do not blindly take + # the values from auth_info, look at the authoritative values. Make + # sure the set is unique. + method_names_set = set(auth_context.get('method_names', [])) + method_names = list(method_names_set) + + app_cred_id = None + if 'application_credential' in method_names: + token_auth = auth_info.auth['identity'] + app_cred_id = token_auth['application_credential']['id'] + + # Do MFA Rule Validation for the user + if not core.UserMFARulesValidator.check_auth_methods_against_rules( + auth_context['user_id'], method_names_set): + raise exception.InsufficientAuthMethods( + user_id=auth_context['user_id'], + methods='[%s]' % ','.join(auth_info.get_method_names())) + + expires_at = auth_context.get('expires_at') + token_audit_id = auth_context.get('audit_id') + + token = PROVIDERS.token_provider_api.issue_token( + auth_context['user_id'], method_names, expires_at=expires_at, + system=system, project_id=project_id, domain_id=domain_id, + auth_context=auth_context, trust_id=trust_id, + app_cred_id=app_cred_id, parent_audit_id=token_audit_id) + + # NOTE(wanghong): We consume a trust use only when we are using + # trusts and have successfully issued a token. + if trust: + PROVIDERS.trust_api.consume_use(token.trust_id) + + return token + except exception.TrustNotFound as e: + LOG.warning(six.text_type(e)) + raise exception.Unauthorized(e) + + +def federated_authenticate_for_token(identity_provider, protocol_id): + auth = { + 'identity': { + 'methods': [protocol_id], + protocol_id: { + 'identity_provider': identity_provider, + 'protocol': protocol_id + } + } + } + return authenticate_for_token(auth) diff --git a/keystone/api/_shared/json_home_relations.py b/keystone/api/_shared/json_home_relations.py index 1065b558ce..a1d809fe6e 100644 --- a/keystone/api/_shared/json_home_relations.py +++ b/keystone/api/_shared/json_home_relations.py @@ -70,3 +70,8 @@ os_federation_parameter_rel_func = functools.partial( os_inherit_resource_rel_func = functools.partial( json_home.build_v3_extension_resource_relation, extension_name='OS-INHERIT', extension_version='1.0') + +# OS-PKI (revoked) "extension" +os_pki_resource_rel_func = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-PKI', extension_version='1.0') diff --git a/keystone/api/auth.py b/keystone/api/auth.py new file mode 100644 index 0000000000..9c5e05dd9e --- /dev/null +++ b/keystone/api/auth.py @@ -0,0 +1,578 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# This file handles all flask-restful resources for /v3/auth +import string + +import flask +import flask_restful +from oslo_log import log +from oslo_serialization import jsonutils +from oslo_utils import strutils +from six.moves import http_client +from six.moves import urllib +import werkzeug.exceptions + +from keystone.api._shared import authentication +from keystone.api._shared import json_home_relations +from keystone.auth import schema as auth_schema +from keystone.common import authorization +from keystone.common import provider_api +from keystone.common import rbac_enforcer +from keystone.common import render_token +from keystone.common import utils as k_utils +from keystone.common import validation +import keystone.conf +from keystone import exception +from keystone.federation import idp as keystone_idp +from keystone.federation import schema as federation_schema +from keystone.federation import utils as federation_utils +from keystone.i18n import _ +from keystone.server import flask as ks_flask + + +CONF = keystone.conf.CONF +ENFORCER = rbac_enforcer.RBACEnforcer +LOG = log.getLogger(__name__) +PROVIDERS = provider_api.ProviderAPIs + + +def _combine_lists_uniquely(a, b): + # it's most likely that only one of these will be filled so avoid + # the combination if possible. + if a and b: + return {x['id']: x for x in a + b}.values() + else: + return a or b + + +def _create_base_saml_assertion(auth): + issuer = CONF.saml.idp_entity_id + sp_id = auth['scope']['service_provider']['id'] + service_provider = PROVIDERS.federation_api.get_sp(sp_id) + federation_utils.assert_enabled_service_provider_object(service_provider) + sp_url = service_provider['sp_url'] + + token_id = auth['identity']['token']['id'] + token = PROVIDERS.token_provider_api.validate_token(token_id) + + if not token.project_scoped: + action = _('Use a project scoped token when attempting to create ' + 'a SAML assertion') + raise exception.ForbiddenAction(action=action) + + subject = token.user['name'] + role_names = [] + for role in token.roles: + role_names.append(role['name']) + project = token.project['name'] + # NOTE(rodrigods): the domain name is necessary in order to distinguish + # between projects and users with the same name in different domains. + project_domain_name = token.project_domain['name'] + subject_domain_name = token.user_domain['name'] + + generator = keystone_idp.SAMLGenerator() + response = generator.samlize_token( + issuer, sp_url, subject, subject_domain_name, + role_names, project, project_domain_name) + return response, service_provider + + +def _build_response_headers(service_provider): + # URLs in header are encoded into bytes + return [('Content-Type', 'text/xml'), + ('X-sp-url', service_provider['sp_url'].encode('utf-8')), + ('X-auth-url', service_provider['auth_url'].encode('utf-8'))] + + +def _get_sso_origin_host(): + """Validate and return originating dashboard URL. + + Make sure the parameter is specified in the request's URL as well its + value belongs to a list of trusted dashboards. + + :raises keystone.exception.ValidationError: ``origin`` query parameter + was not specified. The URL is deemed invalid. + :raises keystone.exception.Unauthorized: URL specified in origin query + parameter does not exist in list of websso trusted dashboards. + :returns: URL with the originating dashboard + + """ + origin = flask.request.args.get('origin') + + if not origin: + msg = 'Request must have an origin query parameter' + tr_msg = _('Request must have an origin query parameter') + LOG.error(msg) + raise exception.ValidationError(tr_msg) + + host = urllib.parse.unquote_plus(origin) + + # change trusted_dashboard hostnames to lowercase before comparison + trusted_dashboards = [k_utils.lower_case_hostname(trusted) + for trusted in CONF.federation.trusted_dashboard] + + if host not in trusted_dashboards: + msg = '%(host)s is not a trusted dashboard host' % {'host': host} + tr_msg = _('%(host)s is not a trusted dashboard host') % { + 'host': host} + LOG.error(msg) + raise exception.Unauthorized(tr_msg) + + return host + + +class _AuthFederationWebSSOBase(ks_flask.ResourceBase): + collection_key = '__UNUSED__' + member_key = '__UNUSED__' + + @staticmethod + def _render_template_response(host, token_id): + with open(CONF.federation.sso_callback_template) as template: + src = string.Template(template.read()) + subs = {'host': host, 'token': token_id} + body = src.substitute(subs) + resp = flask.make_response(body, http_client.OK) + resp.charset = 'utf-8' + resp.headers['Content-Type'] = 'text/html' + return resp + + +class AuthProjectsResource(ks_flask.ResourceBase): + collection_key = 'projects' + member_key = 'project' + + def get(self): + """Get possible project scopes for token. + + GET/HEAD /v3/auth/projects + GET/HEAD /v3/OS-FEDERATION/projects + """ + ENFORCER.enforce_call(action='identity:get_auth_projects') + user_id = self.auth_context.get('user_id') + group_ids = self.auth_context.get('group_ids') + + user_p_refs = [] + grp_p_refs = [] + + if user_id: + try: + user_p_refs = PROVIDERS.assignment_api.list_projects_for_user( + user_id) + except exception.UserNotFound: # nosec + # federated users have an id but they don't link to anything + pass + + if group_ids: + grp_p_refs = PROVIDERS.assignment_api.list_projects_for_groups( + group_ids) + refs = _combine_lists_uniquely(user_p_refs, grp_p_refs) + return self.wrap_collection(refs) + + +class AuthDomainsResource(ks_flask.ResourceBase): + collection_key = 'domains' + member_key = 'domain' + + def get(self): + """Get possible domain scopes for token. + + GET/HEAD /v3/auth/domains + GET/HEAD /v3/OS-FEDERATION/domains + """ + ENFORCER.enforce_call(action='identity:get_auth_domains') + user_id = self.auth_context.get('user_id') + group_ids = self.auth_context.get('group_ids') + + user_d_refs = [] + grp_d_refs = [] + + if user_id: + try: + user_d_refs = PROVIDERS.assignment_api.list_domains_for_user( + user_id) + except exception.UserNotFound: # nosec + # federated users have an id but they don't link to anything + pass + + if group_ids: + grp_d_refs = PROVIDERS.assignment_api.list_domains_for_groups( + group_ids) + + refs = _combine_lists_uniquely(user_d_refs, grp_d_refs) + return self.wrap_collection(refs) + + +class AuthSystemResource(_AuthFederationWebSSOBase): + def get(self): + """Get possible system scopes for token. + + GET/HEAD /v3/auth/system + """ + ENFORCER.enforce_call(action='identity:get_auth_system') + user_id = self.auth_context.get('user_id') + group_ids = self.auth_context.get('group_ids') + + user_assignments = [] + group_assignments = [] + + if user_id: + try: + user_assignments = ( + PROVIDERS.assignment_api.list_system_grants_for_user( + user_id) + ) + except exception.UserNotFound: # nosec + # federated users have an id but they don't link to anything + pass + + if group_ids: + group_assignments = ( + PROVIDERS.assignment_api.list_system_grants_for_groups( + group_ids) + ) + + assignments = _combine_lists_uniquely( + user_assignments, group_assignments) + + if assignments: + response = { + 'system': [{'all': True}], + 'links': { + 'self': ks_flask.base_url(path='auth/system') + } + } + else: + response = { + 'system': [], + 'links': { + 'self': ks_flask.base_url(path='auth/system') + } + } + return response + + +class AuthCatalogResource(_AuthFederationWebSSOBase): + def get(self): + """Get service catalog for token. + + GET/HEAD /v3/auth/catalog + """ + ENFORCER.enforce_call(action='identity:get_auth_catalog') + user_id = self.auth_context.get('user_id') + project_id = self.auth_context.get('project_id') + + if not project_id: + raise exception.Forbidden( + _('A project-scoped token is required to produce a ' + 'service catalog.')) + + return { + 'catalog': PROVIDERS.catalog_api.get_v3_catalog( + user_id, project_id + ), + 'links': { + 'self': ks_flask.base_url(path='auth/catalog') + } + } + + +class AuthTokenOSPKIResource(flask_restful.Resource): + @ks_flask.unenforced_api + def get(self): + """Deprecated; get revoked token list. + + GET/HEAD /v3/auth/tokens/OS-PKI/revoked + """ + if not CONF.token.revoke_by_id: + raise exception.Gone() + # NOTE(lbragstad): This API is deprecated and isn't supported. Keystone + # also doesn't store tokens, so returning a list of revoked tokens + # would require keystone to write invalid tokens to disk, which defeats + # the purpose. Return a 403 instead of removing the API altogether. + raise exception.Forbidden() + + +class AuthTokenResource(_AuthFederationWebSSOBase): + def get(self): + """Validate a token. + + HEAD/GET /v3/auth/tokens + """ + # TODO(morgan): eliminate the check_token action only use validate + # NOTE(morgan): Well lookie here, we have different enforcements + # for no good reason (historical), because the methods previouslly + # had to be named different names. Check which method and do the + # correct enforcement. + if flask.request.method == 'HEAD': + ENFORCER.enforce_call(action='identity:check_token') + else: + ENFORCER.enforce_call(action='identity:validate_token') + + token_id = flask.request.headers.get( + authorization.SUBJECT_TOKEN_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_resp = render_token.render_token_response_from_model( + token, include_catalog=include_catalog) + resp_body = jsonutils.dumps(token_resp) + response = flask.make_response(resp_body, http_client.OK) + response.headers['X-Subject-Token'] = token_id + response.headers['Content-Type'] = 'application/json' + return response + + @ks_flask.unenforced_api + def post(self): + """Issue a token. + + POST /v3/auth/tokens + """ + include_catalog = 'nocatalog' not in flask.request.args + auth_data = self.request_body_json.get('auth') + auth_schema.validate_issue_token_auth(auth_data) + token = authentication.authenticate_for_token(auth_data) + resp_data = render_token.render_token_response_from_model( + token, include_catalog=include_catalog + ) + resp_body = jsonutils.dumps(resp_data) + response = flask.make_response(resp_body, http_client.CREATED) + response.headers['X-Subject-Token'] = token.id + response.headers['Content-Type'] = 'application/json' + return response + + def delete(self): + """Revoke a token. + + DELETE /v3/auth/tokens + """ + ENFORCER.enforce_call(action='identity:revoke_token') + token_id = flask.request.headers.get( + authorization.SUBJECT_TOKEN_HEADER) + PROVIDERS.token_provider_api.revoke_token(token_id) + return None, http_client.NO_CONTENT + + +class AuthFederationWebSSOResource(_AuthFederationWebSSOBase): + @classmethod + def _perform_auth(cls, protocol_id): + try: + remote_id_name = federation_utils.get_remote_id_parameter( + protocol_id) + remote_id = flask.request.environ[remote_id_name] + except KeyError: + msg = 'Missing entity ID from environment' + tr_msg = _('Missing entity ID from environment') + LOG.error(msg) + raise exception.Unauthorized(tr_msg) + + host = _get_sso_origin_host() + ref = PROVIDERS.federation_api.get_idp_from_remote_id(remote_id) + identity_provider = ref['idp_id'] + token = authentication.federated_authenticate_for_token( + identity_provider=identity_provider, protocol_id=protocol_id) + return cls._render_template_response(host, token.id) + + @ks_flask.unenforced_api + def get(self, protocol_id): + return self._perform_auth(protocol_id) + + @ks_flask.unenforced_api + def post(self, protocol_id): + return self._perform_auth(protocol_id) + + +class AuthFederationWebSSOIDPsResource(_AuthFederationWebSSOBase): + @classmethod + def _perform_auth(cls, idp_id, protocol_id): + host = _get_sso_origin_host() + + token = authentication.federated_authenticate_for_token( + identity_provider=idp_id, protocol_id=protocol_id) + return cls._render_template_response(host, token.id) + + @ks_flask.unenforced_api + def get(self, idp_id, protocol_id): + return self._perform_auth(idp_id, protocol_id) + + @ks_flask.unenforced_api + def post(self, idp_id, protocol_id): + return self._perform_auth(idp_id, protocol_id) + + +class AuthFederationSaml2Resource(_AuthFederationWebSSOBase): + def get(self): + raise werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST']) + + @ks_flask.unenforced_api + def post(self): + """Exchange a scoped token for a SAML assertion. + + POST /v3/auth/OS-FEDERATION/saml2 + """ + auth = self.request_body_json.get('auth') + validation.lazy_validate(federation_schema.saml_create, auth) + response, service_provider = _create_base_saml_assertion(auth) + headers = _build_response_headers(service_provider) + response = flask.make_response(response.to_string(), http_client.OK) + for header, value in headers: + response.headers[header] = value + return response + + +class AuthFederationSaml2ECPResource(_AuthFederationWebSSOBase): + def get(self): + raise werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST']) + + @ks_flask.unenforced_api + def post(self): + """Exchange a scoped token for an ECP assertion. + + POST /v3/auth/OS-FEDERATION/saml2/ecp + """ + auth = self.request_body_json.get('auth') + validation.lazy_validate(federation_schema.saml_create, auth) + saml_assertion, service_provider = _create_base_saml_assertion(auth) + relay_state_prefix = service_provider['relay_state_prefix'] + + generator = keystone_idp.ECPGenerator() + ecp_assertion = generator.generate_ecp( + saml_assertion, relay_state_prefix) + headers = _build_response_headers(service_provider) + response = flask.make_response( + ecp_assertion.to_string(), http_client.OK) + for header, value in headers: + response.headers[header] = value + return response + + +class AuthAPI(ks_flask.APIBase): + _name = 'auth' + _import_name = __name__ + resources = [] + resource_mapping = [ + ks_flask.construct_resource_map( + resource=AuthProjectsResource, + url='/auth/projects', + alternate_urls=[dict( + url='/OS-FEDERATION/projects', + json_home=ks_flask.construct_json_home_data( + rel='projects', + resource_relation_func=( + json_home_relations.os_federation_resource_rel_func) + ) + )], + + rel='auth_projects', + resource_kwargs={} + ), + ks_flask.construct_resource_map( + resource=AuthDomainsResource, + url='/auth/domains', + alternate_urls=[dict( + url='/OS-FEDERATION/domains', + json_home=ks_flask.construct_json_home_data( + rel='domains', + resource_relation_func=( + json_home_relations.os_federation_resource_rel_func) + ) + )], + rel='auth_domains', + resource_kwargs={}, + ), + ks_flask.construct_resource_map( + resource=AuthSystemResource, + url='/auth/system', + resource_kwargs={}, + rel='auth_system' + ), + ks_flask.construct_resource_map( + resource=AuthCatalogResource, + url='/auth/catalog', + resource_kwargs={}, + rel='auth_catalog' + ), + ks_flask.construct_resource_map( + resource=AuthTokenOSPKIResource, + url='/auth/tokens/OS-PKI/revoked', + resource_kwargs={}, + rel='revocations', + resource_relation_func=json_home_relations.os_pki_resource_rel_func + ), + ks_flask.construct_resource_map( + resource=AuthTokenResource, + url='/auth/tokens', + resource_kwargs={}, + rel='auth_tokens' + ) + ] + + +class AuthFederationAPI(ks_flask.APIBase): + _name = 'auth/OS-FEDERATION' + _import_name = __name__ + resources = [] + resource_mapping = [ + ks_flask.construct_resource_map( + resource=AuthFederationSaml2Resource, + url='/auth/OS-FEDERATION/saml2', + resource_kwargs={}, + resource_relation_func=( + json_home_relations.os_federation_resource_rel_func), + rel='saml2' + ), + ks_flask.construct_resource_map( + resource=AuthFederationSaml2ECPResource, + url='/auth/OS-FEDERATION/saml2/ecp', + resource_kwargs={}, + resource_relation_func=( + json_home_relations.os_federation_resource_rel_func), + rel='ecp' + ), + ks_flask.construct_resource_map( + resource=AuthFederationWebSSOResource, + url='/auth/OS-FEDERATION/websso/', + resource_kwargs={}, + rel='websso', + resource_relation_func=( + json_home_relations.os_federation_resource_rel_func), + path_vars={ + 'protocol_id': ( + json_home_relations.os_federation_parameter_rel_func( + parameter_name='protocol_id'))} + ), + ks_flask.construct_resource_map( + resource=AuthFederationWebSSOIDPsResource, + url=('/auth/OS-FEDERATION/identity_providers//' + 'protocols//websso'), + resource_kwargs={}, + rel='identity_providers_websso', + resource_relation_func=( + json_home_relations.os_federation_resource_rel_func), + path_vars={ + 'idp_id': ( + json_home_relations.os_federation_parameter_rel_func( + parameter_name='idp_id')), + 'protocol_id': ( + json_home_relations.os_federation_parameter_rel_func( + parameter_name='protocol_id'))} + ) + ] + + +APIs = ( + AuthAPI, + AuthFederationAPI, +) diff --git a/keystone/api/endpoints.py b/keystone/api/endpoints.py index 4b2242f817..a1b73dd379 100644 --- a/keystone/api/endpoints.py +++ b/keystone/api/endpoints.py @@ -23,6 +23,7 @@ from keystone.common import rbac_enforcer from keystone.common import utils from keystone.common import validation from keystone import exception +from keystone import notifications from keystone.server import flask as ks_flask @@ -65,7 +66,7 @@ class EndpointResource(ks_flask.ResourceBase): except exception.RegionNotFound: region = dict(id=endpoint['region_id']) PROVIDERS.catalog_api.create_region( - region, initiator=ks_flask.build_audit_initiator()) + region, initiator=notifications.build_audit_initiator()) return endpoint def _get_endpoint(self, endpoint_id): diff --git a/keystone/api/groups.py b/keystone/api/groups.py index a451c61a9b..fb1ab993a9 100644 --- a/keystone/api/groups.py +++ b/keystone/api/groups.py @@ -22,6 +22,7 @@ from keystone.common import rbac_enforcer from keystone.common import validation from keystone import exception from keystone.identity import schema +from keystone import notifications from keystone.server import flask as ks_flask @@ -163,7 +164,7 @@ class UserGroupCRUDResource(flask_restful.Resource): build_target=functools.partial(self._build_enforcement_target_attr, user_id, group_id)) PROVIDERS.identity_api.add_user_to_group( - user_id, group_id, initiator=ks_flask.build_audit_initiator()) + user_id, group_id, initiator=notifications.build_audit_initiator()) return None, http_client.NO_CONTENT def delete(self, group_id, user_id): @@ -176,7 +177,7 @@ class UserGroupCRUDResource(flask_restful.Resource): build_target=functools.partial(self._build_enforcement_target_attr, user_id, group_id)) PROVIDERS.identity_api.remove_user_from_group( - user_id, group_id, initiator=ks_flask.build_audit_initiator()) + user_id, group_id, initiator=notifications.build_audit_initiator()) return None, http_client.NO_CONTENT diff --git a/keystone/api/os_federation.py b/keystone/api/os_federation.py index dba74cae32..83c4ea8613 100644 --- a/keystone/api/os_federation.py +++ b/keystone/api/os_federation.py @@ -14,18 +14,17 @@ import flask import flask_restful -from oslo_log import versionutils +from oslo_serialization import jsonutils from six.moves import http_client +from keystone.api._shared import authentication from keystone.api._shared import json_home_relations -from keystone.common import authorization from keystone.common import provider_api from keystone.common import rbac_enforcer -from keystone.common import request +from keystone.common import render_token from keystone.common import validation import keystone.conf from keystone import exception -import keystone.federation.controllers from keystone.federation import schema from keystone.federation import utils from keystone.server import flask as ks_flask @@ -380,74 +379,6 @@ class ServiceProvidersResource(_ResourceBase): return None, http_client.NO_CONTENT -class OSFederationProjectResource(flask_restful.Resource): - @versionutils.deprecated(as_of=versionutils.deprecated.JUNO, - what='GET /v3/OS-FEDERATION/projects', - in_favor_of='GET /v3/auth/projects') - def get(self): - """Get projects for user. - - GET/HEAD /OS-FEDERATION/projects - """ - ENFORCER.enforce_call(action='identity:get_auth_projects') - # TODO(morgan): Make this method simply call the endpoint for - # /v3/auth/projects once auth is ported to flask. - auth_context = flask.request.environ.get( - authorization.AUTH_CONTEXT_ENV) - user_id = auth_context.get('user_id') - group_ids = auth_context.get('group_ids') - - user_refs = [] - if user_id: - try: - user_refs = PROVIDERS.assignment_api.list_projects_for_user( - user_id) - except exception.UserNotFound: # nosec - # federated users have an id but they don't link to anything - pass - group_refs = [] - if group_ids: - group_refs = PROVIDERS.assignment_api.list_projects_for_groups( - group_ids) - refs = _combine_lists_uniquely(user_refs, group_refs) - return ks_flask.ResourceBase.wrap_collection( - refs, collection_name='projects') - - -class OSFederationDomainResource(flask_restful.Resource): - @versionutils.deprecated(as_of=versionutils.deprecated.JUNO, - what='GET /v3/OS-FEDERATION/domains', - in_favor_of='GET /v3/auth/domains') - def get(self): - """Get domains for user. - - GET/HEAD /OS-FEDERATION/domains - """ - ENFORCER.enforce_call(action='identity:get_auth_domains') - # TODO(morgan): Make this method simply call the endpoint for - # /v3/auth/domains once auth is ported to flask. - auth_context = flask.request.environ.get( - authorization.AUTH_CONTEXT_ENV) - user_id = auth_context.get('user_id') - group_ids = auth_context.get('group_ids') - - user_refs = [] - if user_id: - try: - user_refs = PROVIDERS.assignment_api.list_domains_for_user( - user_id) - except exception.UserNotFound: # nosec - # federated users have an ide bu they don't link to anything - pass - group_refs = [] - if group_ids: - group_refs = PROVIDERS.assignment_api.list_domains_for_groups( - group_ids) - refs = _combine_lists_uniquely(user_refs, group_refs) - return ks_flask.ResourceBase.wrap_collection( - refs, collection_name='domains') - - class SAML2MetadataResource(flask_restful.Resource): @ks_flask.unenforced_api def get(self): @@ -468,11 +399,6 @@ class SAML2MetadataResource(flask_restful.Resource): class OSFederationAuthResource(flask_restful.Resource): - def _construct_webob_request(self): - # Build a fake(ish) webob request object from the flask request state - # to pass to the Auth Controller's authenticate_for_token. This is - # purely transitional code. - return request.Request(flask.request.environ) @ks_flask.unenforced_api def get(self, idp_id, protocol_id): @@ -493,12 +419,11 @@ class OSFederationAuthResource(flask_restful.Resource): return self._auth(idp_id, protocol_id) def _auth(self, idp_id, protocol_id): - """Build and pass auth data to auth controller. + """Build and pass auth data to authentication code. Build HTTP request body for federated authentication and inject it into the ``authenticate_for_token`` function. """ - compat_controller = keystone.federation.controllers.Auth() auth = { 'identity': { 'methods': [protocol_id], @@ -508,14 +433,12 @@ class OSFederationAuthResource(flask_restful.Resource): }, } } - # NOTE(morgan): for compatibility, make sure we use a webob request - # until /auth is ported to flask. Since this is a webob response, - # deconstruct it and turn it into a flask response. - webob_resp = compat_controller.authenticate_for_token( - self._construct_webob_request(), auth) - flask_resp = flask.make_response( - webob_resp.body, webob_resp.status_code) - flask_resp.headers.extend(webob_resp.headers.dict_of_lists()) + token = authentication.authenticate_for_token(auth) + token_data = render_token.render_token_response_from_model(token) + resp_data = jsonutils.dumps(token_data) + flask_resp = flask.make_response(resp_data, http_client.CREATED) + flask_resp.headers['X-Subject-Token'] = token.id + flask_resp.headers['Content-Type'] = 'application/json' return flask_resp @@ -525,18 +448,6 @@ class OSFederationAPI(ks_flask.APIBase): _api_url_prefix = '/OS-FEDERATION' resources = [] resource_mapping = [ - ks_flask.construct_resource_map( - # NOTE(morgan): No resource relation here, the resource relation is - # to /v3/auth/domains and /v3/auth/projects - resource=OSFederationDomainResource, - url='/domains', - resource_kwargs={}), - ks_flask.construct_resource_map( - # NOTE(morgan): No resource relation here, the resource relation is - # to /v3/auth/domains and /v3/auth/projects - resource=OSFederationProjectResource, - url='/projects', - resource_kwargs={}), ks_flask.construct_resource_map( resource=SAML2MetadataResource, url='/saml2/metadata', diff --git a/keystone/api/os_oauth1.py b/keystone/api/os_oauth1.py index da484592cb..86258292c5 100644 --- a/keystone/api/os_oauth1.py +++ b/keystone/api/os_oauth1.py @@ -174,7 +174,7 @@ class RequestTokenResource(_OAuth1ResourceBase): consumer_id, requested_project_id, request_token_duration, - initiator=ks_flask.build_audit_initiator()) + initiator=notifications.build_audit_initiator()) result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s' % {'key': token_ref['id'], @@ -266,7 +266,7 @@ class AccessTokenResource(_OAuth1ResourceBase): token_ref = PROVIDERS.oauth_api.create_access_token( request_token_id, access_token_duration, - initiator=ks_flask.build_audit_initiator()) + initiator=notifications.build_audit_initiator()) result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s' % {'key': token_ref['id'], diff --git a/keystone/application_credential/core.py b/keystone/application_credential/core.py index 8ee8c57426..31e9325019 100644 --- a/keystone/application_credential/core.py +++ b/keystone/application_credential/core.py @@ -101,7 +101,7 @@ class Manager(manager.Manager): roles.append(PROVIDERS.role_api.get_role(role['id'])) return roles - def authenticate(self, request, application_credential_id, secret): + def authenticate(self, application_credential_id, secret): """Authenticate with an application credential. :param str application_credential_id: Application Credential ID diff --git a/keystone/auth/__init__.py b/keystone/auth/__init__.py index 8477c63a65..f65774d94b 100644 --- a/keystone/auth/__init__.py +++ b/keystone/auth/__init__.py @@ -15,5 +15,3 @@ # NOTE(notmorgan): Be careful in adjusting whitespace here, flake8 checks # get cranky. from keystone.auth import core # noqa - -from keystone.auth import controllers # noqa diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py deleted file mode 100644 index b1872b725a..0000000000 --- a/keystone/auth/controllers.py +++ /dev/null @@ -1,454 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from oslo_log import log -import six - -from keystone.auth import core -from keystone.auth import schema -from keystone.common import authorization -from keystone.common import controller -from keystone.common import provider_api -from keystone.common import wsgi -import keystone.conf -from keystone import exception -from keystone.federation import constants -from keystone.i18n import _ -from keystone.resource import controllers as resource_controllers - - -LOG = log.getLogger(__name__) - -CONF = keystone.conf.CONF -PROVIDERS = provider_api.ProviderAPIs - - -class Auth(controller.V3Controller): - - # Note(atiwari): From V3 auth controller code we are - # calling protection() wrappers, so we need to setup - # the member_name and collection_name attributes of - # auth controller code. - # In the absence of these attributes, default 'entity' - # string will be used to represent the target which is - # generic. Policy can be defined using 'entity' but it - # would not reflect the exact entity that is in context. - # We are defining collection_name = 'tokens' and - # member_name = 'token' to facilitate policy decisions. - collection_name = 'tokens' - member_name = 'token' - - def __init__(self, *args, **kw): - super(Auth, self).__init__(*args, **kw) - keystone.conf.auth.setup_authentication() - self._mfa_rules_validator = core.UserMFARulesValidator() - - def authenticate_for_token(self, request, auth=None): - """Authenticate user and issue a token.""" - include_catalog = 'nocatalog' not in request.params - - schema.validate_issue_token_auth(auth) - - try: - auth_info = core.AuthInfo.create(auth=auth) - auth_context = core.AuthContext(method_names=[], - bind={}) - self.authenticate(request, auth_info, auth_context) - if auth_context.get('access_token_id'): - auth_info.set_scope(None, auth_context['project_id'], None) - self._check_and_set_default_scoping(auth_info, auth_context) - (domain_id, project_id, trust, unscoped, system) = ( - auth_info.get_scope() - ) - trust_id = trust.get('id') if trust else None - - # NOTE(notmorgan): only methods that actually run and succeed will - # be in the auth_context['method_names'] list. Do not blindly take - # the values from auth_info, look at the authoritative values. Make - # sure the set is unique. - method_names_set = set(auth_context.get('method_names', [])) - method_names = list(method_names_set) - - app_cred_id = None - if 'application_credential' in method_names: - token_auth = auth_info.auth['identity'] - app_cred_id = token_auth['application_credential']['id'] - - # Do MFA Rule Validation for the user - if not self._mfa_rules_validator.check_auth_methods_against_rules( - auth_context['user_id'], method_names_set): - raise exception.InsufficientAuthMethods( - user_id=auth_context['user_id'], - methods='[%s]' % ','.join(auth_info.get_method_names())) - - expires_at = auth_context.get('expires_at') - token_audit_id = auth_context.get('audit_id') - - token = PROVIDERS.token_provider_api.issue_token( - auth_context['user_id'], method_names, expires_at=expires_at, - system=system, project_id=project_id, domain_id=domain_id, - auth_context=auth_context, trust_id=trust_id, - app_cred_id=app_cred_id, parent_audit_id=token_audit_id) - token_reference = controller.render_token_response_from_model( - token, include_catalog=include_catalog - ) - - # NOTE(wanghong): We consume a trust use only when we are using - # trusts and have successfully issued a token. - if trust: - PROVIDERS.trust_api.consume_use(token.trust_id) - - return render_token_data_response(token.id, token_reference, - created=True) - except exception.TrustNotFound as e: - LOG.warning(six.text_type(e)) - raise exception.Unauthorized(e) - - def _check_and_set_default_scoping(self, auth_info, auth_context): - (domain_id, project_id, trust, unscoped, system) = ( - auth_info.get_scope() - ) - if trust: - project_id = trust['project_id'] - if system or domain_id or project_id or trust: - # scope is specified - return - - # Skip scoping when unscoped federated token is being issued - if constants.IDENTITY_PROVIDER in auth_context: - return - - # Do not scope if request is for explicitly unscoped token - if unscoped is not None: - return - - # fill in default_project_id if it is available - try: - user_ref = PROVIDERS.identity_api.get_user(auth_context['user_id']) - except exception.UserNotFound as e: - LOG.warning(six.text_type(e)) - raise exception.Unauthorized(e) - - default_project_id = user_ref.get('default_project_id') - if not default_project_id: - # User has no default project. He shall get an unscoped token. - return - - # make sure user's default project is legit before scoping to it - try: - default_project_ref = PROVIDERS.resource_api.get_project( - default_project_id) - default_project_domain_ref = PROVIDERS.resource_api.get_domain( - default_project_ref['domain_id']) - if (default_project_ref.get('enabled', True) and - default_project_domain_ref.get('enabled', True)): - if PROVIDERS.assignment_api.get_roles_for_user_and_project( - user_ref['id'], default_project_id): - auth_info.set_scope(project_id=default_project_id) - else: - msg = ("User %(user_id)s doesn't have access to" - " default project %(project_id)s. The token" - " will be unscoped rather than scoped to the" - " project.") - LOG.debug(msg, - {'user_id': user_ref['id'], - 'project_id': default_project_id}) - else: - msg = ("User %(user_id)s's default project %(project_id)s" - " is disabled. The token will be unscoped rather" - " than scoped to the project.") - LOG.debug(msg, - {'user_id': user_ref['id'], - 'project_id': default_project_id}) - except (exception.ProjectNotFound, exception.DomainNotFound): - # default project or default project domain doesn't exist, - # will issue unscoped token instead - msg = ("User %(user_id)s's default project %(project_id)s not" - " found. The token will be unscoped rather than" - " scoped to the project.") - LOG.debug(msg, {'user_id': user_ref['id'], - 'project_id': default_project_id}) - - def authenticate(self, request, auth_info, auth_context): - """Authenticate user.""" - # NOTE(notmorgan): This is not super pythonic, but we lean on the - # __setitem__ method in auth_context to handle edge cases and security - # of the attributes set by the plugins. This check to ensure - # `auth_context` is an instance of AuthContext is extra insurance and - # will prevent regressions. - - if not isinstance(auth_context, core.AuthContext): - LOG.error( - '`auth_context` passed to the Auth controller ' - '`authenticate` method is not of type ' - '`keystone.auth.core.AuthContext`. For security ' - 'purposes this is required. This is likely a programming ' - 'error. Received object of type `%s`', type(auth_context)) - raise exception.Unauthorized( - _('Cannot Authenticate due to internal error.')) - # The 'external' method allows any 'REMOTE_USER' based authentication - # In some cases the server can set REMOTE_USER as '' instead of - # dropping it, so this must be filtered out - if request.remote_user: - try: - external = core.get_auth_method('external') - resp = external.authenticate(request, - auth_info) - if resp and resp.status: - # NOTE(notmorgan): ``external`` plugin cannot be multi-step - # it is either a plain success/fail. - auth_context.setdefault( - 'method_names', []).insert(0, 'external') - # NOTE(notmorgan): All updates to auth_context is handled - # here in the .authenticate method. - auth_context.update(resp.response_data or {}) - - except exception.AuthMethodNotSupported: - # This will happen there is no 'external' plugin registered - # and the container is performing authentication. - # The 'kerberos' and 'saml' methods will be used this way. - # In those cases, it is correct to not register an - # 'external' plugin; if there is both an 'external' and a - # 'kerberos' plugin, it would run the check on identity twice. - LOG.debug("No 'external' plugin is registered.") - except exception.Unauthorized: - # If external fails then continue and attempt to determine - # user identity using remaining auth methods - LOG.debug("Authorization failed for 'external' auth method.") - - # need to aggregate the results in case two or more methods - # are specified - auth_response = {'methods': []} - for method_name in auth_info.get_method_names(): - method = core.get_auth_method(method_name) - resp = method.authenticate(request, - auth_info.get_method_data(method_name)) - if resp: - if resp.status: - auth_context.setdefault( - 'method_names', []).insert(0, method_name) - # NOTE(notmorgan): All updates to auth_context is handled - # here in the .authenticate method. If the auth attempt was - # not successful do not update the auth_context - resp_method_names = resp.response_data.pop( - 'method_names', []) - auth_context['method_names'].extend(resp_method_names) - auth_context.update(resp.response_data or {}) - elif resp.response_body: - auth_response['methods'].append(method_name) - auth_response[method_name] = resp.response_body - - if auth_response["methods"]: - # authentication continuation required - raise exception.AdditionalAuthRequired(auth_response) - - if 'user_id' not in auth_context: - msg = 'User not found by auth plugin; authentication failed' - tr_msg = _('User not found by auth plugin; authentication failed') - LOG.warning(msg) - raise exception.Unauthorized(tr_msg) - - @controller.protected() - def check_token(self, request): - token_id = request.subject_token - window_seconds = authorization.token_validation_window(request) - include_catalog = 'nocatalog' not in request.params - token = PROVIDERS.token_provider_api.validate_token( - token_id, window_seconds=window_seconds) - token_reference = controller.render_token_response_from_model( - token, include_catalog=include_catalog - ) - # NOTE(morganfainberg): The code in - # ``keystone.common.wsgi.render_response`` will remove the content - # body. - - return render_token_data_response(token.id, token_reference) - - @controller.protected() - def revoke_token(self, request): - return PROVIDERS.token_provider_api.revoke_token(request.subject_token) - - @controller.protected() - def validate_token(self, request): - token_id = request.subject_token - window_seconds = authorization.token_validation_window(request) - include_catalog = 'nocatalog' not in request.params - - token = PROVIDERS.token_provider_api.validate_token( - token_id, window_seconds=window_seconds) - token_reference = controller.render_token_response_from_model( - token, include_catalog=include_catalog - ) - - return render_token_data_response(token.id, token_reference) - - def revocation_list(self, request): - if not CONF.token.revoke_by_id: - raise exception.Gone() - # NOTE(lbragstad): This API is deprecated and isn't supported. Keystone - # also doesn't store tokens, so returning a list of revoked tokens - # would require keystone to write invalid tokens to disk, which defeats - # the purpose. Return a 403 instead of removing the API all together. - # The alternative would be to return a signed response of just an empty - # list. - raise exception.Forbidden() - - def _combine_lists_uniquely(self, a, b): - # it's most likely that only one of these will be filled so avoid - # the combination if possible. - if a and b: - return {x['id']: x for x in a + b}.values() - else: - return a or b - - @controller.protected() - def get_auth_projects(self, request): - user_id = request.auth_context.get('user_id') - group_ids = request.auth_context.get('group_ids') - - user_refs = [] - if user_id: - try: - user_refs = PROVIDERS.assignment_api.list_projects_for_user( - user_id - ) - except exception.UserNotFound: # nosec - # federated users have an id but they don't link to anything - pass - - grp_refs = [] - if group_ids: - grp_refs = PROVIDERS.assignment_api.list_projects_for_groups( - group_ids - ) - - refs = self._combine_lists_uniquely(user_refs, grp_refs) - return resource_controllers.ProjectV3.wrap_collection( - request.context_dict, refs) - - @controller.protected() - def get_auth_domains(self, request): - user_id = request.auth_context.get('user_id') - group_ids = request.auth_context.get('group_ids') - - user_refs = [] - if user_id: - try: - user_refs = PROVIDERS.assignment_api.list_domains_for_user( - user_id - ) - except exception.UserNotFound: # nosec - # federated users have an id but they don't link to anything - pass - - grp_refs = [] - if group_ids: - grp_refs = PROVIDERS.assignment_api.list_domains_for_groups( - group_ids - ) - - refs = self._combine_lists_uniquely(user_refs, grp_refs) - return resource_controllers.DomainV3.wrap_collection( - request.context_dict, refs) - - @controller.protected() - def get_auth_system(self, request): - user_id = request.auth_context.get('user_id') - group_ids = request.auth_context.get('group_ids') - - user_assignments = [] - if user_id: - try: - user_assignments = ( - PROVIDERS.assignment_api.list_system_grants_for_user( - user_id - ) - ) - except exception.UserNotFound: # nosec - # federated users have an id but they don't link to anything - pass - - group_assignments = [] - if group_ids: - group_assignments = ( - PROVIDERS.assignment_api.list_system_grants_for_group( - group_ids - ) - ) - - assignments = self._combine_lists_uniquely( - user_assignments, group_assignments - ) - if assignments: - response = { - 'system': [{'all': True}], - 'links': { - 'self': self.base_url( - request.context_dict, path='auth/system' - ) - } - } - else: - response = { - 'system': [], - 'links': { - 'self': self.base_url( - request.context_dict, path='auth/system' - ) - } - } - return response - - @controller.protected() - def get_auth_catalog(self, request): - user_id = request.auth_context.get('user_id') - project_id = request.auth_context.get('project_id') - - if not project_id: - raise exception.Forbidden( - _('A project-scoped token is required to produce a service ' - 'catalog.')) - - # The V3Controller base methods mostly assume that you're returning - # either a collection or a single element from a collection, neither of - # which apply to the catalog. Because this is a special case, this - # re-implements a tiny bit of work done by the base controller (such as - # self-referential link building) to avoid overriding or refactoring - # several private methods. - return { - 'catalog': PROVIDERS.catalog_api.get_v3_catalog( - user_id, project_id - ), - 'links': {'self': self.base_url(request.context_dict, - path='auth/catalog')} - } - - -# FIXME(gyee): not sure if it belongs here or keystone.common. Park it here -# for now. -def render_token_data_response(token_id, token_data, created=False): - """Render token data HTTP response. - - Stash token ID into the X-Subject-Token header. - - """ - headers = [('X-Subject-Token', token_id)] - - if created: - status = (201, 'Created') - else: - status = (200, 'OK') - - return wsgi.render_response(body=token_data, - status=status, headers=headers) diff --git a/keystone/auth/core.py b/keystone/auth/core.py index 5fdbaab2ee..179f0fc1e2 100644 --- a/keystone/auth/core.py +++ b/keystone/auth/core.py @@ -414,13 +414,14 @@ class AuthInfo(provider_api.ProviderAPIMixin, object): class UserMFARulesValidator(provider_api.ProviderAPIMixin, object): """Helper object that can validate the MFA Rules.""" - @property - def _auth_methods(self): + @classmethod + def _auth_methods(cls): if AUTH_PLUGINS_LOADED: return set(AUTH_METHODS.keys()) raise RuntimeError(_('Auth Method Plugins are not loaded.')) - def check_auth_methods_against_rules(self, user_id, auth_methods): + @classmethod + def check_auth_methods_against_rules(cls, user_id, auth_methods): """Validate the MFA rules against the successful auth methods. :param user_id: The user's ID (uuid). @@ -434,7 +435,7 @@ class UserMFARulesValidator(provider_api.ProviderAPIMixin, object): mfa_rules = user_ref['options'].get(ro.MFA_RULES_OPT.option_name, []) mfa_rules_enabled = user_ref['options'].get( ro.MFA_ENABLED_OPT.option_name, True) - rules = self._parse_rule_structure(mfa_rules, user_ref['id']) + rules = cls._parse_rule_structure(mfa_rules, user_ref['id']) if not rules or not mfa_rules_enabled: # return quickly if the rules are disabled for the user or not set @@ -451,7 +452,7 @@ class UserMFARulesValidator(provider_api.ProviderAPIMixin, object): # disable an auth method, and a rule will still pass making it # impossible to accidently lock-out a subset of users with a # bad keystone.conf - r_set = set(r).intersection(self._auth_methods) + r_set = set(r).intersection(cls._auth_methods()) if set(auth_methods).issuperset(r_set): # Rule Matches no need to continue, return here. LOG.debug('Auth methods for user `%(user_id)s`, `%(methods)s` ' @@ -460,7 +461,7 @@ class UserMFARulesValidator(provider_api.ProviderAPIMixin, object): {'user_id': user_id, 'rule': list(r_set), 'methods': auth_methods, - 'loaded': self._auth_methods}) + 'loaded': cls._auth_methods()}) return True LOG.debug('Auth methods for user `%(user_id)s`, `%(methods)s` did not ' diff --git a/keystone/auth/plugins/application_credential.py b/keystone/auth/plugins/application_credential.py index a690c064fd..54a7af2557 100644 --- a/keystone/auth/plugins/application_credential.py +++ b/keystone/auth/plugins/application_credential.py @@ -23,7 +23,7 @@ METHOD_NAME = 'application_credential' class ApplicationCredential(base.AuthMethodHandler): - def authenticate(self, request, auth_payload): + def authenticate(self, auth_payload): """Authenticate an application.""" response_data = {} app_cred_info = auth_plugins.AppCredInfo.create(auth_payload, @@ -31,7 +31,6 @@ class ApplicationCredential(base.AuthMethodHandler): try: PROVIDERS.application_credential_api.authenticate( - request, application_credential_id=app_cred_info.id, secret=app_cred_info.secret) except AssertionError as e: diff --git a/keystone/auth/plugins/base.py b/keystone/auth/plugins/base.py index 4ae71c4525..92c1100e90 100644 --- a/keystone/auth/plugins/base.py +++ b/keystone/auth/plugins/base.py @@ -33,11 +33,9 @@ class AuthMethodHandler(provider_api.ProviderAPIMixin, object): pass @abc.abstractmethod - def authenticate(self, request, auth_payload): + def authenticate(self, auth_payload): """Authenticate user and return an authentication context. - :param request: context of an authentication request - :type request: common.request.Request :param auth_payload: the payload content of the authentication request for a given method :type auth_payload: dict diff --git a/keystone/auth/plugins/external.py b/keystone/auth/plugins/external.py index 6c094c4a4b..9d1351ada9 100644 --- a/keystone/auth/plugins/external.py +++ b/keystone/auth/plugins/external.py @@ -16,6 +16,7 @@ import abc +import flask import six from keystone.auth.plugins import base @@ -31,21 +32,21 @@ PROVIDERS = provider_api.ProviderAPIs @six.add_metaclass(abc.ABCMeta) class Base(base.AuthMethodHandler): - def authenticate(self, request, auth_payload,): + def authenticate(self, auth_payload): """Use REMOTE_USER to look up the user in the identity backend. The user_id from the actual user from the REMOTE_USER env variable is placed in the response_data. """ response_data = {} - if not request.remote_user: + if not flask.request.remote_user: msg = _('No authenticated user') raise exception.Unauthorized(msg) try: - user_ref = self._authenticate(request) + user_ref = self._authenticate() except Exception: - msg = _('Unable to lookup user %s') % request.remote_user + msg = _('Unable to lookup user %s') % flask.request.remote_user raise exception.Unauthorized(msg) response_data['user_id'] = user_ref['id'] @@ -53,7 +54,7 @@ class Base(base.AuthMethodHandler): response_data=response_data) @abc.abstractmethod - def _authenticate(self, request): + def _authenticate(self): """Look up the user in the identity backend. Return user_ref @@ -62,36 +63,35 @@ class Base(base.AuthMethodHandler): class DefaultDomain(Base): - def _authenticate(self, request): + def _authenticate(self): """Use remote_user to look up the user in the identity backend.""" return PROVIDERS.identity_api.get_user_by_name( - request.remote_user, + flask.request.remote_user, CONF.identity.default_domain_id) class Domain(Base): - def _authenticate(self, request): + def _authenticate(self): """Use remote_user to look up the user in the identity backend. The domain will be extracted from the REMOTE_DOMAIN environment variable if present. If not, the default domain will be used. """ - if request.remote_domain: - ref = PROVIDERS.resource_api.get_domain_by_name( - request.remote_domain - ) + remote_domain = flask.request.environ.get('REMOTE_DOMAIN') + if remote_domain: + ref = PROVIDERS.resource_api.get_domain_by_name(remote_domain) domain_id = ref['id'] else: domain_id = CONF.identity.default_domain_id - return PROVIDERS.identity_api.get_user_by_name(request.remote_user, - domain_id) + return PROVIDERS.identity_api.get_user_by_name( + flask.request.remote_user, domain_id) class KerberosDomain(Domain): """Allows `kerberos` as a method.""" - def _authenticate(self, request): - if request.auth_type != 'Negotiate': + def _authenticate(self): + if flask.request.environ.get('AUTH_TYPE') != 'Negotiate': raise exception.Unauthorized(_("auth_type is not Negotiate")) - return super(KerberosDomain, self)._authenticate(request) + return super(KerberosDomain, self)._authenticate() diff --git a/keystone/auth/plugins/mapped.py b/keystone/auth/plugins/mapped.py index 6a67f1c5a8..5f568a8190 100644 --- a/keystone/auth/plugins/mapped.py +++ b/keystone/auth/plugins/mapped.py @@ -13,6 +13,7 @@ import functools import uuid +import flask from oslo_log import log from pycadf import cadftaxonomy as taxonomy from six.moves.urllib import parse @@ -38,10 +39,9 @@ class Mapped(base.AuthMethodHandler): token_id = auth_payload['id'] return PROVIDERS.token_provider_api.validate_token(token_id) - def authenticate(self, request, auth_payload): + def authenticate(self, auth_payload): """Authenticate mapped user and set an authentication context. - :param request: keystone's request context :param auth_payload: the content of the authentication for a given method @@ -52,13 +52,11 @@ class Mapped(base.AuthMethodHandler): """ if 'id' in auth_payload: token_ref = self._get_token_ref(auth_payload) - response_data = handle_scoped_token(request, - token_ref, + response_data = handle_scoped_token(token_ref, PROVIDERS.federation_api, PROVIDERS.identity_api) else: - response_data = handle_unscoped_token(request, - auth_payload, + response_data = handle_unscoped_token(auth_payload, PROVIDERS.resource_api, PROVIDERS.federation_api, PROVIDERS.identity_api, @@ -69,7 +67,7 @@ class Mapped(base.AuthMethodHandler): response_data=response_data) -def handle_scoped_token(request, token, federation_api, identity_api): +def handle_scoped_token(token, federation_api, identity_api): response_data = {} utils.validate_expiration(token) token_audit_id = token.audit_id @@ -81,7 +79,7 @@ def handle_scoped_token(request, token, federation_api, identity_api): group_ids.append(group_dict['id']) send_notification = functools.partial( notifications.send_saml_audit_notification, 'authenticate', - request, user_id, group_ids, identity_provider, protocol, + user_id, group_ids, identity_provider, protocol, token_audit_id) utils.assert_enabled_identity_provider(federation_api, identity_provider) @@ -108,7 +106,7 @@ def handle_scoped_token(request, token, federation_api, identity_api): return response_data -def handle_unscoped_token(request, auth_payload, resource_api, federation_api, +def handle_unscoped_token(auth_payload, resource_api, federation_api, identity_api, assignment_api, role_api): def validate_shadow_mapping(shadow_projects, existing_roles, idp_domain_id, @@ -199,7 +197,7 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api, return resp - assertion = extract_assertion_data(request) + assertion = extract_assertion_data() try: identity_provider = auth_payload['identity_provider'] except KeyError: @@ -234,7 +232,7 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api, if is_ephemeral_user(mapped_properties): unique_id, display_name = ( - get_user_unique_id_and_display_name(request, mapped_properties) + get_user_unique_id_and_display_name(mapped_properties) ) email = mapped_properties['user'].get('email') user = identity_api.shadow_federated_user(identity_provider, @@ -282,7 +280,6 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api, # after sending the notification outcome = taxonomy.OUTCOME_FAILURE notifications.send_saml_audit_notification('authenticate', - request, user_id, group_ids, identity_provider, protocol, token_id, @@ -291,7 +288,6 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api, else: outcome = taxonomy.OUTCOME_SUCCESS notifications.send_saml_audit_notification('authenticate', - request, user_id, group_ids, identity_provider, protocol, token_id, @@ -300,8 +296,8 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api, return response_data -def extract_assertion_data(request): - assertion = dict(utils.get_assertion_params_from_env(request)) +def extract_assertion_data(): + assertion = dict(utils.get_assertion_params_from_env()) return assertion @@ -329,7 +325,7 @@ def apply_mapping_filter(identity_provider, protocol, assertion, return mapped_properties, mapping_id -def get_user_unique_id_and_display_name(request, mapped_properties): +def get_user_unique_id_and_display_name(mapped_properties): """Setup federated username. Function covers all the cases for properly setting user id, a primary @@ -345,7 +341,6 @@ def get_user_unique_id_and_display_name(request, mapped_properties): 3) If user_id is not set and user_name is, set user_id as url safe version of user_name. - :param request: current request object :param mapped_properties: Properties issued by a RuleProcessor. :type: dictionary @@ -358,7 +353,7 @@ def get_user_unique_id_and_display_name(request, mapped_properties): user = mapped_properties['user'] user_id = user.get('id') - user_name = user.get('name') or request.remote_user + user_name = user.get('name') or flask.request.remote_user if not any([user_id, user_name]): msg = _("Could not map user while setting ephemeral user identity. " diff --git a/keystone/auth/plugins/oauth1.py b/keystone/auth/plugins/oauth1.py index 81d0d89794..3bb05179a5 100644 --- a/keystone/auth/plugins/oauth1.py +++ b/keystone/auth/plugins/oauth1.py @@ -12,25 +12,26 @@ # License for the specific language governing permissions and limitations # under the License. +import flask from oslo_utils import timeutils from keystone.auth.plugins import base -from keystone.common import controller from keystone.common import provider_api from keystone import exception from keystone.i18n import _ from keystone.oauth1 import core as oauth from keystone.oauth1 import validator +from keystone.server import flask as ks_flask PROVIDERS = provider_api.ProviderAPIs class OAuth(base.AuthMethodHandler): - def authenticate(self, request, auth_payload): + def authenticate(self, auth_payload): """Turn a signed request with an access key into a keystone token.""" response_data = {} - oauth_headers = oauth.get_oauth_headers(request.headers) + oauth_headers = oauth.get_oauth_headers(flask.request.headers) access_token_id = oauth_headers.get('oauth_token') if not access_token_id: @@ -47,16 +48,15 @@ class OAuth(base.AuthMethodHandler): if now > expires: raise exception.Unauthorized(_('Access token is expired')) - url = controller.V3Controller.base_url(request.context_dict, - request.path_info) + url = ks_flask.base_url(path=flask.request.path) access_verifier = oauth.ResourceEndpoint( request_validator=validator.OAuthValidator(), token_generator=oauth.token_generator) result, request = access_verifier.validate_protected_resource_request( url, http_method='POST', - body=request.params, - headers=request.headers, + body=flask.request.args, + headers=flask.request.headers, realms=None ) if not result: diff --git a/keystone/auth/plugins/password.py b/keystone/auth/plugins/password.py index ad99ce4713..13f2949272 100644 --- a/keystone/auth/plugins/password.py +++ b/keystone/auth/plugins/password.py @@ -25,14 +25,13 @@ PROVIDERS = provider_api.ProviderAPIs class Password(base.AuthMethodHandler): - def authenticate(self, request, auth_payload): + def authenticate(self, auth_payload): """Try to authenticate against the identity backend.""" response_data = {} user_info = auth_plugins.UserAuthInfo.create(auth_payload, METHOD_NAME) try: PROVIDERS.identity_api.authenticate( - request, user_id=user_info.user_id, password=user_info.password) except AssertionError: diff --git a/keystone/auth/plugins/token.py b/keystone/auth/plugins/token.py index b6d630b58e..dc82f9c155 100644 --- a/keystone/auth/plugins/token.py +++ b/keystone/auth/plugins/token.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import flask from oslo_log import log import six @@ -35,18 +36,18 @@ class Token(base.AuthMethodHandler): token_id = auth_payload['id'] return PROVIDERS.token_provider_api.validate_token(token_id) - def authenticate(self, request, auth_payload): + def authenticate(self, auth_payload): if 'id' not in auth_payload: raise exception.ValidationError(attribute='id', target='token') token = self._get_token_ref(auth_payload) if token.is_federated and PROVIDERS.federation_api: response_data = mapped.handle_scoped_token( - request, token, PROVIDERS.federation_api, + token, PROVIDERS.federation_api, PROVIDERS.identity_api ) else: - response_data = token_authenticate(request, token) + response_data = token_authenticate(token) # NOTE(notmorgan): The Token auth method is *very* special and sets the # previous values to the method_names. This is because it can be used @@ -58,7 +59,7 @@ class Token(base.AuthMethodHandler): response_data=response_data) -def token_authenticate(request, token): +def token_authenticate(token): response_data = {} try: @@ -67,10 +68,11 @@ def token_authenticate(request, token): # state in Keystone. To do so is to invite elevation of # privilege attacks - project_scoped = 'project' in request.json_body['auth'].get( + json_body = flask.request.get_json(silent=True, force=True) or {} + project_scoped = 'project' in json_body['auth'].get( 'scope', {} ) - domain_scoped = 'domain' in request.json_body['auth'].get( + domain_scoped = 'domain' in json_body['auth'].get( 'scope', {} ) diff --git a/keystone/auth/plugins/totp.py b/keystone/auth/plugins/totp.py index 8ddd794376..792484d630 100644 --- a/keystone/auth/plugins/totp.py +++ b/keystone/auth/plugins/totp.py @@ -72,7 +72,7 @@ def _generate_totp_passcode(secret): class TOTP(base.AuthMethodHandler): - def authenticate(self, request, auth_payload): + def authenticate(self, auth_payload): """Try to authenticate using TOTP.""" response_data = {} user_info = plugins.TOTPUserInfo.create(auth_payload, METHOD_NAME) diff --git a/keystone/auth/routers.py b/keystone/auth/routers.py deleted file mode 100644 index 379d0c263a..0000000000 --- a/keystone/auth/routers.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from keystone.api._shared import json_home_relations -from keystone.auth import controllers -from keystone.common import json_home -from keystone.common import wsgi - - -class Routers(wsgi.RoutersBase): - - _path_prefixes = ('auth',) - - def append_v3_routers(self, mapper, routers): - auth_controller = controllers.Auth() - - self._add_resource( - mapper, auth_controller, - path='/auth/tokens', - get_action='validate_token', - head_action='check_token', - post_action='authenticate_for_token', - delete_action='revoke_token', - rel=json_home.build_v3_resource_relation('auth_tokens')) - - self._add_resource( - mapper, auth_controller, - path='/auth/tokens/OS-PKI/revoked', - get_head_action='revocation_list', - rel=json_home.build_v3_extension_resource_relation( - 'OS-PKI', '1.0', 'revocations')) - - self._add_resource( - mapper, auth_controller, - path='/auth/catalog', - get_head_action='get_auth_catalog', - rel=json_home.build_v3_resource_relation('auth_catalog')) - - self._add_resource( - mapper, auth_controller, - path='/auth/projects', - get_head_action='get_auth_projects', - rel=json_home.build_v3_resource_relation('auth_projects')) - - self._add_resource( - mapper, auth_controller, - path='/auth/domains', - get_head_action='get_auth_domains', - rel=json_home.build_v3_resource_relation('auth_domains')) - # NOTE(morgan): explicitly add json_home data for auth_projects and - # auth_domains for OS-FEDERATION here, as auth will always own it - # based upon how the flask scaffolding works. This bit is transitional - # for the move to flask. - for element in ['projects', 'domains']: - resource_data = {'href': '/auth/%s' % element} - json_home.Status.update_resource_data( - resource_data, status=json_home.Status.STABLE) - json_home.JsonHomeResources.append_resource( - json_home_relations.os_federation_resource_rel_func( - resource_name=element), resource_data) - - self._add_resource( - mapper, auth_controller, - path='/auth/system', - get_head_action='get_auth_system', - rel=json_home.build_v3_resource_relation('auth_system')) diff --git a/keystone/common/rbac_enforcer/enforcer.py b/keystone/common/rbac_enforcer/enforcer.py index e408943240..aa48a470c4 100644 --- a/keystone/common/rbac_enforcer/enforcer.py +++ b/keystone/common/rbac_enforcer/enforcer.py @@ -20,9 +20,9 @@ from oslo_utils import strutils from keystone.common import authorization from keystone.common import context -from keystone.common import controller from keystone.common import policies from keystone.common import provider_api +from keystone.common import render_token from keystone.common import utils import keystone.conf from keystone import exception @@ -85,7 +85,7 @@ class RBACEnforcer(object): # oslo.policy for enforcement. This is because oslo.policy shouldn't # know how to deal with an internal object only used within keystone. if 'token' in credentials: - token_ref = controller.render_token_response_from_model( + token_ref = render_token.render_token_response_from_model( credentials['token'] ) credentials_copy = copy.deepcopy(credentials) @@ -210,7 +210,7 @@ class RBACEnforcer(object): ret_dict[target] = {} ret_dict[target]['user_id'] = token.user_id try: - user_domain_id = token.user_domain_id + user_domain_id = token.user['domain_id'] except exception.UnexpectedError: user_domain_id = None if user_domain_id: diff --git a/keystone/common/render_token.py b/keystone/common/render_token.py new file mode 100644 index 0000000000..a74b44f51a --- /dev/null +++ b/keystone/common/render_token.py @@ -0,0 +1,145 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystone.common import provider_api +import keystone.conf + + +CONF = keystone.conf.CONF +PROVIDERS = provider_api.ProviderAPIs + + +def render_token_response_from_model(token, include_catalog=True): + token_reference = { + 'token': { + 'methods': token.methods, + 'user': { + 'domain': { + 'id': token.user_domain['id'], + 'name': token.user_domain['name'] + }, + 'id': token.user_id, + 'name': token.user['name'], + 'password_expires_at': token.user[ + 'password_expires_at' + ] + }, + 'audit_ids': token.audit_ids, + 'expires_at': token.expires_at, + 'issued_at': token.issued_at, + } + } + if token.system_scoped: + token_reference['token']['roles'] = token.roles + token_reference['token']['system'] = {'all': True} + elif token.domain_scoped: + token_reference['token']['domain'] = { + 'id': token.domain['id'], + 'name': token.domain['name'] + } + token_reference['token']['roles'] = token.roles + elif token.trust_scoped: + token_reference['token']['OS-TRUST:trust'] = { + 'id': token.trust_id, + 'trustor_user': {'id': token.trustor['id']}, + 'trustee_user': {'id': token.trustee['id']}, + 'impersonation': token.trust['impersonation'] + } + token_reference['token']['project'] = { + 'domain': { + 'id': token.project_domain['id'], + 'name': token.project_domain['name'] + }, + 'id': token.trust_project['id'], + 'name': token.trust_project['name'] + } + if token.trust.get('impersonation'): + trustor_domain = PROVIDERS.resource_api.get_domain( + token.trustor['domain_id'] + ) + token_reference['token']['user'] = { + 'domain': { + 'id': trustor_domain['id'], + 'name': trustor_domain['name'] + }, + 'id': token.trustor['id'], + 'name': token.trustor['name'], + 'password_expires_at': token.trustor[ + 'password_expires_at' + ] + } + token_reference['token']['roles'] = token.roles + elif token.project_scoped: + token_reference['token']['project'] = { + 'domain': { + 'id': token.project_domain['id'], + 'name': token.project_domain['name'] + }, + 'id': token.project['id'], + 'name': token.project['name'] + } + token_reference['token']['is_domain'] = token.project.get( + 'is_domain', False + ) + token_reference['token']['roles'] = token.roles + ap_name = CONF.resource.admin_project_name + ap_domain_name = CONF.resource.admin_project_domain_name + if ap_name and ap_domain_name: + is_ap = ( + token.project['name'] == ap_name and + ap_domain_name == token.project_domain['name'] + ) + token_reference['token']['is_admin_project'] = is_ap + if include_catalog and not token.unscoped: + user_id = token.user_id + if token.trust_id: + user_id = token.trust['trustor_user_id'] + catalog = PROVIDERS.catalog_api.get_v3_catalog( + user_id, token.project_id + ) + token_reference['token']['catalog'] = catalog + sps = PROVIDERS.federation_api.get_enabled_service_providers() + if sps: + token_reference['token']['service_providers'] = sps + if token.is_federated: + PROVIDERS.federation_api.get_idp(token.identity_provider_id) + federated_dict = dict( + groups=token.federated_groups, + identity_provider={'id': token.identity_provider_id}, + protocol={'id': token.protocol_id}, + + ) + token_reference['token']['user']['OS-FEDERATION'] = ( + federated_dict + ) + token_reference['token']['user']['domain'] = { + 'id': 'Federated', 'name': 'Federated' + } + del token_reference['token']['user']['password_expires_at'] + if token.access_token_id: + token_reference['token']['OS-OAUTH1'] = { + 'access_token_id': token.access_token_id, + 'consumer_id': token.access_token['consumer_id'] + } + if token.application_credential_id: + key = 'application_credential' + token_reference['token'][key] = {} + token_reference['token'][key]['id'] = ( + token.application_credential['id'] + ) + token_reference['token'][key]['name'] = ( + token.application_credential['name'] + ) + restricted = not token.application_credential['unrestricted'] + token_reference['token'][key]['restricted'] = restricted + + return token_reference diff --git a/keystone/contrib/ec2/controllers.py b/keystone/contrib/ec2/controllers.py index c7659e11f4..dc6b22377d 100644 --- a/keystone/contrib/ec2/controllers.py +++ b/keystone/contrib/ec2/controllers.py @@ -43,6 +43,7 @@ from six.moves import http_client from keystone.common import controller from keystone.common import provider_api +from keystone.common import render_token from keystone.common import utils from keystone.common import wsgi import keystone.conf @@ -296,7 +297,7 @@ class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller): token = self.token_provider_api.issue_token( user_ref['id'], method_names, project_id=project_ref['id'] ) - token_reference = controller.render_token_response_from_model(token) + token_reference = render_token.render_token_response_from_model(token) return self.render_token_data_response(token.id, token_reference) @controller.protected(callback=_check_credential_owner_and_user_id_match) diff --git a/keystone/federation/controllers.py b/keystone/federation/controllers.py deleted file mode 100644 index 96dce6d103..0000000000 --- a/keystone/federation/controllers.py +++ /dev/null @@ -1,226 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Workflow logic for the Federation service.""" - -import string - -from oslo_log import log -from six.moves import http_client -from six.moves import urllib -import webob - -from keystone.auth import controllers as auth_controllers -from keystone.common import controller -from keystone.common import provider_api -from keystone.common import utils as k_utils -from keystone.common import validation -from keystone.common import wsgi -import keystone.conf -from keystone import exception -from keystone.federation import idp as keystone_idp -from keystone.federation import schema -from keystone.federation import utils -from keystone.i18n import _ - - -CONF = keystone.conf.CONF -LOG = log.getLogger(__name__) -PROVIDERS = provider_api.ProviderAPIs - - -class _ControllerBase(controller.V3Controller): - """Base behaviors for federation controllers.""" - - @classmethod - def base_url(cls, context, path=None): - """Construct a path and pass it to V3Controller.base_url method.""" - path = '/OS-FEDERATION/' + cls.collection_name - return super(_ControllerBase, cls).base_url(context, path=path) - - -class Auth(auth_controllers.Auth): - - def _get_sso_origin_host(self, request): - """Validate and return originating dashboard URL. - - Make sure the parameter is specified in the request's URL as well its - value belongs to a list of trusted dashboards. - - :param context: request's context - :raises keystone.exception.ValidationError: ``origin`` query parameter - was not specified. The URL is deemed invalid. - :raises keystone.exception.Unauthorized: URL specified in origin query - parameter does not exist in list of websso trusted dashboards. - :returns: URL with the originating dashboard - - """ - origin = request.params.get('origin') - - if not origin: - msg = 'Request must have an origin query parameter' - tr_msg = _('Request must have an origin query parameter') - LOG.error(msg) - raise exception.ValidationError(tr_msg) - - host = urllib.parse.unquote_plus(origin) - - # change trusted_dashboard hostnames to lowercase before comparison - trusted_dashboards = [k_utils.lower_case_hostname(trusted) - for trusted in CONF.federation.trusted_dashboard] - - if host not in trusted_dashboards: - msg = '%(host)s is not a trusted dashboard host' % {'host': host} - tr_msg = _('%(host)s is not a trusted dashboard host') % { - 'host': host} - LOG.error(msg) - raise exception.Unauthorized(tr_msg) - - return host - - def federated_authentication(self, request, idp_id, protocol_id): - """Authenticate from dedicated url endpoint. - - Build HTTP request body for federated authentication and inject - it into the ``authenticate_for_token`` function. - - """ - auth = { - 'identity': { - 'methods': [protocol_id], - protocol_id: { - 'identity_provider': idp_id, - 'protocol': protocol_id - } - } - } - - return self.authenticate_for_token(request, auth=auth) - - def federated_sso_auth(self, request, protocol_id): - try: - remote_id_name = utils.get_remote_id_parameter(protocol_id) - remote_id = request.environ[remote_id_name] - except KeyError: - msg = 'Missing entity ID from environment' - tr_msg = _('Missing entity ID from environment') - LOG.error(msg) - raise exception.Unauthorized(tr_msg) - - host = self._get_sso_origin_host(request) - - ref = PROVIDERS.federation_api.get_idp_from_remote_id(remote_id) - # NOTE(stevemar): the returned object is a simple dict that - # contains the idp_id and remote_id. - identity_provider = ref['idp_id'] - res = self.federated_authentication(request, - identity_provider, - protocol_id) - token_id = res.headers['X-Subject-Token'] - return self.render_html_response(host, token_id) - - def federated_idp_specific_sso_auth(self, request, idp_id, protocol_id): - host = self._get_sso_origin_host(request) - - # NOTE(lbragstad): We validate that the Identity Provider actually - # exists in the Mapped authentication plugin. - res = self.federated_authentication(request, - idp_id, - protocol_id) - token_id = res.headers['X-Subject-Token'] - return self.render_html_response(host, token_id) - - def render_html_response(self, host, token_id): - """Form an HTML Form from a template with autosubmit.""" - headers = [('Content-Type', 'text/html')] - - with open(CONF.federation.sso_callback_template) as template: - src = string.Template(template.read()) - - subs = {'host': host, 'token': token_id} - body = src.substitute(subs) - return webob.Response(body=body, status='200', charset='utf-8', - headerlist=headers) - - def _create_base_saml_assertion(self, context, auth): - issuer = CONF.saml.idp_entity_id - sp_id = auth['scope']['service_provider']['id'] - service_provider = PROVIDERS.federation_api.get_sp(sp_id) - utils.assert_enabled_service_provider_object(service_provider) - sp_url = service_provider['sp_url'] - - token_id = auth['identity']['token']['id'] - token = PROVIDERS.token_provider_api.validate_token(token_id) - - if not token.project_scoped: - action = _('Use a project scoped token when attempting to create ' - 'a SAML assertion') - raise exception.ForbiddenAction(action=action) - - subject = token.user['name'] - role_names = [] - for role in token.roles: - role_names.append(role['name']) - project = token.project['name'] - # NOTE(rodrigods): the domain name is necessary in order to distinguish - # between projects and users with the same name in different domains. - project_domain_name = token.project_domain['name'] - subject_domain_name = token.user_domain['name'] - - generator = keystone_idp.SAMLGenerator() - response = generator.samlize_token( - issuer, sp_url, subject, subject_domain_name, - role_names, project, project_domain_name) - return (response, service_provider) - - def _build_response_headers(self, service_provider): - # URLs in header are encoded into bytes - return [('Content-Type', 'text/xml'), - ('X-sp-url', service_provider['sp_url'].encode('utf-8')), - ('X-auth-url', service_provider['auth_url'].encode('utf-8'))] - - def create_saml_assertion(self, request, auth): - """Exchange a scoped token for a SAML assertion. - - :param auth: Dictionary that contains a token and service provider ID - :returns: SAML Assertion based on properties from the token - """ - validation.lazy_validate(schema.saml_create, auth) - t = self._create_base_saml_assertion(request.context_dict, auth) - (response, service_provider) = t - - headers = self._build_response_headers(service_provider) - return wsgi.render_response( - body=response.to_string(), - status=(http_client.OK, http_client.responses[http_client.OK]), - headers=headers) - - def create_ecp_assertion(self, request, auth): - """Exchange a scoped token for an ECP assertion. - - :param auth: Dictionary that contains a token and service provider ID - :returns: ECP Assertion based on properties from the token - """ - validation.lazy_validate(schema.saml_create, auth) - t = self._create_base_saml_assertion(request.context_dict, auth) - (saml_assertion, service_provider) = t - relay_state_prefix = service_provider['relay_state_prefix'] - - generator = keystone_idp.ECPGenerator() - ecp_assertion = generator.generate_ecp(saml_assertion, - relay_state_prefix) - - headers = self._build_response_headers(service_provider) - return wsgi.render_response( - body=ecp_assertion.to_string(), - status=(http_client.OK, http_client.responses[http_client.OK]), - headers=headers) diff --git a/keystone/federation/routers.py b/keystone/federation/routers.py deleted file mode 100644 index 9bfd11740b..0000000000 --- a/keystone/federation/routers.py +++ /dev/null @@ -1,95 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import functools - -from keystone.common import json_home -from keystone.common import wsgi -from keystone.federation import controllers - - -build_resource_relation = functools.partial( - json_home.build_v3_extension_resource_relation, - extension_name='OS-FEDERATION', extension_version='1.0') - -build_parameter_relation = functools.partial( - json_home.build_v3_extension_parameter_relation, - extension_name='OS-FEDERATION', extension_version='1.0') - -IDP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='idp_id') -PROTOCOL_ID_PARAMETER_RELATION = build_parameter_relation( - parameter_name='protocol_id') -SP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='sp_id') - - -class Routers(wsgi.RoutersBase): - """API Endpoints for the Federation extension. - - The API looks like:: - - GET /auth/OS-FEDERATION/identity_providers/ - {idp_id}/protocols/{protocol_id}/websso - ?origin=https%3A//horizon.example.com - POST /auth/OS-FEDERATION/identity_providers/ - {idp_id}/protocols/{protocol_id}/websso - ?origin=https%3A//horizon.example.com - - - POST /auth/OS-FEDERATION/saml2 - POST /auth/OS-FEDERATION/saml2/ecp - - GET /auth/OS-FEDERATION/websso/{protocol_id} - ?origin=https%3A//horizon.example.com - - POST /auth/OS-FEDERATION/websso/{protocol_id} - ?origin=https%3A//horizon.example.com - - """ - - _path_prefixes = ('auth',) - - def _construct_url(self, suffix): - return "/OS-FEDERATION/%s" % suffix - - def append_v3_routers(self, mapper, routers): - auth_controller = controllers.Auth() - - # Auth operations - self._add_resource( - mapper, auth_controller, - path='/auth' + self._construct_url('saml2'), - post_action='create_saml_assertion', - rel=build_resource_relation(resource_name='saml2')) - self._add_resource( - mapper, auth_controller, - path='/auth' + self._construct_url('saml2/ecp'), - post_action='create_ecp_assertion', - rel=build_resource_relation(resource_name='ecp')) - self._add_resource( - mapper, auth_controller, - path='/auth' + self._construct_url('websso/{protocol_id}'), - get_post_action='federated_sso_auth', - rel=build_resource_relation(resource_name='websso'), - path_vars={ - 'protocol_id': PROTOCOL_ID_PARAMETER_RELATION, - }) - self._add_resource( - mapper, auth_controller, - path='/auth' + self._construct_url( - 'identity_providers/{idp_id}/protocols/{protocol_id}/websso'), - get_post_action='federated_idp_specific_sso_auth', - rel=build_resource_relation( - resource_name='identity_providers_websso'), - path_vars={ - 'idp_id': IDP_ID_PARAMETER_RELATION, - 'protocol_id': PROTOCOL_ID_PARAMETER_RELATION, - }) diff --git a/keystone/federation/utils.py b/keystone/federation/utils.py index d483c79baa..91edba2666 100644 --- a/keystone/federation/utils.py +++ b/keystone/federation/utils.py @@ -15,6 +15,7 @@ import ast import re +import flask import jsonschema from oslo_config import cfg from oslo_log import log @@ -416,10 +417,10 @@ def transform_to_group_ids(group_names, mapping_id, group['name']) -def get_assertion_params_from_env(request): - LOG.debug('Environment variables: %s', request.environ) +def get_assertion_params_from_env(): + LOG.debug('Environment variables: %s', flask.request.environ) prefix = CONF.federation.assertion_prefix - for k, v in list(request.environ.items()): + for k, v in list(flask.request.environ.items()): if not k.startswith(prefix): continue # These bytes may be decodable as ISO-8859-1 according to Section diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 9ddbbc607d..cd7d87b88b 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -105,7 +105,7 @@ class UserV3(controller.V3Controller): attribute='password') try: PROVIDERS.identity_api.change_password( - request, user_id, original_password, + user_id, original_password, password, initiator=request.audit_initiator) except AssertionError as e: raise exception.Unauthorized(_( diff --git a/keystone/identity/core.py b/keystone/identity/core.py index dfd17cd0ee..3da3c90c5c 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -900,10 +900,19 @@ class Manager(manager.Manager): # - clear/set domain_ids for drivers that do not support domains # - create any ID mapping that might be required + # TODO(morgan): The split of "authenticate" and "_authenticate" is done + # until user API is converted to flask. This is to make webob and flask + # play nicely with the authenticate mechanism during self-service password + # changes. While this is in place, CADF notifications will not be emitted + # for self-service password changes indicating an auth attempt was being + # made. This is a very limited time transitional change. @notifications.emit_event('authenticate') + def authenticate(self, user_id, password): + return self._authenticate(user_id, password) + @domains_configured @exception_translated('assertion') - def authenticate(self, request, user_id, password): + def _authenticate(self, user_id, password): domain_id, driver, entity_id = ( self._get_domain_driver_and_entity_id(user_id)) ref = driver.authenticate(entity_id, password) @@ -1376,12 +1385,14 @@ class Manager(manager.Manager): group_entity_id) @domains_configured - def change_password(self, request, user_id, original_password, + def change_password(self, user_id, original_password, new_password, initiator=None): # authenticate() will raise an AssertionError if authentication fails try: - self.authenticate(request, user_id, original_password) + # TODO(morgan): When users is ported to flask, ensure this is + # mapped back to self.authenticate instead of self._authenticate. + self._authenticate(user_id, original_password) except exception.PasswordExpired: # If a password has expired, we want users to be able to change it pass diff --git a/keystone/middleware/auth.py b/keystone/middleware/auth.py index 1aebf6cb78..ccd0325bac 100644 --- a/keystone/middleware/auth.py +++ b/keystone/middleware/auth.py @@ -15,8 +15,8 @@ from oslo_log import log from keystone.common import authorization from keystone.common import context -from keystone.common import controller from keystone.common import provider_api +from keystone.common import render_token from keystone.common import tokenless_auth from keystone.common import wsgi import keystone.conf @@ -45,7 +45,7 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin, def fetch_token(self, token, **kwargs): try: token_model = self.token_provider_api.validate_token(token) - return controller.render_token_response_from_model(token_model) + return render_token.render_token_response_from_model(token_model) except exception.TokenNotFound: raise auth_token.InvalidToken(_('Could not find token')) diff --git a/keystone/notifications.py b/keystone/notifications.py index dfbac8593f..3f522aac58 100644 --- a/keystone/notifications.py +++ b/keystone/notifications.py @@ -19,6 +19,7 @@ import functools import inspect import socket +import flask from oslo_log import log import oslo_messaging from oslo_utils import reflection @@ -27,9 +28,11 @@ from pycadf import cadftaxonomy as taxonomy from pycadf import cadftype from pycadf import credential from pycadf import eventfactory +from pycadf import host from pycadf import reason from pycadf import resource +from keystone.common import context from keystone.common import provider_api from keystone.common import utils import keystone.conf @@ -82,6 +85,26 @@ REMOVE_APP_CREDS_FOR_USER = 'remove_application_credentials_for_user' DOMAIN_DELETED = 'domain_deleted' +def build_audit_initiator(): + """A pyCADF initiator describing the current authenticated context.""" + pycadf_host = host.Host(address=flask.request.remote_addr, + agent=str(flask.request.user_agent)) + initiator = resource.Resource(typeURI=taxonomy.ACCOUNT_USER, + host=pycadf_host) + oslo_context = flask.request.environ.get(context.REQUEST_CONTEXT_ENV) + if oslo_context.user_id: + initiator.id = utils.resource_uuid(oslo_context.user_id) + initiator.user_id = oslo_context.user_id + + if oslo_context.project_id: + initiator.project_id = oslo_context.project_id + + if oslo_context.domain_id: + initiator.domain_id = oslo_context.domain_id + + return initiator + + class Audit(object): """Namespace for audit notification functions. @@ -515,14 +538,14 @@ class CadfNotificationWrapper(object): def __call__(self, f): @functools.wraps(f) - def wrapper(wrapped_self, request, user_id, *args, **kwargs): + def wrapper(wrapped_self, user_id, *args, **kwargs): """Will always send a notification.""" target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER) - initiator = request.audit_initiator + initiator = build_audit_initiator() initiator.user_id = user_id initiator.id = utils.resource_uuid(user_id) try: - result = f(wrapped_self, request, user_id, *args, **kwargs) + result = f(wrapped_self, user_id, *args, **kwargs) except (exception.AccountLocked, exception.PasswordExpired) as ex: # Send a CADF event with a reason for PCI-DSS related @@ -657,15 +680,13 @@ class CadfRoleAssignmentNotificationWrapper(object): return wrapper -def send_saml_audit_notification(action, request, user_id, group_ids, +def send_saml_audit_notification(action, user_id, group_ids, identity_provider, protocol, token_id, outcome): """Send notification to inform observers about SAML events. :param action: Action being audited :type action: str - :param request: Current request to collect request info from - :type request: keystone.common.request.Request :param user_id: User ID from Keystone token :type user_id: str :param group_ids: List of Group IDs from Keystone token @@ -679,7 +700,7 @@ def send_saml_audit_notification(action, request, user_id, group_ids, :param outcome: One of :class:`pycadf.cadftaxonomy` :type outcome: str """ - initiator = request.audit_initiator + initiator = build_audit_initiator() target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER) audit_type = SAML_AUDIT_TYPE user_id = user_id or taxonomy.UNKNOWN diff --git a/keystone/server/flask/__init__.py b/keystone/server/flask/__init__.py index 6b49188e5c..9167a9c18c 100644 --- a/keystone/server/flask/__init__.py +++ b/keystone/server/flask/__init__.py @@ -16,7 +16,6 @@ from keystone.server.flask.common import APIBase # noqa from keystone.server.flask.common import base_url # noqa -from keystone.server.flask.common import build_audit_initiator # noqa from keystone.server.flask.common import construct_json_home_data # noqa from keystone.server.flask.common import construct_resource_map # noqa from keystone.server.flask.common import full_url # noqa @@ -29,5 +28,5 @@ from keystone.server.flask.common import unenforced_api # noqa # NOTE(morgan): This allows for from keystone.flask import * and have all the # cool stuff needed to develop new APIs within a module/subsystem __all__ = ('APIBase', 'JsonHomeData', 'ResourceBase', 'ResourceMap', - 'base_url', 'build_audit_initiator', 'construct_json_home_data', + 'base_url', 'construct_json_home_data', 'construct_resource_map', 'full_url', 'unenforced_api') diff --git a/keystone/server/flask/application.py b/keystone/server/flask/application.py index 3fed7b5763..cfa55d83c0 100644 --- a/keystone/server/flask/application.py +++ b/keystone/server/flask/application.py @@ -26,11 +26,9 @@ import werkzeug.wsgi import keystone.api from keystone.application_credential import routers as app_cred_routers from keystone.assignment import routers as assignment_routers -from keystone.auth import routers as auth_routers from keystone.common import wsgi as keystone_wsgi from keystone.contrib.ec2 import routers as ec2_routers from keystone.contrib.s3 import routers as s3_routers -from keystone.federation import routers as federation_routers from keystone.identity import routers as identity_routers from keystone.oauth1 import routers as oauth1_routers from keystone.resource import routers as resource_routers @@ -38,7 +36,8 @@ from keystone.resource import routers as resource_routers # TODO(morgan): _MOVED_API_PREFIXES to be removed when the legacy dispatch # support is removed. _MOVED_API_PREFIXES = frozenset( - ['credentials', + ['auth', + 'credentials', 'domains', 'endpoints', 'groups', @@ -64,12 +63,10 @@ _MOVED_API_PREFIXES = frozenset( LOG = log.getLogger(__name__) -ALL_API_ROUTERS = [auth_routers, - assignment_routers, +ALL_API_ROUTERS = [assignment_routers, identity_routers, app_cred_routers, resource_routers, - federation_routers, oauth1_routers, ec2_routers, s3_routers] diff --git a/keystone/server/flask/common.py b/keystone/server/flask/common.py index b184757f4f..889107cf2e 100644 --- a/keystone/server/flask/common.py +++ b/keystone/server/flask/common.py @@ -25,9 +25,6 @@ import flask_restful.utils from oslo_log import log from oslo_log import versionutils from oslo_serialization import jsonutils -from pycadf import cadftaxonomy as taxonomy -from pycadf import host -from pycadf import resource import six from six.moves import http_client @@ -40,6 +37,7 @@ from keystone.common import utils import keystone.conf from keystone import exception from keystone.i18n import _ +from keystone import notifications # NOTE(morgan): Capture the relevant part of the flask url route rule for @@ -92,15 +90,21 @@ def construct_resource_map(resource, url, resource_kwargs, alternate_urls=None, Additional keyword arguments not specified above will be passed as-is to :meth:`flask.Flask.add_url_rule`. - :param alternate_urls: An iterable (list) of urls that also map to the - resource. These are used to ensure API compat when - a "new" path is more correct for the API but old - paths must continue to work. Example: + :param alternate_urls: An iterable (list) of dictionaries containing urls + and associated json home REL data. Each element is + expected to be a dictionary with a 'url' key and an + optional 'json_home' key for a 'JsonHomeData' named + tuple These urls will also map to the resource. + These are used to ensure API compatibility when a + "new" path is more correct for the API but old paths + must continue to work. Example: `/auth/domains` being the new path for `/OS-FEDERATION/domains`. The `OS-FEDERATION` part - would be listed as an alternate url. These are not - added to the JSON Home Document. - :type: any iterable or None + would be listed as an alternate url. If a + 'json_home' key is provided, the original path + with the new json_home data will be added to the + JSON Home Document. + :type: iterable or None :param rel: :type rel: str or None :param status: JSON Home API Status, e.g. "STABLE" @@ -153,26 +157,6 @@ def _remove_content_type_on_204(resp): return resp -def build_audit_initiator(): - """A pyCADF initiator describing the current authenticated context.""" - pycadf_host = host.Host(address=flask.request.remote_addr, - agent=str(flask.request.user_agent)) - initiator = resource.Resource(typeURI=taxonomy.ACCOUNT_USER, - host=pycadf_host) - oslo_context = flask.request.environ.get(context.REQUEST_CONTEXT_ENV) - if oslo_context.user_id: - initiator.id = utils.resource_uuid(oslo_context.user_id) - initiator.user_id = oslo_context.user_id - - if oslo_context.project_id: - initiator.project_id = oslo_context.project_id - - if oslo_context.domain_id: - initiator.domain_id = oslo_context.domain_id - - return initiator - - @six.add_metaclass(abc.ABCMeta) class APIBase(object): @@ -427,19 +411,33 @@ class APIBase(object): def _add_mapped_resources(self): # Add resource mappings, non-standard resource connections for r in self.resource_mapping: + alt_url_json_home_data = [] LOG.debug( 'Adding resource routes to API %(name)s: ' '[%(url)r %(kwargs)r]', {'name': self._name, 'url': r.url, 'kwargs': r.kwargs}) - self.api.add_resource(r.resource, r.url, **r.kwargs) + urls = [r.url] if r.alternate_urls is not None: - LOG.debug( - 'Adding additional resource routes (alternate) to API' - '%(name)s: [%(urls)r %(kwargs)r]', - {'name': self._name, 'urls': r.alternate_urls, - 'kwargs': r.kwargs}) - self.api.add_resource(r.resource, *r.alternate_urls, - **r.kwargs) + for element in r.alternate_urls: + if self._api_url_prefix: + LOG.debug( + 'Unable to add additional resource route ' + '`%(route)s` to API %(name)s because API has a ' + 'URL prefix. Only APIs without explicit prefixes ' + 'can have alternate URL routes added.', + {'route': element['url'], 'name': self._name} + ) + continue + LOG.debug( + 'Adding additional resource route (alternate) to API' + '%(name)s: [%(url)r %(kwargs)r]', + {'name': self._name, 'url': element['url'], + 'kwargs': r.kwargs}) + urls.append(element['url']) + if element.get('json_home'): + alt_url_json_home_data.append(element['json_home']) + # Add all URL routes at once. + self.api.add_resource(r.resource, *urls, **r.kwargs) # Build the JSON Home data and add it to the relevant JSON Home # Documents for explicit JSON Home data. @@ -462,6 +460,12 @@ class APIBase(object): r.json_home_data.rel, resource_data) + for element in alt_url_json_home_data: + # Append the "new" path (resource) data with the old rel + # reference. + json_home.JsonHomeResources.append_resource( + element.rel, resource_data) + def _register_before_request_functions(self, functions=None): """Register functions to be executed in the `before request` phase. @@ -764,7 +768,7 @@ class ResourceBase(flask_restful.Resource): As a property. """ - return build_audit_initiator() + return notifications.build_audit_initiator() @staticmethod def query_filter_is_true(filter_name): diff --git a/keystone/server/flask/core.py b/keystone/server/flask/core.py index ddcbe4295a..2bba996edd 100644 --- a/keystone/server/flask/core.py +++ b/keystone/server/flask/core.py @@ -146,6 +146,8 @@ def initialize_application(name, post_log_configured_function=lambda: None, config_files = [dev_conf] keystone.server.configure(config_files=config_files) + # explicitly load auth configuration + keystone.conf.auth.setup_authentication() # Log the options used when starting if we're in debug mode... if CONF.debug: diff --git a/keystone/tests/unit/application_credential/test_backends.py b/keystone/tests/unit/application_credential/test_backends.py index f25dde8884..a798a9c14f 100644 --- a/keystone/tests/unit/application_credential/test_backends.py +++ b/keystone/tests/unit/application_credential/test_backends.py @@ -257,13 +257,11 @@ class ApplicationCredentialTests(object): app_cred = self._new_app_cred_data(self.user_foo['id'], project_id=self.tenant_bar['id']) resp = self.app_cred_api.create_application_credential(app_cred) - self.app_cred_api.authenticate( - self.make_request(), resp['id'], resp['secret']) + self.app_cred_api.authenticate(resp['id'], resp['secret']) def test_authenticate_not_found(self): self.assertRaises(AssertionError, self.app_cred_api.authenticate, - self.make_request(), uuid.uuid4().hex, uuid.uuid4().hex) @@ -275,7 +273,6 @@ class ApplicationCredentialTests(object): resp = self.app_cred_api.create_application_credential(app_cred) self.assertRaises(AssertionError, self.app_cred_api.authenticate, - self.make_request(), resp['id'], resp['secret']) @@ -287,6 +284,5 @@ class ApplicationCredentialTests(object): self.assertNotEqual(badpass, resp['secret']) self.assertRaises(AssertionError, self.app_cred_api.authenticate, - self.make_request(), resp['id'], badpass) diff --git a/keystone/tests/unit/common/test_notifications.py b/keystone/tests/unit/common/test_notifications.py index 4908740836..355679ae50 100644 --- a/keystone/tests/unit/common/test_notifications.py +++ b/keystone/tests/unit/common/test_notifications.py @@ -745,25 +745,26 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest): user_ref = unit.new_user_ref(domain_id=self.domain_id, password=password) user_ref = PROVIDERS.identity_api.create_user(user_ref) - PROVIDERS.identity_api.authenticate( - self.make_request(), user_ref['id'], password - ) + with self.make_request(): + PROVIDERS.identity_api.authenticate(user_ref['id'], password) freezer.stop() reason_type = (exception.PasswordExpired.message_format % {'user_id': user_ref['id']}) expected_reason = {'reasonCode': '401', 'reasonType': reason_type} - self.assertRaises(exception.PasswordExpired, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user_ref['id'], - password=password) + with self.make_request(): + self.assertRaises(exception.PasswordExpired, + PROVIDERS.identity_api.authenticate, + user_id=user_ref['id'], + password=password) self._assert_last_audit(None, 'authenticate', None, cadftaxonomy.ACCOUNT_USER, reason=expected_reason) def test_locked_out_user_sends_notification(self): + # TODO(morgan): skip this test until users is ported to flask. + self.skipTest('Users are not handled via flask.') password = uuid.uuid4().hex new_password = uuid.uuid4().hex expected_responses = [AssertionError, AssertionError, AssertionError, @@ -776,12 +777,12 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest): expected_reason = {'reasonCode': '401', 'reasonType': reason_type} for ex in expected_responses: - self.assertRaises(ex, - PROVIDERS.identity_api.change_password, - self.make_request(), - user_id=user_ref['id'], - original_password=new_password, - new_password=new_password) + with self.make_request(): + self.assertRaises(ex, + PROVIDERS.identity_api.change_password, + user_id=user_ref['id'], + original_password=new_password, + new_password=new_password) self._assert_last_audit(None, 'authenticate', None, cadftaxonomy.ACCOUNT_USER, @@ -801,16 +802,17 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest): user_ref = unit.new_user_ref(domain_id=self.domain_id, password=password) user_ref = PROVIDERS.identity_api.create_user(user_ref) - PROVIDERS.identity_api.change_password( - self.make_request(), user_id=user_ref['id'], - original_password=password, new_password=new_password - ) - self.assertRaises(exception.PasswordValidationError, - PROVIDERS.identity_api.change_password, - self.make_request(), - user_id=user_ref['id'], - original_password=new_password, - new_password=password) + with self.make_request(): + PROVIDERS.identity_api.change_password( + user_id=user_ref['id'], + original_password=password, new_password=new_password + ) + with self.make_request(): + self.assertRaises(exception.PasswordValidationError, + PROVIDERS.identity_api.change_password, + user_id=user_ref['id'], + original_password=new_password, + new_password=password) self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user', cadftaxonomy.SECURITY_ACCOUNT_USER, @@ -828,12 +830,12 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest): user_ref = unit.new_user_ref(domain_id=self.domain_id, password=password) user_ref = PROVIDERS.identity_api.create_user(user_ref) - self.assertRaises(exception.PasswordValidationError, - PROVIDERS.identity_api.change_password, - self.make_request(), - user_id=user_ref['id'], - original_password=password, - new_password=invalid_password) + with self.make_request(): + self.assertRaises(exception.PasswordValidationError, + PROVIDERS.identity_api.change_password, + user_id=user_ref['id'], + original_password=password, + new_password=invalid_password) self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user', cadftaxonomy.SECURITY_ACCOUNT_USER, @@ -858,16 +860,17 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest): {'min_age_days': min_days, 'days_left': days_left}) expected_reason = {'reasonCode': '400', 'reasonType': reason_type} - PROVIDERS.identity_api.change_password( - self.make_request(), user_id=user_ref['id'], - original_password=password, new_password=new_password - ) - self.assertRaises(exception.PasswordValidationError, - PROVIDERS.identity_api.change_password, - self.make_request(), - user_id=user_ref['id'], - original_password=new_password, - new_password=next_password) + with self.make_request(): + PROVIDERS.identity_api.change_password( + user_id=user_ref['id'], + original_password=password, new_password=new_password + ) + with self.make_request(): + self.assertRaises(exception.PasswordValidationError, + PROVIDERS.identity_api.change_password, + user_id=user_ref['id'], + original_password=new_password, + new_password=next_password) self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user', cadftaxonomy.SECURITY_ACCOUNT_USER, diff --git a/keystone/tests/unit/contrib/federation/test_utils.py b/keystone/tests/unit/contrib/federation/test_utils.py index 5b9b3ed317..4f59e9df62 100644 --- a/keystone/tests/unit/contrib/federation/test_utils.py +++ b/keystone/tests/unit/contrib/federation/test_utils.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import flask import uuid from oslo_config import fixture as config_fixture from oslo_serialization import jsonutils -import webob from keystone.auth.plugins import mapped import keystone.conf @@ -31,6 +31,13 @@ FAKE_MAPPING_ID = uuid.uuid4().hex class MappingRuleEngineTests(unit.BaseTestCase): """A class for testing the mapping rule engine.""" + def setUp(self): + super(MappingRuleEngineTests, self).setUp() + # create dummy app so we can setup a request context for our + # tests. + self.flask_app = flask.Flask(__name__) + self.cleanup_instance('flask_app') + def assertValidMappedUserObject(self, mapped_properties, user_type='ephemeral', domain_id=None): @@ -510,7 +517,7 @@ class MappingRuleEngineTests(unit.BaseTestCase): self.assertValidMappedUserObject(mapped_properties) self.assertEqual('jsmith', mapped_properties['user']['name']) unique_id, display_name = mapped.get_user_unique_id_and_display_name( - {}, mapped_properties) + mapped_properties) self.assertEqual('jsmith', unique_id) self.assertEqual('jsmith', display_name) @@ -533,7 +540,7 @@ class MappingRuleEngineTests(unit.BaseTestCase): self.assertIsNotNone(mapped_properties) self.assertValidMappedUserObject(mapped_properties) unique_id, display_name = mapped.get_user_unique_id_and_display_name( - {}, mapped_properties) + mapped_properties) self.assertEqual('tbo', display_name) self.assertEqual('abc123%40example.com', unique_id) @@ -549,15 +556,15 @@ class MappingRuleEngineTests(unit.BaseTestCase): as it was not explicitly specified in the mapping. """ - request = webob.Request.blank('/') mapping = mapping_fixtures.MAPPING_USER_IDS rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = mapping_fixtures.ADMIN_ASSERTION mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) self.assertValidMappedUserObject(mapped_properties) - unique_id, display_name = mapped.get_user_unique_id_and_display_name( - request, mapped_properties) + with self.flask_app.test_request_context(): + unique_id, display_name = ( + mapped.get_user_unique_id_and_display_name(mapped_properties)) self.assertEqual('bob', unique_id) self.assertEqual('bob', display_name) @@ -566,13 +573,14 @@ class MappingRuleEngineTests(unit.BaseTestCase): mapping = mapping_fixtures.MAPPING_USER_IDS assertion = mapping_fixtures.ADMIN_ASSERTION FAKE_MAPPING_ID = uuid.uuid4().hex - request = webob.Request.blank('/', remote_user='remote_user') rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) self.assertValidMappedUserObject(mapped_properties) - unique_id, display_name = mapped.get_user_unique_id_and_display_name( - request, mapped_properties) + with self.flask_app.test_request_context( + environ_base={'REMOTE_USER': 'remote_user'}): + unique_id, display_name = ( + mapped.get_user_unique_id_and_display_name(mapped_properties)) self.assertEqual('bob', unique_id) self.assertEqual('remote_user', display_name) @@ -597,7 +605,6 @@ class MappingRuleEngineTests(unit.BaseTestCase): not to change it. """ - request = webob.Request.blank('/') testcases = [(mapping_fixtures.CUSTOMER_ASSERTION, 'bwilliams'), (mapping_fixtures.EMPLOYEE_ASSERTION, 'tbo')] for assertion, exp_user_name in testcases: @@ -607,8 +614,7 @@ class MappingRuleEngineTests(unit.BaseTestCase): self.assertIsNotNone(mapped_properties) self.assertValidMappedUserObject(mapped_properties) unique_id, display_name = ( - mapped.get_user_unique_id_and_display_name(request, - mapped_properties) + mapped.get_user_unique_id_and_display_name(mapped_properties) ) self.assertEqual(exp_user_name, display_name) self.assertEqual('abc123%40example.com', unique_id) @@ -821,12 +827,14 @@ class TestUnicodeAssertionData(unit.BaseTestCase): # pulled from the HTTP headers. These bytes may be decodable as # ISO-8859-1 according to Section 3.2.4 of RFC 7230. Let's assume # that our web server plugins are correctly encoding the data. - request = webob.Request.blank( - '/path', - environ=mapping_fixtures.UNICODE_NAME_ASSERTION) - data = mapping_utils.get_assertion_params_from_env(request) - # NOTE(dstanek): keystone.auth.plugins.mapped - return dict(data) + # Create a dummy application + app = flask.Flask(__name__) + with app.test_request_context( + path='/path', + environ_overrides=mapping_fixtures.UNICODE_NAME_ASSERTION): + data = mapping_utils.get_assertion_params_from_env() + # NOTE(dstanek): keystone.auth.plugins.mapped + return dict(data) def test_unicode(self): mapping = self._pull_mapping_rules_from_the_database() diff --git a/keystone/tests/unit/core.py b/keystone/tests/unit/core.py index 67c6e1d7e4..10ebf831fb 100644 --- a/keystone/tests/unit/core.py +++ b/keystone/tests/unit/core.py @@ -15,6 +15,7 @@ from __future__ import absolute_import import atexit import base64 +import contextlib import datetime import functools import hashlib @@ -46,7 +47,6 @@ import keystone.api from keystone.common import context from keystone.common import json_home from keystone.common import provider_api -from keystone.common import request from keystone.common import sql import keystone.conf from keystone import exception @@ -684,19 +684,27 @@ class TestCase(BaseTestCase): def _policy_fixture(self): return ksfixtures.Policy(dirs.etc('policy.json'), self.config_fixture) + @contextlib.contextmanager def make_request(self, path='/', **kwargs): + # standup a fake app and request context with a passed in/known + # environment. + is_admin = kwargs.pop('is_admin', False) environ = kwargs.setdefault('environ', {}) + query_string = kwargs.pop('query_string', None) + if query_string: + # Make sure query string is properly added to the context + path = '{path}?{qs}'.format(path=path, qs=query_string) if not environ.get(context.REQUEST_CONTEXT_ENV): environ[context.REQUEST_CONTEXT_ENV] = context.RequestContext( is_admin=is_admin, authenticated=kwargs.pop('authenticated', True)) - req = request.Request.blank(path=path, **kwargs) - req.context_dict['is_admin'] = is_admin - - return req + # Create a dummy flask app to work with + app = flask.Flask(__name__) + with app.test_request_context(path=path, environ_overrides=environ): + yield def config_overrides(self): # NOTE(morganfainberg): enforce config_overrides can only ever be @@ -779,6 +787,8 @@ class TestCase(BaseTestCase): new=mocked_register_auth_plugin_opt)) self.config_overrides() + # explicitly load auth configuration + keystone.conf.auth.setup_authentication() # NOTE(morganfainberg): ensure config_overrides has been called. self.addCleanup(self._assert_config_overrides_called) diff --git a/keystone/tests/unit/identity/shadow_users/test_backend.py b/keystone/tests/unit/identity/shadow_users/test_backend.py index 487c1f749e..ee89edf403 100644 --- a/keystone/tests/unit/identity/shadow_users/test_backend.py +++ b/keystone/tests/unit/identity/shadow_users/test_backend.py @@ -121,10 +121,10 @@ class ShadowUsersBackendTests(object): now = datetime.datetime.utcnow().date() password = uuid.uuid4().hex user = self._create_user(password) - user_auth = PROVIDERS.identity_api.authenticate( - self.make_request(), - user_id=user['id'], - password=password) + with self.make_request(): + user_auth = PROVIDERS.identity_api.authenticate( + user_id=user['id'], + password=password) user_ref = self._get_user_ref(user_auth['id']) self.assertGreaterEqual(now, user_ref.last_active_at) @@ -133,10 +133,10 @@ class ShadowUsersBackendTests(object): disable_user_account_days_inactive=None) password = uuid.uuid4().hex user = self._create_user(password) - user_auth = PROVIDERS.identity_api.authenticate( - self.make_request(), - user_id=user['id'], - password=password) + with self.make_request(): + user_auth = PROVIDERS.identity_api.authenticate( + user_id=user['id'], + password=password) user_ref = self._get_user_ref(user_auth['id']) self.assertIsNone(user_ref.last_active_at) diff --git a/keystone/tests/unit/identity/test_backend_sql.py b/keystone/tests/unit/identity/test_backend_sql.py index 6e31525717..7c43cfe4ea 100644 --- a/keystone/tests/unit/identity/test_backend_sql.py +++ b/keystone/tests/unit/identity/test_backend_sql.py @@ -272,21 +272,21 @@ class DisableInactiveUserTests(test_backend_sql.SqlTests): datetime.datetime.utcnow() - datetime.timedelta(days=self.max_inactive_days + 1)) user = self._create_user(self.user_dict, last_active_at.date()) - self.assertRaises(exception.UserDisabled, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user['id'], - password=self.password) - # verify that the user is actually disabled - user = PROVIDERS.identity_api.get_user(user['id']) - self.assertFalse(user['enabled']) - # set the user to enabled and authenticate - user['enabled'] = True - PROVIDERS.identity_api.update_user(user['id'], user) - user = PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user['id'], password=self.password - ) - self.assertTrue(user['enabled']) + with self.make_request(): + self.assertRaises(exception.UserDisabled, + PROVIDERS.identity_api.authenticate, + user_id=user['id'], + password=self.password) + # verify that the user is actually disabled + user = PROVIDERS.identity_api.get_user(user['id']) + self.assertFalse(user['enabled']) + # set the user to enabled and authenticate + user['enabled'] = True + PROVIDERS.identity_api.update_user(user['id'], user) + user = PROVIDERS.identity_api.authenticate( + user_id=user['id'], password=self.password + ) + self.assertTrue(user['enabled']) def test_authenticate_user_not_disabled_due_to_inactivity(self): # create user and set last_active_at just below the max @@ -294,9 +294,10 @@ class DisableInactiveUserTests(test_backend_sql.SqlTests): datetime.datetime.utcnow() - datetime.timedelta(days=self.max_inactive_days - 1)).date() user = self._create_user(self.user_dict, last_active_at) - user = PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user['id'], password=self.password - ) + with self.make_request(): + user = PROVIDERS.identity_api.authenticate( + user_id=user['id'], password=self.password + ) self.assertTrue(user['enabled']) def test_get_user_disabled_due_to_inactivity(self): @@ -392,22 +393,21 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests): password = uuid.uuid4().hex user = self._create_user(password) # Attempt to change to the same password - self.assertRaises(exception.PasswordValidationError, - PROVIDERS.identity_api.change_password, - self.make_request(), - user_id=user['id'], - original_password=password, - new_password=password) - # Attempt to change to a unique password - new_password = uuid.uuid4().hex - self.assertValidChangePassword(user['id'], password, new_password) - # Attempt to change back to the initial password - self.assertRaises(exception.PasswordValidationError, - PROVIDERS.identity_api.change_password, - self.make_request(), - user_id=user['id'], - original_password=new_password, - new_password=password) + with self.make_request(): + self.assertRaises(exception.PasswordValidationError, + PROVIDERS.identity_api.change_password, + user_id=user['id'], + original_password=password, + new_password=password) + # Attempt to change to a unique password + new_password = uuid.uuid4().hex + self.assertValidChangePassword(user['id'], password, new_password) + # Attempt to change back to the initial password + self.assertRaises(exception.PasswordValidationError, + PROVIDERS.identity_api.change_password, + user_id=user['id'], + original_password=new_password, + new_password=password) def test_validate_password_history_with_valid_password(self): passwords = [uuid.uuid4().hex, uuid.uuid4().hex, uuid.uuid4().hex, @@ -441,12 +441,12 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests): # Self-service change password self.assertValidChangePassword(user['id'], passwords[0], passwords[1]) # Attempt to update with a previous password - self.assertRaises(exception.PasswordValidationError, - PROVIDERS.identity_api.change_password, - self.make_request(), - user_id=user['id'], - original_password=passwords[1], - new_password=passwords[0]) + with self.make_request(): + self.assertRaises(exception.PasswordValidationError, + PROVIDERS.identity_api.change_password, + user_id=user['id'], + original_password=passwords[1], + new_password=passwords[0]) def test_disable_password_history_and_repeat_same_password(self): self.config_fixture.config(group='security_compliance', @@ -462,22 +462,23 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests): user = self._create_user(passwords[0]) # Attempt to change password to a unique password user['password'] = passwords[1] - PROVIDERS.identity_api.update_user(user['id'], user) - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user['id'], password=passwords[1] - ) - # Attempt to change password with the same password - user['password'] = passwords[1] - PROVIDERS.identity_api.update_user(user['id'], user) - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user['id'], password=passwords[1] - ) - # Attempt to change password with the initial password - user['password'] = passwords[0] - PROVIDERS.identity_api.update_user(user['id'], user) - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user['id'], password=passwords[0] - ) + with self.make_request(): + PROVIDERS.identity_api.update_user(user['id'], user) + PROVIDERS.identity_api.authenticate( + user_id=user['id'], password=passwords[1] + ) + # Attempt to change password with the same password + user['password'] = passwords[1] + PROVIDERS.identity_api.update_user(user['id'], user) + PROVIDERS.identity_api.authenticate( + user_id=user['id'], password=passwords[1] + ) + # Attempt to change password with the initial password + user['password'] = passwords[0] + PROVIDERS.identity_api.update_user(user['id'], user) + PROVIDERS.identity_api.authenticate( + user_id=user['id'], password=passwords[0] + ) def test_truncate_passwords(self): user = self._create_user(uuid.uuid4().hex) @@ -535,13 +536,14 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests): return PROVIDERS.identity_api.create_user(user) def assertValidChangePassword(self, user_id, password, new_password): - PROVIDERS.identity_api.change_password( - self.make_request(), user_id=user_id, original_password=password, - new_password=new_password - ) - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user_id, password=new_password - ) + with self.make_request(): + PROVIDERS.identity_api.change_password( + user_id=user_id, original_password=password, + new_password=new_password + ) + PROVIDERS.identity_api.authenticate( + user_id=user_id, password=new_password + ) def _add_passwords_to_history(self, user, n): for _ in range(n): @@ -573,24 +575,23 @@ class LockingOutUserTests(test_backend_sql.SqlTests): self.user = PROVIDERS.identity_api.create_user(user_dict) def test_locking_out_user_after_max_failed_attempts(self): - # authenticate with wrong password - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=self.user['id'], - password=uuid.uuid4().hex) - # authenticate with correct password - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=self.user['id'], - password=self.password - ) - # test locking out user after max failed attempts - self._fail_auth_repeatedly(self.user['id']) - self.assertRaises(exception.AccountLocked, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=self.user['id'], - password=uuid.uuid4().hex) + with self.make_request(): + # authenticate with wrong password + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=self.user['id'], + password=uuid.uuid4().hex) + # authenticate with correct password + PROVIDERS.identity_api.authenticate( + user_id=self.user['id'], + password=self.password + ) + # test locking out user after max failed attempts + self._fail_auth_repeatedly(self.user['id']) + self.assertRaises(exception.AccountLocked, + PROVIDERS.identity_api.authenticate, + user_id=self.user['id'], + password=uuid.uuid4().hex) def test_lock_out_for_ignored_user(self): # mark the user as exempt from failed password attempts @@ -601,90 +602,89 @@ class LockingOutUserTests(test_backend_sql.SqlTests): # fail authentication repeatedly the max number of times self._fail_auth_repeatedly(self.user['id']) # authenticate with wrong password, account should not be locked - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=self.user['id'], - password=uuid.uuid4().hex) - # authenticate with correct password, account should not be locked - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=self.user['id'], - password=self.password - ) + with self.make_request(): + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=self.user['id'], + password=uuid.uuid4().hex) + # authenticate with correct password, account should not be locked + PROVIDERS.identity_api.authenticate( + user_id=self.user['id'], + password=self.password + ) def test_set_enabled_unlocks_user(self): - # lockout user - self._fail_auth_repeatedly(self.user['id']) - self.assertRaises(exception.AccountLocked, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=self.user['id'], - password=uuid.uuid4().hex) - # set enabled, user should be unlocked - self.user['enabled'] = True - PROVIDERS.identity_api.update_user(self.user['id'], self.user) - user_ret = PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=self.user['id'], - password=self.password - ) - self.assertTrue(user_ret['enabled']) + with self.make_request(): + # lockout user + self._fail_auth_repeatedly(self.user['id']) + self.assertRaises(exception.AccountLocked, + PROVIDERS.identity_api.authenticate, + user_id=self.user['id'], + password=uuid.uuid4().hex) + # set enabled, user should be unlocked + self.user['enabled'] = True + PROVIDERS.identity_api.update_user(self.user['id'], self.user) + user_ret = PROVIDERS.identity_api.authenticate( + user_id=self.user['id'], + password=self.password + ) + self.assertTrue(user_ret['enabled']) def test_lockout_duration(self): # freeze time with freezegun.freeze_time(datetime.datetime.utcnow()) as frozen_time: - # lockout user - self._fail_auth_repeatedly(self.user['id']) - self.assertRaises(exception.AccountLocked, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=self.user['id'], - password=uuid.uuid4().hex) - # freeze time past the duration, user should be unlocked and failed - # auth count should get reset - frozen_time.tick(delta=datetime.timedelta( - seconds=CONF.security_compliance.lockout_duration + 1)) - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=self.user['id'], - password=self.password - ) - # test failed auth count was reset by authenticating with the wrong - # password, should raise an assertion error and not account locked - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=self.user['id'], - password=uuid.uuid4().hex) + with self.make_request(): + # lockout user + self._fail_auth_repeatedly(self.user['id']) + self.assertRaises(exception.AccountLocked, + PROVIDERS.identity_api.authenticate, + user_id=self.user['id'], + password=uuid.uuid4().hex) + # freeze time past the duration, user should be unlocked and + # failed auth count should get reset + frozen_time.tick(delta=datetime.timedelta( + seconds=CONF.security_compliance.lockout_duration + 1)) + PROVIDERS.identity_api.authenticate( + user_id=self.user['id'], + password=self.password + ) + # test failed auth count was reset by authenticating with the + # wrong password, should raise an assertion error and not + # account locked + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=self.user['id'], + password=uuid.uuid4().hex) def test_lockout_duration_failed_auth_cnt_resets(self): # freeze time with freezegun.freeze_time(datetime.datetime.utcnow()) as frozen_time: - # lockout user - self._fail_auth_repeatedly(self.user['id']) - self.assertRaises(exception.AccountLocked, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=self.user['id'], - password=uuid.uuid4().hex) - # freeze time past the duration, failed_auth_cnt should reset - frozen_time.tick(delta=datetime.timedelta( - seconds=CONF.security_compliance.lockout_duration + 1)) - # repeat failed auth the max times - self._fail_auth_repeatedly(self.user['id']) - # test user account is locked - self.assertRaises(exception.AccountLocked, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=self.user['id'], - password=uuid.uuid4().hex) + with self.make_request(): + # lockout user + self._fail_auth_repeatedly(self.user['id']) + self.assertRaises(exception.AccountLocked, + PROVIDERS.identity_api.authenticate, + user_id=self.user['id'], + password=uuid.uuid4().hex) + # freeze time past the duration, failed_auth_cnt should reset + frozen_time.tick(delta=datetime.timedelta( + seconds=CONF.security_compliance.lockout_duration + 1)) + # repeat failed auth the max times + self._fail_auth_repeatedly(self.user['id']) + # test user account is locked + self.assertRaises(exception.AccountLocked, + PROVIDERS.identity_api.authenticate, + user_id=self.user['id'], + password=uuid.uuid4().hex) def _fail_auth_repeatedly(self, user_id): wrong_password = uuid.uuid4().hex for _ in range(CONF.security_compliance.lockout_failure_attempts): - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user_id, - password=wrong_password) + with self.make_request(): + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=user_id, + password=wrong_password) class PasswordExpiresValidationTests(test_backend_sql.SqlTests): @@ -705,11 +705,11 @@ class PasswordExpiresValidationTests(test_backend_sql.SqlTests): ) user = self._create_user(self.user_dict, password_created_at) # test password is expired - self.assertRaises(exception.PasswordExpired, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user['id'], - password=self.password) + with self.make_request(): + self.assertRaises(exception.PasswordExpired, + PROVIDERS.identity_api.authenticate, + user_id=user['id'], + password=self.password) def test_authenticate_with_non_expired_password(self): # set password created_at so that the password will not expire @@ -720,9 +720,10 @@ class PasswordExpiresValidationTests(test_backend_sql.SqlTests): ) user = self._create_user(self.user_dict, password_created_at) # test password is not expired - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user['id'], password=self.password - ) + with self.make_request(): + PROVIDERS.identity_api.authenticate( + user_id=user['id'], password=self.password + ) def test_authenticate_with_expired_password_for_ignore_user_option(self): # set user to have the 'ignore_password_expiry' option set to False @@ -735,22 +736,22 @@ class PasswordExpiresValidationTests(test_backend_sql.SqlTests): days=CONF.security_compliance.password_expires_days + 1) ) user = self._create_user(self.user_dict, password_created_at) - self.assertRaises(exception.PasswordExpired, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user['id'], - password=self.password) + with self.make_request(): + self.assertRaises(exception.PasswordExpired, + PROVIDERS.identity_api.authenticate, + user_id=user['id'], + password=self.password) - # update user to explicitly have the expiry option to True - user['options'][ - iro.IGNORE_PASSWORD_EXPIRY_OPT.option_name] = True - user = PROVIDERS.identity_api.update_user( - user['id'], user - ) - # test password is not expired due to ignore option - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user['id'], password=self.password - ) + # update user to explicitly have the expiry option to True + user['options'][ + iro.IGNORE_PASSWORD_EXPIRY_OPT.option_name] = True + user = PROVIDERS.identity_api.update_user( + user['id'], user + ) + # test password is not expired due to ignore option + PROVIDERS.identity_api.authenticate( + user_id=user['id'], password=self.password + ) def _get_test_user_dict(self, password): test_user_dict = { @@ -790,12 +791,12 @@ class MinimumPasswordAgeTests(test_backend_sql.SqlTests): self.assertValidChangePassword(self.user['id'], self.initial_password, new_password) # user cannot change password before min age - self.assertRaises(exception.PasswordAgeValidationError, - PROVIDERS.identity_api.change_password, - self.make_request(), - user_id=self.user['id'], - original_password=new_password, - new_password=uuid.uuid4().hex) + with self.make_request(): + self.assertRaises(exception.PasswordAgeValidationError, + PROVIDERS.identity_api.change_password, + user_id=self.user['id'], + original_password=new_password, + new_password=uuid.uuid4().hex) def test_user_can_change_password_after_min_age(self): # user can change password after create @@ -818,12 +819,13 @@ class MinimumPasswordAgeTests(test_backend_sql.SqlTests): self.assertValidChangePassword(self.user['id'], self.initial_password, new_password) # user cannot change password before min age - self.assertRaises(exception.PasswordAgeValidationError, - PROVIDERS.identity_api.change_password, - self.make_request(), - user_id=self.user['id'], - original_password=new_password, - new_password=uuid.uuid4().hex) + + with self.make_request(): + self.assertRaises(exception.PasswordAgeValidationError, + PROVIDERS.identity_api.change_password, + user_id=self.user['id'], + original_password=new_password, + new_password=uuid.uuid4().hex) # admin reset new_password = uuid.uuid4().hex self.user['password'] = new_password @@ -833,13 +835,14 @@ class MinimumPasswordAgeTests(test_backend_sql.SqlTests): uuid.uuid4().hex) def assertValidChangePassword(self, user_id, password, new_password): - PROVIDERS.identity_api.change_password( - self.make_request(), user_id=user_id, original_password=password, - new_password=new_password - ) - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user_id, password=new_password - ) + with self.make_request(): + PROVIDERS.identity_api.change_password( + user_id=user_id, original_password=password, + new_password=new_password + ) + PROVIDERS.identity_api.authenticate( + user_id=user_id, password=new_password + ) def _create_new_user(self, password): user = { @@ -881,16 +884,17 @@ class ChangePasswordRequiredAfterFirstUse(test_backend_sql.SqlTests): return PROVIDERS.identity_api.create_user(user_dict) def assertPasswordIsExpired(self, user_id, password): - self.assertRaises(exception.PasswordExpired, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user_id, - password=password) + with self.make_request(): + self.assertRaises(exception.PasswordExpired, + PROVIDERS.identity_api.authenticate, + user_id=user_id, + password=password) def assertPasswordIsNotExpired(self, user_id, password): - PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user_id, password=password - ) + with self.make_request(): + PROVIDERS.identity_api.authenticate( + user_id=user_id, password=password + ) def test_password_expired_after_create(self): # create user, password expired @@ -899,9 +903,10 @@ class ChangePasswordRequiredAfterFirstUse(test_backend_sql.SqlTests): self.assertPasswordIsExpired(user['id'], initial_password) # change password (self-service), password not expired new_password = uuid.uuid4().hex - PROVIDERS.identity_api.change_password( - self.make_request(), user['id'], initial_password, new_password - ) + with self.make_request(): + PROVIDERS.identity_api.change_password( + user['id'], initial_password, new_password + ) self.assertPasswordIsNotExpired(user['id'], new_password) def test_password_expired_after_reset(self): @@ -920,9 +925,10 @@ class ChangePasswordRequiredAfterFirstUse(test_backend_sql.SqlTests): self.assertPasswordIsExpired(user['id'], admin_password) # change password (self-service), password not expired new_password = uuid.uuid4().hex - PROVIDERS.identity_api.change_password( - self.make_request(), user['id'], admin_password, new_password - ) + with self.make_request(): + PROVIDERS.identity_api.change_password( + user['id'], admin_password, new_password + ) self.assertPasswordIsNotExpired(user['id'], new_password) def test_password_not_expired_when_feature_disabled(self): diff --git a/keystone/tests/unit/identity/test_backends.py b/keystone/tests/unit/identity/test_backends.py index b402fe1382..64a95a1ff2 100644 --- a/keystone/tests/unit/identity/test_backends.py +++ b/keystone/tests/unit/identity/test_backends.py @@ -43,25 +43,25 @@ class IdentityTests(object): return domain_id def test_authenticate_bad_user(self): - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=uuid.uuid4().hex, - password=self.user_foo['password']) + with self.make_request(): + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=uuid.uuid4().hex, + password=self.user_foo['password']) def test_authenticate_bad_password(self): - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=self.user_foo['id'], - password=uuid.uuid4().hex) + with self.make_request(): + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=self.user_foo['id'], + password=uuid.uuid4().hex) def test_authenticate(self): - user_ref = PROVIDERS.identity_api.authenticate( - self.make_request(), - user_id=self.user_sna['id'], - password=self.user_sna['password']) - # NOTE(termie): the password field is left in user_sna to make + with self.make_request(): + user_ref = PROVIDERS.identity_api.authenticate( + user_id=self.user_sna['id'], + password=self.user_sna['password']) + # NOTE(termie): the password field is left in user_sna to make # it easier to authenticate in tests, but should # not be returned by the api self.user_sna.pop('password') @@ -83,10 +83,10 @@ class IdentityTests(object): PROVIDERS.assignment_api.add_role_to_user_and_project( new_user['id'], self.tenant_baz['id'], role_member['id'] ) - user_ref = PROVIDERS.identity_api.authenticate( - self.make_request(), - user_id=new_user['id'], - password=user['password']) + with self.make_request(): + user_ref = PROVIDERS.identity_api.authenticate( + user_id=new_user['id'], + password=user['password']) self.assertNotIn('password', user_ref) # NOTE(termie): the password field is left in user_sna to make # it easier to authenticate in tests, but should @@ -103,11 +103,11 @@ class IdentityTests(object): user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) PROVIDERS.identity_api.create_user(user) - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=id_, - password='password') + with self.make_request(): + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=id_, + password='password') def test_create_unicode_user_name(self): unicode_name = u'name \u540d\u5b57' @@ -394,16 +394,15 @@ class IdentityTests(object): PROVIDERS.identity_api.get_user(user['id']) # Make sure the user is not allowed to login # with a password that is empty string or None - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user['id'], - password='') - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user['id'], - password=None) + with self.make_request(): + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=user['id'], + password='') + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=user['id'], + password=None) def test_create_user_none_password(self): user = unit.new_user_ref(password=None, @@ -412,16 +411,15 @@ class IdentityTests(object): PROVIDERS.identity_api.get_user(user['id']) # Make sure the user is not allowed to login # with a password that is empty string or None - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user['id'], - password='') - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user['id'], - password=None) + with self.make_request(): + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=user['id'], + password='') + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=user['id'], + password=None) def test_create_user_invalid_name_fails(self): user = unit.new_user_ref(name=None, diff --git a/keystone/tests/unit/server/test_keystone_flask.py b/keystone/tests/unit/server/test_keystone_flask.py index a0deff326d..69ea3343d1 100644 --- a/keystone/tests/unit/server/test_keystone_flask.py +++ b/keystone/tests/unit/server/test_keystone_flask.py @@ -15,6 +15,7 @@ import uuid import fixtures import flask import flask_restful +import functools from oslo_policy import policy from oslo_serialization import jsonutils from testtools import matchers @@ -402,11 +403,19 @@ class TestKeystoneFlaskCommon(rest.RestfulTestCase): expected_status_code=420) def test_construct_resource_map(self): + resource_name = 'arguments' param_relation = json_home.build_v3_parameter_relation( 'argument_id') + alt_rel_func = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='extension', extension_version='1.0') url = '/v3/arguments/' - old_url = ['/v3/old_arguments/'] - resource_name = 'arguments' + old_url = [dict( + url='/v3/old_arguments/', + json_home=flask_common.construct_json_home_data( + rel='arguments', + resource_relation_func=alt_rel_func) + )] mapping = flask_common.construct_resource_map( resource=_TestResourceWithCollectionInfo, @@ -420,13 +429,17 @@ class TestKeystoneFlaskCommon(rest.RestfulTestCase): self.assertEqual(_TestResourceWithCollectionInfo, mapping.resource) self.assertEqual(url, mapping.url) - self.assertEqual(old_url, mapping.alternate_urls) self.assertEqual(json_home.build_v3_resource_relation(resource_name), mapping.json_home_data.rel) self.assertEqual(json_home.Status.EXPERIMENTAL, mapping.json_home_data.status) self.assertEqual({'argument_id': param_relation}, mapping.json_home_data.path_vars) + # Check the alternate URL data is populated sanely + self.assertEqual(1, len(mapping.alternate_urls)) + alt_url_data = mapping.alternate_urls[0] + self.assertEqual(old_url[0]['url'], alt_url_data['url']) + self.assertEqual(old_url[0]['json_home'], alt_url_data['json_home']) def test_instantiate_and_register_to_app(self): # Test that automatic instantiation and registration to app works. diff --git a/keystone/tests/unit/test_auth_plugin.py b/keystone/tests/unit/test_auth_plugin.py index 42ae4cc5d1..f3598ba5df 100644 --- a/keystone/tests/unit/test_auth_plugin.py +++ b/keystone/tests/unit/test_auth_plugin.py @@ -17,6 +17,7 @@ import uuid import mock import stevedore +from keystone.api._shared import authentication from keystone import auth from keystone.auth.plugins import base from keystone.auth.plugins import mapped @@ -32,7 +33,7 @@ DEMO_USER_ID = uuid.uuid4().hex class SimpleChallengeResponse(base.AuthMethodHandler): - def authenticate(self, context, auth_payload): + def authenticate(self, auth_payload): response_data = {} if 'response' in auth_payload: if auth_payload['response'] != EXPECTED_RESPONSE: @@ -50,9 +51,6 @@ class SimpleChallengeResponse(base.AuthMethodHandler): class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase): - def setUp(self): - super(TestAuthPlugin, self).setUp() - self.api = auth.controllers.Auth() def test_unsupported_auth_method(self): method_name = uuid.uuid4().hex @@ -85,7 +83,8 @@ class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase): auth_info = auth.core.AuthInfo.create(auth_data) auth_context = auth.core.AuthContext(method_names=[]) try: - self.api.authenticate(self.make_request(), auth_info, auth_context) + with self.make_request(): + authentication.authenticate(auth_info, auth_context) except exception.AdditionalAuthRequired as e: self.assertIn('methods', e.authentication) self.assertIn(METHOD_NAME, e.authentication['methods']) @@ -99,7 +98,8 @@ class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase): auth_data = {'identity': auth_data} auth_info = auth.core.AuthInfo.create(auth_data) auth_context = auth.core.AuthContext(method_names=[]) - self.api.authenticate(self.make_request(), auth_info, auth_context) + with self.make_request(): + authentication.authenticate(auth_info, auth_context) self.assertEqual(DEMO_USER_ID, auth_context['user_id']) # test incorrect response @@ -109,11 +109,11 @@ class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase): auth_data = {'identity': auth_data} auth_info = auth.core.AuthInfo.create(auth_data) auth_context = auth.core.AuthContext(method_names=[]) - self.assertRaises(exception.Unauthorized, - self.api.authenticate, - self.make_request(), - auth_info, - auth_context) + with self.make_request(): + self.assertRaises(exception.Unauthorized, + authentication.authenticate, + auth_info, + auth_context) def test_duplicate_method(self): # Having the same method twice doesn't cause load_auth_methods to fail. @@ -138,9 +138,6 @@ class TestAuthPluginDynamicOptions(TestAuthPlugin): class TestMapped(unit.TestCase): - def setUp(self): - super(TestMapped, self).setUp() - self.api = auth.controllers.Auth() def config_files(self): config_files = super(TestMapped, self).config_files() @@ -151,7 +148,6 @@ class TestMapped(unit.TestCase): with mock.patch.object(auth.plugins.mapped.Mapped, 'authenticate', return_value=None) as authenticate: - request = self.make_request() auth_data = { 'identity': { 'methods': [method_name], @@ -162,10 +158,10 @@ class TestMapped(unit.TestCase): auth_context = auth.core.AuthContext( method_names=[], user_id=uuid.uuid4().hex) - self.api.authenticate(request, auth_info, auth_context) + with self.make_request(): + authentication.authenticate(auth_info, auth_context) # make sure Mapped plugin got invoked with the correct payload - ((context, auth_payload), - kwargs) = authenticate.call_args + ((auth_payload,), kwargs) = authenticate.call_args self.assertEqual(method_name, auth_payload['protocol']) def test_mapped_with_remote_user(self): @@ -186,11 +182,10 @@ class TestMapped(unit.TestCase): 'authenticate', return_value=None) as authenticate: auth_info = auth.core.AuthInfo.create(auth_data) - request = self.make_request(environ={'REMOTE_USER': 'foo@idp.com'}) - self.api.authenticate(request, auth_info, auth_context) + with self.make_request(environ={'REMOTE_USER': 'foo@idp.com'}): + authentication.authenticate(auth_info, auth_context) # make sure Mapped plugin got invoked with the correct payload - ((context, auth_payload), - kwargs) = authenticate.call_args + ((auth_payload,), kwargs) = authenticate.call_args self.assertEqual(method_name, auth_payload['protocol']) @mock.patch('keystone.auth.plugins.mapped.PROVIDERS') @@ -203,15 +198,18 @@ class TestMapped(unit.TestCase): mock_providers.role_api = mock.Mock() test_mapped = mapped.Mapped() - request = self.make_request() auth_payload = {'identity_provider': 'test_provider'} - self.assertRaises(exception.ValidationError, test_mapped.authenticate, - request, auth_payload) + with self.make_request(): + self.assertRaises( + exception.ValidationError, test_mapped.authenticate, + auth_payload) auth_payload = {'protocol': 'saml2'} - self.assertRaises(exception.ValidationError, test_mapped.authenticate, - request, auth_payload) + with self.make_request(): + self.assertRaises( + exception.ValidationError, test_mapped.authenticate, + auth_payload) def test_supporting_multiple_methods(self): method_names = ('saml2', 'openid', 'x509', 'mapped') diff --git a/keystone/tests/unit/test_backend_ldap.py b/keystone/tests/unit/test_backend_ldap.py index 381dd94761..d8e2a82b6e 100644 --- a/keystone/tests/unit/test_backend_ldap.py +++ b/keystone/tests/unit/test_backend_ldap.py @@ -765,11 +765,11 @@ class BaseLDAPIdentity(LDAPTestSetup, IdentityTests, AssignmentTests, driver.user.LDAP_USER = None driver.user.LDAP_PASSWORD = None - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=user['id'], - password=None) + with self.make_request(): + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=user['id'], + password=None) @mock.patch.object(versionutils, 'report_deprecated_feature') def test_user_crud(self, mock_deprecator): @@ -1988,10 +1988,10 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity, unit.TestCase): driver = PROVIDERS.identity_api._select_identity_driver( CONF.identity.default_domain_id) driver.user.enabled_emulation_dn = 'cn=test,dc=test' - PROVIDERS.identity_api.authenticate( - self.make_request(), - user_id=self.user_foo['id'], - password=self.user_foo['password']) + with self.make_request(): + PROVIDERS.identity_api.authenticate( + user_id=self.user_foo['id'], + password=self.user_foo['password']) def test_user_enable_attribute_mask(self): self.skip_test_overrides( @@ -2334,10 +2334,10 @@ class BaseMultiLDAPandSQLIdentity(object): for user_num in range(self.domain_count): user = 'user%s' % user_num - PROVIDERS.identity_api.authenticate( - self.make_request(), - user_id=users[user]['id'], - password=users[user]['password']) + with self.make_request(): + PROVIDERS.identity_api.authenticate( + user_id=users[user]['id'], + password=users[user]['password']) class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, diff --git a/keystone/tests/unit/test_backend_ldap_pool.py b/keystone/tests/unit/test_backend_ldap_pool.py index 959872f2a9..1754942fb3 100644 --- a/keystone/tests/unit/test_backend_ldap_pool.py +++ b/keystone/tests/unit/test_backend_ldap_pool.py @@ -176,10 +176,10 @@ class LdapPoolCommonTestMixin(object): # authenticate so that connection is added to pool before password # change - user_ref = PROVIDERS.identity_api.authenticate( - self.make_request(), - user_id=self.user_sna['id'], - password=self.user_sna['password']) + with self.make_request(): + user_ref = PROVIDERS.identity_api.authenticate( + user_id=self.user_sna['id'], + password=self.user_sna['password']) self.user_sna.pop('password') self.user_sna['enabled'] = True @@ -191,10 +191,10 @@ class LdapPoolCommonTestMixin(object): # now authenticate again to make sure new password works with # connection pool - user_ref2 = PROVIDERS.identity_api.authenticate( - self.make_request(), - user_id=self.user_sna['id'], - password=new_password) + with self.make_request(): + user_ref2 = PROVIDERS.identity_api.authenticate( + user_id=self.user_sna['id'], + password=new_password) user_ref.pop('password') self.assertUserDictEqual(user_ref, user_ref2) @@ -202,11 +202,11 @@ class LdapPoolCommonTestMixin(object): # Authentication with old password would not work here as there # is only one connection in pool which get bind again with updated # password..so no old bind is maintained in this case. - self.assertRaises(AssertionError, - PROVIDERS.identity_api.authenticate, - self.make_request(), - user_id=self.user_sna['id'], - password=old_password) + with self.make_request(): + self.assertRaises(AssertionError, + PROVIDERS.identity_api.authenticate, + user_id=self.user_sna['id'], + password=old_password) class LDAPIdentity(LdapPoolCommonTestMixin, diff --git a/keystone/tests/unit/test_cli.py b/keystone/tests/unit/test_cli.py index 71679aea4c..9e6f11dec0 100644 --- a/keystone/tests/unit/test_cli.py +++ b/keystone/tests/unit/test_cli.py @@ -150,10 +150,10 @@ class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase): self.assertEqual(system_roles[0]['id'], admin_role['id']) # NOTE(morganfainberg): Pass an empty context, it isn't used by # `authenticate` method. - PROVIDERS.identity_api.authenticate( - self.make_request(), - user['id'], - bootstrap.password) + with self.make_request(): + PROVIDERS.identity_api.authenticate( + user['id'], + bootstrap.password) if bootstrap.region_id: region = PROVIDERS.catalog_api.get_region(bootstrap.region_id) @@ -284,10 +284,10 @@ class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase): self._do_test_bootstrap(self.bootstrap) # Sanity check that the original password works again. - PROVIDERS.identity_api.authenticate( - self.make_request(), - user_id, - self.bootstrap.password) + with self.make_request(): + PROVIDERS.identity_api.authenticate( + user_id, + self.bootstrap.password) class CliBootStrapTestCaseWithEnvironment(CliBootStrapTestCase): diff --git a/keystone/tests/unit/test_ldap_pool_livetest.py b/keystone/tests/unit/test_ldap_pool_livetest.py index e143b98671..c96ccb7156 100644 --- a/keystone/tests/unit/test_ldap_pool_livetest.py +++ b/keystone/tests/unit/test_ldap_pool_livetest.py @@ -109,10 +109,10 @@ class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin, CONF.identity.default_domain_id, password=password) - PROVIDERS.identity_api.authenticate( - self.make_request(), - user_id=user['id'], - password=password) + with self.make_request(): + PROVIDERS.identity_api.authenticate( + user_id=user['id'], + password=password) return PROVIDERS.identity_api.get_user(user['id']) @@ -179,8 +179,9 @@ class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin, # successfully which is not desired if password change is frequent # use case in a deployment. # This can happen in multiple concurrent connections case only. - user_ref = PROVIDERS.identity_api.authenticate( - self.make_request(), user_id=user['id'], password=old_password) + with self.make_request(): + user_ref = PROVIDERS.identity_api.authenticate( + user_id=user['id'], password=old_password) self.assertDictEqual(user, user_ref) diff --git a/keystone/tests/unit/test_v3.py b/keystone/tests/unit/test_v3.py index 097be065a0..9e971a9107 100644 --- a/keystone/tests/unit/test_v3.py +++ b/keystone/tests/unit/test_v3.py @@ -21,7 +21,6 @@ from six.moves import http_client from testtools import matchers import webtest -from keystone import auth from keystone.common import authorization from keystone.common import cache from keystone.common import provider_api @@ -1215,18 +1214,6 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, for attribute in attributes: self.assertIsNotNone(entity.get(attribute)) - def build_external_auth_request(self, remote_user, - remote_domain=None, auth_data=None, - kerberos=False): - environment = self.build_external_auth_environ( - remote_user, remote_domain) - if not auth_data: - auth_data = self.build_authentication_request( - kerberos=kerberos)['auth'] - auth_info = auth.core.AuthInfo.create(auth_data) - auth_context = auth.core.AuthContext(method_names=[]) - return self.make_request(environ=environment), auth_info, auth_context - def build_external_auth_environ(self, remote_user, remote_domain=None): environment = {'REMOTE_USER': remote_user, 'AUTH_TYPE': 'Negotiate'} if remote_domain: diff --git a/keystone/tests/unit/test_v3_federation.py b/keystone/tests/unit/test_v3_federation.py index 9ad289acac..0b81c3de09 100644 --- a/keystone/tests/unit/test_v3_federation.py +++ b/keystone/tests/unit/test_v3_federation.py @@ -19,6 +19,7 @@ from testtools import matchers import uuid import fixtures +import flask from lxml import etree import mock from oslo_serialization import jsonutils @@ -32,12 +33,12 @@ xmldsig = importutils.try_import("saml2.xmldsig") if not xmldsig: xmldsig = importutils.try_import("xmldsig") -from keystone.auth import controllers as auth_controllers -from keystone.common import controller +from keystone.api._shared import authentication +from keystone.api import auth as auth_api from keystone.common import provider_api +from keystone.common import render_token import keystone.conf from keystone import exception -from keystone.federation import controllers as federation_controllers from keystone.federation import idp as keystone_idp from keystone.models import token_model from keystone import notifications @@ -149,13 +150,13 @@ class FederatedSetupMixin(object): idp=None, assertion='EMPLOYEE_ASSERTION', environment=None): - api = federation_controllers.Auth() environment = environment or {} environment.update(getattr(mapping_fixtures, assertion)) - request = self.make_request(environ=environment) - if idp is None: - idp = self.IDP - r = api.federated_authentication(request, idp, self.PROTOCOL) + with self.make_request(environ=environment): + if idp is None: + idp = self.IDP + r = authentication.federated_authenticate_for_token( + protocol_id=self.PROTOCOL, identity_provider=idp) return r def idp_ref(self, id=None): @@ -198,9 +199,9 @@ class FederatedSetupMixin(object): } } - def _inject_assertion(self, request, variant): + def _inject_assertion(self, variant): assertion = getattr(mapping_fixtures, variant) - request.context_dict['environment'].update(assertion) + flask.request.environ.update(assertion) def load_federation_sample_data(self): """Inject additional data.""" @@ -759,60 +760,65 @@ class FederatedSetupMixin(object): PROVIDERS.federation_api.create_protocol( self.idp_with_remote['id'], self.proto_saml['id'], self.proto_saml ) - # Generate fake tokens - request = self.make_request() - self.tokens = {} - VARIANTS = ('EMPLOYEE_ASSERTION', 'CUSTOMER_ASSERTION', - 'ADMIN_ASSERTION') - api = auth_controllers.Auth() - for variant in VARIANTS: - self._inject_assertion(request, variant) - r = api.authenticate_for_token(request, self.UNSCOPED_V3_SAML2_REQ) - self.tokens[variant] = r.headers.get('X-Subject-Token') + with self.make_request(): + self.tokens = {} + VARIANTS = ('EMPLOYEE_ASSERTION', 'CUSTOMER_ASSERTION', + 'ADMIN_ASSERTION') + for variant in VARIANTS: + self._inject_assertion(variant) + r = authentication.authenticate_for_token( + self.UNSCOPED_V3_SAML2_REQ) + self.tokens[variant] = r.id - self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN = self._scope_request( - uuid.uuid4().hex, 'project', self.proj_customers['id']) + self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN = ( + self._scope_request( + uuid.uuid4().hex, 'project', self.proj_customers['id'])) - self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE = self._scope_request( - self.tokens['EMPLOYEE_ASSERTION'], 'project', - self.proj_employees['id']) + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE = ( + self._scope_request( + self.tokens['EMPLOYEE_ASSERTION'], 'project', + self.proj_employees['id'])) - self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN = self._scope_request( - self.tokens['ADMIN_ASSERTION'], 'project', - self.proj_employees['id']) + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN = self._scope_request( + self.tokens['ADMIN_ASSERTION'], 'project', + self.proj_employees['id']) - self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN = self._scope_request( - self.tokens['ADMIN_ASSERTION'], 'project', - self.proj_customers['id']) + self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN = self._scope_request( + self.tokens['ADMIN_ASSERTION'], 'project', + self.proj_customers['id']) - self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER = self._scope_request( - self.tokens['CUSTOMER_ASSERTION'], 'project', - self.proj_employees['id']) + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER = ( + self._scope_request( + self.tokens['CUSTOMER_ASSERTION'], 'project', + self.proj_employees['id'])) - self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER = self._scope_request( - self.tokens['CUSTOMER_ASSERTION'], 'project', - self.project_inherited['id']) + self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER = ( + self._scope_request( + self.tokens['CUSTOMER_ASSERTION'], 'project', + self.project_inherited['id'])) - self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER = self._scope_request( - self.tokens['CUSTOMER_ASSERTION'], 'domain', self.domainA['id']) + self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER = self._scope_request( + self.tokens['CUSTOMER_ASSERTION'], 'domain', + self.domainA['id']) - self.TOKEN_SCOPE_DOMAIN_B_FROM_CUSTOMER = self._scope_request( - self.tokens['CUSTOMER_ASSERTION'], 'domain', - self.domainB['id']) + self.TOKEN_SCOPE_DOMAIN_B_FROM_CUSTOMER = self._scope_request( + self.tokens['CUSTOMER_ASSERTION'], 'domain', + self.domainB['id']) - self.TOKEN_SCOPE_DOMAIN_D_FROM_CUSTOMER = self._scope_request( - self.tokens['CUSTOMER_ASSERTION'], 'domain', self.domainD['id']) + self.TOKEN_SCOPE_DOMAIN_D_FROM_CUSTOMER = self._scope_request( + self.tokens['CUSTOMER_ASSERTION'], 'domain', + self.domainD['id']) - self.TOKEN_SCOPE_DOMAIN_A_FROM_ADMIN = self._scope_request( - self.tokens['ADMIN_ASSERTION'], 'domain', self.domainA['id']) + self.TOKEN_SCOPE_DOMAIN_A_FROM_ADMIN = self._scope_request( + self.tokens['ADMIN_ASSERTION'], 'domain', self.domainA['id']) - self.TOKEN_SCOPE_DOMAIN_B_FROM_ADMIN = self._scope_request( - self.tokens['ADMIN_ASSERTION'], 'domain', self.domainB['id']) + self.TOKEN_SCOPE_DOMAIN_B_FROM_ADMIN = self._scope_request( + self.tokens['ADMIN_ASSERTION'], 'domain', self.domainB['id']) - self.TOKEN_SCOPE_DOMAIN_C_FROM_ADMIN = self._scope_request( - self.tokens['ADMIN_ASSERTION'], 'domain', - self.domainC['id']) + self.TOKEN_SCOPE_DOMAIN_C_FROM_ADMIN = self._scope_request( + self.tokens['ADMIN_ASSERTION'], 'domain', + self.domainC['id']) class FederatedIdentityProviderTests(test_v3.RestfulTestCase): @@ -1866,7 +1872,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): super(FederatedTokenTests, self).setUp() self._notifications = [] - def fake_saml_notify(action, request, user_id, group_ids, + def fake_saml_notify(action, user_id, group_ids, identity_provider, protocol, token_id, outcome): note = { 'action': action, @@ -1902,12 +1908,12 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): def test_issue_unscoped_token(self): r = self._issue_unscoped_token() - self.assertIsNotNone(r.headers.get('X-Subject-Token')) - self.assertValidMappedUser(r.json['token']) + token_resp = render_token.render_token_response_from_model(r)['token'] + self.assertValidMappedUser(token_resp) def test_issue_the_same_unscoped_token_with_user_deleted(self): r = self._issue_unscoped_token() - token = r.json['token'] + token = render_token.render_token_response_from_model(r)['token'] user1 = token['user'] user_id1 = user1.pop('id') @@ -1916,7 +1922,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): PROVIDERS.identity_api.delete_user(user_id1) r = self._issue_unscoped_token() - token = r.json['token'] + token = render_token.render_token_response_from_model(r)['token'] user2 = token['user'] user_id2 = user2.pop('id') @@ -1942,42 +1948,37 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): def test_issue_unscoped_token_group_names_in_mapping(self): r = self._issue_unscoped_token(assertion='ANOTHER_CUSTOMER_ASSERTION') ref_groups = set([self.group_customers['id'], self.group_admins['id']]) - token_resp = r.json_body - token_groups = token_resp['token']['user']['OS-FEDERATION']['groups'] + token_groups = r.federated_groups token_groups = set([group['id'] for group in token_groups]) self.assertEqual(ref_groups, token_groups) def test_issue_unscoped_tokens_nonexisting_group(self): - r = self._issue_unscoped_token(assertion='ANOTHER_TESTER_ASSERTION') - self.assertIsNotNone(r.headers.get('X-Subject-Token')) + self._issue_unscoped_token(assertion='ANOTHER_TESTER_ASSERTION') def test_issue_unscoped_token_with_remote_no_attribute(self): - r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, - environment={ - self.REMOTE_ID_ATTR: - self.REMOTE_IDS[0] - }) - self.assertIsNotNone(r.headers.get('X-Subject-Token')) + self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, + environment={ + self.REMOTE_ID_ATTR: + self.REMOTE_IDS[0] + }) def test_issue_unscoped_token_with_remote(self): self.config_fixture.config(group='federation', remote_id_attribute=self.REMOTE_ID_ATTR) - r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, - environment={ - self.REMOTE_ID_ATTR: - self.REMOTE_IDS[0] - }) - self.assertIsNotNone(r.headers.get('X-Subject-Token')) + self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, + environment={ + self.REMOTE_ID_ATTR: + self.REMOTE_IDS[0] + }) def test_issue_unscoped_token_with_saml2_remote(self): self.config_fixture.config(group='saml2', remote_id_attribute=self.REMOTE_ID_ATTR) - r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, - environment={ - self.REMOTE_ID_ATTR: - self.REMOTE_IDS[0] - }) - self.assertIsNotNone(r.headers.get('X-Subject-Token')) + self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, + environment={ + self.REMOTE_ID_ATTR: + self.REMOTE_IDS[0] + }) def test_issue_unscoped_token_with_remote_different(self): self.config_fixture.config(group='federation', @@ -2001,12 +2002,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): remote_id_attribute=self.REMOTE_ID_ATTR) self.config_fixture.config(group='federation', remote_id_attribute=uuid.uuid4().hex) - r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, - environment={ - self.REMOTE_ID_ATTR: - self.REMOTE_IDS[0] - }) - self.assertIsNotNone(r.headers.get('X-Subject-Token')) + self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, + environment={ + self.REMOTE_ID_ATTR: + self.REMOTE_IDS[0] + }) def test_issue_unscoped_token_with_remote_unavailable(self): self.config_fixture.config(group='federation', @@ -2020,14 +2020,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): def test_issue_unscoped_token_with_remote_user_as_empty_string(self): # make sure that REMOTE_USER set as the empty string won't interfere - r = self._issue_unscoped_token(environment={'REMOTE_USER': ''}) - self.assertIsNotNone(r.headers.get('X-Subject-Token')) + self._issue_unscoped_token(environment={'REMOTE_USER': ''}) def test_issue_unscoped_token_no_groups(self): r = self._issue_unscoped_token(assertion='USER_NO_GROUPS_ASSERTION') - self.assertIsNotNone(r.headers.get('X-Subject-Token')) - token_resp = r.json_body - token_groups = token_resp['token']['user']['OS-FEDERATION']['groups'] + token_groups = r.federated_groups self.assertEqual(0, len(token_groups)) def test_issue_scoped_token_no_groups(self): @@ -2037,11 +2034,9 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): """ # issue unscoped token with no groups r = self._issue_unscoped_token(assertion='USER_NO_GROUPS_ASSERTION') - self.assertIsNotNone(r.headers.get('X-Subject-Token')) - token_resp = r.json_body - token_groups = token_resp['token']['user']['OS-FEDERATION']['groups'] + token_groups = r.federated_groups self.assertEqual(0, len(token_groups)) - unscoped_token = r.headers.get('X-Subject-Token') + unscoped_token = r.id # let admin get roles in a project self.proj_employees @@ -2068,16 +2063,14 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): non string objects and return token id in the HTTP header. """ - api = auth_controllers.Auth() environ = { 'malformed_object': object(), 'another_bad_idea': tuple(range(10)), 'yet_another_bad_param': dict(zip(uuid.uuid4().hex, range(32))) } environ.update(mapping_fixtures.EMPLOYEE_ASSERTION) - request = self.make_request(environ=environ) - r = api.authenticate_for_token(request, self.UNSCOPED_V3_SAML2_REQ) - self.assertIsNotNone(r.headers.get('X-Subject-Token')) + with self.make_request(environ=environ): + authentication.authenticate_for_token(self.UNSCOPED_V3_SAML2_REQ) def test_scope_to_project_once_notify(self): r = self.v3_create_token( @@ -2208,12 +2201,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): expected_status=http_client.NOT_FOUND) def test_issue_token_from_rules_without_user(self): - api = auth_controllers.Auth() environ = copy.deepcopy(mapping_fixtures.BAD_TESTER_ASSERTION) - request = self.make_request(environ=environ) - self.assertRaises(exception.Unauthorized, - api.authenticate_for_token, - request, self.UNSCOPED_V3_SAML2_REQ) + with self.make_request(environ=environ): + self.assertRaises(exception.Unauthorized, + authentication.authenticate_for_token, + self.UNSCOPED_V3_SAML2_REQ) def test_issue_token_with_nonexistent_group(self): """Inject assertion that matches rule issuing bad group id. @@ -2356,11 +2348,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): """ r = self._issue_unscoped_token() - token_resp = r.json_body['token'] + token_resp = render_token.render_token_response_from_model(r)['token'] # NOTE(lbragstad): Ensure only 'saml2' is in the method list. - self.assertListEqual(['saml2'], token_resp['methods']) + self.assertListEqual(['saml2'], r.methods) self.assertValidMappedUser(token_resp) - employee_unscoped_token_id = r.headers.get('X-Subject-Token') + employee_unscoped_token_id = r.id r = self.get('/auth/projects', token=employee_unscoped_token_id) projects = r.result['projects'] random_project = random.randint(0, len(projects) - 1) @@ -2432,14 +2424,13 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules) r = self._issue_unscoped_token(assertion='TESTER_ASSERTION') - token_id = r.headers.get('X-Subject-Token') # delete group PROVIDERS.identity_api.delete_group(group['id']) # scope token to project_all, expect HTTP 500 scoped_token = self._scope_request( - token_id, 'project', + r.id, 'project', self.project_all['id']) self.v3_create_token( @@ -2498,7 +2489,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): } PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules) r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') - assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups'] + assigned_group_ids = r.federated_groups self.assertEqual(1, len(assigned_group_ids)) self.assertEqual(group['id'], assigned_group_ids[0]['id']) @@ -2571,7 +2562,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): } PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules) r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') - assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups'] + assigned_group_ids = r.federated_groups self.assertEqual(len(group_ids), len(assigned_group_ids)) for group in assigned_group_ids: self.assertIn(group['id'], group_ids) @@ -2644,7 +2635,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): } PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules) r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') - assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups'] + assigned_group_ids = r.federated_groups self.assertEqual(len(group_ids), len(assigned_group_ids)) for group in assigned_group_ids: self.assertIn(group['id'], group_ids) @@ -2706,7 +2697,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): } PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules) r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') - assigned_groups = r.json['token']['user']['OS-FEDERATION']['groups'] + assigned_groups = r.federated_groups self.assertEqual(len(assigned_groups), 0) def test_not_setting_whitelist_accepts_all_values(self): @@ -2776,7 +2767,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): } PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules) r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') - assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups'] + assigned_group_ids = r.federated_groups self.assertEqual(len(group_ids), len(assigned_group_ids)) for group in assigned_group_ids: self.assertIn(group['id'], group_ids) @@ -2791,8 +2782,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): """ self.config_fixture.config(group='federation', assertion_prefix=self.ASSERTION_PREFIX) - r = self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION_PREFIXED') - self.assertIsNotNone(r.headers.get('X-Subject-Token')) + self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION_PREFIXED') def test_assertion_prefix_parameter_expect_fail(self): """Test parameters filtering based on the prefix. @@ -2804,8 +2794,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): Expect server to raise exception.Unathorized exception. """ - r = self._issue_unscoped_token() - self.assertIsNotNone(r.headers.get('X-Subject-Token')) + self._issue_unscoped_token() self.config_fixture.config(group='federation', assertion_prefix='UserName') @@ -2814,23 +2803,24 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): def test_unscoped_token_has_user_domain(self): r = self._issue_unscoped_token() - self._check_domains_are_valid(r.json_body['token']) + self._check_domains_are_valid( + render_token.render_token_response_from_model(r)['token']) def test_scoped_token_has_user_domain(self): r = self.v3_create_token( self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE) - self._check_domains_are_valid(r.result['token']) + self._check_domains_are_valid(r.json_body['token']) def test_issue_unscoped_token_for_local_user(self): r = self._issue_unscoped_token(assertion='LOCAL_USER_ASSERTION') - token_resp = r.json_body['token'] - self.assertListEqual(['saml2'], token_resp['methods']) - self.assertEqual(self.user['id'], token_resp['user']['id']) - self.assertEqual(self.user['name'], token_resp['user']['name']) - self.assertEqual(self.domain['id'], token_resp['user']['domain']['id']) + self.assertListEqual(['saml2'], r.methods) + self.assertEqual(self.user['id'], r.user_id) + self.assertEqual(self.user['name'], r.user['name']) + self.assertEqual(self.domain['id'], r.user_domain['id']) # Make sure the token is not scoped - self.assertNotIn('project', token_resp) - self.assertNotIn('domain', token_resp) + self.assertIsNone(r.domain_id) + self.assertIsNone(r.project_id) + self.assertTrue(r.unscoped) def test_issue_token_for_local_user_user_not_found(self): self.assertRaises(exception.Unauthorized, @@ -2839,11 +2829,10 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): def test_user_name_and_id_in_federation_token(self): r = self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION') - token = r.json_body['token'] self.assertEqual( mapping_fixtures.EMPLOYEE_ASSERTION['UserName'], - token['user']['name']) - self.assertNotEqual(token['user']['name'], token['user']['id']) + r.user['name']) + self.assertNotEqual(r.user['name'], r.user_id) r = self.v3_create_token( self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE) token = r.json_body['token'] @@ -2878,18 +2867,18 @@ class FernetFederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): def test_federated_unscoped_token(self): resp = self._issue_unscoped_token() - self.assertEqual(204, len(resp.headers['X-Subject-Token'])) - self.assertValidMappedUser(resp.json_body['token']) + self.assertValidMappedUser( + render_token.render_token_response_from_model(resp)['token']) def test_federated_unscoped_token_with_multiple_groups(self): assertion = 'ANOTHER_CUSTOMER_ASSERTION' resp = self._issue_unscoped_token(assertion=assertion) - self.assertEqual(226, len(resp.headers['X-Subject-Token'])) - self.assertValidMappedUser(resp.json_body['token']) + self.assertValidMappedUser( + render_token.render_token_response_from_model(resp)['token']) def test_validate_federated_unscoped_token(self): resp = self._issue_unscoped_token() - unscoped_token = resp.headers.get('X-Subject-Token') + unscoped_token = resp.id # assert that the token we received is valid self.get('/auth/tokens/', headers={'X-Subject-Token': unscoped_token}) @@ -2902,8 +2891,9 @@ class FernetFederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): """ resp = self._issue_unscoped_token() - self.assertValidMappedUser(resp.json_body['token']) - unscoped_token = resp.headers.get('X-Subject-Token') + self.assertValidMappedUser( + render_token.render_token_response_from_model(resp)['token']) + unscoped_token = resp.id resp = self.get('/auth/projects', token=unscoped_token) projects = resp.result['projects'] random_project = random.randint(0, len(projects) - 1) @@ -2941,11 +2931,11 @@ class FederatedTokenTestsMethodToken(FederatedTokenTests): """ r = self._issue_unscoped_token() - token_resp = r.json_body['token'] + token_resp = render_token.render_token_response_from_model(r)['token'] # NOTE(lbragstad): Ensure only 'saml2' is in the method list. - self.assertListEqual(['saml2'], token_resp['methods']) + self.assertListEqual(['saml2'], r.methods) self.assertValidMappedUser(token_resp) - employee_unscoped_token_id = r.headers.get('X-Subject-Token') + employee_unscoped_token_id = r.id r = self.get('/auth/projects', token=employee_unscoped_token_id) projects = r.result['projects'] random_project = random.randint(0, len(projects) - 1) @@ -2979,11 +2969,11 @@ class FederatedUserTests(test_v3.RestfulTestCase, FederatedSetupMixin): def test_user_id_persistense(self): """Ensure user_id is persistend for multiple federated authn calls.""" r = self._issue_unscoped_token() - user_id = r.json_body['token']['user']['id'] + user_id = r.user_id self.assertNotEmpty(PROVIDERS.identity_api.get_user(user_id)) r = self._issue_unscoped_token() - user_id2 = r.json_body['token']['user']['id'] + user_id2 = r.user_id self.assertNotEmpty(PROVIDERS.identity_api.get_user(user_id2)) self.assertEqual(user_id, user_id2) @@ -3272,7 +3262,7 @@ class FederatedUserTests(test_v3.RestfulTestCase, FederatedSetupMixin): # Authenticate to create a new federated_user entry with a foreign # key pointing to the protocol r = self._issue_unscoped_token() - user_id = r.json_body['token']['user']['id'] + user_id = r.user_id self.assertNotEmpty(PROVIDERS.identity_api.get_user(user_id)) # Now we should be able to delete the protocol @@ -3280,10 +3270,10 @@ class FederatedUserTests(test_v3.RestfulTestCase, FederatedSetupMixin): def _authenticate_via_saml(self): r = self._issue_unscoped_token() - unscoped_token = r.headers['X-Subject-Token'] - token_resp = r.json_body['token'] + unscoped_token = r.id + token_resp = render_token.render_token_response_from_model(r)['token'] self.assertValidMappedUser(token_resp) - return token_resp['user']['id'], unscoped_token + return r.user_id, unscoped_token class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin): @@ -3351,8 +3341,9 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin): self.assertNotIn(project['name'], self.expected_results) response = self._issue_unscoped_token() - self.assertValidMappedUser(response.json_body['token']) - unscoped_token = response.headers.get('X-Subject-Token') + self.assertValidMappedUser( + render_token.render_token_response_from_model(response)['token']) + unscoped_token = response.id response = self.get('/auth/projects', token=unscoped_token) projects = response.json_body['projects'] for project in projects: @@ -3364,8 +3355,9 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin): def test_shadow_mapping_create_projects_role_assignments(self): response = self._issue_unscoped_token() - self.assertValidMappedUser(response.json_body['token']) - unscoped_token = response.headers.get('X-Subject-Token') + self.assertValidMappedUser( + render_token.render_token_response_from_model(response)['token']) + unscoped_token = response.id response = self.get('/auth/projects', token=unscoped_token) projects = response.json_body['projects'] for project in projects: @@ -3391,8 +3383,9 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin): def test_shadow_mapping_creates_project_in_identity_provider_domain(self): response = self._issue_unscoped_token() - self.assertValidMappedUser(response.json_body['token']) - unscoped_token = response.headers.get('X-Subject-Token') + self.assertValidMappedUser( + render_token.render_token_response_from_model(response)['token']) + unscoped_token = response.id response = self.get('/auth/projects', token=unscoped_token) projects = response.json_body['projects'] for project in projects: @@ -3401,12 +3394,13 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin): def test_shadow_mapping_is_idempotent(self): """Test that projects remain idempotent for every federated auth.""" response = self._issue_unscoped_token() - self.assertValidMappedUser(response.json_body['token']) - unscoped_token = response.headers.get('X-Subject-Token') + self.assertValidMappedUser( + render_token.render_token_response_from_model(response)['token']) + unscoped_token = response.id response = self.get('/auth/projects', token=unscoped_token) project_ids = [p['id'] for p in response.json_body['projects']] response = self._issue_unscoped_token() - unscoped_token = response.headers.get('X-Subject-Token') + unscoped_token = response.id response = self.get('/auth/projects', token=unscoped_token) projects = response.json_body['projects'] for project in projects: @@ -3438,8 +3432,8 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin): ) PROVIDERS.role_api.create_role(member_role_ref['id'], member_role_ref) response = self._issue_unscoped_token() - user_id = response.json_body['token']['user']['id'] - unscoped_token = response.headers.get('X-Subject-Token') + user_id = response.user_id + unscoped_token = response.id response = self.get('/auth/projects', token=unscoped_token) projects = response.json_body['projects'] staging_project = PROVIDERS.resource_api.get_project_by_name( @@ -3500,7 +3494,7 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin): ) response = self._issue_unscoped_token() # user_id = response.json_body['token']['user']['id'] - unscoped_token = response.headers.get('X-Subject-Token') + unscoped_token = response.id response = self.get('/auth/projects', token=unscoped_token) projects = response.json_body['projects'] self.expected_results = { @@ -3532,8 +3526,8 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin): # to them. This test verifies that this is no longer true. # Authenticate once to create the projects response = self._issue_unscoped_token() - self.assertValidMappedUser(response.json_body['token']) - unscoped_token = response.headers.get('X-Subject-Token') + self.assertValidMappedUser( + render_token.render_token_response_from_model(response)['token']) # Assign admin role to newly-created project to another user staging_project = PROVIDERS.resource_api.get_project_by_name( @@ -3548,8 +3542,9 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin): # Authenticate again with the federated user and verify roles response = self._issue_unscoped_token() - self.assertValidMappedUser(response.json_body['token']) - unscoped_token = response.headers.get('X-Subject-Token') + self.assertValidMappedUser( + render_token.render_token_response_from_model(response)['token']) + unscoped_token = response.id scope = self._scope_request( unscoped_token, 'project', staging_project['id'] ) @@ -4602,10 +4597,6 @@ class WebSSOTests(FederatedTokenTests): ORIGIN = urllib.parse.quote_plus(TRUSTED_DASHBOARD) PROTOCOL_REMOTE_ID_ATTR = uuid.uuid4().hex - def setUp(self): - super(WebSSOTests, self).setUp() - self.api = federation_controllers.Auth() - def config_overrides(self): super(WebSSOTests, self).config_overrides() self.config_fixture.config( @@ -4616,34 +4607,39 @@ class WebSSOTests(FederatedTokenTests): def test_render_callback_template(self): token_id = uuid.uuid4().hex - resp = self.api.render_html_response(self.TRUSTED_DASHBOARD, token_id) + with self.make_request(): + resp = ( + auth_api._AuthFederationWebSSOBase._render_template_response( + self.TRUSTED_DASHBOARD, token_id)) # The expected value in the assertions bellow need to be 'str' in # Python 2 and 'bytes' in Python 3 - self.assertIn(token_id.encode('utf-8'), resp.body) - self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.body) + self.assertIn(token_id.encode('utf-8'), resp.data) + self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.data) def test_federated_sso_auth(self): environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0], 'QUERY_STRING': 'origin=%s' % self.ORIGIN} environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) - request = self.make_request(environ=environment) - resp = self.api.federated_sso_auth(request, self.PROTOCOL) - # `resp.body` will be `str` in Python 2 and `bytes` in Python 3 + with self.make_request(environ=environment): + resp = auth_api.AuthFederationWebSSOResource._perform_auth( + self.PROTOCOL) + # `resp.data` will be `str` in Python 2 and `bytes` in Python 3 # which is why expected value: `self.TRUSTED_DASHBOARD` # needs to be encoded - self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.body) + self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.data) def test_get_sso_origin_host_case_insensitive(self): # test lowercase hostname in trusted_dashboard environ = {'QUERY_STRING': 'origin=http://horizon.com'} - request = self.make_request(environ=environ) - host = self.api._get_sso_origin_host(request) - self.assertEqual("http://horizon.com", host) - # test uppercase hostname in trusted_dashboard - self.config_fixture.config(group='federation', - trusted_dashboard=['http://Horizon.com']) - host = self.api._get_sso_origin_host(request) - self.assertEqual("http://horizon.com", host) + with self.make_request(environ=environ): + host = auth_api._get_sso_origin_host() + self.assertEqual("http://horizon.com", host) + # test uppercase hostname in trusted_dashboard + self.config_fixture.config( + group='federation', + trusted_dashboard=['http://Horizon.com']) + host = auth_api._get_sso_origin_host() + self.assertEqual("http://horizon.com", host) def test_federated_sso_auth_with_protocol_specific_remote_id(self): self.config_fixture.config( @@ -4653,76 +4649,82 @@ class WebSSOTests(FederatedTokenTests): environment = {self.PROTOCOL_REMOTE_ID_ATTR: self.REMOTE_IDS[0], 'QUERY_STRING': 'origin=%s' % self.ORIGIN} environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) - request = self.make_request(environ=environment) - resp = self.api.federated_sso_auth(request, self.PROTOCOL) - # `resp.body` will be `str` in Python 2 and `bytes` in Python 3 + with self.make_request(environ=environment): + resp = auth_api.AuthFederationWebSSOResource._perform_auth( + self.PROTOCOL) + # `resp.data` will be `str` in Python 2 and `bytes` in Python 3 # which is why expected value: `self.TRUSTED_DASHBOARD` # needs to be encoded - self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.body) + self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.data) def test_federated_sso_auth_bad_remote_id(self): environment = {self.REMOTE_ID_ATTR: self.IDP, 'QUERY_STRING': 'origin=%s' % self.ORIGIN} environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) - request = self.make_request(environ=environment) - self.assertRaises(exception.IdentityProviderNotFound, - self.api.federated_sso_auth, - request, self.PROTOCOL) + with self.make_request(environ=environment): + self.assertRaises( + exception.IdentityProviderNotFound, + auth_api.AuthFederationWebSSOResource._perform_auth, + self.PROTOCOL) def test_federated_sso_missing_query(self): environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]} environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) - request = self.make_request(environ=environment) - self.assertRaises(exception.ValidationError, - self.api.federated_sso_auth, - request, self.PROTOCOL) + with self.make_request(environ=environment): + self.assertRaises( + exception.ValidationError, + auth_api.AuthFederationWebSSOResource._perform_auth, + self.PROTOCOL) def test_federated_sso_missing_query_bad_remote_id(self): environment = {self.REMOTE_ID_ATTR: self.IDP} environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) - request = self.make_request(environ=environment) - self.assertRaises(exception.ValidationError, - self.api.federated_sso_auth, - request, self.PROTOCOL) + with self.make_request(environ=environment): + self.assertRaises( + exception.ValidationError, + auth_api.AuthFederationWebSSOResource._perform_auth, + self.PROTOCOL) def test_federated_sso_untrusted_dashboard(self): environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0], 'QUERY_STRING': 'origin=%s' % uuid.uuid4().hex} environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) - request = self.make_request(environ=environment) - self.assertRaises(exception.Unauthorized, - self.api.federated_sso_auth, - request, self.PROTOCOL) + with self.make_request(environ=environment): + self.assertRaises( + exception.Unauthorized, + auth_api.AuthFederationWebSSOResource._perform_auth, + self.PROTOCOL) def test_federated_sso_untrusted_dashboard_bad_remote_id(self): environment = {self.REMOTE_ID_ATTR: self.IDP, 'QUERY_STRING': 'origin=%s' % uuid.uuid4().hex} environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) - request = self.make_request(environ=environment) - self.assertRaises(exception.Unauthorized, - self.api.federated_sso_auth, - request, self.PROTOCOL) + with self.make_request(environ=environment): + self.assertRaises( + exception.Unauthorized, + auth_api.AuthFederationWebSSOResource._perform_auth, + self.PROTOCOL) def test_federated_sso_missing_remote_id(self): environment = copy.deepcopy(mapping_fixtures.EMPLOYEE_ASSERTION) - request = self.make_request(environ=environment, - query_string='origin=%s' % self.ORIGIN) - self.assertRaises(exception.Unauthorized, - self.api.federated_sso_auth, - request, self.PROTOCOL) + with self.make_request(environ=environment, + query_string='origin=%s' % self.ORIGIN): + self.assertRaises( + exception.Unauthorized, + auth_api.AuthFederationWebSSOResource._perform_auth, + self.PROTOCOL) def test_identity_provider_specific_federated_authentication(self): environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]} environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) - request = self.make_request(environ=environment, - query_string='origin=%s' % self.ORIGIN) - resp = self.api.federated_idp_specific_sso_auth(request, - self.idp['id'], - self.PROTOCOL) - # `resp.body` will be `str` in Python 2 and `bytes` in Python 3 + with self.make_request(environ=environment, + query_string='origin=%s' % self.ORIGIN): + resp = auth_api.AuthFederationWebSSOIDPsResource._perform_auth( + self.idp['id'], self.PROTOCOL) + # `resp.data` will be `str` in Python 2 and `bytes` in Python 3 # which is why the expected value: `self.TRUSTED_DASHBOARD` # needs to be encoded - self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.body) + self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.data) class K2KServiceCatalogTests(test_v3.RestfulTestCase): @@ -4779,7 +4781,7 @@ class K2KServiceCatalogTests(test_v3.RestfulTestCase): model = token_model.TokenModel() model.user_id = self.user_id model.methods = ['password'] - token = controller.render_token_response_from_model(model) + token = render_token.render_token_response_from_model(model) ref = {} for r in (self.sp_alpha, self.sp_beta, self.sp_gamma): ref.update(r) @@ -4799,7 +4801,7 @@ class K2KServiceCatalogTests(test_v3.RestfulTestCase): model = token_model.TokenModel() model.user_id = self.user_id model.methods = ['password'] - token = controller.render_token_response_from_model(model) + token = render_token.render_token_response_from_model(model) ref = {} for r in (self.sp_beta, self.sp_gamma): ref.update(r) @@ -4819,7 +4821,7 @@ class K2KServiceCatalogTests(test_v3.RestfulTestCase): model = token_model.TokenModel() model.user_id = self.user_id model.methods = ['password'] - token = controller.render_token_response_from_model(model) + token = render_token.render_token_response_from_model(model) self.assertNotIn('service_providers', token['token'], message=('Expected Service Catalog not to have ' 'service_providers'))