Convert auth to flask native dispatching

Convert the /auth paths to flask native dispatching.

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@ -0,0 +1,243 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# Shared code for Authentication flows. This module is where actual auth
# happens. The code here is shared between Federation and Auth.
# TODO(morgan): Deprecate all auth flows in /v3/OS-FEDERATION, merge this code
# into keystone.api.auth. For now this is the best place for the code to
# exist.
import flask
from oslo_log import log
import six
from keystone.auth import core
from keystone.common import provider_api
from keystone import exception
from keystone.federation import constants
from keystone.i18n import _
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
def _check_and_set_default_scoping(auth_info, auth_context):
(domain_id, project_id, trust, unscoped, system) = (
auth_info.get_scope()
)
if trust:
project_id = trust['project_id']
if system or domain_id or project_id or trust:
# scope is specified
return
# Skip scoping when unscoped federated token is being issued
if constants.IDENTITY_PROVIDER in auth_context:
return
# Do not scope if request is for explicitly unscoped token
if unscoped is not None:
return
# fill in default_project_id if it is available
try:
user_ref = PROVIDERS.identity_api.get_user(auth_context['user_id'])
except exception.UserNotFound as e:
LOG.warning(six.text_type(e))
raise exception.Unauthorized(e)
default_project_id = user_ref.get('default_project_id')
if not default_project_id:
# User has no default project. He shall get an unscoped token.
return
# make sure user's default project is legit before scoping to it
try:
default_project_ref = PROVIDERS.resource_api.get_project(
default_project_id)
default_project_domain_ref = PROVIDERS.resource_api.get_domain(
default_project_ref['domain_id'])
if (default_project_ref.get('enabled', True) and
default_project_domain_ref.get('enabled', True)):
if PROVIDERS.assignment_api.get_roles_for_user_and_project(
user_ref['id'], default_project_id):
auth_info.set_scope(project_id=default_project_id)
else:
msg = ("User %(user_id)s doesn't have access to"
" default project %(project_id)s. The token"
" will be unscoped rather than scoped to the"
" project.")
LOG.debug(msg,
{'user_id': user_ref['id'],
'project_id': default_project_id})
else:
msg = ("User %(user_id)s's default project %(project_id)s"
" is disabled. The token will be unscoped rather"
" than scoped to the project.")
LOG.debug(msg,
{'user_id': user_ref['id'],
'project_id': default_project_id})
except (exception.ProjectNotFound, exception.DomainNotFound):
# default project or default project domain doesn't exist,
# will issue unscoped token instead
msg = ("User %(user_id)s's default project %(project_id)s not"
" found. The token will be unscoped rather than"
" scoped to the project.")
LOG.debug(msg, {'user_id': user_ref['id'],
'project_id': default_project_id})
def authenticate(auth_info, auth_context):
"""Authenticate user."""
# NOTE(notmorgan): This is not super pythonic, but we lean on the
# __setitem__ method in auth_context to handle edge cases and security
# of the attributes set by the plugins. This check to ensure
# `auth_context` is an instance of AuthContext is extra insurance and
# will prevent regressions.
if not isinstance(auth_context, core.AuthContext):
LOG.error(
'`auth_context` passed to the Auth controller '
'`authenticate` method is not of type '
'`keystone.auth.core.AuthContext`. For security '
'purposes this is required. This is likely a programming '
'error. Received object of type `%s`', type(auth_context))
raise exception.Unauthorized(
_('Cannot Authenticate due to internal error.'))
# The 'external' method allows any 'REMOTE_USER' based authentication
# In some cases the server can set REMOTE_USER as '' instead of
# dropping it, so this must be filtered out
if flask.request.remote_user:
try:
external = core.get_auth_method('external')
resp = external.authenticate(auth_info)
if resp and resp.status:
# NOTE(notmorgan): ``external`` plugin cannot be multi-step
# it is either a plain success/fail.
auth_context.setdefault(
'method_names', []).insert(0, 'external')
# NOTE(notmorgan): All updates to auth_context is handled
# here in the .authenticate method.
auth_context.update(resp.response_data or {})
except exception.AuthMethodNotSupported:
# This will happen there is no 'external' plugin registered
# and the container is performing authentication.
# The 'kerberos' and 'saml' methods will be used this way.
# In those cases, it is correct to not register an
# 'external' plugin; if there is both an 'external' and a
# 'kerberos' plugin, it would run the check on identity twice.
LOG.debug("No 'external' plugin is registered.")
except exception.Unauthorized:
# If external fails then continue and attempt to determine
# user identity using remaining auth methods
LOG.debug("Authorization failed for 'external' auth method.")
# need to aggregate the results in case two or more methods
# are specified
auth_response = {'methods': []}
for method_name in auth_info.get_method_names():
method = core.get_auth_method(method_name)
resp = method.authenticate(auth_info.get_method_data(method_name))
if resp:
if resp.status:
auth_context.setdefault(
'method_names', []).insert(0, method_name)
# NOTE(notmorgan): All updates to auth_context is handled
# here in the .authenticate method. If the auth attempt was
# not successful do not update the auth_context
resp_method_names = resp.response_data.pop(
'method_names', [])
auth_context['method_names'].extend(resp_method_names)
auth_context.update(resp.response_data or {})
elif resp.response_body:
auth_response['methods'].append(method_name)
auth_response[method_name] = resp.response_body
if auth_response["methods"]:
# authentication continuation required
raise exception.AdditionalAuthRequired(auth_response)
if 'user_id' not in auth_context:
msg = 'User not found by auth plugin; authentication failed'
tr_msg = _('User not found by auth plugin; authentication failed')
LOG.warning(msg)
raise exception.Unauthorized(tr_msg)
def authenticate_for_token(auth=None):
"""Authenticate user and issue a token."""
try:
auth_info = core.AuthInfo.create(auth=auth)
auth_context = core.AuthContext(method_names=[],
bind={})
authenticate(auth_info, auth_context)
if auth_context.get('access_token_id'):
auth_info.set_scope(None, auth_context['project_id'], None)
_check_and_set_default_scoping(auth_info, auth_context)
(domain_id, project_id, trust, unscoped, system) = (
auth_info.get_scope()
)
trust_id = trust.get('id') if trust else None
# NOTE(notmorgan): only methods that actually run and succeed will
# be in the auth_context['method_names'] list. Do not blindly take
# the values from auth_info, look at the authoritative values. Make
# sure the set is unique.
method_names_set = set(auth_context.get('method_names', []))
method_names = list(method_names_set)
app_cred_id = None
if 'application_credential' in method_names:
token_auth = auth_info.auth['identity']
app_cred_id = token_auth['application_credential']['id']
# Do MFA Rule Validation for the user
if not core.UserMFARulesValidator.check_auth_methods_against_rules(
auth_context['user_id'], method_names_set):
raise exception.InsufficientAuthMethods(
user_id=auth_context['user_id'],
methods='[%s]' % ','.join(auth_info.get_method_names()))
expires_at = auth_context.get('expires_at')
token_audit_id = auth_context.get('audit_id')
token = PROVIDERS.token_provider_api.issue_token(
auth_context['user_id'], method_names, expires_at=expires_at,
system=system, project_id=project_id, domain_id=domain_id,
auth_context=auth_context, trust_id=trust_id,
app_cred_id=app_cred_id, parent_audit_id=token_audit_id)
# NOTE(wanghong): We consume a trust use only when we are using
# trusts and have successfully issued a token.
if trust:
PROVIDERS.trust_api.consume_use(token.trust_id)
return token
except exception.TrustNotFound as e:
LOG.warning(six.text_type(e))
raise exception.Unauthorized(e)
def federated_authenticate_for_token(identity_provider, protocol_id):
auth = {
'identity': {
'methods': [protocol_id],
protocol_id: {
'identity_provider': identity_provider,
'protocol': protocol_id
}
}
}
return authenticate_for_token(auth)

View File

@ -70,3 +70,8 @@ os_federation_parameter_rel_func = functools.partial(
os_inherit_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation,
extension_name='OS-INHERIT', extension_version='1.0')
# OS-PKI (revoked) "extension"
os_pki_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation,
extension_name='OS-PKI', extension_version='1.0')

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

