2015-01-07 21:20:51 +00:00
|
|
|
import redis
|
|
|
|
import os
|
|
|
|
import json
|
|
|
|
import ldap
|
2015-01-08 20:27:49 +00:00
|
|
|
import peewee
|
2015-02-05 18:06:56 +00:00
|
|
|
import OpenSSL
|
|
|
|
import logging
|
2015-01-07 21:20:51 +00:00
|
|
|
|
2015-02-05 18:06:56 +00:00
|
|
|
from fnmatch import fnmatch
|
2015-01-07 21:20:51 +00:00
|
|
|
from data.users import LDAPConnection
|
|
|
|
from flask import Flask
|
|
|
|
from flask.ext.mail import Mail, Message
|
|
|
|
from data.database import validate_database_url, User
|
|
|
|
from storage import get_storage_driver
|
2015-01-09 21:23:31 +00:00
|
|
|
from app import app, CONFIG_PROVIDER
|
2015-01-07 21:20:51 +00:00
|
|
|
from auth.auth_context import get_authenticated_user
|
|
|
|
from util.oauth import GoogleOAuthConfig, GithubOAuthConfig
|
|
|
|
|
2015-02-05 18:06:56 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
SSL_FILENAMES = ['ssl.cert', 'ssl.key']
|
|
|
|
|
2015-01-16 21:10:40 +00:00
|
|
|
def get_storage_provider(config):
|
|
|
|
parameters = config.get('DISTRIBUTED_STORAGE_CONFIG', {}).get('local', ['LocalStorage', {}])
|
|
|
|
try:
|
|
|
|
return get_storage_driver(parameters)
|
|
|
|
except TypeError:
|
|
|
|
raise Exception('Missing required storage configuration parameter(s)')
|
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
def validate_service_for_config(service, config):
|
|
|
|
""" Attempts to validate the configuration for the given service. """
|
|
|
|
if not service in _VALIDATORS:
|
|
|
|
return {
|
|
|
|
'status': False
|
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
|
|
|
_VALIDATORS[service](config)
|
|
|
|
return {
|
|
|
|
'status': True
|
|
|
|
}
|
|
|
|
except Exception as ex:
|
2015-02-05 18:06:56 +00:00
|
|
|
logger.exception('Validation exception')
|
2015-01-07 21:20:51 +00:00
|
|
|
return {
|
|
|
|
'status': False,
|
|
|
|
'reason': str(ex)
|
|
|
|
}
|
|
|
|
|
2015-01-08 18:26:24 +00:00
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
def _validate_database(config):
|
|
|
|
""" Validates connecting to the database. """
|
2015-01-08 20:27:49 +00:00
|
|
|
try:
|
|
|
|
validate_database_url(config['DB_URI'])
|
|
|
|
except peewee.OperationalError as ex:
|
|
|
|
if ex.args and len(ex.args) > 1:
|
|
|
|
raise Exception(ex.args[1])
|
|
|
|
else:
|
|
|
|
raise ex
|
2015-01-07 21:20:51 +00:00
|
|
|
|
2015-01-08 18:26:24 +00:00
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
def _validate_redis(config):
|
|
|
|
""" Validates connecting to redis. """
|
2015-01-08 20:27:49 +00:00
|
|
|
redis_config = config.get('BUILDLOGS_REDIS', {})
|
|
|
|
if not 'host' in redis_config:
|
|
|
|
raise Exception('Missing redis hostname')
|
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
client = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
|
|
|
|
client.ping()
|
|
|
|
|
2015-01-08 18:26:24 +00:00
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
def _validate_registry_storage(config):
|
|
|
|
""" Validates registry storage. """
|
2015-01-16 21:10:40 +00:00
|
|
|
driver = get_storage_provider(config)
|
2015-01-07 21:20:51 +00:00
|
|
|
|
|
|
|
# Put and remove a temporary file.
|
|
|
|
driver.put_content('_verify', 'testing 123')
|
|
|
|
driver.remove('_verify')
|
|
|
|
|
2015-01-16 21:10:40 +00:00
|
|
|
# Run setup on the driver if the read/write succeeded.
|
|
|
|
try:
|
|
|
|
driver.setup()
|
|
|
|
except Exception as ex:
|
|
|
|
raise Exception('Could not prepare storage: %s' % str(ex))
|
|
|
|
|
2015-01-08 18:26:24 +00:00
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
def _validate_mailing(config):
|
|
|
|
""" Validates sending email. """
|
|
|
|
test_app = Flask("mail-test-app")
|
|
|
|
test_app.config.update(config)
|
|
|
|
test_app.config.update({
|
|
|
|
'MAIL_FAIL_SILENTLY': False,
|
|
|
|
'TESTING': False
|
|
|
|
})
|
|
|
|
|
|
|
|
test_mail = Mail(test_app)
|
2015-03-03 18:56:32 +00:00
|
|
|
test_msg = Message("Test e-mail from %s" % app.config['REGISTRY_TITLE'],
|
|
|
|
sender=config.get('MAIL_DEFAULT_SENDER'))
|
2015-01-07 21:20:51 +00:00
|
|
|
test_msg.add_recipient(get_authenticated_user().email)
|
|
|
|
test_mail.send(test_msg)
|
|
|
|
|
2015-01-08 18:26:24 +00:00
|
|
|
|
|
|
|
def _validate_github(config_key):
|
|
|
|
return lambda config: _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. """
|
2015-01-08 18:56:17 +00:00
|
|
|
github_config = config.get(config_key)
|
|
|
|
if not github_config:
|
|
|
|
raise Exception('Missing Github client id and client secret')
|
|
|
|
|
|
|
|
endpoint = github_config.get('GITHUB_ENDPOINT')
|
2015-01-08 18:26:24 +00:00
|
|
|
if not endpoint:
|
|
|
|
raise Exception('Missing Github Endpoint')
|
|
|
|
|
|
|
|
if endpoint.find('http://') != 0 and endpoint.find('https://') != 0:
|
|
|
|
raise Exception('Github Endpoint must start with http:// or https://')
|
|
|
|
|
2015-01-08 18:56:17 +00:00
|
|
|
if not github_config.get('CLIENT_ID'):
|
2015-01-08 18:26:24 +00:00
|
|
|
raise Exception('Missing Client ID')
|
|
|
|
|
2015-01-08 18:56:17 +00:00
|
|
|
if not github_config.get('CLIENT_SECRET'):
|
2015-01-08 18:26:24 +00:00
|
|
|
raise Exception('Missing Client Secret')
|
|
|
|
|
2015-03-04 00:58:42 +00:00
|
|
|
if github_config.get('ORG_RESTRICT') and not github_config.get('ALLOWED_ORGANIZATIONS'):
|
|
|
|
raise Exception('Organization restriction must have at least one allowed organization')
|
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
client = app.config['HTTPCLIENT']
|
2015-01-08 18:26:24 +00:00
|
|
|
oauth = GithubOAuthConfig(config, config_key)
|
|
|
|
result = oauth.validate_client_id_and_secret(client)
|
|
|
|
if not result:
|
|
|
|
raise Exception('Invalid client id or client secret')
|
2015-01-07 21:20:51 +00:00
|
|
|
|
2015-03-04 00:58:42 +00:00
|
|
|
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)
|
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
|
2015-01-08 18:56:17 +00:00
|
|
|
def _validate_google_login(config):
|
|
|
|
""" 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')
|
|
|
|
|
|
|
|
if not google_login_config.get('CLIENT_ID'):
|
|
|
|
raise Exception('Missing Client ID')
|
|
|
|
|
|
|
|
if not google_login_config.get('CLIENT_SECRET'):
|
|
|
|
raise Exception('Missing Client Secret')
|
|
|
|
|
|
|
|
client = app.config['HTTPCLIENT']
|
|
|
|
oauth = GoogleOAuthConfig(config, 'GOOGLE_LOGIN_CONFIG')
|
|
|
|
result = oauth.validate_client_id_and_secret(client)
|
|
|
|
if not result:
|
|
|
|
raise Exception('Invalid client id or client secret')
|
|
|
|
|
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
def _validate_ssl(config):
|
|
|
|
""" Validates the SSL configuration (if enabled). """
|
|
|
|
if config.get('PREFERRED_URL_SCHEME', 'http') != 'https':
|
|
|
|
return
|
|
|
|
|
|
|
|
for filename in SSL_FILENAMES:
|
2015-01-09 21:23:31 +00:00
|
|
|
if not CONFIG_PROVIDER.volume_file_exists(filename):
|
2015-01-07 21:20:51 +00:00
|
|
|
raise Exception('Missing required SSL file: %s' % filename)
|
|
|
|
|
2015-02-05 18:06:56 +00:00
|
|
|
with CONFIG_PROVIDER.get_volume_file(SSL_FILENAMES[0]) as f:
|
|
|
|
cert_contents = f.read()
|
|
|
|
|
|
|
|
# Validate the certificate.
|
|
|
|
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?')
|
|
|
|
|
|
|
|
if cert.has_expired():
|
|
|
|
raise Exception('The specified SSL certificate has expired.')
|
|
|
|
|
|
|
|
private_key_path = None
|
|
|
|
with CONFIG_PROVIDER.get_volume_file(SSL_FILENAMES[1]) as f:
|
|
|
|
private_key_path = f.name
|
|
|
|
|
|
|
|
if not private_key_path:
|
|
|
|
# Only in testing.
|
|
|
|
return
|
|
|
|
|
|
|
|
# Validate the private key with the certificate.
|
|
|
|
context = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
|
|
|
|
context.use_certificate(cert)
|
|
|
|
|
|
|
|
try:
|
|
|
|
context.use_privatekey_file(private_key_path)
|
|
|
|
except:
|
|
|
|
raise Exception('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))
|
|
|
|
|
|
|
|
# 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')
|
|
|
|
|
2015-02-12 19:00:26 +00:00
|
|
|
# Build the list of allowed host patterns.
|
|
|
|
hosts = set([common_name])
|
|
|
|
|
|
|
|
# Find the DNS extension, if any.
|
|
|
|
for i in range(0, cert.get_extension_count()):
|
|
|
|
ext = cert.get_extension(i)
|
|
|
|
if ext.get_short_name() == 'subjectAltName':
|
|
|
|
value = str(ext)
|
|
|
|
hosts.update([host.strip()[4:] for host in value.split(',')])
|
|
|
|
|
|
|
|
# Check each host.
|
|
|
|
for host in hosts:
|
|
|
|
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']))
|
2015-02-05 18:06:56 +00:00
|
|
|
|
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
|
|
|
|
def _validate_ldap(config):
|
|
|
|
""" Validates the LDAP connection. """
|
|
|
|
if config.get('AUTHENTICATION_TYPE', 'Database') != 'LDAP':
|
|
|
|
return
|
|
|
|
|
|
|
|
# Note: raises ldap.INVALID_CREDENTIALS on failure
|
|
|
|
admin_dn = config.get('LDAP_ADMIN_DN')
|
|
|
|
admin_passwd = config.get('LDAP_ADMIN_PASSWD')
|
|
|
|
|
|
|
|
if not admin_dn:
|
|
|
|
raise Exception('Missing Admin DN for LDAP configuration')
|
|
|
|
|
|
|
|
if not admin_passwd:
|
|
|
|
raise Exception('Missing Admin Password for LDAP configuration')
|
|
|
|
|
|
|
|
ldap_uri = config.get('LDAP_URI', 'ldap://localhost')
|
2015-03-16 18:33:53 +00:00
|
|
|
if not ldap_uri.startswith('ldap://') and not ldap_uri.startswith('ldaps://'):
|
|
|
|
raise Exception('LDAP URI must start with ldap:// or ldaps://')
|
2015-01-07 21:20:51 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
with LDAPConnection(ldap_uri, admin_dn, admin_passwd):
|
|
|
|
pass
|
|
|
|
except ldap.LDAPError as ex:
|
|
|
|
values = ex.args[0] if ex.args else {}
|
2015-03-16 18:33:53 +00:00
|
|
|
if not isinstance(values, dict):
|
|
|
|
raise Exception(str(ex.args))
|
|
|
|
|
2015-01-07 21:20:51 +00:00
|
|
|
raise Exception(values.get('desc', 'Unknown error'))
|
|
|
|
|
|
|
|
|
|
|
|
_VALIDATORS = {
|
|
|
|
'database': _validate_database,
|
|
|
|
'redis': _validate_redis,
|
|
|
|
'registry-storage': _validate_registry_storage,
|
|
|
|
'mail': _validate_mailing,
|
2015-01-08 18:26:24 +00:00
|
|
|
'github-login': _validate_github('GITHUB_LOGIN_CONFIG'),
|
|
|
|
'github-trigger': _validate_github('GITHUB_TRIGGER_CONFIG'),
|
2015-01-08 18:56:17 +00:00
|
|
|
'google-login': _validate_google_login,
|
2015-01-07 21:20:51 +00:00
|
|
|
'ssl': _validate_ssl,
|
|
|
|
'ldap': _validate_ldap,
|
|
|
|
}
|