Convert auth to flask native dispatching

Convert the /auth paths to flask native dispatching.

A minor change to additional_urls was implemented to ensure all
urls are added at once instead of individually (causing an over-
write issue within flask as a single resource may only have a
single set of URL mappings).

Alternate URLs now support adding alternate JSON Home rel links.
This is to support the case of OS-FEDERATION auth routes moving
to /auth. The old JSON Home entries must exist but reference
the new paths.

This port includes the following test changes (needed due to the
way flask handles requests and the way requests are passed through
the auth system):

* Implemented keystone.common.render_token (module)
  containing render_token_response_from_model and use it instead
  of keystone.common.controller.render_token_response_from_model.

  Minor differences occur in render_token_response_from_model in
  the keystone.common.render_token module, this is simply
  for referencing data from flask instead of the request object.

* Test cases have been modified to no longer rely on the auth
  controller(s) directly

* Test cases now use "make_request" as a context manager
  since authenticate/authenticate_for_token directly
  reference the flask contexts and must have an explicit
  context pushed.

* Test cases no longer pass request objects into methods
  such as authenticate/authenticate_for_token or similar
  methods on the auth plugins

* Test cases for federation reference the token model now
  where possible instead of the rendered token response.
  Rendered token responses are generated where needed.

* Auth Plugin Configuration is done in test core as well.
  This is because Auth controller does not exist.

NOTE: This is a massive change, but must of these changes
were now easily uncoupled because of how far reaching auth
is.

