Merge pull request #1905 from coreos-inc/external-auth-search
Add support for entity search against external auth users not yet linked
This commit is contained in:
commit
934cdecbd6
16 changed files with 817 additions and 100 deletions
|
@ -65,6 +65,9 @@ class BaseAvatar(object):
|
||||||
def get_data_for_org(self, org):
|
def get_data_for_org(self, org):
|
||||||
return self.get_data(org.username, org.email, 'org')
|
return self.get_data(org.username, org.email, 'org')
|
||||||
|
|
||||||
|
def get_data_for_external_user(self, external_user):
|
||||||
|
return self.get_data(external_user.username, external_user.email, 'user')
|
||||||
|
|
||||||
def get_data(self, name, email_or_id, kind='user'):
|
def get_data(self, name, email_or_id, kind='user'):
|
||||||
""" Computes and returns the full data block for the avatar:
|
""" Computes and returns the full data block for the avatar:
|
||||||
{
|
{
|
||||||
|
|
|
@ -499,8 +499,7 @@ def get_matching_user_namespaces(namespace_prefix, username, limit=10):
|
||||||
|
|
||||||
return _basequery.filter_to_repos_for_user(base_query, username).limit(limit)
|
return _basequery.filter_to_repos_for_user(base_query, username).limit(limit)
|
||||||
|
|
||||||
def get_matching_users(username_prefix, robot_namespace=None,
|
def get_matching_users(username_prefix, robot_namespace=None, organization=None, limit=20):
|
||||||
organization=None):
|
|
||||||
user_search = _basequery.prefix_search(User.username, username_prefix)
|
user_search = _basequery.prefix_search(User.username, username_prefix)
|
||||||
direct_user_query = (user_search & (User.organization == False) & (User.robot == False))
|
direct_user_query = (user_search & (User.organization == False) & (User.robot == False))
|
||||||
|
|
||||||
|
@ -516,23 +515,24 @@ def get_matching_users(username_prefix, robot_namespace=None,
|
||||||
|
|
||||||
if organization:
|
if organization:
|
||||||
query = (query
|
query = (query
|
||||||
.select(User.username, User.email, User.robot, fn.Sum(Team.id))
|
.select(User.id, User.username, User.email, User.robot, fn.Sum(Team.id))
|
||||||
.join(TeamMember, JOIN_LEFT_OUTER)
|
.join(TeamMember, JOIN_LEFT_OUTER)
|
||||||
.join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) &
|
.join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) &
|
||||||
(Team.organization == organization))))
|
(Team.organization == organization))))
|
||||||
|
|
||||||
class MatchingUserResult(object):
|
class MatchingUserResult(object):
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
self.username = args[0]
|
self.id = args[0]
|
||||||
self.email = args[1]
|
self.username = args[1]
|
||||||
self.robot = args[2]
|
self.email = args[2]
|
||||||
|
self.robot = args[3]
|
||||||
|
|
||||||
if organization:
|
if organization:
|
||||||
self.is_org_member = (args[3] != None)
|
self.is_org_member = (args[3] != None)
|
||||||
else:
|
else:
|
||||||
self.is_org_member = None
|
self.is_org_member = None
|
||||||
|
|
||||||
return (MatchingUserResult(*args) for args in query.tuples().limit(10))
|
return (MatchingUserResult(*args) for args in query.tuples().limit(limit))
|
||||||
|
|
||||||
|
|
||||||
def verify_user(username_or_email, password):
|
def verify_user(username_or_email, password):
|
||||||
|
@ -749,3 +749,16 @@ def get_region_locations(user):
|
||||||
""" Returns the locations defined as preferred storage for the given user. """
|
""" Returns the locations defined as preferred storage for the given user. """
|
||||||
query = UserRegion.select().join(ImageStorageLocation).where(UserRegion.user == user)
|
query = UserRegion.select().join(ImageStorageLocation).where(UserRegion.user == user)
|
||||||
return set([region.location.name for region in query])
|
return set([region.location.name for region in query])
|
||||||
|
|
||||||
|
def get_federated_logins(user_ids, service_name):
|
||||||
|
""" Returns all federated logins for the given user ids under the given external service. """
|
||||||
|
if not user_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return (FederatedLogin
|
||||||
|
.select()
|
||||||
|
.join(User)
|
||||||
|
.switch(FederatedLogin)
|
||||||
|
.join(LoginService)
|
||||||
|
.where(FederatedLogin.user << user_ids,
|
||||||
|
LoginService.name == service_name))
|
||||||
|
|
|
@ -9,7 +9,7 @@ from data import model
|
||||||
from data.users.database import DatabaseUsers
|
from data.users.database import DatabaseUsers
|
||||||
from data.users.externalldap import LDAPUsers
|
from data.users.externalldap import LDAPUsers
|
||||||
from data.users.externaljwt import ExternalJWTAuthN
|
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
|
from util.security.aes import AESCipher
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -54,17 +54,23 @@ def get_users_handler(config, config_provider, override_config_dir):
|
||||||
verify_url = config.get('JWT_VERIFY_ENDPOINT')
|
verify_url = config.get('JWT_VERIFY_ENDPOINT')
|
||||||
issuer = config.get('JWT_AUTH_ISSUER')
|
issuer = config.get('JWT_AUTH_ISSUER')
|
||||||
max_fresh_s = config.get('JWT_AUTH_MAX_FRESH_S', 300)
|
max_fresh_s = config.get('JWT_AUTH_MAX_FRESH_S', 300)
|
||||||
return ExternalJWTAuthN(verify_url, issuer, override_config_dir, config['HTTPCLIENT'],
|
|
||||||
max_fresh_s)
|
query_url = config.get('JWT_QUERY_ENDPOINT', None)
|
||||||
|
getuser_url = config.get('JWT_GETUSER_ENDPOINT', None)
|
||||||
|
|
||||||
|
return ExternalJWTAuthN(verify_url, query_url, getuser_url, issuer, override_config_dir,
|
||||||
|
config['HTTPCLIENT'], max_fresh_s)
|
||||||
|
|
||||||
if authentication_type == 'Keystone':
|
if authentication_type == 'Keystone':
|
||||||
auth_url = config.get('KEYSTONE_AUTH_URL')
|
auth_url = config.get('KEYSTONE_AUTH_URL')
|
||||||
|
auth_version = int(config.get('KEYSTONE_AUTH_VERSION', 2))
|
||||||
timeout = config.get('KEYSTONE_AUTH_TIMEOUT')
|
timeout = config.get('KEYSTONE_AUTH_TIMEOUT')
|
||||||
keystone_admin_username = config.get('KEYSTONE_ADMIN_USERNAME')
|
keystone_admin_username = config.get('KEYSTONE_ADMIN_USERNAME')
|
||||||
keystone_admin_password = config.get('KEYSTONE_ADMIN_PASSWORD')
|
keystone_admin_password = config.get('KEYSTONE_ADMIN_PASSWORD')
|
||||||
keystone_admin_tenant = config.get('KEYSTONE_ADMIN_TENANT')
|
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)
|
raise RuntimeError('Unknown authentication type: %s' % authentication_type)
|
||||||
|
|
||||||
|
@ -139,6 +145,31 @@ class UserAuthentication(object):
|
||||||
|
|
||||||
return data.get('password', encrypted)
|
return data.get('password', encrypted)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def federated_service(self):
|
||||||
|
""" Returns the name of the federated service for the auth system. If none, should return None.
|
||||||
|
"""
|
||||||
|
return self.state.federated_service
|
||||||
|
|
||||||
|
def query_users(self, query, limit=20):
|
||||||
|
""" Performs a lookup against the user system for the specified query. The returned tuple
|
||||||
|
will be of the form (results, federated_login_id, err_msg). If the method is unsupported,
|
||||||
|
the results portion of the tuple will be None instead of empty list.
|
||||||
|
|
||||||
|
Note that this method can and will return results for users not yet found within the
|
||||||
|
database; it is the responsibility of the caller to call link_user if they need the
|
||||||
|
database row for the user system record.
|
||||||
|
|
||||||
|
Results will be in the form of objects's with username and email fields.
|
||||||
|
"""
|
||||||
|
return self.state.query_users(query, limit)
|
||||||
|
|
||||||
|
def link_user(self, username_or_email):
|
||||||
|
""" Returns a tuple containing the database user record linked to the given username/email
|
||||||
|
and any error that occurred when trying to link the user.
|
||||||
|
"""
|
||||||
|
return self.state.link_user(username_or_email)
|
||||||
|
|
||||||
def confirm_existing_user(self, username, password):
|
def confirm_existing_user(self, username, password):
|
||||||
""" Verifies that the given password matches to the given DB username. Unlike
|
""" Verifies that the given password matches to the given DB username. Unlike
|
||||||
verify_credentials, this call first translates the DB user via the FederatedLogin table
|
verify_credentials, this call first translates the DB user via the FederatedLogin table
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
from data import model
|
from data import model
|
||||||
|
|
||||||
class DatabaseUsers(object):
|
class DatabaseUsers(object):
|
||||||
|
@property
|
||||||
|
def federated_service(self):
|
||||||
|
return None
|
||||||
|
|
||||||
def verify_credentials(self, username_or_email, password):
|
def verify_credentials(self, username_or_email, password):
|
||||||
""" Simply delegate to the model implementation. """
|
""" Simply delegate to the model implementation. """
|
||||||
result = model.user.verify_user(username_or_email, password)
|
result = model.user.verify_user(username_or_email, password)
|
||||||
|
@ -16,3 +20,11 @@ class DatabaseUsers(object):
|
||||||
def confirm_existing_user(self, username, password):
|
def confirm_existing_user(self, username, password):
|
||||||
return self.verify_credentials(username, password)
|
return self.verify_credentials(username, password)
|
||||||
|
|
||||||
|
def link_user(self, username_or_email):
|
||||||
|
""" Never used since all users being added are already, by definition, in the database. """
|
||||||
|
return (None, 'Unsupported for this authentication system')
|
||||||
|
|
||||||
|
def query_users(self, query, limit):
|
||||||
|
""" No need to implement, as we already query for users directly in the database. """
|
||||||
|
return (None, '', '')
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from data.users.federated import FederatedUsers, VerifiedCredentials
|
from data.users.federated import FederatedUsers, UserInformation
|
||||||
from util.security import jwtutil
|
from util.security import jwtutil
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,10 +13,13 @@ class ExternalJWTAuthN(FederatedUsers):
|
||||||
""" Delegates authentication to a REST endpoint that returns JWTs. """
|
""" Delegates authentication to a REST endpoint that returns JWTs. """
|
||||||
PUBLIC_KEY_FILENAME = 'jwt-authn.cert'
|
PUBLIC_KEY_FILENAME = 'jwt-authn.cert'
|
||||||
|
|
||||||
def __init__(self, verify_url, issuer, override_config_dir, http_client, max_fresh_s,
|
def __init__(self, verify_url, query_url, getuser_url, issuer, override_config_dir, http_client,
|
||||||
public_key_path=None):
|
max_fresh_s, public_key_path=None):
|
||||||
super(ExternalJWTAuthN, self).__init__('jwtauthn')
|
super(ExternalJWTAuthN, self).__init__('jwtauthn')
|
||||||
self.verify_url = verify_url
|
self.verify_url = verify_url
|
||||||
|
self.query_url = query_url
|
||||||
|
self.getuser_url = getuser_url
|
||||||
|
|
||||||
self.issuer = issuer
|
self.issuer = issuer
|
||||||
self.client = http_client
|
self.client = http_client
|
||||||
self.max_fresh_s = max_fresh_s
|
self.max_fresh_s = max_fresh_s
|
||||||
|
@ -32,34 +35,80 @@ class ExternalJWTAuthN(FederatedUsers):
|
||||||
with open(public_key_path) as public_key_file:
|
with open(public_key_path) as public_key_file:
|
||||||
self.public_key = public_key_file.read()
|
self.public_key = public_key_file.read()
|
||||||
|
|
||||||
def verify_credentials(self, username_or_email, password):
|
|
||||||
result = self.client.get(self.verify_url, timeout=2, auth=(username_or_email, password))
|
|
||||||
|
|
||||||
|
def get_user(self, username_or_email):
|
||||||
|
if self.getuser_url is None:
|
||||||
|
return (None, 'No endpoint defined for retrieving user')
|
||||||
|
|
||||||
|
(payload, err_msg) = self._execute_call(self.getuser_url, 'quay.io/jwtauthn/getuser',
|
||||||
|
params=dict(username=username_or_email))
|
||||||
|
if err_msg is not None:
|
||||||
|
return (None, err_msg)
|
||||||
|
|
||||||
|
if not 'sub' in payload:
|
||||||
|
raise Exception('Missing sub field in JWT')
|
||||||
|
|
||||||
|
if not 'email' in payload:
|
||||||
|
raise Exception('Missing email field in JWT')
|
||||||
|
|
||||||
|
# Parse out the username and email.
|
||||||
|
user_info = UserInformation(username=payload['sub'], email=payload['email'], id=payload['sub'])
|
||||||
|
return (user_info, None)
|
||||||
|
|
||||||
|
|
||||||
|
def query_users(self, query, limit=20):
|
||||||
|
if self.query_url is None:
|
||||||
|
return (None, self.federated_service, 'No endpoint defined for querying users')
|
||||||
|
|
||||||
|
(payload, err_msg) = self._execute_call(self.query_url, 'quay.io/jwtauthn/query',
|
||||||
|
params=dict(query=query, limit=limit))
|
||||||
|
if err_msg is not None:
|
||||||
|
return (None, self.federated_service, err_msg)
|
||||||
|
|
||||||
|
query_results = []
|
||||||
|
for result in payload['results'][0:limit]:
|
||||||
|
user_info = UserInformation(username=result['username'], email=result['email'],
|
||||||
|
id=result['username'])
|
||||||
|
query_results.append(user_info)
|
||||||
|
|
||||||
|
return (query_results, self.federated_service, None)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_credentials(self, username_or_email, password):
|
||||||
|
(payload, err_msg) = self._execute_call(self.verify_url, 'quay.io/jwtauthn',
|
||||||
|
auth=(username_or_email, password))
|
||||||
|
if err_msg is not None:
|
||||||
|
return (None, err_msg)
|
||||||
|
|
||||||
|
if not 'sub' in payload:
|
||||||
|
raise Exception('Missing sub field in JWT')
|
||||||
|
|
||||||
|
if not 'email' in payload:
|
||||||
|
raise Exception('Missing email field in JWT')
|
||||||
|
|
||||||
|
user_info = UserInformation(username=payload['sub'], email=payload['email'], id=payload['sub'])
|
||||||
|
return (user_info, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_call(self, url, aud, auth=None, params=None):
|
||||||
|
""" Executes a call to the external JWT auth provider. """
|
||||||
|
result = self.client.get(url, timeout=2, auth=auth, params=params)
|
||||||
if result.status_code != 200:
|
if result.status_code != 200:
|
||||||
return (None, result.text or 'Invalid username or password')
|
return (None, result.text or 'Could not make JWT auth call')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result_data = json.loads(result.text)
|
result_data = json.loads(result.text)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise Exception('Returned JWT Authentication body does not contain JSON')
|
raise Exception('Returned JWT body for url %s does not contain JSON', url)
|
||||||
|
|
||||||
# Load the JWT returned.
|
# Load the JWT returned.
|
||||||
encoded = result_data.get('token', '')
|
encoded = result_data.get('token', '')
|
||||||
exp_limit_options = jwtutil.exp_max_s_option(self.max_fresh_s)
|
exp_limit_options = jwtutil.exp_max_s_option(self.max_fresh_s)
|
||||||
try:
|
try:
|
||||||
payload = jwtutil.decode(encoded, self.public_key, algorithms=['RS256'],
|
payload = jwtutil.decode(encoded, self.public_key, algorithms=['RS256'],
|
||||||
audience='quay.io/jwtauthn', issuer=self.issuer,
|
audience=aud, issuer=self.issuer,
|
||||||
options=exp_limit_options)
|
options=exp_limit_options)
|
||||||
|
return (payload, None)
|
||||||
except jwtutil.InvalidTokenError:
|
except jwtutil.InvalidTokenError:
|
||||||
logger.exception('Exception when decoding returned JWT')
|
logger.exception('Exception when decoding returned JWT for url %s', url)
|
||||||
return (None, 'Invalid username or password')
|
return (None, 'Exception when decoding returned JWT')
|
||||||
|
|
||||||
if not 'sub' in payload:
|
|
||||||
raise Exception('Missing username field in JWT')
|
|
||||||
|
|
||||||
if not 'email' in payload:
|
|
||||||
raise Exception('Missing email field in JWT')
|
|
||||||
|
|
||||||
# Parse out the username and email.
|
|
||||||
return (VerifiedCredentials(username=payload['sub'], email=payload['email']), None)
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import ldap
|
import ldap
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import itertools
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from data.users.federated import FederatedUsers, VerifiedCredentials
|
from data.users.federated import FederatedUsers, UserInformation
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -25,11 +26,11 @@ class LDAPConnection(object):
|
||||||
self._user_dn = user_dn
|
self._user_dn = user_dn
|
||||||
self._user_pw = user_pw
|
self._user_pw = user_pw
|
||||||
self._allow_tls_fallback = allow_tls_fallback
|
self._allow_tls_fallback = allow_tls_fallback
|
||||||
|
|
||||||
self._conn = None
|
self._conn = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
trace_level = 2 if os.environ.get('USERS_DEBUG') == '1' else 0
|
trace_level = 2 if os.environ.get('USERS_DEBUG') == '1' else 0
|
||||||
|
|
||||||
self._conn = ldap.initialize(self._ldap_uri, trace_level=trace_level)
|
self._conn = ldap.initialize(self._ldap_uri, trace_level=trace_level)
|
||||||
self._conn.set_option(ldap.OPT_REFERRALS, 1)
|
self._conn.set_option(ldap.OPT_REFERRALS, 1)
|
||||||
|
|
||||||
|
@ -43,6 +44,10 @@ class LDAPConnection(object):
|
||||||
def __exit__(self, exc_type, value, tb):
|
def __exit__(self, exc_type, value, tb):
|
||||||
self._conn.unbind_s()
|
self._conn.unbind_s()
|
||||||
|
|
||||||
|
def _take(n, iterable):
|
||||||
|
"Return first n items of the iterable as a list"
|
||||||
|
return list(itertools.islice(iterable, n))
|
||||||
|
|
||||||
|
|
||||||
class LDAPUsers(FederatedUsers):
|
class LDAPUsers(FederatedUsers):
|
||||||
_LDAPResult = namedtuple('LDAPResult', ['dn', 'attrs'])
|
_LDAPResult = namedtuple('LDAPResult', ['dn', 'attrs'])
|
||||||
|
@ -51,6 +56,7 @@ class LDAPUsers(FederatedUsers):
|
||||||
allow_tls_fallback=False, secondary_user_rdns=None):
|
allow_tls_fallback=False, secondary_user_rdns=None):
|
||||||
|
|
||||||
super(LDAPUsers, self).__init__('ldap')
|
super(LDAPUsers, self).__init__('ldap')
|
||||||
|
|
||||||
self._ldap = LDAPConnectionBuilder(ldap_uri, admin_dn, admin_passwd, allow_tls_fallback)
|
self._ldap = LDAPConnectionBuilder(ldap_uri, admin_dn, admin_passwd, allow_tls_fallback)
|
||||||
self._ldap_uri = ldap_uri
|
self._ldap_uri = ldap_uri
|
||||||
self._uid_attr = uid_attr
|
self._uid_attr = uid_attr
|
||||||
|
@ -109,7 +115,10 @@ class LDAPUsers(FederatedUsers):
|
||||||
logger.exception('LDAP search exception')
|
logger.exception('LDAP search exception')
|
||||||
return (None, 'Username not found')
|
return (None, 'Username not found')
|
||||||
|
|
||||||
def _ldap_user_search(self, username_or_email):
|
def _ldap_user_search(self, username_or_email, limit=20):
|
||||||
|
if not username_or_email:
|
||||||
|
return (None, 'Empty username/email')
|
||||||
|
|
||||||
# Verify the admin connection works first. We do this here to avoid wrapping
|
# Verify the admin connection works first. We do this here to avoid wrapping
|
||||||
# the entire block in the INVALID CREDENTIALS check.
|
# the entire block in the INVALID CREDENTIALS check.
|
||||||
try:
|
try:
|
||||||
|
@ -130,22 +139,72 @@ class LDAPUsers(FederatedUsers):
|
||||||
return (None, err_msg)
|
return (None, err_msg)
|
||||||
|
|
||||||
logger.debug('Found matching pairs: %s', pairs)
|
logger.debug('Found matching pairs: %s', pairs)
|
||||||
results = [LDAPUsers._LDAPResult(*pair) for pair in pairs]
|
results = [LDAPUsers._LDAPResult(*pair) for pair in _take(limit, pairs)]
|
||||||
|
|
||||||
# Filter out pairs without DNs. Some LDAP impls will return such
|
# Filter out pairs without DNs. Some LDAP impls will return such pairs.
|
||||||
# pairs.
|
|
||||||
with_dns = [result for result in results if result.dn]
|
with_dns = [result for result in results if result.dn]
|
||||||
if len(with_dns) < 1:
|
return (with_dns, None)
|
||||||
return (None, 'Username not found')
|
|
||||||
|
|
||||||
# If we have found a single pair, then return it.
|
def _ldap_single_user_search(self, username_or_email):
|
||||||
if len(with_dns) == 1:
|
with_dns, err_msg = self._ldap_user_search(username_or_email)
|
||||||
return (with_dns[0], None)
|
if err_msg is not None:
|
||||||
|
return (None, err_msg)
|
||||||
|
|
||||||
# Otherwise, there are multiple pairs with DNs, so find the one with the mail
|
# Make sure we have at least one result.
|
||||||
# attribute (if any).
|
if len(with_dns) < 1:
|
||||||
with_mail = [result for result in results if result.attrs.get(self._email_attr)]
|
return (None, 'Username not found')
|
||||||
return (with_mail[0] if with_mail else with_dns[0], None)
|
|
||||||
|
# If we have found a single pair, then return it.
|
||||||
|
if len(with_dns) == 1:
|
||||||
|
return (with_dns[0], None)
|
||||||
|
|
||||||
|
# Otherwise, there are multiple pairs with DNs, so find the one with the mail
|
||||||
|
# attribute (if any).
|
||||||
|
with_mail = [result for result in with_dns if result.attrs.get(self._email_attr)]
|
||||||
|
return (with_mail[0] if with_mail else with_dns[0], None)
|
||||||
|
|
||||||
|
def _credential_for_user(self, response):
|
||||||
|
if not response.get(self._uid_attr):
|
||||||
|
return (None, 'Missing uid field "%s" in user record' % self._uid_attr)
|
||||||
|
|
||||||
|
if not response.get(self._email_attr):
|
||||||
|
return (None, 'Missing mail field "%s" in user record' % self._email_attr)
|
||||||
|
|
||||||
|
username = response[self._uid_attr][0].decode('utf-8')
|
||||||
|
email = response[self._email_attr][0]
|
||||||
|
return (UserInformation(username=username, email=email, id=username), None)
|
||||||
|
|
||||||
|
def get_user(self, username_or_email):
|
||||||
|
""" Looks up a username or email in LDAP. """
|
||||||
|
logger.debug('Looking up LDAP username or email %s', username_or_email)
|
||||||
|
(found_user, err_msg) = self._ldap_single_user_search(username_or_email)
|
||||||
|
if err_msg is not None:
|
||||||
|
return (None, err_msg)
|
||||||
|
|
||||||
|
logger.debug('Found user for LDAP username or email %s', username_or_email)
|
||||||
|
_, found_response = found_user
|
||||||
|
return self._credential_for_user(found_response)
|
||||||
|
|
||||||
|
def query_users(self, query, limit=20):
|
||||||
|
""" Queries LDAP for matching users. """
|
||||||
|
if not query:
|
||||||
|
return (None, 'Empty query')
|
||||||
|
|
||||||
|
logger.debug('Got query %s with limit %s', query, limit)
|
||||||
|
(results, err_msg) = self._ldap_user_search(query + '*', limit=limit)
|
||||||
|
if err_msg is not None:
|
||||||
|
return (None, err_msg)
|
||||||
|
|
||||||
|
final_results = []
|
||||||
|
for result in results[0:limit]:
|
||||||
|
credentials, err_msg = self._credential_for_user(result.attrs)
|
||||||
|
if err_msg is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
final_results.append(credentials)
|
||||||
|
|
||||||
|
logger.debug('For query %s found results %s', query, final_results)
|
||||||
|
return (final_results, 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. """
|
||||||
|
@ -153,7 +212,7 @@ class LDAPUsers(FederatedUsers):
|
||||||
if not password:
|
if not password:
|
||||||
return (None, 'Anonymous binding not allowed')
|
return (None, 'Anonymous binding not allowed')
|
||||||
|
|
||||||
(found_user, err_msg) = self._ldap_user_search(username_or_email)
|
(found_user, err_msg) = self._ldap_single_user_search(username_or_email)
|
||||||
if found_user is None:
|
if found_user is None:
|
||||||
return (None, err_msg)
|
return (None, err_msg)
|
||||||
|
|
||||||
|
@ -183,14 +242,4 @@ class LDAPUsers(FederatedUsers):
|
||||||
logger.exception('Invalid LDAP credentials')
|
logger.exception('Invalid LDAP credentials')
|
||||||
return (None, 'Invalid password')
|
return (None, 'Invalid password')
|
||||||
|
|
||||||
# Now check if we have a federated login for this user
|
return self._credential_for_user(found_response)
|
||||||
if not found_response.get(self._uid_attr):
|
|
||||||
return (None, 'Missing uid field "%s" in user record' % self._uid_attr)
|
|
||||||
|
|
||||||
if not found_response.get(self._email_attr):
|
|
||||||
return (None, 'Missing mail field "%s" in user record' % self._email_attr)
|
|
||||||
|
|
||||||
username = found_response[self._uid_attr][0].decode('utf-8')
|
|
||||||
email = found_response[self._email_attr][0]
|
|
||||||
return (VerifiedCredentials(username=username, email=email), None)
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from util.validation import generate_valid_usernames
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
VerifiedCredentials = namedtuple('VerifiedCredentials', ['username', 'email'])
|
UserInformation = namedtuple('UserInformation', ['username', 'email', 'id'])
|
||||||
|
|
||||||
class FederatedUsers(object):
|
class FederatedUsers(object):
|
||||||
""" Base class for all federated users systems. """
|
""" Base class for all federated users systems. """
|
||||||
|
@ -15,11 +15,26 @@ class FederatedUsers(object):
|
||||||
def __init__(self, federated_service):
|
def __init__(self, federated_service):
|
||||||
self._federated_service = federated_service
|
self._federated_service = federated_service
|
||||||
|
|
||||||
|
@property
|
||||||
|
def federated_service(self):
|
||||||
|
return self._federated_service
|
||||||
|
|
||||||
|
def get_user(self, username_or_email):
|
||||||
|
""" Retrieves the user with the given username or email, returning a tuple containing
|
||||||
|
a UserInformation (if success) and the error message (on failure).
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def verify_credentials(self, username_or_email, password):
|
def verify_credentials(self, username_or_email, password):
|
||||||
""" Verifies the given credentials against the backing federated service, returning
|
""" Verifies the given credentials against the backing federated service, returning
|
||||||
a tuple containing a VerifiedCredentials (if success) and the error message (if failed). """
|
a tuple containing a UserInformation (on success) and the error message (on failure).
|
||||||
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def query_users(self, query, limit=20):
|
||||||
|
""" If implemented, get_user must be implemented as well. """
|
||||||
|
return (None, 'Not supported')
|
||||||
|
|
||||||
def _get_federated_user(self, username, email):
|
def _get_federated_user(self, username, email):
|
||||||
db_user = model.user.verify_federated_login(self._federated_service, username)
|
db_user = model.user.verify_federated_login(self._federated_service, username)
|
||||||
if not db_user:
|
if not db_user:
|
||||||
|
@ -34,7 +49,8 @@ class FederatedUsers(object):
|
||||||
return (None, 'Unable to pick a username. Please report this to your administrator.')
|
return (None, 'Unable to pick a username. Please report this to your administrator.')
|
||||||
|
|
||||||
db_user = model.user.create_federated_user(valid_username, email, self._federated_service,
|
db_user = model.user.create_federated_user(valid_username, email, self._federated_service,
|
||||||
username, set_password_notification=False)
|
username,
|
||||||
|
set_password_notification=False)
|
||||||
else:
|
else:
|
||||||
# Update the db attributes from the federated service.
|
# Update the db attributes from the federated service.
|
||||||
db_user.email = email
|
db_user.email = email
|
||||||
|
@ -42,6 +58,13 @@ class FederatedUsers(object):
|
||||||
|
|
||||||
return (db_user, None)
|
return (db_user, None)
|
||||||
|
|
||||||
|
def link_user(self, username_or_email):
|
||||||
|
(credentials, err_msg) = self.get_user(username_or_email)
|
||||||
|
if credentials is None:
|
||||||
|
return (None, err_msg)
|
||||||
|
|
||||||
|
return self._get_federated_user(credentials.username, credentials.email)
|
||||||
|
|
||||||
def verify_and_link_user(self, username_or_email, password):
|
def verify_and_link_user(self, username_or_email, password):
|
||||||
""" Verifies the given credentials and, if valid, creates/links a database user to the
|
""" Verifies the given credentials and, if valid, creates/links a database user to the
|
||||||
associated federated service.
|
associated federated service.
|
||||||
|
|
|
@ -1,19 +1,34 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import itertools
|
||||||
|
|
||||||
from keystoneclient.v2_0 import client as kclient
|
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 AuthorizationFailure as KeystoneAuthorizationFailure
|
||||||
from keystoneclient.exceptions import Unauthorized as KeystoneUnauthorized
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = 10 # seconds
|
DEFAULT_TIMEOUT = 10 # seconds
|
||||||
|
|
||||||
class KeystoneUsers(FederatedUsers):
|
def _take(n, iterable):
|
||||||
""" Delegates authentication to OpenStack Keystone. """
|
"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):
|
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.auth_url = auth_url
|
||||||
self.admin_username = admin_username
|
self.admin_username = admin_username
|
||||||
self.admin_password = admin_password
|
self.admin_password = admin_password
|
||||||
|
@ -43,4 +58,75 @@ class KeystoneUsers(FederatedUsers):
|
||||||
logger.exception('Keystone unauthorized admin')
|
logger.exception('Keystone unauthorized admin')
|
||||||
return (None, 'Keystone admin credentials are invalid: %s' % kut.message)
|
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')
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
""" Conduct searches against all registry context. """
|
""" Conduct searches against all registry context. """
|
||||||
|
|
||||||
from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource,
|
from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource,
|
||||||
require_scope, path_param)
|
require_scope, path_param, internal_only, Unauthorized, InvalidRequest,
|
||||||
|
show_if)
|
||||||
from data import model
|
from data import model
|
||||||
from auth.permissions import (OrganizationMemberPermission, ReadRepositoryPermission,
|
from auth.permissions import (OrganizationMemberPermission, ReadRepositoryPermission,
|
||||||
UserAdminPermission, AdministerOrganizationPermission,
|
UserAdminPermission, AdministerOrganizationPermission,
|
||||||
ReadRepositoryPermission)
|
ReadRepositoryPermission)
|
||||||
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
|
from app import avatar, authentication
|
||||||
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
|
||||||
|
@ -16,6 +17,32 @@ 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>')
|
||||||
|
@internal_only
|
||||||
|
class LinkExternalEntity(ApiResource):
|
||||||
|
""" Resource for linking external entities to internal users. """
|
||||||
|
@nickname('linkExternalUser')
|
||||||
|
def post(self, username):
|
||||||
|
# Only allowed if there is a logged in user.
|
||||||
|
if not get_authenticated_user():
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
# Try to link the user with the given *external* username, to an internal record.
|
||||||
|
(user, err_msg) = authentication.link_user(username)
|
||||||
|
if user is None:
|
||||||
|
raise InvalidRequest(err_msg, payload={'username': username})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'entity': {
|
||||||
|
'name': user.username,
|
||||||
|
'kind': 'user',
|
||||||
|
'is_robot': False,
|
||||||
|
'avatar': avatar.get_data_for_user(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/entities/<prefix>')
|
@resource('/v1/entities/<prefix>')
|
||||||
class EntitySearch(ApiResource):
|
class EntitySearch(ApiResource):
|
||||||
""" Resource for searching entities. """
|
""" Resource for searching entities. """
|
||||||
|
@ -69,7 +96,22 @@ class EntitySearch(ApiResource):
|
||||||
if admin_permission.can():
|
if admin_permission.can():
|
||||||
robot_namespace = namespace_name
|
robot_namespace = namespace_name
|
||||||
|
|
||||||
users = model.user.get_matching_users(prefix, robot_namespace, organization)
|
# Lookup users in the database for the prefix query.
|
||||||
|
users = model.user.get_matching_users(prefix, robot_namespace, organization, limit=10)
|
||||||
|
|
||||||
|
# Lookup users via the user system for the prefix query. We'll filter out any users that
|
||||||
|
# already exist in the database.
|
||||||
|
external_users, federated_id, _ = authentication.query_users(prefix, limit=10)
|
||||||
|
filtered_external_users = []
|
||||||
|
if external_users and federated_id is not None:
|
||||||
|
users = list(users)
|
||||||
|
user_ids = [user.id for user in users]
|
||||||
|
|
||||||
|
# Filter the users if any are already found via the database. We do so by looking up all
|
||||||
|
# the found users in the federated user system.
|
||||||
|
federated_query = model.user.get_federated_logins(user_ids, federated_id)
|
||||||
|
found = {result.service_ident for result in federated_query}
|
||||||
|
filtered_external_users = [user for user in external_users if not user.username in found]
|
||||||
|
|
||||||
def entity_team_view(team):
|
def entity_team_view(team):
|
||||||
result = {
|
result = {
|
||||||
|
@ -93,11 +135,20 @@ class EntitySearch(ApiResource):
|
||||||
|
|
||||||
return user_json
|
return user_json
|
||||||
|
|
||||||
|
def external_view(user):
|
||||||
|
result = {
|
||||||
|
'name': user.username,
|
||||||
|
'kind': 'external',
|
||||||
|
'avatar': avatar.get_data_for_external_user(user)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
team_data = [entity_team_view(team) for team in teams]
|
team_data = [entity_team_view(team) for team in teams]
|
||||||
user_data = [user_view(user) for user in users]
|
user_data = [user_view(user) for user in users]
|
||||||
|
external_data = [external_view(user) for user in filtered_external_users]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'results': team_data + user_data + org_data
|
'results': team_data + user_data + org_data + external_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -523,6 +523,15 @@
|
||||||
|
|
||||||
<!-- Keystone Authentication -->
|
<!-- Keystone Authentication -->
|
||||||
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'Keystone'">
|
<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>
|
<tr>
|
||||||
<td>Keystone Authentication URL:</td>
|
<td>Keystone Authentication URL:</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -573,20 +582,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'JWT'">
|
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'JWT'">
|
||||||
<tr>
|
|
||||||
<td>User Verification Endpoint:</td>
|
|
||||||
<td>
|
|
||||||
<span class="config-string-field" binding="config.JWT_VERIFY_ENDPOINT"
|
|
||||||
pattern="http(s)?://.+"></span>
|
|
||||||
<div class="help-text">
|
|
||||||
The URL (starting with http or https) on the JWT authentication server for verifying username and password credentials.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="help-text" style="margin-top: 6px;">
|
|
||||||
Credentials will be sent in the <code>Authorization</code> header as Basic Auth, and this endpoint should return <code>200 OK</code> on success (or a <code>4**</code> otherwise).
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Authentication Issuer:</td>
|
<td>Authentication Issuer:</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -606,6 +601,50 @@
|
||||||
</div
|
</div
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>User Verification Endpoint:</td>
|
||||||
|
<td>
|
||||||
|
<span class="config-string-field" binding="config.JWT_VERIFY_ENDPOINT"
|
||||||
|
pattern="http(s)?://.+"></span>
|
||||||
|
<div class="help-text">
|
||||||
|
The URL (starting with http or https) on the JWT authentication server for verifying username and password credentials.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help-text" style="margin-top: 6px;">
|
||||||
|
Credentials will be sent in the <code>Authorization</code> header as Basic Auth, and this endpoint should return <code>200 OK</code> on success (or a <code>4**</code> otherwise).
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>User Query Endpoint:</td>
|
||||||
|
<td>
|
||||||
|
<span class="config-string-field" binding="config.JWT_QUERY_ENDPOINT"
|
||||||
|
pattern="http(s)?://.+" is-optional="true"></span>
|
||||||
|
<div class="help-text">
|
||||||
|
The URL (starting with http or https) on the JWT authentication server for looking up
|
||||||
|
users based on a prefix query. This is optional.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help-text" style="margin-top: 6px;">
|
||||||
|
The prefix query will be sent as a query parameter with name <code>query</code>.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>User Lookup Endpoint:</td>
|
||||||
|
<td>
|
||||||
|
<span class="config-string-field" binding="config.JWT_GETUSER_ENDPOINT"
|
||||||
|
pattern="http(s)?://.+" is-optional="true"></span>
|
||||||
|
<div class="help-text">
|
||||||
|
The URL (starting with http or https) on the JWT authentication server for looking up
|
||||||
|
a user by username or email address.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help-text" style="margin-top: 6px;">
|
||||||
|
The username or email address will be sent as a query parameter with name <code>username</code>.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- LDAP Authentication -->
|
<!-- LDAP Authentication -->
|
||||||
|
|
|
@ -148,6 +148,18 @@ angular.module('quay').directive('entitySearch', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.setEntityInternal = function(entity, updateTypeahead) {
|
$scope.setEntityInternal = function(entity, updateTypeahead) {
|
||||||
|
// If the entity is an external entity, convert it to a known user via an API call.
|
||||||
|
if (entity.kind == 'external') {
|
||||||
|
var params = {
|
||||||
|
'username': entity.name
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.linkExternalUser(null, params).then(function(resp) {
|
||||||
|
$scope.setEntityInternal(resp['entity'], updateTypeahead);
|
||||||
|
}, ApiService.errorDisplay('Could not link external user'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (updateTypeahead) {
|
if (updateTypeahead) {
|
||||||
$(input).typeahead('val', $scope.autoClear ? '' : entity.name);
|
$(input).typeahead('val', $scope.autoClear ? '' : entity.name);
|
||||||
} else {
|
} else {
|
||||||
|
@ -193,7 +205,7 @@ angular.module('quay').directive('entitySearch', function () {
|
||||||
var entity = data.results[i];
|
var entity = data.results[i];
|
||||||
|
|
||||||
var found = 'user';
|
var found = 'user';
|
||||||
if (entity.kind == 'user') {
|
if (entity.kind == 'user' || entity.kind == 'external') {
|
||||||
found = entity.is_robot ? 'robot' : 'user';
|
found = entity.is_robot ? 'robot' : 'user';
|
||||||
} else if (entity.kind == 'team') {
|
} else if (entity.kind == 'team') {
|
||||||
found = 'team';
|
found = 'team';
|
||||||
|
@ -276,6 +288,8 @@ angular.module('quay').directive('entitySearch', function () {
|
||||||
template = '<div class="entity-mini-listing">';
|
template = '<div class="entity-mini-listing">';
|
||||||
if (datum.entity.kind == 'user' && !datum.entity.is_robot) {
|
if (datum.entity.kind == 'user' && !datum.entity.is_robot) {
|
||||||
template += '<i class="fa fa-user fa-lg"></i>';
|
template += '<i class="fa fa-user fa-lg"></i>';
|
||||||
|
} else if (datum.entity.kind == 'external') {
|
||||||
|
template += '<i class="fa fa-user fa-lg"></i>';
|
||||||
} else if (datum.entity.kind == 'user' && datum.entity.is_robot) {
|
} else if (datum.entity.kind == 'user' && datum.entity.is_robot) {
|
||||||
template += '<i class="fa ci-robot fa-lg"></i>';
|
template += '<i class="fa ci-robot fa-lg"></i>';
|
||||||
} else if (datum.entity.kind == 'team') {
|
} else if (datum.entity.kind == 'team') {
|
||||||
|
|
Binary file not shown.
|
@ -53,6 +53,58 @@ class JWTAuthTestCase(LiveServerTestCase):
|
||||||
data = base64.b64decode(request.headers['Authorization'][len('Basic '):])
|
data = base64.b64decode(request.headers['Authorization'][len('Basic '):])
|
||||||
return data.split(':', 1)
|
return data.split(':', 1)
|
||||||
|
|
||||||
|
@jwt_app.route('/user/query', methods=['GET'])
|
||||||
|
def query_users():
|
||||||
|
query = request.args.get('query')
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
if user['name'].startswith(query):
|
||||||
|
results.append({
|
||||||
|
'username': user['name'],
|
||||||
|
'email': user['email'],
|
||||||
|
})
|
||||||
|
|
||||||
|
token_data = {
|
||||||
|
'iss': 'authy',
|
||||||
|
'aud': 'quay.io/jwtauthn/query',
|
||||||
|
'nbf': datetime.utcnow(),
|
||||||
|
'iat': datetime.utcnow(),
|
||||||
|
'exp': datetime.utcnow() + timedelta(seconds=60),
|
||||||
|
'results': results,
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded = jwt.encode(token_data, private_key, 'RS256')
|
||||||
|
return jsonify({
|
||||||
|
'token': encoded
|
||||||
|
})
|
||||||
|
|
||||||
|
@jwt_app.route('/user/get', methods=['GET'])
|
||||||
|
def get_user():
|
||||||
|
username = request.args.get('username')
|
||||||
|
|
||||||
|
if username == 'disabled':
|
||||||
|
return make_response('User is currently disabled', 401)
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
if user['name'] == username or user['email'] == username:
|
||||||
|
token_data = {
|
||||||
|
'iss': 'authy',
|
||||||
|
'aud': 'quay.io/jwtauthn/getuser',
|
||||||
|
'nbf': datetime.utcnow(),
|
||||||
|
'iat': datetime.utcnow(),
|
||||||
|
'exp': datetime.utcnow() + timedelta(seconds=60),
|
||||||
|
'sub': user['name'],
|
||||||
|
'email': user['email']
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded = jwt.encode(token_data, private_key, 'RS256')
|
||||||
|
return jsonify({
|
||||||
|
'token': encoded
|
||||||
|
})
|
||||||
|
|
||||||
|
return make_response('Invalid username or password', 404)
|
||||||
|
|
||||||
@jwt_app.route('/user/verify', methods=['GET'])
|
@jwt_app.route('/user/verify', methods=['GET'])
|
||||||
def verify_user():
|
def verify_user():
|
||||||
username, password = _get_basic_auth()
|
username, password = _get_basic_auth()
|
||||||
|
@ -80,7 +132,7 @@ class JWTAuthTestCase(LiveServerTestCase):
|
||||||
'token': encoded
|
'token': encoded
|
||||||
})
|
})
|
||||||
|
|
||||||
return make_response('', 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
|
||||||
|
@ -94,7 +146,11 @@ class JWTAuthTestCase(LiveServerTestCase):
|
||||||
|
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
|
|
||||||
self.jwt_auth = ExternalJWTAuthN(self.get_server_url() + '/user/verify', 'authy', '',
|
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)
|
app.config['HTTPCLIENT'], 300, JWTAuthTestCase.public_key.name)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
@ -142,11 +198,78 @@ class JWTAuthTestCase(LiveServerTestCase):
|
||||||
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_erorr(self):
|
def test_disabled_user_custom_error(self):
|
||||||
result, error_message = self.jwt_auth.verify_and_link_user('disabled', 'password')
|
result, error_message = self.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):
|
||||||
|
# Lookup `cool`.
|
||||||
|
results, identifier, error_message = self.jwt_auth.query_users('cool')
|
||||||
|
self.assertIsNone(error_message)
|
||||||
|
self.assertEquals('jwtauthn', identifier)
|
||||||
|
self.assertEquals(1, len(results))
|
||||||
|
|
||||||
|
self.assertEquals('cooluser', results[0].username)
|
||||||
|
self.assertEquals('user@domain.com', results[0].email)
|
||||||
|
|
||||||
|
# Lookup `some`.
|
||||||
|
results, identifier, error_message = self.jwt_auth.query_users('some')
|
||||||
|
self.assertIsNone(error_message)
|
||||||
|
self.assertEquals('jwtauthn', identifier)
|
||||||
|
self.assertEquals(1, len(results))
|
||||||
|
|
||||||
|
self.assertEquals('some.neat.user', results[0].username)
|
||||||
|
self.assertEquals('neat@domain.com', results[0].email)
|
||||||
|
|
||||||
|
# Lookup `unknown`.
|
||||||
|
results, identifier, error_message = self.jwt_auth.query_users('unknown')
|
||||||
|
self.assertIsNone(error_message)
|
||||||
|
self.assertEquals('jwtauthn', identifier)
|
||||||
|
self.assertEquals(0, len(results))
|
||||||
|
|
||||||
|
def test_get_user(self):
|
||||||
|
# Lookup cooluser.
|
||||||
|
result, error_message = self.jwt_auth.get_user('cooluser')
|
||||||
|
self.assertIsNone(error_message)
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
|
||||||
|
self.assertEquals('cooluser', result.username)
|
||||||
|
self.assertEquals('user@domain.com', result.email)
|
||||||
|
|
||||||
|
# Lookup some.neat.user.
|
||||||
|
result, error_message = self.jwt_auth.get_user('some.neat.user')
|
||||||
|
self.assertIsNone(error_message)
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
|
||||||
|
self.assertEquals('some.neat.user', result.username)
|
||||||
|
self.assertEquals('neat@domain.com', result.email)
|
||||||
|
|
||||||
|
# Lookup unknown user.
|
||||||
|
result, error_message = self.jwt_auth.get_user('unknownuser')
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_link_user(self):
|
||||||
|
# Link cooluser.
|
||||||
|
user, error_message = self.jwt_auth.link_user('cooluser')
|
||||||
|
self.assertIsNone(error_message)
|
||||||
|
self.assertIsNotNone(user)
|
||||||
|
self.assertEquals('cooluser', user.username)
|
||||||
|
|
||||||
|
# Link again. Should return the same user record.
|
||||||
|
user_again, _ = self.jwt_auth.link_user('cooluser')
|
||||||
|
self.assertEquals(user_again.id, user.id)
|
||||||
|
|
||||||
|
# Confirm cooluser.
|
||||||
|
result, _ = self.jwt_auth.confirm_existing_user('cooluser', 'password')
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
self.assertEquals('cooluser', result.username)
|
||||||
|
|
||||||
|
def test_link_invalid_user(self):
|
||||||
|
user, error_message = self.jwt_auth.link_user('invaliduser')
|
||||||
|
self.assertIsNotNone(error_message)
|
||||||
|
self.assertIsNone(user)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -4,16 +4,15 @@ import unittest
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from flask import Flask, request, abort
|
from flask import Flask, request, abort, make_response
|
||||||
from flask_testing import LiveServerTestCase
|
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
|
_PORT_NUMBER = 5001
|
||||||
|
|
||||||
|
class KeystoneAuthTestsMixin():
|
||||||
class KeystoneAuthTests(LiveServerTestCase):
|
|
||||||
maxDiff = None
|
maxDiff = None
|
||||||
|
|
||||||
def create_app(self):
|
def create_app(self):
|
||||||
|
@ -44,6 +43,106 @@ class KeystoneAuthTests(LiveServerTestCase):
|
||||||
|
|
||||||
abort(404)
|
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'])
|
@ks_app.route('/v2.0/auth/tokens', methods=['POST'])
|
||||||
def tokens():
|
def tokens():
|
||||||
|
@ -89,9 +188,15 @@ class KeystoneAuthTests(LiveServerTestCase):
|
||||||
return ks_app
|
return ks_app
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
setup_database_for_testing(self)
|
||||||
self.session = requests.Session()
|
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):
|
def test_invalid_user(self):
|
||||||
(user, _) = self.keystone.verify_credentials('unknownuser', 'password')
|
(user, _) = self.keystone.verify_credentials('unknownuser', 'password')
|
||||||
|
@ -111,6 +216,48 @@ class KeystoneAuthTests(LiveServerTestCase):
|
||||||
self.assertEquals(user.username, 'some.neat.user')
|
self.assertEquals(user.username, 'some.neat.user')
|
||||||
self.assertEquals(user.email, 'some.neat.user@example.com')
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -4,6 +4,7 @@ from app import app
|
||||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||||
from data.users import LDAPUsers
|
from data.users import LDAPUsers
|
||||||
from mockldap import MockLdap
|
from mockldap import MockLdap
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
class TestLDAP(unittest.TestCase):
|
class TestLDAP(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -198,6 +199,60 @@ class TestLDAP(unittest.TestCase):
|
||||||
(response, _) = self.ldap.confirm_existing_user('someuser', 'somepass')
|
(response, _) = self.ldap.confirm_existing_user('someuser', 'somepass')
|
||||||
self.assertEquals(response.username, 'someuser')
|
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)
|
||||||
|
|
||||||
|
def test_query(self):
|
||||||
|
def initializer(uri, trace_level=0):
|
||||||
|
obj = self.mockldap[uri]
|
||||||
|
|
||||||
|
# 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*))')([
|
||||||
|
('uid=cool.user,ou=employees,dc=quay,dc=io', {
|
||||||
|
'dc': ['quay', 'io'],
|
||||||
|
'ou': 'employees',
|
||||||
|
'uid': ['cool.user', 'referred'],
|
||||||
|
'userPassword': ['somepass'],
|
||||||
|
'mail': ['foo@bar.com']
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
obj.search_s.seed('ou=otheremployees,dc=quay,dc=io', 2, '(|(uid=cool*)(mail=cool*))')([])
|
||||||
|
|
||||||
|
obj.search_s.seed('ou=employees,dc=quay,dc=io', 2, '(|(uid=unknown*)(mail=unknown*))')([])
|
||||||
|
obj.search_s.seed('ou=otheremployees,dc=quay,dc=io', 2,
|
||||||
|
'(|(uid=unknown*)(mail=unknown*))')([])
|
||||||
|
return obj
|
||||||
|
|
||||||
|
with patch('ldap.initialize', new=initializer):
|
||||||
|
# Lookup cool.
|
||||||
|
(response, error_message) = self.ldap.query_users('cool')
|
||||||
|
self.assertIsNone(error_message)
|
||||||
|
self.assertEquals(1, len(response))
|
||||||
|
|
||||||
|
user_info = response[0]
|
||||||
|
self.assertEquals("cool.user", user_info.username)
|
||||||
|
self.assertEquals("foo@bar.com", user_info.email)
|
||||||
|
|
||||||
|
# Lookup unknown.
|
||||||
|
(response, error_message) = self.ldap.query_users('unknown')
|
||||||
|
self.assertIsNone(error_message)
|
||||||
|
self.assertEquals(0, len(response))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ from data.database import validate_database_url
|
||||||
from data.users import LDAP_CERT_FILENAME
|
from data.users import LDAP_CERT_FILENAME
|
||||||
from data.users.externaljwt import ExternalJWTAuthN
|
from data.users.externaljwt import ExternalJWTAuthN
|
||||||
from data.users.externalldap import LDAPConnection, LDAPUsers
|
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 storage import get_storage_driver
|
||||||
from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig
|
from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig
|
||||||
from util.secscan.api import SecurityScannerAPI
|
from util.secscan.api import SecurityScannerAPI
|
||||||
|
@ -372,6 +372,9 @@ def _validate_jwt(config, password):
|
||||||
return
|
return
|
||||||
|
|
||||||
verify_endpoint = config.get('JWT_VERIFY_ENDPOINT')
|
verify_endpoint = config.get('JWT_VERIFY_ENDPOINT')
|
||||||
|
query_endpoint = config.get('JWT_QUERY_ENDPOINT', None)
|
||||||
|
getuser_endpoint = config.get('JWT_GETUSER_ENDPOINT', None)
|
||||||
|
|
||||||
issuer = config.get('JWT_AUTH_ISSUER')
|
issuer = config.get('JWT_AUTH_ISSUER')
|
||||||
|
|
||||||
if not verify_endpoint:
|
if not verify_endpoint:
|
||||||
|
@ -382,7 +385,8 @@ def _validate_jwt(config, password):
|
||||||
|
|
||||||
# Try to instatiate the JWT authentication mechanism. This will raise an exception if
|
# Try to instatiate the JWT authentication mechanism. This will raise an exception if
|
||||||
# the key cannot be found.
|
# the key cannot be found.
|
||||||
users = ExternalJWTAuthN(verify_endpoint, issuer, OVERRIDE_CONFIG_DIRECTORY,
|
users = ExternalJWTAuthN(verify_endpoint, query_endpoint, getuser_endpoint, issuer,
|
||||||
|
OVERRIDE_CONFIG_DIRECTORY,
|
||||||
app.config['HTTPCLIENT'],
|
app.config['HTTPCLIENT'],
|
||||||
app.config.get('JWT_AUTH_MAX_FRESH_S', 300))
|
app.config.get('JWT_AUTH_MAX_FRESH_S', 300))
|
||||||
|
|
||||||
|
@ -392,7 +396,24 @@ def _validate_jwt(config, password):
|
||||||
if not result:
|
if not result:
|
||||||
raise Exception(('Verification of superuser %s failed: %s. \n\nThe user either does not ' +
|
raise Exception(('Verification of superuser %s failed: %s. \n\nThe user either does not ' +
|
||||||
'exist in the remote authentication system ' +
|
'exist in the remote authentication system ' +
|
||||||
'OR JWT auth is misconfigured.') % (username, err_msg))
|
'OR JWT auth is misconfigured') % (username, err_msg))
|
||||||
|
|
||||||
|
# If the query endpoint exists, ensure we can query to find the current user and that we can
|
||||||
|
# look up users directly.
|
||||||
|
if query_endpoint:
|
||||||
|
(results, err_msg) = users.query_users(username)
|
||||||
|
if not results:
|
||||||
|
err_msg = err_msg or ('Could not find users matching query: %s' % username)
|
||||||
|
raise Exception('Query endpoint is misconfigured or not returning proper users: %s' % err_msg)
|
||||||
|
|
||||||
|
# Make sure the get user endpoint is also configured.
|
||||||
|
if not getuser_endpoint:
|
||||||
|
raise Exception('The lookup user endpoint must be configured if the query endpoint is set')
|
||||||
|
|
||||||
|
(result, err_msg) = users.get_user(username)
|
||||||
|
if not result:
|
||||||
|
err_msg = err_msg or ('Could not find user %s' % username)
|
||||||
|
raise Exception('Lookup endpoint is misconfigured or not returning properly: %s' % err_msg)
|
||||||
|
|
||||||
|
|
||||||
def _validate_keystone(config, password):
|
def _validate_keystone(config, password):
|
||||||
|
@ -401,6 +422,7 @@ def _validate_keystone(config, password):
|
||||||
return
|
return
|
||||||
|
|
||||||
auth_url = config.get('KEYSTONE_AUTH_URL')
|
auth_url = config.get('KEYSTONE_AUTH_URL')
|
||||||
|
auth_version = int(config.get('KEYSTONE_AUTH_VERSION', 2))
|
||||||
admin_username = config.get('KEYSTONE_ADMIN_USERNAME')
|
admin_username = config.get('KEYSTONE_ADMIN_USERNAME')
|
||||||
admin_password = config.get('KEYSTONE_ADMIN_PASSWORD')
|
admin_password = config.get('KEYSTONE_ADMIN_PASSWORD')
|
||||||
admin_tenant = config.get('KEYSTONE_ADMIN_TENANT')
|
admin_tenant = config.get('KEYSTONE_ADMIN_TENANT')
|
||||||
|
@ -417,7 +439,7 @@ def _validate_keystone(config, password):
|
||||||
if not admin_tenant:
|
if not admin_tenant:
|
||||||
raise Exception('Missing 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.
|
# Verify that the superuser exists. If not, raise an exception.
|
||||||
username = get_authenticated_user().username
|
username = get_authenticated_user().username
|
||||||
|
|
Reference in a new issue