summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-07-17 05:01:50 +0000
committerGerrit Code Review <review@openstack.org>2013-07-17 05:01:50 +0000
commitb5384074dcc6229c2c23bd5edd2981d71607fe3a (patch)
treefcc27c11caeb276365d5390808396793dc20f1a7
parent7f4891ddc3da7457df09c0cc8bbfe8a888063feb (diff)
parent88c319e6bce98082f9a90b8b27726793d5366326 (diff)
downloadkeystone-b5384074dcc6229c2c23bd5edd2981d71607fe3a.tar.gz
keystone-b5384074dcc6229c2c23bd5edd2981d71607fe3a.tar.xz
keystone-b5384074dcc6229c2c23bd5edd2981d71607fe3a.zip
Merge "Pluggable Remote User"
-rw-r--r--doc/source/configuration.rst11
-rw-r--r--etc/keystone.conf.sample3
-rw-r--r--keystone/auth/controllers.py22
-rw-r--r--keystone/auth/plugins/external.py80
-rw-r--r--keystone/common/config.py8
-rw-r--r--tests/auth_plugin_external_disabled.conf2
-rw-r--r--tests/auth_plugin_external_domain.conf3
-rw-r--r--tests/test_auth_plugin.conf2
-rw-r--r--tests/test_v3_auth.py97
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,