Standardizing the Federation Process

Split functionality for attribute extraction and mapping layer
for saml2 auth plugin. Renamed to mapped.py, leaving original file
for backwards compatibility.

Co-Authored-By: Guang Yee <guang.yee@hp.com>
Implements: bp generic-mapping-federation

Change-Id: If9a8cbf62334c796a70e939bf00e7b194e5749c8
This commit is contained in:
Kristy Siu 2014-07-31 15:58:13 +01:00 committed by guang-yee
parent a1da397aa8
commit b96a7bd0c5
6 changed files with 184 additions and 72 deletions

View File

@ -473,6 +473,10 @@ class Auth(controller.V3Controller):
# 'external' plugin; if there is both an 'external' and a
# 'kerberos' plugin, it would run the check on identity twice.
pass
except exception.Unauthorized:
# If external fails then continue and attempt to determine
# user identity using remaining auth methods
pass
# need to aggregate the results in case two or more methods
# are specified

View File

@ -0,0 +1,97 @@
# 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 six.moves.urllib import parse
from keystone import auth
from keystone.common import dependency
from keystone.contrib import federation
from keystone.contrib.federation import utils
from keystone.openstack.common import jsonutils
@dependency.requires('federation_api', 'identity_api', 'token_api')
class Mapped(auth.AuthMethodHandler):
def authenticate(self, context, auth_payload, auth_context):
"""Authenticate mapped user and return an authentication context.
:param context: keystone's request context
:param auth_payload: the content of the authentication for a
given method
:param auth_context: user authentication context, a dictionary
shared by all plugins.
In addition to ``user_id`` in ``auth_context``, this plugin sets
``group_ids``, ``OS-FEDERATION:identity_provider`` and
``OS-FEDERATION:protocol``
"""
if 'id' in auth_payload:
fields = self._handle_scoped_token(auth_payload)
else:
fields = self._handle_unscoped_token(context, auth_payload)
auth_context.update(fields)
def _handle_scoped_token(self, auth_payload):
token_ref = self.token_api.get_token(auth_payload['id'])
utils.validate_expiration(token_ref)
_federation = token_ref['user'][federation.FEDERATION]
identity_provider = _federation['identity_provider']['id']
protocol = _federation['protocol']['id']
group_ids = [group['id'] for group in _federation['groups']]
mapping = self.federation_api.get_mapping_from_idp_and_protocol(
identity_provider, protocol)
utils.validate_groups(group_ids, mapping['id'], self.identity_api)
return {
'user_id': token_ref['user_id'],
'group_ids': group_ids,
federation.IDENTITY_PROVIDER: identity_provider,
federation.PROTOCOL: protocol
}
def _handle_unscoped_token(self, context, auth_payload):
user_id, assertion = self._extract_assertion_data(context)
if user_id:
assertion['user_id'] = user_id
identity_provider = auth_payload['identity_provider']
protocol = auth_payload['protocol']
mapped_properties = self._apply_mapping_filter(identity_provider,
protocol,
assertion)
if not user_id:
user_id = parse.quote(mapped_properties['name'])
return {
'user_id': user_id,
'group_ids': mapped_properties['group_ids'],
federation.IDENTITY_PROVIDER: identity_provider,
federation.PROTOCOL: protocol
}
def _extract_assertion_data(self, context):
assertion = dict(utils.get_assertion_params_from_env(context))
user_id = context['environment'].get('REMOTE_USER')
return user_id, assertion
def _apply_mapping_filter(self, identity_provider, protocol, assertion):
mapping = self.federation_api.get_mapping_from_idp_and_protocol(
identity_provider, protocol)
rules = jsonutils.loads(mapping['rules'])
rule_processor = utils.RuleProcessor(rules)
mapped_properties = rule_processor.process(assertion)
utils.validate_groups(mapped_properties['group_ids'],
mapping['id'], self.identity_api)
return mapped_properties

View File

