Add support to Keystone Auth for external user linking

Also adds Keystone V3 support
This commit is contained in:
Joseph Schorr 2016-10-27 15:35:52 -04:00
parent fbb524e34e
commit b3d1d7227c
5 changed files with 262 additions and 17 deletions

View file

@ -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)

View file

@ -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')

View file

@ -523,6 +523,15 @@
<!-- Keystone Authentication -->
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'Keystone'">
<tr>
<td>Keystone API Version:</td>
<td>
<select ng-model="config.KEYSTONE_AUTH_VERSION">
<option value="2">2.0</option>
<option value="3">V3</option>
</select>
</td>
</tr>
<tr>
<td>Keystone Authentication URL:</td>
<td>

View file

@ -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/<userid>', methods=['GET'])
def getv3user(userid):
for user in users:
if user['username'] == userid:
return json.dumps({
'user': {
"domain_id": "default",
"enabled": True,
"id": user['username'],
"links": {},
"name": user['username'],
"email": user['username'] + '@example.com',
}
})
abort(404)
@ks_app.route('/v3/identity/users', methods=['GET'])
def v3identity():
returned = []
for user in users:
if not request.args.get('name') or user['username'].startswith(request.args.get('name')):
returned.append({
"domain_id": "default",
"enabled": True,
"id": user['username'],
"links": {},
"name": user['username'],
"email": user['username'] + '@example.com',
})
return json.dumps({"users": returned})
@ks_app.route('/v3/auth/tokens', methods=['POST'])
def v3tokens():
creds = request.json['auth']['identity']['password']['user']
for user in users:
if creds['name'] == user['username'] and creds['password'] == user['password']:
data = json.dumps({
"token": {
"methods": [
"password"
],
"roles": [
{
"id": "9fe2ff9ee4384b1894a90878d3e92bab",
"name": "_member_"
},
{
"id": "c703057be878458588961ce9a0ce686b",
"name": "admin"
}
],
"project": {
"domain": {
"id": "default",
"name": "Default"
},
"id": "8538a3f13f9541b28c2620eb19065e45",
"name": "admin"
},
"catalog": [
{
"endpoints": [
{
"url": self.get_server_url() + '/v3/identity',
"region": "RegionOne",
"interface": "admin",
"id": "29beb2f1567642eb810b042b6719ea88"
},
],
"type": "identity",
"id": "bd73972c0e14fb69bae8ff76e112a90",
"name": "keystone"
}
],
"extras": {
},
"user": {
"domain": {
"id": "default",
"name": "Default"
},
"id": user['username'],
"name": "admin"
},
"audit_ids": [
"yRt0UrxJSs6-WYJgwEMMmg"
],
"issued_at": "2014-06-16T22:24:26.089380",
"expires_at": "2020-06-16T23:24:26Z",
}
})
response = make_response(data, 200)
response.headers['X-Subject-Token'] = 'sometoken'
return response
abort(403)
@ks_app.route('/v2.0/auth/tokens', methods=['POST'])
def tokens():
@ -89,9 +188,15 @@ class KeystoneAuthTests(LiveServerTestCase):
return ks_app
def setUp(self):
setup_database_for_testing(self)
self.session = requests.Session()
self.keystone = KeystoneUsers(self.get_server_url() + '/v2.0/auth', 'adminuser', 'adminpass',
'admintenant')
def tearDown(self):
finished_database_for_testing(self)
@property
def keystone(self):
raise NotImplementedError
def test_invalid_user(self):
(user, _) = self.keystone.verify_credentials('unknownuser', 'password')
@ -111,6 +216,48 @@ class KeystoneAuthTests(LiveServerTestCase):
self.assertEquals(user.username, 'some.neat.user')
self.assertEquals(user.email, 'some.neat.user@example.com')
class KeystoneV2AuthTests(KeystoneAuthTestsMixin, LiveServerTestCase):
@property
def keystone(self):
return get_keystone_users(2, self.get_server_url() + '/v2.0/auth',
'adminuser', 'adminpass', 'admintenant')
class KeystoneV3AuthTests(KeystoneAuthTestsMixin, LiveServerTestCase):
@property
def keystone(self):
return get_keystone_users(3, self.get_server_url() + '/v3',
'adminuser', 'adminpass', 'admintenant')
def test_query(self):
# Lookup cool.
(response, error_message) = self.keystone.query_users('cool')
self.assertIsNone(error_message)
self.assertEquals(1, len(response))
user_info = response[0]
self.assertEquals("cooluser", user_info.username)
# Lookup unknown.
(response, error_message) = self.keystone.query_users('unknown')
self.assertIsNone(error_message)
self.assertEquals(0, len(response))
def test_link_user(self):
# Link someuser.
user, error_message = self.keystone.link_user('cooluser')
self.assertIsNone(error_message)
self.assertIsNotNone(user)
self.assertEquals('cooluser', user.username)
self.assertEquals('cooluser@example.com', user.email)
# Link again. Should return the same user record.
user_again, _ = self.keystone.link_user('cooluser')
self.assertEquals(user_again.id, user.id)
# Confirm someuser.
result, _ = self.keystone.confirm_existing_user('cooluser', 'password')
self.assertIsNotNone(result)
self.assertEquals('cooluser', result.username)
if __name__ == '__main__':
unittest.main()

View file

@ -21,7 +21,7 @@ from data.database import validate_database_url
from data.users import LDAP_CERT_FILENAME
from data.users.externaljwt import ExternalJWTAuthN
from data.users.externalldap import LDAPConnection, LDAPUsers
from data.users.keystone import KeystoneUsers
from data.users.keystone import get_keystone_users
from storage import get_storage_driver
from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig
from util.secscan.api import SecurityScannerAPI
@ -422,6 +422,7 @@ def _validate_keystone(config, password):
return
auth_url = config.get('KEYSTONE_AUTH_URL')
auth_version = int(config.get('KEYSTONE_AUTH_VERSION', 2))
admin_username = config.get('KEYSTONE_ADMIN_USERNAME')
admin_password = config.get('KEYSTONE_ADMIN_PASSWORD')
admin_tenant = config.get('KEYSTONE_ADMIN_TENANT')
@ -438,7 +439,7 @@ def _validate_keystone(config, password):
if not admin_tenant:
raise Exception('Missing admin tenant')
users = KeystoneUsers(auth_url, admin_username, admin_password, admin_tenant)
users = get_keystone_users(auth_version, auth_url, admin_username, admin_password, admin_tenant)
# Verify that the superuser exists. If not, raise an exception.
username = get_authenticated_user().username