diff --git a/data/users/__init__.py b/data/users/__init__.py index fc504c530..210902115 100644 --- a/data/users/__init__.py +++ b/data/users/__init__.py @@ -9,7 +9,7 @@ from data import model from data.users.database import DatabaseUsers from data.users.externalldap import LDAPUsers from data.users.externaljwt import ExternalJWTAuthN -from data.users.keystone import KeystoneUsers +from data.users.keystone import get_keystone_users from util.security.aes import AESCipher logger = logging.getLogger(__name__) @@ -63,12 +63,14 @@ def get_users_handler(config, config_provider, override_config_dir): if authentication_type == 'Keystone': auth_url = config.get('KEYSTONE_AUTH_URL') + auth_version = int(config.get('KEYSTONE_AUTH_VERSION', 2)) timeout = config.get('KEYSTONE_AUTH_TIMEOUT') keystone_admin_username = config.get('KEYSTONE_ADMIN_USERNAME') keystone_admin_password = config.get('KEYSTONE_ADMIN_PASSWORD') keystone_admin_tenant = config.get('KEYSTONE_ADMIN_TENANT') - return KeystoneUsers(auth_url, keystone_admin_username, keystone_admin_password, - keystone_admin_tenant, timeout) + + return get_keystone_users(auth_version, auth_url, keystone_admin_username, + keystone_admin_password, keystone_admin_tenant, timeout) raise RuntimeError('Unknown authentication type: %s' % authentication_type) diff --git a/data/users/keystone.py b/data/users/keystone.py index 4c9a191f7..ecff5b701 100644 --- a/data/users/keystone.py +++ b/data/users/keystone.py @@ -1,19 +1,34 @@ import logging import os +import itertools from keystoneclient.v2_0 import client as kclient +from keystoneclient.v3 import client as kv3client from keystoneclient.exceptions import AuthorizationFailure as KeystoneAuthorizationFailure from keystoneclient.exceptions import Unauthorized as KeystoneUnauthorized -from data.users.federated import FederatedUsers, VerifiedCredentials +from data.users.federated import FederatedUsers, UserInformation logger = logging.getLogger(__name__) DEFAULT_TIMEOUT = 10 # seconds -class KeystoneUsers(FederatedUsers): - """ Delegates authentication to OpenStack Keystone. """ +def _take(n, iterable): + "Return first n items of the iterable as a list" + return list(itertools.islice(iterable, n)) + + +def get_keystone_users(auth_version, auth_url, admin_username, admin_password, admin_tenant, + timeout=None): + if auth_version == 3: + return KeystoneV3Users(auth_url, admin_username, admin_password, admin_tenant, timeout) + else: + return KeystoneV2Users(auth_url, admin_username, admin_password, admin_tenant, timeout) + + +class KeystoneV2Users(FederatedUsers): + """ Delegates authentication to OpenStack Keystone V2. """ def __init__(self, auth_url, admin_username, admin_password, admin_tenant, timeout=None): - super(KeystoneUsers, self).__init__('keystone') + super(KeystoneV2Users, self).__init__('keystone') self.auth_url = auth_url self.admin_username = admin_username self.admin_password = admin_password @@ -43,4 +58,75 @@ class KeystoneUsers(FederatedUsers): logger.exception('Keystone unauthorized admin') return (None, 'Keystone admin credentials are invalid: %s' % kut.message) - return (VerifiedCredentials(username=username_or_email, email=user.email), None) + return (UserInformation(username=username_or_email, email=user.email, id=user_id), None) + + def query_users(self, query, limit=20): + return (None, 'Unsupported in Keystone V2') + + def get_user(self, username_or_email): + return (None, 'Unsupported in Keystone V2') + + +class KeystoneV3Users(FederatedUsers): + """ Delegates authentication to OpenStack Keystone V3. """ + def __init__(self, auth_url, admin_username, admin_password, admin_tenant, timeout=None): + super(KeystoneV3Users, self).__init__('keystone') + self.auth_url = auth_url + self.admin_username = admin_username + self.admin_password = admin_password + self.admin_tenant = admin_tenant + self.timeout = timeout or DEFAULT_TIMEOUT + self.debug = os.environ.get('USERS_DEBUG') == '1' + + def verify_credentials(self, username_or_email, password): + try: + keystone_client = kv3client.Client(username=username_or_email, password=password, + auth_url=self.auth_url, timeout=self.timeout, + debug=self.debug) + user_id = keystone_client.user_id + user = keystone_client.users.get(user_id) + return (self._user_info(user), None) + except KeystoneAuthorizationFailure as kaf: + logger.exception('Keystone auth failure for user: %s', username_or_email) + return (None, kaf.message or 'Invalid username or password') + except KeystoneUnauthorized as kut: + logger.exception('Keystone unauthorized for user: %s', username_or_email) + return (None, kut.message or 'Invalid username or password') + + def get_user(self, username_or_email): + users_found, err_msg = self.query_users(username_or_email) + if err_msg is not None: + return (None, err_msg) + + if len(users_found) != 1: + return (None, 'Single user not found') + + return (users_found[0], None) + + @staticmethod + def _user_info(user): + # Because Keystone uses defined attributes... + email = user.email if hasattr(user, 'email') else '' + return UserInformation(user.name, email, user.id) + + def query_users(self, query, limit=20): + if len(query) < 3: + return ([], None) + + try: + keystone_client = kv3client.Client(username=self.admin_username, password=self.admin_password, + tenant_name=self.admin_tenant, auth_url=self.auth_url, + timeout=self.timeout, debug=self.debug) + found_users = list(_take(limit, keystone_client.users.list(name=query))) + logger.debug('For Keystone query %s found users: %s', query, found_users) + if not found_users: + return ([], None) + + return ([self._user_info(user) for user in found_users], None) + except KeystoneAuthorizationFailure as kaf: + logger.exception('Keystone auth failure for admin user for query %s', query) + return (None, kaf.message or 'Invalid admin username or password') + except KeystoneUnauthorized as kut: + logger.exception('Keystone unauthorized for admin user for query %s', query) + return (None, kut.message or 'Invalid admin username or password') + diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 96a1c0758..daf7cf2a2 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -523,6 +523,15 @@
Keystone API Version: | ++ + | +
Keystone Authentication URL: |
diff --git a/test/test_keystone_auth.py b/test/test_keystone_auth.py
index 0603c7f25..990264e74 100644
--- a/test/test_keystone_auth.py
+++ b/test/test_keystone_auth.py
@@ -4,16 +4,15 @@ import unittest
import requests
-from flask import Flask, request, abort
+from flask import Flask, request, abort, make_response
from flask_testing import LiveServerTestCase
-from data.users.keystone import KeystoneUsers
-
+from data.users.keystone import get_keystone_users
+from initdb import setup_database_for_testing, finished_database_for_testing
_PORT_NUMBER = 5001
-
-class KeystoneAuthTests(LiveServerTestCase):
+class KeystoneAuthTestsMixin():
maxDiff = None
def create_app(self):
@@ -44,6 +43,106 @@ class KeystoneAuthTests(LiveServerTestCase):
abort(404)
+ @ks_app.route('/v3/identity/users/ |