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