@ -0,0 +1,578 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# This file handles all flask-restful resources for /v3/auth
import string
import flask
import flask_restful
from oslo_log import log
from oslo_serialization import jsonutils
from oslo_utils import strutils
from six.moves import http_client
from six.moves import urllib
import werkzeug.exceptions
from keystone.api._shared import authentication
from keystone.api._shared import json_home_relations
from keystone.auth import schema as auth_schema
from keystone.common import authorization
from keystone.common import provider_api
from keystone.common import rbac_enforcer
from keystone.common import render_token
from keystone.common import utils as k_utils
from keystone.common import validation
import keystone.conf
from keystone import exception
from keystone.federation import idp as keystone_idp
from keystone.federation import schema as federation_schema
from keystone.federation import utils as federation_utils
from keystone.i18n import _
from keystone.server import flask as ks_flask
CONF = keystone.conf.CONF
ENFORCER = rbac_enforcer.RBACEnforcer
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
def _combine_lists_uniquely(a, b):
# it's most likely that only one of these will be filled so avoid
# the combination if possible.
if a and b:
return {x['id']: x for x in a + b}.values()
else:
return a or b
def _create_base_saml_assertion(auth):
issuer = CONF.saml.idp_entity_id
sp_id = auth['scope']['service_provider']['id']
service_provider = PROVIDERS.federation_api.get_sp(sp_id)
federation_utils.assert_enabled_service_provider_object(service_provider)
sp_url = service_provider['sp_url']
token_id = auth['identity']['token']['id']
token = PROVIDERS.token_provider_api.validate_token(token_id)
if not token.project_scoped:
action = _('Use a project scoped token when attempting to create '
'a SAML assertion')
raise exception.ForbiddenAction(action=action)
subject = token.user['name']
role_names = []
for role in token.roles:
role_names.append(role['name'])
project = token.project['name']
# NOTE(rodrigods): the domain name is necessary in order to distinguish
# between projects and users with the same name in different domains.
project_domain_name = token.project_domain['name']
subject_domain_name = token.user_domain['name']
generator = keystone_idp.SAMLGenerator()
response = generator.samlize_token(
issuer, sp_url, subject, subject_domain_name,
role_names, project, project_domain_name)
return response, service_provider
def _build_response_headers(service_provider):
# URLs in header are encoded into bytes
return [('Content-Type', 'text/xml'),
('X-sp-url', service_provider['sp_url'].encode('utf-8')),
('X-auth-url', service_provider['auth_url'].encode('utf-8'))]
def _get_sso_origin_host():
"""Validate and return originating dashboard URL.
Make sure the parameter is specified in the request's URL as well its
value belongs to a list of trusted dashboards.
:raises keystone.exception.ValidationError: ``origin`` query parameter
was not specified. The URL is deemed invalid.
:raises keystone.exception.Unauthorized: URL specified in origin query
parameter does not exist in list of websso trusted dashboards.
:returns: URL with the originating dashboard
"""
origin = flask.request.args.get('origin')
if not origin:
msg = 'Request must have an origin query parameter'
tr_msg = _('Request must have an origin query parameter')
LOG.error(msg)
raise exception.ValidationError(tr_msg)
host = urllib.parse.unquote_plus(origin)
# change trusted_dashboard hostnames to lowercase before comparison
trusted_dashboards = [k_utils.lower_case_hostname(trusted)
for trusted in CONF.federation.trusted_dashboard]
if host not in trusted_dashboards:
msg = '%(host)s is not a trusted dashboard host' % {'host': host}
tr_msg = _('%(host)s is not a trusted dashboard host') % {
'host': host}
LOG.error(msg)
raise exception.Unauthorized(tr_msg)
return host
class _AuthFederationWebSSOBase(ks_flask.ResourceBase):
collection_key = '__UNUSED__'
member_key = '__UNUSED__'
@staticmethod
def _render_template_response(host, token_id):
with open(CONF.federation.sso_callback_template) as template:
src = string.Template(template.read())
subs = {'host': host, 'token': token_id}
body = src.substitute(subs)
resp = flask.make_response(body, http_client.OK)
resp.charset = 'utf-8'
resp.headers['Content-Type'] = 'text/html'
return resp
class AuthProjectsResource(ks_flask.ResourceBase):
collection_key = 'projects'
member_key = 'project'
def get(self):
"""Get possible project scopes for token.
GET/HEAD /v3/auth/projects
GET/HEAD /v3/OS-FEDERATION/projects
"""
ENFORCER.enforce_call(action='identity:get_auth_projects')
user_id = self.auth_context.get('user_id')
group_ids = self.auth_context.get('group_ids')
user_p_refs = []
grp_p_refs = []
if user_id:
try:
user_p_refs = PROVIDERS.assignment_api.list_projects_for_user(
user_id)
except exception.UserNotFound: # nosec
# federated users have an id but they don't link to anything
pass
if group_ids:
grp_p_refs = PROVIDERS.assignment_api.list_projects_for_groups(
group_ids)
refs = _combine_lists_uniquely(user_p_refs, grp_p_refs)
return self.wrap_collection(refs)
class AuthDomainsResource(ks_flask.ResourceBase):
collection_key = 'domains'
member_key = 'domain'
def get(self):
"""Get possible domain scopes for token.
GET/HEAD /v3/auth/domains
GET/HEAD /v3/OS-FEDERATION/domains
"""
ENFORCER.enforce_call(action='identity:get_auth_domains')
user_id = self.auth_context.get('user_id')
group_ids = self.auth_context.get('group_ids')
user_d_refs = []
grp_d_refs = []
if user_id:
try:
user_d_refs = PROVIDERS.assignment_api.list_domains_for_user(
user_id)
except exception.UserNotFound: # nosec
# federated users have an id but they don't link to anything
pass
if group_ids:
grp_d_refs = PROVIDERS.assignment_api.list_domains_for_groups(
group_ids)
refs = _combine_lists_uniquely(user_d_refs, grp_d_refs)
return self.wrap_collection(refs)
class AuthSystemResource(_AuthFederationWebSSOBase):
def get(self):
"""Get possible system scopes for token.
GET/HEAD /v3/auth/system
"""
ENFORCER.enforce_call(action='identity:get_auth_system')
user_id = self.auth_context.get('user_id')
group_ids = self.auth_context.get('group_ids')
user_assignments = []
group_assignments = []
if user_id:
try:
user_assignments = (
PROVIDERS.assignment_api.list_system_grants_for_user(
user_id)
)
except exception.UserNotFound: # nosec
# federated users have an id but they don't link to anything
pass
if group_ids:
group_assignments = (
PROVIDERS.assignment_api.list_system_grants_for_groups(
group_ids)
)
assignments = _combine_lists_uniquely(
user_assignments, group_assignments)
if assignments:
response = {
'system': [{'all': True}],
'links': {
'self': ks_flask.base_url(path='auth/system')
}
}
else:
response = {
'system': [],
'links': {
'self': ks_flask.base_url(path='auth/system')
}
}
return response
class AuthCatalogResource(_AuthFederationWebSSOBase):
def get(self):
"""Get service catalog for token.
GET/HEAD /v3/auth/catalog
"""
ENFORCER.enforce_call(action='identity:get_auth_catalog')
user_id = self.auth_context.get('user_id')
project_id = self.auth_context.get('project_id')
if not project_id:
raise exception.Forbidden(
_('A project-scoped token is required to produce a '
'service catalog.'))
return {
'catalog': PROVIDERS.catalog_api.get_v3_catalog(
user_id, project_id
),
'links': {
'self': ks_flask.base_url(path='auth/catalog')
}
}
class AuthTokenOSPKIResource(flask_restful.Resource):
@ks_flask.unenforced_api
def get(self):
"""Deprecated; get revoked token list.
GET/HEAD /v3/auth/tokens/OS-PKI/revoked
"""
if not CONF.token.revoke_by_id:
raise exception.Gone()
# NOTE(lbragstad): This API is deprecated and isn't supported. Keystone
# also doesn't store tokens, so returning a list of revoked tokens
# would require keystone to write invalid tokens to disk, which defeats
# the purpose. Return a 403 instead of removing the API altogether.
raise exception.Forbidden()
class AuthTokenResource(_AuthFederationWebSSOBase):
def get(self):
"""Validate a token.
HEAD/GET /v3/auth/tokens
"""
# TODO(morgan): eliminate the check_token action only use validate
# NOTE(morgan): Well lookie here, we have different enforcements
# for no good reason (historical), because the methods previouslly
# had to be named different names. Check which method and do the
# correct enforcement.
if flask.request.method == 'HEAD':
ENFORCER.enforce_call(action='identity:check_token')
else:
ENFORCER.enforce_call(action='identity:validate_token')
token_id = flask.request.headers.get(
authorization.SUBJECT_TOKEN_HEADER)
allow_expired = strutils.bool_from_string(
flask.request.args.get('allow_expired'))
window_secs = CONF.token.allow_expired_window if allow_expired else 0
include_catalog = 'nocatalog' not in flask.request.args
token = PROVIDERS.token_provider_api.validate_token(
token_id, window_seconds=window_secs)
token_resp = render_token.render_token_response_from_model(
token, include_catalog=include_catalog)
resp_body = jsonutils.dumps(token_resp)
response = flask.make_response(resp_body, http_client.OK)
response.headers['X-Subject-Token'] = token_id
response.headers['Content-Type'] = 'application/json'
return response
@ks_flask.unenforced_api
def post(self):
"""Issue a token.
POST /v3/auth/tokens
"""
include_catalog = 'nocatalog' not in flask.request.args
auth_data = self.request_body_json.get('auth')
auth_schema.validate_issue_token_auth(auth_data)
token = authentication.authenticate_for_token(auth_data)
resp_data = render_token.render_token_response_from_model(
token, include_catalog=include_catalog
)
resp_body = jsonutils.dumps(resp_data)
response = flask.make_response(resp_body, http_client.CREATED)
response.headers['X-Subject-Token'] = token.id
response.headers['Content-Type'] = 'application/json'
return response
def delete(self):
"""Revoke a token.
DELETE /v3/auth/tokens
"""
ENFORCER.enforce_call(action='identity:revoke_token')
token_id = flask.request.headers.get(
authorization.SUBJECT_TOKEN_HEADER)
PROVIDERS.token_provider_api.revoke_token(token_id)
return None, http_client.NO_CONTENT
class AuthFederationWebSSOResource(_AuthFederationWebSSOBase):
@classmethod
def _perform_auth(cls, protocol_id):
try:
remote_id_name = federation_utils.get_remote_id_parameter(
protocol_id)
remote_id = flask.request.environ[remote_id_name]
except KeyError:
msg = 'Missing entity ID from environment'
tr_msg = _('Missing entity ID from environment')
LOG.error(msg)
raise exception.Unauthorized(tr_msg)
host = _get_sso_origin_host()
ref = PROVIDERS.federation_api.get_idp_from_remote_id(remote_id)
identity_provider = ref['idp_id']
token = authentication.federated_authenticate_for_token(
identity_provider=identity_provider, protocol_id=protocol_id)
return cls._render_template_response(host, token.id)
@ks_flask.unenforced_api
def get(self, protocol_id):
return self._perform_auth(protocol_id)
@ks_flask.unenforced_api
def post(self, protocol_id):
return self._perform_auth(protocol_id)
class AuthFederationWebSSOIDPsResource(_AuthFederationWebSSOBase):
@classmethod
def _perform_auth(cls, idp_id, protocol_id):
host = _get_sso_origin_host()
token = authentication.federated_authenticate_for_token(
identity_provider=idp_id, protocol_id=protocol_id)
return cls._render_template_response(host, token.id)
@ks_flask.unenforced_api
def get(self, idp_id, protocol_id):
return self._perform_auth(idp_id, protocol_id)
@ks_flask.unenforced_api
def post(self, idp_id, protocol_id):
return self._perform_auth(idp_id, protocol_id)
class AuthFederationSaml2Resource(_AuthFederationWebSSOBase):
def get(self):
raise werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST'])
@ks_flask.unenforced_api
def post(self):
"""Exchange a scoped token for a SAML assertion.
POST /v3/auth/OS-FEDERATION/saml2
"""
auth = self.request_body_json.get('auth')
validation.lazy_validate(federation_schema.saml_create, auth)
response, service_provider = _create_base_saml_assertion(auth)
headers = _build_response_headers(service_provider)
response = flask.make_response(response.to_string(), http_client.OK)
for header, value in headers:
response.headers[header] = value
return response
class AuthFederationSaml2ECPResource(_AuthFederationWebSSOBase):
def get(self):
raise werkzeug.exceptions.MethodNotAllowed(valid_methods=['POST'])
@ks_flask.unenforced_api
def post(self):
"""Exchange a scoped token for an ECP assertion.
POST /v3/auth/OS-FEDERATION/saml2/ecp
"""
auth = self.request_body_json.get('auth')
validation.lazy_validate(federation_schema.saml_create, auth)
saml_assertion, service_provider = _create_base_saml_assertion(auth)
relay_state_prefix = service_provider['relay_state_prefix']
generator = keystone_idp.ECPGenerator()
ecp_assertion = generator.generate_ecp(
saml_assertion, relay_state_prefix)
headers = _build_response_headers(service_provider)
response = flask.make_response(
ecp_assertion.to_string(), http_client.OK)
for header, value in headers:
response.headers[header] = value
return response
class AuthAPI(ks_flask.APIBase):
_name = 'auth'
_import_name = __name__
resources = []
resource_mapping = [
ks_flask.construct_resource_map(
resource=AuthProjectsResource,
url='/auth/projects',
alternate_urls=[dict(
url='/OS-FEDERATION/projects',
json_home=ks_flask.construct_json_home_data(
rel='projects',
resource_relation_func=(
json_home_relations.os_federation_resource_rel_func)
)
)],
rel='auth_projects',
resource_kwargs={}
),
ks_flask.construct_resource_map(
resource=AuthDomainsResource,
url='/auth/domains',
alternate_urls=[dict(
url='/OS-FEDERATION/domains',
json_home=ks_flask.construct_json_home_data(
rel='domains',
resource_relation_func=(
json_home_relations.os_federation_resource_rel_func)
)
)],
rel='auth_domains',
resource_kwargs={},
),
ks_flask.construct_resource_map(
resource=AuthSystemResource,
url='/auth/system',
resource_kwargs={},
rel='auth_system'
),
ks_flask.construct_resource_map(
resource=AuthCatalogResource,
url='/auth/catalog',
resource_kwargs={},
rel='auth_catalog'
),
ks_flask.construct_resource_map(
resource=AuthTokenOSPKIResource,
url='/auth/tokens/OS-PKI/revoked',
resource_kwargs={},
rel='revocations',
resource_relation_func=json_home_relations.os_pki_resource_rel_func
),
ks_flask.construct_resource_map(
resource=AuthTokenResource,
url='/auth/tokens',
resource_kwargs={},
rel='auth_tokens'
)
]
class AuthFederationAPI(ks_flask.APIBase):
_name = 'auth/OS-FEDERATION'
_import_name = __name__
resources = []
resource_mapping = [
ks_flask.construct_resource_map(
resource=AuthFederationSaml2Resource,
url='/auth/OS-FEDERATION/saml2',
resource_kwargs={},
resource_relation_func=(
json_home_relations.os_federation_resource_rel_func),
rel='saml2'
),
ks_flask.construct_resource_map(
resource=AuthFederationSaml2ECPResource,
url='/auth/OS-FEDERATION/saml2/ecp',
resource_kwargs={},
resource_relation_func=(
json_home_relations.os_federation_resource_rel_func),
rel='ecp'
),
ks_flask.construct_resource_map(
resource=AuthFederationWebSSOResource,
url='/auth/OS-FEDERATION/websso/<string:protocol_id>',
resource_kwargs={},
rel='websso',
resource_relation_func=(
json_home_relations.os_federation_resource_rel_func),
path_vars={
'protocol_id': (
json_home_relations.os_federation_parameter_rel_func(
parameter_name='protocol_id'))}
),
ks_flask.construct_resource_map(
resource=AuthFederationWebSSOIDPsResource,
url=('/auth/OS-FEDERATION/identity_providers/<string:idp_id>/'
'protocols/<string:protocol_id>/websso'),
resource_kwargs={},
rel='identity_providers_websso',
resource_relation_func=(
json_home_relations.os_federation_resource_rel_func),
path_vars={
'idp_id': (
json_home_relations.os_federation_parameter_rel_func(
parameter_name='idp_id')),
'protocol_id': (
json_home_relations.os_federation_parameter_rel_func(
parameter_name='protocol_id'))}
)
]
APIs = (
AuthAPI,
AuthFederationAPI,
)

