Fix config validator for storage and add a test suite

Note that the test suite doesn't fully verify that each validation succeeds; rather, it ensures that the proper system (storage, security scanning, etc) is called with the configuration and returns at all (usually with an expected error). This should prevent us from forgetting to update these code paths when we change config-based systems. Longer term, we might want to have these tests stand up fake/mock versions of the endpoint services as well, for end-to-end testing.
This commit is contained in:
Joseph Schorr 2016-11-29 15:20:46 -05:00
parent b7aac159ae
commit 236655adb4
2 changed files with 445 additions and 97 deletions

View file

@ -0,0 +1,331 @@
import unittest
import redis
import moto
import json
from httmock import urlmatch, HTTMock
from initdb import setup_database_for_testing, finished_database_for_testing
from util.config.validator import VALIDATORS, ConfigValidationException
from util.morecollections import AttrDict
from app import app
class TestValidateConfig(unittest.TestCase):
validated = set([])
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 validate(self, service, config, user=None, password=None):
self.validated.add(service)
config['TESTING'] = True
VALIDATORS[service](config, user, password)
def test_validate_redis(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Missing redis hostname'):
self.validate('redis', {})
with self.assertRaises(redis.ConnectionError):
self.validate('redis', {
'BUILDLOGS_REDIS': {
'host': 'somehost',
},
})
def test_validate_mail(self):
# Skip mail.
self.validated.add('mail')
def test_validate_database(self):
with self.assertRaisesRegexp(Exception, 'database not properly initialized'):
self.validate('database', {
'DB_URI': 'mysql://somehost',
})
def test_validate_jwt(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Missing JWT Verification endpoint'):
self.validate('jwt', {
'AUTHENTICATION_TYPE': 'JWT',
})
with self.assertRaisesRegexp(ConfigValidationException, 'Missing JWT Issuer ID'):
self.validate('jwt', {
'AUTHENTICATION_TYPE': 'JWT',
'JWT_VERIFY_ENDPOINT': 'somehost',
})
with self.assertRaisesRegexp(Exception, 'JWT Authentication public key file'):
self.validate('jwt', {
'AUTHENTICATION_TYPE': 'JWT',
'JWT_VERIFY_ENDPOINT': 'somehost',
'JWT_AUTH_ISSUER': 'someissuer',
})
# TODO(jschorr): Add another test once we switch JWT auth to use the config provider to
# find the file
def test_validate_registry_storage(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Storage configuration required'):
self.validate('registry-storage', {})
with self.assertRaisesRegexp(ConfigValidationException, 'Locally mounted directory not'):
self.validate('registry-storage', {
'FEATURE_STORAGE_REPLICATION': True,
'DISTRIBUTED_STORAGE_CONFIG': {
'default': ('LocalStorage', {
'storage_path': '',
}),
}
})
with self.assertRaisesRegexp(ConfigValidationException, 'No such file or directory'):
self.validate('registry-storage', {
'DISTRIBUTED_STORAGE_CONFIG': {
'default': ('LocalStorage', {
'storage_path': '',
}),
}
})
with self.assertRaisesRegexp(ConfigValidationException, 'not under a mounted volume'):
self.validate('registry-storage', {
'DISTRIBUTED_STORAGE_CONFIG': {
'default': ('LocalStorage', {
'storage_path': '/tmp/somepath',
}),
}
})
with moto.mock_s3():
with self.assertRaisesRegexp(ConfigValidationException, 'S3ResponseError: 404 Not Found'):
self.validate('registry-storage', {
'DISTRIBUTED_STORAGE_CONFIG': {
'default': ('S3Storage', {
's3_access_key': 'invalid',
's3_secret_key': 'invalid',
's3_bucket': 'somebucket',
'storage_path': ''
}),
}
})
def test_validate_bittorrent(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Missing announce URL'):
self.validate('bittorrent', {})
announcer_hit = [False]
@urlmatch(netloc=r'somehost', path='/announce')
def handler(url, request):
announcer_hit[0] = True
return {'status_code': 200, 'content': ''}
with HTTMock(handler):
self.validate('bittorrent', {
'BITTORRENT_ANNOUNCE_URL': 'http://somehost/announce',
})
self.assertTrue(announcer_hit[0])
def test_validate_ssl(self):
self.validate('ssl', {
'PREFERRED_URL_SCHEME': 'http',
})
self.validate('ssl', {
'PREFERRED_URL_SCHEME': 'https',
'EXTERNAL_TLS_TERMINATION': True,
})
with self.assertRaisesRegexp(ConfigValidationException, 'Missing required SSL file'):
self.validate('ssl', {
'PREFERRED_URL_SCHEME': 'https',
})
# TODO(jschorr): Add SSL verification tests once file lookup is fixed.
def test_validate_keystone(self):
with self.assertRaisesRegexp(ConfigValidationException,
'Verification of superuser someuser failed'):
self.validate('keystone', {
'AUTHENTICATION_TYPE': 'Keystone',
'KEYSTONE_AUTH_URL': 'somehost',
'KEYSTONE_AUTH_VERSION': 2,
'KEYSTONE_ADMIN_USERNAME': 'someusername',
'KEYSTONE_ADMIN_PASSWORD': 'somepassword',
'KEYSTONE_ADMIN_TENANT': 'sometenant',
}, user=AttrDict(dict(username='someuser')))
def test_validate_ldap(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Missing Admin DN for LDAP'):
self.validate('ldap', {
'AUTHENTICATION_TYPE': 'LDAP',
})
with self.assertRaisesRegexp(ConfigValidationException, 'Missing Admin Password for LDAP'):
self.validate('ldap', {
'AUTHENTICATION_TYPE': 'LDAP',
'LDAP_ADMIN_DN': 'somedn',
})
with self.assertRaisesRegexp(ConfigValidationException, 'Can\'t contact LDAP server'):
self.validate('ldap', {
'AUTHENTICATION_TYPE': 'LDAP',
'LDAP_ADMIN_DN': 'somedn',
'LDAP_ADMIN_PASSWD': 'somepass',
'LDAP_URI': 'ldap://localhost',
})
def test_validate_signer(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Unknown signing engine'):
self.validate('signer', {
'SIGNING_ENGINE': 'foobar',
})
def test_validate_security_scanner(self):
url_hit = [False]
@urlmatch(netloc=r'somehost')
def handler(url, request):
url_hit[0] = True
return {'status_code': 200, 'content': ''}
with HTTMock(handler):
self.validate('security-scanner', {
'DISTRIBUTED_STORAGE_PREFERENCE': ['local'],
'DISTRIBUTED_STORAGE_CONFIG': {
'default': ('LocalStorage', {
'storage_path': '',
}),
},
'SECURITY_SCANNER_ENDPOINT': 'http://somehost',
})
def test_validate_github_trigger(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Missing GitHub client id'):
self.validate('github-trigger', {})
url_hit = [False]
@urlmatch(netloc=r'somehost')
def handler(url, request):
url_hit[0] = True
return {'status_code': 200, 'content': ''}
with HTTMock(handler):
with self.assertRaisesRegexp(Exception, 'Endpoint is not a Github'):
self.validate('github-trigger', {
'GITHUB_TRIGGER_CONFIG': {
'GITHUB_ENDPOINT': 'http://somehost',
'CLIENT_ID': 'foo',
'CLIENT_SECRET': 'bar',
},
})
self.assertTrue(url_hit[0])
def test_validate_github_login(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Missing GitHub client id'):
self.validate('github-login', {})
url_hit = [False]
@urlmatch(netloc=r'somehost')
def handler(url, request):
url_hit[0] = True
return {'status_code': 200, 'content': ''}
with HTTMock(handler):
with self.assertRaisesRegexp(Exception, 'Endpoint is not a Github'):
self.validate('github-login', {
'GITHUB_LOGIN_CONFIG': {
'GITHUB_ENDPOINT': 'http://somehost',
'CLIENT_ID': 'foo',
'CLIENT_SECRET': 'bar',
},
})
self.assertTrue(url_hit[0])
def test_validate_bitbucket_trigger(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Missing client ID and client secret'):
self.validate('bitbucket-trigger', {})
url_hit = [False]
@urlmatch(netloc=r'bitbucket.org')
def handler(url, request):
url_hit[0] = True
return {
'status_code': 200,
'content': 'oauth_token=foo&oauth_token_secret=bar',
}
with HTTMock(handler):
self.validate('bitbucket-trigger', {
'BITBUCKET_TRIGGER_CONFIG': {
'CONSUMER_KEY': 'foo',
'CONSUMER_SECRET': 'bar',
},
})
self.assertTrue(url_hit[0])
def test_validate_google_login(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Missing client ID and client secret'):
self.validate('google-login', {})
url_hit = [False]
@urlmatch(netloc=r'www.googleapis.com', path='/oauth2/v3/token')
def handler(url, request):
url_hit[0] = True
return {'status_code': 200, 'content': ''}
with HTTMock(handler):
self.validate('google-login', {
'GOOGLE_LOGIN_CONFIG': {
'CLIENT_ID': 'foo',
'CLIENT_SECRET': 'bar',
},
})
self.assertTrue(url_hit[0])
def test_validate_gitlab_trigger(self):
with self.assertRaisesRegexp(ConfigValidationException, 'Missing GitLab client id'):
self.validate('gitlab-trigger', {})
url_hit = [False]
@urlmatch(netloc=r'somegitlab', path='/oauth/token')
def handler(url, request):
url_hit[0] = True
return {'status_code': 200, 'content': '{}'}
with HTTMock(handler):
with self.assertRaisesRegexp(ConfigValidationException, "Invalid client id or client secret"):
self.validate('gitlab-trigger', {
'GITLAB_TRIGGER_CONFIG': {
'GITLAB_ENDPOINT': 'http://somegitlab',
'CLIENT_ID': 'foo',
'CLIENT_SECRET': 'bar',
},
})
self.assertTrue(url_hit[0])
@classmethod
def tearDownClass(cls):
not_run = set(VALIDATORS.keys()) - cls.validated
assert not not_run, not_run
if __name__ == '__main__':
unittest.main()

View file

@ -29,9 +29,12 @@ from util.secscan.api import SecurityScannerAPI
from util.registry.torrent import torrent_jwt
from util.security.signing import SIGNING_ENGINES
logger = logging.getLogger(__name__)
class ConfigValidationException(Exception):
""" Exception raised when the configuration fails to validate for a known reason. """
pass
# Note: Only add files required for HTTPS to the SSL_FILESNAMES list.
SSL_FILENAMES = ['ssl.cert', 'ssl.key']
@ -50,22 +53,22 @@ def get_storage_providers(config):
try:
for name, parameters in storage_config.items():
drivers[name] = (parameters[0], get_storage_driver(None, parameters))
except TypeError:
drivers[name] = (parameters[0], get_storage_driver(None, None, None, parameters))
except TypeError as te:
logger.exception('Missing required storage configuration provider')
raise Exception('Missing required storage configuration parameter(s): %s' % name)
raise ConfigValidationException('Missing required parameter(s) for storage %s' % name)
return drivers
def validate_service_for_config(service, config, password=None):
""" Attempts to validate the configuration for the given service. """
if not service in _VALIDATORS:
if not service in VALIDATORS:
return {
'status': False
}
try:
_VALIDATORS[service](config, password)
VALIDATORS[service](config, get_authenticated_user(), password)
return {
'status': True
}
@ -77,40 +80,40 @@ def validate_service_for_config(service, config, password=None):
}
def _validate_database(config, _):
def _validate_database(config, user_obj, _):
""" Validates connecting to the database. """
try:
validate_database_url(config['DB_URI'], config.get('DB_CONNECTION_ARGS', {}))
except peewee.OperationalError as ex:
if ex.args and len(ex.args) > 1:
raise Exception(ex.args[1])
raise ConfigValidationException(ex.args[1])
else:
raise ex
def _validate_redis(config, _):
def _validate_redis(config, user_obj, _):
""" Validates connecting to redis. """
redis_config = config.get('BUILDLOGS_REDIS', {})
if not 'host' in redis_config:
raise Exception('Missing redis hostname')
raise ConfigValidationException('Missing redis hostname')
client = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
client.ping()
def _validate_registry_storage(config, _):
def _validate_registry_storage(config, user_obj, _):
""" Validates registry storage. """
replication_enabled = config.get('FEATURE_STORAGE_REPLICATION', False)
providers = get_storage_providers(config).items()
if not providers:
raise Exception('Storage configuration required')
raise ConfigValidationException('Storage configuration required')
for name, (storage_type, driver) in providers:
try:
if replication_enabled and storage_type == 'LocalStorage':
raise Exception('Locally mounted directory not supported with storage replication')
raise ConfigValidationException('Locally mounted directory not supported ' +
'with storage replication')
# Run validation on the driver.
driver.validate(app.config['HTTPCLIENT'])
@ -118,10 +121,10 @@ def _validate_registry_storage(config, _):
# Run setup on the driver if the read/write succeeded.
driver.setup()
except Exception as ex:
raise Exception('Invalid storage configuration: %s: %s' % (name, str(ex)))
raise ConfigValidationException('Invalid storage configuration: %s: %s' % (name, str(ex)))
def _validate_mailing(config, _):
def _validate_mailing(config, user_obj, _):
""" Validates sending email. """
test_app = Flask("mail-test-app")
test_app.config.update(config)
@ -133,85 +136,86 @@ def _validate_mailing(config, _):
test_mail = Mail(test_app)
test_msg = Message("Test e-mail from %s" % app.config['REGISTRY_TITLE'],
sender=config.get('MAIL_DEFAULT_SENDER'))
test_msg.add_recipient(get_authenticated_user().email)
test_msg.add_recipient(user_obj.email)
test_mail.send(test_msg)
def _validate_gitlab(config, _):
def _validate_gitlab(config, user_obj, _):
""" Validates the OAuth credentials and API endpoint for a GitLab service. """
github_config = config.get('GITLAB_TRIGGER_CONFIG')
if not github_config:
raise Exception('Missing GitLab client id and client secret')
raise ConfigValidationException('Missing GitLab client id and client secret')
endpoint = github_config.get('GITLAB_ENDPOINT')
if not endpoint:
raise Exception('Missing GitLab Endpoint')
raise ConfigValidationException('Missing GitLab Endpoint')
if endpoint.find('http://') != 0 and endpoint.find('https://') != 0:
raise Exception('GitLab Endpoint must start with http:// or https://')
raise ConfigValidationException('GitLab Endpoint must start with http:// or https://')
if not github_config.get('CLIENT_ID'):
raise Exception('Missing Client ID')
raise ConfigValidationException('Missing Client ID')
if not github_config.get('CLIENT_SECRET'):
raise Exception('Missing Client Secret')
raise ConfigValidationException('Missing Client Secret')
client = app.config['HTTPCLIENT']
oauth = GitLabOAuthConfig(config, 'GITLAB_TRIGGER_CONFIG')
result = oauth.validate_client_id_and_secret(client, app.config)
if not result:
raise Exception('Invalid client id or client secret')
raise ConfigValidationException('Invalid client id or client secret')
def _validate_github(config_key):
return lambda config, _: _validate_github_with_key(config_key, config)
return lambda config, user_obj, _: _validate_github_with_key(config_key, config)
def _validate_github_with_key(config_key, config):
""" Validates the OAuth credentials and API endpoint for a Github service. """
github_config = config.get(config_key)
if not github_config:
raise Exception('Missing GitHub client id and client secret')
raise ConfigValidationException('Missing GitHub client id and client secret')
endpoint = github_config.get('GITHUB_ENDPOINT')
if not endpoint:
raise Exception('Missing GitHub Endpoint')
raise ConfigValidationException('Missing GitHub Endpoint')
if endpoint.find('http://') != 0 and endpoint.find('https://') != 0:
raise Exception('Github Endpoint must start with http:// or https://')
raise ConfigValidationException('Github Endpoint must start with http:// or https://')
if not github_config.get('CLIENT_ID'):
raise Exception('Missing Client ID')
raise ConfigValidationException('Missing Client ID')
if not github_config.get('CLIENT_SECRET'):
raise Exception('Missing Client Secret')
raise ConfigValidationException('Missing Client Secret')
if github_config.get('ORG_RESTRICT') and not github_config.get('ALLOWED_ORGANIZATIONS'):
raise Exception('Organization restriction must have at least one allowed organization')
raise ConfigValidationException('Organization restriction must have at least one allowed ' +
'organization')
client = app.config['HTTPCLIENT']
oauth = GithubOAuthConfig(config, config_key)
result = oauth.validate_client_id_and_secret(client, app.config)
if not result:
raise Exception('Invalid client id or client secret')
raise ConfigValidationException('Invalid client id or client secret')
if github_config.get('ALLOWED_ORGANIZATIONS'):
for org_id in github_config.get('ALLOWED_ORGANIZATIONS'):
if not oauth.validate_organization(org_id, client):
raise Exception('Invalid organization: %s' % org_id)
raise ConfigValidationException('Invalid organization: %s' % org_id)
def _validate_bitbucket(config, _):
def _validate_bitbucket(config, user_obj, _):
""" Validates the config for BitBucket. """
trigger_config = config.get('BITBUCKET_TRIGGER_CONFIG')
if not trigger_config:
raise Exception('Missing client ID and client secret')
raise ConfigValidationException('Missing client ID and client secret')
if not trigger_config.get('CONSUMER_KEY'):
raise Exception('Missing Consumer Key')
raise ConfigValidationException('Missing Consumer Key')
if not trigger_config.get('CONSUMER_SECRET'):
raise Exception('Missing Consumer Secret')
raise ConfigValidationException('Missing Consumer Secret')
key = trigger_config['CONSUMER_KEY']
secret = trigger_config['CONSUMER_SECRET']
@ -220,29 +224,29 @@ def _validate_bitbucket(config, _):
bitbucket_client = BitBucket(key, secret, callback_url)
(result, _, _) = bitbucket_client.get_authorization_url()
if not result:
raise Exception('Invaid consumer key or secret')
raise ConfigValidationException('Invalid consumer key or secret')
def _validate_google_login(config, _):
def _validate_google_login(config, user_obj, _):
""" Validates the Google Login client ID and secret. """
google_login_config = config.get('GOOGLE_LOGIN_CONFIG')
if not google_login_config:
raise Exception('Missing client ID and client secret')
raise ConfigValidationException('Missing client ID and client secret')
if not google_login_config.get('CLIENT_ID'):
raise Exception('Missing Client ID')
raise ConfigValidationException('Missing Client ID')
if not google_login_config.get('CLIENT_SECRET'):
raise Exception('Missing Client Secret')
raise ConfigValidationException('Missing Client Secret')
client = app.config['HTTPCLIENT']
oauth = GoogleOAuthConfig(config, 'GOOGLE_LOGIN_CONFIG')
result = oauth.validate_client_id_and_secret(client, app.config)
if not result:
raise Exception('Invalid client id or client secret')
raise ConfigValidationException('Invalid client id or client secret')
def _validate_ssl(config, _):
def _validate_ssl(config, user_obj, _):
""" Validates the SSL configuration (if enabled). """
# Skip if non-SSL.
@ -255,7 +259,7 @@ def _validate_ssl(config, _):
for filename in SSL_FILENAMES:
if not config_provider.volume_file_exists(filename):
raise Exception('Missing required SSL file: %s' % filename)
raise ConfigValidationException('Missing required SSL file: %s' % filename)
with config_provider.get_volume_file(SSL_FILENAMES[0]) as f:
cert_contents = f.read()
@ -264,10 +268,10 @@ def _validate_ssl(config, _):
try:
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_contents)
except:
raise Exception('Could not parse certificate file. Is it a valid PEM certificate?')
raise ConfigValidationException('Could not parse certificate file. Is it a valid PEM certificate?')
if cert.has_expired():
raise Exception('The specified SSL certificate has expired.')
raise ConfigValidationException('The specified SSL certificate has expired.')
private_key_path = None
with config_provider.get_volume_file(SSL_FILENAMES[1]) as f:
@ -284,17 +288,17 @@ def _validate_ssl(config, _):
try:
context.use_privatekey_file(private_key_path)
except:
raise Exception('Could not parse key file. Is it a valid PEM private key?')
raise ConfigValidationException('Could not parse key file. Is it a valid PEM private key?')
try:
context.check_privatekey()
except OpenSSL.SSL.Error as e:
raise Exception('SSL key failed to validate: %s' % str(e))
raise ConfigValidationException('SSL key failed to validate: %s' % str(e))
# Verify the hostname matches the name in the certificate.
common_name = cert.get_subject().commonName
if common_name is None:
raise Exception('Missing CommonName (CN) from SSL certificate')
raise ConfigValidationException('Missing CommonName (CN) from SSL certificate')
# Build the list of allowed host patterns.
hosts = set([common_name])
@ -311,12 +315,13 @@ def _validate_ssl(config, _):
if fnmatch(config['SERVER_HOSTNAME'], host):
return
raise Exception('Supported names "%s" in SSL cert do not match server hostname "%s"' %
(', '.join(list(hosts)), config['SERVER_HOSTNAME']))
msg = ('Supported names "%s" in SSL cert do not match server hostname "%s"' %
(', '.join(list(hosts)), config['SERVER_HOSTNAME']))
raise ConfigValidationException(msg)
def _validate_ldap(config, password):
def _validate_ldap(config, user_obj, password):
""" Validates the LDAP connection. """
if config.get('AUTHENTICATION_TYPE', 'Database') != 'LDAP':
return
@ -330,14 +335,14 @@ def _validate_ldap(config, password):
admin_passwd = config.get('LDAP_ADMIN_PASSWD')
if not admin_dn:
raise Exception('Missing Admin DN for LDAP configuration')
raise ConfigValidationException('Missing Admin DN for LDAP configuration')
if not admin_passwd:
raise Exception('Missing Admin Password for LDAP configuration')
raise ConfigValidationException('Missing Admin Password for LDAP configuration')
ldap_uri = config.get('LDAP_URI', 'ldap://localhost')
if not ldap_uri.startswith('ldap://') and not ldap_uri.startswith('ldaps://'):
raise Exception('LDAP URI must start with ldap:// or ldaps://')
raise ConfigValidationException('LDAP URI must start with ldap:// or ldaps://')
allow_tls_fallback = config.get('LDAP_ALLOW_INSECURE_FALLBACK', False)
@ -347,9 +352,9 @@ def _validate_ldap(config, password):
except ldap.LDAPError as ex:
values = ex.args[0] if ex.args else {}
if not isinstance(values, dict):
raise Exception(str(ex.args))
raise ConfigValidationException(str(ex.args))
raise Exception(values.get('desc', 'Unknown error'))
raise ConfigValidationException(values.get('desc', 'Unknown error'))
# Verify that the superuser exists. If not, raise an exception.
base_dn = config.get('LDAP_BASE_DN')
@ -361,15 +366,16 @@ def _validate_ldap(config, password):
users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr,
allow_tls_fallback, requires_email=requires_email)
username = get_authenticated_user().username
username = user_obj.username
(result, err_msg) = users.verify_credentials(username, password)
if not result:
raise Exception(('Verification of superuser %s failed: %s. \n\nThe user either does not exist ' +
'in the remote authentication system ' +
'OR LDAP auth is misconfigured.') % (username, err_msg))
msg = ('Verification of superuser %s failed: %s. \n\nThe user either does not exist ' +
'in the remote authentication system ' +
'OR LDAP auth is misconfigured.') % (username, err_msg)
raise ConfigValidationException(msg)
def _validate_jwt(config, password):
def _validate_jwt(config, user_obj, password):
""" Validates the JWT authentication system. """
if config.get('AUTHENTICATION_TYPE', 'Database') != 'JWT':
return
@ -381,10 +387,10 @@ def _validate_jwt(config, password):
issuer = config.get('JWT_AUTH_ISSUER')
if not verify_endpoint:
raise Exception('Missing JWT Verification endpoint')
raise ConfigValidationException('Missing JWT Verification endpoint')
if not issuer:
raise Exception('Missing JWT Issuer ID')
raise ConfigValidationException('Missing JWT Issuer ID')
# Try to instatiate the JWT authentication mechanism. This will raise an exception if
# the key cannot be found.
@ -395,12 +401,13 @@ def _validate_jwt(config, password):
requires_email=config.get('FEATURE_MAILING', True))
# Verify that the superuser exists. If not, raise an exception.
username = get_authenticated_user().username
username = user_obj.username
(result, err_msg) = users.verify_credentials(username, password)
if not result:
raise Exception(('Verification of superuser %s failed: %s. \n\nThe user either does not ' +
'exist in the remote authentication system ' +
'OR JWT auth is misconfigured') % (username, err_msg))
msg = ('Verification of superuser %s failed: %s. \n\nThe user either does not ' +
'exist in the remote authentication system ' +
'OR JWT auth is misconfigured') % (username, err_msg)
raise ConfigValidationException(msg)
# If the query endpoint exists, ensure we can query to find the current user and that we can
# look up users directly.
@ -408,19 +415,22 @@ def _validate_jwt(config, password):
(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)
raise ConfigValidationException('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')
raise ConfigValidationException('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)
raise ConfigValidationException('Lookup endpoint is misconfigured or not returning ' +
'properly: %s' % err_msg)
def _validate_keystone(config, password):
def _validate_keystone(config, user_obj, password):
""" Validates the Keystone authentication system. """
if config.get('AUTHENTICATION_TYPE', 'Database') != 'Keystone':
return
@ -432,50 +442,52 @@ def _validate_keystone(config, password):
admin_tenant = config.get('KEYSTONE_ADMIN_TENANT')
if not auth_url:
raise Exception('Missing authentication URL')
raise ConfigValidationException('Missing authentication URL')
if not admin_username:
raise Exception('Missing admin username')
raise ConfigValidationException('Missing admin username')
if not admin_password:
raise Exception('Missing admin password')
raise ConfigValidationException('Missing admin password')
if not admin_tenant:
raise Exception('Missing admin tenant')
raise ConfigValidationException('Missing admin tenant')
requires_email = config.get('FEATURE_MAILING', True)
users = get_keystone_users(auth_version, auth_url, admin_username, admin_password, admin_tenant,
requires_email)
# Verify that the superuser exists. If not, raise an exception.
username = get_authenticated_user().username
username = user_obj.username
(result, err_msg) = users.verify_credentials(username, password)
if not result:
raise Exception(('Verification of superuser %s failed: %s \n\nThe user either does not ' +
'exist in the remote authentication system ' +
'OR Keystone auth is misconfigured.') % (username, err_msg))
msg = ('Verification of superuser %s failed: %s \n\nThe user either does not ' +
'exist in the remote authentication system ' +
'OR Keystone auth is misconfigured.') % (username, err_msg)
raise ConfigValidationException(msg)
def _validate_signer(config, _):
def _validate_signer(config, user_obj, _):
""" Validates the GPG public+private key pair used for signing converted ACIs. """
if config.get('SIGNING_ENGINE') is None:
return
if config['SIGNING_ENGINE'] not in SIGNING_ENGINES:
raise Exception('Unknown signing engine: %s' % config['SIGNING_ENGINE'])
raise ConfigValidationException('Unknown signing engine: %s' % config['SIGNING_ENGINE'])
engine = SIGNING_ENGINES[config['SIGNING_ENGINE']](config, config_provider)
engine.detached_sign(StringIO('test string'))
def _validate_security_scanner(config, _):
def _validate_security_scanner(config, user_obj, _):
""" Validates the configuration for talking to a Quay Security Scanner. """
# Generate a temporary Quay key to use for signing the outgoing requests.
setup_jwt_proxy()
# Wait a few seconds for the JWT proxy to startup.
time.sleep(2)
if not config.get('TESTING', False):
# Generate a temporary Quay key to use for signing the outgoing requests.
setup_jwt_proxy()
# Wait a few seconds for the JWT proxy to startup.
time.sleep(2)
# Make a ping request to the security service.
client = app.config['HTTPCLIENT']
@ -483,11 +495,14 @@ def _validate_security_scanner(config, _):
response = api.ping()
if response.status_code != 200:
message = 'Expected 200 status code, got %s: %s' % (response.status_code, response.text)
raise Exception('Could not ping security scanner: %s' % message)
raise ConfigValidationException('Could not ping security scanner: %s' % message)
def _validate_bittorrent(config, _):
def _validate_bittorrent(config, user_obj, _):
""" Validates the configuration for using BitTorrent for downloads. """
announce_url = config.get('BITTORRENT_ANNOUNCE_URL')
if not announce_url:
raise ConfigValidationException('Missing announce URL')
# Ensure that the tracker is reachable and accepts requests signed with a registry key.
client = app.config['HTTPCLIENT']
@ -505,24 +520,26 @@ def _validate_bittorrent(config, _):
encoded_jwt = torrent_jwt(params)
params['jwt'] = encoded_jwt
resp = client.get(config['BITTORRENT_ANNOUNCE_URL'], timeout=5, params=params)
resp = client.get(announce_url, timeout=5, params=params)
logger.debug('Got tracker response: %s: %s', resp.status_code, resp.text)
if resp.status_code == 404:
raise Exception('Announce path not found; did you forget `/announce`?')
raise ConfigValidationException('Announce path not found; did you forget `/announce`?')
if resp.status_code == 500:
raise Exception('Did not get expected response from Tracker; please check your settings')
raise ConfigValidationException('Did not get expected response from Tracker; ' +
'please check your settings')
if resp.status_code == 200:
if 'invalid jwt' in resp.text:
raise Exception('Could not authorize to Tracker; is your Tracker properly configured?')
raise ConfigValidationException('Could not authorize to Tracker; is your Tracker ' +
'properly configured?')
if 'failure reason' in resp.text:
raise Exception('Could not validate signed announce request: ' + resp.text)
raise ConfigValidationException('Could not validate signed announce request: ' + resp.text)
_VALIDATORS = {
VALIDATORS = {
'database': _validate_database,
'redis': _validate_redis,
'registry-storage': _validate_registry_storage,