Fix external auth returns for query_user calls
Adds the missing field on the query_user calls, updates the external auth tests to ensure it is returned properly, and adds new end-to-end tests which call the external auth engines via the *API*, to ensure this doesn't break again
This commit is contained in:
parent
f0b19b26c9
commit
3203fd6de1
8 changed files with 834 additions and 651 deletions
|
@ -187,12 +187,12 @@ class LDAPUsers(FederatedUsers):
|
||||||
def query_users(self, query, limit=20):
|
def query_users(self, query, limit=20):
|
||||||
""" Queries LDAP for matching users. """
|
""" Queries LDAP for matching users. """
|
||||||
if not query:
|
if not query:
|
||||||
return (None, 'Empty query')
|
return (None, self.federated_service, 'Empty query')
|
||||||
|
|
||||||
logger.debug('Got query %s with limit %s', query, limit)
|
logger.debug('Got query %s with limit %s', query, limit)
|
||||||
(results, err_msg) = self._ldap_user_search(query + '*', limit=limit)
|
(results, err_msg) = self._ldap_user_search(query + '*', limit=limit)
|
||||||
if err_msg is not None:
|
if err_msg is not None:
|
||||||
return (None, err_msg)
|
return (None, self.federated_service, err_msg)
|
||||||
|
|
||||||
final_results = []
|
final_results = []
|
||||||
for result in results[0:limit]:
|
for result in results[0:limit]:
|
||||||
|
@ -203,7 +203,7 @@ class LDAPUsers(FederatedUsers):
|
||||||
final_results.append(credentials)
|
final_results.append(credentials)
|
||||||
|
|
||||||
logger.debug('For query %s found results %s', query, final_results)
|
logger.debug('For query %s found results %s', query, final_results)
|
||||||
return (final_results, None)
|
return (final_results, self.federated_service, None)
|
||||||
|
|
||||||
def verify_credentials(self, username_or_email, password):
|
def verify_credentials(self, username_or_email, password):
|
||||||
""" Verify the credentials with LDAP. """
|
""" Verify the credentials with LDAP. """
|
||||||
|
|
|
@ -69,7 +69,7 @@ class KeystoneV2Users(FederatedUsers):
|
||||||
return (UserInformation(username=username_or_email, email=email, id=user_id), None)
|
return (UserInformation(username=username_or_email, email=email, id=user_id), None)
|
||||||
|
|
||||||
def query_users(self, query, limit=20):
|
def query_users(self, query, limit=20):
|
||||||
return (None, 'Unsupported in Keystone V2')
|
return (None, self.federated_service, 'Unsupported in Keystone V2')
|
||||||
|
|
||||||
def get_user(self, username_or_email):
|
def get_user(self, username_or_email):
|
||||||
return (None, 'Unsupported in Keystone V2')
|
return (None, 'Unsupported in Keystone V2')
|
||||||
|
@ -108,7 +108,7 @@ class KeystoneV3Users(FederatedUsers):
|
||||||
return (None, kut.message or 'Invalid username or password')
|
return (None, kut.message or 'Invalid username or password')
|
||||||
|
|
||||||
def get_user(self, username_or_email):
|
def get_user(self, username_or_email):
|
||||||
users_found, err_msg = self.query_users(username_or_email)
|
users_found, _, err_msg = self.query_users(username_or_email)
|
||||||
if err_msg is not None:
|
if err_msg is not None:
|
||||||
return (None, err_msg)
|
return (None, err_msg)
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@ class KeystoneV3Users(FederatedUsers):
|
||||||
|
|
||||||
def query_users(self, query, limit=20):
|
def query_users(self, query, limit=20):
|
||||||
if len(query) < 3:
|
if len(query) < 3:
|
||||||
return ([], None)
|
return ([], self.federated_service, None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
keystone_client = kv3client.Client(username=self.admin_username, password=self.admin_password,
|
keystone_client = kv3client.Client(username=self.admin_username, password=self.admin_password,
|
||||||
|
@ -137,13 +137,13 @@ class KeystoneV3Users(FederatedUsers):
|
||||||
found_users = list(_take(limit, keystone_client.users.list(name=query)))
|
found_users = list(_take(limit, keystone_client.users.list(name=query)))
|
||||||
logger.debug('For Keystone query %s found users: %s', query, found_users)
|
logger.debug('For Keystone query %s found users: %s', query, found_users)
|
||||||
if not found_users:
|
if not found_users:
|
||||||
return ([], None)
|
return ([], self.federated_service, None)
|
||||||
|
|
||||||
return ([self._user_info(user) for user in found_users], None)
|
return ([self._user_info(user) for user in found_users], self.federated_service, None)
|
||||||
except KeystoneAuthorizationFailure as kaf:
|
except KeystoneAuthorizationFailure as kaf:
|
||||||
logger.exception('Keystone auth failure for admin user for query %s', query)
|
logger.exception('Keystone auth failure for admin user for query %s', query)
|
||||||
return (None, kaf.message or 'Invalid admin username or password')
|
return (None, self.federated_service, kaf.message or 'Invalid admin username or password')
|
||||||
except KeystoneUnauthorized as kut:
|
except KeystoneUnauthorized as kut:
|
||||||
logger.exception('Keystone unauthorized for admin user for query %s', query)
|
logger.exception('Keystone unauthorized for admin user for query %s', query)
|
||||||
return (None, kut.message or 'Invalid admin username or password')
|
return (None, self.federated_service, kut.message or 'Invalid admin username or password')
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ from auth.permissions import (OrganizationMemberPermission, ReadRepositoryPermis
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from app import avatar, authentication
|
from app import avatar, authentication
|
||||||
|
from flask import abort
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from stringscore import liquidmetal
|
from stringscore import liquidmetal
|
||||||
from util.names import parse_robot_username
|
from util.names import parse_robot_username
|
||||||
|
@ -17,13 +18,15 @@ from util.names import parse_robot_username
|
||||||
import anunidecode # Don't listen to pylint's lies. This import is required.
|
import anunidecode # Don't listen to pylint's lies. This import is required.
|
||||||
import math
|
import math
|
||||||
|
|
||||||
@show_if(authentication.federated_service) # Only enabled for non-DB auth.
|
|
||||||
@resource('/v1/entities/link/<username>')
|
@resource('/v1/entities/link/<username>')
|
||||||
@internal_only
|
@internal_only
|
||||||
class LinkExternalEntity(ApiResource):
|
class LinkExternalEntity(ApiResource):
|
||||||
""" Resource for linking external entities to internal users. """
|
""" Resource for linking external entities to internal users. """
|
||||||
@nickname('linkExternalUser')
|
@nickname('linkExternalUser')
|
||||||
def post(self, username):
|
def post(self, username):
|
||||||
|
if not authentication.federated_service:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
# Only allowed if there is a logged in user.
|
# Only allowed if there is a logged in user.
|
||||||
if not get_authenticated_user():
|
if not get_authenticated_user():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
|
import multiprocessing
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
|
||||||
from data.database import LogEntryKind, LogEntry
|
from data.database import LogEntryKind, LogEntry
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
class assert_action_logged(object):
|
class assert_action_logged(object):
|
||||||
""" Specialized assertion for ensuring that a log entry of a particular kind was added under the
|
""" Specialized assertion for ensuring that a log entry of a particular kind was added under the
|
||||||
|
@ -20,3 +25,59 @@ class assert_action_logged(object):
|
||||||
updated_count = self._get_log_count()
|
updated_count = self._get_log_count()
|
||||||
error_msg = 'Missing new log entry of kind %s' % self.log_kind
|
error_msg = 'Missing new log entry of kind %s' % self.log_kind
|
||||||
assert self.existing_count == (updated_count - 1), error_msg
|
assert self.existing_count == (updated_count - 1), error_msg
|
||||||
|
|
||||||
|
_LIVESERVER_TIMEOUT = 5
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def liveserver_app(flask_app, port):
|
||||||
|
"""
|
||||||
|
Based on https://github.com/jarus/flask-testing/blob/master/flask_testing/utils.py
|
||||||
|
|
||||||
|
Runs the given Flask app as a live web server locally, on the given port, starting it
|
||||||
|
when called and terminating after the yield.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
with liveserver_app(flask_app, port):
|
||||||
|
# Code that makes use of the app.
|
||||||
|
"""
|
||||||
|
shared = {}
|
||||||
|
|
||||||
|
def _can_ping_server():
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock.connect(('localhost', port))
|
||||||
|
except socket.error:
|
||||||
|
success = False
|
||||||
|
else:
|
||||||
|
success = True
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
def _spawn_live_server():
|
||||||
|
worker = lambda app, port: app.run(port=port, use_reloader=False)
|
||||||
|
shared['process'] = multiprocessing.Process(target=worker, args=(flask_app, port))
|
||||||
|
shared['process'].start()
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
while True:
|
||||||
|
elapsed_time = (time.time() - start_time)
|
||||||
|
if elapsed_time > _LIVESERVER_TIMEOUT:
|
||||||
|
_terminate_live_server()
|
||||||
|
raise RuntimeError("Failed to start the server after %d seconds. " % _LIVESERVER_TIMEOUT)
|
||||||
|
|
||||||
|
if _can_ping_server():
|
||||||
|
break
|
||||||
|
|
||||||
|
def _terminate_live_server():
|
||||||
|
if shared.get('process'):
|
||||||
|
shared.get('process').terminate()
|
||||||
|
shared.pop('process')
|
||||||
|
|
||||||
|
try:
|
||||||
|
_spawn_live_server()
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
_terminate_live_server()
|
||||||
|
|
57
test/test_endtoend_auth.py
Normal file
57
test/test_endtoend_auth.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import unittest
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from endpoints.api.search import EntitySearch, LinkExternalEntity
|
||||||
|
from test.test_api_usage import ApiTestCase, ADMIN_ACCESS_USER
|
||||||
|
|
||||||
|
from test.test_ldap import mock_ldap
|
||||||
|
from test.test_external_jwt_authn import fake_jwt
|
||||||
|
from test.test_keystone_auth import fake_keystone
|
||||||
|
|
||||||
|
class EndToEndAuthMixin:
|
||||||
|
def test_entity_search(self):
|
||||||
|
with self.get_authentication() as auth:
|
||||||
|
with patch('endpoints.api.search.authentication', auth):
|
||||||
|
# Try an unknown prefix.
|
||||||
|
json_data = self.getJsonResponse(EntitySearch, params=dict(prefix='unknown'))
|
||||||
|
results = json_data['results']
|
||||||
|
self.assertEquals(0, len(results))
|
||||||
|
|
||||||
|
# Try a known prefix.
|
||||||
|
json_data = self.getJsonResponse(EntitySearch, params=dict(prefix='cool'))
|
||||||
|
results = json_data['results']
|
||||||
|
self.assertEquals(1, len(results))
|
||||||
|
self.assertEquals('external', results[0]['kind'])
|
||||||
|
self.assertEquals('cool.user', results[0]['name'])
|
||||||
|
|
||||||
|
def test_link_external_entity(self):
|
||||||
|
with self.get_authentication() as auth:
|
||||||
|
with patch('endpoints.api.search.authentication', auth):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# Try an unknown user.
|
||||||
|
self.postResponse(LinkExternalEntity, params=dict(username='unknownuser'),
|
||||||
|
expected_code=400)
|
||||||
|
|
||||||
|
# Try a known user.
|
||||||
|
json_data = self.postJsonResponse(LinkExternalEntity, params=dict(username='cool.user'))
|
||||||
|
entity = json_data['entity']
|
||||||
|
self.assertEquals('cool_user', entity['name'])
|
||||||
|
self.assertEquals('user', entity['kind'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestLDAPEndToEnd(ApiTestCase, EndToEndAuthMixin):
|
||||||
|
def get_authentication(self):
|
||||||
|
return mock_ldap()
|
||||||
|
|
||||||
|
class TestJWTEndToEnd(ApiTestCase, EndToEndAuthMixin):
|
||||||
|
def get_authentication(self):
|
||||||
|
return fake_jwt()
|
||||||
|
|
||||||
|
class TestKeystoneEndToEnd(ApiTestCase, EndToEndAuthMixin):
|
||||||
|
def get_authentication(self):
|
||||||
|
return fake_keystone(3)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
|
@ -3,30 +3,46 @@ import unittest
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from flask import Flask, jsonify, request, make_response
|
from flask import Flask, jsonify, request, make_response
|
||||||
from flask_testing import LiveServerTestCase
|
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from data.users import ExternalJWTAuthN
|
from data.users import ExternalJWTAuthN
|
||||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||||
|
from test.helpers import liveserver_app
|
||||||
|
|
||||||
|
|
||||||
_PORT_NUMBER = 5001
|
_PORT_NUMBER = 5001
|
||||||
|
|
||||||
class JWTAuthTestMixin(object):
|
@contextmanager
|
||||||
maxDiff = None
|
def fake_jwt(requires_email=True):
|
||||||
|
""" Context manager which instantiates and runs a webserver with a fake JWT implementation,
|
||||||
|
until the result is yielded.
|
||||||
|
|
||||||
@property
|
Usage:
|
||||||
def emails(self):
|
with fake_jwt() as jwt_auth:
|
||||||
raise NotImplementedError
|
# Make jwt_auth requests.
|
||||||
|
"""
|
||||||
|
jwt_app, port, public_key = _create_app(requires_email)
|
||||||
|
server_url = 'http://' + jwt_app.config['SERVER_HOSTNAME']
|
||||||
|
|
||||||
@classmethod
|
verify_url = server_url + '/user/verify'
|
||||||
def setUpClass(cls):
|
query_url = server_url + '/user/query'
|
||||||
|
getuser_url = server_url + '/user/get'
|
||||||
|
|
||||||
|
jwt_auth = ExternalJWTAuthN(verify_url, query_url, getuser_url, 'authy', '',
|
||||||
|
app.config['HTTPCLIENT'], 300, public_key.name,
|
||||||
|
requires_email=requires_email)
|
||||||
|
|
||||||
|
with liveserver_app(jwt_app, port):
|
||||||
|
yield jwt_auth
|
||||||
|
|
||||||
|
def _generate_certs():
|
||||||
public_key = NamedTemporaryFile(delete=True)
|
public_key = NamedTemporaryFile(delete=True)
|
||||||
|
|
||||||
key = RSA.generate(1024)
|
key = RSA.generate(1024)
|
||||||
|
@ -36,21 +52,21 @@ class JWTAuthTestMixin(object):
|
||||||
public_key.write(pubkey.exportKey('OpenSSH'))
|
public_key.write(pubkey.exportKey('OpenSSH'))
|
||||||
public_key.seek(0)
|
public_key.seek(0)
|
||||||
|
|
||||||
JWTAuthTestCase.public_key = public_key
|
return (public_key, private_key_data)
|
||||||
JWTAuthTestCase.private_key_data = private_key_data
|
|
||||||
|
|
||||||
def create_app(self):
|
def _create_app(emails=True):
|
||||||
global _PORT_NUMBER
|
global _PORT_NUMBER
|
||||||
_PORT_NUMBER = _PORT_NUMBER + 1
|
_PORT_NUMBER = _PORT_NUMBER + 1
|
||||||
|
|
||||||
|
public_key, private_key_data = _generate_certs()
|
||||||
|
|
||||||
users = [
|
users = [
|
||||||
{'name': 'cooluser', 'email': 'user@domain.com', 'password': 'password'},
|
{'name': 'cool.user', 'email': 'user@domain.com', 'password': 'password'},
|
||||||
{'name': 'some.neat.user', 'email': 'neat@domain.com', 'password': 'foobar'}
|
{'name': 'some.neat.user', 'email': 'neat@domain.com', 'password': 'foobar'}
|
||||||
]
|
]
|
||||||
|
|
||||||
jwt_app = Flask('testjwt')
|
jwt_app = Flask('testjwt')
|
||||||
private_key = JWTAuthTestCase.private_key_data
|
jwt_app.config['SERVER_HOSTNAME'] = 'localhost:%s' % _PORT_NUMBER
|
||||||
jwt_app.config['LIVESERVER_PORT'] = _PORT_NUMBER
|
|
||||||
|
|
||||||
def _get_basic_auth():
|
def _get_basic_auth():
|
||||||
data = base64.b64decode(request.headers['Authorization'][len('Basic '):])
|
data = base64.b64decode(request.headers['Authorization'][len('Basic '):])
|
||||||
|
@ -67,7 +83,7 @@ class JWTAuthTestMixin(object):
|
||||||
'username': user['name'],
|
'username': user['name'],
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.emails:
|
if emails:
|
||||||
result['email'] = user['email']
|
result['email'] = user['email']
|
||||||
|
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
@ -81,7 +97,7 @@ class JWTAuthTestMixin(object):
|
||||||
'results': results,
|
'results': results,
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded = jwt.encode(token_data, private_key, 'RS256')
|
encoded = jwt.encode(token_data, private_key_data, 'RS256')
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'token': encoded
|
'token': encoded
|
||||||
})
|
})
|
||||||
|
@ -105,7 +121,7 @@ class JWTAuthTestMixin(object):
|
||||||
'email': user['email'],
|
'email': user['email'],
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded = jwt.encode(token_data, private_key, 'RS256')
|
encoded = jwt.encode(token_data, private_key_data, 'RS256')
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'token': encoded
|
'token': encoded
|
||||||
})
|
})
|
||||||
|
@ -134,7 +150,7 @@ class JWTAuthTestMixin(object):
|
||||||
'email': user['email'],
|
'email': user['email'],
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded = jwt.encode(token_data, private_key, 'RS256')
|
encoded = jwt.encode(token_data, private_key_data, 'RS256')
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'token': encoded
|
'token': encoded
|
||||||
})
|
})
|
||||||
|
@ -142,9 +158,17 @@ class JWTAuthTestMixin(object):
|
||||||
return make_response('Invalid username or password', 404)
|
return make_response('Invalid username or password', 404)
|
||||||
|
|
||||||
jwt_app.config['TESTING'] = True
|
jwt_app.config['TESTING'] = True
|
||||||
return jwt_app
|
return jwt_app, _PORT_NUMBER, public_key
|
||||||
|
|
||||||
|
|
||||||
|
class JWTAuthTestMixin:
|
||||||
|
""" Mixin defining all the JWT auth tests. """
|
||||||
|
maxDiff = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def emails(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
setup_database_for_testing(self)
|
setup_database_for_testing(self)
|
||||||
self.app = app.test_client()
|
self.app = app.test_client()
|
||||||
|
@ -153,76 +177,72 @@ class JWTAuthTestMixin(object):
|
||||||
|
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
|
|
||||||
verify_url = self.get_server_url() + '/user/verify'
|
|
||||||
query_url = self.get_server_url() + '/user/query'
|
|
||||||
getuser_url = self.get_server_url() + '/user/get'
|
|
||||||
|
|
||||||
self.jwt_auth = ExternalJWTAuthN(verify_url, query_url, getuser_url, 'authy', '',
|
|
||||||
app.config['HTTPCLIENT'], 300, JWTAuthTestCase.public_key.name,
|
|
||||||
requires_email=self.emails)
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
finished_database_for_testing(self)
|
finished_database_for_testing(self)
|
||||||
self.ctx.__exit__(True, None, None)
|
self.ctx.__exit__(True, None, None)
|
||||||
|
|
||||||
def test_verify_and_link_user(self):
|
def test_verify_and_link_user(self):
|
||||||
result, error_message = self.jwt_auth.verify_and_link_user('invaliduser', 'foobar')
|
with fake_jwt(self.emails) as jwt_auth:
|
||||||
|
result, error_message = jwt_auth.verify_and_link_user('invaliduser', 'foobar')
|
||||||
self.assertEquals('Invalid username or password', error_message)
|
self.assertEquals('Invalid username or password', error_message)
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
|
|
||||||
result, _ = self.jwt_auth.verify_and_link_user('cooluser', 'invalidpassword')
|
result, _ = jwt_auth.verify_and_link_user('cool.user', 'invalidpassword')
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
|
|
||||||
result, _ = self.jwt_auth.verify_and_link_user('cooluser', 'password')
|
result, _ = jwt_auth.verify_and_link_user('cool.user', 'password')
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
self.assertEquals('cooluser', result.username)
|
self.assertEquals('cool_user', result.username)
|
||||||
|
|
||||||
result, _ = self.jwt_auth.verify_and_link_user('some.neat.user', 'foobar')
|
result, _ = jwt_auth.verify_and_link_user('some.neat.user', 'foobar')
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
self.assertEquals('some_neat_user', result.username)
|
self.assertEquals('some_neat_user', result.username)
|
||||||
|
|
||||||
def test_confirm_existing_user(self):
|
def test_confirm_existing_user(self):
|
||||||
|
with fake_jwt(self.emails) as jwt_auth:
|
||||||
# Create the users in the DB.
|
# Create the users in the DB.
|
||||||
result, _ = self.jwt_auth.verify_and_link_user('cooluser', 'password')
|
result, _ = jwt_auth.verify_and_link_user('cool.user', 'password')
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
|
|
||||||
result, _ = self.jwt_auth.verify_and_link_user('some.neat.user', 'foobar')
|
result, _ = jwt_auth.verify_and_link_user('some.neat.user', 'foobar')
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
|
|
||||||
# Confirm a user with the same internal and external username.
|
# Confirm a user with the same internal and external username.
|
||||||
result, _ = self.jwt_auth.confirm_existing_user('cooluser', 'invalidpassword')
|
result, _ = jwt_auth.confirm_existing_user('cool_user', 'invalidpassword')
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
|
|
||||||
result, _ = self.jwt_auth.confirm_existing_user('cooluser', 'password')
|
result, _ = jwt_auth.confirm_existing_user('cool_user', 'password')
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
self.assertEquals('cooluser', result.username)
|
self.assertEquals('cool_user', result.username)
|
||||||
|
|
||||||
# Fail to confirm the *external* username, which should return nothing.
|
# Fail to confirm the *external* username, which should return nothing.
|
||||||
result, _ = self.jwt_auth.confirm_existing_user('some.neat.user', 'password')
|
result, _ = jwt_auth.confirm_existing_user('some.neat.user', 'password')
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
|
|
||||||
# Now confirm the internal username.
|
# Now confirm the internal username.
|
||||||
result, _ = self.jwt_auth.confirm_existing_user('some_neat_user', 'foobar')
|
result, _ = jwt_auth.confirm_existing_user('some_neat_user', 'foobar')
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
self.assertEquals('some_neat_user', result.username)
|
self.assertEquals('some_neat_user', result.username)
|
||||||
|
|
||||||
def test_disabled_user_custom_error(self):
|
def test_disabled_user_custom_error(self):
|
||||||
result, error_message = self.jwt_auth.verify_and_link_user('disabled', 'password')
|
with fake_jwt(self.emails) as jwt_auth:
|
||||||
|
result, error_message = jwt_auth.verify_and_link_user('disabled', 'password')
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
self.assertEquals('User is currently disabled', error_message)
|
self.assertEquals('User is currently disabled', error_message)
|
||||||
|
|
||||||
def test_query(self):
|
def test_query(self):
|
||||||
|
with fake_jwt(self.emails) as jwt_auth:
|
||||||
# Lookup `cool`.
|
# Lookup `cool`.
|
||||||
results, identifier, error_message = self.jwt_auth.query_users('cool')
|
results, identifier, error_message = jwt_auth.query_users('cool')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertEquals('jwtauthn', identifier)
|
self.assertEquals('jwtauthn', identifier)
|
||||||
self.assertEquals(1, len(results))
|
self.assertEquals(1, len(results))
|
||||||
|
|
||||||
self.assertEquals('cooluser', results[0].username)
|
self.assertEquals('cool.user', results[0].username)
|
||||||
self.assertEquals('user@domain.com' if self.emails else None, results[0].email)
|
self.assertEquals('user@domain.com' if self.emails else None, results[0].email)
|
||||||
|
|
||||||
# Lookup `some`.
|
# Lookup `some`.
|
||||||
results, identifier, error_message = self.jwt_auth.query_users('some')
|
results, identifier, error_message = jwt_auth.query_users('some')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertEquals('jwtauthn', identifier)
|
self.assertEquals('jwtauthn', identifier)
|
||||||
self.assertEquals(1, len(results))
|
self.assertEquals(1, len(results))
|
||||||
|
@ -231,22 +251,23 @@ class JWTAuthTestMixin(object):
|
||||||
self.assertEquals('neat@domain.com' if self.emails else None, results[0].email)
|
self.assertEquals('neat@domain.com' if self.emails else None, results[0].email)
|
||||||
|
|
||||||
# Lookup `unknown`.
|
# Lookup `unknown`.
|
||||||
results, identifier, error_message = self.jwt_auth.query_users('unknown')
|
results, identifier, error_message = jwt_auth.query_users('unknown')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertEquals('jwtauthn', identifier)
|
self.assertEquals('jwtauthn', identifier)
|
||||||
self.assertEquals(0, len(results))
|
self.assertEquals(0, len(results))
|
||||||
|
|
||||||
def test_get_user(self):
|
def test_get_user(self):
|
||||||
# Lookup cooluser.
|
with fake_jwt(self.emails) as jwt_auth:
|
||||||
result, error_message = self.jwt_auth.get_user('cooluser')
|
# Lookup cool.user.
|
||||||
|
result, error_message = jwt_auth.get_user('cool.user')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
|
|
||||||
self.assertEquals('cooluser', result.username)
|
self.assertEquals('cool.user', result.username)
|
||||||
self.assertEquals('user@domain.com', result.email)
|
self.assertEquals('user@domain.com', result.email)
|
||||||
|
|
||||||
# Lookup some.neat.user.
|
# Lookup some.neat.user.
|
||||||
result, error_message = self.jwt_auth.get_user('some.neat.user')
|
result, error_message = jwt_auth.get_user('some.neat.user')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
|
|
||||||
|
@ -254,38 +275,42 @@ class JWTAuthTestMixin(object):
|
||||||
self.assertEquals('neat@domain.com', result.email)
|
self.assertEquals('neat@domain.com', result.email)
|
||||||
|
|
||||||
# Lookup unknown user.
|
# Lookup unknown user.
|
||||||
result, error_message = self.jwt_auth.get_user('unknownuser')
|
result, error_message = jwt_auth.get_user('unknownuser')
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
|
|
||||||
def test_link_user(self):
|
def test_link_user(self):
|
||||||
# Link cooluser.
|
with fake_jwt(self.emails) as jwt_auth:
|
||||||
user, error_message = self.jwt_auth.link_user('cooluser')
|
# Link cool.user.
|
||||||
|
user, error_message = jwt_auth.link_user('cool.user')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertIsNotNone(user)
|
self.assertIsNotNone(user)
|
||||||
self.assertEquals('cooluser', user.username)
|
self.assertEquals('cool_user', user.username)
|
||||||
|
|
||||||
# Link again. Should return the same user record.
|
# Link again. Should return the same user record.
|
||||||
user_again, _ = self.jwt_auth.link_user('cooluser')
|
user_again, _ = jwt_auth.link_user('cool.user')
|
||||||
self.assertEquals(user_again.id, user.id)
|
self.assertEquals(user_again.id, user.id)
|
||||||
|
|
||||||
# Confirm cooluser.
|
# Confirm cool.user.
|
||||||
result, _ = self.jwt_auth.confirm_existing_user('cooluser', 'password')
|
result, _ = jwt_auth.confirm_existing_user('cool_user', 'password')
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
self.assertEquals('cooluser', result.username)
|
self.assertEquals('cool_user', result.username)
|
||||||
|
|
||||||
def test_link_invalid_user(self):
|
def test_link_invalid_user(self):
|
||||||
user, error_message = self.jwt_auth.link_user('invaliduser')
|
with fake_jwt(self.emails) as jwt_auth:
|
||||||
|
user, error_message = jwt_auth.link_user('invaliduser')
|
||||||
self.assertIsNotNone(error_message)
|
self.assertIsNotNone(error_message)
|
||||||
self.assertIsNone(user)
|
self.assertIsNone(user)
|
||||||
|
|
||||||
|
|
||||||
class JWTAuthNoEmailTestCase(JWTAuthTestMixin, LiveServerTestCase):
|
class JWTAuthNoEmailTestCase(JWTAuthTestMixin, unittest.TestCase):
|
||||||
|
""" Test cases for JWT auth, with emails disabled. """
|
||||||
@property
|
@property
|
||||||
def emails(self):
|
def emails(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class JWTAuthTestCase(JWTAuthTestMixin, LiveServerTestCase):
|
class JWTAuthTestCase(JWTAuthTestMixin, unittest.TestCase):
|
||||||
|
""" Test cases for JWT auth, with emails enabled. """
|
||||||
@property
|
@property
|
||||||
def emails(self):
|
def emails(self):
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -5,33 +5,50 @@ import unittest
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from flask import Flask, request, abort, make_response
|
from flask import Flask, request, abort, make_response
|
||||||
from flask_testing import LiveServerTestCase
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from helpers import liveserver_app
|
||||||
from data.users.keystone import get_keystone_users
|
from data.users.keystone import get_keystone_users
|
||||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||||
|
|
||||||
_PORT_NUMBER = 5001
|
_PORT_NUMBER = 5001
|
||||||
|
|
||||||
class KeystoneAuthTestsMixin():
|
@contextmanager
|
||||||
maxDiff = None
|
def fake_keystone(version, requires_email=True):
|
||||||
|
""" Context manager which instantiates and runs a webserver with a fake Keystone implementation,
|
||||||
|
until the result is yielded.
|
||||||
|
|
||||||
@property
|
Usage:
|
||||||
def emails(self):
|
with fake_keystone(version) as keystone_auth:
|
||||||
raise NotImplementedError
|
# Make keystone_auth requests.
|
||||||
|
"""
|
||||||
|
keystone_app, port = _create_app(requires_email)
|
||||||
|
server_url = 'http://' + keystone_app.config['SERVER_HOSTNAME']
|
||||||
|
endpoint_url = server_url + '/v3'
|
||||||
|
if version == 2:
|
||||||
|
endpoint_url = server_url + '/v2.0/auth'
|
||||||
|
|
||||||
def create_app(self):
|
keystone_auth = get_keystone_users(version, endpoint_url,
|
||||||
|
'adminuser', 'adminpass', 'admintenant',
|
||||||
|
requires_email=requires_email)
|
||||||
|
with liveserver_app(keystone_app, port):
|
||||||
|
yield keystone_auth
|
||||||
|
|
||||||
|
|
||||||
|
def _create_app(requires_email=True):
|
||||||
global _PORT_NUMBER
|
global _PORT_NUMBER
|
||||||
_PORT_NUMBER = _PORT_NUMBER + 1
|
_PORT_NUMBER = _PORT_NUMBER + 1
|
||||||
|
|
||||||
|
server_url = 'http://localhost:%s' % (_PORT_NUMBER)
|
||||||
|
|
||||||
users = [
|
users = [
|
||||||
{'username': 'adminuser', 'name': 'Admin User', 'password': 'adminpass'},
|
{'username': 'adminuser', 'name': 'Admin User', 'password': 'adminpass'},
|
||||||
{'username': 'cooluser', 'name': 'Cool User', 'password': 'password'},
|
{'username': 'cool.user', 'name': 'Cool User', 'password': 'password'},
|
||||||
{'username': 'some.neat.user', 'name': 'Neat User', 'password': 'foobar'},
|
{'username': 'some.neat.user', 'name': 'Neat User', 'password': 'foobar'},
|
||||||
]
|
]
|
||||||
|
|
||||||
ks_app = Flask('testks')
|
ks_app = Flask('testks')
|
||||||
ks_app.config['LIVESERVER_PORT'] = _PORT_NUMBER
|
ks_app.config['SERVER_HOSTNAME'] = 'localhost:%s' % _PORT_NUMBER
|
||||||
|
|
||||||
if os.environ.get('DEBUG') == 'true':
|
if os.environ.get('DEBUG') == 'true':
|
||||||
ks_app.config['DEBUG'] = True
|
ks_app.config['DEBUG'] = True
|
||||||
|
|
||||||
|
@ -40,7 +57,7 @@ class KeystoneAuthTestsMixin():
|
||||||
for user in users:
|
for user in users:
|
||||||
if user['username'] == userid:
|
if user['username'] == userid:
|
||||||
user_data = {}
|
user_data = {}
|
||||||
if self.emails:
|
if requires_email:
|
||||||
user_data['email'] = userid + '@example.com'
|
user_data['email'] = userid + '@example.com'
|
||||||
|
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
|
@ -61,7 +78,7 @@ class KeystoneAuthTestsMixin():
|
||||||
"name": user['username'],
|
"name": user['username'],
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.emails:
|
if requires_email:
|
||||||
user_data['email'] = user['username'] + '@example.com'
|
user_data['email'] = user['username'] + '@example.com'
|
||||||
|
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
|
@ -118,7 +135,7 @@ class KeystoneAuthTestsMixin():
|
||||||
{
|
{
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
{
|
{
|
||||||
"url": self.get_server_url() + '/v3/identity',
|
"url": server_url + '/v3/identity',
|
||||||
"region": "RegionOne",
|
"region": "RegionOne",
|
||||||
"interface": "admin",
|
"interface": "admin",
|
||||||
"id": "29beb2f1567642eb810b042b6719ea88"
|
"id": "29beb2f1567642eb810b042b6719ea88"
|
||||||
|
@ -171,7 +188,7 @@ class KeystoneAuthTestsMixin():
|
||||||
{
|
{
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
{
|
{
|
||||||
"adminURL": self.get_server_url() + '/v2.0/admin',
|
"adminURL": server_url + '/v2.0/admin',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"endpoints_links": [],
|
"endpoints_links": [],
|
||||||
|
@ -195,7 +212,18 @@ class KeystoneAuthTestsMixin():
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
return ks_app
|
return ks_app, _PORT_NUMBER
|
||||||
|
|
||||||
|
|
||||||
|
class KeystoneAuthTestsMixin:
|
||||||
|
maxDiff = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def emails(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def fake_keystone(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
setup_database_for_testing(self)
|
setup_database_for_testing(self)
|
||||||
|
@ -204,100 +232,95 @@ class KeystoneAuthTestsMixin():
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
finished_database_for_testing(self)
|
finished_database_for_testing(self)
|
||||||
|
|
||||||
@property
|
|
||||||
def keystone(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def test_invalid_user(self):
|
def test_invalid_user(self):
|
||||||
(user, _) = self.keystone.verify_credentials('unknownuser', 'password')
|
with self.fake_keystone() as keystone:
|
||||||
|
(user, _) = keystone.verify_credentials('unknownuser', 'password')
|
||||||
self.assertIsNone(user)
|
self.assertIsNone(user)
|
||||||
|
|
||||||
def test_invalid_password(self):
|
def test_invalid_password(self):
|
||||||
(user, _) = self.keystone.verify_credentials('cooluser', 'notpassword')
|
with self.fake_keystone() as keystone:
|
||||||
|
(user, _) = keystone.verify_credentials('cool.user', 'notpassword')
|
||||||
self.assertIsNone(user)
|
self.assertIsNone(user)
|
||||||
|
|
||||||
def test_cooluser(self):
|
def test_cooluser(self):
|
||||||
(user, _) = self.keystone.verify_credentials('cooluser', 'password')
|
with self.fake_keystone() as keystone:
|
||||||
self.assertEquals(user.username, 'cooluser')
|
(user, _) = keystone.verify_credentials('cool.user', 'password')
|
||||||
self.assertEquals(user.email, 'cooluser@example.com' if self.emails else None)
|
self.assertEquals(user.username, 'cool.user')
|
||||||
|
self.assertEquals(user.email, 'cool.user@example.com' if self.emails else None)
|
||||||
|
|
||||||
def test_neatuser(self):
|
def test_neatuser(self):
|
||||||
(user, _) = self.keystone.verify_credentials('some.neat.user', 'foobar')
|
with self.fake_keystone() as keystone:
|
||||||
|
(user, _) = keystone.verify_credentials('some.neat.user', 'foobar')
|
||||||
self.assertEquals(user.username, 'some.neat.user')
|
self.assertEquals(user.username, 'some.neat.user')
|
||||||
self.assertEquals(user.email, 'some.neat.user@example.com' if self.emails else None)
|
self.assertEquals(user.email, 'some.neat.user@example.com' if self.emails else None)
|
||||||
|
|
||||||
class KeystoneV2AuthNoEmailTests(KeystoneAuthTestsMixin, LiveServerTestCase):
|
|
||||||
@property
|
class KeystoneV2AuthNoEmailTests(KeystoneAuthTestsMixin, unittest.TestCase):
|
||||||
def keystone(self):
|
def fake_keystone(self):
|
||||||
return get_keystone_users(2, self.get_server_url() + '/v2.0/auth',
|
return fake_keystone(2, requires_email=False)
|
||||||
'adminuser', 'adminpass', 'admintenant',
|
|
||||||
requires_email=False)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def emails(self):
|
def emails(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class KeystoneV3AuthNoEmailTests(KeystoneAuthTestsMixin, LiveServerTestCase):
|
class KeystoneV3AuthNoEmailTests(KeystoneAuthTestsMixin, unittest.TestCase):
|
||||||
@property
|
def fake_keystone(self):
|
||||||
def keystone(self):
|
return fake_keystone(3, requires_email=False)
|
||||||
return get_keystone_users(3, self.get_server_url() + '/v3',
|
|
||||||
'adminuser', 'adminpass', 'admintenant',
|
|
||||||
requires_email=False)
|
|
||||||
@property
|
@property
|
||||||
def emails(self):
|
def emails(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class KeystoneV2AuthTests(KeystoneAuthTestsMixin, LiveServerTestCase):
|
class KeystoneV2AuthTests(KeystoneAuthTestsMixin, unittest.TestCase):
|
||||||
@property
|
def fake_keystone(self):
|
||||||
def keystone(self):
|
return fake_keystone(2, requires_email=True)
|
||||||
return get_keystone_users(2, self.get_server_url() + '/v2.0/auth',
|
|
||||||
'adminuser', 'adminpass', 'admintenant',
|
|
||||||
requires_email=True)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def emails(self):
|
def emails(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
class KeystoneV3AuthTests(KeystoneAuthTestsMixin, LiveServerTestCase):
|
class KeystoneV3AuthTests(KeystoneAuthTestsMixin, unittest.TestCase):
|
||||||
@property
|
def fake_keystone(self):
|
||||||
def keystone(self):
|
return fake_keystone(3, requires_email=True)
|
||||||
return get_keystone_users(3, self.get_server_url() + '/v3',
|
|
||||||
'adminuser', 'adminpass', 'admintenant',
|
|
||||||
requires_email=True)
|
|
||||||
|
|
||||||
def emails(self):
|
def emails(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def test_query(self):
|
def test_query(self):
|
||||||
|
with self.fake_keystone() as keystone:
|
||||||
# Lookup cool.
|
# Lookup cool.
|
||||||
(response, error_message) = self.keystone.query_users('cool')
|
(response, federated_id, error_message) = keystone.query_users('cool')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertEquals(1, len(response))
|
self.assertEquals(1, len(response))
|
||||||
|
self.assertEquals('keystone', federated_id)
|
||||||
|
|
||||||
user_info = response[0]
|
user_info = response[0]
|
||||||
self.assertEquals("cooluser", user_info.username)
|
self.assertEquals("cool.user", user_info.username)
|
||||||
|
|
||||||
# Lookup unknown.
|
# Lookup unknown.
|
||||||
(response, error_message) = self.keystone.query_users('unknown')
|
(response, federated_id, error_message) = keystone.query_users('unknown')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertEquals(0, len(response))
|
self.assertEquals(0, len(response))
|
||||||
|
self.assertEquals('keystone', federated_id)
|
||||||
|
|
||||||
def test_link_user(self):
|
def test_link_user(self):
|
||||||
|
with self.fake_keystone() as keystone:
|
||||||
# Link someuser.
|
# Link someuser.
|
||||||
user, error_message = self.keystone.link_user('cooluser')
|
user, error_message = keystone.link_user('cool.user')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertIsNotNone(user)
|
self.assertIsNotNone(user)
|
||||||
self.assertEquals('cooluser', user.username)
|
self.assertEquals('cool_user', user.username)
|
||||||
self.assertEquals('cooluser@example.com', user.email)
|
self.assertEquals('cool.user@example.com', user.email)
|
||||||
|
|
||||||
# Link again. Should return the same user record.
|
# Link again. Should return the same user record.
|
||||||
user_again, _ = self.keystone.link_user('cooluser')
|
user_again, _ = keystone.link_user('cool.user')
|
||||||
self.assertEquals(user_again.id, user.id)
|
self.assertEquals(user_again.id, user.id)
|
||||||
|
|
||||||
# Confirm someuser.
|
# Confirm someuser.
|
||||||
result, _ = self.keystone.confirm_existing_user('cooluser', 'password')
|
result, _ = keystone.confirm_existing_user('cool_user', 'password')
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
self.assertEquals('cooluser', result.username)
|
self.assertEquals('cool_user', result.username)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -6,15 +6,25 @@ from data.users import LDAPUsers
|
||||||
from data import model
|
from data import model
|
||||||
from mockldap import MockLdap
|
from mockldap import MockLdap
|
||||||
from mock import patch
|
from mock import patch
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
class TestLDAP(unittest.TestCase):
|
def _create_ldap(requires_email=True):
|
||||||
def setUp(self):
|
base_dn = ['dc=quay', 'dc=io']
|
||||||
setup_database_for_testing(self)
|
admin_dn = 'uid=testy,ou=employees,dc=quay,dc=io'
|
||||||
self.app = app.test_client()
|
admin_passwd = 'password'
|
||||||
self.ctx = app.test_request_context()
|
user_rdn = ['ou=employees']
|
||||||
self.ctx.__enter__()
|
uid_attr = 'uid'
|
||||||
|
email_attr = 'mail'
|
||||||
|
secondary_user_rdns = ['ou=otheremployees']
|
||||||
|
|
||||||
self.mockldap = MockLdap({
|
ldap = LDAPUsers('ldap://localhost', base_dn, admin_dn, admin_passwd, user_rdn,
|
||||||
|
uid_attr, email_attr, secondary_user_rdns=secondary_user_rdns,
|
||||||
|
requires_email=requires_email)
|
||||||
|
return ldap
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_ldap(requires_email=True):
|
||||||
|
mockldap = MockLdap({
|
||||||
'dc=quay,dc=io': {'dc': ['quay', 'io']},
|
'dc=quay,dc=io': {'dc': ['quay', 'io']},
|
||||||
'ou=employees,dc=quay,dc=io': {
|
'ou=employees,dc=quay,dc=io': {
|
||||||
'dc': ['quay', 'io'],
|
'dc': ['quay', 'io'],
|
||||||
|
@ -76,157 +86,8 @@ class TestLDAP(unittest.TestCase):
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
self.mockldap.start()
|
|
||||||
self.ldap = self._create_ldap(requires_email=True)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.mockldap.stop()
|
|
||||||
finished_database_for_testing(self)
|
|
||||||
self.ctx.__exit__(True, None, None)
|
|
||||||
|
|
||||||
def _create_ldap(self, requires_email=True):
|
|
||||||
base_dn = ['dc=quay', 'dc=io']
|
|
||||||
admin_dn = 'uid=testy,ou=employees,dc=quay,dc=io'
|
|
||||||
admin_passwd = 'password'
|
|
||||||
user_rdn = ['ou=employees']
|
|
||||||
uid_attr = 'uid'
|
|
||||||
email_attr = 'mail'
|
|
||||||
secondary_user_rdns = ['ou=otheremployees']
|
|
||||||
|
|
||||||
ldap = LDAPUsers('ldap://localhost', base_dn, admin_dn, admin_passwd, user_rdn,
|
|
||||||
uid_attr, email_attr, secondary_user_rdns=secondary_user_rdns,
|
|
||||||
requires_email=requires_email)
|
|
||||||
return ldap
|
|
||||||
|
|
||||||
def test_invalid_admin_password(self):
|
|
||||||
base_dn = ['dc=quay', 'dc=io']
|
|
||||||
admin_dn = 'uid=testy,ou=employees,dc=quay,dc=io'
|
|
||||||
admin_passwd = 'INVALIDPASSWORD'
|
|
||||||
user_rdn = ['ou=employees']
|
|
||||||
uid_attr = 'uid'
|
|
||||||
email_attr = 'mail'
|
|
||||||
|
|
||||||
ldap = LDAPUsers('ldap://localhost', base_dn, admin_dn, admin_passwd, user_rdn,
|
|
||||||
uid_attr, email_attr)
|
|
||||||
|
|
||||||
self.ldap = ldap
|
|
||||||
|
|
||||||
# Try to login.
|
|
||||||
(response, err_msg) = self.ldap.verify_and_link_user('someuser', 'somepass')
|
|
||||||
self.assertIsNone(response)
|
|
||||||
self.assertEquals('LDAP Admin dn or password is invalid', err_msg)
|
|
||||||
|
|
||||||
def test_login(self):
|
|
||||||
# Verify we can login.
|
|
||||||
(response, _) = self.ldap.verify_and_link_user('someuser', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'someuser')
|
|
||||||
self.assertTrue(model.user.has_user_prompt(response, 'confirm_username'))
|
|
||||||
|
|
||||||
# Verify we can confirm the user.
|
|
||||||
(response, _) = self.ldap.confirm_existing_user('someuser', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'someuser')
|
|
||||||
|
|
||||||
def test_login_secondary(self):
|
|
||||||
# Verify we can login.
|
|
||||||
(response, _) = self.ldap.verify_and_link_user('secondaryuser', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'secondaryuser')
|
|
||||||
|
|
||||||
# Verify we can confirm the user.
|
|
||||||
(response, _) = self.ldap.confirm_existing_user('secondaryuser', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'secondaryuser')
|
|
||||||
|
|
||||||
def test_invalid_password(self):
|
|
||||||
# Verify we cannot login with an invalid password.
|
|
||||||
(response, err_msg) = self.ldap.verify_and_link_user('someuser', 'invalidpass')
|
|
||||||
self.assertIsNone(response)
|
|
||||||
self.assertEquals(err_msg, 'Invalid password')
|
|
||||||
|
|
||||||
# Verify we cannot confirm the user.
|
|
||||||
(response, err_msg) = self.ldap.confirm_existing_user('someuser', 'invalidpass')
|
|
||||||
self.assertIsNone(response)
|
|
||||||
self.assertEquals(err_msg, 'Invalid user')
|
|
||||||
|
|
||||||
def test_missing_mail(self):
|
|
||||||
(response, err_msg) = self.ldap.get_user('nomail')
|
|
||||||
self.assertIsNone(response)
|
|
||||||
self.assertEquals('Missing mail field "mail" in user record', err_msg)
|
|
||||||
|
|
||||||
def test_missing_mail_allowed(self):
|
|
||||||
ldap = self._create_ldap(requires_email=False)
|
|
||||||
(response, _) = ldap.get_user('nomail')
|
|
||||||
self.assertEquals(response.username, 'nomail')
|
|
||||||
|
|
||||||
def test_confirm_different_username(self):
|
|
||||||
# Verify that the user is logged in and their username was adjusted.
|
|
||||||
(response, _) = self.ldap.verify_and_link_user('cool.user', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'cool_user')
|
|
||||||
|
|
||||||
# Verify we can confirm the user's quay username.
|
|
||||||
(response, _) = self.ldap.confirm_existing_user('cool_user', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'cool_user')
|
|
||||||
|
|
||||||
# Verify that we *cannot* confirm the LDAP username.
|
|
||||||
(response, _) = self.ldap.confirm_existing_user('cool.user', 'somepass')
|
|
||||||
self.assertIsNone(response)
|
|
||||||
|
|
||||||
def test_referral(self):
|
|
||||||
(response, _) = self.ldap.verify_and_link_user('referred', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'cool_user')
|
|
||||||
|
|
||||||
# Verify we can confirm the user's quay username.
|
|
||||||
(response, _) = self.ldap.confirm_existing_user('cool_user', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'cool_user')
|
|
||||||
|
|
||||||
def test_invalid_referral(self):
|
|
||||||
(response, _) = self.ldap.verify_and_link_user('invalidreferred', 'somepass')
|
|
||||||
self.assertIsNone(response)
|
|
||||||
|
|
||||||
def test_multientry(self):
|
|
||||||
(response, _) = self.ldap.verify_and_link_user('multientry', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'multientry')
|
|
||||||
|
|
||||||
def test_login_empty_userdn(self):
|
|
||||||
base_dn = ['ou=employees', 'dc=quay', 'dc=io']
|
|
||||||
admin_dn = 'uid=testy,ou=employees,dc=quay,dc=io'
|
|
||||||
admin_passwd = 'password'
|
|
||||||
user_rdn = []
|
|
||||||
uid_attr = 'uid'
|
|
||||||
email_attr = 'mail'
|
|
||||||
secondary_user_rdns = ['ou=otheremployees']
|
|
||||||
|
|
||||||
ldap = LDAPUsers('ldap://localhost', base_dn, admin_dn, admin_passwd, user_rdn,
|
|
||||||
uid_attr, email_attr, secondary_user_rdns=secondary_user_rdns)
|
|
||||||
|
|
||||||
self.ldap = ldap
|
|
||||||
|
|
||||||
# Verify we can login.
|
|
||||||
(response, _) = self.ldap.verify_and_link_user('someuser', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'someuser')
|
|
||||||
|
|
||||||
# Verify we can confirm the user.
|
|
||||||
(response, _) = self.ldap.confirm_existing_user('someuser', 'somepass')
|
|
||||||
self.assertEquals(response.username, 'someuser')
|
|
||||||
|
|
||||||
def test_link_user(self):
|
|
||||||
# Link someuser.
|
|
||||||
user, error_message = self.ldap.link_user('someuser')
|
|
||||||
self.assertIsNone(error_message)
|
|
||||||
self.assertIsNotNone(user)
|
|
||||||
self.assertEquals('someuser', user.username)
|
|
||||||
|
|
||||||
# Link again. Should return the same user record.
|
|
||||||
user_again, _ = self.ldap.link_user('someuser')
|
|
||||||
self.assertEquals(user_again.id, user.id)
|
|
||||||
|
|
||||||
# Confirm someuser.
|
|
||||||
result, _ = self.ldap.confirm_existing_user('someuser', 'somepass')
|
|
||||||
self.assertIsNotNone(result)
|
|
||||||
self.assertEquals('someuser', result.username)
|
|
||||||
self.assertTrue(model.user.has_user_prompt(user, 'confirm_username'))
|
|
||||||
|
|
||||||
def test_query(self):
|
|
||||||
def initializer(uri, trace_level=0):
|
def initializer(uri, trace_level=0):
|
||||||
obj = self.mockldap[uri]
|
obj = mockldap[uri]
|
||||||
|
|
||||||
# Seed to "support" wildcard queries, which MockLDAP does not support natively.
|
# Seed to "support" wildcard queries, which MockLDAP does not support natively.
|
||||||
obj.search_s.seed('ou=employees,dc=quay,dc=io', 2, '(|(uid=cool*)(mail=cool*))')([
|
obj.search_s.seed('ou=employees,dc=quay,dc=io', 2, '(|(uid=cool*)(mail=cool*))')([
|
||||||
|
@ -246,20 +107,173 @@ class TestLDAP(unittest.TestCase):
|
||||||
'(|(uid=unknown*)(mail=unknown*))')([])
|
'(|(uid=unknown*)(mail=unknown*))')([])
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
mockldap.start()
|
||||||
with patch('ldap.initialize', new=initializer):
|
with patch('ldap.initialize', new=initializer):
|
||||||
|
yield _create_ldap(requires_email=requires_email)
|
||||||
|
mockldap.stop()
|
||||||
|
|
||||||
|
|
||||||
|
class TestLDAP(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
setup_database_for_testing(self)
|
||||||
|
self.app = app.test_client()
|
||||||
|
self.ctx = app.test_request_context()
|
||||||
|
self.ctx.__enter__()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
finished_database_for_testing(self)
|
||||||
|
self.ctx.__exit__(True, None, None)
|
||||||
|
|
||||||
|
def test_invalid_admin_password(self):
|
||||||
|
base_dn = ['dc=quay', 'dc=io']
|
||||||
|
admin_dn = 'uid=testy,ou=employees,dc=quay,dc=io'
|
||||||
|
admin_passwd = 'INVALIDPASSWORD'
|
||||||
|
user_rdn = ['ou=employees']
|
||||||
|
uid_attr = 'uid'
|
||||||
|
email_attr = 'mail'
|
||||||
|
|
||||||
|
with mock_ldap():
|
||||||
|
ldap = LDAPUsers('ldap://localhost', base_dn, admin_dn, admin_passwd, user_rdn,
|
||||||
|
uid_attr, email_attr)
|
||||||
|
|
||||||
|
# Try to login.
|
||||||
|
(response, err_msg) = ldap.verify_and_link_user('someuser', 'somepass')
|
||||||
|
self.assertIsNone(response)
|
||||||
|
self.assertEquals('LDAP Admin dn or password is invalid', err_msg)
|
||||||
|
|
||||||
|
def test_login(self):
|
||||||
|
with mock_ldap() as ldap:
|
||||||
|
# Verify we can login.
|
||||||
|
(response, _) = ldap.verify_and_link_user('someuser', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'someuser')
|
||||||
|
self.assertTrue(model.user.has_user_prompt(response, 'confirm_username'))
|
||||||
|
|
||||||
|
# Verify we can confirm the user.
|
||||||
|
(response, _) = ldap.confirm_existing_user('someuser', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'someuser')
|
||||||
|
|
||||||
|
def test_login_secondary(self):
|
||||||
|
with mock_ldap() as ldap:
|
||||||
|
# Verify we can login.
|
||||||
|
(response, _) = ldap.verify_and_link_user('secondaryuser', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'secondaryuser')
|
||||||
|
|
||||||
|
# Verify we can confirm the user.
|
||||||
|
(response, _) = ldap.confirm_existing_user('secondaryuser', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'secondaryuser')
|
||||||
|
|
||||||
|
def test_invalid_password(self):
|
||||||
|
with mock_ldap() as ldap:
|
||||||
|
# Verify we cannot login with an invalid password.
|
||||||
|
(response, err_msg) = ldap.verify_and_link_user('someuser', 'invalidpass')
|
||||||
|
self.assertIsNone(response)
|
||||||
|
self.assertEquals(err_msg, 'Invalid password')
|
||||||
|
|
||||||
|
# Verify we cannot confirm the user.
|
||||||
|
(response, err_msg) = ldap.confirm_existing_user('someuser', 'invalidpass')
|
||||||
|
self.assertIsNone(response)
|
||||||
|
self.assertEquals(err_msg, 'Invalid user')
|
||||||
|
|
||||||
|
def test_missing_mail(self):
|
||||||
|
with mock_ldap() as ldap:
|
||||||
|
(response, err_msg) = ldap.get_user('nomail')
|
||||||
|
self.assertIsNone(response)
|
||||||
|
self.assertEquals('Missing mail field "mail" in user record', err_msg)
|
||||||
|
|
||||||
|
def test_missing_mail_allowed(self):
|
||||||
|
with mock_ldap(requires_email=False) as ldap:
|
||||||
|
(response, _) = ldap.get_user('nomail')
|
||||||
|
self.assertEquals(response.username, 'nomail')
|
||||||
|
|
||||||
|
def test_confirm_different_username(self):
|
||||||
|
with mock_ldap() as ldap:
|
||||||
|
# Verify that the user is logged in and their username was adjusted.
|
||||||
|
(response, _) = ldap.verify_and_link_user('cool.user', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'cool_user')
|
||||||
|
|
||||||
|
# Verify we can confirm the user's quay username.
|
||||||
|
(response, _) = ldap.confirm_existing_user('cool_user', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'cool_user')
|
||||||
|
|
||||||
|
# Verify that we *cannot* confirm the LDAP username.
|
||||||
|
(response, _) = ldap.confirm_existing_user('cool.user', 'somepass')
|
||||||
|
self.assertIsNone(response)
|
||||||
|
|
||||||
|
def test_referral(self):
|
||||||
|
with mock_ldap() as ldap:
|
||||||
|
(response, _) = ldap.verify_and_link_user('referred', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'cool_user')
|
||||||
|
|
||||||
|
# Verify we can confirm the user's quay username.
|
||||||
|
(response, _) = ldap.confirm_existing_user('cool_user', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'cool_user')
|
||||||
|
|
||||||
|
def test_invalid_referral(self):
|
||||||
|
with mock_ldap() as ldap:
|
||||||
|
(response, _) = ldap.verify_and_link_user('invalidreferred', 'somepass')
|
||||||
|
self.assertIsNone(response)
|
||||||
|
|
||||||
|
def test_multientry(self):
|
||||||
|
with mock_ldap() as ldap:
|
||||||
|
(response, _) = ldap.verify_and_link_user('multientry', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'multientry')
|
||||||
|
|
||||||
|
def test_login_empty_userdn(self):
|
||||||
|
with mock_ldap():
|
||||||
|
base_dn = ['ou=employees', 'dc=quay', 'dc=io']
|
||||||
|
admin_dn = 'uid=testy,ou=employees,dc=quay,dc=io'
|
||||||
|
admin_passwd = 'password'
|
||||||
|
user_rdn = []
|
||||||
|
uid_attr = 'uid'
|
||||||
|
email_attr = 'mail'
|
||||||
|
secondary_user_rdns = ['ou=otheremployees']
|
||||||
|
|
||||||
|
ldap = LDAPUsers('ldap://localhost', base_dn, admin_dn, admin_passwd, user_rdn,
|
||||||
|
uid_attr, email_attr, secondary_user_rdns=secondary_user_rdns)
|
||||||
|
|
||||||
|
# Verify we can login.
|
||||||
|
(response, _) = ldap.verify_and_link_user('someuser', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'someuser')
|
||||||
|
|
||||||
|
# Verify we can confirm the user.
|
||||||
|
(response, _) = ldap.confirm_existing_user('someuser', 'somepass')
|
||||||
|
self.assertEquals(response.username, 'someuser')
|
||||||
|
|
||||||
|
def test_link_user(self):
|
||||||
|
with mock_ldap() as ldap:
|
||||||
|
# Link someuser.
|
||||||
|
user, error_message = ldap.link_user('someuser')
|
||||||
|
self.assertIsNone(error_message)
|
||||||
|
self.assertIsNotNone(user)
|
||||||
|
self.assertEquals('someuser', user.username)
|
||||||
|
|
||||||
|
# Link again. Should return the same user record.
|
||||||
|
user_again, _ = ldap.link_user('someuser')
|
||||||
|
self.assertEquals(user_again.id, user.id)
|
||||||
|
|
||||||
|
# Confirm someuser.
|
||||||
|
result, _ = ldap.confirm_existing_user('someuser', 'somepass')
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
self.assertEquals('someuser', result.username)
|
||||||
|
self.assertTrue(model.user.has_user_prompt(user, 'confirm_username'))
|
||||||
|
|
||||||
|
def test_query(self):
|
||||||
|
with mock_ldap() as ldap:
|
||||||
# Lookup cool.
|
# Lookup cool.
|
||||||
(response, error_message) = self.ldap.query_users('cool')
|
(response, federated_id, error_message) = ldap.query_users('cool')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertEquals(1, len(response))
|
self.assertEquals(1, len(response))
|
||||||
|
self.assertEquals('ldap', federated_id)
|
||||||
|
|
||||||
user_info = response[0]
|
user_info = response[0]
|
||||||
self.assertEquals("cool.user", user_info.username)
|
self.assertEquals("cool.user", user_info.username)
|
||||||
self.assertEquals("foo@bar.com", user_info.email)
|
self.assertEquals("foo@bar.com", user_info.email)
|
||||||
|
|
||||||
# Lookup unknown.
|
# Lookup unknown.
|
||||||
(response, error_message) = self.ldap.query_users('unknown')
|
(response, federated_id, error_message) = ldap.query_users('unknown')
|
||||||
self.assertIsNone(error_message)
|
self.assertIsNone(error_message)
|
||||||
self.assertEquals(0, len(response))
|
self.assertEquals(0, len(response))
|
||||||
|
self.assertEquals('ldap', federated_id)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
Reference in a new issue