View File

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

View File

@ -22,6 +22,7 @@ from keystone.common import rbac_enforcer
from keystone.common import validation
from keystone import exception
from keystone.identity import schema
from keystone import notifications
from keystone.server import flask as ks_flask
@ -163,7 +164,7 @@ class UserGroupCRUDResource(flask_restful.Resource):
build_target=functools.partial(self._build_enforcement_target_attr,
user_id, group_id))
PROVIDERS.identity_api.add_user_to_group(
user_id, group_id, initiator=ks_flask.build_audit_initiator())
user_id, group_id, initiator=notifications.build_audit_initiator())
return None, http_client.NO_CONTENT
def delete(self, group_id, user_id):
@ -176,7 +177,7 @@ class UserGroupCRUDResource(flask_restful.Resource):
build_target=functools.partial(self._build_enforcement_target_attr,
user_id, group_id))
PROVIDERS.identity_api.remove_user_from_group(
user_id, group_id, initiator=ks_flask.build_audit_initiator())
user_id, group_id, initiator=notifications.build_audit_initiator())
return None, http_client.NO_CONTENT

View File

@ -14,18 +14,17 @@
import flask
import flask_restful
from oslo_log import versionutils
from oslo_serialization import jsonutils
from six.moves import http_client
from keystone.api._shared import authentication
from keystone.api._shared import json_home_relations
from keystone.common import authorization
from keystone.common import provider_api
from keystone.common import rbac_enforcer
from keystone.common import request
from keystone.common import render_token
from keystone.common import validation
import keystone.conf
from keystone import exception
import keystone.federation.controllers
from keystone.federation import schema
from keystone.federation import utils
from keystone.server import flask as ks_flask
@ -380,74 +379,6 @@ class ServiceProvidersResource(_ResourceBase):
return None, http_client.NO_CONTENT
class OSFederationProjectResource(flask_restful.Resource):
@versionutils.deprecated(as_of=versionutils.deprecated.JUNO,
what='GET /v3/OS-FEDERATION/projects',
in_favor_of='GET /v3/auth/projects')
def get(self):
"""Get projects for user.
GET/HEAD /OS-FEDERATION/projects
"""
ENFORCER.enforce_call(action='identity:get_auth_projects')
# TODO(morgan): Make this method simply call the endpoint for
# /v3/auth/projects once auth is ported to flask.
auth_context = flask.request.environ.get(
authorization.AUTH_CONTEXT_ENV)
user_id = auth_context.get('user_id')
group_ids = auth_context.get('group_ids')
user_refs = []
if user_id:
try:
user_refs = PROVIDERS.assignment_api.list_projects_for_user(
user_id)
except exception.UserNotFound: # nosec
# federated users have an id but they don't link to anything
pass
group_refs = []
if group_ids:
group_refs = PROVIDERS.assignment_api.list_projects_for_groups(
group_ids)
refs = _combine_lists_uniquely(user_refs, group_refs)
return ks_flask.ResourceBase.wrap_collection(
refs, collection_name='projects')
class OSFederationDomainResource(flask_restful.Resource):
@versionutils.deprecated(as_of=versionutils.deprecated.JUNO,
what='GET /v3/OS-FEDERATION/domains',
in_favor_of='GET /v3/auth/domains')
def get(self):
"""Get domains for user.
GET/HEAD /OS-FEDERATION/domains
"""
ENFORCER.enforce_call(action='identity:get_auth_domains')
# TODO(morgan): Make this method simply call the endpoint for
# /v3/auth/domains once auth is ported to flask.
auth_context = flask.request.environ.get(
authorization.AUTH_CONTEXT_ENV)
user_id = auth_context.get('user_id')
group_ids = auth_context.get('group_ids')
user_refs = []
if user_id:
try:
user_refs = PROVIDERS.assignment_api.list_domains_for_user(
user_id)
except exception.UserNotFound: # nosec
# federated users have an ide bu they don't link to anything
pass
group_refs = []
if group_ids:
group_refs = PROVIDERS.assignment_api.list_domains_for_groups(
group_ids)
refs = _combine_lists_uniquely(user_refs, group_refs)
return ks_flask.ResourceBase.wrap_collection(
refs, collection_name='domains')
class SAML2MetadataResource(flask_restful.Resource):
@ks_flask.unenforced_api
def get(self):
@ -468,11 +399,6 @@ class SAML2MetadataResource(flask_restful.Resource):
class OSFederationAuthResource(flask_restful.Resource):
def _construct_webob_request(self):
# Build a fake(ish) webob request object from the flask request state
# to pass to the Auth Controller's authenticate_for_token. This is
# purely transitional code.
return request.Request(flask.request.environ)
@ks_flask.unenforced_api
def get(self, idp_id, protocol_id):
@ -493,12 +419,11 @@ class OSFederationAuthResource(flask_restful.Resource):
return self._auth(idp_id, protocol_id)
def _auth(self, idp_id, protocol_id):
"""Build and pass auth data to auth controller.
"""Build and pass auth data to authentication code.
Build HTTP request body for federated authentication and inject
it into the ``authenticate_for_token`` function.
"""
compat_controller = keystone.federation.controllers.Auth()
auth = {
'identity': {
'methods': [protocol_id],
@ -508,14 +433,12 @@ class OSFederationAuthResource(flask_restful.Resource):
},
}
}
# NOTE(morgan): for compatibility, make sure we use a webob request
# until /auth is ported to flask. Since this is a webob response,
# deconstruct it and turn it into a flask response.
webob_resp = compat_controller.authenticate_for_token(
self._construct_webob_request(), auth)
flask_resp = flask.make_response(
webob_resp.body, webob_resp.status_code)
flask_resp.headers.extend(webob_resp.headers.dict_of_lists())
token = authentication.authenticate_for_token(auth)
token_data = render_token.render_token_response_from_model(token)
resp_data = jsonutils.dumps(token_data)
flask_resp = flask.make_response(resp_data, http_client.CREATED)
flask_resp.headers['X-Subject-Token'] = token.id
flask_resp.headers['Content-Type'] = 'application/json'
return flask_resp
@ -525,18 +448,6 @@ class OSFederationAPI(ks_flask.APIBase):
_api_url_prefix = '/OS-FEDERATION'
resources = []
resource_mapping = [
ks_flask.construct_resource_map(
# NOTE(morgan): No resource relation here, the resource relation is
# to /v3/auth/domains and /v3/auth/projects
resource=OSFederationDomainResource,
url='/domains',
resource_kwargs={}),
ks_flask.construct_resource_map(
# NOTE(morgan): No resource relation here, the resource relation is
# to /v3/auth/domains and /v3/auth/projects
resource=OSFederationProjectResource,
url='/projects',
resource_kwargs={}),
ks_flask.construct_resource_map(
resource=SAML2MetadataResource,
url='/saml2/metadata',

View File

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

View File

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

View File

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

View File

@ -1,454 +0,0 @@
# Copyright 2013 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log
import six
from keystone.auth import core
from keystone.auth import schema
from keystone.common import authorization
from keystone.common import controller
from keystone.common import provider_api
from keystone.common import wsgi
import keystone.conf
from keystone import exception
from keystone.federation import constants
from keystone.i18n import _
from keystone.resource import controllers as resource_controllers
LOG = log.getLogger(__name__)
CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
class Auth(controller.V3Controller):
# Note(atiwari): From V3 auth controller code we are
# calling protection() wrappers, so we need to setup
# the member_name and collection_name attributes of
# auth controller code.
# In the absence of these attributes, default 'entity'
# string will be used to represent the target which is
# generic. Policy can be defined using 'entity' but it
# would not reflect the exact entity that is in context.
# We are defining collection_name = 'tokens' and
# member_name = 'token' to facilitate policy decisions.
collection_name = 'tokens'
member_name = 'token'
def __init__(self, *args, **kw):
super(Auth, self).__init__(*args, **kw)
keystone.conf.auth.setup_authentication()
self._mfa_rules_validator = core.UserMFARulesValidator()
def authenticate_for_token(self, request, auth=None):
"""Authenticate user and issue a token."""
include_catalog = 'nocatalog' not in request.params
schema.validate_issue_token_auth(auth)
try:
auth_info = core.AuthInfo.create(auth=auth)
auth_context = core.AuthContext(method_names=[],
bind={})
self.authenticate(request, auth_info, auth_context)
if auth_context.get('access_token_id'):
auth_info.set_scope(None, auth_context['project_id'], None)
self._check_and_set_default_scoping(auth_info, auth_context)
(domain_id, project_id, trust, unscoped, system) = (
auth_info.get_scope()
)
trust_id = trust.get('id') if trust else None
# NOTE(notmorgan): only methods that actually run and succeed will
# be in the auth_context['method_names'] list. Do not blindly take
# the values from auth_info, look at the authoritative values. Make
# sure the set is unique.
method_names_set = set(auth_context.get('method_names', []))
method_names = list(method_names_set)
app_cred_id = None
if 'application_credential' in method_names:
token_auth = auth_info.auth['identity']
app_cred_id = token_auth['application_credential']['id']
# Do MFA Rule Validation for the user
if not self._mfa_rules_validator.check_auth_methods_against_rules(
auth_context['user_id'], method_names_set):
raise exception.InsufficientAuthMethods(
user_id=auth_context['user_id'],
methods='[%s]' % ','.join(auth_info.get_method_names()))
expires_at = auth_context.get('expires_at')
token_audit_id = auth_context.get('audit_id')
token = PROVIDERS.token_provider_api.issue_token(
auth_context['user_id'], method_names, expires_at=expires_at,
system=system, project_id=project_id, domain_id=domain_id,
auth_context=auth_context, trust_id=trust_id,
app_cred_id=app_cred_id, parent_audit_id=token_audit_id)
token_reference = controller.render_token_response_from_model(
token, include_catalog=include_catalog
)
# NOTE(wanghong): We consume a trust use only when we are using
# trusts and have successfully issued a token.
if trust:
PROVIDERS.trust_api.consume_use(token.trust_id)
return render_token_data_response(token.id, token_reference,
created=True)
except exception.TrustNotFound as e:
LOG.warning(six.text_type(e))
raise exception.Unauthorized(e)
def _check_and_set_default_scoping(self, auth_info, auth_context):
(domain_id, project_id, trust, unscoped, system) = (
auth_info.get_scope()
)
if trust:
project_id = trust['project_id']
if system or domain_id or project_id or trust:
# scope is specified
return
# Skip scoping when unscoped federated token is being issued
if constants.IDENTITY_PROVIDER in auth_context:
return
# Do not scope if request is for explicitly unscoped token
if unscoped is not None:
return
# fill in default_project_id if it is available
try:
user_ref = PROVIDERS.identity_api.get_user(auth_context['user_id'])
except exception.UserNotFound as e:
LOG.warning(six.text_type(e))
raise exception.Unauthorized(e)
default_project_id = user_ref.get('default_project_id')
if not default_project_id:
# User has no default project. He shall get an unscoped token.
return
# make sure user's default project is legit before scoping to it
try:
default_project_ref = PROVIDERS.resource_api.get_project(
default_project_id)
default_project_domain_ref = PROVIDERS.resource_api.get_domain(
default_project_ref['domain_id'])
if (default_project_ref.get('enabled', True) and
default_project_domain_ref.get('enabled', True)):
if PROVIDERS.assignment_api.get_roles_for_user_and_project(
user_ref['id'], default_project_id):
auth_info.set_scope(project_id=default_project_id)
else:
msg = ("User %(user_id)s doesn't have access to"
" default project %(project_id)s. The token"
" will be unscoped rather than scoped to the"
" project.")
LOG.debug(msg,
{'user_id': user_ref['id'],
'project_id': default_project_id})
else:
msg = ("User %(user_id)s's default project %(project_id)s"
" is disabled. The token will be unscoped rather"
" than scoped to the project.")
LOG.debug(msg,
{'user_id': user_ref['id'],
'project_id': default_project_id})
except (exception.ProjectNotFound, exception.DomainNotFound):
# default project or default project domain doesn't exist,
# will issue unscoped token instead
msg = ("User %(user_id)s's default project %(project_id)s not"
" found. The token will be unscoped rather than"
" scoped to the project.")
LOG.debug(msg, {'user_id': user_ref['id'],
'project_id': default_project_id})
def authenticate(self, request, auth_info, auth_context):
"""Authenticate user."""
# NOTE(notmorgan): This is not super pythonic, but we lean on the
# __setitem__ method in auth_context to handle edge cases and security
# of the attributes set by the plugins. This check to ensure
# `auth_context` is an instance of AuthContext is extra insurance and
# will prevent regressions.
if not isinstance(auth_context, core.AuthContext):
LOG.error(
'`auth_context` passed to the Auth controller '
'`authenticate` method is not of type '
'`keystone.auth.core.AuthContext`. For security '
'purposes this is required. This is likely a programming '
'error. Received object of type `%s`', type(auth_context))
raise exception.Unauthorized(
_('Cannot Authenticate due to internal error.'))
# The 'external' method allows any 'REMOTE_USER' based authentication
# In some cases the server can set REMOTE_USER as '' instead of
# dropping it, so this must be filtered out
if request.remote_user:
try:
external = core.get_auth_method('external')
resp = external.authenticate(request,
auth_info)
if resp and resp.status:
# NOTE(notmorgan): ``external`` plugin cannot be multi-step
# it is either a plain success/fail.
auth_context.setdefault(
'method_names', []).insert(0, 'external')
# NOTE(notmorgan): All updates to auth_context is handled
# here in the .authenticate method.
auth_context.update(resp.response_data or {})
except exception.AuthMethodNotSupported:
# This will happen there is no 'external' plugin registered
# and the container is performing authentication.
# The 'kerberos' and 'saml' methods will be used this way.
# In those cases, it is correct to not register an
# 'external' plugin; if there is both an 'external' and a
# 'kerberos' plugin, it would run the check on identity twice.
LOG.debug("No 'external' plugin is registered.")
except exception.Unauthorized:
# If external fails then continue and attempt to determine
# user identity using remaining auth methods
LOG.debug("Authorization failed for 'external' auth method.")
# need to aggregate the results in case two or more methods
# are specified
auth_response = {'methods': []}
for method_name in auth_info.get_method_names():
method = core.get_auth_method(method_name)
resp = method.authenticate(request,
auth_info.get_method_data(method_name))
if resp:
if resp.status:
auth_context.setdefault(
'method_names', []).insert(0, method_name)
# NOTE(notmorgan): All updates to auth_context is handled
# here in the .authenticate method. If the auth attempt was
# not successful do not update the auth_context
resp_method_names = resp.response_data.pop(
'method_names', [])
auth_context['method_names'].extend(resp_method_names)
auth_context.update(resp.response_data or {})
elif resp.response_body:
auth_response['methods'].append(method_name)
auth_response[method_name] = resp.response_body
if auth_response["methods"]:
# authentication continuation required
raise exception.AdditionalAuthRequired(auth_response)
if 'user_id' not in auth_context:
msg = 'User not found by auth plugin; authentication failed'
tr_msg = _('User not found by auth plugin; authentication failed')
LOG.warning(msg)
raise exception.Unauthorized(tr_msg)
@controller.protected()
def check_token(self, request):
token_id = request.subject_token
window_seconds = authorization.token_validation_window(request)
include_catalog = 'nocatalog' not in request.params
token = PROVIDERS.token_provider_api.validate_token(
token_id, window_seconds=window_seconds)
token_reference = controller.render_token_response_from_model(
token, include_catalog=include_catalog
)
# NOTE(morganfainberg): The code in
# ``keystone.common.wsgi.render_response`` will remove the content
# body.
return render_token_data_response(token.id, token_reference)
@controller.protected()
def revoke_token(self, request):
return PROVIDERS.token_provider_api.revoke_token(request.subject_token)
@controller.protected()
def validate_token(self, request):
token_id = request.subject_token
window_seconds = authorization.token_validation_window(request)
include_catalog = 'nocatalog' not in request.params
token = PROVIDERS.token_provider_api.validate_token(
token_id, window_seconds=window_seconds)
token_reference = controller.render_token_response_from_model(
token, include_catalog=include_catalog
)
return render_token_data_response(token.id, token_reference)
def revocation_list(self, request):
if not CONF.token.revoke_by_id:
raise exception.Gone()
# NOTE(lbragstad): This API is deprecated and isn't supported. Keystone
# also doesn't store tokens, so returning a list of revoked tokens
# would require keystone to write invalid tokens to disk, which defeats
# the purpose. Return a 403 instead of removing the API all together.
# The alternative would be to return a signed response of just an empty
# list.
raise exception.Forbidden()
def _combine_lists_uniquely(self, a, b):
# it's most likely that only one of these will be filled so avoid
# the combination if possible.
if a and b:
return {x['id']: x for x in a + b}.values()
else:
return a or b
@controller.protected()
def get_auth_projects(self, request):
user_id = request.auth_context.get('user_id')
group_ids = request.auth_context.get('group_ids')
user_refs = []
if user_id:
try:
user_refs = PROVIDERS.assignment_api.list_projects_for_user(
user_id
)
except exception.UserNotFound: # nosec
# federated users have an id but they don't link to anything
pass
grp_refs = []
if group_ids:
grp_refs = PROVIDERS.assignment_api.list_projects_for_groups(
group_ids
)
refs = self._combine_lists_uniquely(user_refs, grp_refs)
return resource_controllers.ProjectV3.wrap_collection(
request.context_dict, refs)
@controller.protected()
def get_auth_domains(self, request):
user_id = request.auth_context.get('user_id')
group_ids = request.auth_context.get('group_ids')
user_refs = []
if user_id:
try:
user_refs = PROVIDERS.assignment_api.list_domains_for_user(
user_id
)
except exception.UserNotFound: # nosec
# federated users have an id but they don't link to anything
pass
grp_refs = []
if group_ids:
grp_refs = PROVIDERS.assignment_api.list_domains_for_groups(
group_ids
)
refs = self._combine_lists_uniquely(user_refs, grp_refs)
return resource_controllers.DomainV3.wrap_collection(
request.context_dict, refs)
@controller.protected()
def get_auth_system(self, request):
user_id = request.auth_context.get('user_id')
group_ids = request.auth_context.get('group_ids')
user_assignments = []
if user_id:
try:
user_assignments = (
PROVIDERS.assignment_api.list_system_grants_for_user(
user_id
)
)
except exception.UserNotFound: # nosec
# federated users have an id but they don't link to anything
pass
group_assignments = []
if group_ids:
group_assignments = (
PROVIDERS.assignment_api.list_system_grants_for_group(
group_ids
)
)
assignments = self._combine_lists_uniquely(
user_assignments, group_assignments
)
if assignments:
response = {
'system': [{'all': True}],
'links': {
'self': self.base_url(
request.context_dict, path='auth/system'
)
}
}
else:
response = {
'system': [],
'links': {
'self': self.base_url(
request.context_dict, path='auth/system'
)
}
}
return response
@controller.protected()
def get_auth_catalog(self, request):
user_id = request.auth_context.get('user_id')
project_id = request.auth_context.get('project_id')
if not project_id:
raise exception.Forbidden(
_('A project-scoped token is required to produce a service '
'catalog.'))
# The V3Controller base methods mostly assume that you're returning
# either a collection or a single element from a collection, neither of
# which apply to the catalog. Because this is a special case, this
# re-implements a tiny bit of work done by the base controller (such as
# self-referential link building) to avoid overriding or refactoring
# several private methods.
return {
'catalog': PROVIDERS.catalog_api.get_v3_catalog(
user_id, project_id
),
'links': {'self': self.base_url(request.context_dict,
path='auth/catalog')}
}
# FIXME(gyee): not sure if it belongs here or keystone.common. Park it here
# for now.
def render_token_data_response(token_id, token_data, created=False):
"""Render token data HTTP response.
Stash token ID into the X-Subject-Token header.
"""
headers = [('X-Subject-Token', token_id)]
if created:
status = (201, 'Created')
else:
status = (200, 'OK')
return wsgi.render_response(body=token_data,
status=status, headers=headers)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import flask
from oslo_log import log
import six
@ -35,18 +36,18 @@ class Token(base.AuthMethodHandler):
token_id = auth_payload['id']
return PROVIDERS.token_provider_api.validate_token(token_id)
def authenticate(self, request, auth_payload):
def authenticate(self, auth_payload):
if 'id' not in auth_payload:
raise exception.ValidationError(attribute='id',
target='token')
token = self._get_token_ref(auth_payload)
if token.is_federated and PROVIDERS.federation_api:
response_data = mapped.handle_scoped_token(
request, token, PROVIDERS.federation_api,
token, PROVIDERS.federation_api,
PROVIDERS.identity_api
)
else:
response_data = token_authenticate(request, token)
response_data = token_authenticate(token)
# NOTE(notmorgan): The Token auth method is *very* special and sets the
# previous values to the method_names. This is because it can be used
@ -58,7 +59,7 @@ class Token(base.AuthMethodHandler):
response_data=response_data)
def token_authenticate(request, token):
def token_authenticate(token):
response_data = {}
try:
@ -67,10 +68,11 @@ def token_authenticate(request, token):
# state in Keystone. To do so is to invite elevation of
# privilege attacks
project_scoped = 'project' in request.json_body['auth'].get(
json_body = flask.request.get_json(silent=True, force=True) or {}
project_scoped = 'project' in json_body['auth'].get(
'scope', {}
)
domain_scoped = 'domain' in request.json_body['auth'].get(
domain_scoped = 'domain' in json_body['auth'].get(
'scope', {}
)

View File

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

View File

@ -1,77 +0,0 @@
# Copyright 2012 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from keystone.api._shared import json_home_relations
from keystone.auth import controllers
from keystone.common import json_home
from keystone.common import wsgi
class Routers(wsgi.RoutersBase):
_path_prefixes = ('auth',)
def append_v3_routers(self, mapper, routers):
auth_controller = controllers.Auth()
self._add_resource(
mapper, auth_controller,
path='/auth/tokens',
get_action='validate_token',
head_action='check_token',
post_action='authenticate_for_token',
delete_action='revoke_token',
rel=json_home.build_v3_resource_relation('auth_tokens'))
self._add_resource(
mapper, auth_controller,
path='/auth/tokens/OS-PKI/revoked',
get_head_action='revocation_list',
rel=json_home.build_v3_extension_resource_relation(
'OS-PKI', '1.0', 'revocations'))
self._add_resource(
mapper, auth_controller,
path='/auth/catalog',
get_head_action='get_auth_catalog',
rel=json_home.build_v3_resource_relation('auth_catalog'))
self._add_resource(
mapper, auth_controller,
path='/auth/projects',
get_head_action='get_auth_projects',
rel=json_home.build_v3_resource_relation('auth_projects'))
self._add_resource(
mapper, auth_controller,
path='/auth/domains',
get_head_action='get_auth_domains',
rel=json_home.build_v3_resource_relation('auth_domains'))
# NOTE(morgan): explicitly add json_home data for auth_projects and
# auth_domains for OS-FEDERATION here, as auth will always own it
# based upon how the flask scaffolding works. This bit is transitional
# for the move to flask.
for element in ['projects', 'domains']:
resource_data = {'href': '/auth/%s' % element}
json_home.Status.update_resource_data(
resource_data, status=json_home.Status.STABLE)
json_home.JsonHomeResources.append_resource(
json_home_relations.os_federation_resource_rel_func(
resource_name=element), resource_data)
self._add_resource(
mapper, auth_controller,
path='/auth/system',
get_head_action='get_auth_system',
rel=json_home.build_v3_resource_relation('auth_system'))

View File

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

View File

@ -0,0 +1,145 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from keystone.common import provider_api
import keystone.conf
CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
def render_token_response_from_model(token, include_catalog=True):
token_reference = {
'token': {
'methods': token.methods,
'user': {
'domain': {
'id': token.user_domain['id'],
'name': token.user_domain['name']
},
'id': token.user_id,
'name': token.user['name'],
'password_expires_at': token.user[
'password_expires_at'
]
},
'audit_ids': token.audit_ids,
'expires_at': token.expires_at,
'issued_at': token.issued_at,
}
}
if token.system_scoped:
token_reference['token']['roles'] = token.roles
token_reference['token']['system'] = {'all': True}
elif token.domain_scoped:
token_reference['token']['domain'] = {
'id': token.domain['id'],
'name': token.domain['name']
}
token_reference['token']['roles'] = token.roles
elif token.trust_scoped:
token_reference['token']['OS-TRUST:trust'] = {
'id': token.trust_id,
'trustor_user': {'id': token.trustor['id']},
'trustee_user': {'id': token.trustee['id']},
'impersonation': token.trust['impersonation']
}
token_reference['token']['project'] = {
'domain': {
'id': token.project_domain['id'],
'name': token.project_domain['name']
},
'id': token.trust_project['id'],
'name': token.trust_project['name']
}
if token.trust.get('impersonation'):
trustor_domain = PROVIDERS.resource_api.get_domain(
token.trustor['domain_id']
)
token_reference['token']['user'] = {
'domain': {
'id': trustor_domain['id'],
'name': trustor_domain['name']
},
'id': token.trustor['id'],
'name': token.trustor['name'],
'password_expires_at': token.trustor[
'password_expires_at'
]
}
token_reference['token']['roles'] = token.roles
elif token.project_scoped:
token_reference['token']['project'] = {
'domain': {
'id': token.project_domain['id'],
'name': token.project_domain['name']
},
'id': token.project['id'],
'name': token.project['name']
}
token_reference['token']['is_domain'] = token.project.get(
'is_domain', False
)
token_reference['token']['roles'] = token.roles
ap_name = CONF.resource.admin_project_name
ap_domain_name = CONF.resource.admin_project_domain_name
if ap_name and ap_domain_name:
is_ap = (
token.project['name'] == ap_name and
ap_domain_name == token.project_domain['name']
)
token_reference['token']['is_admin_project'] = is_ap
if include_catalog and not token.unscoped:
user_id = token.user_id
if token.trust_id:
user_id = token.trust['trustor_user_id']
catalog = PROVIDERS.catalog_api.get_v3_catalog(
user_id, token.project_id
)
token_reference['token']['catalog'] = catalog
sps = PROVIDERS.federation_api.get_enabled_service_providers()
if sps:
token_reference['token']['service_providers'] = sps
if token.is_federated:
PROVIDERS.federation_api.get_idp(token.identity_provider_id)
federated_dict = dict(
groups=token.federated_groups,
identity_provider={'id': token.identity_provider_id},
protocol={'id': token.protocol_id},
)
token_reference['token']['user']['OS-FEDERATION'] = (
federated_dict
)
token_reference['token']['user']['domain'] = {
'id': 'Federated', 'name': 'Federated'
}
del token_reference['token']['user']['password_expires_at']
if token.access_token_id:
token_reference['token']['OS-OAUTH1'] = {
'access_token_id': token.access_token_id,
'consumer_id': token.access_token['consumer_id']
}
if token.application_credential_id:
key = 'application_credential'
token_reference['token'][key] = {}
token_reference['token'][key]['id'] = (
token.application_credential['id']
)
token_reference['token'][key]['name'] = (
token.application_credential['name']
)
restricted = not token.application_credential['unrestricted']
token_reference['token'][key]['restricted'] = restricted
return token_reference

View File

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

View File

@ -1,226 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Workflow logic for the Federation service."""
import string
from oslo_log import log
from six.moves import http_client
from six.moves import urllib
import webob
from keystone.auth import controllers as auth_controllers
from keystone.common import controller
from keystone.common import provider_api
from keystone.common import utils as k_utils
from keystone.common import validation
from keystone.common import wsgi
import keystone.conf
from keystone import exception
from keystone.federation import idp as keystone_idp
from keystone.federation import schema
from keystone.federation import utils
from keystone.i18n import _
CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
class _ControllerBase(controller.V3Controller):
"""Base behaviors for federation controllers."""
@classmethod
def base_url(cls, context, path=None):
"""Construct a path and pass it to V3Controller.base_url method."""
path = '/OS-FEDERATION/' + cls.collection_name
return super(_ControllerBase, cls).base_url(context, path=path)
class Auth(auth_controllers.Auth):
def _get_sso_origin_host(self, request):
"""Validate and return originating dashboard URL.
Make sure the parameter is specified in the request's URL as well its
value belongs to a list of trusted dashboards.
:param context: request's context
:raises keystone.exception.ValidationError: ``origin`` query parameter
was not specified. The URL is deemed invalid.
:raises keystone.exception.Unauthorized: URL specified in origin query
parameter does not exist in list of websso trusted dashboards.
:returns: URL with the originating dashboard
"""
origin = request.params.get('origin')
if not origin:
msg = 'Request must have an origin query parameter'
tr_msg = _('Request must have an origin query parameter')
LOG.error(msg)
raise exception.ValidationError(tr_msg)
host = urllib.parse.unquote_plus(origin)
# change trusted_dashboard hostnames to lowercase before comparison
trusted_dashboards = [k_utils.lower_case_hostname(trusted)
for trusted in CONF.federation.trusted_dashboard]
if host not in trusted_dashboards:
msg = '%(host)s is not a trusted dashboard host' % {'host': host}
tr_msg = _('%(host)s is not a trusted dashboard host') % {
'host': host}
LOG.error(msg)
raise exception.Unauthorized(tr_msg)
return host
def federated_authentication(self, request, idp_id, protocol_id):
"""Authenticate from dedicated url endpoint.
Build HTTP request body for federated authentication and inject
it into the ``authenticate_for_token`` function.
"""
auth = {
'identity': {
'methods': [protocol_id],
protocol_id: {
'identity_provider': idp_id,
'protocol': protocol_id
}
}
}
return self.authenticate_for_token(request, auth=auth)
def federated_sso_auth(self, request, protocol_id):
try:
remote_id_name = utils.get_remote_id_parameter(protocol_id)
remote_id = request.environ[remote_id_name]
except KeyError:
msg = 'Missing entity ID from environment'
tr_msg = _('Missing entity ID from environment')
LOG.error(msg)
raise exception.Unauthorized(tr_msg)
host = self._get_sso_origin_host(request)
ref = PROVIDERS.federation_api.get_idp_from_remote_id(remote_id)
# NOTE(stevemar): the returned object is a simple dict that
# contains the idp_id and remote_id.
identity_provider = ref['idp_id']
res = self.federated_authentication(request,
identity_provider,
protocol_id)
token_id = res.headers['X-Subject-Token']
return self.render_html_response(host, token_id)
def federated_idp_specific_sso_auth(self, request, idp_id, protocol_id):
host = self._get_sso_origin_host(request)
# NOTE(lbragstad): We validate that the Identity Provider actually
# exists in the Mapped authentication plugin.
res = self.federated_authentication(request,
idp_id,
protocol_id)
token_id = res.headers['X-Subject-Token']
return self.render_html_response(host, token_id)
def render_html_response(self, host, token_id):
"""Form an HTML Form from a template with autosubmit."""
headers = [('Content-Type', 'text/html')]
with open(CONF.federation.sso_callback_template) as template:
src = string.Template(template.read())
subs = {'host': host, 'token': token_id}
body = src.substitute(subs)
return webob.Response(body=body, status='200', charset='utf-8',
headerlist=headers)
def _create_base_saml_assertion(self, context, auth):
issuer = CONF.saml.idp_entity_id
sp_id = auth['scope']['service_provider']['id']
service_provider = PROVIDERS.federation_api.get_sp(sp_id)
utils.assert_enabled_service_provider_object(service_provider)
sp_url = service_provider['sp_url']
token_id = auth['identity']['token']['id']
token = PROVIDERS.token_provider_api.validate_token(token_id)
if not token.project_scoped:
action = _('Use a project scoped token when attempting to create '
'a SAML assertion')
raise exception.ForbiddenAction(action=action)
subject = token.user['name']
role_names = []
for role in token.roles:
role_names.append(role['name'])
project = token.project['name']
# NOTE(rodrigods): the domain name is necessary in order to distinguish
# between projects and users with the same name in different domains.
project_domain_name = token.project_domain['name']
subject_domain_name = token.user_domain['name']
generator = keystone_idp.SAMLGenerator()
response = generator.samlize_token(
issuer, sp_url, subject, subject_domain_name,
role_names, project, project_domain_name)
return (response, service_provider)
def _build_response_headers(self, service_provider):
# URLs in header are encoded into bytes
return [('Content-Type', 'text/xml'),
('X-sp-url', service_provider['sp_url'].encode('utf-8')),
('X-auth-url', service_provider['auth_url'].encode('utf-8'))]
def create_saml_assertion(self, request, auth):
"""Exchange a scoped token for a SAML assertion.
:param auth: Dictionary that contains a token and service provider ID
:returns: SAML Assertion based on properties from the token
"""
validation.lazy_validate(schema.saml_create, auth)
t = self._create_base_saml_assertion(request.context_dict, auth)
(response, service_provider) = t
headers = self._build_response_headers(service_provider)
return wsgi.render_response(
body=response.to_string(),
status=(http_client.OK, http_client.responses[http_client.OK]),
headers=headers)
def create_ecp_assertion(self, request, auth):
"""Exchange a scoped token for an ECP assertion.
:param auth: Dictionary that contains a token and service provider ID
:returns: ECP Assertion based on properties from the token
"""
validation.lazy_validate(schema.saml_create, auth)
t = self._create_base_saml_assertion(request.context_dict, auth)
(saml_assertion, service_provider) = t
relay_state_prefix = service_provider['relay_state_prefix']
generator = keystone_idp.ECPGenerator()
ecp_assertion = generator.generate_ecp(saml_assertion,
relay_state_prefix)
headers = self._build_response_headers(service_provider)
return wsgi.render_response(
body=ecp_assertion.to_string(),
status=(http_client.OK, http_client.responses[http_client.OK]),
headers=headers)

