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
|
# 'external' plugin; if there is both an 'external' and a
|
||||||
# 'kerberos' plugin, it would run the check on identity twice.
|
# 'kerberos' plugin, it would run the check on identity twice.
|
||||||
pass
|
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
|
# need to aggregate the results in case two or more methods
|
||||||
# are specified
|
# are specified
|
||||||
|
|
|
@ -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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from six.moves.urllib import parse
|
from keystone.auth.plugins import mapped
|
||||||
|
|
||||||
from keystone import auth
|
""" Provide an entry point to authenticate with SAML2
|
||||||
from keystone.common import dependency
|
|
||||||
from keystone.contrib import federation
|
This plugin subclasses mapped.Mapped, and may be specified in keystone.conf:
|
||||||
from keystone.contrib.federation import utils
|
|
||||||
from keystone.openstack.common import jsonutils
|
[auth]
|
||||||
|
methods = external,password,token,saml2
|
||||||
|
saml2 = keystone.auth.plugins.mapped.Mapped
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@dependency.requires('federation_api', 'identity_api', 'token_api')
|
class Saml2(mapped.Mapped):
|
||||||
class Saml2(auth.AuthMethodHandler):
|
|
||||||
|
|
||||||
method = 'saml2'
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -235,8 +235,8 @@ class Auth(auth_controllers.Auth):
|
||||||
"""
|
"""
|
||||||
auth = {
|
auth = {
|
||||||
'identity': {
|
'identity': {
|
||||||
'methods': ['saml2'],
|
'methods': [protocol],
|
||||||
'saml2': {
|
protocol: {
|
||||||
'identity_provider': identity_provider,
|
'identity_provider': identity_provider,
|
||||||
'protocol': protocol
|
'protocol': protocol
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
[auth]
|
[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
|
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 uuid
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
from keystone import auth
|
from keystone import auth
|
||||||
from keystone import exception
|
from keystone import exception
|
||||||
from keystone import tests
|
from keystone import tests
|
||||||
|
@ -152,3 +154,67 @@ class TestInvalidAuthMethodRegistration(tests.TestCase):
|
||||||
methods=['keystone.tests.test_auth_plugin.NoMethodAuthPlugin'])
|
methods=['keystone.tests.test_auth_plugin.NoMethodAuthPlugin'])
|
||||||
self.clear_auth_plugin_registry()
|
self.clear_auth_plugin_registry()
|
||||||
self.assertRaises(ValueError, auth.controllers.load_auth_methods)
|
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