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