View File

@ -1,95 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import functools
from keystone.common import json_home
from keystone.common import wsgi
from keystone.federation import controllers
build_resource_relation = functools.partial(
json_home.build_v3_extension_resource_relation,
extension_name='OS-FEDERATION', extension_version='1.0')
build_parameter_relation = functools.partial(
json_home.build_v3_extension_parameter_relation,
extension_name='OS-FEDERATION', extension_version='1.0')
IDP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='idp_id')
PROTOCOL_ID_PARAMETER_RELATION = build_parameter_relation(
parameter_name='protocol_id')
SP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='sp_id')
class Routers(wsgi.RoutersBase):
"""API Endpoints for the Federation extension.
The API looks like::
GET /auth/OS-FEDERATION/identity_providers/
{idp_id}/protocols/{protocol_id}/websso
?origin=https%3A//horizon.example.com
POST /auth/OS-FEDERATION/identity_providers/
{idp_id}/protocols/{protocol_id}/websso
?origin=https%3A//horizon.example.com
POST /auth/OS-FEDERATION/saml2
POST /auth/OS-FEDERATION/saml2/ecp
GET /auth/OS-FEDERATION/websso/{protocol_id}
?origin=https%3A//horizon.example.com
POST /auth/OS-FEDERATION/websso/{protocol_id}
?origin=https%3A//horizon.example.com
"""
_path_prefixes = ('auth',)
def _construct_url(self, suffix):
return "/OS-FEDERATION/%s" % suffix
def append_v3_routers(self, mapper, routers):
auth_controller = controllers.Auth()
# Auth operations
self._add_resource(
mapper, auth_controller,
path='/auth' + self._construct_url('saml2'),
post_action='create_saml_assertion',
rel=build_resource_relation(resource_name='saml2'))
self._add_resource(
mapper, auth_controller,
path='/auth' + self._construct_url('saml2/ecp'),
post_action='create_ecp_assertion',
rel=build_resource_relation(resource_name='ecp'))
self._add_resource(
mapper, auth_controller,
path='/auth' + self._construct_url('websso/{protocol_id}'),
get_post_action='federated_sso_auth',
rel=build_resource_relation(resource_name='websso'),
path_vars={
'protocol_id': PROTOCOL_ID_PARAMETER_RELATION,
})
self._add_resource(
mapper, auth_controller,
path='/auth' + self._construct_url(
'identity_providers/{idp_id}/protocols/{protocol_id}/websso'),
get_post_action='federated_idp_specific_sso_auth',
rel=build_resource_relation(
resource_name='identity_providers_websso'),
path_vars={
'idp_id': IDP_ID_PARAMETER_RELATION,
'protocol_id': PROTOCOL_ID_PARAMETER_RELATION,
})

