This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/util/config/validator.py

368 lines
12 KiB
Python
Raw Normal View History

import redis
import os
import json
import ldap
import peewee
2015-02-05 18:06:56 +00:00
import OpenSSL
import logging
2015-02-05 18:06:56 +00:00
from fnmatch import fnmatch
from data.users import LDAPConnection, ExternalJWTAuthN, LDAPUsers
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
from auth.auth_context import get_authenticated_user
from util.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig
from bitbucket import BitBucket
from app import app, CONFIG_PROVIDER, get_app_url, OVERRIDE_CONFIG_DIRECTORY
2015-02-05 18:06:56 +00:00
logger = logging.getLogger(__name__)
# Note: Only add files required for HTTPS to the SSL_FILESNAMES list.
SSL_FILENAMES = ['ssl.cert', 'ssl.key']
DB_SSL_FILENAMES = ['database.pem']
JWT_FILENAMES = ['jwt-authn.cert']
CONFIG_FILENAMES = SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES
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)')
def validate_service_for_config(service, config, password=None):
""" Attempts to validate the configuration for the given service. """
if not service in _VALIDATORS:
return {
'status': False
}
try:
_VALIDATORS[service](config, password)
return {
'status': True
}
except Exception as ex:
2015-02-05 18:06:56 +00:00
logger.exception('Validation exception')
return {
'status': False,
'reason': str(ex)
}
def _validate_database(config, _):
""" 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])
else:
raise ex
def _validate_redis(config, _):
""" Validates connecting to redis. """
redis_config = config.get('BUILDLOGS_REDIS', {})
if not 'host' in redis_config:
raise Exception('Missing redis hostname')
client = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
client.ping()
def _validate_registry_storage(config, _):
""" Validates registry storage. """
driver = get_storage_provider(config)
# Put and remove a temporary file.
driver.put_content('_verify', 'testing 123')
driver.remove('_verify')
# 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))
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)
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_mail.send(test_msg)
def _validate_gitlab(config, _):
""" 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')
endpoint = github_config.get('GITLAB_ENDPOINT')
if not endpoint:
raise Exception('Missing GitLab Endpoint')
if endpoint.find('http://') != 0 and endpoint.find('https://') != 0:
raise Exception('GitLab Endpoint must start with http:// or https://')
if not github_config.get('CLIENT_ID'):
raise Exception('Missing Client ID')
if not github_config.get('CLIENT_SECRET'):
raise Exception('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')
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. """
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')
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://')
if not github_config.get('CLIENT_ID'):
raise Exception('Missing Client ID')
if not github_config.get('CLIENT_SECRET'):
raise Exception('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')
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')
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)
def _validate_bitbucket(config, _):
""" Validates the config for BitBucket. """
trigger_config = config.get('BITBUCKET_TRIGGER_CONFIG')
if not trigger_config:
raise Exception('Missing client ID and client secret')
if not trigger_config.get('CONSUMER_KEY'):
raise Exception('Missing Consumer Key')
if not trigger_config.get('CONSUMER_SECRET'):
raise Exception('Missing Consumer Secret')
key = trigger_config['CONSUMER_KEY']
secret = trigger_config['CONSUMER_SECRET']
callback_url = '%s/oauth1/bitbucket/callback/trigger/' % (get_app_url())
bitbucket_client = BitBucket(key, secret, callback_url)
(result, _, _) = bitbucket_client.get_authorization_url()
if not result:
raise Exception('Invaid consumer key or secret')
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, app.config)
if not result:
raise Exception('Invalid client id or client secret')
def _validate_ssl(config, _):
""" Validates the SSL configuration (if enabled). """
if config.get('PREFERRED_URL_SCHEME', 'http') != 'https':
return
for filename in SSL_FILENAMES:
if not CONFIG_PROVIDER.volume_file_exists(filename):
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')
# 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
def _validate_ldap(config, password):
""" 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')
if not ldap_uri.startswith('ldap://') and not ldap_uri.startswith('ldaps://'):
raise Exception('LDAP URI must start with ldap:// or ldaps://')
try:
with LDAPConnection(ldap_uri, admin_dn, admin_passwd):
pass
except ldap.LDAPError as ex:
values = ex.args[0] if ex.args else {}
if not isinstance(values, dict):
raise Exception(str(ex.args))
raise Exception(values.get('desc', 'Unknown error'))
# Verify that the superuser exists. If not, raise an exception.
base_dn = config.get('LDAP_BASE_DN')
user_rdn = config.get('LDAP_USER_RDN', [])
uid_attr = config.get('LDAP_UID_ATTR', 'uid')
email_attr = config.get('LDAP_EMAIL_ATTR', 'mail')
users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr)
username = get_authenticated_user().username
(result, err_msg) = users.verify_user(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))
def _validate_jwt(config, password):
""" Validates the JWT authentication system. """
if config.get('AUTHENTICATION_TYPE', 'Database') != 'JWT':
return
verify_endpoint = config.get('JWT_VERIFY_ENDPOINT')
issuer = config.get('JWT_AUTH_ISSUER')
if not verify_endpoint:
raise Exception('Missing JWT Verification endpoint')
if not issuer:
raise Exception('Missing JWT Issuer ID')
# Try to instatiate the JWT authentication mechanism. This will raise an exception if
# the key cannot be found.
users = ExternalJWTAuthN(verify_endpoint, issuer, OVERRIDE_CONFIG_DIRECTORY,
app.config['HTTPCLIENT'])
# Verify that the superuser exists. If not, raise an exception.
username = get_authenticated_user().username
(result, err_msg) = users.verify_user(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))
_VALIDATORS = {
'database': _validate_database,
'redis': _validate_redis,
'registry-storage': _validate_registry_storage,
'mail': _validate_mailing,
'github-login': _validate_github('GITHUB_LOGIN_CONFIG'),
'github-trigger': _validate_github('GITHUB_TRIGGER_CONFIG'),
'gitlab-trigger': _validate_gitlab,
'bitbucket-trigger': _validate_bitbucket,
'google-login': _validate_google_login,
'ssl': _validate_ssl,
'ldap': _validate_ldap,
'jwt': _validate_jwt,
}