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:
parent
a1da397aa8
commit
b96a7bd0c5
@ -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
|
||||
|
97
keystone/auth/plugins/mapped.py
Normal file
97
keystone/auth/plugins/mapped.py
Normal 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
|
@ -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:
|
||||
|
||||
@dependency.requires('federation_api', 'identity_api', 'token_api')
|
||||
class Saml2(auth.AuthMethodHandler):
|
||||
|
||||
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.
|
||||
|
||||
[auth]
|
||||
methods = external,password,token,saml2
|
||||
saml2 = keystone.auth.plugins.mapped.Mapped
|
||||
"""
|
||||
|
||||
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)
|
||||
class Saml2(mapped.Mapped):
|
||||
|
||||
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
|
||||
}
|
||||
method = 'saml2'
|
||||
|
@ -235,8 +235,8 @@ class Auth(auth_controllers.Auth):
|
||||
"""
|
||||
auth = {
|
||||
'identity': {
|
||||
'methods': ['saml2'],
|
||||
'saml2': {
|
||||
'methods': [protocol],
|
||||
protocol: {
|
||||
'identity_provider': identity_provider,
|
||||
'protocol': protocol
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user