View File

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

View File

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

View File

@ -900,10 +900,19 @@ class Manager(manager.Manager):
# - clear/set domain_ids for drivers that do not support domains
# - create any ID mapping that might be required
# TODO(morgan): The split of "authenticate" and "_authenticate" is done
# until user API is converted to flask. This is to make webob and flask
# play nicely with the authenticate mechanism during self-service password
# changes. While this is in place, CADF notifications will not be emitted
# for self-service password changes indicating an auth attempt was being
# made. This is a very limited time transitional change.
@notifications.emit_event('authenticate')
def authenticate(self, user_id, password):
return self._authenticate(user_id, password)
@domains_configured
@exception_translated('assertion')
def authenticate(self, request, user_id, password):
def _authenticate(self, user_id, password):
domain_id, driver, entity_id = (
self._get_domain_driver_and_entity_id(user_id))
ref = driver.authenticate(entity_id, password)
@ -1376,12 +1385,14 @@ class Manager(manager.Manager):
group_entity_id)
@domains_configured
def change_password(self, request, user_id, original_password,
def change_password(self, user_id, original_password,
new_password, initiator=None):
# authenticate() will raise an AssertionError if authentication fails
try:
self.authenticate(request, user_id, original_password)
# TODO(morgan): When users is ported to flask, ensure this is
# mapped back to self.authenticate instead of self._authenticate.
self._authenticate(user_id, original_password)
except exception.PasswordExpired:
# If a password has expired, we want users to be able to change it
pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -146,6 +146,8 @@ def initialize_application(name, post_log_configured_function=lambda: None,
config_files = [dev_conf]
keystone.server.configure(config_files=config_files)
# explicitly load auth configuration
keystone.conf.auth.setup_authentication()
# Log the options used when starting if we're in debug mode...
if CONF.debug:

View File

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

View File

@ -745,25 +745,26 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
user_ref = unit.new_user_ref(domain_id=self.domain_id,
password=password)
user_ref = PROVIDERS.identity_api.create_user(user_ref)
PROVIDERS.identity_api.authenticate(
self.make_request(), user_ref['id'], password
)
with self.make_request():
PROVIDERS.identity_api.authenticate(user_ref['id'], password)
freezer.stop()
reason_type = (exception.PasswordExpired.message_format %
{'user_id': user_ref['id']})
expected_reason = {'reasonCode': '401',
'reasonType': reason_type}
self.assertRaises(exception.PasswordExpired,
PROVIDERS.identity_api.authenticate,
self.make_request(),
user_id=user_ref['id'],
password=password)
with self.make_request():
self.assertRaises(exception.PasswordExpired,
PROVIDERS.identity_api.authenticate,
user_id=user_ref['id'],
password=password)
self._assert_last_audit(None, 'authenticate', None,
cadftaxonomy.ACCOUNT_USER,
reason=expected_reason)
def test_locked_out_user_sends_notification(self):
# TODO(morgan): skip this test until users is ported to flask.
self.skipTest('Users are not handled via flask.')
password = uuid.uuid4().hex
new_password = uuid.uuid4().hex
expected_responses = [AssertionError, AssertionError, AssertionError,
@ -776,12 +777,12 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
expected_reason = {'reasonCode': '401',
'reasonType': reason_type}
for ex in expected_responses:
self.assertRaises(ex,
PROVIDERS.identity_api.change_password,
self.make_request(),
user_id=user_ref['id'],
original_password=new_password,
new_password=new_password)
with self.make_request():
self.assertRaises(ex,
PROVIDERS.identity_api.change_password,
user_id=user_ref['id'],
original_password=new_password,
new_password=new_password)
self._assert_last_audit(None, 'authenticate', None,
cadftaxonomy.ACCOUNT_USER,
@ -801,16 +802,17 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
user_ref = unit.new_user_ref(domain_id=self.domain_id,
password=password)
user_ref = PROVIDERS.identity_api.create_user(user_ref)
PROVIDERS.identity_api.change_password(
self.make_request(), user_id=user_ref['id'],
original_password=password, new_password=new_password
)
self.assertRaises(exception.PasswordValidationError,
PROVIDERS.identity_api.change_password,
self.make_request(),
user_id=user_ref['id'],
original_password=new_password,
new_password=password)
with self.make_request():
PROVIDERS.identity_api.change_password(
user_id=user_ref['id'],
original_password=password, new_password=new_password
)
with self.make_request():
self.assertRaises(exception.PasswordValidationError,
PROVIDERS.identity_api.change_password,
user_id=user_ref['id'],
original_password=new_password,
new_password=password)
self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user',
cadftaxonomy.SECURITY_ACCOUNT_USER,
@ -828,12 +830,12 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
user_ref = unit.new_user_ref(domain_id=self.domain_id,
password=password)
user_ref = PROVIDERS.identity_api.create_user(user_ref)
self.assertRaises(exception.PasswordValidationError,
PROVIDERS.identity_api.change_password,
self.make_request(),
user_id=user_ref['id'],
original_password=password,
new_password=invalid_password)
with self.make_request():
self.assertRaises(exception.PasswordValidationError,
PROVIDERS.identity_api.change_password,
user_id=user_ref['id'],
original_password=password,
new_password=invalid_password)
self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user',
cadftaxonomy.SECURITY_ACCOUNT_USER,
@ -858,16 +860,17 @@ class CADFNotificationsForPCIDSSEvents(BaseNotificationTest):
{'min_age_days': min_days, 'days_left': days_left})
expected_reason = {'reasonCode': '400',
'reasonType': reason_type}
PROVIDERS.identity_api.change_password(
self.make_request(), user_id=user_ref['id'],
original_password=password, new_password=new_password
)
self.assertRaises(exception.PasswordValidationError,
PROVIDERS.identity_api.change_password,
self.make_request(),
user_id=user_ref['id'],
original_password=new_password,
new_password=next_password)
with self.make_request():
PROVIDERS.identity_api.change_password(
user_id=user_ref['id'],
original_password=password, new_password=new_password
)
with self.make_request():
self.assertRaises(exception.PasswordValidationError,
PROVIDERS.identity_api.change_password,
user_id=user_ref['id'],
original_password=new_password,
new_password=next_password)
self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user',
cadftaxonomy.SECURITY_ACCOUNT_USER,

View File

@ -10,11 +10,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import flask
import uuid
from oslo_config import fixture as config_fixture
from oslo_serialization import jsonutils
import webob
from keystone.auth.plugins import mapped
import keystone.conf
@ -31,6 +31,13 @@ FAKE_MAPPING_ID = uuid.uuid4().hex
class MappingRuleEngineTests(unit.BaseTestCase):
"""A class for testing the mapping rule engine."""
def setUp(self):
super(MappingRuleEngineTests, self).setUp()
# create dummy app so we can setup a request context for our
# tests.
self.flask_app = flask.Flask(__name__)
self.cleanup_instance('flask_app')
def assertValidMappedUserObject(self, mapped_properties,
user_type='ephemeral',
domain_id=None):
@ -510,7 +517,7 @@ class MappingRuleEngineTests(unit.BaseTestCase):
self.assertValidMappedUserObject(mapped_properties)
self.assertEqual('jsmith', mapped_properties['user']['name'])
unique_id, display_name = mapped.get_user_unique_id_and_display_name(
{}, mapped_properties)
mapped_properties)
self.assertEqual('jsmith', unique_id)
self.assertEqual('jsmith', display_name)
@ -533,7 +540,7 @@ class MappingRuleEngineTests(unit.BaseTestCase):
self.assertIsNotNone(mapped_properties)
self.assertValidMappedUserObject(mapped_properties)
unique_id, display_name = mapped.get_user_unique_id_and_display_name(
{}, mapped_properties)
mapped_properties)
self.assertEqual('tbo', display_name)
self.assertEqual('abc123%40example.com', unique_id)
@ -549,15 +556,15 @@ class MappingRuleEngineTests(unit.BaseTestCase):
as it was not explicitly specified in the mapping.
"""
request = webob.Request.blank('/')
mapping = mapping_fixtures.MAPPING_USER_IDS
rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules'])
assertion = mapping_fixtures.ADMIN_ASSERTION
mapped_properties = rp.process(assertion)
self.assertIsNotNone(mapped_properties)
self.assertValidMappedUserObject(mapped_properties)
unique_id, display_name = mapped.get_user_unique_id_and_display_name(
request, mapped_properties)
with self.flask_app.test_request_context():
unique_id, display_name = (
mapped.get_user_unique_id_and_display_name(mapped_properties))
self.assertEqual('bob', unique_id)
self.assertEqual('bob', display_name)
@ -566,13 +573,14 @@ class MappingRuleEngineTests(unit.BaseTestCase):
mapping = mapping_fixtures.MAPPING_USER_IDS
assertion = mapping_fixtures.ADMIN_ASSERTION
FAKE_MAPPING_ID = uuid.uuid4().hex
request = webob.Request.blank('/', remote_user='remote_user')
rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules'])
mapped_properties = rp.process(assertion)
self.assertIsNotNone(mapped_properties)
self.assertValidMappedUserObject(mapped_properties)
unique_id, display_name = mapped.get_user_unique_id_and_display_name(
request, mapped_properties)
with self.flask_app.test_request_context(
environ_base={'REMOTE_USER': 'remote_user'}):
unique_id, display_name = (
mapped.get_user_unique_id_and_display_name(mapped_properties))
self.assertEqual('bob', unique_id)
self.assertEqual('remote_user', display_name)
@ -597,7 +605,6 @@ class MappingRuleEngineTests(unit.BaseTestCase):
not to change it.
"""
request = webob.Request.blank('/')
testcases = [(mapping_fixtures.CUSTOMER_ASSERTION, 'bwilliams'),
(mapping_fixtures.EMPLOYEE_ASSERTION, 'tbo')]
for assertion, exp_user_name in testcases:
@ -607,8 +614,7 @@ class MappingRuleEngineTests(unit.BaseTestCase):
self.assertIsNotNone(mapped_properties)
self.assertValidMappedUserObject(mapped_properties)
unique_id, display_name = (
mapped.get_user_unique_id_and_display_name(request,
mapped_properties)
mapped.get_user_unique_id_and_display_name(mapped_properties)
)
self.assertEqual(exp_user_name, display_name)
self.assertEqual('abc123%40example.com', unique_id)
@ -821,12 +827,14 @@ class TestUnicodeAssertionData(unit.BaseTestCase):
# pulled from the HTTP headers. These bytes may be decodable as
# ISO-8859-1 according to Section 3.2.4 of RFC 7230. Let's assume
# that our web server plugins are correctly encoding the data.
request = webob.Request.blank(
'/path',
environ=mapping_fixtures.UNICODE_NAME_ASSERTION)
data = mapping_utils.get_assertion_params_from_env(request)
# NOTE(dstanek): keystone.auth.plugins.mapped
return dict(data)
# Create a dummy application
app = flask.Flask(__name__)
with app.test_request_context(
path='/path',
environ_overrides=mapping_fixtures.UNICODE_NAME_ASSERTION):
data = mapping_utils.get_assertion_params_from_env()
# NOTE(dstanek): keystone.auth.plugins.mapped
return dict(data)
def test_unicode(self):
mapping = self._pull_mapping_rules_from_the_database()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,6 @@ from six.moves import http_client
from testtools import matchers
import webtest
from keystone import auth
from keystone.common import authorization
from keystone.common import cache
from keystone.common import provider_api
@ -1215,18 +1214,6 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase,
for attribute in attributes:
self.assertIsNotNone(entity.get(attribute))
def build_external_auth_request(self, remote_user,
remote_domain=None, auth_data=None,
kerberos=False):
environment = self.build_external_auth_environ(
remote_user, remote_domain)
if not auth_data:
auth_data = self.build_authentication_request(
kerberos=kerberos)['auth']
auth_info = auth.core.AuthInfo.create(auth_data)
auth_context = auth.core.AuthContext(method_names=[])
return self.make_request(environ=environment), auth_info, auth_context
def build_external_auth_environ(self, remote_user, remote_domain=None):
environment = {'REMOTE_USER': remote_user, 'AUTH_TYPE': 'Negotiate'}
if remote_domain:

View File

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