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:
parent
8e33c78232
commit
d97832e8e8
@ -10,6 +10,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from keystone.api import auth
|
||||
from keystone.api import credentials
|
||||
from keystone.api import discovery
|
||||
from keystone.api import domains
|
||||
@ -33,6 +34,7 @@ from keystone.api import system
|
||||
from keystone.api import trusts
|
||||
|
||||
__all__ = (
|
||||
'auth',
|
||||
'discovery',
|
||||
'credentials',
|
||||
'domains',
|
||||
@ -58,6 +60,7 @@ __all__ = (
|
||||
|
||||
__apis__ = (
|
||||
discovery,
|
||||
auth,
|
||||
credentials,
|
||||
domains,
|
||||
endpoints,
|
||||
|
243
keystone/api/_shared/authentication.py
Normal file
243
keystone/api/_shared/authentication.py
Normal 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)
|
@ -70,3 +70,8 @@ os_federation_parameter_rel_func = functools.partial(
|
||||
os_inherit_resource_rel_func = functools.partial(
|
||||
json_home.build_v3_extension_resource_relation,
|
||||
extension_name='OS-INHERIT', extension_version='1.0')
|
||||
|
||||
# OS-PKI (revoked) "extension"
|
||||
os_pki_resource_rel_func = functools.partial(
|
||||
json_home.build_v3_extension_resource_relation,
|
||||
extension_name='OS-PKI', extension_version='1.0')
|
||||
|
578
keystone/api/auth.py
Normal file
578
keystone/api/auth.py
Normal 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,
|
||||
)
|
@ -23,6 +23,7 @@ from keystone.common import rbac_enforcer
|
||||
from keystone.common import utils
|
||||
from keystone.common import validation
|
||||
from keystone import exception
|
||||
from keystone import notifications
|
||||
from keystone.server import flask as ks_flask
|
||||
|
||||
|
||||
@ -65,7 +66,7 @@ class EndpointResource(ks_flask.ResourceBase):
|
||||
except exception.RegionNotFound:
|
||||
region = dict(id=endpoint['region_id'])
|
||||
PROVIDERS.catalog_api.create_region(
|
||||
region, initiator=ks_flask.build_audit_initiator())
|
||||
region, initiator=notifications.build_audit_initiator())
|
||||
return endpoint
|
||||
|
||||
def _get_endpoint(self, endpoint_id):
|
||||
|
@ -22,6 +22,7 @@ from keystone.common import rbac_enforcer
|
||||
from keystone.common import validation
|
||||
from keystone import exception
|
||||
from keystone.identity import schema
|
||||
from keystone import notifications
|
||||
from keystone.server import flask as ks_flask
|
||||
|
||||
|
||||
@ -163,7 +164,7 @@ class UserGroupCRUDResource(flask_restful.Resource):
|
||||
build_target=functools.partial(self._build_enforcement_target_attr,
|
||||
user_id, group_id))
|
||||
PROVIDERS.identity_api.add_user_to_group(
|
||||
user_id, group_id, initiator=ks_flask.build_audit_initiator())
|
||||
user_id, group_id, initiator=notifications.build_audit_initiator())
|
||||
return None, http_client.NO_CONTENT
|
||||
|
||||
def delete(self, group_id, user_id):
|
||||
@ -176,7 +177,7 @@ class UserGroupCRUDResource(flask_restful.Resource):
|
||||
build_target=functools.partial(self._build_enforcement_target_attr,
|
||||
user_id, group_id))
|
||||
PROVIDERS.identity_api.remove_user_from_group(
|
||||
user_id, group_id, initiator=ks_flask.build_audit_initiator())
|
||||
user_id, group_id, initiator=notifications.build_audit_initiator())
|
||||
return None, http_client.NO_CONTENT
|
||||
|
||||
|
||||
|
@ -14,18 +14,17 @@
|
||||
|
||||
import flask
|
||||
import flask_restful
|
||||
from oslo_log import versionutils
|
||||
from oslo_serialization import jsonutils
|
||||
from six.moves import http_client
|
||||
|
||||
from keystone.api._shared import authentication
|
||||
from keystone.api._shared import json_home_relations
|
||||
from keystone.common import authorization
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import rbac_enforcer
|
||||
from keystone.common import request
|
||||
from keystone.common import render_token
|
||||
from keystone.common import validation
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
import keystone.federation.controllers
|
||||
from keystone.federation import schema
|
||||
from keystone.federation import utils
|
||||
from keystone.server import flask as ks_flask
|
||||
@ -380,74 +379,6 @@ class ServiceProvidersResource(_ResourceBase):
|
||||
return None, http_client.NO_CONTENT
|
||||
|
||||
|
||||
class OSFederationProjectResource(flask_restful.Resource):
|
||||
@versionutils.deprecated(as_of=versionutils.deprecated.JUNO,
|
||||
what='GET /v3/OS-FEDERATION/projects',
|
||||
in_favor_of='GET /v3/auth/projects')
|
||||
def get(self):
|
||||
"""Get projects for user.
|
||||
|
||||
GET/HEAD /OS-FEDERATION/projects
|
||||
"""
|
||||
ENFORCER.enforce_call(action='identity:get_auth_projects')
|
||||
# TODO(morgan): Make this method simply call the endpoint for
|
||||
# /v3/auth/projects once auth is ported to flask.
|
||||
auth_context = flask.request.environ.get(
|
||||
authorization.AUTH_CONTEXT_ENV)
|
||||
user_id = auth_context.get('user_id')
|
||||
group_ids = auth_context.get('group_ids')
|
||||
|
||||
user_refs = []
|
||||
if user_id:
|
||||
try:
|
||||
user_refs = PROVIDERS.assignment_api.list_projects_for_user(
|
||||
user_id)
|
||||
except exception.UserNotFound: # nosec
|
||||
# federated users have an id but they don't link to anything
|
||||
pass
|
||||
group_refs = []
|
||||
if group_ids:
|
||||
group_refs = PROVIDERS.assignment_api.list_projects_for_groups(
|
||||
group_ids)
|
||||
refs = _combine_lists_uniquely(user_refs, group_refs)
|
||||
return ks_flask.ResourceBase.wrap_collection(
|
||||
refs, collection_name='projects')
|
||||
|
||||
|
||||
class OSFederationDomainResource(flask_restful.Resource):
|
||||
@versionutils.deprecated(as_of=versionutils.deprecated.JUNO,
|
||||
what='GET /v3/OS-FEDERATION/domains',
|
||||
in_favor_of='GET /v3/auth/domains')
|
||||
def get(self):
|
||||
"""Get domains for user.
|
||||
|
||||
GET/HEAD /OS-FEDERATION/domains
|
||||
"""
|
||||
ENFORCER.enforce_call(action='identity:get_auth_domains')
|
||||
# TODO(morgan): Make this method simply call the endpoint for
|
||||
# /v3/auth/domains once auth is ported to flask.
|
||||
auth_context = flask.request.environ.get(
|
||||
authorization.AUTH_CONTEXT_ENV)
|
||||
user_id = auth_context.get('user_id')
|
||||
group_ids = auth_context.get('group_ids')
|
||||
|
||||
user_refs = []
|
||||
if user_id:
|
||||
try:
|
||||
user_refs = PROVIDERS.assignment_api.list_domains_for_user(
|
||||
user_id)
|
||||
except exception.UserNotFound: # nosec
|
||||
# federated users have an ide bu they don't link to anything
|
||||
pass
|
||||
group_refs = []
|
||||
if group_ids:
|
||||
group_refs = PROVIDERS.assignment_api.list_domains_for_groups(
|
||||
group_ids)
|
||||
refs = _combine_lists_uniquely(user_refs, group_refs)
|
||||
return ks_flask.ResourceBase.wrap_collection(
|
||||
refs, collection_name='domains')
|
||||
|
||||
|
||||
class SAML2MetadataResource(flask_restful.Resource):
|
||||
@ks_flask.unenforced_api
|
||||
def get(self):
|
||||
@ -468,11 +399,6 @@ class SAML2MetadataResource(flask_restful.Resource):
|
||||
|
||||
|
||||
class OSFederationAuthResource(flask_restful.Resource):
|
||||
def _construct_webob_request(self):
|
||||
# Build a fake(ish) webob request object from the flask request state
|
||||
# to pass to the Auth Controller's authenticate_for_token. This is
|
||||
# purely transitional code.
|
||||
return request.Request(flask.request.environ)
|
||||
|
||||
@ks_flask.unenforced_api
|
||||
def get(self, idp_id, protocol_id):
|
||||
@ -493,12 +419,11 @@ class OSFederationAuthResource(flask_restful.Resource):
|
||||
return self._auth(idp_id, protocol_id)
|
||||
|
||||
def _auth(self, idp_id, protocol_id):
|
||||
"""Build and pass auth data to auth controller.
|
||||
"""Build and pass auth data to authentication code.
|
||||
|
||||
Build HTTP request body for federated authentication and inject
|
||||
it into the ``authenticate_for_token`` function.
|
||||
"""
|
||||
compat_controller = keystone.federation.controllers.Auth()
|
||||
auth = {
|
||||
'identity': {
|
||||
'methods': [protocol_id],
|
||||
@ -508,14 +433,12 @@ class OSFederationAuthResource(flask_restful.Resource):
|
||||
},
|
||||
}
|
||||
}
|
||||
# NOTE(morgan): for compatibility, make sure we use a webob request
|
||||
# until /auth is ported to flask. Since this is a webob response,
|
||||
# deconstruct it and turn it into a flask response.
|
||||
webob_resp = compat_controller.authenticate_for_token(
|
||||
self._construct_webob_request(), auth)
|
||||
flask_resp = flask.make_response(
|
||||
webob_resp.body, webob_resp.status_code)
|
||||
flask_resp.headers.extend(webob_resp.headers.dict_of_lists())
|
||||
token = authentication.authenticate_for_token(auth)
|
||||
token_data = render_token.render_token_response_from_model(token)
|
||||
resp_data = jsonutils.dumps(token_data)
|
||||
flask_resp = flask.make_response(resp_data, http_client.CREATED)
|
||||
flask_resp.headers['X-Subject-Token'] = token.id
|
||||
flask_resp.headers['Content-Type'] = 'application/json'
|
||||
return flask_resp
|
||||
|
||||
|
||||
@ -525,18 +448,6 @@ class OSFederationAPI(ks_flask.APIBase):
|
||||
_api_url_prefix = '/OS-FEDERATION'
|
||||
resources = []
|
||||
resource_mapping = [
|
||||
ks_flask.construct_resource_map(
|
||||
# NOTE(morgan): No resource relation here, the resource relation is
|
||||
# to /v3/auth/domains and /v3/auth/projects
|
||||
resource=OSFederationDomainResource,
|
||||
url='/domains',
|
||||
resource_kwargs={}),
|
||||
ks_flask.construct_resource_map(
|
||||
# NOTE(morgan): No resource relation here, the resource relation is
|
||||
# to /v3/auth/domains and /v3/auth/projects
|
||||
resource=OSFederationProjectResource,
|
||||
url='/projects',
|
||||
resource_kwargs={}),
|
||||
ks_flask.construct_resource_map(
|
||||
resource=SAML2MetadataResource,
|
||||
url='/saml2/metadata',
|
||||
|
@ -174,7 +174,7 @@ class RequestTokenResource(_OAuth1ResourceBase):
|
||||
consumer_id,
|
||||
requested_project_id,
|
||||
request_token_duration,
|
||||
initiator=ks_flask.build_audit_initiator())
|
||||
initiator=notifications.build_audit_initiator())
|
||||
|
||||
result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s'
|
||||
% {'key': token_ref['id'],
|
||||
@ -266,7 +266,7 @@ class AccessTokenResource(_OAuth1ResourceBase):
|
||||
token_ref = PROVIDERS.oauth_api.create_access_token(
|
||||
request_token_id,
|
||||
access_token_duration,
|
||||
initiator=ks_flask.build_audit_initiator())
|
||||
initiator=notifications.build_audit_initiator())
|
||||
|
||||
result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s'
|
||||
% {'key': token_ref['id'],
|
||||
|
@ -101,7 +101,7 @@ class Manager(manager.Manager):
|
||||
roles.append(PROVIDERS.role_api.get_role(role['id']))
|
||||
return roles
|
||||
|
||||
def authenticate(self, request, application_credential_id, secret):
|
||||
def authenticate(self, application_credential_id, secret):
|
||||
"""Authenticate with an application credential.
|
||||
|
||||
:param str application_credential_id: Application Credential ID
|
||||
|
@ -15,5 +15,3 @@
|
||||
# NOTE(notmorgan): Be careful in adjusting whitespace here, flake8 checks
|
||||
# get cranky.
|
||||
from keystone.auth import core # noqa
|
||||
|
||||
from keystone.auth import controllers # noqa
|
||||
|
@ -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)
|
@ -414,13 +414,14 @@ class AuthInfo(provider_api.ProviderAPIMixin, object):
|
||||
class UserMFARulesValidator(provider_api.ProviderAPIMixin, object):
|
||||
"""Helper object that can validate the MFA Rules."""
|
||||
|
||||
@property
|
||||
def _auth_methods(self):
|
||||
@classmethod
|
||||
def _auth_methods(cls):
|
||||
if AUTH_PLUGINS_LOADED:
|
||||
return set(AUTH_METHODS.keys())
|
||||
raise RuntimeError(_('Auth Method Plugins are not loaded.'))
|
||||
|
||||
def check_auth_methods_against_rules(self, user_id, auth_methods):
|
||||
@classmethod
|
||||
def check_auth_methods_against_rules(cls, user_id, auth_methods):
|
||||
"""Validate the MFA rules against the successful auth methods.
|
||||
|
||||
:param user_id: The user's ID (uuid).
|
||||
@ -434,7 +435,7 @@ class UserMFARulesValidator(provider_api.ProviderAPIMixin, object):
|
||||
mfa_rules = user_ref['options'].get(ro.MFA_RULES_OPT.option_name, [])
|
||||
mfa_rules_enabled = user_ref['options'].get(
|
||||
ro.MFA_ENABLED_OPT.option_name, True)
|
||||
rules = self._parse_rule_structure(mfa_rules, user_ref['id'])
|
||||
rules = cls._parse_rule_structure(mfa_rules, user_ref['id'])
|
||||
|
||||
if not rules or not mfa_rules_enabled:
|
||||
# return quickly if the rules are disabled for the user or not set
|
||||
@ -451,7 +452,7 @@ class UserMFARulesValidator(provider_api.ProviderAPIMixin, object):
|
||||
# disable an auth method, and a rule will still pass making it
|
||||
# impossible to accidently lock-out a subset of users with a
|
||||
# bad keystone.conf
|
||||
r_set = set(r).intersection(self._auth_methods)
|
||||
r_set = set(r).intersection(cls._auth_methods())
|
||||
if set(auth_methods).issuperset(r_set):
|
||||
# Rule Matches no need to continue, return here.
|
||||
LOG.debug('Auth methods for user `%(user_id)s`, `%(methods)s` '
|
||||
@ -460,7 +461,7 @@ class UserMFARulesValidator(provider_api.ProviderAPIMixin, object):
|
||||
{'user_id': user_id,
|
||||
'rule': list(r_set),
|
||||
'methods': auth_methods,
|
||||
'loaded': self._auth_methods})
|
||||
'loaded': cls._auth_methods()})
|
||||
return True
|
||||
|
||||
LOG.debug('Auth methods for user `%(user_id)s`, `%(methods)s` did not '
|
||||
|
@ -23,7 +23,7 @@ METHOD_NAME = 'application_credential'
|
||||
|
||||
|
||||
class ApplicationCredential(base.AuthMethodHandler):
|
||||
def authenticate(self, request, auth_payload):
|
||||
def authenticate(self, auth_payload):
|
||||
"""Authenticate an application."""
|
||||
response_data = {}
|
||||
app_cred_info = auth_plugins.AppCredInfo.create(auth_payload,
|
||||
@ -31,7 +31,6 @@ class ApplicationCredential(base.AuthMethodHandler):
|
||||
|
||||
try:
|
||||
PROVIDERS.application_credential_api.authenticate(
|
||||
request,
|
||||
application_credential_id=app_cred_info.id,
|
||||
secret=app_cred_info.secret)
|
||||
except AssertionError as e:
|
||||
|
@ -33,11 +33,9 @@ class AuthMethodHandler(provider_api.ProviderAPIMixin, object):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def authenticate(self, request, auth_payload):
|
||||
def authenticate(self, auth_payload):
|
||||
"""Authenticate user and return an authentication context.
|
||||
|
||||
:param request: context of an authentication request
|
||||
:type request: common.request.Request
|
||||
:param auth_payload: the payload content of the authentication request
|
||||
for a given method
|
||||
:type auth_payload: dict
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import abc
|
||||
|
||||
import flask
|
||||
import six
|
||||
|
||||
from keystone.auth.plugins import base
|
||||
@ -31,21 +32,21 @@ PROVIDERS = provider_api.ProviderAPIs
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Base(base.AuthMethodHandler):
|
||||
def authenticate(self, request, auth_payload,):
|
||||
def authenticate(self, auth_payload):
|
||||
"""Use REMOTE_USER to look up the user in the identity backend.
|
||||
|
||||
The user_id from the actual user from the REMOTE_USER env variable is
|
||||
placed in the response_data.
|
||||
"""
|
||||
response_data = {}
|
||||
if not request.remote_user:
|
||||
if not flask.request.remote_user:
|
||||
msg = _('No authenticated user')
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
try:
|
||||
user_ref = self._authenticate(request)
|
||||
user_ref = self._authenticate()
|
||||
except Exception:
|
||||
msg = _('Unable to lookup user %s') % request.remote_user
|
||||
msg = _('Unable to lookup user %s') % flask.request.remote_user
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
response_data['user_id'] = user_ref['id']
|
||||
@ -53,7 +54,7 @@ class Base(base.AuthMethodHandler):
|
||||
response_data=response_data)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _authenticate(self, request):
|
||||
def _authenticate(self):
|
||||
"""Look up the user in the identity backend.
|
||||
|
||||
Return user_ref
|
||||
@ -62,36 +63,35 @@ class Base(base.AuthMethodHandler):
|
||||
|
||||
|
||||
class DefaultDomain(Base):
|
||||
def _authenticate(self, request):
|
||||
def _authenticate(self):
|
||||
"""Use remote_user to look up the user in the identity backend."""
|
||||
return PROVIDERS.identity_api.get_user_by_name(
|
||||
request.remote_user,
|
||||
flask.request.remote_user,
|
||||
CONF.identity.default_domain_id)
|
||||
|
||||
|
||||
class Domain(Base):
|
||||
def _authenticate(self, request):
|
||||
def _authenticate(self):
|
||||
"""Use remote_user to look up the user in the identity backend.
|
||||
|
||||
The domain will be extracted from the REMOTE_DOMAIN environment
|
||||
variable if present. If not, the default domain will be used.
|
||||
"""
|
||||
if request.remote_domain:
|
||||
ref = PROVIDERS.resource_api.get_domain_by_name(
|
||||
request.remote_domain
|
||||
)
|
||||
remote_domain = flask.request.environ.get('REMOTE_DOMAIN')
|
||||
if remote_domain:
|
||||
ref = PROVIDERS.resource_api.get_domain_by_name(remote_domain)
|
||||
domain_id = ref['id']
|
||||
else:
|
||||
domain_id = CONF.identity.default_domain_id
|
||||
|
||||
return PROVIDERS.identity_api.get_user_by_name(request.remote_user,
|
||||
domain_id)
|
||||
return PROVIDERS.identity_api.get_user_by_name(
|
||||
flask.request.remote_user, domain_id)
|
||||
|
||||
|
||||
class KerberosDomain(Domain):
|
||||
"""Allows `kerberos` as a method."""
|
||||
|
||||
def _authenticate(self, request):
|
||||
if request.auth_type != 'Negotiate':
|
||||
def _authenticate(self):
|
||||
if flask.request.environ.get('AUTH_TYPE') != 'Negotiate':
|
||||
raise exception.Unauthorized(_("auth_type is not Negotiate"))
|
||||
return super(KerberosDomain, self)._authenticate(request)
|
||||
return super(KerberosDomain, self)._authenticate()
|
||||
|
@ -13,6 +13,7 @@
|
||||
import functools
|
||||
import uuid
|
||||
|
||||
import flask
|
||||
from oslo_log import log
|
||||
from pycadf import cadftaxonomy as taxonomy
|
||||
from six.moves.urllib import parse
|
||||
@ -38,10 +39,9 @@ class Mapped(base.AuthMethodHandler):
|
||||
token_id = auth_payload['id']
|
||||
return PROVIDERS.token_provider_api.validate_token(token_id)
|
||||
|
||||
def authenticate(self, request, auth_payload):
|
||||
def authenticate(self, auth_payload):
|
||||
"""Authenticate mapped user and set an authentication context.
|
||||
|
||||
:param request: keystone's request context
|
||||
:param auth_payload: the content of the authentication for a
|
||||
given method
|
||||
|
||||
@ -52,13 +52,11 @@ class Mapped(base.AuthMethodHandler):
|
||||
"""
|
||||
if 'id' in auth_payload:
|
||||
token_ref = self._get_token_ref(auth_payload)
|
||||
response_data = handle_scoped_token(request,
|
||||
token_ref,
|
||||
response_data = handle_scoped_token(token_ref,
|
||||
PROVIDERS.federation_api,
|
||||
PROVIDERS.identity_api)
|
||||
else:
|
||||
response_data = handle_unscoped_token(request,
|
||||
auth_payload,
|
||||
response_data = handle_unscoped_token(auth_payload,
|
||||
PROVIDERS.resource_api,
|
||||
PROVIDERS.federation_api,
|
||||
PROVIDERS.identity_api,
|
||||
@ -69,7 +67,7 @@ class Mapped(base.AuthMethodHandler):
|
||||
response_data=response_data)
|
||||
|
||||
|
||||
def handle_scoped_token(request, token, federation_api, identity_api):
|
||||
def handle_scoped_token(token, federation_api, identity_api):
|
||||
response_data = {}
|
||||
utils.validate_expiration(token)
|
||||
token_audit_id = token.audit_id
|
||||
@ -81,7 +79,7 @@ def handle_scoped_token(request, token, federation_api, identity_api):
|
||||
group_ids.append(group_dict['id'])
|
||||
send_notification = functools.partial(
|
||||
notifications.send_saml_audit_notification, 'authenticate',
|
||||
request, user_id, group_ids, identity_provider, protocol,
|
||||
user_id, group_ids, identity_provider, protocol,
|
||||
token_audit_id)
|
||||
|
||||
utils.assert_enabled_identity_provider(federation_api, identity_provider)
|
||||
@ -108,7 +106,7 @@ def handle_scoped_token(request, token, federation_api, identity_api):
|
||||
return response_data
|
||||
|
||||
|
||||
def handle_unscoped_token(request, auth_payload, resource_api, federation_api,
|
||||
def handle_unscoped_token(auth_payload, resource_api, federation_api,
|
||||
identity_api, assignment_api, role_api):
|
||||
|
||||
def validate_shadow_mapping(shadow_projects, existing_roles, idp_domain_id,
|
||||
@ -199,7 +197,7 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api,
|
||||
|
||||
return resp
|
||||
|
||||
assertion = extract_assertion_data(request)
|
||||
assertion = extract_assertion_data()
|
||||
try:
|
||||
identity_provider = auth_payload['identity_provider']
|
||||
except KeyError:
|
||||
@ -234,7 +232,7 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api,
|
||||
|
||||
if is_ephemeral_user(mapped_properties):
|
||||
unique_id, display_name = (
|
||||
get_user_unique_id_and_display_name(request, mapped_properties)
|
||||
get_user_unique_id_and_display_name(mapped_properties)
|
||||
)
|
||||
email = mapped_properties['user'].get('email')
|
||||
user = identity_api.shadow_federated_user(identity_provider,
|
||||
@ -282,7 +280,6 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api,
|
||||
# after sending the notification
|
||||
outcome = taxonomy.OUTCOME_FAILURE
|
||||
notifications.send_saml_audit_notification('authenticate',
|
||||
request,
|
||||
user_id, group_ids,
|
||||
identity_provider,
|
||||
protocol, token_id,
|
||||
@ -291,7 +288,6 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api,
|
||||
else:
|
||||
outcome = taxonomy.OUTCOME_SUCCESS
|
||||
notifications.send_saml_audit_notification('authenticate',
|
||||
request,
|
||||
user_id, group_ids,
|
||||
identity_provider,
|
||||
protocol, token_id,
|
||||
@ -300,8 +296,8 @@ def handle_unscoped_token(request, auth_payload, resource_api, federation_api,
|
||||
return response_data
|
||||
|
||||
|
||||
def extract_assertion_data(request):
|
||||
assertion = dict(utils.get_assertion_params_from_env(request))
|
||||
def extract_assertion_data():
|
||||
assertion = dict(utils.get_assertion_params_from_env())
|
||||
return assertion
|
||||
|
||||
|
||||
@ -329,7 +325,7 @@ def apply_mapping_filter(identity_provider, protocol, assertion,
|
||||
return mapped_properties, mapping_id
|
||||
|
||||
|
||||
def get_user_unique_id_and_display_name(request, mapped_properties):
|
||||
def get_user_unique_id_and_display_name(mapped_properties):
|
||||
"""Setup federated username.
|
||||
|
||||
Function covers all the cases for properly setting user id, a primary
|
||||
@ -345,7 +341,6 @@ def get_user_unique_id_and_display_name(request, mapped_properties):
|
||||
3) If user_id is not set and user_name is, set user_id as url safe version
|
||||
of user_name.
|
||||
|
||||
:param request: current request object
|
||||
:param mapped_properties: Properties issued by a RuleProcessor.
|
||||
:type: dictionary
|
||||
|
||||
@ -358,7 +353,7 @@ def get_user_unique_id_and_display_name(request, mapped_properties):
|
||||
user = mapped_properties['user']
|
||||
|
||||
user_id = user.get('id')
|
||||
user_name = user.get('name') or request.remote_user
|
||||
user_name = user.get('name') or flask.request.remote_user
|
||||
|
||||
if not any([user_id, user_name]):
|
||||
msg = _("Could not map user while setting ephemeral user identity. "
|
||||
|
@ -12,25 +12,26 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import flask
|
||||
from oslo_utils import timeutils
|
||||
|
||||
from keystone.auth.plugins import base
|
||||
from keystone.common import controller
|
||||
from keystone.common import provider_api
|
||||
from keystone import exception
|
||||
from keystone.i18n import _
|
||||
from keystone.oauth1 import core as oauth
|
||||
from keystone.oauth1 import validator
|
||||
from keystone.server import flask as ks_flask
|
||||
|
||||
|
||||
PROVIDERS = provider_api.ProviderAPIs
|
||||
|
||||
|
||||
class OAuth(base.AuthMethodHandler):
|
||||
def authenticate(self, request, auth_payload):
|
||||
def authenticate(self, auth_payload):
|
||||
"""Turn a signed request with an access key into a keystone token."""
|
||||
response_data = {}
|
||||
oauth_headers = oauth.get_oauth_headers(request.headers)
|
||||
oauth_headers = oauth.get_oauth_headers(flask.request.headers)
|
||||
access_token_id = oauth_headers.get('oauth_token')
|
||||
|
||||
if not access_token_id:
|
||||
@ -47,16 +48,15 @@ class OAuth(base.AuthMethodHandler):
|
||||
if now > expires:
|
||||
raise exception.Unauthorized(_('Access token is expired'))
|
||||
|
||||
url = controller.V3Controller.base_url(request.context_dict,
|
||||
request.path_info)
|
||||
url = ks_flask.base_url(path=flask.request.path)
|
||||
access_verifier = oauth.ResourceEndpoint(
|
||||
request_validator=validator.OAuthValidator(),
|
||||
token_generator=oauth.token_generator)
|
||||
result, request = access_verifier.validate_protected_resource_request(
|
||||
url,
|
||||
http_method='POST',
|
||||
body=request.params,
|
||||
headers=request.headers,
|
||||
body=flask.request.args,
|
||||
headers=flask.request.headers,
|
||||
realms=None
|
||||
)
|
||||
if not result:
|
||||
|
@ -25,14 +25,13 @@ PROVIDERS = provider_api.ProviderAPIs
|
||||
|
||||
class Password(base.AuthMethodHandler):
|
||||
|
||||
def authenticate(self, request, auth_payload):
|
||||
def authenticate(self, auth_payload):
|
||||
"""Try to authenticate against the identity backend."""
|
||||
response_data = {}
|
||||
user_info = auth_plugins.UserAuthInfo.create(auth_payload, METHOD_NAME)
|
||||
|
||||
try:
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
request,
|
||||
user_id=user_info.user_id,
|
||||
password=user_info.password)
|
||||
except AssertionError:
|
||||
|
@ -12,6 +12,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import flask
|
||||
from oslo_log import log
|
||||
import six
|
||||
|
||||
@ -35,18 +36,18 @@ class Token(base.AuthMethodHandler):
|
||||
token_id = auth_payload['id']
|
||||
return PROVIDERS.token_provider_api.validate_token(token_id)
|
||||
|
||||
def authenticate(self, request, auth_payload):
|
||||
def authenticate(self, auth_payload):
|
||||
if 'id' not in auth_payload:
|
||||
raise exception.ValidationError(attribute='id',
|
||||
target='token')
|
||||
token = self._get_token_ref(auth_payload)
|
||||
if token.is_federated and PROVIDERS.federation_api:
|
||||
response_data = mapped.handle_scoped_token(
|
||||
request, token, PROVIDERS.federation_api,
|
||||
token, PROVIDERS.federation_api,
|
||||
PROVIDERS.identity_api
|
||||
)
|
||||
else:
|
||||
response_data = token_authenticate(request, token)
|
||||
response_data = token_authenticate(token)
|
||||
|
||||
# NOTE(notmorgan): The Token auth method is *very* special and sets the
|
||||
# previous values to the method_names. This is because it can be used
|
||||
@ -58,7 +59,7 @@ class Token(base.AuthMethodHandler):
|
||||
response_data=response_data)
|
||||
|
||||
|
||||
def token_authenticate(request, token):
|
||||
def token_authenticate(token):
|
||||
response_data = {}
|
||||
try:
|
||||
|
||||
@ -67,10 +68,11 @@ def token_authenticate(request, token):
|
||||
# state in Keystone. To do so is to invite elevation of
|
||||
# privilege attacks
|
||||
|
||||
project_scoped = 'project' in request.json_body['auth'].get(
|
||||
json_body = flask.request.get_json(silent=True, force=True) or {}
|
||||
project_scoped = 'project' in json_body['auth'].get(
|
||||
'scope', {}
|
||||
)
|
||||
domain_scoped = 'domain' in request.json_body['auth'].get(
|
||||
domain_scoped = 'domain' in json_body['auth'].get(
|
||||
'scope', {}
|
||||
)
|
||||
|
||||
|
@ -72,7 +72,7 @@ def _generate_totp_passcode(secret):
|
||||
|
||||
class TOTP(base.AuthMethodHandler):
|
||||
|
||||
def authenticate(self, request, auth_payload):
|
||||
def authenticate(self, auth_payload):
|
||||
"""Try to authenticate using TOTP."""
|
||||
response_data = {}
|
||||
user_info = plugins.TOTPUserInfo.create(auth_payload, METHOD_NAME)
|
||||
|
@ -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'))
|
@ -20,9 +20,9 @@ from oslo_utils import strutils
|
||||
|
||||
from keystone.common import authorization
|
||||
from keystone.common import context
|
||||
from keystone.common import controller
|
||||
from keystone.common import policies
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import render_token
|
||||
from keystone.common import utils
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
@ -85,7 +85,7 @@ class RBACEnforcer(object):
|
||||
# oslo.policy for enforcement. This is because oslo.policy shouldn't
|
||||
# know how to deal with an internal object only used within keystone.
|
||||
if 'token' in credentials:
|
||||
token_ref = controller.render_token_response_from_model(
|
||||
token_ref = render_token.render_token_response_from_model(
|
||||
credentials['token']
|
||||
)
|
||||
credentials_copy = copy.deepcopy(credentials)
|
||||
@ -210,7 +210,7 @@ class RBACEnforcer(object):
|
||||
ret_dict[target] = {}
|
||||
ret_dict[target]['user_id'] = token.user_id
|
||||
try:
|
||||
user_domain_id = token.user_domain_id
|
||||
user_domain_id = token.user['domain_id']
|
||||
except exception.UnexpectedError:
|
||||
user_domain_id = None
|
||||
if user_domain_id:
|
||||
|
145
keystone/common/render_token.py
Normal file
145
keystone/common/render_token.py
Normal 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
|
@ -43,6 +43,7 @@ from six.moves import http_client
|
||||
|
||||
from keystone.common import controller
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import render_token
|
||||
from keystone.common import utils
|
||||
from keystone.common import wsgi
|
||||
import keystone.conf
|
||||
@ -296,7 +297,7 @@ class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller):
|
||||
token = self.token_provider_api.issue_token(
|
||||
user_ref['id'], method_names, project_id=project_ref['id']
|
||||
)
|
||||
token_reference = controller.render_token_response_from_model(token)
|
||||
token_reference = render_token.render_token_response_from_model(token)
|
||||
return self.render_token_data_response(token.id, token_reference)
|
||||
|
||||
@controller.protected(callback=_check_credential_owner_and_user_id_match)
|
||||
|
@ -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)
|
@ -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,
|
||||
})
|
@ -15,6 +15,7 @@
|
||||
import ast
|
||||
import re
|
||||
|
||||
import flask
|
||||
import jsonschema
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
@ -416,10 +417,10 @@ def transform_to_group_ids(group_names, mapping_id,
|
||||
group['name'])
|
||||
|
||||
|
||||
def get_assertion_params_from_env(request):
|
||||
LOG.debug('Environment variables: %s', request.environ)
|
||||
def get_assertion_params_from_env():
|
||||
LOG.debug('Environment variables: %s', flask.request.environ)
|
||||
prefix = CONF.federation.assertion_prefix
|
||||
for k, v in list(request.environ.items()):
|
||||
for k, v in list(flask.request.environ.items()):
|
||||
if not k.startswith(prefix):
|
||||
continue
|
||||
# These bytes may be decodable as ISO-8859-1 according to Section
|
||||
|
@ -105,7 +105,7 @@ class UserV3(controller.V3Controller):
|
||||
attribute='password')
|
||||
try:
|
||||
PROVIDERS.identity_api.change_password(
|
||||
request, user_id, original_password,
|
||||
user_id, original_password,
|
||||
password, initiator=request.audit_initiator)
|
||||
except AssertionError as e:
|
||||
raise exception.Unauthorized(_(
|
||||
|
@ -900,10 +900,19 @@ class Manager(manager.Manager):
|
||||
# - clear/set domain_ids for drivers that do not support domains
|
||||
# - create any ID mapping that might be required
|
||||
|
||||
# TODO(morgan): The split of "authenticate" and "_authenticate" is done
|
||||
# until user API is converted to flask. This is to make webob and flask
|
||||
# play nicely with the authenticate mechanism during self-service password
|
||||
# changes. While this is in place, CADF notifications will not be emitted
|
||||
# for self-service password changes indicating an auth attempt was being
|
||||
# made. This is a very limited time transitional change.
|
||||
@notifications.emit_event('authenticate')
|
||||
def authenticate(self, user_id, password):
|
||||
return self._authenticate(user_id, password)
|
||||
|
||||
@domains_configured
|
||||
@exception_translated('assertion')
|
||||
def authenticate(self, request, user_id, password):
|
||||
def _authenticate(self, user_id, password):
|
||||
domain_id, driver, entity_id = (
|
||||
self._get_domain_driver_and_entity_id(user_id))
|
||||
ref = driver.authenticate(entity_id, password)
|
||||
@ -1376,12 +1385,14 @@ class Manager(manager.Manager):
|
||||
group_entity_id)
|
||||
|
||||
@domains_configured
|
||||
def change_password(self, request, user_id, original_password,
|
||||
def change_password(self, user_id, original_password,
|
||||
new_password, initiator=None):
|
||||
|
||||
# authenticate() will raise an AssertionError if authentication fails
|
||||
try:
|
||||
self.authenticate(request, user_id, original_password)
|
||||
# TODO(morgan): When users is ported to flask, ensure this is
|
||||
# mapped back to self.authenticate instead of self._authenticate.
|
||||
self._authenticate(user_id, original_password)
|
||||
except exception.PasswordExpired:
|
||||
# If a password has expired, we want users to be able to change it
|
||||
pass
|
||||
|
@ -15,8 +15,8 @@ from oslo_log import log
|
||||
|
||||
from keystone.common import authorization
|
||||
from keystone.common import context
|
||||
from keystone.common import controller
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import render_token
|
||||
from keystone.common import tokenless_auth
|
||||
from keystone.common import wsgi
|
||||
import keystone.conf
|
||||
@ -45,7 +45,7 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
|
||||
def fetch_token(self, token, **kwargs):
|
||||
try:
|
||||
token_model = self.token_provider_api.validate_token(token)
|
||||
return controller.render_token_response_from_model(token_model)
|
||||
return render_token.render_token_response_from_model(token_model)
|
||||
except exception.TokenNotFound:
|
||||
raise auth_token.InvalidToken(_('Could not find token'))
|
||||
|
||||
|
@ -19,6 +19,7 @@ import functools
|
||||
import inspect
|
||||
import socket
|
||||
|
||||
import flask
|
||||
from oslo_log import log
|
||||
import oslo_messaging
|
||||
from oslo_utils import reflection
|
||||
@ -27,9 +28,11 @@ from pycadf import cadftaxonomy as taxonomy
|
||||
from pycadf import cadftype
|
||||
from pycadf import credential
|
||||
from pycadf import eventfactory
|
||||
from pycadf import host
|
||||
from pycadf import reason
|
||||
from pycadf import resource
|
||||
|
||||
from keystone.common import context
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import utils
|
||||
import keystone.conf
|
||||
@ -82,6 +85,26 @@ REMOVE_APP_CREDS_FOR_USER = 'remove_application_credentials_for_user'
|
||||
DOMAIN_DELETED = 'domain_deleted'
|
||||
|
||||
|
||||
def build_audit_initiator():
|
||||
"""A pyCADF initiator describing the current authenticated context."""
|
||||
pycadf_host = host.Host(address=flask.request.remote_addr,
|
||||
agent=str(flask.request.user_agent))
|
||||
initiator = resource.Resource(typeURI=taxonomy.ACCOUNT_USER,
|
||||
host=pycadf_host)
|
||||
oslo_context = flask.request.environ.get(context.REQUEST_CONTEXT_ENV)
|
||||
if oslo_context.user_id:
|
||||
initiator.id = utils.resource_uuid(oslo_context.user_id)
|
||||
initiator.user_id = oslo_context.user_id
|
||||
|
||||
if oslo_context.project_id:
|
||||
initiator.project_id = oslo_context.project_id
|
||||
|
||||
if oslo_context.domain_id:
|
||||
initiator.domain_id = oslo_context.domain_id
|
||||
|
||||
return initiator
|
||||
|
||||
|
||||
class Audit(object):
|
||||
"""Namespace for audit notification functions.
|
||||
|
||||
@ -515,14 +538,14 @@ class CadfNotificationWrapper(object):
|
||||
|
||||
def __call__(self, f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(wrapped_self, request, user_id, *args, **kwargs):
|
||||
def wrapper(wrapped_self, user_id, *args, **kwargs):
|
||||
"""Will always send a notification."""
|
||||
target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER)
|
||||
initiator = request.audit_initiator
|
||||
initiator = build_audit_initiator()
|
||||
initiator.user_id = user_id
|
||||
initiator.id = utils.resource_uuid(user_id)
|
||||
try:
|
||||
result = f(wrapped_self, request, user_id, *args, **kwargs)
|
||||
result = f(wrapped_self, user_id, *args, **kwargs)
|
||||
except (exception.AccountLocked,
|
||||
exception.PasswordExpired) as ex:
|
||||
# Send a CADF event with a reason for PCI-DSS related
|
||||
@ -657,15 +680,13 @@ class CadfRoleAssignmentNotificationWrapper(object):
|
||||
return wrapper
|
||||
|
||||
|
||||
def send_saml_audit_notification(action, request, user_id, group_ids,
|
||||
def send_saml_audit_notification(action, user_id, group_ids,
|
||||
identity_provider, protocol, token_id,
|
||||
outcome):
|
||||
"""Send notification to inform observers about SAML events.
|
||||
|
||||
:param action: Action being audited
|
||||
:type action: str
|
||||
:param request: Current request to collect request info from
|
||||
:type request: keystone.common.request.Request
|
||||
:param user_id: User ID from Keystone token
|
||||
:type user_id: str
|
||||
:param group_ids: List of Group IDs from Keystone token
|
||||
@ -679,7 +700,7 @@ def send_saml_audit_notification(action, request, user_id, group_ids,
|
||||
:param outcome: One of :class:`pycadf.cadftaxonomy`
|
||||
:type outcome: str
|
||||
"""
|
||||
initiator = request.audit_initiator
|
||||
initiator = build_audit_initiator()
|
||||
target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER)
|
||||
audit_type = SAML_AUDIT_TYPE
|
||||
user_id = user_id or taxonomy.UNKNOWN
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
from keystone.server.flask.common import APIBase # noqa
|
||||
from keystone.server.flask.common import base_url # noqa
|
||||
from keystone.server.flask.common import build_audit_initiator # noqa
|
||||
from keystone.server.flask.common import construct_json_home_data # noqa
|
||||
from keystone.server.flask.common import construct_resource_map # noqa
|
||||
from keystone.server.flask.common import full_url # noqa
|
||||
@ -29,5 +28,5 @@ from keystone.server.flask.common import unenforced_api # noqa
|
||||
# NOTE(morgan): This allows for from keystone.flask import * and have all the
|
||||
# cool stuff needed to develop new APIs within a module/subsystem
|
||||
__all__ = ('APIBase', 'JsonHomeData', 'ResourceBase', 'ResourceMap',
|
||||
'base_url', 'build_audit_initiator', 'construct_json_home_data',
|
||||
'base_url', 'construct_json_home_data',
|
||||
'construct_resource_map', 'full_url', 'unenforced_api')
|
||||
|
@ -26,11 +26,9 @@ import werkzeug.wsgi
|
||||
import keystone.api
|
||||
from keystone.application_credential import routers as app_cred_routers
|
||||
from keystone.assignment import routers as assignment_routers
|
||||
from keystone.auth import routers as auth_routers
|
||||
from keystone.common import wsgi as keystone_wsgi
|
||||
from keystone.contrib.ec2 import routers as ec2_routers
|
||||
from keystone.contrib.s3 import routers as s3_routers
|
||||
from keystone.federation import routers as federation_routers
|
||||
from keystone.identity import routers as identity_routers
|
||||
from keystone.oauth1 import routers as oauth1_routers
|
||||
from keystone.resource import routers as resource_routers
|
||||
@ -38,7 +36,8 @@ from keystone.resource import routers as resource_routers
|
||||
# TODO(morgan): _MOVED_API_PREFIXES to be removed when the legacy dispatch
|
||||
# support is removed.
|
||||
_MOVED_API_PREFIXES = frozenset(
|
||||
['credentials',
|
||||
['auth',
|
||||
'credentials',
|
||||
'domains',
|
||||
'endpoints',
|
||||
'groups',
|
||||
@ -64,12 +63,10 @@ _MOVED_API_PREFIXES = frozenset(
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
ALL_API_ROUTERS = [auth_routers,
|
||||
assignment_routers,
|
||||
ALL_API_ROUTERS = [assignment_routers,
|
||||
identity_routers,
|
||||
app_cred_routers,
|
||||
resource_routers,
|
||||
federation_routers,
|
||||
oauth1_routers,
|
||||
ec2_routers,
|
||||
s3_routers]
|
||||
|
@ -25,9 +25,6 @@ import flask_restful.utils
|
||||
from oslo_log import log
|
||||
from oslo_log import versionutils
|
||||
from oslo_serialization import jsonutils
|
||||
from pycadf import cadftaxonomy as taxonomy
|
||||
from pycadf import host
|
||||
from pycadf import resource
|
||||
import six
|
||||
from six.moves import http_client
|
||||
|
||||
@ -40,6 +37,7 @@ from keystone.common import utils
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
from keystone.i18n import _
|
||||
from keystone import notifications
|
||||
|
||||
|
||||
# NOTE(morgan): Capture the relevant part of the flask url route rule for
|
||||
@ -92,15 +90,21 @@ def construct_resource_map(resource, url, resource_kwargs, alternate_urls=None,
|
||||
Additional keyword arguments not specified above
|
||||
will be passed as-is to
|
||||
:meth:`flask.Flask.add_url_rule`.
|
||||
:param alternate_urls: An iterable (list) of urls that also map to the
|
||||
resource. These are used to ensure API compat when
|
||||
a "new" path is more correct for the API but old
|
||||
paths must continue to work. Example:
|
||||
:param alternate_urls: An iterable (list) of dictionaries containing urls
|
||||
and associated json home REL data. Each element is
|
||||
expected to be a dictionary with a 'url' key and an
|
||||
optional 'json_home' key for a 'JsonHomeData' named
|
||||
tuple These urls will also map to the resource.
|
||||
These are used to ensure API compatibility when a
|
||||
"new" path is more correct for the API but old paths
|
||||
must continue to work. Example:
|
||||
`/auth/domains` being the new path for
|
||||
`/OS-FEDERATION/domains`. The `OS-FEDERATION` part
|
||||
would be listed as an alternate url. These are not
|
||||
added to the JSON Home Document.
|
||||
:type: any iterable or None
|
||||
would be listed as an alternate url. If a
|
||||
'json_home' key is provided, the original path
|
||||
with the new json_home data will be added to the
|
||||
JSON Home Document.
|
||||
:type: iterable or None
|
||||
:param rel:
|
||||
:type rel: str or None
|
||||
:param status: JSON Home API Status, e.g. "STABLE"
|
||||
@ -153,26 +157,6 @@ def _remove_content_type_on_204(resp):
|
||||
return resp
|
||||
|
||||
|
||||
def build_audit_initiator():
|
||||
"""A pyCADF initiator describing the current authenticated context."""
|
||||
pycadf_host = host.Host(address=flask.request.remote_addr,
|
||||
agent=str(flask.request.user_agent))
|
||||
initiator = resource.Resource(typeURI=taxonomy.ACCOUNT_USER,
|
||||
host=pycadf_host)
|
||||
oslo_context = flask.request.environ.get(context.REQUEST_CONTEXT_ENV)
|
||||
if oslo_context.user_id:
|
||||
initiator.id = utils.resource_uuid(oslo_context.user_id)
|
||||
initiator.user_id = oslo_context.user_id
|
||||
|
||||
if oslo_context.project_id:
|
||||
initiator.project_id = oslo_context.project_id
|
||||
|
||||
if oslo_context.domain_id:
|
||||
initiator.domain_id = oslo_context.domain_id
|
||||
|
||||
return initiator
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class APIBase(object):
|
||||
|
||||
@ -427,19 +411,33 @@ class APIBase(object):
|
||||
def _add_mapped_resources(self):
|
||||
# Add resource mappings, non-standard resource connections
|
||||
for r in self.resource_mapping:
|
||||
alt_url_json_home_data = []
|
||||
LOG.debug(
|
||||
'Adding resource routes to API %(name)s: '
|
||||
'[%(url)r %(kwargs)r]',
|
||||
{'name': self._name, 'url': r.url, 'kwargs': r.kwargs})
|
||||
self.api.add_resource(r.resource, r.url, **r.kwargs)
|
||||
urls = [r.url]
|
||||
if r.alternate_urls is not None:
|
||||
for element in r.alternate_urls:
|
||||
if self._api_url_prefix:
|
||||
LOG.debug(
|
||||
'Adding additional resource routes (alternate) to API'
|
||||
'%(name)s: [%(urls)r %(kwargs)r]',
|
||||
{'name': self._name, 'urls': r.alternate_urls,
|
||||
'Unable to add additional resource route '
|
||||
'`%(route)s` to API %(name)s because API has a '
|
||||
'URL prefix. Only APIs without explicit prefixes '
|
||||
'can have alternate URL routes added.',
|
||||
{'route': element['url'], 'name': self._name}
|
||||
)
|
||||
continue
|
||||
LOG.debug(
|
||||
'Adding additional resource route (alternate) to API'
|
||||
'%(name)s: [%(url)r %(kwargs)r]',
|
||||
{'name': self._name, 'url': element['url'],
|
||||
'kwargs': r.kwargs})
|
||||
self.api.add_resource(r.resource, *r.alternate_urls,
|
||||
**r.kwargs)
|
||||
urls.append(element['url'])
|
||||
if element.get('json_home'):
|
||||
alt_url_json_home_data.append(element['json_home'])
|
||||
# Add all URL routes at once.
|
||||
self.api.add_resource(r.resource, *urls, **r.kwargs)
|
||||
|
||||
# Build the JSON Home data and add it to the relevant JSON Home
|
||||
# Documents for explicit JSON Home data.
|
||||
@ -462,6 +460,12 @@ class APIBase(object):
|
||||
r.json_home_data.rel,
|
||||
resource_data)
|
||||
|
||||
for element in alt_url_json_home_data:
|
||||
# Append the "new" path (resource) data with the old rel
|
||||
# reference.
|
||||
json_home.JsonHomeResources.append_resource(
|
||||
element.rel, resource_data)
|
||||
|
||||
def _register_before_request_functions(self, functions=None):
|
||||
"""Register functions to be executed in the `before request` phase.
|
||||
|
||||
@ -764,7 +768,7 @@ class ResourceBase(flask_restful.Resource):
|
||||
|
||||
As a property.
|
||||
"""
|
||||
return build_audit_initiator()
|
||||
return notifications.build_audit_initiator()
|
||||
|
||||
@staticmethod
|
||||
def query_filter_is_true(filter_name):
|
||||
|
@ -146,6 +146,8 @@ def initialize_application(name, post_log_configured_function=lambda: None,
|
||||
config_files = [dev_conf]
|
||||
|
||||
keystone.server.configure(config_files=config_files)
|
||||
# explicitly load auth configuration
|
||||
keystone.conf.auth.setup_authentication()
|
||||
|
||||
# Log the options used when starting if we're in debug mode...
|
||||
if CONF.debug:
|
||||
|
@ -257,13 +257,11 @@ class ApplicationCredentialTests(object):
|
||||
app_cred = self._new_app_cred_data(self.user_foo['id'],
|
||||
project_id=self.tenant_bar['id'])
|
||||
resp = self.app_cred_api.create_application_credential(app_cred)
|
||||
self.app_cred_api.authenticate(
|
||||
self.make_request(), resp['id'], resp['secret'])
|
||||
self.app_cred_api.authenticate(resp['id'], resp['secret'])
|
||||
|
||||
def test_authenticate_not_found(self):
|
||||
self.assertRaises(AssertionError,
|
||||
self.app_cred_api.authenticate,
|
||||
self.make_request(),
|
||||
uuid.uuid4().hex,
|
||||
uuid.uuid4().hex)
|
||||
|
||||
@ -275,7 +273,6 @@ class ApplicationCredentialTests(object):
|
||||
resp = self.app_cred_api.create_application_credential(app_cred)
|
||||
self.assertRaises(AssertionError,
|
||||
self.app_cred_api.authenticate,
|
||||
self.make_request(),
|
||||
resp['id'],
|
||||
resp['secret'])
|
||||
|
||||
@ -287,6 +284,5 @@ class ApplicationCredentialTests(object):
|
||||
self.assertNotEqual(badpass, resp['secret'])
|
||||
self.assertRaises(AssertionError,
|
||||
self.app_cred_api.authenticate,
|
||||
self.make_request(),
|
||||
resp['id'],
|
||||
badpass)
|
||||
|
@ -745,18 +745,17 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
|
||||
user_ref = unit.new_user_ref(domain_id=self.domain_id,
|
||||
password=password)
|
||||
user_ref = PROVIDERS.identity_api.create_user(user_ref)
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_ref['id'], password
|
||||
)
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.authenticate(user_ref['id'], password)
|
||||
freezer.stop()
|
||||
|
||||
reason_type = (exception.PasswordExpired.message_format %
|
||||
{'user_id': user_ref['id']})
|
||||
expected_reason = {'reasonCode': '401',
|
||||
'reasonType': reason_type}
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordExpired,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
password=password)
|
||||
self._assert_last_audit(None, 'authenticate', None,
|
||||
@ -764,6 +763,8 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
|
||||
reason=expected_reason)
|
||||
|
||||
def test_locked_out_user_sends_notification(self):
|
||||
# TODO(morgan): skip this test until users is ported to flask.
|
||||
self.skipTest('Users are not handled via flask.')
|
||||
password = uuid.uuid4().hex
|
||||
new_password = uuid.uuid4().hex
|
||||
expected_responses = [AssertionError, AssertionError, AssertionError,
|
||||
@ -776,9 +777,9 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
|
||||
expected_reason = {'reasonCode': '401',
|
||||
'reasonType': reason_type}
|
||||
for ex in expected_responses:
|
||||
with self.make_request():
|
||||
self.assertRaises(ex,
|
||||
PROVIDERS.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
original_password=new_password,
|
||||
new_password=new_password)
|
||||
@ -801,13 +802,14 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
|
||||
user_ref = unit.new_user_ref(domain_id=self.domain_id,
|
||||
password=password)
|
||||
user_ref = PROVIDERS.identity_api.create_user(user_ref)
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.change_password(
|
||||
self.make_request(), user_id=user_ref['id'],
|
||||
user_id=user_ref['id'],
|
||||
original_password=password, new_password=new_password
|
||||
)
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
PROVIDERS.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
original_password=new_password,
|
||||
new_password=password)
|
||||
@ -828,9 +830,9 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
|
||||
user_ref = unit.new_user_ref(domain_id=self.domain_id,
|
||||
password=password)
|
||||
user_ref = PROVIDERS.identity_api.create_user(user_ref)
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
PROVIDERS.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
original_password=password,
|
||||
new_password=invalid_password)
|
||||
@ -858,13 +860,14 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
|
||||
{'min_age_days': min_days, 'days_left': days_left})
|
||||
expected_reason = {'reasonCode': '400',
|
||||
'reasonType': reason_type}
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.change_password(
|
||||
self.make_request(), user_id=user_ref['id'],
|
||||
user_id=user_ref['id'],
|
||||
original_password=password, new_password=new_password
|
||||
)
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
PROVIDERS.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user_ref['id'],
|
||||
original_password=new_password,
|
||||
new_password=next_password)
|
||||
|
@ -10,11 +10,11 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import flask
|
||||
import uuid
|
||||
|
||||
from oslo_config import fixture as config_fixture
|
||||
from oslo_serialization import jsonutils
|
||||
import webob
|
||||
|
||||
from keystone.auth.plugins import mapped
|
||||
import keystone.conf
|
||||
@ -31,6 +31,13 @@ FAKE_MAPPING_ID = uuid.uuid4().hex
|
||||
class MappingRuleEngineTests(unit.BaseTestCase):
|
||||
"""A class for testing the mapping rule engine."""
|
||||
|
||||
def setUp(self):
|
||||
super(MappingRuleEngineTests, self).setUp()
|
||||
# create dummy app so we can setup a request context for our
|
||||
# tests.
|
||||
self.flask_app = flask.Flask(__name__)
|
||||
self.cleanup_instance('flask_app')
|
||||
|
||||
def assertValidMappedUserObject(self, mapped_properties,
|
||||
user_type='ephemeral',
|
||||
domain_id=None):
|
||||
@ -510,7 +517,7 @@ class MappingRuleEngineTests(unit.BaseTestCase):
|
||||
self.assertValidMappedUserObject(mapped_properties)
|
||||
self.assertEqual('jsmith', mapped_properties['user']['name'])
|
||||
unique_id, display_name = mapped.get_user_unique_id_and_display_name(
|
||||
{}, mapped_properties)
|
||||
mapped_properties)
|
||||
self.assertEqual('jsmith', unique_id)
|
||||
self.assertEqual('jsmith', display_name)
|
||||
|
||||
@ -533,7 +540,7 @@ class MappingRuleEngineTests(unit.BaseTestCase):
|
||||
self.assertIsNotNone(mapped_properties)
|
||||
self.assertValidMappedUserObject(mapped_properties)
|
||||
unique_id, display_name = mapped.get_user_unique_id_and_display_name(
|
||||
{}, mapped_properties)
|
||||
mapped_properties)
|
||||
self.assertEqual('tbo', display_name)
|
||||
self.assertEqual('abc123%40example.com', unique_id)
|
||||
|
||||
@ -549,15 +556,15 @@ class MappingRuleEngineTests(unit.BaseTestCase):
|
||||
as it was not explicitly specified in the mapping.
|
||||
|
||||
"""
|
||||
request = webob.Request.blank('/')
|
||||
mapping = mapping_fixtures.MAPPING_USER_IDS
|
||||
rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules'])
|
||||
assertion = mapping_fixtures.ADMIN_ASSERTION
|
||||
mapped_properties = rp.process(assertion)
|
||||
self.assertIsNotNone(mapped_properties)
|
||||
self.assertValidMappedUserObject(mapped_properties)
|
||||
unique_id, display_name = mapped.get_user_unique_id_and_display_name(
|
||||
request, mapped_properties)
|
||||
with self.flask_app.test_request_context():
|
||||
unique_id, display_name = (
|
||||
mapped.get_user_unique_id_and_display_name(mapped_properties))
|
||||
self.assertEqual('bob', unique_id)
|
||||
self.assertEqual('bob', display_name)
|
||||
|
||||
@ -566,13 +573,14 @@ class MappingRuleEngineTests(unit.BaseTestCase):
|
||||
mapping = mapping_fixtures.MAPPING_USER_IDS
|
||||
assertion = mapping_fixtures.ADMIN_ASSERTION
|
||||
FAKE_MAPPING_ID = uuid.uuid4().hex
|
||||
request = webob.Request.blank('/', remote_user='remote_user')
|
||||
rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules'])
|
||||
mapped_properties = rp.process(assertion)
|
||||
self.assertIsNotNone(mapped_properties)
|
||||
self.assertValidMappedUserObject(mapped_properties)
|
||||
unique_id, display_name = mapped.get_user_unique_id_and_display_name(
|
||||
request, mapped_properties)
|
||||
with self.flask_app.test_request_context(
|
||||
environ_base={'REMOTE_USER': 'remote_user'}):
|
||||
unique_id, display_name = (
|
||||
mapped.get_user_unique_id_and_display_name(mapped_properties))
|
||||
self.assertEqual('bob', unique_id)
|
||||
self.assertEqual('remote_user', display_name)
|
||||
|
||||
@ -597,7 +605,6 @@ class MappingRuleEngineTests(unit.BaseTestCase):
|
||||
not to change it.
|
||||
|
||||
"""
|
||||
request = webob.Request.blank('/')
|
||||
testcases = [(mapping_fixtures.CUSTOMER_ASSERTION, 'bwilliams'),
|
||||
(mapping_fixtures.EMPLOYEE_ASSERTION, 'tbo')]
|
||||
for assertion, exp_user_name in testcases:
|
||||
@ -607,8 +614,7 @@ class MappingRuleEngineTests(unit.BaseTestCase):
|
||||
self.assertIsNotNone(mapped_properties)
|
||||
self.assertValidMappedUserObject(mapped_properties)
|
||||
unique_id, display_name = (
|
||||
mapped.get_user_unique_id_and_display_name(request,
|
||||
mapped_properties)
|
||||
mapped.get_user_unique_id_and_display_name(mapped_properties)
|
||||
)
|
||||
self.assertEqual(exp_user_name, display_name)
|
||||
self.assertEqual('abc123%40example.com', unique_id)
|
||||
@ -821,10 +827,12 @@ class TestUnicodeAssertionData(unit.BaseTestCase):
|
||||
# pulled from the HTTP headers. These bytes may be decodable as
|
||||
# ISO-8859-1 according to Section 3.2.4 of RFC 7230. Let's assume
|
||||
# that our web server plugins are correctly encoding the data.
|
||||
request = webob.Request.blank(
|
||||
'/path',
|
||||
environ=mapping_fixtures.UNICODE_NAME_ASSERTION)
|
||||
data = mapping_utils.get_assertion_params_from_env(request)
|
||||
# Create a dummy application
|
||||
app = flask.Flask(__name__)
|
||||
with app.test_request_context(
|
||||
path='/path',
|
||||
environ_overrides=mapping_fixtures.UNICODE_NAME_ASSERTION):
|
||||
data = mapping_utils.get_assertion_params_from_env()
|
||||
# NOTE(dstanek): keystone.auth.plugins.mapped
|
||||
return dict(data)
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
from __future__ import absolute_import
|
||||
import atexit
|
||||
import base64
|
||||
import contextlib
|
||||
import datetime
|
||||
import functools
|
||||
import hashlib
|
||||
@ -46,7 +47,6 @@ import keystone.api
|
||||
from keystone.common import context
|
||||
from keystone.common import json_home
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import request
|
||||
from keystone.common import sql
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
@ -684,19 +684,27 @@ class TestCase(BaseTestCase):
|
||||
def _policy_fixture(self):
|
||||
return ksfixtures.Policy(dirs.etc('policy.json'), self.config_fixture)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def make_request(self, path='/', **kwargs):
|
||||
# standup a fake app and request context with a passed in/known
|
||||
# environment.
|
||||
|
||||
is_admin = kwargs.pop('is_admin', False)
|
||||
environ = kwargs.setdefault('environ', {})
|
||||
query_string = kwargs.pop('query_string', None)
|
||||
if query_string:
|
||||
# Make sure query string is properly added to the context
|
||||
path = '{path}?{qs}'.format(path=path, qs=query_string)
|
||||
|
||||
if not environ.get(context.REQUEST_CONTEXT_ENV):
|
||||
environ[context.REQUEST_CONTEXT_ENV] = context.RequestContext(
|
||||
is_admin=is_admin,
|
||||
authenticated=kwargs.pop('authenticated', True))
|
||||
|
||||
req = request.Request.blank(path=path, **kwargs)
|
||||
req.context_dict['is_admin'] = is_admin
|
||||
|
||||
return req
|
||||
# Create a dummy flask app to work with
|
||||
app = flask.Flask(__name__)
|
||||
with app.test_request_context(path=path, environ_overrides=environ):
|
||||
yield
|
||||
|
||||
def config_overrides(self):
|
||||
# NOTE(morganfainberg): enforce config_overrides can only ever be
|
||||
@ -779,6 +787,8 @@ class TestCase(BaseTestCase):
|
||||
new=mocked_register_auth_plugin_opt))
|
||||
|
||||
self.config_overrides()
|
||||
# explicitly load auth configuration
|
||||
keystone.conf.auth.setup_authentication()
|
||||
# NOTE(morganfainberg): ensure config_overrides has been called.
|
||||
self.addCleanup(self._assert_config_overrides_called)
|
||||
|
||||
|
@ -121,8 +121,8 @@ class ShadowUsersBackendTests(object):
|
||||
now = datetime.datetime.utcnow().date()
|
||||
password = uuid.uuid4().hex
|
||||
user = self._create_user(password)
|
||||
with self.make_request():
|
||||
user_auth = PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password=password)
|
||||
user_ref = self._get_user_ref(user_auth['id'])
|
||||
@ -133,8 +133,8 @@ class ShadowUsersBackendTests(object):
|
||||
disable_user_account_days_inactive=None)
|
||||
password = uuid.uuid4().hex
|
||||
user = self._create_user(password)
|
||||
with self.make_request():
|
||||
user_auth = PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password=password)
|
||||
user_ref = self._get_user_ref(user_auth['id'])
|
||||
|
@ -272,9 +272,9 @@ class DisableInactiveUserTests(test_backend_sql.SqlTests):
|
||||
datetime.datetime.utcnow() -
|
||||
datetime.timedelta(days=self.max_inactive_days + 1))
|
||||
user = self._create_user(self.user_dict, last_active_at.date())
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.UserDisabled,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password=self.password)
|
||||
# verify that the user is actually disabled
|
||||
@ -284,7 +284,7 @@ class DisableInactiveUserTests(test_backend_sql.SqlTests):
|
||||
user['enabled'] = True
|
||||
PROVIDERS.identity_api.update_user(user['id'], user)
|
||||
user = PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=user['id'], password=self.password
|
||||
user_id=user['id'], password=self.password
|
||||
)
|
||||
self.assertTrue(user['enabled'])
|
||||
|
||||
@ -294,8 +294,9 @@ class DisableInactiveUserTests(test_backend_sql.SqlTests):
|
||||
datetime.datetime.utcnow() -
|
||||
datetime.timedelta(days=self.max_inactive_days - 1)).date()
|
||||
user = self._create_user(self.user_dict, last_active_at)
|
||||
with self.make_request():
|
||||
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'])
|
||||
|
||||
@ -392,9 +393,9 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests):
|
||||
password = uuid.uuid4().hex
|
||||
user = self._create_user(password)
|
||||
# Attempt to change to the same password
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
PROVIDERS.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
original_password=password,
|
||||
new_password=password)
|
||||
@ -404,7 +405,6 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests):
|
||||
# Attempt to change back to the initial password
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
PROVIDERS.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
original_password=new_password,
|
||||
new_password=password)
|
||||
@ -441,9 +441,9 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests):
|
||||
# Self-service change password
|
||||
self.assertValidChangePassword(user['id'], passwords[0], passwords[1])
|
||||
# Attempt to update with a previous password
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordValidationError,
|
||||
PROVIDERS.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
original_password=passwords[1],
|
||||
new_password=passwords[0])
|
||||
@ -462,21 +462,22 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests):
|
||||
user = self._create_user(passwords[0])
|
||||
# Attempt to change password to a unique password
|
||||
user['password'] = passwords[1]
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.update_user(user['id'], user)
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=user['id'], password=passwords[1]
|
||||
user_id=user['id'], password=passwords[1]
|
||||
)
|
||||
# Attempt to change password with the same password
|
||||
user['password'] = passwords[1]
|
||||
PROVIDERS.identity_api.update_user(user['id'], user)
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=user['id'], password=passwords[1]
|
||||
user_id=user['id'], password=passwords[1]
|
||||
)
|
||||
# Attempt to change password with the initial password
|
||||
user['password'] = passwords[0]
|
||||
PROVIDERS.identity_api.update_user(user['id'], user)
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=user['id'], password=passwords[0]
|
||||
user_id=user['id'], password=passwords[0]
|
||||
)
|
||||
|
||||
def test_truncate_passwords(self):
|
||||
@ -535,12 +536,13 @@ class PasswordHistoryValidationTests(test_backend_sql.SqlTests):
|
||||
return PROVIDERS.identity_api.create_user(user)
|
||||
|
||||
def assertValidChangePassword(self, user_id, password, new_password):
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.change_password(
|
||||
self.make_request(), user_id=user_id, original_password=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
|
||||
user_id=user_id, password=new_password
|
||||
)
|
||||
|
||||
def _add_passwords_to_history(self, user, n):
|
||||
@ -573,22 +575,21 @@ class LockingOutUserTests(test_backend_sql.SqlTests):
|
||||
self.user = PROVIDERS.identity_api.create_user(user_dict)
|
||||
|
||||
def test_locking_out_user_after_max_failed_attempts(self):
|
||||
with self.make_request():
|
||||
# authenticate with wrong password
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=self.user['id'],
|
||||
password=uuid.uuid4().hex)
|
||||
# authenticate with correct password
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=self.user['id'],
|
||||
user_id=self.user['id'],
|
||||
password=self.password
|
||||
)
|
||||
# test locking out user after max failed attempts
|
||||
self._fail_auth_repeatedly(self.user['id'])
|
||||
self.assertRaises(exception.AccountLocked,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=self.user['id'],
|
||||
password=uuid.uuid4().hex)
|
||||
|
||||
@ -601,30 +602,30 @@ class LockingOutUserTests(test_backend_sql.SqlTests):
|
||||
# fail authentication repeatedly the max number of times
|
||||
self._fail_auth_repeatedly(self.user['id'])
|
||||
# authenticate with wrong password, account should not be locked
|
||||
with self.make_request():
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=self.user['id'],
|
||||
password=uuid.uuid4().hex)
|
||||
# authenticate with correct password, account should not be locked
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=self.user['id'],
|
||||
user_id=self.user['id'],
|
||||
password=self.password
|
||||
)
|
||||
|
||||
def test_set_enabled_unlocks_user(self):
|
||||
with self.make_request():
|
||||
# lockout user
|
||||
self._fail_auth_repeatedly(self.user['id'])
|
||||
self.assertRaises(exception.AccountLocked,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=self.user['id'],
|
||||
password=uuid.uuid4().hex)
|
||||
# set enabled, user should be unlocked
|
||||
self.user['enabled'] = True
|
||||
PROVIDERS.identity_api.update_user(self.user['id'], self.user)
|
||||
user_ret = PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=self.user['id'],
|
||||
user_id=self.user['id'],
|
||||
password=self.password
|
||||
)
|
||||
self.assertTrue(user_ret['enabled'])
|
||||
@ -632,37 +633,37 @@ class LockingOutUserTests(test_backend_sql.SqlTests):
|
||||
def test_lockout_duration(self):
|
||||
# freeze time
|
||||
with freezegun.freeze_time(datetime.datetime.utcnow()) as frozen_time:
|
||||
with self.make_request():
|
||||
# lockout user
|
||||
self._fail_auth_repeatedly(self.user['id'])
|
||||
self.assertRaises(exception.AccountLocked,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=self.user['id'],
|
||||
password=uuid.uuid4().hex)
|
||||
# freeze time past the duration, user should be unlocked and failed
|
||||
# auth count should get reset
|
||||
# freeze time past the duration, user should be unlocked and
|
||||
# failed auth count should get reset
|
||||
frozen_time.tick(delta=datetime.timedelta(
|
||||
seconds=CONF.security_compliance.lockout_duration + 1))
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=self.user['id'],
|
||||
user_id=self.user['id'],
|
||||
password=self.password
|
||||
)
|
||||
# test failed auth count was reset by authenticating with the wrong
|
||||
# password, should raise an assertion error and not account locked
|
||||
# test failed auth count was reset by authenticating with the
|
||||
# wrong password, should raise an assertion error and not
|
||||
# account locked
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=self.user['id'],
|
||||
password=uuid.uuid4().hex)
|
||||
|
||||
def test_lockout_duration_failed_auth_cnt_resets(self):
|
||||
# freeze time
|
||||
with freezegun.freeze_time(datetime.datetime.utcnow()) as frozen_time:
|
||||
with self.make_request():
|
||||
# lockout user
|
||||
self._fail_auth_repeatedly(self.user['id'])
|
||||
self.assertRaises(exception.AccountLocked,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=self.user['id'],
|
||||
password=uuid.uuid4().hex)
|
||||
# freeze time past the duration, failed_auth_cnt should reset
|
||||
@ -673,16 +674,15 @@ class LockingOutUserTests(test_backend_sql.SqlTests):
|
||||
# test user account is locked
|
||||
self.assertRaises(exception.AccountLocked,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=self.user['id'],
|
||||
password=uuid.uuid4().hex)
|
||||
|
||||
def _fail_auth_repeatedly(self, user_id):
|
||||
wrong_password = uuid.uuid4().hex
|
||||
for _ in range(CONF.security_compliance.lockout_failure_attempts):
|
||||
with self.make_request():
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user_id,
|
||||
password=wrong_password)
|
||||
|
||||
@ -705,9 +705,9 @@ class PasswordExpiresValidationTests(test_backend_sql.SqlTests):
|
||||
)
|
||||
user = self._create_user(self.user_dict, password_created_at)
|
||||
# test password is expired
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordExpired,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password=self.password)
|
||||
|
||||
@ -720,8 +720,9 @@ class PasswordExpiresValidationTests(test_backend_sql.SqlTests):
|
||||
)
|
||||
user = self._create_user(self.user_dict, password_created_at)
|
||||
# test password is not expired
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=user['id'], password=self.password
|
||||
user_id=user['id'], password=self.password
|
||||
)
|
||||
|
||||
def test_authenticate_with_expired_password_for_ignore_user_option(self):
|
||||
@ -735,9 +736,9 @@ class PasswordExpiresValidationTests(test_backend_sql.SqlTests):
|
||||
days=CONF.security_compliance.password_expires_days + 1)
|
||||
)
|
||||
user = self._create_user(self.user_dict, password_created_at)
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordExpired,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password=self.password)
|
||||
|
||||
@ -749,7 +750,7 @@ class PasswordExpiresValidationTests(test_backend_sql.SqlTests):
|
||||
)
|
||||
# test password is not expired due to ignore option
|
||||
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):
|
||||
@ -790,9 +791,9 @@ class MinimumPasswordAgeTests(test_backend_sql.SqlTests):
|
||||
self.assertValidChangePassword(self.user['id'], self.initial_password,
|
||||
new_password)
|
||||
# user cannot change password before min age
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordAgeValidationError,
|
||||
PROVIDERS.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=self.user['id'],
|
||||
original_password=new_password,
|
||||
new_password=uuid.uuid4().hex)
|
||||
@ -818,9 +819,10 @@ class MinimumPasswordAgeTests(test_backend_sql.SqlTests):
|
||||
self.assertValidChangePassword(self.user['id'], self.initial_password,
|
||||
new_password)
|
||||
# user cannot change password before min age
|
||||
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordAgeValidationError,
|
||||
PROVIDERS.identity_api.change_password,
|
||||
self.make_request(),
|
||||
user_id=self.user['id'],
|
||||
original_password=new_password,
|
||||
new_password=uuid.uuid4().hex)
|
||||
@ -833,12 +835,13 @@ class MinimumPasswordAgeTests(test_backend_sql.SqlTests):
|
||||
uuid.uuid4().hex)
|
||||
|
||||
def assertValidChangePassword(self, user_id, password, new_password):
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.change_password(
|
||||
self.make_request(), user_id=user_id, original_password=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
|
||||
user_id=user_id, password=new_password
|
||||
)
|
||||
|
||||
def _create_new_user(self, password):
|
||||
@ -881,15 +884,16 @@ class ChangePasswordRequiredAfterFirstUse(test_backend_sql.SqlTests):
|
||||
return PROVIDERS.identity_api.create_user(user_dict)
|
||||
|
||||
def assertPasswordIsExpired(self, user_id, password):
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.PasswordExpired,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user_id,
|
||||
password=password)
|
||||
|
||||
def assertPasswordIsNotExpired(self, user_id, password):
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=user_id, password=password
|
||||
user_id=user_id, password=password
|
||||
)
|
||||
|
||||
def test_password_expired_after_create(self):
|
||||
@ -899,8 +903,9 @@ class ChangePasswordRequiredAfterFirstUse(test_backend_sql.SqlTests):
|
||||
self.assertPasswordIsExpired(user['id'], initial_password)
|
||||
# change password (self-service), password not expired
|
||||
new_password = uuid.uuid4().hex
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.change_password(
|
||||
self.make_request(), user['id'], initial_password, new_password
|
||||
user['id'], initial_password, new_password
|
||||
)
|
||||
self.assertPasswordIsNotExpired(user['id'], new_password)
|
||||
|
||||
@ -920,8 +925,9 @@ class ChangePasswordRequiredAfterFirstUse(test_backend_sql.SqlTests):
|
||||
self.assertPasswordIsExpired(user['id'], admin_password)
|
||||
# change password (self-service), password not expired
|
||||
new_password = uuid.uuid4().hex
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.change_password(
|
||||
self.make_request(), user['id'], admin_password, new_password
|
||||
user['id'], admin_password, new_password
|
||||
)
|
||||
self.assertPasswordIsNotExpired(user['id'], new_password)
|
||||
|
||||
|
@ -43,22 +43,22 @@ class IdentityTests(object):
|
||||
return domain_id
|
||||
|
||||
def test_authenticate_bad_user(self):
|
||||
with self.make_request():
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=uuid.uuid4().hex,
|
||||
password=self.user_foo['password'])
|
||||
|
||||
def test_authenticate_bad_password(self):
|
||||
with self.make_request():
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=self.user_foo['id'],
|
||||
password=uuid.uuid4().hex)
|
||||
|
||||
def test_authenticate(self):
|
||||
with self.make_request():
|
||||
user_ref = PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user_id=self.user_sna['id'],
|
||||
password=self.user_sna['password'])
|
||||
# NOTE(termie): the password field is left in user_sna to make
|
||||
@ -83,8 +83,8 @@ class IdentityTests(object):
|
||||
PROVIDERS.assignment_api.add_role_to_user_and_project(
|
||||
new_user['id'], self.tenant_baz['id'], role_member['id']
|
||||
)
|
||||
with self.make_request():
|
||||
user_ref = PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user_id=new_user['id'],
|
||||
password=user['password'])
|
||||
self.assertNotIn('password', user_ref)
|
||||
@ -103,9 +103,9 @@ class IdentityTests(object):
|
||||
user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id)
|
||||
PROVIDERS.identity_api.create_user(user)
|
||||
|
||||
with self.make_request():
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=id_,
|
||||
password='password')
|
||||
|
||||
@ -394,14 +394,13 @@ class IdentityTests(object):
|
||||
PROVIDERS.identity_api.get_user(user['id'])
|
||||
# Make sure the user is not allowed to login
|
||||
# with a password that is empty string or None
|
||||
with self.make_request():
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password='')
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password=None)
|
||||
|
||||
@ -412,14 +411,13 @@ class IdentityTests(object):
|
||||
PROVIDERS.identity_api.get_user(user['id'])
|
||||
# Make sure the user is not allowed to login
|
||||
# with a password that is empty string or None
|
||||
with self.make_request():
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password='')
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password=None)
|
||||
|
||||
|
@ -15,6 +15,7 @@ import uuid
|
||||
import fixtures
|
||||
import flask
|
||||
import flask_restful
|
||||
import functools
|
||||
from oslo_policy import policy
|
||||
from oslo_serialization import jsonutils
|
||||
from testtools import matchers
|
||||
@ -402,11 +403,19 @@ class TestKeystoneFlaskCommon(rest.RestfulTestCase):
|
||||
expected_status_code=420)
|
||||
|
||||
def test_construct_resource_map(self):
|
||||
resource_name = 'arguments'
|
||||
param_relation = json_home.build_v3_parameter_relation(
|
||||
'argument_id')
|
||||
alt_rel_func = functools.partial(
|
||||
json_home.build_v3_extension_resource_relation,
|
||||
extension_name='extension', extension_version='1.0')
|
||||
url = '/v3/arguments/<string:argument_id>'
|
||||
old_url = ['/v3/old_arguments/<string:argument_id>']
|
||||
resource_name = 'arguments'
|
||||
old_url = [dict(
|
||||
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(
|
||||
resource=_TestResourceWithCollectionInfo,
|
||||
@ -420,13 +429,17 @@ class TestKeystoneFlaskCommon(rest.RestfulTestCase):
|
||||
self.assertEqual(_TestResourceWithCollectionInfo,
|
||||
mapping.resource)
|
||||
self.assertEqual(url, mapping.url)
|
||||
self.assertEqual(old_url, mapping.alternate_urls)
|
||||
self.assertEqual(json_home.build_v3_resource_relation(resource_name),
|
||||
mapping.json_home_data.rel)
|
||||
self.assertEqual(json_home.Status.EXPERIMENTAL,
|
||||
mapping.json_home_data.status)
|
||||
self.assertEqual({'argument_id': param_relation},
|
||||
mapping.json_home_data.path_vars)
|
||||
# Check the alternate URL data is populated sanely
|
||||
self.assertEqual(1, len(mapping.alternate_urls))
|
||||
alt_url_data = mapping.alternate_urls[0]
|
||||
self.assertEqual(old_url[0]['url'], alt_url_data['url'])
|
||||
self.assertEqual(old_url[0]['json_home'], alt_url_data['json_home'])
|
||||
|
||||
def test_instantiate_and_register_to_app(self):
|
||||
# Test that automatic instantiation and registration to app works.
|
||||
|
@ -17,6 +17,7 @@ import uuid
|
||||
import mock
|
||||
import stevedore
|
||||
|
||||
from keystone.api._shared import authentication
|
||||
from keystone import auth
|
||||
from keystone.auth.plugins import base
|
||||
from keystone.auth.plugins import mapped
|
||||
@ -32,7 +33,7 @@ DEMO_USER_ID = uuid.uuid4().hex
|
||||
|
||||
|
||||
class SimpleChallengeResponse(base.AuthMethodHandler):
|
||||
def authenticate(self, context, auth_payload):
|
||||
def authenticate(self, auth_payload):
|
||||
response_data = {}
|
||||
if 'response' in auth_payload:
|
||||
if auth_payload['response'] != EXPECTED_RESPONSE:
|
||||
@ -50,9 +51,6 @@ class SimpleChallengeResponse(base.AuthMethodHandler):
|
||||
|
||||
|
||||
class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase):
|
||||
def setUp(self):
|
||||
super(TestAuthPlugin, self).setUp()
|
||||
self.api = auth.controllers.Auth()
|
||||
|
||||
def test_unsupported_auth_method(self):
|
||||
method_name = uuid.uuid4().hex
|
||||
@ -85,7 +83,8 @@ class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase):
|
||||
auth_info = auth.core.AuthInfo.create(auth_data)
|
||||
auth_context = auth.core.AuthContext(method_names=[])
|
||||
try:
|
||||
self.api.authenticate(self.make_request(), auth_info, auth_context)
|
||||
with self.make_request():
|
||||
authentication.authenticate(auth_info, auth_context)
|
||||
except exception.AdditionalAuthRequired as e:
|
||||
self.assertIn('methods', e.authentication)
|
||||
self.assertIn(METHOD_NAME, e.authentication['methods'])
|
||||
@ -99,7 +98,8 @@ class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase):
|
||||
auth_data = {'identity': auth_data}
|
||||
auth_info = auth.core.AuthInfo.create(auth_data)
|
||||
auth_context = auth.core.AuthContext(method_names=[])
|
||||
self.api.authenticate(self.make_request(), auth_info, auth_context)
|
||||
with self.make_request():
|
||||
authentication.authenticate(auth_info, auth_context)
|
||||
self.assertEqual(DEMO_USER_ID, auth_context['user_id'])
|
||||
|
||||
# test incorrect response
|
||||
@ -109,9 +109,9 @@ class TestAuthPlugin(unit.SQLDriverOverrides, unit.TestCase):
|
||||
auth_data = {'identity': auth_data}
|
||||
auth_info = auth.core.AuthInfo.create(auth_data)
|
||||
auth_context = auth.core.AuthContext(method_names=[])
|
||||
with self.make_request():
|
||||
self.assertRaises(exception.Unauthorized,
|
||||
self.api.authenticate,
|
||||
self.make_request(),
|
||||
authentication.authenticate,
|
||||
auth_info,
|
||||
auth_context)
|
||||
|
||||
@ -138,9 +138,6 @@ class TestAuthPluginDynamicOptions(TestAuthPlugin):
|
||||
|
||||
|
||||
class TestMapped(unit.TestCase):
|
||||
def setUp(self):
|
||||
super(TestMapped, self).setUp()
|
||||
self.api = auth.controllers.Auth()
|
||||
|
||||
def config_files(self):
|
||||
config_files = super(TestMapped, self).config_files()
|
||||
@ -151,7 +148,6 @@ class TestMapped(unit.TestCase):
|
||||
with mock.patch.object(auth.plugins.mapped.Mapped,
|
||||
'authenticate',
|
||||
return_value=None) as authenticate:
|
||||
request = self.make_request()
|
||||
auth_data = {
|
||||
'identity': {
|
||||
'methods': [method_name],
|
||||
@ -162,10 +158,10 @@ class TestMapped(unit.TestCase):
|
||||
auth_context = auth.core.AuthContext(
|
||||
method_names=[],
|
||||
user_id=uuid.uuid4().hex)
|
||||
self.api.authenticate(request, auth_info, auth_context)
|
||||
with self.make_request():
|
||||
authentication.authenticate(auth_info, auth_context)
|
||||
# make sure Mapped plugin got invoked with the correct payload
|
||||
((context, auth_payload),
|
||||
kwargs) = authenticate.call_args
|
||||
((auth_payload,), kwargs) = authenticate.call_args
|
||||
self.assertEqual(method_name, auth_payload['protocol'])
|
||||
|
||||
def test_mapped_with_remote_user(self):
|
||||
@ -186,11 +182,10 @@ class TestMapped(unit.TestCase):
|
||||
'authenticate',
|
||||
return_value=None) as authenticate:
|
||||
auth_info = auth.core.AuthInfo.create(auth_data)
|
||||
request = self.make_request(environ={'REMOTE_USER': 'foo@idp.com'})
|
||||
self.api.authenticate(request, auth_info, auth_context)
|
||||
with self.make_request(environ={'REMOTE_USER': 'foo@idp.com'}):
|
||||
authentication.authenticate(auth_info, auth_context)
|
||||
# make sure Mapped plugin got invoked with the correct payload
|
||||
((context, auth_payload),
|
||||
kwargs) = authenticate.call_args
|
||||
((auth_payload,), kwargs) = authenticate.call_args
|
||||
self.assertEqual(method_name, auth_payload['protocol'])
|
||||
|
||||
@mock.patch('keystone.auth.plugins.mapped.PROVIDERS')
|
||||
@ -203,15 +198,18 @@ class TestMapped(unit.TestCase):
|
||||
mock_providers.role_api = mock.Mock()
|
||||
|
||||
test_mapped = mapped.Mapped()
|
||||
request = self.make_request()
|
||||
|
||||
auth_payload = {'identity_provider': 'test_provider'}
|
||||
self.assertRaises(exception.ValidationError, test_mapped.authenticate,
|
||||
request, auth_payload)
|
||||
with self.make_request():
|
||||
self.assertRaises(
|
||||
exception.ValidationError, test_mapped.authenticate,
|
||||
auth_payload)
|
||||
|
||||
auth_payload = {'protocol': 'saml2'}
|
||||
self.assertRaises(exception.ValidationError, test_mapped.authenticate,
|
||||
request, auth_payload)
|
||||
with self.make_request():
|
||||
self.assertRaises(
|
||||
exception.ValidationError, test_mapped.authenticate,
|
||||
auth_payload)
|
||||
|
||||
def test_supporting_multiple_methods(self):
|
||||
method_names = ('saml2', 'openid', 'x509', 'mapped')
|
||||
|
@ -765,9 +765,9 @@ class BaseLDAPIdentity(LDAPTestSetup, IdentityTests, AssignmentTests,
|
||||
driver.user.LDAP_USER = None
|
||||
driver.user.LDAP_PASSWORD = None
|
||||
|
||||
with self.make_request():
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password=None)
|
||||
|
||||
@ -1988,8 +1988,8 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity, unit.TestCase):
|
||||
driver = PROVIDERS.identity_api._select_identity_driver(
|
||||
CONF.identity.default_domain_id)
|
||||
driver.user.enabled_emulation_dn = 'cn=test,dc=test'
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user_id=self.user_foo['id'],
|
||||
password=self.user_foo['password'])
|
||||
|
||||
@ -2334,8 +2334,8 @@ class BaseMultiLDAPandSQLIdentity(object):
|
||||
|
||||
for user_num in range(self.domain_count):
|
||||
user = 'user%s' % user_num
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user_id=users[user]['id'],
|
||||
password=users[user]['password'])
|
||||
|
||||
|
@ -176,8 +176,8 @@ class LdapPoolCommonTestMixin(object):
|
||||
|
||||
# authenticate so that connection is added to pool before password
|
||||
# change
|
||||
with self.make_request():
|
||||
user_ref = PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user_id=self.user_sna['id'],
|
||||
password=self.user_sna['password'])
|
||||
|
||||
@ -191,8 +191,8 @@ class LdapPoolCommonTestMixin(object):
|
||||
|
||||
# now authenticate again to make sure new password works with
|
||||
# connection pool
|
||||
with self.make_request():
|
||||
user_ref2 = PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user_id=self.user_sna['id'],
|
||||
password=new_password)
|
||||
|
||||
@ -202,9 +202,9 @@ class LdapPoolCommonTestMixin(object):
|
||||
# Authentication with old password would not work here as there
|
||||
# is only one connection in pool which get bind again with updated
|
||||
# password..so no old bind is maintained in this case.
|
||||
with self.make_request():
|
||||
self.assertRaises(AssertionError,
|
||||
PROVIDERS.identity_api.authenticate,
|
||||
self.make_request(),
|
||||
user_id=self.user_sna['id'],
|
||||
password=old_password)
|
||||
|
||||
|
@ -150,8 +150,8 @@ class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase):
|
||||
self.assertEqual(system_roles[0]['id'], admin_role['id'])
|
||||
# NOTE(morganfainberg): Pass an empty context, it isn't used by
|
||||
# `authenticate` method.
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user['id'],
|
||||
bootstrap.password)
|
||||
|
||||
@ -284,8 +284,8 @@ class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase):
|
||||
self._do_test_bootstrap(self.bootstrap)
|
||||
|
||||
# Sanity check that the original password works again.
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user_id,
|
||||
self.bootstrap.password)
|
||||
|
||||
|
@ -109,8 +109,8 @@ class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin,
|
||||
CONF.identity.default_domain_id,
|
||||
password=password)
|
||||
|
||||
with self.make_request():
|
||||
PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(),
|
||||
user_id=user['id'],
|
||||
password=password)
|
||||
|
||||
@ -179,8 +179,9 @@ class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin,
|
||||
# successfully which is not desired if password change is frequent
|
||||
# use case in a deployment.
|
||||
# This can happen in multiple concurrent connections case only.
|
||||
with self.make_request():
|
||||
user_ref = PROVIDERS.identity_api.authenticate(
|
||||
self.make_request(), user_id=user['id'], password=old_password)
|
||||
user_id=user['id'], password=old_password)
|
||||
|
||||
self.assertDictEqual(user, user_ref)
|
||||
|
||||
|
@ -21,7 +21,6 @@ from six.moves import http_client
|
||||
from testtools import matchers
|
||||
import webtest
|
||||
|
||||
from keystone import auth
|
||||
from keystone.common import authorization
|
||||
from keystone.common import cache
|
||||
from keystone.common import provider_api
|
||||
@ -1215,18 +1214,6 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase,
|
||||
for attribute in attributes:
|
||||
self.assertIsNotNone(entity.get(attribute))
|
||||
|
||||
def build_external_auth_request(self, remote_user,
|
||||
remote_domain=None, auth_data=None,
|
||||
kerberos=False):
|
||||
environment = self.build_external_auth_environ(
|
||||
remote_user, remote_domain)
|
||||
if not auth_data:
|
||||
auth_data = self.build_authentication_request(
|
||||
kerberos=kerberos)['auth']
|
||||
auth_info = auth.core.AuthInfo.create(auth_data)
|
||||
auth_context = auth.core.AuthContext(method_names=[])
|
||||
return self.make_request(environ=environment), auth_info, auth_context
|
||||
|
||||
def build_external_auth_environ(self, remote_user, remote_domain=None):
|
||||
environment = {'REMOTE_USER': remote_user, 'AUTH_TYPE': 'Negotiate'}
|
||||
if remote_domain:
|
||||
|
@ -19,6 +19,7 @@ from testtools import matchers
|
||||
import uuid
|
||||
|
||||
import fixtures
|
||||
import flask
|
||||
from lxml import etree
|
||||
import mock
|
||||
from oslo_serialization import jsonutils
|
||||
@ -32,12 +33,12 @@ xmldsig = importutils.try_import("saml2.xmldsig")
|
||||
if not xmldsig:
|
||||
xmldsig = importutils.try_import("xmldsig")
|
||||
|
||||
from keystone.auth import controllers as auth_controllers
|
||||
from keystone.common import controller
|
||||
from keystone.api._shared import authentication
|
||||
from keystone.api import auth as auth_api
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import render_token
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
from keystone.federation import controllers as federation_controllers
|
||||
from keystone.federation import idp as keystone_idp
|
||||
from keystone.models import token_model
|
||||
from keystone import notifications
|
||||
@ -149,13 +150,13 @@ class FederatedSetupMixin(object):
|
||||
idp=None,
|
||||
assertion='EMPLOYEE_ASSERTION',
|
||||
environment=None):
|
||||
api = federation_controllers.Auth()
|
||||
environment = environment or {}
|
||||
environment.update(getattr(mapping_fixtures, assertion))
|
||||
request = self.make_request(environ=environment)
|
||||
with self.make_request(environ=environment):
|
||||
if idp is None:
|
||||
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
|
||||
|
||||
def idp_ref(self, id=None):
|
||||
@ -198,9 +199,9 @@ class FederatedSetupMixin(object):
|
||||
}
|
||||
}
|
||||
|
||||
def _inject_assertion(self, request, variant):
|
||||
def _inject_assertion(self, variant):
|
||||
assertion = getattr(mapping_fixtures, variant)
|
||||
request.context_dict['environment'].update(assertion)
|
||||
flask.request.environ.update(assertion)
|
||||
|
||||
def load_federation_sample_data(self):
|
||||
"""Inject additional data."""
|
||||
@ -759,24 +760,25 @@ class FederatedSetupMixin(object):
|
||||
PROVIDERS.federation_api.create_protocol(
|
||||
self.idp_with_remote['id'], self.proto_saml['id'], self.proto_saml
|
||||
)
|
||||
# Generate fake tokens
|
||||
request = self.make_request()
|
||||
|
||||
with self.make_request():
|
||||
self.tokens = {}
|
||||
VARIANTS = ('EMPLOYEE_ASSERTION', 'CUSTOMER_ASSERTION',
|
||||
'ADMIN_ASSERTION')
|
||||
api = auth_controllers.Auth()
|
||||
for variant in VARIANTS:
|
||||
self._inject_assertion(request, variant)
|
||||
r = api.authenticate_for_token(request, self.UNSCOPED_V3_SAML2_REQ)
|
||||
self.tokens[variant] = r.headers.get('X-Subject-Token')
|
||||
self._inject_assertion(variant)
|
||||
r = authentication.authenticate_for_token(
|
||||
self.UNSCOPED_V3_SAML2_REQ)
|
||||
self.tokens[variant] = r.id
|
||||
|
||||
self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN = self._scope_request(
|
||||
uuid.uuid4().hex, 'project', self.proj_customers['id'])
|
||||
self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN = (
|
||||
self._scope_request(
|
||||
uuid.uuid4().hex, 'project', self.proj_customers['id']))
|
||||
|
||||
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE = self._scope_request(
|
||||
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE = (
|
||||
self._scope_request(
|
||||
self.tokens['EMPLOYEE_ASSERTION'], 'project',
|
||||
self.proj_employees['id'])
|
||||
self.proj_employees['id']))
|
||||
|
||||
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN = self._scope_request(
|
||||
self.tokens['ADMIN_ASSERTION'], 'project',
|
||||
@ -786,23 +788,27 @@ class FederatedSetupMixin(object):
|
||||
self.tokens['ADMIN_ASSERTION'], 'project',
|
||||
self.proj_customers['id'])
|
||||
|
||||
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER = self._scope_request(
|
||||
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER = (
|
||||
self._scope_request(
|
||||
self.tokens['CUSTOMER_ASSERTION'], 'project',
|
||||
self.proj_employees['id'])
|
||||
self.proj_employees['id']))
|
||||
|
||||
self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER = self._scope_request(
|
||||
self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER = (
|
||||
self._scope_request(
|
||||
self.tokens['CUSTOMER_ASSERTION'], 'project',
|
||||
self.project_inherited['id'])
|
||||
self.project_inherited['id']))
|
||||
|
||||
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.tokens['CUSTOMER_ASSERTION'], 'domain',
|
||||
self.domainB['id'])
|
||||
|
||||
self.TOKEN_SCOPE_DOMAIN_D_FROM_CUSTOMER = self._scope_request(
|
||||
self.tokens['CUSTOMER_ASSERTION'], 'domain', self.domainD['id'])
|
||||
self.tokens['CUSTOMER_ASSERTION'], 'domain',
|
||||
self.domainD['id'])
|
||||
|
||||
self.TOKEN_SCOPE_DOMAIN_A_FROM_ADMIN = self._scope_request(
|
||||
self.tokens['ADMIN_ASSERTION'], 'domain', self.domainA['id'])
|
||||
@ -1866,7 +1872,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
super(FederatedTokenTests, self).setUp()
|
||||
self._notifications = []
|
||||
|
||||
def fake_saml_notify(action, request, user_id, group_ids,
|
||||
def fake_saml_notify(action, user_id, group_ids,
|
||||
identity_provider, protocol, token_id, outcome):
|
||||
note = {
|
||||
'action': action,
|
||||
@ -1902,12 +1908,12 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
def test_issue_unscoped_token(self):
|
||||
r = self._issue_unscoped_token()
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
self.assertValidMappedUser(r.json['token'])
|
||||
token_resp = render_token.render_token_response_from_model(r)['token']
|
||||
self.assertValidMappedUser(token_resp)
|
||||
|
||||
def test_issue_the_same_unscoped_token_with_user_deleted(self):
|
||||
r = self._issue_unscoped_token()
|
||||
token = r.json['token']
|
||||
token = render_token.render_token_response_from_model(r)['token']
|
||||
user1 = token['user']
|
||||
user_id1 = user1.pop('id')
|
||||
|
||||
@ -1916,7 +1922,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
PROVIDERS.identity_api.delete_user(user_id1)
|
||||
|
||||
r = self._issue_unscoped_token()
|
||||
token = r.json['token']
|
||||
token = render_token.render_token_response_from_model(r)['token']
|
||||
user2 = token['user']
|
||||
user_id2 = user2.pop('id')
|
||||
|
||||
@ -1942,42 +1948,37 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
def test_issue_unscoped_token_group_names_in_mapping(self):
|
||||
r = self._issue_unscoped_token(assertion='ANOTHER_CUSTOMER_ASSERTION')
|
||||
ref_groups = set([self.group_customers['id'], self.group_admins['id']])
|
||||
token_resp = r.json_body
|
||||
token_groups = token_resp['token']['user']['OS-FEDERATION']['groups']
|
||||
token_groups = r.federated_groups
|
||||
token_groups = set([group['id'] for group in token_groups])
|
||||
self.assertEqual(ref_groups, token_groups)
|
||||
|
||||
def test_issue_unscoped_tokens_nonexisting_group(self):
|
||||
r = self._issue_unscoped_token(assertion='ANOTHER_TESTER_ASSERTION')
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
self._issue_unscoped_token(assertion='ANOTHER_TESTER_ASSERTION')
|
||||
|
||||
def test_issue_unscoped_token_with_remote_no_attribute(self):
|
||||
r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
|
||||
self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
|
||||
environment={
|
||||
self.REMOTE_ID_ATTR:
|
||||
self.REMOTE_IDS[0]
|
||||
})
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
|
||||
def test_issue_unscoped_token_with_remote(self):
|
||||
self.config_fixture.config(group='federation',
|
||||
remote_id_attribute=self.REMOTE_ID_ATTR)
|
||||
r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
|
||||
self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
|
||||
environment={
|
||||
self.REMOTE_ID_ATTR:
|
||||
self.REMOTE_IDS[0]
|
||||
})
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
|
||||
def test_issue_unscoped_token_with_saml2_remote(self):
|
||||
self.config_fixture.config(group='saml2',
|
||||
remote_id_attribute=self.REMOTE_ID_ATTR)
|
||||
r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
|
||||
self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
|
||||
environment={
|
||||
self.REMOTE_ID_ATTR:
|
||||
self.REMOTE_IDS[0]
|
||||
})
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
|
||||
def test_issue_unscoped_token_with_remote_different(self):
|
||||
self.config_fixture.config(group='federation',
|
||||
@ -2001,12 +2002,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
remote_id_attribute=self.REMOTE_ID_ATTR)
|
||||
self.config_fixture.config(group='federation',
|
||||
remote_id_attribute=uuid.uuid4().hex)
|
||||
r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
|
||||
self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
|
||||
environment={
|
||||
self.REMOTE_ID_ATTR:
|
||||
self.REMOTE_IDS[0]
|
||||
})
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
|
||||
def test_issue_unscoped_token_with_remote_unavailable(self):
|
||||
self.config_fixture.config(group='federation',
|
||||
@ -2020,14 +2020,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
def test_issue_unscoped_token_with_remote_user_as_empty_string(self):
|
||||
# make sure that REMOTE_USER set as the empty string won't interfere
|
||||
r = self._issue_unscoped_token(environment={'REMOTE_USER': ''})
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
self._issue_unscoped_token(environment={'REMOTE_USER': ''})
|
||||
|
||||
def test_issue_unscoped_token_no_groups(self):
|
||||
r = self._issue_unscoped_token(assertion='USER_NO_GROUPS_ASSERTION')
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
token_resp = r.json_body
|
||||
token_groups = token_resp['token']['user']['OS-FEDERATION']['groups']
|
||||
token_groups = r.federated_groups
|
||||
self.assertEqual(0, len(token_groups))
|
||||
|
||||
def test_issue_scoped_token_no_groups(self):
|
||||
@ -2037,11 +2034,9 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
"""
|
||||
# issue unscoped token with no groups
|
||||
r = self._issue_unscoped_token(assertion='USER_NO_GROUPS_ASSERTION')
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
token_resp = r.json_body
|
||||
token_groups = token_resp['token']['user']['OS-FEDERATION']['groups']
|
||||
token_groups = r.federated_groups
|
||||
self.assertEqual(0, len(token_groups))
|
||||
unscoped_token = r.headers.get('X-Subject-Token')
|
||||
unscoped_token = r.id
|
||||
|
||||
# let admin get roles in a project
|
||||
self.proj_employees
|
||||
@ -2068,16 +2063,14 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
non string objects and return token id in the HTTP header.
|
||||
|
||||
"""
|
||||
api = auth_controllers.Auth()
|
||||
environ = {
|
||||
'malformed_object': object(),
|
||||
'another_bad_idea': tuple(range(10)),
|
||||
'yet_another_bad_param': dict(zip(uuid.uuid4().hex, range(32)))
|
||||
}
|
||||
environ.update(mapping_fixtures.EMPLOYEE_ASSERTION)
|
||||
request = self.make_request(environ=environ)
|
||||
r = api.authenticate_for_token(request, self.UNSCOPED_V3_SAML2_REQ)
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
with self.make_request(environ=environ):
|
||||
authentication.authenticate_for_token(self.UNSCOPED_V3_SAML2_REQ)
|
||||
|
||||
def test_scope_to_project_once_notify(self):
|
||||
r = self.v3_create_token(
|
||||
@ -2208,12 +2201,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
expected_status=http_client.NOT_FOUND)
|
||||
|
||||
def test_issue_token_from_rules_without_user(self):
|
||||
api = auth_controllers.Auth()
|
||||
environ = copy.deepcopy(mapping_fixtures.BAD_TESTER_ASSERTION)
|
||||
request = self.make_request(environ=environ)
|
||||
with self.make_request(environ=environ):
|
||||
self.assertRaises(exception.Unauthorized,
|
||||
api.authenticate_for_token,
|
||||
request, self.UNSCOPED_V3_SAML2_REQ)
|
||||
authentication.authenticate_for_token,
|
||||
self.UNSCOPED_V3_SAML2_REQ)
|
||||
|
||||
def test_issue_token_with_nonexistent_group(self):
|
||||
"""Inject assertion that matches rule issuing bad group id.
|
||||
@ -2356,11 +2348,11 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
"""
|
||||
r = self._issue_unscoped_token()
|
||||
token_resp = r.json_body['token']
|
||||
token_resp = render_token.render_token_response_from_model(r)['token']
|
||||
# NOTE(lbragstad): Ensure only 'saml2' is in the method list.
|
||||
self.assertListEqual(['saml2'], token_resp['methods'])
|
||||
self.assertListEqual(['saml2'], r.methods)
|
||||
self.assertValidMappedUser(token_resp)
|
||||
employee_unscoped_token_id = r.headers.get('X-Subject-Token')
|
||||
employee_unscoped_token_id = r.id
|
||||
r = self.get('/auth/projects', token=employee_unscoped_token_id)
|
||||
projects = r.result['projects']
|
||||
random_project = random.randint(0, len(projects) - 1)
|
||||
@ -2432,14 +2424,13 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
|
||||
|
||||
r = self._issue_unscoped_token(assertion='TESTER_ASSERTION')
|
||||
token_id = r.headers.get('X-Subject-Token')
|
||||
|
||||
# delete group
|
||||
PROVIDERS.identity_api.delete_group(group['id'])
|
||||
|
||||
# scope token to project_all, expect HTTP 500
|
||||
scoped_token = self._scope_request(
|
||||
token_id, 'project',
|
||||
r.id, 'project',
|
||||
self.project_all['id'])
|
||||
|
||||
self.v3_create_token(
|
||||
@ -2498,7 +2489,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
}
|
||||
PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
|
||||
r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION')
|
||||
assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups']
|
||||
assigned_group_ids = r.federated_groups
|
||||
self.assertEqual(1, len(assigned_group_ids))
|
||||
self.assertEqual(group['id'], assigned_group_ids[0]['id'])
|
||||
|
||||
@ -2571,7 +2562,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
}
|
||||
PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
|
||||
r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION')
|
||||
assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups']
|
||||
assigned_group_ids = r.federated_groups
|
||||
self.assertEqual(len(group_ids), len(assigned_group_ids))
|
||||
for group in assigned_group_ids:
|
||||
self.assertIn(group['id'], group_ids)
|
||||
@ -2644,7 +2635,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
}
|
||||
PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
|
||||
r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION')
|
||||
assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups']
|
||||
assigned_group_ids = r.federated_groups
|
||||
self.assertEqual(len(group_ids), len(assigned_group_ids))
|
||||
for group in assigned_group_ids:
|
||||
self.assertIn(group['id'], group_ids)
|
||||
@ -2706,7 +2697,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
}
|
||||
PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
|
||||
r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION')
|
||||
assigned_groups = r.json['token']['user']['OS-FEDERATION']['groups']
|
||||
assigned_groups = r.federated_groups
|
||||
self.assertEqual(len(assigned_groups), 0)
|
||||
|
||||
def test_not_setting_whitelist_accepts_all_values(self):
|
||||
@ -2776,7 +2767,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
}
|
||||
PROVIDERS.federation_api.update_mapping(self.mapping['id'], rules)
|
||||
r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION')
|
||||
assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups']
|
||||
assigned_group_ids = r.federated_groups
|
||||
self.assertEqual(len(group_ids), len(assigned_group_ids))
|
||||
for group in assigned_group_ids:
|
||||
self.assertIn(group['id'], group_ids)
|
||||
@ -2791,8 +2782,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
"""
|
||||
self.config_fixture.config(group='federation',
|
||||
assertion_prefix=self.ASSERTION_PREFIX)
|
||||
r = self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION_PREFIXED')
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION_PREFIXED')
|
||||
|
||||
def test_assertion_prefix_parameter_expect_fail(self):
|
||||
"""Test parameters filtering based on the prefix.
|
||||
@ -2804,8 +2794,7 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
Expect server to raise exception.Unathorized exception.
|
||||
|
||||
"""
|
||||
r = self._issue_unscoped_token()
|
||||
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
|
||||
self._issue_unscoped_token()
|
||||
self.config_fixture.config(group='federation',
|
||||
assertion_prefix='UserName')
|
||||
|
||||
@ -2814,23 +2803,24 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
def test_unscoped_token_has_user_domain(self):
|
||||
r = self._issue_unscoped_token()
|
||||
self._check_domains_are_valid(r.json_body['token'])
|
||||
self._check_domains_are_valid(
|
||||
render_token.render_token_response_from_model(r)['token'])
|
||||
|
||||
def test_scoped_token_has_user_domain(self):
|
||||
r = self.v3_create_token(
|
||||
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE)
|
||||
self._check_domains_are_valid(r.result['token'])
|
||||
self._check_domains_are_valid(r.json_body['token'])
|
||||
|
||||
def test_issue_unscoped_token_for_local_user(self):
|
||||
r = self._issue_unscoped_token(assertion='LOCAL_USER_ASSERTION')
|
||||
token_resp = r.json_body['token']
|
||||
self.assertListEqual(['saml2'], token_resp['methods'])
|
||||
self.assertEqual(self.user['id'], token_resp['user']['id'])
|
||||
self.assertEqual(self.user['name'], token_resp['user']['name'])
|
||||
self.assertEqual(self.domain['id'], token_resp['user']['domain']['id'])
|
||||
self.assertListEqual(['saml2'], r.methods)
|
||||
self.assertEqual(self.user['id'], r.user_id)
|
||||
self.assertEqual(self.user['name'], r.user['name'])
|
||||
self.assertEqual(self.domain['id'], r.user_domain['id'])
|
||||
# Make sure the token is not scoped
|
||||
self.assertNotIn('project', token_resp)
|
||||
self.assertNotIn('domain', token_resp)
|
||||
self.assertIsNone(r.domain_id)
|
||||
self.assertIsNone(r.project_id)
|
||||
self.assertTrue(r.unscoped)
|
||||
|
||||
def test_issue_token_for_local_user_user_not_found(self):
|
||||
self.assertRaises(exception.Unauthorized,
|
||||
@ -2839,11 +2829,10 @@ class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
def test_user_name_and_id_in_federation_token(self):
|
||||
r = self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION')
|
||||
token = r.json_body['token']
|
||||
self.assertEqual(
|
||||
mapping_fixtures.EMPLOYEE_ASSERTION['UserName'],
|
||||
token['user']['name'])
|
||||
self.assertNotEqual(token['user']['name'], token['user']['id'])
|
||||
r.user['name'])
|
||||
self.assertNotEqual(r.user['name'], r.user_id)
|
||||
r = self.v3_create_token(
|
||||
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE)
|
||||
token = r.json_body['token']
|
||||
@ -2878,18 +2867,18 @@ class FernetFederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
def test_federated_unscoped_token(self):
|
||||
resp = self._issue_unscoped_token()
|
||||
self.assertEqual(204, len(resp.headers['X-Subject-Token']))
|
||||
self.assertValidMappedUser(resp.json_body['token'])
|
||||
self.assertValidMappedUser(
|
||||
render_token.render_token_response_from_model(resp)['token'])
|
||||
|
||||
def test_federated_unscoped_token_with_multiple_groups(self):
|
||||
assertion = 'ANOTHER_CUSTOMER_ASSERTION'
|
||||
resp = self._issue_unscoped_token(assertion=assertion)
|
||||
self.assertEqual(226, len(resp.headers['X-Subject-Token']))
|
||||
self.assertValidMappedUser(resp.json_body['token'])
|
||||
self.assertValidMappedUser(
|
||||
render_token.render_token_response_from_model(resp)['token'])
|
||||
|
||||
def test_validate_federated_unscoped_token(self):
|
||||
resp = self._issue_unscoped_token()
|
||||
unscoped_token = resp.headers.get('X-Subject-Token')
|
||||
unscoped_token = resp.id
|
||||
# assert that the token we received is valid
|
||||
self.get('/auth/tokens/', headers={'X-Subject-Token': unscoped_token})
|
||||
|
||||
@ -2902,8 +2891,9 @@ class FernetFederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
"""
|
||||
resp = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(resp.json_body['token'])
|
||||
unscoped_token = resp.headers.get('X-Subject-Token')
|
||||
self.assertValidMappedUser(
|
||||
render_token.render_token_response_from_model(resp)['token'])
|
||||
unscoped_token = resp.id
|
||||
resp = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = resp.result['projects']
|
||||
random_project = random.randint(0, len(projects) - 1)
|
||||
@ -2941,11 +2931,11 @@ class FederatedTokenTestsMethodToken(FederatedTokenTests):
|
||||
|
||||
"""
|
||||
r = self._issue_unscoped_token()
|
||||
token_resp = r.json_body['token']
|
||||
token_resp = render_token.render_token_response_from_model(r)['token']
|
||||
# NOTE(lbragstad): Ensure only 'saml2' is in the method list.
|
||||
self.assertListEqual(['saml2'], token_resp['methods'])
|
||||
self.assertListEqual(['saml2'], r.methods)
|
||||
self.assertValidMappedUser(token_resp)
|
||||
employee_unscoped_token_id = r.headers.get('X-Subject-Token')
|
||||
employee_unscoped_token_id = r.id
|
||||
r = self.get('/auth/projects', token=employee_unscoped_token_id)
|
||||
projects = r.result['projects']
|
||||
random_project = random.randint(0, len(projects) - 1)
|
||||
@ -2979,11 +2969,11 @@ class FederatedUserTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
def test_user_id_persistense(self):
|
||||
"""Ensure user_id is persistend for multiple federated authn calls."""
|
||||
r = self._issue_unscoped_token()
|
||||
user_id = r.json_body['token']['user']['id']
|
||||
user_id = r.user_id
|
||||
self.assertNotEmpty(PROVIDERS.identity_api.get_user(user_id))
|
||||
|
||||
r = self._issue_unscoped_token()
|
||||
user_id2 = r.json_body['token']['user']['id']
|
||||
user_id2 = r.user_id
|
||||
self.assertNotEmpty(PROVIDERS.identity_api.get_user(user_id2))
|
||||
self.assertEqual(user_id, user_id2)
|
||||
|
||||
@ -3272,7 +3262,7 @@ class FederatedUserTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
# Authenticate to create a new federated_user entry with a foreign
|
||||
# key pointing to the protocol
|
||||
r = self._issue_unscoped_token()
|
||||
user_id = r.json_body['token']['user']['id']
|
||||
user_id = r.user_id
|
||||
self.assertNotEmpty(PROVIDERS.identity_api.get_user(user_id))
|
||||
|
||||
# Now we should be able to delete the protocol
|
||||
@ -3280,10 +3270,10 @@ class FederatedUserTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
def _authenticate_via_saml(self):
|
||||
r = self._issue_unscoped_token()
|
||||
unscoped_token = r.headers['X-Subject-Token']
|
||||
token_resp = r.json_body['token']
|
||||
unscoped_token = r.id
|
||||
token_resp = render_token.render_token_response_from_model(r)['token']
|
||||
self.assertValidMappedUser(token_resp)
|
||||
return token_resp['user']['id'], unscoped_token
|
||||
return r.user_id, unscoped_token
|
||||
|
||||
|
||||
class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
@ -3351,8 +3341,9 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
self.assertNotIn(project['name'], self.expected_results)
|
||||
|
||||
response = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(response.json_body['token'])
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
self.assertValidMappedUser(
|
||||
render_token.render_token_response_from_model(response)['token'])
|
||||
unscoped_token = response.id
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
for project in projects:
|
||||
@ -3364,8 +3355,9 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
def test_shadow_mapping_create_projects_role_assignments(self):
|
||||
response = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(response.json_body['token'])
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
self.assertValidMappedUser(
|
||||
render_token.render_token_response_from_model(response)['token'])
|
||||
unscoped_token = response.id
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
for project in projects:
|
||||
@ -3391,8 +3383,9 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
def test_shadow_mapping_creates_project_in_identity_provider_domain(self):
|
||||
response = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(response.json_body['token'])
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
self.assertValidMappedUser(
|
||||
render_token.render_token_response_from_model(response)['token'])
|
||||
unscoped_token = response.id
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
for project in projects:
|
||||
@ -3401,12 +3394,13 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
def test_shadow_mapping_is_idempotent(self):
|
||||
"""Test that projects remain idempotent for every federated auth."""
|
||||
response = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(response.json_body['token'])
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
self.assertValidMappedUser(
|
||||
render_token.render_token_response_from_model(response)['token'])
|
||||
unscoped_token = response.id
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
project_ids = [p['id'] for p in response.json_body['projects']]
|
||||
response = self._issue_unscoped_token()
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
unscoped_token = response.id
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
for project in projects:
|
||||
@ -3438,8 +3432,8 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
)
|
||||
PROVIDERS.role_api.create_role(member_role_ref['id'], member_role_ref)
|
||||
response = self._issue_unscoped_token()
|
||||
user_id = response.json_body['token']['user']['id']
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
user_id = response.user_id
|
||||
unscoped_token = response.id
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
staging_project = PROVIDERS.resource_api.get_project_by_name(
|
||||
@ -3500,7 +3494,7 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
)
|
||||
response = self._issue_unscoped_token()
|
||||
# user_id = response.json_body['token']['user']['id']
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
unscoped_token = response.id
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
self.expected_results = {
|
||||
@ -3532,8 +3526,8 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
# to them. This test verifies that this is no longer true.
|
||||
# Authenticate once to create the projects
|
||||
response = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(response.json_body['token'])
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
self.assertValidMappedUser(
|
||||
render_token.render_token_response_from_model(response)['token'])
|
||||
|
||||
# Assign admin role to newly-created project to another user
|
||||
staging_project = PROVIDERS.resource_api.get_project_by_name(
|
||||
@ -3548,8 +3542,9 @@ class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
# Authenticate again with the federated user and verify roles
|
||||
response = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(response.json_body['token'])
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
self.assertValidMappedUser(
|
||||
render_token.render_token_response_from_model(response)['token'])
|
||||
unscoped_token = response.id
|
||||
scope = self._scope_request(
|
||||
unscoped_token, 'project', staging_project['id']
|
||||
)
|
||||
@ -4602,10 +4597,6 @@ class WebSSOTests(FederatedTokenTests):
|
||||
ORIGIN = urllib.parse.quote_plus(TRUSTED_DASHBOARD)
|
||||
PROTOCOL_REMOTE_ID_ATTR = uuid.uuid4().hex
|
||||
|
||||
def setUp(self):
|
||||
super(WebSSOTests, self).setUp()
|
||||
self.api = federation_controllers.Auth()
|
||||
|
||||
def config_overrides(self):
|
||||
super(WebSSOTests, self).config_overrides()
|
||||
self.config_fixture.config(
|
||||
@ -4616,33 +4607,38 @@ class WebSSOTests(FederatedTokenTests):
|
||||
|
||||
def test_render_callback_template(self):
|
||||
token_id = uuid.uuid4().hex
|
||||
resp = self.api.render_html_response(self.TRUSTED_DASHBOARD, token_id)
|
||||
with self.make_request():
|
||||
resp = (
|
||||
auth_api._AuthFederationWebSSOBase._render_template_response(
|
||||
self.TRUSTED_DASHBOARD, token_id))
|
||||
# The expected value in the assertions bellow need to be 'str' in
|
||||
# Python 2 and 'bytes' in Python 3
|
||||
self.assertIn(token_id.encode('utf-8'), resp.body)
|
||||
self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.body)
|
||||
self.assertIn(token_id.encode('utf-8'), resp.data)
|
||||
self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.data)
|
||||
|
||||
def test_federated_sso_auth(self):
|
||||
environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0],
|
||||
'QUERY_STRING': 'origin=%s' % self.ORIGIN}
|
||||
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
|
||||
request = self.make_request(environ=environment)
|
||||
resp = self.api.federated_sso_auth(request, self.PROTOCOL)
|
||||
# `resp.body` will be `str` in Python 2 and `bytes` in Python 3
|
||||
with self.make_request(environ=environment):
|
||||
resp = auth_api.AuthFederationWebSSOResource._perform_auth(
|
||||
self.PROTOCOL)
|
||||
# `resp.data` will be `str` in Python 2 and `bytes` in Python 3
|
||||
# which is why expected value: `self.TRUSTED_DASHBOARD`
|
||||
# needs to be encoded
|
||||
self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.body)
|
||||
self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.data)
|
||||
|
||||
def test_get_sso_origin_host_case_insensitive(self):
|
||||
# test lowercase hostname in trusted_dashboard
|
||||
environ = {'QUERY_STRING': 'origin=http://horizon.com'}
|
||||
request = self.make_request(environ=environ)
|
||||
host = self.api._get_sso_origin_host(request)
|
||||
with self.make_request(environ=environ):
|
||||
host = auth_api._get_sso_origin_host()
|
||||
self.assertEqual("http://horizon.com", host)
|
||||
# test uppercase hostname in trusted_dashboard
|
||||
self.config_fixture.config(group='federation',
|
||||
self.config_fixture.config(
|
||||
group='federation',
|
||||
trusted_dashboard=['http://Horizon.com'])
|
||||
host = self.api._get_sso_origin_host(request)
|
||||
host = auth_api._get_sso_origin_host()
|
||||
self.assertEqual("http://horizon.com", host)
|
||||
|
||||
def test_federated_sso_auth_with_protocol_specific_remote_id(self):
|
||||
@ -4653,76 +4649,82 @@ class WebSSOTests(FederatedTokenTests):
|
||||
environment = {self.PROTOCOL_REMOTE_ID_ATTR: self.REMOTE_IDS[0],
|
||||
'QUERY_STRING': 'origin=%s' % self.ORIGIN}
|
||||
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
|
||||
request = self.make_request(environ=environment)
|
||||
resp = self.api.federated_sso_auth(request, self.PROTOCOL)
|
||||
# `resp.body` will be `str` in Python 2 and `bytes` in Python 3
|
||||
with self.make_request(environ=environment):
|
||||
resp = auth_api.AuthFederationWebSSOResource._perform_auth(
|
||||
self.PROTOCOL)
|
||||
# `resp.data` will be `str` in Python 2 and `bytes` in Python 3
|
||||
# which is why expected value: `self.TRUSTED_DASHBOARD`
|
||||
# needs to be encoded
|
||||
self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.body)
|
||||
self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.data)
|
||||
|
||||
def test_federated_sso_auth_bad_remote_id(self):
|
||||
environment = {self.REMOTE_ID_ATTR: self.IDP,
|
||||
'QUERY_STRING': 'origin=%s' % self.ORIGIN}
|
||||
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
|
||||
request = self.make_request(environ=environment)
|
||||
self.assertRaises(exception.IdentityProviderNotFound,
|
||||
self.api.federated_sso_auth,
|
||||
request, self.PROTOCOL)
|
||||
with self.make_request(environ=environment):
|
||||
self.assertRaises(
|
||||
exception.IdentityProviderNotFound,
|
||||
auth_api.AuthFederationWebSSOResource._perform_auth,
|
||||
self.PROTOCOL)
|
||||
|
||||
def test_federated_sso_missing_query(self):
|
||||
environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]}
|
||||
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
|
||||
request = self.make_request(environ=environment)
|
||||
self.assertRaises(exception.ValidationError,
|
||||
self.api.federated_sso_auth,
|
||||
request, self.PROTOCOL)
|
||||
with self.make_request(environ=environment):
|
||||
self.assertRaises(
|
||||
exception.ValidationError,
|
||||
auth_api.AuthFederationWebSSOResource._perform_auth,
|
||||
self.PROTOCOL)
|
||||
|
||||
def test_federated_sso_missing_query_bad_remote_id(self):
|
||||
environment = {self.REMOTE_ID_ATTR: self.IDP}
|
||||
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
|
||||
request = self.make_request(environ=environment)
|
||||
self.assertRaises(exception.ValidationError,
|
||||
self.api.federated_sso_auth,
|
||||
request, self.PROTOCOL)
|
||||
with self.make_request(environ=environment):
|
||||
self.assertRaises(
|
||||
exception.ValidationError,
|
||||
auth_api.AuthFederationWebSSOResource._perform_auth,
|
||||
self.PROTOCOL)
|
||||
|
||||
def test_federated_sso_untrusted_dashboard(self):
|
||||
environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0],
|
||||
'QUERY_STRING': 'origin=%s' % uuid.uuid4().hex}
|
||||
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
|
||||
request = self.make_request(environ=environment)
|
||||
self.assertRaises(exception.Unauthorized,
|
||||
self.api.federated_sso_auth,
|
||||
request, self.PROTOCOL)
|
||||
with self.make_request(environ=environment):
|
||||
self.assertRaises(
|
||||
exception.Unauthorized,
|
||||
auth_api.AuthFederationWebSSOResource._perform_auth,
|
||||
self.PROTOCOL)
|
||||
|
||||
def test_federated_sso_untrusted_dashboard_bad_remote_id(self):
|
||||
environment = {self.REMOTE_ID_ATTR: self.IDP,
|
||||
'QUERY_STRING': 'origin=%s' % uuid.uuid4().hex}
|
||||
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
|
||||
request = self.make_request(environ=environment)
|
||||
self.assertRaises(exception.Unauthorized,
|
||||
self.api.federated_sso_auth,
|
||||
request, self.PROTOCOL)
|
||||
with self.make_request(environ=environment):
|
||||
self.assertRaises(
|
||||
exception.Unauthorized,
|
||||
auth_api.AuthFederationWebSSOResource._perform_auth,
|
||||
self.PROTOCOL)
|
||||
|
||||
def test_federated_sso_missing_remote_id(self):
|
||||
environment = copy.deepcopy(mapping_fixtures.EMPLOYEE_ASSERTION)
|
||||
request = self.make_request(environ=environment,
|
||||
query_string='origin=%s' % self.ORIGIN)
|
||||
self.assertRaises(exception.Unauthorized,
|
||||
self.api.federated_sso_auth,
|
||||
request, self.PROTOCOL)
|
||||
with self.make_request(environ=environment,
|
||||
query_string='origin=%s' % self.ORIGIN):
|
||||
self.assertRaises(
|
||||
exception.Unauthorized,
|
||||
auth_api.AuthFederationWebSSOResource._perform_auth,
|
||||
self.PROTOCOL)
|
||||
|
||||
def test_identity_provider_specific_federated_authentication(self):
|
||||
environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]}
|
||||
environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
|
||||
request = self.make_request(environ=environment,
|
||||
query_string='origin=%s' % self.ORIGIN)
|
||||
resp = self.api.federated_idp_specific_sso_auth(request,
|
||||
self.idp['id'],
|
||||
self.PROTOCOL)
|
||||
# `resp.body` will be `str` in Python 2 and `bytes` in Python 3
|
||||
with self.make_request(environ=environment,
|
||||
query_string='origin=%s' % self.ORIGIN):
|
||||
resp = auth_api.AuthFederationWebSSOIDPsResource._perform_auth(
|
||||
self.idp['id'], self.PROTOCOL)
|
||||
# `resp.data` will be `str` in Python 2 and `bytes` in Python 3
|
||||
# which is why the expected value: `self.TRUSTED_DASHBOARD`
|
||||
# needs to be encoded
|
||||
self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.body)
|
||||
self.assertIn(self.TRUSTED_DASHBOARD.encode('utf-8'), resp.data)
|
||||
|
||||
|
||||
class K2KServiceCatalogTests(test_v3.RestfulTestCase):
|
||||
@ -4779,7 +4781,7 @@ class K2KServiceCatalogTests(test_v3.RestfulTestCase):
|
||||
model = token_model.TokenModel()
|
||||
model.user_id = self.user_id
|
||||
model.methods = ['password']
|
||||
token = controller.render_token_response_from_model(model)
|
||||
token = render_token.render_token_response_from_model(model)
|
||||
ref = {}
|
||||
for r in (self.sp_alpha, self.sp_beta, self.sp_gamma):
|
||||
ref.update(r)
|
||||
@ -4799,7 +4801,7 @@ class K2KServiceCatalogTests(test_v3.RestfulTestCase):
|
||||
model = token_model.TokenModel()
|
||||
model.user_id = self.user_id
|
||||
model.methods = ['password']
|
||||
token = controller.render_token_response_from_model(model)
|
||||
token = render_token.render_token_response_from_model(model)
|
||||
ref = {}
|
||||
for r in (self.sp_beta, self.sp_gamma):
|
||||
ref.update(r)
|
||||
@ -4819,7 +4821,7 @@ class K2KServiceCatalogTests(test_v3.RestfulTestCase):
|
||||
model = token_model.TokenModel()
|
||||
model.user_id = self.user_id
|
||||
model.methods = ['password']
|
||||
token = controller.render_token_response_from_model(model)
|
||||
token = render_token.render_token_response_from_model(model)
|
||||
self.assertNotIn('service_providers', token['token'],
|
||||
message=('Expected Service Catalog not to have '
|
||||
'service_providers'))
|
||||
|
Loading…
Reference in New Issue
Block a user