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:
commit
d04b1faaee
2 changed files with 445 additions and 97 deletions
331
test/test_validate_config.py
Normal file
331
test/test_validate_config.py
Normal 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()
|
|
@ -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,
|
||||
|
|
Reference in a new issue