Change-Id: I636928102875760726cc3493775a2be48e774fd7
Partial-Bug: #1776504
This commit is contained in:
morgan fainberg 2018-09-18 10:54:59 -07:00 committed by Morgan Fainberg
parent 8e33c78232
commit d97832e8e8
50 changed files with 1801 additions and 1716 deletions

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from keystone.api import auth
from keystone.api import credentials from keystone.api import credentials
from keystone.api import discovery from keystone.api import discovery
from keystone.api import domains from keystone.api import domains
@ -33,6 +34,7 @@ from keystone.api import system
from keystone.api import trusts from keystone.api import trusts
__all__ = ( __all__ = (
'auth',
'discovery', 'discovery',
'credentials', 'credentials',
'domains', 'domains',
@ -58,6 +60,7 @@ __all__ = (
__apis__ = ( __apis__ = (
discovery, discovery,
auth,
credentials, credentials,
domains, domains,
endpoints, endpoints,

View File

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

View File

@ -70,3 +70,8 @@ os_federation_parameter_rel_func = functools.partial(
os_inherit_resource_rel_func = functools.partial( os_inherit_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation, json_home.build_v3_extension_resource_relation,
extension_name='OS-INHERIT', extension_version='1.0') 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')

578
keystone/api/auth.py Normal file
View File

@ -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/<string:protocol_id>',
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/<string:idp_id>/'
'protocols/<string:protocol_id>/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,
)

View File

@ -23,6 +23,7 @@ from keystone.common import rbac_enforcer
from keystone.common import utils from keystone.common import utils
from keystone.common import validation from keystone.common import validation
from keystone import exception from keystone import exception
from keystone import notifications
from keystone.server import flask as ks_flask from keystone.server import flask as ks_flask
@ -65,7 +66,7 @@ class EndpointResource(ks_flask.ResourceBase):
except exception.RegionNotFound: except exception.RegionNotFound:
region = dict(id=endpoint['region_id']) region = dict(id=endpoint['region_id'])
PROVIDERS.catalog_api.create_region( PROVIDERS.catalog_api.create_region(
region, initiator=ks_flask.build_audit_initiator()) region, initiator=notifications.build_audit_initiator())
return endpoint return endpoint
def _get_endpoint(self, endpoint_id): def _get_endpoint(self, endpoint_id):

View File

@ -22,6 +22,7 @@ from keystone.common import rbac_enforcer
from keystone.common import validation from keystone.common import validation
from keystone import exception from keystone import exception
from keystone.identity import schema from keystone.identity import schema
from keystone import notifications
from keystone.server import flask as ks_flask 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, build_target=functools.partial(self._build_enforcement_target_attr,
user_id, group_id)) user_id, group_id))
PROVIDERS.identity_api.add_user_to_group( 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 return None, http_client.NO_CONTENT
def delete(self, group_id, user_id): 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, build_target=functools.partial(self._build_enforcement_target_attr,
user_id, group_id)) user_id, group_id))
PROVIDERS.identity_api.remove_user_from_group( 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 return None, http_client.NO_CONTENT

View File

@ -14,18 +14,17 @@
import flask import flask
import flask_restful import flask_restful
from oslo_log import versionutils from oslo_serialization import jsonutils
from six.moves import http_client from six.moves import http_client
from keystone.api._shared import authentication
from keystone.api._shared import json_home_relations from keystone.api._shared import json_home_relations
from keystone.common import authorization
from keystone.common import provider_api from keystone.common import provider_api
from keystone.common import rbac_enforcer from keystone.common import rbac_enforcer
from keystone.common import request from keystone.common import render_token
from keystone.common import validation from keystone.common import validation
import keystone.conf import keystone.conf
from keystone import exception from keystone import exception
import keystone.federation.controllers
from keystone.federation import schema from keystone.federation import schema
from keystone.federation import utils from keystone.federation import utils
from keystone.server import flask as ks_flask from keystone.server import flask as ks_flask
@ -380,74 +379,6 @@ class ServiceProvidersResource(_ResourceBase):
return None, http_client.NO_CONTENT 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): class SAML2MetadataResource(flask_restful.Resource):
@ks_flask.unenforced_api @ks_flask.unenforced_api
def get(self): def get(self):
@ -468,11 +399,6 @@ class SAML2MetadataResource(flask_restful.Resource):
class OSFederationAuthResource(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 @ks_flask.unenforced_api
def get(self, idp_id, protocol_id): def get(self, idp_id, protocol_id):
@ -493,12 +419,11 @@ class OSFederationAuthResource(flask_restful.Resource):
return self._auth(idp_id, protocol_id) return self._auth(idp_id, protocol_id)
def _auth(self, 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 Build HTTP request body for federated authentication and inject
it into the ``authenticate_for_token`` function. it into the ``authenticate_for_token`` function.
""" """
compat_controller = keystone.federation.controllers.Auth()
auth = { auth = {
'identity': { 'identity': {
'methods': [protocol_id], 'methods': [protocol_id],
@ -508,14 +433,12 @@ class OSFederationAuthResource(flask_restful.Resource):
}, },
} }
} }
# NOTE(morgan): for compatibility, make sure we use a webob request token = authentication.authenticate_for_token(auth)
# until /auth is ported to flask. Since this is a webob response, token_data = render_token.render_token_response_from_model(token)
# deconstruct it and turn it into a flask response. resp_data = jsonutils.dumps(token_data)
webob_resp = compat_controller.authenticate_for_token( flask_resp = flask.make_response(resp_data, http_client.CREATED)
self._construct_webob_request(), auth) flask_resp.headers['X-Subject-Token'] = token.id
flask_resp = flask.make_response( flask_resp.headers['Content-Type'] = 'application/json'
webob_resp.body, webob_resp.status_code)
flask_resp.headers.extend(webob_resp.headers.dict_of_lists())
return flask_resp return flask_resp
@ -525,18 +448,6 @@ class OSFederationAPI(ks_flask.APIBase):
_api_url_prefix = '/OS-FEDERATION' _api_url_prefix = '/OS-FEDERATION'
resources = [] resources = []
resource_mapping = [ 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( ks_flask.construct_resource_map(
resource=SAML2MetadataResource, resource=SAML2MetadataResource,
url='/saml2/metadata', url='/saml2/metadata',

View File

@ -174,7 +174,7 @@ class RequestTokenResource(_OAuth1ResourceBase):
consumer_id, consumer_id,
requested_project_id, requested_project_id,
request_token_duration, request_token_duration,
initiator=ks_flask.build_audit_initiator()) initiator=notifications.build_audit_initiator())
result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s' result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s'
% {'key': token_ref['id'], % {'key': token_ref['id'],
@ -266,7 +266,7 @@ class AccessTokenResource(_OAuth1ResourceBase):
token_ref = PROVIDERS.oauth_api.create_access_token( token_ref = PROVIDERS.oauth_api.create_access_token(
request_token_id, request_token_id,
access_token_duration, access_token_duration,
initiator=ks_flask.build_audit_initiator()) initiator=notifications.build_audit_initiator())
result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s' result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s'
% {'key': token_ref['id'], % {'key': token_ref['id'],

View File

@ -101,7 +101,7 @@ class Manager(manager.Manager):
roles.append(PROVIDERS.role_api.get_role(role['id'])) roles.append(PROVIDERS.role_api.get_role(role['id']))
return roles return roles
def authenticate(self, request, application_credential_id, secret): def authenticate(self, application_credential_id, secret):
"""Authenticate with an application credential. """Authenticate with an application credential.
:param str application_credential_id: Application Credential ID :param str application_credential_id: Application Credential ID

View File

@ -15,5 +15,3 @@
# NOTE(notmorgan): Be careful in adjusting whitespace here, flake8 checks # NOTE(notmorgan): Be careful in adjusting whitespace here, flake8 checks
# get cranky. # get cranky.
from keystone.auth import core # noqa from keystone.auth import core # noqa
from keystone.auth import controllers # noqa

View File

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

View File

@ -414,13 +414,14 @@ class AuthInfo(provider_api.ProviderAPIMixin, object):
class UserMFARulesValidator(provider_api.ProviderAPIMixin, object): class UserMFARulesValidator(provider_api.ProviderAPIMixin, object):
"""Helper object that can validate the MFA Rules.""" """Helper object that can validate the MFA Rules."""
@property @classmethod
def _auth_methods(self): def _auth_methods(cls):
if AUTH_PLUGINS_LOADED: if AUTH_PLUGINS_LOADED:
return set(AUTH_METHODS.keys()) return set(AUTH_METHODS.keys())
raise RuntimeError(_('Auth Method Plugins are not loaded.')) 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. """Validate the MFA rules against the successful auth methods.
:param user_id: The user's ID (uuid). :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 = user_ref['options'].get(ro.MFA_RULES_OPT.option_name, [])
mfa_rules_enabled = user_ref['options'].get( mfa_rules_enabled = user_ref['options'].get(
ro.MFA_ENABLED_OPT.option_name, True) 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: if not rules or not mfa_rules_enabled:
# return quickly if the rules are disabled for the user or not set # 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 # disable an auth method, and a rule will still pass making it
# impossible to accidently lock-out a subset of users with a # impossible to accidently lock-out a subset of users with a
# bad keystone.conf # 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): if set(auth_methods).issuperset(r_set):
# Rule Matches no need to continue, return here. # Rule Matches no need to continue, return here.
LOG.debug('Auth methods for user `%(user_id)s`, `%(methods)s` ' 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, {'user_id': user_id,
'rule': list(r_set), 'rule': list(r_set),
'methods': auth_methods, 'methods': auth_methods,
'loaded': self._auth_methods}) 'loaded': cls._auth_methods()})
return True return True
LOG.debug('Auth methods for user `%(user_id)s`, `%(methods)s` did not ' LOG.debug('Auth methods for user `%(user_id)s`, `%(methods)s` did not '

View File

@ -23,7 +23,7 @@ METHOD_NAME = 'application_credential'
class ApplicationCredential(base.AuthMethodHandler): class ApplicationCredential(base.AuthMethodHandler):
def authenticate(self, request, auth_payload): def authenticate(self, auth_payload):
"""Authenticate an application.""" """Authenticate an application."""
response_data = {} response_data = {}
app_cred_info = auth_plugins.AppCredInfo.create(auth_payload, app_cred_info = auth_plugins.AppCredInfo.create(auth_payload,
@ -31,7 +31,6 @@ class ApplicationCredential(base.AuthMethodHandler):
try: try:
PROVIDERS.application_credential_api.authenticate( PROVIDERS.application_credential_api.authenticate(
request,
application_credential_id=app_cred_info.id, application_credential_id=app_cred_info.id,
secret=app_cred_info.secret) secret=app_cred_info.secret)
except AssertionError as e: except AssertionError as e:

View File

@ -33,11 +33,9 @@ class AuthMethodHandler(provider_api.ProviderAPIMixin, object):
pass pass
@abc.abstractmethod @abc.abstractmethod
def authenticate(self, request, auth_payload): def authenticate(self, auth_payload):
"""Authenticate user and return an authentication context. """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 :param auth_payload: the payload content of the authentication request
for a given method for a given method
:type auth_payload: dict :type auth_payload: dict

View File

@ -16,6 +16,7 @@
import abc import abc
import flask
import six import six
from keystone.auth.plugins import base from keystone.auth.plugins import base
@ -31,21 +32,21 @@ PROVIDERS = provider_api.ProviderAPIs
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class Base(base.AuthMethodHandler): 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. """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 The user_id from the actual user from the REMOTE_USER env variable is
placed in the response_data. placed in the response_data.
""" """
response_data = {} response_data = {}
if not request.remote_user: if not flask.request.remote_user:
msg = _('No authenticated user') msg = _('No authenticated user')
raise exception.Unauthorized(msg) raise exception.Unauthorized(msg)
try: try:
user_ref = self._authenticate(request) user_ref = self._authenticate()
except Exception: 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) raise exception.Unauthorized(msg)
response_data['user_id'] = user_ref['id'] response_data['user_id'] = user_ref['id']
@ -53,7 +54,7 @@ class Base(base.AuthMethodHandler):
response_data=response_data) response_data=response_data)
@abc.abstractmethod @abc.abstractmethod
def _authenticate(self, request): def _authenticate(self):
"""Look up the user in the identity backend. """Look up the user in the identity backend.
Return user_ref Return user_ref
@ -62,36 +63,35 @@ class Base(base.AuthMethodHandler):
class DefaultDomain(Base): class DefaultDomain(Base):
def _authenticate(self, request): def _authenticate(self):
"""Use remote_user to look up the user in the identity backend.""" """Use remote_user to look up the user in the identity backend."""
return PROVIDERS.identity_api.get_user_by_name( return PROVIDERS.identity_api.get_user_by_name(
request.remote_user, flask.request.remote_user,
CONF.identity.default_domain_id) CONF.identity.default_domain_id)
class Domain(Base): class Domain(Base):
def _authenticate(self, request): def _authenticate(self):
"""Use remote_user to look up the user in the identity backend. """Use remote_user to look up the user in the identity backend.
The domain will be extracted from the REMOTE_DOMAIN environment The domain will be extracted from the REMOTE_DOMAIN environment
variable if present. If not, the default domain will be used. variable if present. If not, the default domain will be used.
""" """
if request.remote_domain: remote_domain = flask.request.environ.get('REMOTE_DOMAIN')
ref = PROVIDERS.resource_api.get_domain_by_name( if remote_domain:
request.remote_domain ref = PROVIDERS.resource_api.get_domain_by_name(remote_domain)
)
domain_id = ref['id'] domain_id = ref['id']
else: else:
domain_id = CONF.identity.default_domain_id domain_id = CONF.identity.default_domain_id
return PROVIDERS.identity_api.get_user_by_name(request.remote_user, return PROVIDERS.identity_api.get_user_by_name(
domain_id) flask.request.remote_user, domain_id)
class KerberosDomain(Domain): class KerberosDomain(Domain):
"""Allows `kerberos` as a method.""" """Allows `kerberos` as a method."""
def _authenticate(self, request): def _authenticate(self):
if request.auth_type != 'Negotiate': if flask.request.environ.get('AUTH_TYPE') != 'Negotiate':
raise exception.Unauthorized(_("auth_type is not Negotiate")) raise exception.Unauthorized(_("auth_type is not Negotiate"))
return super(KerberosDomain, self)._authenticate(request) return super(KerberosDomain, self)._authenticate()

View File

@ -13,6 +13,7 @@
import functools import functools
import uuid import uuid
import flask
from oslo_log import log from oslo_log import log
from pycadf import cadftaxonomy as taxonomy from pycadf import cadftaxonomy as taxonomy
from six.moves.urllib import parse from six.moves.urllib import parse
@ -38,10 +39,9 @@ class Mapped(base.AuthMethodHandler):
token_id = auth_payload['id'] token_id = auth_payload['id']
return PROVIDERS.token_provider_api.validate_token(token_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. """Authenticate mapped user and set an authentication context.
:param request: keystone's request context
:param auth_payload: the content of the authentication for a :param auth_payload: the content of the authentication for a
given method given method
@ -52,13 +52,11 @@ class Mapped(base.AuthMethodHandler):
""" """
if 'id' in auth_payload: if 'id' in auth_payload:
token_ref = self._get_token_ref(auth_payload) token_ref = self._get_token_ref(auth_payload)
response_data = handle_scoped_token(request, response_data = handle_scoped_token(token_ref,
token_ref,
PROVIDERS.federation_api, PROVIDERS.federation_api,
PROVIDERS.identity_api) PROVIDERS.identity_api)
else: else:
response_data = handle_unscoped_token(request, response_data = handle_unscoped_token(auth_payload,
auth_payload,
PROVIDERS.resource_api, PROVIDERS.resource_api,
PROVIDERS.federation_api, PROVIDERS.federation_api,
PROVIDERS.identity_api, PROVIDERS.identity_api,
@ -69,7 +67,7 @@ class Mapped(base.AuthMethodHandler):
response_data=response_data) 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 = {} response_data = {}
utils.validate_expiration(token) utils.validate_expiration(token)
token_audit_id = token.audit_id 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']) group_ids.append(group_dict['id'])
send_notification = functools.partial( send_notification = functools.partial(
notifications.send_saml_audit_notification, 'authenticate', notifications.send_saml_audit_notification, 'authenticate',
request, user_id, group_ids, identity_provider, protocol, user_id, group_ids, identity_provider, protocol,
token_audit_id) token_audit_id)
utils.assert_enabled_identity_provider(federation_api, identity_provider) 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 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): identity_api, assignment_api, role_api):
def validate_shadow_mapping(shadow_projects, existing_roles, idp_domain_id, 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 return resp
assertion = extract_assertion_data(request) assertion = extract_assertion_data()
try: try:
identity_provider = auth_payload['identity_provider'] identity_provider = auth_payload['identity_provider']
except KeyError: except KeyError:
@ -234,7 +232,7 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api,
if is_ephemeral_user(mapped_properties): if is_ephemeral_user(mapped_properties):
unique_id, display_name = ( 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') email = mapped_properties['user'].get('email')
user = identity_api.shadow_federated_user(identity_provider, 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 # after sending the notification
outcome = taxonomy.OUTCOME_FAILURE outcome = taxonomy.OUTCOME_FAILURE
notifications.send_saml_audit_notification('authenticate', notifications.send_saml_audit_notification('authenticate',
request,
user_id, group_ids, user_id, group_ids,
identity_provider, identity_provider,
protocol, token_id, protocol, token_id,
@ -291,7 +288,6 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api,
else: else:
outcome = taxonomy.OUTCOME_SUCCESS outcome = taxonomy.OUTCOME_SUCCESS
notifications.send_saml_audit_notification('authenticate', notifications.send_saml_audit_notification('authenticate',
request,
user_id, group_ids, user_id, group_ids,
identity_provider, identity_provider,
protocol, token_id, protocol, token_id,
@ -300,8 +296,8 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api,
return response_data return response_data
def extract_assertion_data(request): def extract_assertion_data():
assertion = dict(utils.get_assertion_params_from_env(request)) assertion = dict(utils.get_assertion_params_from_env())
return assertion return assertion
@ -329,7 +325,7 @@ def apply_mapping_filter(identity_provider, protocol, assertion,
return mapped_properties, mapping_id 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. """Setup federated username.
Function covers all the cases for properly setting user id, a primary 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 3) If user_id is not set and user_name is, set user_id as url safe version
of user_name. of user_name.
:param request: current request object
:param mapped_properties: Properties issued by a RuleProcessor. :param mapped_properties: Properties issued by a RuleProcessor.
:type: dictionary :type: dictionary
@ -358,7 +353,7 @@ def get_user_unique_id_and_display_name(request, mapped_properties):
user = mapped_properties['user'] user = mapped_properties['user']
user_id = user.get('id') 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]): if not any([user_id, user_name]):
msg = _("Could not map user while setting ephemeral user identity. " msg = _("Could not map user while setting ephemeral user identity. "

View File

@ -12,25 +12,26 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import flask
from oslo_utils import timeutils from oslo_utils import timeutils
from keystone.auth.plugins import base from keystone.auth.plugins import base
from keystone.common import controller
from keystone.common import provider_api from keystone.common import provider_api
from keystone import exception from keystone import exception
from keystone.i18n import _ from keystone.i18n import _
from keystone.oauth1 import core as oauth from keystone.oauth1 import core as oauth
from keystone.oauth1 import validator from keystone.oauth1 import validator
from keystone.server import flask as ks_flask
PROVIDERS = provider_api.ProviderAPIs PROVIDERS = provider_api.ProviderAPIs
class OAuth(base.AuthMethodHandler): 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.""" """Turn a signed request with an access key into a keystone token."""
response_data = {} 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') access_token_id = oauth_headers.get('oauth_token')
if not access_token_id: if not access_token_id:
@ -47,16 +48,15 @@ class OAuth(base.AuthMethodHandler):
if now > expires: if now > expires:
raise exception.Unauthorized(_('Access token is expired')) raise exception.Unauthorized(_('Access token is expired'))
url = controller.V3Controller.base_url(request.context_dict, url = ks_flask.base_url(path=flask.request.path)
request.path_info)
access_verifier = oauth.ResourceEndpoint( access_verifier = oauth.ResourceEndpoint(
request_validator=validator.OAuthValidator(), request_validator=validator.OAuthValidator(),
token_generator=oauth.token_generator) token_generator=oauth.token_generator)
result, request = access_verifier.validate_protected_resource_request( result, request = access_verifier.validate_protected_resource_request(
url, url,
http_method='POST', http_method='POST',
body=request.params, body=flask.request.args,
headers=request.headers, headers=flask.request.headers,
realms=None realms=None
) )
if not result: if not result:

View File

@ -25,14 +25,13 @@ PROVIDERS = provider_api.ProviderAPIs
class Password(base.AuthMethodHandler): class Password(base.AuthMethodHandler):
def authenticate(self, request, auth_payload): def authenticate(self, auth_payload):
"""Try to authenticate against the identity backend.""" """Try to authenticate against the identity backend."""
response_data = {} response_data = {}
user_info = auth_plugins.UserAuthInfo.create(auth_payload, METHOD_NAME) user_info = auth_plugins.UserAuthInfo.create(auth_payload, METHOD_NAME)
try: try:
PROVIDERS.identity_api.authenticate( PROVIDERS.identity_api.authenticate(
request,
user_id=user_info.user_id, user_id=user_info.user_id,
password=user_info.password) password=user_info.password)
except AssertionError: except AssertionError:

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import flask
from oslo_log import log from oslo_log import log
import six import six
@ -35,18 +36,18 @@ class Token(base.AuthMethodHandler):
token_id = auth_payload['id'] token_id = auth_payload['id']
return PROVIDERS.token_provider_api.validate_token(token_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: if 'id' not in auth_payload:
raise exception.ValidationError(attribute='id', raise exception.ValidationError(attribute='id',
target='token') target='token')
token = self._get_token_ref(auth_payload) token = self._get_token_ref(auth_payload)
if token.is_federated and PROVIDERS.federation_api: if token.is_federated and PROVIDERS.federation_api:
response_data = mapped.handle_scoped_token( response_data = mapped.handle_scoped_token(
request, token, PROVIDERS.federation_api, token, PROVIDERS.federation_api,
PROVIDERS.identity_api PROVIDERS.identity_api
) )
else: else:
response_data = token_authenticate(request, token) response_data = token_authenticate(token)
# NOTE(notmorgan): The Token auth method is *very* special and sets the # 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 # 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) response_data=response_data)
def token_authenticate(request, token): def token_authenticate(token):
response_data = {} response_data = {}
try: try:
@ -67,10 +68,11 @@ def token_authenticate(request, token):
# state in Keystone. To do so is to invite elevation of # state in Keystone. To do so is to invite elevation of
# privilege attacks # 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', {} 'scope', {}
) )
domain_scoped = 'domain' in request.json_body['auth'].get( domain_scoped = 'domain' in json_body['auth'].get(
'scope', {} 'scope', {}
) )

View File

@ -72,7 +72,7 @@ def _generate_totp_passcode(secret):
class TOTP(base.AuthMethodHandler): class TOTP(base.AuthMethodHandler):
def authenticate(self, request, auth_payload): def authenticate(self, auth_payload):
"""Try to authenticate using TOTP.""" """Try to authenticate using TOTP."""
response_data = {} response_data = {}
user_info = plugins.TOTPUserInfo.create(auth_payload, METHOD_NAME) user_info = plugins.TOTPUserInfo.create(auth_payload, METHOD_NAME)

View File

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

View File

@ -20,9 +20,9 @@ from oslo_utils import strutils
from keystone.common import authorization from keystone.common import authorization
from keystone.common import context from keystone.common import context
from keystone.common import controller
from keystone.common import policies from keystone.common import policies
from keystone.common import provider_api from keystone.common import provider_api
from keystone.common import render_token
from keystone.common import utils from keystone.common import utils
import keystone.conf import keystone.conf
from keystone import exception from keystone import exception
@ -85,7 +85,7 @@ class RBACEnforcer(object):
# oslo.policy for enforcement. This is because oslo.policy shouldn't # oslo.policy for enforcement. This is because oslo.policy shouldn't
# know how to deal with an internal object only used within keystone. # know how to deal with an internal object only used within keystone.
if 'token' in credentials: if 'token' in credentials:
token_ref = controller.render_token_response_from_model( token_ref = render_token.render_token_response_from_model(
credentials['token'] credentials['token']
) )
credentials_copy = copy.deepcopy(credentials) credentials_copy = copy.deepcopy(credentials)
@ -210,7 +210,7 @@ class RBACEnforcer(object):
ret_dict[target] = {} ret_dict[target] = {}
ret_dict[target]['user_id'] = token.user_id ret_dict[target]['user_id'] = token.user_id
try: try:
user_domain_id = token.user_domain_id user_domain_id = token.user['domain_id']
except exception.UnexpectedError: except exception.UnexpectedError:
user_domain_id = None user_domain_id = None
if user_domain_id: if user_domain_id:

View File

@ -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

View File

@ -43,6 +43,7 @@ from six.moves import http_client
from keystone.common import controller from keystone.common import controller
from keystone.common import provider_api from keystone.common import provider_api
from keystone.common import render_token
from keystone.common import utils from keystone.common import utils
from keystone.common import wsgi from keystone.common import wsgi
import keystone.conf import keystone.conf
@ -296,7 +297,7 @@ class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller):
token = self.token_provider_api.issue_token( token = self.token_provider_api.issue_token(
user_ref['id'], method_names, project_id=project_ref['id'] 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) return self.render_token_data_response(token.id, token_reference)
@controller.protected(callback=_check_credential_owner_and_user_id_match) @controller.protected(callback=_check_credential_owner_and_user_id_match)

View File

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

View File

@ -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,
})

View File

@ -15,6 +15,7 @@
import ast import ast
import re import re
import flask
import jsonschema import jsonschema
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
@ -416,10 +417,10 @@ def transform_to_group_ids(group_names, mapping_id,
group['name']) group['name'])
def get_assertion_params_from_env(request): def get_assertion_params_from_env():
LOG.debug('Environment variables: %s', request.environ) LOG.debug('Environment variables: %s', flask.request.environ)
prefix = CONF.federation.assertion_prefix 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): if not k.startswith(prefix):
continue continue
# These bytes may be decodable as ISO-8859-1 according to Section # These bytes may be decodable as ISO-8859-1 according to Section

View File

@ -105,7 +105,7 @@ class UserV3(controller.V3Controller):
attribute='password') attribute='password')
try: try:
PROVIDERS.identity_api.change_password( PROVIDERS.identity_api.change_password(
request, user_id, original_password, user_id, original_password,
password, initiator=request.audit_initiator) password, initiator=request.audit_initiator)
except AssertionError as e: except AssertionError as e:
raise exception.Unauthorized(_( raise exception.Unauthorized(_(

View File

@ -900,10 +900,19 @@ class Manager(manager.Manager):
# - clear/set domain_ids for drivers that do not support domains # - clear/set domain_ids for drivers that do not support domains
# - create any ID mapping that might be required # - 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') @notifications.emit_event('authenticate')
def authenticate(self, user_id, password):
return self._authenticate(user_id, password)
@domains_configured @domains_configured
@exception_translated('assertion') @exception_translated('assertion')
def authenticate(self, request, user_id, password): def _authenticate(self, user_id, password):
domain_id, driver, entity_id = ( domain_id, driver, entity_id = (
self._get_domain_driver_and_entity_id(user_id)) self._get_domain_driver_and_entity_id(user_id))
ref = driver.authenticate(entity_id, password) ref = driver.authenticate(entity_id, password)
@ -1376,12 +1385,14 @@ class Manager(manager.Manager):
group_entity_id) group_entity_id)
@domains_configured @domains_configured
def change_password(self, request, user_id, original_password, def change_password(self, user_id, original_password,
new_password, initiator=None): new_password, initiator=None):
# authenticate() will raise an AssertionError if authentication fails # authenticate() will raise an AssertionError if authentication fails
try: 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: except exception.PasswordExpired:
# If a password has expired, we want users to be able to change it # If a password has expired, we want users to be able to change it
pass pass

View File

@ -15,8 +15,8 @@ from oslo_log import log
from keystone.common import authorization from keystone.common import authorization
from keystone.common import context from keystone.common import context
from keystone.common import controller
from keystone.common import provider_api from keystone.common import provider_api
from keystone.common import render_token
from keystone.common import tokenless_auth from keystone.common import tokenless_auth
from keystone.common import wsgi from keystone.common import wsgi
import keystone.conf import keystone.conf
@ -45,7 +45,7 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
def fetch_token(self, token, **kwargs): def fetch_token(self, token, **kwargs):
try: try:
token_model = self.token_provider_api.validate_token(token) 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: except exception.TokenNotFound:
raise auth_token.InvalidToken(_('Could not find token')) raise auth_token.InvalidToken(_('Could not find token'))

View File

@ -19,6 +19,7 @@ import functools
import inspect import inspect
import socket import socket
import flask
from oslo_log import log from oslo_log import log
import oslo_messaging import oslo_messaging
from oslo_utils import reflection from oslo_utils import reflection
@ -27,9 +28,11 @@ from pycadf import cadftaxonomy as taxonomy
from pycadf import cadftype from pycadf import cadftype
from pycadf import credential from pycadf import credential
from pycadf import eventfactory from pycadf import eventfactory
from pycadf import host
from pycadf import reason from pycadf import reason
from pycadf import resource from pycadf import resource
from keystone.common import context
from keystone.common import provider_api from keystone.common import provider_api
from keystone.common import utils from keystone.common import utils
import keystone.conf import keystone.conf
@ -82,6 +85,26 @@ REMOVE_APP_CREDS_FOR_USER = 'remove_application_credentials_for_user'
DOMAIN_DELETED = 'domain_deleted' 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): class Audit(object):
"""Namespace for audit notification functions. """Namespace for audit notification functions.
@ -515,14 +538,14 @@ class CadfNotificationWrapper(object):
def __call__(self, f): def __call__(self, f):
@functools.wraps(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.""" """Will always send a notification."""
target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER) target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER)
initiator = request.audit_initiator initiator = build_audit_initiator()
initiator.user_id = user_id initiator.user_id = user_id
initiator.id = utils.resource_uuid(user_id) initiator.id = utils.resource_uuid(user_id)
try: try:
result = f(wrapped_self, request, user_id, *args, **kwargs) result = f(wrapped_self, user_id, *args, **kwargs)
except (exception.AccountLocked, except (exception.AccountLocked,
exception.PasswordExpired) as ex: exception.PasswordExpired) as ex:
# Send a CADF event with a reason for PCI-DSS related # Send a CADF event with a reason for PCI-DSS related
@ -657,15 +680,13 @@ class CadfRoleAssignmentNotificationWrapper(object):
return wrapper 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, identity_provider, protocol, token_id,
outcome): outcome):
"""Send notification to inform observers about SAML events. """Send notification to inform observers about SAML events.
:param action: Action being audited :param action: Action being audited
:type action: str :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 :param user_id: User ID from Keystone token
:type user_id: str :type user_id: str
:param group_ids: List of Group IDs from Keystone token :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` :param outcome: One of :class:`pycadf.cadftaxonomy`
:type outcome: str :type outcome: str
""" """
initiator = request.audit_initiator initiator = build_audit_initiator()
target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER) target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER)
audit_type = SAML_AUDIT_TYPE audit_type = SAML_AUDIT_TYPE
user_id = user_id or taxonomy.UNKNOWN user_id = user_id or taxonomy.UNKNOWN

View File

@ -16,7 +16,6 @@
from keystone.server.flask.common import APIBase # noqa from keystone.server.flask.common import APIBase # noqa
from keystone.server.flask.common import base_url # 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_json_home_data # noqa
from keystone.server.flask.common import construct_resource_map # noqa from keystone.server.flask.common import construct_resource_map # noqa
from keystone.server.flask.common import full_url # 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 # NOTE(morgan): This allows for from keystone.flask import * and have all the
# cool stuff needed to develop new APIs within a module/subsystem # cool stuff needed to develop new APIs within a module/subsystem
__all__ = ('APIBase', 'JsonHomeData', 'ResourceBase', 'ResourceMap', __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') 'construct_resource_map', 'full_url', 'unenforced_api')

View File

@ -26,11 +26,9 @@ import werkzeug.wsgi
import keystone.api import keystone.api
from keystone.application_credential import routers as app_cred_routers from keystone.application_credential import routers as app_cred_routers
from keystone.assignment import routers as assignment_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.common import wsgi as keystone_wsgi
from keystone.contrib.ec2 import routers as ec2_routers from keystone.contrib.ec2 import routers as ec2_routers
from keystone.contrib.s3 import routers as s3_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.identity import routers as identity_routers
from keystone.oauth1 import routers as oauth1_routers from keystone.oauth1 import routers as oauth1_routers
from keystone.resource import routers as resource_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 # TODO(morgan): _MOVED_API_PREFIXES to be removed when the legacy dispatch
# support is removed. # support is removed.
_MOVED_API_PREFIXES = frozenset( _MOVED_API_PREFIXES = frozenset(
['credentials', ['auth',
'credentials',
'domains', 'domains',
'endpoints', 'endpoints',
'groups', 'groups',
@ -64,12 +63,10 @@ _MOVED_API_PREFIXES = frozenset(
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
ALL_API_ROUTERS = [auth_routers, ALL_API_ROUTERS = [assignment_routers,
assignment_routers,
identity_routers, identity_routers,
app_cred_routers, app_cred_routers,
resource_routers, resource_routers,
federation_routers,
oauth1_routers, oauth1_routers,
ec2_routers, ec2_routers,
s3_routers] s3_routers]

View File

@ -25,9 +25,6 @@ import flask_restful.utils
from oslo_log import log from oslo_log import log
from oslo_log import versionutils from oslo_log import versionutils
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from pycadf import cadftaxonomy as taxonomy
from pycadf import host
from pycadf import resource
import six import six
from six.moves import http_client from six.moves import http_client
@ -40,6 +37,7 @@ from keystone.common import utils
import keystone.conf import keystone.conf
from keystone import exception from keystone import exception
from keystone.i18n import _ from keystone.i18n import _
from keystone import notifications
# NOTE(morgan): Capture the relevant part of the flask url route rule for # 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 Additional keyword arguments not specified above
will be passed as-is to will be passed as-is to
:meth:`flask.Flask.add_url_rule`. :meth:`flask.Flask.add_url_rule`.
:param alternate_urls: An iterable (list) of urls that also map to the :param alternate_urls: An iterable (list) of dictionaries containing urls
resource. These are used to ensure API compat when and associated json home REL data. Each element is
a "new" path is more correct for the API but old expected to be a dictionary with a 'url' key and an
paths must continue to work. Example: 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 `/auth/domains` being the new path for
`/OS-FEDERATION/domains`. The `OS-FEDERATION` part `/OS-FEDERATION/domains`. The `OS-FEDERATION` part
would be listed as an alternate url. These are not would be listed as an alternate url. If a
added to the JSON Home Document. 'json_home' key is provided, the original path
:type: any iterable or None with the new json_home data will be added to the
JSON Home Document.
:type: iterable or None
:param rel: :param rel:
:type rel: str or None :type rel: str or None
:param status: JSON Home API Status, e.g. "STABLE" :param status: JSON Home API Status, e.g. "STABLE"
@ -153,26 +157,6 @@ def _remove_content_type_on_204(resp):
return 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) @six.add_metaclass(abc.ABCMeta)
class APIBase(object): class APIBase(object):
@ -427,19 +411,33 @@ class APIBase(object):
def _add_mapped_resources(self): def _add_mapped_resources(self):
# Add resource mappings, non-standard resource connections # Add resource mappings, non-standard resource connections
for r in self.resource_mapping: for r in self.resource_mapping:
alt_url_json_home_data = []
LOG.debug( LOG.debug(
'Adding resource routes to API %(name)s: ' 'Adding resource routes to API %(name)s: '
'[%(url)r %(kwargs)r]', '[%(url)r %(kwargs)r]',
{'name': self._name, 'url': r.url, 'kwargs': r.kwargs}) {'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: if r.alternate_urls is not None:
LOG.debug( for element in r.alternate_urls:
'Adding additional resource routes (alternate) to API' if self._api_url_prefix:
'%(name)s: [%(urls)r %(kwargs)r]', LOG.debug(
{'name': self._name, 'urls': r.alternate_urls, 'Unable to add additional resource route '
'kwargs': r.kwargs}) '`%(route)s` to API %(name)s because API has a '
self.api.add_resource(r.resource, *r.alternate_urls, 'URL prefix. Only APIs without explicit prefixes '
**r.kwargs) '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 # Build the JSON Home data and add it to the relevant JSON Home
# Documents for explicit JSON Home data. # Documents for explicit JSON Home data.
@ -462,6 +460,12 @@ class APIBase(object):
r.json_home_data.rel, r.json_home_data.rel,
resource_data) 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): def _register_before_request_functions(self, functions=None):
"""Register functions to be executed in the `before request` phase. """Register functions to be executed in the `before request` phase.
@ -764,7 +768,7 @@ class ResourceBase(flask_restful.Resource):
As a property. As a property.
""" """
return build_audit_initiator() return notifications.build_audit_initiator()
@staticmethod @staticmethod
def query_filter_is_true(filter_name): def query_filter_is_true(filter_name):

View File

@ -146,6 +146,8 @@ def initialize_application(name, post_log_configured_function=lambda: None,
config_files = [dev_conf] config_files = [dev_conf]
keystone.server.configure(config_files=config_files) 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... # Log the options used when starting if we're in debug mode...
if CONF.debug: if CONF.debug:

View File

@ -257,13 +257,11 @@ class ApplicationCredentialTests(object):
app_cred = self._new_app_cred_data(self.user_foo['id'], app_cred = self._new_app_cred_data(self.user_foo['id'],
project_id=self.tenant_bar['id']) project_id=self.tenant_bar['id'])
resp = self.app_cred_api.create_application_credential(app_cred) resp = self.app_cred_api.create_application_credential(app_cred)
self.app_cred_api.authenticate( self.app_cred_api.authenticate(resp['id'], resp['secret'])
self.make_request(), resp['id'], resp['secret'])
def test_authenticate_not_found(self): def test_authenticate_not_found(self):
self.assertRaises(AssertionError, self.assertRaises(AssertionError,
self.app_cred_api.authenticate, self.app_cred_api.authenticate,
self.make_request(),
uuid.uuid4().hex, uuid.uuid4().hex,
uuid.uuid4().hex) uuid.uuid4().hex)
@ -275,7 +273,6 @@ class ApplicationCredentialTests(object):
resp = self.app_cred_api.create_application_credential(app_cred) resp = self.app_cred_api.create_application_credential(app_cred)
self.assertRaises(AssertionError, self.assertRaises(AssertionError,
self.app_cred_api.authenticate, self.app_cred_api.authenticate,
self.make_request(),
resp['id'], resp['id'],
resp['secret']) resp['secret'])
@ -287,6 +284,5 @@ class ApplicationCredentialTests(object):
self.assertNotEqual(badpass, resp['secret']) self.assertNotEqual(badpass, resp['secret'])
self.assertRaises(AssertionError, self.assertRaises(AssertionError,
self.app_cred_api.authenticate, self.app_cred_api.authenticate,
self.make_request(),
resp['id'], resp['id'],
badpass) badpass)

View File

@ -745,25 +745,26 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
user_ref = unit.new_user_ref(domain_id=self.domain_id, user_ref = unit.new_user_ref(domain_id=self.domain_id,
password=password) password=password)
user_ref = PROVIDERS.identity_api.create_user(user_ref) user_ref = PROVIDERS.identity_api.create_user(user_ref)
PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_ref['id'], password PROVIDERS.identity_api.authenticate(user_ref['id'], password)
)
freezer.stop() freezer.stop()
reason_type = (exception.PasswordExpired.message_format % reason_type = (exception.PasswordExpired.message_format %
{'user_id': user_ref['id']}) {'user_id': user_ref['id']})
expected_reason = {'reasonCode': '401', expected_reason = {'reasonCode': '401',
'reasonType': reason_type} 'reasonType': reason_type}
self.assertRaises(exception.PasswordExpired, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(exception.PasswordExpired,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=user_ref['id'], user_id=user_ref['id'],
password=password) password=password)
self._assert_last_audit(None, 'authenticate', None, self._assert_last_audit(None, 'authenticate', None,
cadftaxonomy.ACCOUNT_USER, cadftaxonomy.ACCOUNT_USER,
reason=expected_reason) reason=expected_reason)
def test_locked_out_user_sends_notification(self): 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 password = uuid.uuid4().hex
new_password = uuid.uuid4().hex new_password = uuid.uuid4().hex
expected_responses = [AssertionError, AssertionError, AssertionError, expected_responses = [AssertionError, AssertionError, AssertionError,
@ -776,12 +777,12 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
expected_reason = {'reasonCode': '401', expected_reason = {'reasonCode': '401',
'reasonType': reason_type} 'reasonType': reason_type}
for ex in expected_responses: for ex in expected_responses:
self.assertRaises(ex, with self.make_request():
PROVIDERS.identity_api.change_password, self.assertRaises(ex,
self.make_request(), PROVIDERS.identity_api.change_password,
user_id=user_ref['id'], user_id=user_ref['id'],
original_password=new_password, original_password=new_password,
new_password=new_password) new_password=new_password)
self._assert_last_audit(None, 'authenticate', None, self._assert_last_audit(None, 'authenticate', None,
cadftaxonomy.ACCOUNT_USER, cadftaxonomy.ACCOUNT_USER,
@ -801,16 +802,17 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
user_ref = unit.new_user_ref(domain_id=self.domain_id, user_ref = unit.new_user_ref(domain_id=self.domain_id,
password=password) password=password)
user_ref = PROVIDERS.identity_api.create_user(user_ref) user_ref = PROVIDERS.identity_api.create_user(user_ref)
PROVIDERS.identity_api.change_password( with self.make_request():
self.make_request(), user_id=user_ref['id'], PROVIDERS.identity_api.change_password(
original_password=password, new_password=new_password user_id=user_ref['id'],
) original_password=password, new_password=new_password
self.assertRaises(exception.PasswordValidationError, )
PROVIDERS.identity_api.change_password, with self.make_request():
self.make_request(), self.assertRaises(exception.PasswordValidationError,
user_id=user_ref['id'], PROVIDERS.identity_api.change_password,
original_password=new_password, user_id=user_ref['id'],
new_password=password) original_password=new_password,
new_password=password)
self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user', self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user',
cadftaxonomy.SECURITY_ACCOUNT_USER, cadftaxonomy.SECURITY_ACCOUNT_USER,
@ -828,12 +830,12 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
user_ref = unit.new_user_ref(domain_id=self.domain_id, user_ref = unit.new_user_ref(domain_id=self.domain_id,
password=password) password=password)
user_ref = PROVIDERS.identity_api.create_user(user_ref) user_ref = PROVIDERS.identity_api.create_user(user_ref)
self.assertRaises(exception.PasswordValidationError, with self.make_request():
PROVIDERS.identity_api.change_password, self.assertRaises(exception.PasswordValidationError,
self.make_request(), PROVIDERS.identity_api.change_password,
user_id=user_ref['id'], user_id=user_ref['id'],
original_password=password, original_password=password,
new_password=invalid_password) new_password=invalid_password)
self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user', self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user',
cadftaxonomy.SECURITY_ACCOUNT_USER, cadftaxonomy.SECURITY_ACCOUNT_USER,
@ -858,16 +860,17 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
{'min_age_days': min_days, 'days_left': days_left}) {'min_age_days': min_days, 'days_left': days_left})
expected_reason = {'reasonCode': '400', expected_reason = {'reasonCode': '400',
'reasonType': reason_type} 'reasonType': reason_type}
PROVIDERS.identity_api.change_password( with self.make_request():
self.make_request(), user_id=user_ref['id'], PROVIDERS.identity_api.change_password(
original_password=password, new_password=new_password user_id=user_ref['id'],
) original_password=password, new_password=new_password
self.assertRaises(exception.PasswordValidationError, )
PROVIDERS.identity_api.change_password, with self.make_request():
self.make_request(), self.assertRaises(exception.PasswordValidationError,
user_id=user_ref['id'], PROVIDERS.identity_api.change_password,
original_password=new_password, user_id=user_ref['id'],
new_password=next_password) original_password=new_password,
new_password=next_password)
self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user', self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user',
cadftaxonomy.SECURITY_ACCOUNT_USER, cadftaxonomy.SECURITY_ACCOUNT_USER,

View File

@ -10,11 +10,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import flask
import uuid import uuid
from oslo_config import fixture as config_fixture from oslo_config import fixture as config_fixture
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
import webob
from keystone.auth.plugins import mapped from keystone.auth.plugins import mapped
import keystone.conf import keystone.conf
@ -31,6 +31,13 @@ FAKE_MAPPING_ID = uuid.uuid4().hex
class MappingRuleEngineTests(unit.BaseTestCase): class MappingRuleEngineTests(unit.BaseTestCase):
"""A class for testing the mapping rule engine.""" """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, def assertValidMappedUserObject(self, mapped_properties,
user_type='ephemeral', user_type='ephemeral',
domain_id=None): domain_id=None):
@ -510,7 +517,7 @@ class MappingRuleEngineTests(unit.BaseTestCase):
self.assertValidMappedUserObject(mapped_properties) self.assertValidMappedUserObject(mapped_properties)
self.assertEqual('jsmith', mapped_properties['user']['name']) self.assertEqual('jsmith', mapped_properties['user']['name'])
unique_id, display_name = mapped.get_user_unique_id_and_display_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', unique_id)
self.assertEqual('jsmith', display_name) self.assertEqual('jsmith', display_name)
@ -533,7 +540,7 @@ class MappingRuleEngineTests(unit.BaseTestCase):
self.assertIsNotNone(mapped_properties) self.assertIsNotNone(mapped_properties)
self.assertValidMappedUserObject(mapped_properties) self.assertValidMappedUserObject(mapped_properties)
unique_id, display_name = mapped.get_user_unique_id_and_display_name( unique_id, display_name = mapped.get_user_unique_id_and_display_name(
{}, mapped_properties) mapped_properties)
self.assertEqual('tbo', display_name) self.assertEqual('tbo', display_name)
self.assertEqual('abc123%40example.com', unique_id) self.assertEqual('abc123%40example.com', unique_id)
@ -549,15 +556,15 @@ class MappingRuleEngineTests(unit.BaseTestCase):
as it was not explicitly specified in the mapping. as it was not explicitly specified in the mapping.
""" """
request = webob.Request.blank('/')
mapping = mapping_fixtures.MAPPING_USER_IDS mapping = mapping_fixtures.MAPPING_USER_IDS
rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules'])
assertion = mapping_fixtures.ADMIN_ASSERTION assertion = mapping_fixtures.ADMIN_ASSERTION
mapped_properties = rp.process(assertion) mapped_properties = rp.process(assertion)
self.assertIsNotNone(mapped_properties) self.assertIsNotNone(mapped_properties)
self.assertValidMappedUserObject(mapped_properties) self.assertValidMappedUserObject(mapped_properties)
unique_id, display_name = mapped.get_user_unique_id_and_display_name( with self.flask_app.test_request_context():
request, mapped_properties) unique_id, display_name = (
mapped.get_user_unique_id_and_display_name(mapped_properties))
self.assertEqual('bob', unique_id) self.assertEqual('bob', unique_id)
self.assertEqual('bob', display_name) self.assertEqual('bob', display_name)
@ -566,13 +573,14 @@ class MappingRuleEngineTests(unit.BaseTestCase):
mapping = mapping_fixtures.MAPPING_USER_IDS mapping = mapping_fixtures.MAPPING_USER_IDS
assertion = mapping_fixtures.ADMIN_ASSERTION assertion = mapping_fixtures.ADMIN_ASSERTION
FAKE_MAPPING_ID = uuid.uuid4().hex FAKE_MAPPING_ID = uuid.uuid4().hex
request = webob.Request.blank('/', remote_user='remote_user')
rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules'])
mapped_properties = rp.process(assertion) mapped_properties = rp.process(assertion)
self.assertIsNotNone(mapped_properties) self.assertIsNotNone(mapped_properties)
self.assertValidMappedUserObject(mapped_properties) self.assertValidMappedUserObject(mapped_properties)
unique_id, display_name = mapped.get_user_unique_id_and_display_name( with self.flask_app.test_request_context(
request, mapped_properties) 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('bob', unique_id)
self.assertEqual('remote_user', display_name) self.assertEqual('remote_user', display_name)
@ -597,7 +605,6 @@ class MappingRuleEngineTests(unit.BaseTestCase):
not to change it. not to change it.
""" """
request = webob.Request.blank('/')
testcases = [(mapping_fixtures.CUSTOMER_ASSERTION, 'bwilliams'), testcases = [(mapping_fixtures.CUSTOMER_ASSERTION, 'bwilliams'),
(mapping_fixtures.EMPLOYEE_ASSERTION, 'tbo')] (mapping_fixtures.EMPLOYEE_ASSERTION, 'tbo')]
for assertion, exp_user_name in testcases: for assertion, exp_user_name in testcases:
@ -607,8 +614,7 @@ class MappingRuleEngineTests(unit.BaseTestCase):
self.assertIsNotNone(mapped_properties) self.assertIsNotNone(mapped_properties)
self.assertValidMappedUserObject(mapped_properties) self.assertValidMappedUserObject(mapped_properties)
unique_id, display_name = ( unique_id, display_name = (
mapped.get_user_unique_id_and_display_name(request, mapped.get_user_unique_id_and_display_name(mapped_properties)
mapped_properties)
) )
self.assertEqual(exp_user_name, display_name) self.assertEqual(exp_user_name, display_name)
self.assertEqual('abc123%40example.com', unique_id) 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 # 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 # 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. # that our web server plugins are correctly encoding the data.
request = webob.Request.blank( # Create a dummy application
'/path', app = flask.Flask(__name__)
environ=mapping_fixtures.UNICODE_NAME_ASSERTION) with app.test_request_context(
data = mapping_utils.get_assertion_params_from_env(request) path='/path',
# NOTE(dstanek): keystone.auth.plugins.mapped environ_overrides=mapping_fixtures.UNICODE_NAME_ASSERTION):
return dict(data) data = mapping_utils.get_assertion_params_from_env()
# NOTE(dstanek): keystone.auth.plugins.mapped
return dict(data)
def test_unicode(self): def test_unicode(self):
mapping = self._pull_mapping_rules_from_the_database() mapping = self._pull_mapping_rules_from_the_database()

View File

@ -15,6 +15,7 @@
from __future__ import absolute_import from __future__ import absolute_import
import atexit import atexit
import base64 import base64
import contextlib
import datetime import datetime
import functools import functools
import hashlib import hashlib
@ -46,7 +47,6 @@ import keystone.api
from keystone.common import context from keystone.common import context
from keystone.common import json_home from keystone.common import json_home
from keystone.common import provider_api from keystone.common import provider_api
from keystone.common import request
from keystone.common import sql from keystone.common import sql
import keystone.conf import keystone.conf
from keystone import exception from keystone import exception
@ -684,19 +684,27 @@ class TestCase(BaseTestCase):
def _policy_fixture(self): def _policy_fixture(self):
return ksfixtures.Policy(dirs.etc('policy.json'), self.config_fixture) return ksfixtures.Policy(dirs.etc('policy.json'), self.config_fixture)
@contextlib.contextmanager
def make_request(self, path='/', **kwargs): 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) is_admin = kwargs.pop('is_admin', False)
environ = kwargs.setdefault('environ', {}) 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): if not environ.get(context.REQUEST_CONTEXT_ENV):
environ[context.REQUEST_CONTEXT_ENV] = context.RequestContext( environ[context.REQUEST_CONTEXT_ENV] = context.RequestContext(
is_admin=is_admin, is_admin=is_admin,
authenticated=kwargs.pop('authenticated', True)) authenticated=kwargs.pop('authenticated', True))
req = request.Request.blank(path=path, **kwargs) # Create a dummy flask app to work with
req.context_dict['is_admin'] = is_admin app = flask.Flask(__name__)
with app.test_request_context(path=path, environ_overrides=environ):
return req yield
def config_overrides(self): def config_overrides(self):
# NOTE(morganfainberg): enforce config_overrides can only ever be # NOTE(morganfainberg): enforce config_overrides can only ever be
@ -779,6 +787,8 @@ class TestCase(BaseTestCase):
new=mocked_register_auth_plugin_opt)) new=mocked_register_auth_plugin_opt))
self.config_overrides() self.config_overrides()
# explicitly load auth configuration
keystone.conf.auth.setup_authentication()
# NOTE(morganfainberg): ensure config_overrides has been called. # NOTE(morganfainberg): ensure config_overrides has been called.
self.addCleanup(self._assert_config_overrides_called) self.addCleanup(self._assert_config_overrides_called)

View File

@ -121,10 +121,10 @@ class ShadowUsersBackendTests(object):
now = datetime.datetime.utcnow().date() now = datetime.datetime.utcnow().date()
password = uuid.uuid4().hex password = uuid.uuid4().hex
user = self._create_user(password) user = self._create_user(password)
user_auth = PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_auth = PROVIDERS.identity_api.authenticate(
user_id=user['id'], user_id=user['id'],
password=password) password=password)
user_ref = self._get_user_ref(user_auth['id']) user_ref = self._get_user_ref(user_auth['id'])
self.assertGreaterEqual(now, user_ref.last_active_at) self.assertGreaterEqual(now, user_ref.last_active_at)
@ -133,10 +133,10 @@ class ShadowUsersBackendTests(object):
disable_user_account_days_inactive=None) disable_user_account_days_inactive=None)
password = uuid.uuid4().hex password = uuid.uuid4().hex
user = self._create_user(password) user = self._create_user(password)
user_auth = PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_auth = PROVIDERS.identity_api.authenticate(
user_id=user['id'], user_id=user['id'],
password=password) password=password)
user_ref = self._get_user_ref(user_auth['id']) user_ref = self._get_user_ref(user_auth['id'])
self.assertIsNone(user_ref.last_active_at) self.assertIsNone(user_ref.last_active_at)

View File

@ -272,21 +272,21 @@ class DisableInactiveUserTests(test_backend_sql.SqlTests):
datetime.datetime.utcnow() - datetime.datetime.utcnow() -
datetime.timedelta(days=self.max_inactive_days + 1)) datetime.timedelta(days=self.max_inactive_days + 1))
user = self._create_user(self.user_dict, last_active_at.date()) user = self._create_user(self.user_dict, last_active_at.date())
self.assertRaises(exception.UserDisabled, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(exception.UserDisabled,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=user['id'], user_id=user['id'],
password=self.password) password=self.password)
# verify that the user is actually disabled # verify that the user is actually disabled
user = PROVIDERS.identity_api.get_user(user['id']) user = PROVIDERS.identity_api.get_user(user['id'])
self.assertFalse(user['enabled']) self.assertFalse(user['enabled'])
# set the user to enabled and authenticate # set the user to enabled and authenticate
user['enabled'] = True user['enabled'] = True
PROVIDERS.identity_api.update_user(user['id'], user) PROVIDERS.identity_api.update_user(user['id'], user)
user = PROVIDERS.identity_api.authenticate( user = PROVIDERS.identity_api.authenticate(
self.make_request(), user_id=user['id'], password=self.password user_id=user['id'], password=self.password
) )
self.assertTrue(user['enabled']) self.assertTrue(user['enabled'])
def test_authenticate_user_not_disabled_due_to_inactivity(self): def test_authenticate_user_not_disabled_due_to_inactivity(self):
# create user and set last_active_at just below the max # 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.datetime.utcnow() -
datetime.timedelta(days=self.max_inactive_days - 1)).date() datetime.timedelta(days=self.max_inactive_days - 1)).date()
user = self._create_user(self.user_dict, last_active_at) user = self._create_user(self.user_dict, last_active_at)
user = PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_id=user['id'], password=self.password user = PROVIDERS.identity_api.authenticate(
) user_id=user['id'], password=self.password
)
self.assertTrue(user['enabled']) self.assertTrue(user['enabled'])
def test_get_user_disabled_due_to_inactivity(self): def test_get_user_disabled_due_to_inactivity(self):
@ -392,22 +393,21 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests):
password = uuid.uuid4().hex password = uuid.uuid4().hex
user = self._create_user(password) user = self._create_user(password)
# Attempt to change to the same password # Attempt to change to the same password
self.assertRaises(exception.PasswordValidationError, with self.make_request():
PROVIDERS.identity_api.change_password, self.assertRaises(exception.PasswordValidationError,
self.make_request(), PROVIDERS.identity_api.change_password,
user_id=user['id'], user_id=user['id'],
original_password=password, original_password=password,
new_password=password) new_password=password)
# Attempt to change to a unique password # Attempt to change to a unique password
new_password = uuid.uuid4().hex new_password = uuid.uuid4().hex
self.assertValidChangePassword(user['id'], password, new_password) self.assertValidChangePassword(user['id'], password, new_password)
# Attempt to change back to the initial password # Attempt to change back to the initial password
self.assertRaises(exception.PasswordValidationError, self.assertRaises(exception.PasswordValidationError,
PROVIDERS.identity_api.change_password, PROVIDERS.identity_api.change_password,
self.make_request(), user_id=user['id'],
user_id=user['id'], original_password=new_password,
original_password=new_password, new_password=password)
new_password=password)
def test_validate_password_history_with_valid_password(self): def test_validate_password_history_with_valid_password(self):
passwords = [uuid.uuid4().hex, uuid.uuid4().hex, uuid.uuid4().hex, 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-service change password
self.assertValidChangePassword(user['id'], passwords[0], passwords[1]) self.assertValidChangePassword(user['id'], passwords[0], passwords[1])
# Attempt to update with a previous password # Attempt to update with a previous password
self.assertRaises(exception.PasswordValidationError, with self.make_request():
PROVIDERS.identity_api.change_password, self.assertRaises(exception.PasswordValidationError,
self.make_request(), PROVIDERS.identity_api.change_password,
user_id=user['id'], user_id=user['id'],
original_password=passwords[1], original_password=passwords[1],
new_password=passwords[0]) new_password=passwords[0])
def test_disable_password_history_and_repeat_same_password(self): def test_disable_password_history_and_repeat_same_password(self):
self.config_fixture.config(group='security_compliance', self.config_fixture.config(group='security_compliance',
@ -462,22 +462,23 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests):
user = self._create_user(passwords[0]) user = self._create_user(passwords[0])
# Attempt to change password to a unique password # Attempt to change password to a unique password
user['password'] = passwords[1] user['password'] = passwords[1]
PROVIDERS.identity_api.update_user(user['id'], user) with self.make_request():
PROVIDERS.identity_api.authenticate( PROVIDERS.identity_api.update_user(user['id'], user)
self.make_request(), user_id=user['id'], password=passwords[1] PROVIDERS.identity_api.authenticate(
) user_id=user['id'], password=passwords[1]
# Attempt to change password with the same password )
user['password'] = passwords[1] # Attempt to change password with the same password
PROVIDERS.identity_api.update_user(user['id'], user) user['password'] = passwords[1]
PROVIDERS.identity_api.authenticate( PROVIDERS.identity_api.update_user(user['id'], user)
self.make_request(), user_id=user['id'], password=passwords[1] PROVIDERS.identity_api.authenticate(
) user_id=user['id'], password=passwords[1]
# Attempt to change password with the initial password )
user['password'] = passwords[0] # Attempt to change password with the initial password
PROVIDERS.identity_api.update_user(user['id'], user) user['password'] = passwords[0]
PROVIDERS.identity_api.authenticate( PROVIDERS.identity_api.update_user(user['id'], user)
self.make_request(), user_id=user['id'], password=passwords[0] PROVIDERS.identity_api.authenticate(
) user_id=user['id'], password=passwords[0]
)
def test_truncate_passwords(self): def test_truncate_passwords(self):
user = self._create_user(uuid.uuid4().hex) user = self._create_user(uuid.uuid4().hex)
@ -535,13 +536,14 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests):
return PROVIDERS.identity_api.create_user(user) return PROVIDERS.identity_api.create_user(user)
def assertValidChangePassword(self, user_id, password, new_password): def assertValidChangePassword(self, user_id, password, new_password):
PROVIDERS.identity_api.change_password( with self.make_request():
self.make_request(), user_id=user_id, original_password=password, PROVIDERS.identity_api.change_password(
new_password=new_password 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 PROVIDERS.identity_api.authenticate(
) user_id=user_id, password=new_password
)
def _add_passwords_to_history(self, user, n): def _add_passwords_to_history(self, user, n):
for _ in range(n): for _ in range(n):
@ -573,24 +575,23 @@ class LockingOutUserTests(test_backend_sql.SqlTests):
self.user = PROVIDERS.identity_api.create_user(user_dict) self.user = PROVIDERS.identity_api.create_user(user_dict)
def test_locking_out_user_after_max_failed_attempts(self): def test_locking_out_user_after_max_failed_attempts(self):
# authenticate with wrong password with self.make_request():
self.assertRaises(AssertionError, # authenticate with wrong password
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=self.user['id'], user_id=self.user['id'],
password=uuid.uuid4().hex) password=uuid.uuid4().hex)
# authenticate with correct password # authenticate with correct password
PROVIDERS.identity_api.authenticate( PROVIDERS.identity_api.authenticate(
self.make_request(), user_id=self.user['id'], user_id=self.user['id'],
password=self.password password=self.password
) )
# test locking out user after max failed attempts # test locking out user after max failed attempts
self._fail_auth_repeatedly(self.user['id']) self._fail_auth_repeatedly(self.user['id'])
self.assertRaises(exception.AccountLocked, self.assertRaises(exception.AccountLocked,
PROVIDERS.identity_api.authenticate, PROVIDERS.identity_api.authenticate,
self.make_request(), user_id=self.user['id'],
user_id=self.user['id'], password=uuid.uuid4().hex)
password=uuid.uuid4().hex)
def test_lock_out_for_ignored_user(self): def test_lock_out_for_ignored_user(self):
# mark the user as exempt from failed password attempts # 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 # fail authentication repeatedly the max number of times
self._fail_auth_repeatedly(self.user['id']) self._fail_auth_repeatedly(self.user['id'])
# authenticate with wrong password, account should not be locked # authenticate with wrong password, account should not be locked
self.assertRaises(AssertionError, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=self.user['id'], user_id=self.user['id'],
password=uuid.uuid4().hex) password=uuid.uuid4().hex)
# authenticate with correct password, account should not be locked # authenticate with correct password, account should not be locked
PROVIDERS.identity_api.authenticate( PROVIDERS.identity_api.authenticate(
self.make_request(), user_id=self.user['id'], user_id=self.user['id'],
password=self.password password=self.password
) )
def test_set_enabled_unlocks_user(self): def test_set_enabled_unlocks_user(self):
# lockout user with self.make_request():
self._fail_auth_repeatedly(self.user['id']) # lockout user
self.assertRaises(exception.AccountLocked, self._fail_auth_repeatedly(self.user['id'])
PROVIDERS.identity_api.authenticate, self.assertRaises(exception.AccountLocked,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=self.user['id'], user_id=self.user['id'],
password=uuid.uuid4().hex) password=uuid.uuid4().hex)
# set enabled, user should be unlocked # set enabled, user should be unlocked
self.user['enabled'] = True self.user['enabled'] = True
PROVIDERS.identity_api.update_user(self.user['id'], self.user) PROVIDERS.identity_api.update_user(self.user['id'], self.user)
user_ret = PROVIDERS.identity_api.authenticate( user_ret = PROVIDERS.identity_api.authenticate(
self.make_request(), user_id=self.user['id'], user_id=self.user['id'],
password=self.password password=self.password
) )
self.assertTrue(user_ret['enabled']) self.assertTrue(user_ret['enabled'])
def test_lockout_duration(self): def test_lockout_duration(self):
# freeze time # freeze time
with freezegun.freeze_time(datetime.datetime.utcnow()) as frozen_time: with freezegun.freeze_time(datetime.datetime.utcnow()) as frozen_time:
# lockout user with self.make_request():
self._fail_auth_repeatedly(self.user['id']) # lockout user
self.assertRaises(exception.AccountLocked, self._fail_auth_repeatedly(self.user['id'])
PROVIDERS.identity_api.authenticate, self.assertRaises(exception.AccountLocked,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=self.user['id'], user_id=self.user['id'],
password=uuid.uuid4().hex) password=uuid.uuid4().hex)
# freeze time past the duration, user should be unlocked and failed # freeze time past the duration, user should be unlocked and
# auth count should get reset # failed auth count should get reset
frozen_time.tick(delta=datetime.timedelta( frozen_time.tick(delta=datetime.timedelta(
seconds=CONF.security_compliance.lockout_duration + 1)) seconds=CONF.security_compliance.lockout_duration + 1))
PROVIDERS.identity_api.authenticate( PROVIDERS.identity_api.authenticate(
self.make_request(), user_id=self.user['id'], user_id=self.user['id'],
password=self.password password=self.password
) )
# test failed auth count was reset by authenticating with the wrong # test failed auth count was reset by authenticating with the
# password, should raise an assertion error and not account locked # wrong password, should raise an assertion error and not
self.assertRaises(AssertionError, # account locked
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=self.user['id'], user_id=self.user['id'],
password=uuid.uuid4().hex) password=uuid.uuid4().hex)
def test_lockout_duration_failed_auth_cnt_resets(self): def test_lockout_duration_failed_auth_cnt_resets(self):
# freeze time # freeze time
with freezegun.freeze_time(datetime.datetime.utcnow()) as frozen_time: with freezegun.freeze_time(datetime.datetime.utcnow()) as frozen_time:
# lockout user with self.make_request():
self._fail_auth_repeatedly(self.user['id']) # lockout user
self.assertRaises(exception.AccountLocked, self._fail_auth_repeatedly(self.user['id'])
PROVIDERS.identity_api.authenticate, self.assertRaises(exception.AccountLocked,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=self.user['id'], user_id=self.user['id'],
password=uuid.uuid4().hex) password=uuid.uuid4().hex)
# freeze time past the duration, failed_auth_cnt should reset # freeze time past the duration, failed_auth_cnt should reset
frozen_time.tick(delta=datetime.timedelta( frozen_time.tick(delta=datetime.timedelta(
seconds=CONF.security_compliance.lockout_duration + 1)) seconds=CONF.security_compliance.lockout_duration + 1))
# repeat failed auth the max times # repeat failed auth the max times
self._fail_auth_repeatedly(self.user['id']) self._fail_auth_repeatedly(self.user['id'])
# test user account is locked # test user account is locked
self.assertRaises(exception.AccountLocked, self.assertRaises(exception.AccountLocked,
PROVIDERS.identity_api.authenticate, PROVIDERS.identity_api.authenticate,
self.make_request(), user_id=self.user['id'],
user_id=self.user['id'], password=uuid.uuid4().hex)
password=uuid.uuid4().hex)
def _fail_auth_repeatedly(self, user_id): def _fail_auth_repeatedly(self, user_id):
wrong_password = uuid.uuid4().hex wrong_password = uuid.uuid4().hex
for _ in range(CONF.security_compliance.lockout_failure_attempts): for _ in range(CONF.security_compliance.lockout_failure_attempts):
self.assertRaises(AssertionError, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=user_id, user_id=user_id,
password=wrong_password) password=wrong_password)
class PasswordExpiresValidationTests(test_backend_sql.SqlTests): 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) user = self._create_user(self.user_dict, password_created_at)
# test password is expired # test password is expired
self.assertRaises(exception.PasswordExpired, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(exception.PasswordExpired,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=user['id'], user_id=user['id'],
password=self.password) password=self.password)
def test_authenticate_with_non_expired_password(self): def test_authenticate_with_non_expired_password(self):
# set password created_at so that the password will not expire # 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) user = self._create_user(self.user_dict, password_created_at)
# test password is not expired # test password is not expired
PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_id=user['id'], password=self.password PROVIDERS.identity_api.authenticate(
) user_id=user['id'], password=self.password
)
def test_authenticate_with_expired_password_for_ignore_user_option(self): def test_authenticate_with_expired_password_for_ignore_user_option(self):
# set user to have the 'ignore_password_expiry' option set to False # 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) days=CONF.security_compliance.password_expires_days + 1)
) )
user = self._create_user(self.user_dict, password_created_at) user = self._create_user(self.user_dict, password_created_at)
self.assertRaises(exception.PasswordExpired, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(exception.PasswordExpired,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=user['id'], user_id=user['id'],
password=self.password) password=self.password)
# update user to explicitly have the expiry option to True # update user to explicitly have the expiry option to True
user['options'][ user['options'][
iro.IGNORE_PASSWORD_EXPIRY_OPT.option_name] = True iro.IGNORE_PASSWORD_EXPIRY_OPT.option_name] = True
user = PROVIDERS.identity_api.update_user( user = PROVIDERS.identity_api.update_user(
user['id'], user user['id'], user
) )
# test password is not expired due to ignore option # test password is not expired due to ignore option
PROVIDERS.identity_api.authenticate( PROVIDERS.identity_api.authenticate(
self.make_request(), user_id=user['id'], password=self.password user_id=user['id'], password=self.password
) )
def _get_test_user_dict(self, password): def _get_test_user_dict(self, password):
test_user_dict = { test_user_dict = {
@ -790,12 +791,12 @@ class MinimumPasswordAgeTests(test_backend_sql.SqlTests):
self.assertValidChangePassword(self.user['id'], self.initial_password, self.assertValidChangePassword(self.user['id'], self.initial_password,
new_password) new_password)
# user cannot change password before min age # user cannot change password before min age
self.assertRaises(exception.PasswordAgeValidationError, with self.make_request():
PROVIDERS.identity_api.change_password, self.assertRaises(exception.PasswordAgeValidationError,
self.make_request(), PROVIDERS.identity_api.change_password,
user_id=self.user['id'], user_id=self.user['id'],
original_password=new_password, original_password=new_password,
new_password=uuid.uuid4().hex) new_password=uuid.uuid4().hex)
def test_user_can_change_password_after_min_age(self): def test_user_can_change_password_after_min_age(self):
# user can change password after create # user can change password after create
@ -818,12 +819,13 @@ class MinimumPasswordAgeTests(test_backend_sql.SqlTests):
self.assertValidChangePassword(self.user['id'], self.initial_password, self.assertValidChangePassword(self.user['id'], self.initial_password,
new_password) new_password)
# user cannot change password before min age # user cannot change password before min age
self.assertRaises(exception.PasswordAgeValidationError,
PROVIDERS.identity_api.change_password, with self.make_request():
self.make_request(), self.assertRaises(exception.PasswordAgeValidationError,
user_id=self.user['id'], PROVIDERS.identity_api.change_password,
original_password=new_password, user_id=self.user['id'],
new_password=uuid.uuid4().hex) original_password=new_password,
new_password=uuid.uuid4().hex)
# admin reset # admin reset
new_password = uuid.uuid4().hex new_password = uuid.uuid4().hex
self.user['password'] = new_password self.user['password'] = new_password
@ -833,13 +835,14 @@ class MinimumPasswordAgeTests(test_backend_sql.SqlTests):
uuid.uuid4().hex) uuid.uuid4().hex)
def assertValidChangePassword(self, user_id, password, new_password): def assertValidChangePassword(self, user_id, password, new_password):
PROVIDERS.identity_api.change_password( with self.make_request():
self.make_request(), user_id=user_id, original_password=password, PROVIDERS.identity_api.change_password(
new_password=new_password 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 PROVIDERS.identity_api.authenticate(
) user_id=user_id, password=new_password
)
def _create_new_user(self, password): def _create_new_user(self, password):
user = { user = {
@ -881,16 +884,17 @@ class ChangePasswordRequiredAfterFirstUse(test_backend_sql.SqlTests):
return PROVIDERS.identity_api.create_user(user_dict) return PROVIDERS.identity_api.create_user(user_dict)
def assertPasswordIsExpired(self, user_id, password): def assertPasswordIsExpired(self, user_id, password):
self.assertRaises(exception.PasswordExpired, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(exception.PasswordExpired,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=user_id, user_id=user_id,
password=password) password=password)
def assertPasswordIsNotExpired(self, user_id, password): def assertPasswordIsNotExpired(self, user_id, password):
PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_id=user_id, password=password PROVIDERS.identity_api.authenticate(
) user_id=user_id, password=password
)
def test_password_expired_after_create(self): def test_password_expired_after_create(self):
# create user, password expired # create user, password expired
@ -899,9 +903,10 @@ class ChangePasswordRequiredAfterFirstUse(test_backend_sql.SqlTests):
self.assertPasswordIsExpired(user['id'], initial_password) self.assertPasswordIsExpired(user['id'], initial_password)
# change password (self-service), password not expired # change password (self-service), password not expired
new_password = uuid.uuid4().hex new_password = uuid.uuid4().hex
PROVIDERS.identity_api.change_password( with self.make_request():
self.make_request(), user['id'], initial_password, new_password PROVIDERS.identity_api.change_password(
) user['id'], initial_password, new_password
)
self.assertPasswordIsNotExpired(user['id'], new_password) self.assertPasswordIsNotExpired(user['id'], new_password)
def test_password_expired_after_reset(self): def test_password_expired_after_reset(self):
@ -920,9 +925,10 @@ class ChangePasswordRequiredAfterFirstUse(test_backend_sql.SqlTests):
self.assertPasswordIsExpired(user['id'], admin_password) self.assertPasswordIsExpired(user['id'], admin_password)
# change password (self-service), password not expired # change password (self-service), password not expired
new_password = uuid.uuid4().hex new_password = uuid.uuid4().hex
PROVIDERS.identity_api.change_password( with self.make_request():
self.make_request(), user['id'], admin_password, new_password PROVIDERS.identity_api.change_password(
) user['id'], admin_password, new_password
)
self.assertPasswordIsNotExpired(user['id'], new_password) self.assertPasswordIsNotExpired(user['id'], new_password)
def test_password_not_expired_when_feature_disabled(self): def test_password_not_expired_when_feature_disabled(self):

View File

@ -43,25 +43,25 @@ class IdentityTests(object):
return domain_id return domain_id
def test_authenticate_bad_user(self): def test_authenticate_bad_user(self):
self.assertRaises(AssertionError, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=uuid.uuid4().hex, user_id=uuid.uuid4().hex,
password=self.user_foo['password']) password=self.user_foo['password'])
def test_authenticate_bad_password(self): def test_authenticate_bad_password(self):
self.assertRaises(AssertionError, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=self.user_foo['id'], user_id=self.user_foo['id'],
password=uuid.uuid4().hex) password=uuid.uuid4().hex)
def test_authenticate(self): def test_authenticate(self):
user_ref = PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_ref = PROVIDERS.identity_api.authenticate(
user_id=self.user_sna['id'], user_id=self.user_sna['id'],
password=self.user_sna['password']) password=self.user_sna['password'])
# NOTE(termie): the password field is left in user_sna to make # NOTE(termie): the password field is left in user_sna to make
# it easier to authenticate in tests, but should # it easier to authenticate in tests, but should
# not be returned by the api # not be returned by the api
self.user_sna.pop('password') self.user_sna.pop('password')
@ -83,10 +83,10 @@ class IdentityTests(object):
PROVIDERS.assignment_api.add_role_to_user_and_project( PROVIDERS.assignment_api.add_role_to_user_and_project(
new_user['id'], self.tenant_baz['id'], role_member['id'] new_user['id'], self.tenant_baz['id'], role_member['id']
) )
user_ref = PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_ref = PROVIDERS.identity_api.authenticate(
user_id=new_user['id'], user_id=new_user['id'],
password=user['password']) password=user['password'])
self.assertNotIn('password', user_ref) self.assertNotIn('password', user_ref)
# NOTE(termie): the password field is left in user_sna to make # NOTE(termie): the password field is left in user_sna to make
# it easier to authenticate in tests, but should # 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) user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id)
PROVIDERS.identity_api.create_user(user) PROVIDERS.identity_api.create_user(user)
self.assertRaises(AssertionError, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=id_, user_id=id_,
password='password') password='password')
def test_create_unicode_user_name(self): def test_create_unicode_user_name(self):
unicode_name = u'name \u540d\u5b57' unicode_name = u'name \u540d\u5b57'
@ -394,16 +394,15 @@ class IdentityTests(object):
PROVIDERS.identity_api.get_user(user['id']) PROVIDERS.identity_api.get_user(user['id'])
# Make sure the user is not allowed to login # Make sure the user is not allowed to login
# with a password that is empty string or None # with a password that is empty string or None
self.assertRaises(AssertionError, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=user['id'], user_id=user['id'],
password='') password='')
self.assertRaises(AssertionError, self.assertRaises(AssertionError,
PROVIDERS.identity_api.authenticate, PROVIDERS.identity_api.authenticate,
self.make_request(), user_id=user['id'],
user_id=user['id'], password=None)
password=None)
def test_create_user_none_password(self): def test_create_user_none_password(self):
user = unit.new_user_ref(password=None, user = unit.new_user_ref(password=None,
@ -412,16 +411,15 @@ class IdentityTests(object):
PROVIDERS.identity_api.get_user(user['id']) PROVIDERS.identity_api.get_user(user['id'])
# Make sure the user is not allowed to login # Make sure the user is not allowed to login
# with a password that is empty string or None # with a password that is empty string or None
self.assertRaises(AssertionError, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=user['id'], user_id=user['id'],
password='') password='')
self.assertRaises(AssertionError, self.assertRaises(AssertionError,
PROVIDERS.identity_api.authenticate, PROVIDERS.identity_api.authenticate,
self.make_request(), user_id=user['id'],
user_id=user['id'], password=None)
password=None)
def test_create_user_invalid_name_fails(self): def test_create_user_invalid_name_fails(self):
user = unit.new_user_ref(name=None, user = unit.new_user_ref(name=None,

View File

@ -15,6 +15,7 @@ import uuid
import fixtures import fixtures
import flask import flask
import flask_restful import flask_restful
import functools
from oslo_policy import policy from oslo_policy import policy
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from testtools import matchers from testtools import matchers
@ -402,11 +403,19 @@ class TestKeystoneFlaskCommon(rest.RestfulTestCase):
expected_status_code=420) expected_status_code=420)
def test_construct_resource_map(self): def test_construct_resource_map(self):
resource_name = 'arguments'
param_relation = json_home.build_v3_parameter_relation( param_relation = json_home.build_v3_parameter_relation(
'argument_id') 'argument_id')
alt_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation,
extension_name='extension', extension_version='1.0')
url = '/v3/arguments/<string:argument_id>' url = '/v3/arguments/<string:argument_id>'
old_url = ['/v3/old_arguments/<string:argument_id>'] old_url = [dict(
resource_name = 'arguments' url='/v3/old_arguments/<string:argument_id>',
json_home=flask_common.construct_json_home_data(
rel='arguments',
resource_relation_func=alt_rel_func)
)]
mapping = flask_common.construct_resource_map( mapping = flask_common.construct_resource_map(
resource=_TestResourceWithCollectionInfo, resource=_TestResourceWithCollectionInfo,
@ -420,13 +429,17 @@ class TestKeystoneFlaskCommon(rest.RestfulTestCase):
self.assertEqual(_TestResourceWithCollectionInfo, self.assertEqual(_TestResourceWithCollectionInfo,
mapping.resource) mapping.resource)
self.assertEqual(url, mapping.url) self.assertEqual(url, mapping.url)
self.assertEqual(old_url, mapping.alternate_urls)
self.assertEqual(json_home.build_v3_resource_relation(resource_name), self.assertEqual(json_home.build_v3_resource_relation(resource_name),
mapping.json_home_data.rel) mapping.json_home_data.rel)
self.assertEqual(json_home.Status.EXPERIMENTAL, self.assertEqual(json_home.Status.EXPERIMENTAL,
mapping.json_home_data.status) mapping.json_home_data.status)
self.assertEqual({'argument_id': param_relation}, self.assertEqual({'argument_id': param_relation},
mapping.json_home_data.path_vars) 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): def test_instantiate_and_register_to_app(self):
# Test that automatic instantiation and registration to app works. # Test that automatic instantiation and registration to app works.

View File

@ -17,6 +17,7 @@ import uuid
import mock import mock
import stevedore import stevedore
from keystone.api._shared import authentication
from keystone import auth from keystone import auth
from keystone.auth.plugins import base from keystone.auth.plugins import base
from keystone.auth.plugins import mapped from keystone.auth.plugins import mapped
@ -32,7 +33,7 @@ DEMO_USER_ID = uuid.uuid4().hex
class SimpleChallengeResponse(base.AuthMethodHandler): class SimpleChallengeResponse(base.AuthMethodHandler):
def authenticate(self, context, auth_payload): def authenticate(self, auth_payload):
response_data = {} response_data = {}
if 'response' in auth_payload: if 'response' in auth_payload:
if auth_payload['response'] != EXPECTED_RESPONSE: if auth_payload['response'] != EXPECTED_RESPONSE:
@ -50,9 +51,6 @@ class SimpleChallengeResponse(base.AuthMethodHandler):
class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase): class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase):
def setUp(self):
super(TestAuthPlugin, self).setUp()
self.api = auth.controllers.Auth()
def test_unsupported_auth_method(self): def test_unsupported_auth_method(self):
method_name = uuid.uuid4().hex method_name = uuid.uuid4().hex
@ -85,7 +83,8 @@ class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase):
auth_info = auth.core.AuthInfo.create(auth_data) auth_info = auth.core.AuthInfo.create(auth_data)
auth_context = auth.core.AuthContext(method_names=[]) auth_context = auth.core.AuthContext(method_names=[])
try: 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: except exception.AdditionalAuthRequired as e:
self.assertIn('methods', e.authentication) self.assertIn('methods', e.authentication)
self.assertIn(METHOD_NAME, e.authentication['methods']) self.assertIn(METHOD_NAME, e.authentication['methods'])
@ -99,7 +98,8 @@ class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase):
auth_data = {'identity': auth_data} auth_data = {'identity': auth_data}
auth_info = auth.core.AuthInfo.create(auth_data) auth_info = auth.core.AuthInfo.create(auth_data)
auth_context = auth.core.AuthContext(method_names=[]) 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']) self.assertEqual(DEMO_USER_ID, auth_context['user_id'])
# test incorrect response # test incorrect response
@ -109,11 +109,11 @@ class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase):
auth_data = {'identity': auth_data} auth_data = {'identity': auth_data}
auth_info = auth.core.AuthInfo.create(auth_data) auth_info = auth.core.AuthInfo.create(auth_data)
auth_context = auth.core.AuthContext(method_names=[]) auth_context = auth.core.AuthContext(method_names=[])
self.assertRaises(exception.Unauthorized, with self.make_request():
self.api.authenticate, self.assertRaises(exception.Unauthorized,
self.make_request(), authentication.authenticate,
auth_info, auth_info,
auth_context) auth_context)
def test_duplicate_method(self): def test_duplicate_method(self):
# Having the same method twice doesn't cause load_auth_methods to fail. # Having the same method twice doesn't cause load_auth_methods to fail.
@ -138,9 +138,6 @@ class TestAuthPluginDynamicOptions(TestAuthPlugin):
class TestMapped(unit.TestCase): class TestMapped(unit.TestCase):
def setUp(self):
super(TestMapped, self).setUp()
self.api = auth.controllers.Auth()
def config_files(self): def config_files(self):
config_files = super(TestMapped, self).config_files() config_files = super(TestMapped, self).config_files()
@ -151,7 +148,6 @@ class TestMapped(unit.TestCase):
with mock.patch.object(auth.plugins.mapped.Mapped, with mock.patch.object(auth.plugins.mapped.Mapped,
'authenticate', 'authenticate',
return_value=None) as authenticate: return_value=None) as authenticate:
request = self.make_request()
auth_data = { auth_data = {
'identity': { 'identity': {
'methods': [method_name], 'methods': [method_name],
@ -162,10 +158,10 @@ class TestMapped(unit.TestCase):
auth_context = auth.core.AuthContext( auth_context = auth.core.AuthContext(
method_names=[], method_names=[],
user_id=uuid.uuid4().hex) 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 # make sure Mapped plugin got invoked with the correct payload
((context, auth_payload), ((auth_payload,), kwargs) = authenticate.call_args
kwargs) = authenticate.call_args
self.assertEqual(method_name, auth_payload['protocol']) self.assertEqual(method_name, auth_payload['protocol'])
def test_mapped_with_remote_user(self): def test_mapped_with_remote_user(self):
@ -186,11 +182,10 @@ class TestMapped(unit.TestCase):
'authenticate', 'authenticate',
return_value=None) as authenticate: return_value=None) as authenticate:
auth_info = auth.core.AuthInfo.create(auth_data) auth_info = auth.core.AuthInfo.create(auth_data)
request = self.make_request(environ={'REMOTE_USER': 'foo@idp.com'}) with self.make_request(environ={'REMOTE_USER': 'foo@idp.com'}):
self.api.authenticate(request, auth_info, auth_context) authentication.authenticate(auth_info, auth_context)
# make sure Mapped plugin got invoked with the correct payload # make sure Mapped plugin got invoked with the correct payload
((context, auth_payload), ((auth_payload,), kwargs) = authenticate.call_args
kwargs) = authenticate.call_args
self.assertEqual(method_name, auth_payload['protocol']) self.assertEqual(method_name, auth_payload['protocol'])
@mock.patch('keystone.auth.plugins.mapped.PROVIDERS') @mock.patch('keystone.auth.plugins.mapped.PROVIDERS')
@ -203,15 +198,18 @@ class TestMapped(unit.TestCase):
mock_providers.role_api = mock.Mock() mock_providers.role_api = mock.Mock()
test_mapped = mapped.Mapped() test_mapped = mapped.Mapped()
request = self.make_request()
auth_payload = {'identity_provider': 'test_provider'} auth_payload = {'identity_provider': 'test_provider'}
self.assertRaises(exception.ValidationError, test_mapped.authenticate, with self.make_request():
request, auth_payload) self.assertRaises(
exception.ValidationError, test_mapped.authenticate,
auth_payload)
auth_payload = {'protocol': 'saml2'} auth_payload = {'protocol': 'saml2'}
self.assertRaises(exception.ValidationError, test_mapped.authenticate, with self.make_request():
request, auth_payload) self.assertRaises(
exception.ValidationError, test_mapped.authenticate,
auth_payload)
def test_supporting_multiple_methods(self): def test_supporting_multiple_methods(self):
method_names = ('saml2', 'openid', 'x509', 'mapped') method_names = ('saml2', 'openid', 'x509', 'mapped')

View File

@ -765,11 +765,11 @@ class BaseLDAPIdentity(LDAPTestSetup, IdentityTests, AssignmentTests,
driver.user.LDAP_USER = None driver.user.LDAP_USER = None
driver.user.LDAP_PASSWORD = None driver.user.LDAP_PASSWORD = None
self.assertRaises(AssertionError, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=user['id'], user_id=user['id'],
password=None) password=None)
@mock.patch.object(versionutils, 'report_deprecated_feature') @mock.patch.object(versionutils, 'report_deprecated_feature')
def test_user_crud(self, mock_deprecator): def test_user_crud(self, mock_deprecator):
@ -1988,10 +1988,10 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity, unit.TestCase):
driver = PROVIDERS.identity_api._select_identity_driver( driver = PROVIDERS.identity_api._select_identity_driver(
CONF.identity.default_domain_id) CONF.identity.default_domain_id)
driver.user.enabled_emulation_dn = 'cn=test,dc=test' driver.user.enabled_emulation_dn = 'cn=test,dc=test'
PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), PROVIDERS.identity_api.authenticate(
user_id=self.user_foo['id'], user_id=self.user_foo['id'],
password=self.user_foo['password']) password=self.user_foo['password'])
def test_user_enable_attribute_mask(self): def test_user_enable_attribute_mask(self):
self.skip_test_overrides( self.skip_test_overrides(
@ -2334,10 +2334,10 @@ class BaseMultiLDAPandSQLIdentity(object):
for user_num in range(self.domain_count): for user_num in range(self.domain_count):
user = 'user%s' % user_num user = 'user%s' % user_num
PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), PROVIDERS.identity_api.authenticate(
user_id=users[user]['id'], user_id=users[user]['id'],
password=users[user]['password']) password=users[user]['password'])
class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides,

View File

@ -176,10 +176,10 @@ class LdapPoolCommonTestMixin(object):
# authenticate so that connection is added to pool before password # authenticate so that connection is added to pool before password
# change # change
user_ref = PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_ref = PROVIDERS.identity_api.authenticate(
user_id=self.user_sna['id'], user_id=self.user_sna['id'],
password=self.user_sna['password']) password=self.user_sna['password'])
self.user_sna.pop('password') self.user_sna.pop('password')
self.user_sna['enabled'] = True self.user_sna['enabled'] = True
@ -191,10 +191,10 @@ class LdapPoolCommonTestMixin(object):
# now authenticate again to make sure new password works with # now authenticate again to make sure new password works with
# connection pool # connection pool
user_ref2 = PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_ref2 = PROVIDERS.identity_api.authenticate(
user_id=self.user_sna['id'], user_id=self.user_sna['id'],
password=new_password) password=new_password)
user_ref.pop('password') user_ref.pop('password')
self.assertUserDictEqual(user_ref, user_ref2) self.assertUserDictEqual(user_ref, user_ref2)
@ -202,11 +202,11 @@ class LdapPoolCommonTestMixin(object):
# Authentication with old password would not work here as there # Authentication with old password would not work here as there
# is only one connection in pool which get bind again with updated # is only one connection in pool which get bind again with updated
# password..so no old bind is maintained in this case. # password..so no old bind is maintained in this case.
self.assertRaises(AssertionError, with self.make_request():
PROVIDERS.identity_api.authenticate, self.assertRaises(AssertionError,
self.make_request(), PROVIDERS.identity_api.authenticate,
user_id=self.user_sna['id'], user_id=self.user_sna['id'],
password=old_password) password=old_password)
class LDAPIdentity(LdapPoolCommonTestMixin, class LDAPIdentity(LdapPoolCommonTestMixin,

View File

@ -150,10 +150,10 @@ class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase):
self.assertEqual(system_roles[0]['id'], admin_role['id']) self.assertEqual(system_roles[0]['id'], admin_role['id'])
# NOTE(morganfainberg): Pass an empty context, it isn't used by # NOTE(morganfainberg): Pass an empty context, it isn't used by
# `authenticate` method. # `authenticate` method.
PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), PROVIDERS.identity_api.authenticate(
user['id'], user['id'],
bootstrap.password) bootstrap.password)
if bootstrap.region_id: if bootstrap.region_id:
region = PROVIDERS.catalog_api.get_region(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) self._do_test_bootstrap(self.bootstrap)
# Sanity check that the original password works again. # Sanity check that the original password works again.
PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), PROVIDERS.identity_api.authenticate(
user_id, user_id,
self.bootstrap.password) self.bootstrap.password)
class CliBootStrapTestCaseWithEnvironment(CliBootStrapTestCase): class CliBootStrapTestCaseWithEnvironment(CliBootStrapTestCase):

View File

@ -109,10 +109,10 @@ class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin,
CONF.identity.default_domain_id, CONF.identity.default_domain_id,
password=password) password=password)
PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), PROVIDERS.identity_api.authenticate(
user_id=user['id'], user_id=user['id'],
password=password) password=password)
return PROVIDERS.identity_api.get_user(user['id']) 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 # successfully which is not desired if password change is frequent
# use case in a deployment. # use case in a deployment.
# This can happen in multiple concurrent connections case only. # This can happen in multiple concurrent connections case only.
user_ref = PROVIDERS.identity_api.authenticate( with self.make_request():
self.make_request(), user_id=user['id'], password=old_password) user_ref = PROVIDERS.identity_api.authenticate(
user_id=user['id'], password=old_password)
self.assertDictEqual(user, user_ref) self.assertDictEqual(user, user_ref)

View File

@ -21,7 +21,6 @@ from six.moves import http_client
from testtools import matchers from testtools import matchers
import webtest import webtest
from keystone import auth
from keystone.common import authorization from keystone.common import authorization
from keystone.common import cache from keystone.common import cache
from keystone.common import provider_api from keystone.common import provider_api
@ -1215,18 +1214,6 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase,
for attribute in attributes: for attribute in attributes:
self.assertIsNotNone(entity.get(attribute)) 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): def build_external_auth_environ(self, remote_user, remote_domain=None):
environment = {'REMOTE_USER': remote_user, 'AUTH_TYPE': 'Negotiate'} environment = {'REMOTE_USER': remote_user, 'AUTH_TYPE': 'Negotiate'}
if remote_domain: if remote_domain:

View File

@ -19,6 +19,7 @@ from testtools import matchers
import uuid import uuid
import fixtures import fixtures
import flask
from lxml import etree from lxml import etree
import mock import mock
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
@ -32,12 +33,12 @@ xmldsig = importutils.try_import("saml2.xmldsig")
if not xmldsig: if not xmldsig:
xmldsig = importutils.try_import("xmldsig") xmldsig = importutils.try_import("xmldsig")
from keystone.auth import controllers as auth_controllers from keystone.api._shared import authentication
from keystone.common import controller from keystone.api import auth as auth_api
from keystone.common import provider_api from keystone.common import provider_api
from keystone.common import render_token
import keystone.conf import keystone.conf
from keystone import exception from keystone import exception
from keystone.federation import controllers as federation_controllers
from keystone.federation import idp as keystone_idp from keystone.federation import idp as keystone_idp
from keystone.models import token_model from keystone.models import token_model
from keystone import notifications from keystone import notifications
@ -149,13 +150,13 @@ class FederatedSetupMixin(object):
idp=None, idp=None,
assertion='EMPLOYEE_ASSERTION', assertion='EMPLOYEE_ASSERTION',
environment=None): environment=None):
api = federation_controllers.Auth()
environment = environment or {} environment = environment or {}
environment.update(getattr(mapping_fixtures, assertion)) environment.update(getattr(mapping_fixtures, assertion))
request = self.make_request(environ=environment) with self.make_request(environ=environment):
if idp is None: if idp is None:
idp = self.IDP idp = self.IDP
r = api.federated_authentication(request, idp, self.PROTOCOL) r = authentication.federated_authenticate_for_token(
protocol_id=self.PROTOCOL, identity_provider=idp)
return r return r
def idp_ref(self, id=None): 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) assertion = getattr(mapping_fixtures, variant)
request.context_dict['environment'].update(assertion) flask.request.environ.update(assertion)
def load_federation_sample_data(self): def load_federation_sample_data(self):
"""Inject additional data.""" """Inject additional data."""
@ -759,60 +760,65 @@ class FederatedSetupMixin(object):
PROVIDERS.federation_api.create_protocol( PROVIDERS.federation_api.create_protocol(
self.idp_with_remote['id'], self.proto_saml['id'], self.proto_saml self.idp_with_remote['id'], self.proto_saml['id'], self.proto_saml
) )
# Generate fake tokens
request = self.make_request()
self.tokens = {} with self.make_request():
VARIANTS = ('EMPLOYEE_ASSERTION', 'CUSTOMER_ASSERTION', self.tokens = {}
'ADMIN_ASSERTION') VARIANTS = ('EMPLOYEE_ASSERTION', 'CUSTOMER_ASSERTION',
api = auth_controllers.Auth() 'ADMIN_ASSERTION')
for variant in VARIANTS: for variant in VARIANTS:
self._inject_assertion(request, variant) self._inject_assertion(variant)
r = api.authenticate_for_token(request, self.UNSCOPED_V3_SAML2_REQ) r = authentication.authenticate_for_token(
self.tokens[variant] = r.headers.get('X-Subject-Token') self.UNSCOPED_V3_SAML2_REQ)
self.tokens[variant] = r.id
self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN = self._scope_request( self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN = (
uuid.uuid4().hex, 'project', self.proj_customers['id']) self._scope_request(
uuid.uuid4().hex, 'project', self.proj_customers['id']))
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE = self._scope_request( self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE = (
self.tokens['EMPLOYEE_ASSERTION'], 'project', self._scope_request(
self.proj_employees['id']) self.tokens['EMPLOYEE_ASSERTION'], 'project',
self.proj_employees['id']))
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN = self._scope_request( self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN = self._scope_request(
self.tokens['ADMIN_ASSERTION'], 'project', self.tokens['ADMIN_ASSERTION'], 'project',
self.proj_employees['id']) self.proj_employees['id'])
self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN = self._scope_request( self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN = self._scope_request(
self.tokens['ADMIN_ASSERTION'], 'project', self.tokens['ADMIN_ASSERTION'], 'project',
self.proj_customers['id']) self.proj_customers['id'])
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER = self._scope_request( self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER = (
self.tokens['CUSTOMER_ASSERTION'], 'project', self._scope_request(
self.proj_employees['id']) self.tokens['CUSTOMER_ASSERTION'], 'project',
self.proj_employees['id']))
self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER = self._scope_request( self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER = (
self.tokens['CUSTOMER_ASSERTION'], 'project', self._scope_request(
self.project_inherited['id']) self.tokens['CUSTOMER_ASSERTION'], 'project',
self.project_inherited['id']))
self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER = self._scope_request( self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER = self._scope_request(
self.tokens['CUSTOMER_ASSERTION'], 'domain', self.domainA['id']) self.tokens['CUSTOMER_ASSERTION'], 'domain',
self.domainA['id'])
self.TOKEN_SCOPE_DOMAIN_B_FROM_CUSTOMER = self._scope_request( self.TOKEN_SCOPE_DOMAIN_B_FROM_CUSTOMER = self._scope_request(
self.tokens['CUSTOMER_ASSERTION'], 'domain', self.tokens['CUSTOMER_ASSERTION'], 'domain',
self.domainB['id']) self.domainB['id'])
self.TOKEN_SCOPE_DOMAIN_D_FROM_CUSTOMER = self._scope_request( self.TOKEN_SCOPE_DOMAIN_D_FROM_CUSTOMER = self._scope_request(
self.tokens['CUSTOMER_ASSERTION'], 'domain', self.domainD['id']) self.tokens['CUSTOMER_ASSERTION'], 'domain',
self.domainD['id'])
self.TOKEN_SCOPE_DOMAIN_A_FROM_ADMIN = self._scope_request( self.TOKEN_SCOPE_DOMAIN_A_FROM_ADMIN = self._scope_request(
self.tokens['ADMIN_ASSERTION'], 'domain', self.domainA['id']) self.tokens['ADMIN_ASSERTION'], 'domain', self.domainA['id'])
self.TOKEN_SCOPE_DOMAIN_B_FROM_ADMIN = self._scope_request( self.TOKEN_SCOPE_DOMAIN_B_FROM_ADMIN = self._scope_request(
self.tokens['ADMIN_ASSERTION'], 'domain', self.domainB['id']) self.tokens['ADMIN_ASSERTION'], 'domain', self.domainB['id'])
self.TOKEN_SCOPE_DOMAIN_C_FROM_ADMIN = self._scope_request( self.TOKEN_SCOPE_DOMAIN_C_FROM_ADMIN = self._scope_request(
self.tokens['ADMIN_ASSERTION'], 'domain', self.tokens['ADMIN_ASSERTION'], 'domain',
self.domainC['id']) self.domainC['id'])
class FederatedIdentityProviderTests(test_v3.RestfulTestCase): class FederatedIdentityProviderTests(test_v3.RestfulTestCase):
@ -1866,7 +1872,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
super(FederatedTokenTests, self).setUp() super(FederatedTokenTests, self).setUp()
self._notifications = [] 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): identity_provider, protocol, token_id, outcome):
note = { note = {
'action': action, 'action': action,
@ -1902,12 +1908,12 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
def test_issue_unscoped_token(self): def test_issue_unscoped_token(self):
r = self._issue_unscoped_token() r = self._issue_unscoped_token()
self.assertIsNotNone(r.headers.get('X-Subject-Token')) token_resp = render_token.render_token_response_from_model(r)['token']
self.assertValidMappedUser(r.json['token']) self.assertValidMappedUser(token_resp)
def test_issue_the_same_unscoped_token_with_user_deleted(self): def test_issue_the_same_unscoped_token_with_user_deleted(self):
r = self._issue_unscoped_token() r = self._issue_unscoped_token()
token = r.json['token'] token = render_token.render_token_response_from_model(r)['token']
user1 = token['user'] user1 = token['user']
user_id1 = user1.pop('id') user_id1 = user1.pop('id')
@ -1916,7 +1922,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
PROVIDERS.identity_api.delete_user(user_id1) PROVIDERS.identity_api.delete_user(user_id1)
r = self._issue_unscoped_token() r = self._issue_unscoped_token()
token = r.json['token'] token = render_token.render_token_response_from_model(r)['token']
user2 = token['user'] user2 = token['user']
user_id2 = user2.pop('id') 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): def test_issue_unscoped_token_group_names_in_mapping(self):
r = self._issue_unscoped_token(assertion='ANOTHER_CUSTOMER_ASSERTION') r = self._issue_unscoped_token(assertion='ANOTHER_CUSTOMER_ASSERTION')
ref_groups = set([self.group_customers['id'], self.group_admins['id']]) ref_groups = set([self.group_customers['id'], self.group_admins['id']])
token_resp = r.json_body token_groups = r.federated_groups
token_groups = token_resp['token']['user']['OS-FEDERATION']['groups']
token_groups = set([group['id'] for group in token_groups]) token_groups = set([group['id'] for group in token_groups])
self.assertEqual(ref_groups, token_groups) self.assertEqual(ref_groups, token_groups)
def test_issue_unscoped_tokens_nonexisting_group(self): def test_issue_unscoped_tokens_nonexisting_group(self):
r = self._issue_unscoped_token(assertion='ANOTHER_TESTER_ASSERTION') self._issue_unscoped_token(assertion='ANOTHER_TESTER_ASSERTION')
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
def test_issue_unscoped_token_with_remote_no_attribute(self): def test_issue_unscoped_token_with_remote_no_attribute(self):
r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
environment={ environment={
self.REMOTE_ID_ATTR: self.REMOTE_ID_ATTR:
self.REMOTE_IDS[0] self.REMOTE_IDS[0]
}) })
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
def test_issue_unscoped_token_with_remote(self): def test_issue_unscoped_token_with_remote(self):
self.config_fixture.config(group='federation', self.config_fixture.config(group='federation',
remote_id_attribute=self.REMOTE_ID_ATTR) remote_id_attribute=self.REMOTE_ID_ATTR)
r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
environment={ environment={
self.REMOTE_ID_ATTR: self.REMOTE_ID_ATTR:
self.REMOTE_IDS[0] self.REMOTE_IDS[0]
}) })
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
def test_issue_unscoped_token_with_saml2_remote(self): def test_issue_unscoped_token_with_saml2_remote(self):
self.config_fixture.config(group='saml2', self.config_fixture.config(group='saml2',
remote_id_attribute=self.REMOTE_ID_ATTR) remote_id_attribute=self.REMOTE_ID_ATTR)
r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
environment={ environment={
self.REMOTE_ID_ATTR: self.REMOTE_ID_ATTR:
self.REMOTE_IDS[0] self.REMOTE_IDS[0]
}) })
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
def test_issue_unscoped_token_with_remote_different(self): def test_issue_unscoped_token_with_remote_different(self):
self.config_fixture.config(group='federation', self.config_fixture.config(group='federation',
@ -2001,12 +2002,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
remote_id_attribute=self.REMOTE_ID_ATTR) remote_id_attribute=self.REMOTE_ID_ATTR)
self.config_fixture.config(group='federation', self.config_fixture.config(group='federation',
remote_id_attribute=uuid.uuid4().hex) remote_id_attribute=uuid.uuid4().hex)
r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
environment={ environment={
self.REMOTE_ID_ATTR: self.REMOTE_ID_ATTR:
self.REMOTE_IDS[0] self.REMOTE_IDS[0]
}) })
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
def test_issue_unscoped_token_with_remote_unavailable(self): def test_issue_unscoped_token_with_remote_unavailable(self):
self.config_fixture.config(group='federation', 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): 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 # make sure that REMOTE_USER set as the empty string won't interfere
r = self._issue_unscoped_token(environment={'REMOTE_USER': ''}) self._issue_unscoped_token(environment={'REMOTE_USER': ''})
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
def test_issue_unscoped_token_no_groups(self): def test_issue_unscoped_token_no_groups(self):
r = self._issue_unscoped_token(assertion='USER_NO_GROUPS_ASSERTION') r = self._issue_unscoped_token(assertion='USER_NO_GROUPS_ASSERTION')
self.assertIsNotNone(r.headers.get('X-Subject-Token')) token_groups = r.federated_groups
token_resp = r.json_body
token_groups = token_resp['token']['user']['OS-FEDERATION']['groups']
self.assertEqual(0, len(token_groups)) self.assertEqual(0, len(token_groups))
def test_issue_scoped_token_no_groups(self): def test_issue_scoped_token_no_groups(self):
@ -2037,11 +2034,9 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
""" """
# issue unscoped token with no groups # issue unscoped token with no groups
r = self._issue_unscoped_token(assertion='USER_NO_GROUPS_ASSERTION') r = self._issue_unscoped_token(assertion='USER_NO_GROUPS_ASSERTION')
self.assertIsNotNone(r.headers.get('X-Subject-Token')) token_groups = r.federated_groups
token_resp = r.json_body
token_groups = token_resp['token']['user']['OS-FEDERATION']['groups']
self.assertEqual(0, len(token_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 # let admin get roles in a project
self.proj_employees self.proj_employees
@ -2068,16 +2063,14 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
non string objects and return token id in the HTTP header. non string objects and return token id in the HTTP header.
""" """
api = auth_controllers.Auth()
environ = { environ = {
'malformed_object': object(), 'malformed_object': object(),
'another_bad_idea': tuple(range(10)), 'another_bad_idea': tuple(range(10)),
'yet_another_bad_param': dict(zip(uuid.uuid4().hex, range(32))) 'yet_another_bad_param': dict(zip(uuid.uuid4().hex, range(32)))
} }
environ.update(mapping_fixtures.EMPLOYEE_ASSERTION) environ.update(mapping_fixtures.EMPLOYEE_ASSERTION)
request = self.make_request(environ=environ) with self.make_request(environ=environ):
r = api.authenticate_for_token(request, self.UNSCOPED_V3_SAML2_REQ) authentication.authenticate_for_token(self.UNSCOPED_V3_SAML2_REQ)
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
def test_scope_to_project_once_notify(self): def test_scope_to_project_once_notify(self):
r = self.v3_create_token( r = self.v3_create_token(
@ -2208,12 +2201,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
expected_status=http_client.NOT_FOUND) expected_status=http_client.NOT_FOUND)
def test_issue_token_from_rules_without_user(self): def test_issue_token_from_rules_without_user(self):
api = auth_controllers.Auth()
environ = copy.deepcopy(mapping_fixtures.BAD_TESTER_ASSERTION) environ = copy.deepcopy(mapping_fixtures.BAD_TESTER_ASSERTION)
request = self.make_request(environ=environ) with self.make_request(environ=environ):
self.assertRaises(exception.Unauthorized, self.assertRaises(exception.Unauthorized,
api.authenticate_for_token, authentication.authenticate_for_token,
request, self.UNSCOPED_V3_SAML2_REQ) self.UNSCOPED_V3_SAML2_REQ)
def test_issue_token_with_nonexistent_group(self): def test_issue_token_with_nonexistent_group(self):
"""Inject assertion that matches rule issuing bad group id. """Inject assertion that matches rule issuing bad group id.
@ -2356,11 +2348,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
""" """
r = self._issue_unscoped_token() 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. # 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) 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) r = self.get('/auth/projects', token=employee_unscoped_token_id)
projects = r.result['projects'] projects = r.result['projects']
random_project = random.randint(0, len(projects) - 1) 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) PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
r = self._issue_unscoped_token(assertion='TESTER_ASSERTION') r = self._issue_unscoped_token(assertion='TESTER_ASSERTION')
token_id = r.headers.get('X-Subject-Token')
# delete group # delete group
PROVIDERS.identity_api.delete_group(group['id']) PROVIDERS.identity_api.delete_group(group['id'])
# scope token to project_all, expect HTTP 500 # scope token to project_all, expect HTTP 500
scoped_token = self._scope_request( scoped_token = self._scope_request(
token_id, 'project', r.id, 'project',
self.project_all['id']) self.project_all['id'])
self.v3_create_token( self.v3_create_token(
@ -2498,7 +2489,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
} }
PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules) PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') 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(1, len(assigned_group_ids))
self.assertEqual(group['id'], assigned_group_ids[0]['id']) 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) PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') 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)) self.assertEqual(len(group_ids), len(assigned_group_ids))
for group in assigned_group_ids: for group in assigned_group_ids:
self.assertIn(group['id'], 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) PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') 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)) self.assertEqual(len(group_ids), len(assigned_group_ids))
for group in assigned_group_ids: for group in assigned_group_ids:
self.assertIn(group['id'], 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) PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') 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) self.assertEqual(len(assigned_groups), 0)
def test_not_setting_whitelist_accepts_all_values(self): 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) PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') 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)) self.assertEqual(len(group_ids), len(assigned_group_ids))
for group in assigned_group_ids: for group in assigned_group_ids:
self.assertIn(group['id'], group_ids) self.assertIn(group['id'], group_ids)
@ -2791,8 +2782,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
""" """
self.config_fixture.config(group='federation', self.config_fixture.config(group='federation',
assertion_prefix=self.ASSERTION_PREFIX) assertion_prefix=self.ASSERTION_PREFIX)
r = self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION_PREFIXED') self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION_PREFIXED')
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
def test_assertion_prefix_parameter_expect_fail(self): def test_assertion_prefix_parameter_expect_fail(self):
"""Test parameters filtering based on the prefix. """Test parameters filtering based on the prefix.
@ -2804,8 +2794,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
Expect server to raise exception.Unathorized exception. Expect server to raise exception.Unathorized exception.
""" """
r = self._issue_unscoped_token() self._issue_unscoped_token()
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
self.config_fixture.config(group='federation', self.config_fixture.config(group='federation',
assertion_prefix='UserName') assertion_prefix='UserName')
@ -2814,23 +2803,24 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
def test_unscoped_token_has_user_domain(self): def test_unscoped_token_has_user_domain(self):
r = self._issue_unscoped_token() 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): def test_scoped_token_has_user_domain(self):
r = self.v3_create_token( r = self.v3_create_token(
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE) 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): def test_issue_unscoped_token_for_local_user(self):
r = self._issue_unscoped_token(assertion='LOCAL_USER_ASSERTION') r = self._issue_unscoped_token(assertion='LOCAL_USER_ASSERTION')
token_resp = r.json_body['token'] self.assertListEqual(['saml2'], r.methods)
self.assertListEqual(['saml2'], token_resp['methods']) self.assertEqual(self.user['id'], r.user_id)
self.assertEqual(self.user['id'], token_resp['user']['id']) self.assertEqual(self.user['name'], r.user['name'])
self.assertEqual(self.user['name'], token_resp['user']['name']) self.assertEqual(self.domain['id'], r.user_domain['id'])
self.assertEqual(self.domain['id'], token_resp['user']['domain']['id'])
# Make sure the token is not scoped # Make sure the token is not scoped
self.assertNotIn('project', token_resp) self.assertIsNone(r.domain_id)
self.assertNotIn('domain', token_resp) self.assertIsNone(r.project_id)
self.assertTrue(r.unscoped)
def test_issue_token_for_local_user_user_not_found(self): def test_issue_token_for_local_user_user_not_found(self):
self.assertRaises(exception.Unauthorized, self.assertRaises(exception.Unauthorized,
@ -2839,11 +2829,10 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
def test_user_name_and_id_in_federation_token(self): def test_user_name_and_id_in_federation_token(self):
r = self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION') r = self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION')
token = r.json_body['token']
self.assertEqual( self.assertEqual(
mapping_fixtures.EMPLOYEE_ASSERTION['UserName'], mapping_fixtures.EMPLOYEE_ASSERTION['UserName'],
token['user']['name']) r.user['name'])
self.assertNotEqual(token['user']['name'], token['user']['id']) self.assertNotEqual(r.user['name'], r.user_id)
r = self.v3_create_token( r = self.v3_create_token(
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE) self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE)
token = r.json_body['token'] token = r.json_body['token']
@ -2878,18 +2867,18 @@ class FernetFederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
def test_federated_unscoped_token(self): def test_federated_unscoped_token(self):
resp = self._issue_unscoped_token() resp = self._issue_unscoped_token()
self.assertEqual(204, len(resp.headers['X-Subject-Token'])) self.assertValidMappedUser(
self.assertValidMappedUser(resp.json_body['token']) render_token.render_token_response_from_model(resp)['token'])
def test_federated_unscoped_token_with_multiple_groups(self): def test_federated_unscoped_token_with_multiple_groups(self):
assertion = 'ANOTHER_CUSTOMER_ASSERTION' assertion = 'ANOTHER_CUSTOMER_ASSERTION'
resp = self._issue_unscoped_token(assertion=assertion) resp = self._issue_unscoped_token(assertion=assertion)
self.assertEqual(226, len(resp.headers['X-Subject-Token'])) self.assertValidMappedUser(
self.assertValidMappedUser(resp.json_body['token']) render_token.render_token_response_from_model(resp)['token'])
def test_validate_federated_unscoped_token(self): def test_validate_federated_unscoped_token(self):
resp = self._issue_unscoped_token() 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 # assert that the token we received is valid
self.get('/auth/tokens/', headers={'X-Subject-Token': unscoped_token}) 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() resp = self._issue_unscoped_token()
self.assertValidMappedUser(resp.json_body['token']) self.assertValidMappedUser(
unscoped_token = resp.headers.get('X-Subject-Token') render_token.render_token_response_from_model(resp)['token'])
unscoped_token = resp.id
resp = self.get('/auth/projects', token=unscoped_token) resp = self.get('/auth/projects', token=unscoped_token)
projects = resp.result['projects'] projects = resp.result['projects']
random_project = random.randint(0, len(projects) - 1) random_project = random.randint(0, len(projects) - 1)
@ -2941,11 +2931,11 @@ class FederatedTokenTestsMethodToken(FederatedTokenTests):
""" """
r = self._issue_unscoped_token() 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. # 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) 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) r = self.get('/auth/projects', token=employee_unscoped_token_id)
projects = r.result['projects'] projects = r.result['projects']
random_project = random.randint(0, len(projects) - 1) random_project = random.randint(0, len(projects) - 1)
@ -2979,11 +2969,11 @@ class FederatedUserTests(test_v3.RestfulTestCase, FederatedSetupMixin):
def test_user_id_persistense(self): def test_user_id_persistense(self):
"""Ensure user_id is persistend for multiple federated authn calls.""" """Ensure user_id is persistend for multiple federated authn calls."""
r = self._issue_unscoped_token() 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)) self.assertNotEmpty(PROVIDERS.identity_api.get_user(user_id))
r = self._issue_unscoped_token() 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.assertNotEmpty(PROVIDERS.identity_api.get_user(user_id2))
self.assertEqual(user_id, 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 # Authenticate to create a new federated_user entry with a foreign
# key pointing to the protocol # key pointing to the protocol
r = self._issue_unscoped_token() 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)) self.assertNotEmpty(PROVIDERS.identity_api.get_user(user_id))
# Now we should be able to delete the protocol # Now we should be able to delete the protocol
@ -3280,10 +3270,10 @@ class FederatedUserTests(test_v3.RestfulTestCase, FederatedSetupMixin):
def _authenticate_via_saml(self): def _authenticate_via_saml(self):
r = self._issue_unscoped_token() r = self._issue_unscoped_token()
unscoped_token = r.headers['X-Subject-Token'] unscoped_token = r.id
token_resp = r.json_body['token'] token_resp = render_token.render_token_response_from_model(r)['token']
self.assertValidMappedUser(token_resp) self.assertValidMappedUser(token_resp)
return token_resp['user']['id'], unscoped_token return r.user_id, unscoped_token
class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin): class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
@ -3351,8 +3341,9 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
self.assertNotIn(project['name'], self.expected_results) self.assertNotIn(project['name'], self.expected_results)
response = self._issue_unscoped_token() response = self._issue_unscoped_token()
self.assertValidMappedUser(response.json_body['token']) self.assertValidMappedUser(
unscoped_token = response.headers.get('X-Subject-Token') render_token.render_token_response_from_model(response)['token'])
unscoped_token = response.id
response = self.get('/auth/projects', token=unscoped_token) response = self.get('/auth/projects', token=unscoped_token)
projects = response.json_body['projects'] projects = response.json_body['projects']
for project in projects: for project in projects:
@ -3364,8 +3355,9 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
def test_shadow_mapping_create_projects_role_assignments(self): def test_shadow_mapping_create_projects_role_assignments(self):
response = self._issue_unscoped_token() response = self._issue_unscoped_token()
self.assertValidMappedUser(response.json_body['token']) self.assertValidMappedUser(
unscoped_token = response.headers.get('X-Subject-Token') render_token.render_token_response_from_model(response)['token'])
unscoped_token = response.id
response = self.get('/auth/projects', token=unscoped_token) response = self.get('/auth/projects', token=unscoped_token)
projects = response.json_body['projects'] projects = response.json_body['projects']
for project in 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): def test_shadow_mapping_creates_project_in_identity_provider_domain(self):
response = self._issue_unscoped_token() response = self._issue_unscoped_token()
self.assertValidMappedUser(response.json_body['token']) self.assertValidMappedUser(
unscoped_token = response.headers.get('X-Subject-Token') render_token.render_token_response_from_model(response)['token'])
unscoped_token = response.id
response = self.get('/auth/projects', token=unscoped_token) response = self.get('/auth/projects', token=unscoped_token)
projects = response.json_body['projects'] projects = response.json_body['projects']
for project in projects: for project in projects:
@ -3401,12 +3394,13 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
def test_shadow_mapping_is_idempotent(self): def test_shadow_mapping_is_idempotent(self):
"""Test that projects remain idempotent for every federated auth.""" """Test that projects remain idempotent for every federated auth."""
response = self._issue_unscoped_token() response = self._issue_unscoped_token()
self.assertValidMappedUser(response.json_body['token']) self.assertValidMappedUser(
unscoped_token = response.headers.get('X-Subject-Token') render_token.render_token_response_from_model(response)['token'])
unscoped_token = response.id
response = self.get('/auth/projects', token=unscoped_token) response = self.get('/auth/projects', token=unscoped_token)
project_ids = [p['id'] for p in response.json_body['projects']] project_ids = [p['id'] for p in response.json_body['projects']]
response = self._issue_unscoped_token() 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) response = self.get('/auth/projects', token=unscoped_token)
projects = response.json_body['projects'] projects = response.json_body['projects']
for project in 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) PROVIDERS.role_api.create_role(member_role_ref['id'], member_role_ref)
response = self._issue_unscoped_token() response = self._issue_unscoped_token()
user_id = response.json_body['token']['user']['id'] user_id = response.user_id
unscoped_token = response.headers.get('X-Subject-Token') unscoped_token = response.id
response = self.get('/auth/projects', token=unscoped_token) response = self.get('/auth/projects', token=unscoped_token)
projects = response.json_body['projects'] projects = response.json_body['projects']
staging_project = PROVIDERS.resource_api.get_project_by_name( staging_project = PROVIDERS.resource_api.get_project_by_name(
@ -3500,7 +3494,7 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
) )
response = self._issue_unscoped_token() response = self._issue_unscoped_token()
# user_id = response.json_body['token']['user']['id'] # 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) response = self.get('/auth/projects', token=unscoped_token)
projects = response.json_body['projects'] projects = response.json_body['projects']
self.expected_results = { self.expected_results = {
@ -3532,8 +3526,8 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
# to them. This test verifies that this is no longer true. # to them. This test verifies that this is no longer true.
# Authenticate once to create the projects # Authenticate once to create the projects
response = self._issue_unscoped_token() response = self._issue_unscoped_token()
self.assertValidMappedUser(response.json_body['token']) self.assertValidMappedUser(
unscoped_token = response.headers.get('X-Subject-Token') render_token.render_token_response_from_model(response)['token'])
# Assign admin role to newly-created project to another user # Assign admin role to newly-created project to another user
staging_project = PROVIDERS.resource_api.get_project_by_name( 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 # Authenticate again with the federated user and verify roles
response = self._issue_unscoped_token() response = self._issue_unscoped_token()
self.assertValidMappedUser(response.json_body['token']) self.assertValidMappedUser(
unscoped_token = response.headers.get('X-Subject-Token') render_token.render_token_response_from_model(response)['token'])
unscoped_token = response.id
scope = self._scope_request( scope = self._scope_request(
unscoped_token, 'project', staging_project['id'] unscoped_token, 'project', staging_project['id']
) )
@ -4602,10 +4597,6 @@ class WebSSOTests(FederatedTokenTests):
ORIGIN = urllib.parse.quote_plus(TRUSTED_DASHBOARD) ORIGIN = urllib.parse.quote_plus(TRUSTED_DASHBOARD)
PROTOCOL_REMOTE_ID_ATTR = uuid.uuid4().hex PROTOCOL_REMOTE_ID_ATTR = uuid.uuid4().hex
def setUp(self):
super(WebSSOTests, self).setUp()
self.api = federation_controllers.Auth()
def config_overrides(self): def config_overrides(self):
super(WebSSOTests, self).config_overrides() super(WebSSOTests, self).config_overrides()
self.config_fixture.config( self.config_fixture.config(
@ -4616,34 +4607,39 @@ class WebSSOTests(FederatedTokenTests):
def test_render_callback_template(self): def test_render_callback_template(self):
token_id = uuid.uuid4().hex 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 # The expected value in the assertions bellow need to be 'str' in
# Python 2 and 'bytes' in Python 3 # Python 2 and 'bytes' in Python 3
self.assertIn(token_id.encode('utf-8'), resp.body) self.assertIn(token_id.encode('utf-8'), resp.data)
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(self): def test_federated_sso_auth(self):
environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0], environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0],
'QUERY_STRING': 'origin=%s' % self.ORIGIN} 'QUERY_STRING': 'origin=%s' % self.ORIGIN}
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
request = self.make_request(environ=environment) with self.make_request(environ=environment):
resp = self.api.federated_sso_auth(request, self.PROTOCOL) resp = auth_api.AuthFederationWebSSOResource._perform_auth(
# `resp.body` will be `str` in Python 2 and `bytes` in Python 3 self.PROTOCOL)
# `resp.data` will be `str` in Python 2 and `bytes` in Python 3
# which is why expected value: `self.TRUSTED_DASHBOARD` # which is why expected value: `self.TRUSTED_DASHBOARD`
# needs to be encoded # 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): def test_get_sso_origin_host_case_insensitive(self):
# test lowercase hostname in trusted_dashboard # test lowercase hostname in trusted_dashboard
environ = {'QUERY_STRING': 'origin=http://horizon.com'} environ = {'QUERY_STRING': 'origin=http://horizon.com'}
request = self.make_request(environ=environ) with self.make_request(environ=environ):
host = self.api._get_sso_origin_host(request) host = auth_api._get_sso_origin_host()
self.assertEqual("http://horizon.com", host) self.assertEqual("http://horizon.com", host)
# test uppercase hostname in trusted_dashboard # test uppercase hostname in trusted_dashboard
self.config_fixture.config(group='federation', self.config_fixture.config(
trusted_dashboard=['http://Horizon.com']) group='federation',
host = self.api._get_sso_origin_host(request) trusted_dashboard=['http://Horizon.com'])
self.assertEqual("http://horizon.com", host) host = auth_api._get_sso_origin_host()
self.assertEqual("http://horizon.com", host)
def test_federated_sso_auth_with_protocol_specific_remote_id(self): def test_federated_sso_auth_with_protocol_specific_remote_id(self):
self.config_fixture.config( self.config_fixture.config(
@ -4653,76 +4649,82 @@ class WebSSOTests(FederatedTokenTests):
environment = {self.PROTOCOL_REMOTE_ID_ATTR: self.REMOTE_IDS[0], environment = {self.PROTOCOL_REMOTE_ID_ATTR: self.REMOTE_IDS[0],
'QUERY_STRING': 'origin=%s' % self.ORIGIN} 'QUERY_STRING': 'origin=%s' % self.ORIGIN}
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
request = self.make_request(environ=environment) with self.make_request(environ=environment):
resp = self.api.federated_sso_auth(request, self.PROTOCOL) resp = auth_api.AuthFederationWebSSOResource._perform_auth(
# `resp.body` will be `str` in Python 2 and `bytes` in Python 3 self.PROTOCOL)
# `resp.data` will be `str` in Python 2 and `bytes` in Python 3
# which is why expected value: `self.TRUSTED_DASHBOARD` # which is why expected value: `self.TRUSTED_DASHBOARD`
# needs to be encoded # 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): def test_federated_sso_auth_bad_remote_id(self):
environment = {self.REMOTE_ID_ATTR: self.IDP, environment = {self.REMOTE_ID_ATTR: self.IDP,
'QUERY_STRING': 'origin=%s' % self.ORIGIN} 'QUERY_STRING': 'origin=%s' % self.ORIGIN}
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
request = self.make_request(environ=environment) with self.make_request(environ=environment):
self.assertRaises(exception.IdentityProviderNotFound, self.assertRaises(
self.api.federated_sso_auth, exception.IdentityProviderNotFound,
request, self.PROTOCOL) auth_api.AuthFederationWebSSOResource._perform_auth,
self.PROTOCOL)
def test_federated_sso_missing_query(self): def test_federated_sso_missing_query(self):
environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]} environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]}
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
request = self.make_request(environ=environment) with self.make_request(environ=environment):
self.assertRaises(exception.ValidationError, self.assertRaises(
self.api.federated_sso_auth, exception.ValidationError,
request, self.PROTOCOL) auth_api.AuthFederationWebSSOResource._perform_auth,
self.PROTOCOL)
def test_federated_sso_missing_query_bad_remote_id(self): def test_federated_sso_missing_query_bad_remote_id(self):
environment = {self.REMOTE_ID_ATTR: self.IDP} environment = {self.REMOTE_ID_ATTR: self.IDP}
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
request = self.make_request(environ=environment) with self.make_request(environ=environment):
self.assertRaises(exception.ValidationError, self.assertRaises(
self.api.federated_sso_auth, exception.ValidationError,
request, self.PROTOCOL) auth_api.AuthFederationWebSSOResource._perform_auth,
self.PROTOCOL)
def test_federated_sso_untrusted_dashboard(self): def test_federated_sso_untrusted_dashboard(self):
environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0], environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0],
'QUERY_STRING': 'origin=%s' % uuid.uuid4().hex} 'QUERY_STRING': 'origin=%s' % uuid.uuid4().hex}
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
request = self.make_request(environ=environment) with self.make_request(environ=environment):
self.assertRaises(exception.Unauthorized, self.assertRaises(
self.api.federated_sso_auth, exception.Unauthorized,
request, self.PROTOCOL) auth_api.AuthFederationWebSSOResource._perform_auth,
self.PROTOCOL)
def test_federated_sso_untrusted_dashboard_bad_remote_id(self): def test_federated_sso_untrusted_dashboard_bad_remote_id(self):
environment = {self.REMOTE_ID_ATTR: self.IDP, environment = {self.REMOTE_ID_ATTR: self.IDP,
'QUERY_STRING': 'origin=%s' % uuid.uuid4().hex} 'QUERY_STRING': 'origin=%s' % uuid.uuid4().hex}
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
request = self.make_request(environ=environment) with self.make_request(environ=environment):
self.assertRaises(exception.Unauthorized, self.assertRaises(
self.api.federated_sso_auth, exception.Unauthorized,
request, self.PROTOCOL) auth_api.AuthFederationWebSSOResource._perform_auth,
self.PROTOCOL)
def test_federated_sso_missing_remote_id(self): def test_federated_sso_missing_remote_id(self):
environment = copy.deepcopy(mapping_fixtures.EMPLOYEE_ASSERTION) environment = copy.deepcopy(mapping_fixtures.EMPLOYEE_ASSERTION)
request = self.make_request(environ=environment, with self.make_request(environ=environment,
query_string='origin=%s' % self.ORIGIN) query_string='origin=%s' % self.ORIGIN):
self.assertRaises(exception.Unauthorized, self.assertRaises(
self.api.federated_sso_auth, exception.Unauthorized,
request, self.PROTOCOL) auth_api.AuthFederationWebSSOResource._perform_auth,
self.PROTOCOL)
def test_identity_provider_specific_federated_authentication(self): def test_identity_provider_specific_federated_authentication(self):
environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]} environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]}
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION) environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
request = self.make_request(environ=environment, with self.make_request(environ=environment,
query_string='origin=%s' % self.ORIGIN) query_string='origin=%s' % self.ORIGIN):
resp = self.api.federated_idp_specific_sso_auth(request, resp = auth_api.AuthFederationWebSSOIDPsResource._perform_auth(
self.idp['id'], self.idp['id'], self.PROTOCOL)
self.PROTOCOL) # `resp.data` will be `str` in Python 2 and `bytes` in Python 3
# `resp.body` will be `str` in Python 2 and `bytes` in Python 3
# which is why the expected value: `self.TRUSTED_DASHBOARD` # which is why the expected value: `self.TRUSTED_DASHBOARD`
# needs to be encoded # 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): class K2KServiceCatalogTests(test_v3.RestfulTestCase):
@ -4779,7 +4781,7 @@ class K2KServiceCatalogTests(test_v3.RestfulTestCase):
model = token_model.TokenModel() model = token_model.TokenModel()
model.user_id = self.user_id model.user_id = self.user_id
model.methods = ['password'] model.methods = ['password']
token = controller.render_token_response_from_model(model) token = render_token.render_token_response_from_model(model)
ref = {} ref = {}
for r in (self.sp_alpha, self.sp_beta, self.sp_gamma): for r in (self.sp_alpha, self.sp_beta, self.sp_gamma):
ref.update(r) ref.update(r)
@ -4799,7 +4801,7 @@ class K2KServiceCatalogTests(test_v3.RestfulTestCase):
model = token_model.TokenModel() model = token_model.TokenModel()
model.user_id = self.user_id model.user_id = self.user_id
model.methods = ['password'] model.methods = ['password']
token = controller.render_token_response_from_model(model) token = render_token.render_token_response_from_model(model)
ref = {} ref = {}
for r in (self.sp_beta, self.sp_gamma): for r in (self.sp_beta, self.sp_gamma):
ref.update(r) ref.update(r)
@ -4819,7 +4821,7 @@ class K2KServiceCatalogTests(test_v3.RestfulTestCase):
model = token_model.TokenModel() model = token_model.TokenModel()
model.user_id = self.user_id model.user_id = self.user_id
model.methods = ['password'] 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'], self.assertNotIn('service_providers', token['token'],
message=('Expected Service Catalog not to have ' message=('Expected Service Catalog not to have '
'service_providers')) 'service_providers'))