diff options
author | Jenkins <jenkins@review.openstack.org> | 2013-07-17 05:01:50 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2013-07-17 05:01:50 +0000 |
commit | b5384074dcc6229c2c23bd5edd2981d71607fe3a (patch) | |
tree | fcc27c11caeb276365d5390808396793dc20f1a7 | |
parent | 7f4891ddc3da7457df09c0cc8bbfe8a888063feb (diff) | |
parent | 88c319e6bce98082f9a90b8b27726793d5366326 (diff) | |
download | keystone-b5384074dcc6229c2c23bd5edd2981d71607fe3a.tar.gz keystone-b5384074dcc6229c2c23bd5edd2981d71607fe3a.tar.xz keystone-b5384074dcc6229c2c23bd5edd2981d71607fe3a.zip |
Merge "Pluggable Remote User"
-rw-r--r-- | doc/source/configuration.rst | 11 | ||||
-rw-r--r-- | etc/keystone.conf.sample | 3 | ||||
-rw-r--r-- | keystone/auth/controllers.py | 22 | ||||
-rw-r--r-- | keystone/auth/plugins/external.py | 80 | ||||
-rw-r--r-- | keystone/common/config.py | 8 | ||||
-rw-r--r-- | tests/auth_plugin_external_disabled.conf | 2 | ||||
-rw-r--r-- | tests/auth_plugin_external_domain.conf | 3 | ||||
-rw-r--r-- | tests/test_auth_plugin.conf | 2 | ||||
-rw-r--r-- | tests/test_v3_auth.py | 97 |
9 files changed, 196 insertions, 32 deletions
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index eb08a49c..c4db1be2 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -107,7 +107,10 @@ file. It is up to the plugin to register its own configuration options. * ``methods`` - comma-delimited list of authentication plugin names * ``<plugin name>`` - specify the class which handles to authentication method, in the same manner as one would specify a backend driver. -Keystone provides two authentication methods by default. ``password`` handles password authentication and ``token`` handles token authentication. +Keystone provides three authentication methods by default. ``password`` handles password +authentication and ``token`` handles token authentication. ``external`` is used in conjunction +with authentication performed by a container web server that sets the ``REMOTE_USER`` +environment variable. How to Implement an Authentication Plugin ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -149,6 +152,12 @@ invoked, all plugins must succeed in order to for the entire authentication to be successful. Furthermore, all the plugins invoked must agree on the ``user_id`` in the ``auth_context``. +The ``REMOTE_USER`` environment variable is only set from a containing webserver. However, +to ensure that a user must go through other authentication mechanisms, even if this variable +is set, remove ``external`` from the list of plugins specified in ``methods``. This effectively +disables external authentication. + + Token Provider -------------- diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index 5f052328..7fa232f0 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -262,7 +262,8 @@ # user_additional_attribute_mapping = [auth] -methods = password,token +methods = external,password,token +#external = keystone.auth.plugins.external.ExternalDefault password = keystone.auth.plugins.password.Password token = keystone.auth.plugins.token.Token diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py index 47c44b03..cc4c1605 100644 --- a/keystone/auth/controllers.py +++ b/keystone/auth/controllers.py @@ -321,31 +321,13 @@ class Auth(controller.V3Controller): LOG.exception(e) raise exception.Unauthorized(e) - def _build_remote_user_auth_context(self, context, auth_info, - auth_context): - username = context['REMOTE_USER'] - # FIXME(gyee): REMOTE_USER is not good enough since we are - # requiring domain_id to do user lookup now. Try to get - # the user_id from auth_info for now, assuming external auth - # has check to make sure user is the same as the one specify - # in "identity". - if 'password' in auth_info.get_method_names(): - user_info = auth_info.get_method_data('password') - user_ref = auth_info.lookup_user(user_info['user']) - auth_context['user_id'] = user_ref['id'] - else: - msg = _('Unable to lookup user %s') % (username) - raise exception.Unauthorized(msg) - def authenticate(self, context, auth_info, auth_context): """Authenticate user.""" # user have been authenticated externally if 'REMOTE_USER' in context: - self._build_remote_user_auth_context(context, - auth_info, - auth_context) - return + external = get_auth_method('external') + external.authenticate(context, auth_info, auth_context) # need to aggregate the results in case two or more methods # are specified diff --git a/keystone/auth/plugins/external.py b/keystone/auth/plugins/external.py new file mode 100644 index 00000000..3460ab92 --- /dev/null +++ b/keystone/auth/plugins/external.py @@ -0,0 +1,80 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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. + +"""Keystone External Authentication Plugin""" + +from keystone.common import config +from keystone import exception + + +CONF = config.CONF + + +class ExternalDefault(object): + def authenticate(self, context, auth_info, auth_context): + """Use REMOTE_USER to look up the user in the identity backend + + auth_context is an in-out variable that will be updated with the + username from the REMOTE_USER environment variable. + """ + try: + REMOTE_USER = context['REMOTE_USER'] + except KeyError: + msg = _('No authenticated user') + raise exception.Unauthorized(msg) + try: + names = REMOTE_USER.split('@') + username = names.pop(0) + domain_id = CONF.identity.default_domain_id + user_ref = auth_info.identity_api.get_user_by_name(username, + domain_id) + auth_context['user_id'] = user_ref['id'] + except Exception: + msg = _('Unable to lookup user %s') % (REMOTE_USER) + raise exception.Unauthorized(msg) + + +class ExternalDomain(object): + def authenticate(self, context, auth_info, auth_context): + """Use REMOTE_USER to look up the user in the identity backend + + auth_context is an in-out variable that will be updated with the + username from the REMOTE_USER environment variable. + + If REMOTE_USER contains an `@` assume that the substring before the @ + is the username, and the substring after the @ is the domain name. + """ + try: + REMOTE_USER = context['REMOTE_USER'] + except KeyError: + msg = _('No authenticated user') + raise exception.Unauthorized(msg) + try: + names = REMOTE_USER.split('@') + username = names.pop(0) + if names: + domain_name = names[0] + domain_ref = (auth_info.identity_api. + get_domain_by_name(domain_name)) + domain_id = domain_ref['id'] + else: + domain_id = CONF.identity.default_domain_id + user_ref = auth_info.identity_api.get_user_by_name(username, + domain_id) + auth_context['user_id'] = user_ref['id'] + except Exception: + msg = _('Unable to lookup user %s') % (REMOTE_USER) + raise exception.Unauthorized(msg) diff --git a/keystone/common/config.py b/keystone/common/config.py index 8c181596..030ead69 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -24,7 +24,7 @@ from keystone.common import logging _DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" _DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" -_DEFAULT_AUTH_METHODS = ['password', 'token'] +_DEFAULT_AUTH_METHODS = ['external', 'password', 'token'] COMMON_CLI_OPTS = [ cfg.BoolOpt('debug', @@ -394,7 +394,11 @@ def configure(): register_str( 'token', group='auth', default='keystone.auth.plugins.password.Password') - + #deals with REMOTE_USER authentication + register_str( + 'external', + group='auth', + default='keystone.auth.plugins.external.ExternalDefault') # register any non-default auth methods here (used by extensions, etc) for method_name in CONF.auth.methods: if method_name not in _DEFAULT_AUTH_METHODS: diff --git a/tests/auth_plugin_external_disabled.conf b/tests/auth_plugin_external_disabled.conf new file mode 100644 index 00000000..fed281d4 --- /dev/null +++ b/tests/auth_plugin_external_disabled.conf @@ -0,0 +1,2 @@ +[auth] +methods = password, token diff --git a/tests/auth_plugin_external_domain.conf b/tests/auth_plugin_external_domain.conf new file mode 100644 index 00000000..78f08810 --- /dev/null +++ b/tests/auth_plugin_external_domain.conf @@ -0,0 +1,3 @@ +[auth] +methods = external +external = keystone.auth.plugins.external.ExternalDomain
\ No newline at end of file diff --git a/tests/test_auth_plugin.conf b/tests/test_auth_plugin.conf index efe4bcb4..edec8f79 100644 --- a/tests/test_auth_plugin.conf +++ b/tests/test_auth_plugin.conf @@ -1,3 +1,3 @@ [auth] -methods = password,token,simple-challenge-response +methods = external,password,token,simple-challenge-response simple-challenge-response = challenge_response_method.SimpleChallengeResponse diff --git a/tests/test_v3_auth.py b/tests/test_v3_auth.py index 8c4e4a8c..7255d3fc 100644 --- a/tests/test_v3_auth.py +++ b/tests/test_v3_auth.py @@ -746,6 +746,43 @@ class TestTokenRevoking(test_v3.RestfulTestCase): project_id=self.projectA['id'])) +class TestAuthExternalDisabled(test_v3.RestfulTestCase): + def config_files(self): + list = self._config_file_list[:] + list.append('auth_plugin_external_disabled.conf') + return list + + def test_remote_user_disabled(self): + auth_data = self.build_authentication_request()['auth'] + api = auth.controllers.Auth() + context = {'REMOTE_USER': '%s@%s' % (self.user['name'], + self.domain['id'])} + auth_info = auth.controllers.AuthInfo(None, auth_data) + auth_context = {'extras': {}, 'method_names': []} + self.assertRaises(exception.Unauthorized, + api.authenticate, + context, + auth_info, + auth_context) + + +class TestAuthExternalDomain(test_v3.RestfulTestCase): + def config_files(self): + list = self._config_file_list[:] + list.append('auth_plugin_external_domain.conf') + return list + + def test_remote_user_with_realm(self): + auth_data = self.build_authentication_request()['auth'] + api = auth.controllers.Auth() + context = {'REMOTE_USER': '%s@%s' % + (self.user['name'], self.domain['name'])} + auth_info = auth.controllers.AuthInfo(None, auth_data) + auth_context = {'extras': {}, 'method_names': []} + api.authenticate(context, auth_info, auth_context) + self.assertEqual(auth_context['user_id'], self.user['id']) + + class TestAuthJSON(test_v3.RestfulTestCase): content_type = 'json' @@ -1156,26 +1193,72 @@ class TestAuthJSON(test_v3.RestfulTestCase): password=uuid.uuid4().hex) self.post('/auth/tokens', body=auth_data, expected_status=401) - def test_remote_user(self): + def test_remote_user_no_realm(self): + CONF.auth.methods = 'external' + api = auth.controllers.Auth() + auth_data = self.build_authentication_request()['auth'] + context = {'REMOTE_USER': self.default_domain_user['name']} + auth_info = auth.controllers.AuthInfo(None, auth_data) + auth_context = {'extras': {}, 'method_names': []} + api.authenticate(context, auth_info, auth_context) + self.assertEqual(auth_context['user_id'], + self.default_domain_user['id']) + + def test_remote_user_no_domain(self): + auth_data = self.build_authentication_request()['auth'] + api = auth.controllers.Auth() + context = {'REMOTE_USER': self.user['name']} + auth_info = auth.controllers.AuthInfo(None, auth_data) + auth_context = {'extras': {}, 'method_names': []} + self.assertRaises(exception.Unauthorized, + api.authenticate, + context, + auth_info, + auth_context) + + def test_remote_user_and_password(self): + #both REMOTE_USER and password methods must pass. + #note that they do not have to match auth_data = self.build_authentication_request( - user_id=self.user['id'], + user_domain_id=self.domain['id'], + username=self.user['name'], password=self.user['password'])['auth'] api = auth.controllers.Auth() - context = {'REMOTE_USER': self.user['name']} + context = {'REMOTE_USER': self.default_domain_user['name']} auth_info = auth.controllers.AuthInfo(None, auth_data) auth_context = {'extras': {}, 'method_names': []} api.authenticate(context, auth_info, auth_context) - self.assertEqual(auth_context['user_id'], self.user['id']) - def test_remote_user_no_domain(self): + def test_remote_user_and_explicit_external(self): + #both REMOTE_USER and password methods must pass. + #note that they do not have to match auth_data = self.build_authentication_request( + user_domain_id=self.domain['id'], username=self.user['name'], password=self.user['password'])['auth'] + auth_data['identity']['methods'] = ["password", "external"] + auth_data['identity']['external'] = {} api = auth.controllers.Auth() - context = {'REMOTE_USER': self.user['name']} + context = {} auth_info = auth.controllers.AuthInfo(None, auth_data) auth_context = {'extras': {}, 'method_names': []} - self.assertRaises(exception.ValidationError, + self.assertRaises(exception.Unauthorized, + api.authenticate, + context, + auth_info, + auth_context) + + def test_remote_user_bad_password(self): + #both REMOTE_USER and password methods must pass. + auth_data = self.build_authentication_request( + user_domain_id=self.domain['id'], + username=self.user['name'], + password='badpassword')['auth'] + api = auth.controllers.Auth() + context = {'REMOTE_USER': self.default_domain_user['name']} + auth_info = auth.controllers.AuthInfo(None, auth_data) + auth_context = {'extras': {}, 'method_names': []} + self.assertRaises(exception.Unauthorized, api.authenticate, context, auth_info, |