@ -10,77 +10,18 @@
# License for the specific language governing permissions and limitations
# under the License.
from six.moves.urllib import parse
from keystone.auth.plugins import mapped
from keystone import auth
from keystone.common import dependency
from keystone.contrib import federation
from keystone.contrib.federation import utils
from keystone.openstack.common import jsonutils
""" Provide an entry point to authenticate with SAML2
This plugin subclasses mapped.Mapped, and may be specified in keystone.conf:
[auth]
methods = external,password,token,saml2
saml2 = keystone.auth.plugins.mapped.Mapped
"""
@dependency.requires('federation_api', 'identity_api', 'token_api')
class Saml2(auth.AuthMethodHandler):
class Saml2(mapped.Mapped):
method = 'saml2'
def authenticate(self, context, auth_payload, auth_context):
"""Authenticate federated user and return an authentication context.
:param context: keystone's request context
:param auth_payload: the content of the authentication for a
given method
:param auth_context: user authentication context, a dictionary
shared by all plugins.
In addition to ``user_id`` in ``auth_context``, the ``saml2``
plugin sets ``group_ids``. When handling unscoped tokens,
``OS-FEDERATION:identity_provider`` and ``OS-FEDERATION:protocol``
are set as well.
"""
if 'id' in auth_payload:
fields = self._handle_scoped_token(auth_payload)
else:
fields = self._handle_unscoped_token(context, auth_payload)
auth_context.update(fields)
def _handle_scoped_token(self, auth_payload):
token_ref = self.token_api.get_token(auth_payload['id'])
utils.validate_expiration(token_ref)
_federation = token_ref['user'][federation.FEDERATION]
identity_provider = _federation['identity_provider']['id']
protocol = _federation['protocol']['id']
group_ids = [group['id'] for group in _federation['groups']]
mapping = self.federation_api.get_mapping_from_idp_and_protocol(
identity_provider, protocol)
utils.validate_groups(group_ids, mapping['id'], self.identity_api)
return {
'user_id': token_ref['user_id'],
'group_ids': group_ids,
federation.IDENTITY_PROVIDER: identity_provider,
federation.PROTOCOL: protocol
}
def _handle_unscoped_token(self, context, auth_payload):
assertion = dict(utils.get_assertion_params_from_env(context))
identity_provider = auth_payload['identity_provider']
protocol = auth_payload['protocol']
mapping = self.federation_api.get_mapping_from_idp_and_protocol(
identity_provider, protocol)
rules = jsonutils.loads(mapping['rules'])
rule_processor = utils.RuleProcessor(rules)
mapped_properties = rule_processor.process(assertion)
utils.validate_groups(mapped_properties['group_ids'],
mapping['id'], self.identity_api)
return {
'user_id': parse.quote(mapped_properties['name']),
'group_ids': mapped_properties['group_ids'],
federation.IDENTITY_PROVIDER: identity_provider,
federation.PROTOCOL: protocol
}

View File

@ -235,8 +235,8 @@ class Auth(auth_controllers.Auth):
"""
auth = {
'identity': {
'methods': ['saml2'],
'saml2': {
'methods': [protocol],
protocol: {
'identity_provider': identity_provider,
'protocol': protocol
}

View File

@ -1,3 +1,7 @@
[auth]
methods = external,password,token,simple_challenge_response
methods = external,password,token,simple_challenge_response,saml2,openid,x509
simple_challenge_response = keystone.tests.test_auth_plugin.SimpleChallengeResponse
saml2 = keystone.auth.plugins.mapped.Mapped
openid = keystone.auth.plugins.mapped.Mapped
x509 = keystone.auth.plugins.mapped.Mapped

View File

@ -14,6 +14,8 @@
import uuid
import mock
from keystone import auth
from keystone import exception
from keystone import tests
@ -152,3 +154,67 @@ class TestInvalidAuthMethodRegistration(tests.TestCase):
methods=['keystone.tests.test_auth_plugin.NoMethodAuthPlugin'])
self.clear_auth_plugin_registry()
self.assertRaises(ValueError, auth.controllers.load_auth_methods)
class TestMapped(tests.TestCase):
def setUp(self):
super(TestMapped, self).setUp()
self.load_backends()
self.api = auth.controllers.Auth()
def config_files(self):
config_files = super(TestMapped, self).config_files()
config_files.append(tests.dirs.tests_conf('test_auth_plugin.conf'))
return config_files
def config_overrides(self):
# don't override configs so we can use test_auth_plugin.conf only
pass
def _test_mapped_invocation_with_method_name(self, method_name):
with mock.patch.object(auth.plugins.mapped.Mapped,
'authenticate',
return_value=None) as authenticate:
context = {'environment': {}}
auth_data = {
'identity': {
'methods': [method_name],
method_name: {'protocol': method_name},
}
}
auth_info = auth.controllers.AuthInfo.create(context, auth_data)
auth_context = {'extras': {},
'method_names': [],
'user_id': uuid.uuid4().hex}
self.api.authenticate(context, auth_info, auth_context)
# make sure Mapped plugin got invoked with the correct payload
((context, auth_payload, auth_context),
kwargs) = authenticate.call_args
self.assertEqual(method_name, auth_payload['protocol'])
def test_mapped_with_remote_user(self):
with mock.patch.object(auth.plugins.mapped.Mapped,
'authenticate',
return_value=None) as authenticate:
# external plugin should fail and pass to mapped plugin
method_name = 'saml2'
auth_data = {'methods': [method_name]}
# put the method name in the payload so its easier to correlate
# method name with payload
auth_data[method_name] = {'protocol': method_name}
auth_data = {'identity': auth_data}
auth_info = auth.controllers.AuthInfo.create(None, auth_data)
auth_context = {'extras': {},
'method_names': [],
'user_id': uuid.uuid4().hex}
environment = {'environment': {'REMOTE_USER': 'foo@idp.com'}}
self.api.authenticate(environment, auth_info, auth_context)
# make sure Mapped plugin got invoked with the correct payload
((context, auth_payload, auth_context),
kwargs) = authenticate.call_args
self.assertEqual(auth_payload['protocol'], method_name)
def test_supporting_multiple_methods(self):
for method_name in ['saml2', 'openid', 'x509']:
self._test_mapped_invocation_with_method_name(method_name)