Merge pull request #2176 from coreos-inc/fix-setup-tool

Fix config validator for storage and add a test suite
This commit is contained in:
josephschorr 2016-11-30 13:22:05 -05:00 committed by GitHub
commit d04b1faaee
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.registry.torrent import torrent_jwt
from util.security.signing import SIGNING_ENGINES from util.security.signing import SIGNING_ENGINES
logger = logging.getLogger(__name__) 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. # Note: Only add files required for HTTPS to the SSL_FILESNAMES list.
SSL_FILENAMES = ['ssl.cert', 'ssl.key'] SSL_FILENAMES = ['ssl.cert', 'ssl.key']
@ -50,22 +53,22 @@ def get_storage_providers(config):
try: try:
for name, parameters in storage_config.items(): for name, parameters in storage_config.items():
drivers[name] = (parameters[0], get_storage_driver(None, parameters)) drivers[name] = (parameters[0], get_storage_driver(None, None, None, parameters))
except TypeError: except TypeError as te:
logger.exception('Missing required storage configuration provider') 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 return drivers
def validate_service_for_config(service, config, password=None): def validate_service_for_config(service, config, password=None):
""" Attempts to validate the configuration for the given service. """ """ Attempts to validate the configuration for the given service. """
if not service in _VALIDATORS: if not service in VALIDATORS:
return { return {
'status': False 'status': False
} }
try: try:
_VALIDATORS[service](config, password) VALIDATORS[service](config, get_authenticated_user(), password)
return { return {
'status': True '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. """ """ Validates connecting to the database. """
try: try:
validate_database_url(config['DB_URI'], config.get('DB_CONNECTION_ARGS', {})) validate_database_url(config['DB_URI'], config.get('DB_CONNECTION_ARGS', {}))
except peewee.OperationalError as ex: except peewee.OperationalError as ex:
if ex.args and len(ex.args) > 1: if ex.args and len(ex.args) > 1:
raise Exception(ex.args[1]) raise ConfigValidationException(ex.args[1])
else: else:
raise ex raise ex
def _validate_redis(config, _): def _validate_redis(config, user_obj, _):
""" Validates connecting to redis. """ """ Validates connecting to redis. """
redis_config = config.get('BUILDLOGS_REDIS', {}) redis_config = config.get('BUILDLOGS_REDIS', {})
if not 'host' in redis_config: 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 = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
client.ping() client.ping()
def _validate_registry_storage(config, _): def _validate_registry_storage(config, user_obj, _):
""" Validates registry storage. """ """ Validates registry storage. """
replication_enabled = config.get('FEATURE_STORAGE_REPLICATION', False) replication_enabled = config.get('FEATURE_STORAGE_REPLICATION', False)
providers = get_storage_providers(config).items() providers = get_storage_providers(config).items()
if not providers: if not providers:
raise Exception('Storage configuration required') raise ConfigValidationException('Storage configuration required')
for name, (storage_type, driver) in providers: for name, (storage_type, driver) in providers:
try: try:
if replication_enabled and storage_type == 'LocalStorage': 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. # Run validation on the driver.
driver.validate(app.config['HTTPCLIENT']) driver.validate(app.config['HTTPCLIENT'])
@ -118,10 +121,10 @@ def _validate_registry_storage(config, _):
# Run setup on the driver if the read/write succeeded. # Run setup on the driver if the read/write succeeded.
driver.setup() driver.setup()
except Exception as ex: 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. """ """ Validates sending email. """
test_app = Flask("mail-test-app") test_app = Flask("mail-test-app")
test_app.config.update(config) test_app.config.update(config)
@ -133,85 +136,86 @@ def _validate_mailing(config, _):
test_mail = Mail(test_app) test_mail = Mail(test_app)
test_msg = Message("Test e-mail from %s" % app.config['REGISTRY_TITLE'], test_msg = Message("Test e-mail from %s" % app.config['REGISTRY_TITLE'],
sender=config.get('MAIL_DEFAULT_SENDER')) 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) 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. """ """ Validates the OAuth credentials and API endpoint for a GitLab service. """
github_config = config.get('GITLAB_TRIGGER_CONFIG') github_config = config.get('GITLAB_TRIGGER_CONFIG')
if not github_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') endpoint = github_config.get('GITLAB_ENDPOINT')
if not endpoint: if not endpoint:
raise Exception('Missing GitLab Endpoint') raise ConfigValidationException('Missing GitLab Endpoint')
if endpoint.find('http://') != 0 and endpoint.find('https://') != 0: 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'): if not github_config.get('CLIENT_ID'):
raise Exception('Missing Client ID') raise ConfigValidationException('Missing Client ID')
if not github_config.get('CLIENT_SECRET'): if not github_config.get('CLIENT_SECRET'):
raise Exception('Missing Client Secret') raise ConfigValidationException('Missing Client Secret')
client = app.config['HTTPCLIENT'] client = app.config['HTTPCLIENT']
oauth = GitLabOAuthConfig(config, 'GITLAB_TRIGGER_CONFIG') oauth = GitLabOAuthConfig(config, 'GITLAB_TRIGGER_CONFIG')
result = oauth.validate_client_id_and_secret(client, app.config) result = oauth.validate_client_id_and_secret(client, app.config)
if not result: if not result:
raise Exception('Invalid client id or client secret') raise ConfigValidationException('Invalid client id or client secret')
def _validate_github(config_key): 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): def _validate_github_with_key(config_key, config):
""" Validates the OAuth credentials and API endpoint for a Github service. """ """ Validates the OAuth credentials and API endpoint for a Github service. """
github_config = config.get(config_key) github_config = config.get(config_key)
if not github_config: 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') endpoint = github_config.get('GITHUB_ENDPOINT')
if not endpoint: if not endpoint:
raise Exception('Missing GitHub Endpoint') raise ConfigValidationException('Missing GitHub Endpoint')
if endpoint.find('http://') != 0 and endpoint.find('https://') != 0: 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'): if not github_config.get('CLIENT_ID'):
raise Exception('Missing Client ID') raise ConfigValidationException('Missing Client ID')
if not github_config.get('CLIENT_SECRET'): 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'): 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'] client = app.config['HTTPCLIENT']
oauth = GithubOAuthConfig(config, config_key) oauth = GithubOAuthConfig(config, config_key)
result = oauth.validate_client_id_and_secret(client, app.config) result = oauth.validate_client_id_and_secret(client, app.config)
if not result: 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'): if github_config.get('ALLOWED_ORGANIZATIONS'):
for org_id in github_config.get('ALLOWED_ORGANIZATIONS'): for org_id in github_config.get('ALLOWED_ORGANIZATIONS'):
if not oauth.validate_organization(org_id, client): 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. """ """ Validates the config for BitBucket. """
trigger_config = config.get('BITBUCKET_TRIGGER_CONFIG') trigger_config = config.get('BITBUCKET_TRIGGER_CONFIG')
if not 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'): if not trigger_config.get('CONSUMER_KEY'):
raise Exception('Missing Consumer Key') raise ConfigValidationException('Missing Consumer Key')
if not trigger_config.get('CONSUMER_SECRET'): if not trigger_config.get('CONSUMER_SECRET'):
raise Exception('Missing Consumer Secret') raise ConfigValidationException('Missing Consumer Secret')
key = trigger_config['CONSUMER_KEY'] key = trigger_config['CONSUMER_KEY']
secret = trigger_config['CONSUMER_SECRET'] secret = trigger_config['CONSUMER_SECRET']
@ -220,29 +224,29 @@ def _validate_bitbucket(config, _):
bitbucket_client = BitBucket(key, secret, callback_url) bitbucket_client = BitBucket(key, secret, callback_url)
(result, _, _) = bitbucket_client.get_authorization_url() (result, _, _) = bitbucket_client.get_authorization_url()
if not result: 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. """ """ Validates the Google Login client ID and secret. """
google_login_config = config.get('GOOGLE_LOGIN_CONFIG') google_login_config = config.get('GOOGLE_LOGIN_CONFIG')
if not 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'): 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'): if not google_login_config.get('CLIENT_SECRET'):
raise Exception('Missing Client Secret') raise ConfigValidationException('Missing Client Secret')
client = app.config['HTTPCLIENT'] client = app.config['HTTPCLIENT']
oauth = GoogleOAuthConfig(config, 'GOOGLE_LOGIN_CONFIG') oauth = GoogleOAuthConfig(config, 'GOOGLE_LOGIN_CONFIG')
result = oauth.validate_client_id_and_secret(client, app.config) result = oauth.validate_client_id_and_secret(client, app.config)
if not result: 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). """ """ Validates the SSL configuration (if enabled). """
# Skip if non-SSL. # Skip if non-SSL.
@ -255,7 +259,7 @@ def _validate_ssl(config, _):
for filename in SSL_FILENAMES: for filename in SSL_FILENAMES:
if not config_provider.volume_file_exists(filename): 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: with config_provider.get_volume_file(SSL_FILENAMES[0]) as f:
cert_contents = f.read() cert_contents = f.read()
@ -264,10 +268,10 @@ def _validate_ssl(config, _):
try: try:
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_contents) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_contents)
except: 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(): if cert.has_expired():
raise Exception('The specified SSL certificate has expired.') raise ConfigValidationException('The specified SSL certificate has expired.')
private_key_path = None private_key_path = None
with config_provider.get_volume_file(SSL_FILENAMES[1]) as f: with config_provider.get_volume_file(SSL_FILENAMES[1]) as f:
@ -284,17 +288,17 @@ def _validate_ssl(config, _):
try: try:
context.use_privatekey_file(private_key_path) context.use_privatekey_file(private_key_path)
except: 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: try:
context.check_privatekey() context.check_privatekey()
except OpenSSL.SSL.Error as e: 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. # Verify the hostname matches the name in the certificate.
common_name = cert.get_subject().commonName common_name = cert.get_subject().commonName
if common_name is None: 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. # Build the list of allowed host patterns.
hosts = set([common_name]) hosts = set([common_name])
@ -311,12 +315,13 @@ def _validate_ssl(config, _):
if fnmatch(config['SERVER_HOSTNAME'], host): if fnmatch(config['SERVER_HOSTNAME'], host):
return return
raise Exception('Supported names "%s" in SSL cert do not match server hostname "%s"' % msg = ('Supported names "%s" in SSL cert do not match server hostname "%s"' %
(', '.join(list(hosts)), config['SERVER_HOSTNAME'])) (', '.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. """ """ Validates the LDAP connection. """
if config.get('AUTHENTICATION_TYPE', 'Database') != 'LDAP': if config.get('AUTHENTICATION_TYPE', 'Database') != 'LDAP':
return return
@ -330,14 +335,14 @@ def _validate_ldap(config, password):
admin_passwd = config.get('LDAP_ADMIN_PASSWD') admin_passwd = config.get('LDAP_ADMIN_PASSWD')
if not admin_dn: if not admin_dn:
raise Exception('Missing Admin DN for LDAP configuration') raise ConfigValidationException('Missing Admin DN for LDAP configuration')
if not admin_passwd: 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') ldap_uri = config.get('LDAP_URI', 'ldap://localhost')
if not ldap_uri.startswith('ldap://') and not ldap_uri.startswith('ldaps://'): 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) allow_tls_fallback = config.get('LDAP_ALLOW_INSECURE_FALLBACK', False)
@ -347,9 +352,9 @@ def _validate_ldap(config, password):
except ldap.LDAPError as ex: except ldap.LDAPError as ex:
values = ex.args[0] if ex.args else {} values = ex.args[0] if ex.args else {}
if not isinstance(values, dict): 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. # Verify that the superuser exists. If not, raise an exception.
base_dn = config.get('LDAP_BASE_DN') 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, users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr,
allow_tls_fallback, requires_email=requires_email) allow_tls_fallback, requires_email=requires_email)
username = get_authenticated_user().username username = user_obj.username
(result, err_msg) = users.verify_credentials(username, password) (result, err_msg) = users.verify_credentials(username, password)
if not result: if not result:
raise Exception(('Verification of superuser %s failed: %s. \n\nThe user either does not exist ' + msg = ('Verification of superuser %s failed: %s. \n\nThe user either does not exist ' +
'in the remote authentication system ' + 'in the remote authentication system ' +
'OR LDAP auth is misconfigured.') % (username, err_msg)) '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. """ """ Validates the JWT authentication system. """
if config.get('AUTHENTICATION_TYPE', 'Database') != 'JWT': if config.get('AUTHENTICATION_TYPE', 'Database') != 'JWT':
return return
@ -381,10 +387,10 @@ def _validate_jwt(config, password):
issuer = config.get('JWT_AUTH_ISSUER') issuer = config.get('JWT_AUTH_ISSUER')
if not verify_endpoint: if not verify_endpoint:
raise Exception('Missing JWT Verification endpoint') raise ConfigValidationException('Missing JWT Verification endpoint')
if not issuer: 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 # Try to instatiate the JWT authentication mechanism. This will raise an exception if
# the key cannot be found. # the key cannot be found.
@ -395,12 +401,13 @@ def _validate_jwt(config, password):
requires_email=config.get('FEATURE_MAILING', True)) requires_email=config.get('FEATURE_MAILING', True))
# 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 = user_obj.username
(result, err_msg) = users.verify_credentials(username, password) (result, err_msg) = users.verify_credentials(username, password)
if not result: if not result:
raise Exception(('Verification of superuser %s failed: %s. \n\nThe user either does not ' + msg = ('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)
raise ConfigValidationException(msg)
# If the query endpoint exists, ensure we can query to find the current user and that we can # If the query endpoint exists, ensure we can query to find the current user and that we can
# look up users directly. # look up users directly.
@ -408,19 +415,22 @@ def _validate_jwt(config, password):
(results, err_msg) = users.query_users(username) (results, err_msg) = users.query_users(username)
if not results: if not results:
err_msg = err_msg or ('Could not find users matching query: %s' % username) 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. # Make sure the get user endpoint is also configured.
if not getuser_endpoint: 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) (result, err_msg) = users.get_user(username)
if not result: if not result:
err_msg = err_msg or ('Could not find user %s' % username) 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. """ """ Validates the Keystone authentication system. """
if config.get('AUTHENTICATION_TYPE', 'Database') != 'Keystone': if config.get('AUTHENTICATION_TYPE', 'Database') != 'Keystone':
return return
@ -432,50 +442,52 @@ def _validate_keystone(config, password):
admin_tenant = config.get('KEYSTONE_ADMIN_TENANT') admin_tenant = config.get('KEYSTONE_ADMIN_TENANT')
if not auth_url: if not auth_url:
raise Exception('Missing authentication URL') raise ConfigValidationException('Missing authentication URL')
if not admin_username: if not admin_username:
raise Exception('Missing admin username') raise ConfigValidationException('Missing admin username')
if not admin_password: if not admin_password:
raise Exception('Missing admin password') raise ConfigValidationException('Missing admin password')
if not admin_tenant: if not admin_tenant:
raise Exception('Missing admin tenant') raise ConfigValidationException('Missing admin tenant')
requires_email = config.get('FEATURE_MAILING', True) requires_email = config.get('FEATURE_MAILING', True)
users = get_keystone_users(auth_version, auth_url, admin_username, admin_password, admin_tenant, users = get_keystone_users(auth_version, auth_url, admin_username, admin_password, admin_tenant,
requires_email) requires_email)
# 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 = user_obj.username
(result, err_msg) = users.verify_credentials(username, password) (result, err_msg) = users.verify_credentials(username, password)
if not result: if not result:
raise Exception(('Verification of superuser %s failed: %s \n\nThe user either does not ' + msg = ('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 Keystone auth is misconfigured.') % (username, err_msg)) '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. """ """ Validates the GPG public+private key pair used for signing converted ACIs. """
if config.get('SIGNING_ENGINE') is None: if config.get('SIGNING_ENGINE') is None:
return return
if config['SIGNING_ENGINE'] not in SIGNING_ENGINES: 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 = SIGNING_ENGINES[config['SIGNING_ENGINE']](config, config_provider)
engine.detached_sign(StringIO('test string')) 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. """ """ 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. if not config.get('TESTING', False):
time.sleep(2) # 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. # Make a ping request to the security service.
client = app.config['HTTPCLIENT'] client = app.config['HTTPCLIENT']
@ -483,11 +495,14 @@ def _validate_security_scanner(config, _):
response = api.ping() response = api.ping()
if response.status_code != 200: if response.status_code != 200:
message = 'Expected 200 status code, got %s: %s' % (response.status_code, response.text) 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. """ """ 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. # Ensure that the tracker is reachable and accepts requests signed with a registry key.
client = app.config['HTTPCLIENT'] client = app.config['HTTPCLIENT']
@ -505,24 +520,26 @@ def _validate_bittorrent(config, _):
encoded_jwt = torrent_jwt(params) encoded_jwt = torrent_jwt(params)
params['jwt'] = encoded_jwt 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) logger.debug('Got tracker response: %s: %s', resp.status_code, resp.text)
if resp.status_code == 404: 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: 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 resp.status_code == 200:
if 'invalid jwt' in resp.text: 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: 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, 'database': _validate_database,
'redis': _validate_redis, 'redis': _validate_redis,
'registry-storage': _validate_registry_storage, 'registry-storage': _validate_registry_storage,