Change validators to use the validator_context
Change InstanceKeys to take a namedtuple for context
This commit is contained in:
parent
e967fde3ae
commit
554d4f47a8
31 changed files with 172 additions and 69 deletions
6
app.py
6
app.py
|
@ -50,7 +50,7 @@ from util.metrics.prometheus import PrometheusPlugin
|
||||||
from util.saas.cloudwatch import start_cloudwatch_sender
|
from util.saas.cloudwatch import start_cloudwatch_sender
|
||||||
from util.secscan.api import SecurityScannerAPI
|
from util.secscan.api import SecurityScannerAPI
|
||||||
from util.tufmetadata.api import TUFMetadataAPI
|
from util.tufmetadata.api import TUFMetadataAPI
|
||||||
from util.security.instancekeys import InstanceKeys
|
from util.security.instancekeys import InstanceKeys, instance_keys_context_from_app_config
|
||||||
from util.security.signing import Signer
|
from util.security.signing import Signer
|
||||||
|
|
||||||
|
|
||||||
|
@ -182,7 +182,7 @@ mail = Mail(app)
|
||||||
prometheus = PrometheusPlugin(app)
|
prometheus = PrometheusPlugin(app)
|
||||||
metric_queue = MetricQueue(prometheus)
|
metric_queue = MetricQueue(prometheus)
|
||||||
chunk_cleanup_queue = WorkQueue(app.config['CHUNK_CLEANUP_QUEUE_NAME'], tf, metric_queue=metric_queue)
|
chunk_cleanup_queue = WorkQueue(app.config['CHUNK_CLEANUP_QUEUE_NAME'], tf, metric_queue=metric_queue)
|
||||||
instance_keys = InstanceKeys(app)
|
instance_keys = InstanceKeys(instance_keys_context_from_app_config(app.config))
|
||||||
ip_resolver = IPResolver(app)
|
ip_resolver = IPResolver(app)
|
||||||
storage = Storage(app, metric_queue, chunk_cleanup_queue, instance_keys, config_provider, ip_resolver)
|
storage = Storage(app, metric_queue, chunk_cleanup_queue, instance_keys, config_provider, ip_resolver)
|
||||||
userfiles = Userfiles(app, storage)
|
userfiles = Userfiles(app, storage)
|
||||||
|
@ -196,7 +196,7 @@ authentication = UserAuthentication(app, config_provider, OVERRIDE_CONFIG_DIRECT
|
||||||
userevents = UserEventsBuilderModule(app)
|
userevents = UserEventsBuilderModule(app)
|
||||||
superusers = SuperUserManager(app)
|
superusers = SuperUserManager(app)
|
||||||
signer = Signer(app, config_provider)
|
signer = Signer(app, config_provider)
|
||||||
instance_keys = InstanceKeys(app)
|
instance_keys = InstanceKeys(instance_keys_context_from_app_config(app.config))
|
||||||
label_validator = LabelValidator(app)
|
label_validator = LabelValidator(app)
|
||||||
build_canceller = BuildCanceller(app)
|
build_canceller = BuildCanceller(app)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import subprocess
|
||||||
|
|
||||||
from flask import abort
|
from flask import abort
|
||||||
|
|
||||||
from app import app, config_provider, superusers, OVERRIDE_CONFIG_DIRECTORY
|
from app import app, config_provider, superusers, OVERRIDE_CONFIG_DIRECTORY, ip_resolver
|
||||||
from auth.permissions import SuperUserPermission
|
from auth.permissions import SuperUserPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from data.database import configure
|
from data.database import configure
|
||||||
|
@ -20,7 +20,7 @@ from endpoints.api import (ApiResource, nickname, resource, internal_only, show_
|
||||||
from endpoints.common import common_login
|
from endpoints.common import common_login
|
||||||
from util.config.configutil import add_enterprise_config_defaults
|
from util.config.configutil import add_enterprise_config_defaults
|
||||||
from util.config.database import sync_database_with_config
|
from util.config.database import sync_database_with_config
|
||||||
from util.config.validator import validate_service_for_config, is_valid_config_upload_filename
|
from util.config.validator import validate_service_for_config, is_valid_config_upload_filename, ValidatorContext
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
@ -405,6 +405,7 @@ class SuperUserConfigValidate(ApiResource):
|
||||||
# this is also safe since this method does not access any information not given in the request.
|
# this is also safe since this method does not access any information not given in the request.
|
||||||
if not config_provider.config_exists() or SuperUserPermission().can():
|
if not config_provider.config_exists() or SuperUserPermission().can():
|
||||||
config = request.get_json()['config']
|
config = request.get_json()['config']
|
||||||
return validate_service_for_config(service, config, request.get_json().get('password', ''), app)
|
validator_context = ValidatorContext.from_app(config, request.get_json().get('password', ''), app, ip_resolver)
|
||||||
|
return validate_service_for_config(service, validator_context)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
|
@ -29,6 +29,8 @@ class GitLabOAuthService(OAuthService):
|
||||||
def token_endpoint(self):
|
def token_endpoint(self):
|
||||||
return OAuthEndpoint(slash_join(self._endpoint(), '/oauth/token'))
|
return OAuthEndpoint(slash_join(self._endpoint(), '/oauth/token'))
|
||||||
|
|
||||||
|
# TODO(sam): this signature does not match its parent class. refactor the base method to take the namedtuple URLSchemeAndHostname
|
||||||
|
# TODO cont: reason I did this was to decouple the app, but it requires more refactoring
|
||||||
def validate_client_id_and_secret(self, http_client, url_scheme_and_hostname):
|
def validate_client_id_and_secret(self, http_client, url_scheme_and_hostname):
|
||||||
# We validate the client ID and secret by hitting the OAuth token exchange endpoint with
|
# We validate the client ID and secret by hitting the OAuth token exchange endpoint with
|
||||||
# the real client ID and secret, but a fake auth code to exchange. Gitlab's implementation will
|
# the real client ID and secret, but a fake auth code to exchange. Gitlab's implementation will
|
||||||
|
|
|
@ -7,7 +7,7 @@ from flask_testing import LiveServerTestCase
|
||||||
|
|
||||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||||
from storage import Storage
|
from storage import Storage
|
||||||
from util.security.instancekeys import InstanceKeys
|
from util.security.instancekeys import InstanceKeys, instance_keys_context_from_app_config
|
||||||
|
|
||||||
_PORT_NUMBER = 5001
|
_PORT_NUMBER = 5001
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class TestStorageProxy(LiveServerTestCase):
|
||||||
'test': ['FakeStorage', {}],
|
'test': ['FakeStorage', {}],
|
||||||
}
|
}
|
||||||
|
|
||||||
instance_keys = InstanceKeys(self.test_app)
|
instance_keys = InstanceKeys(instance_keys_context_from_app_config(self.test_app.config))
|
||||||
self.storage = Storage(self.test_app, instance_keys=instance_keys)
|
self.storage = Storage(self.test_app, instance_keys=instance_keys)
|
||||||
self.test_app.config['DISTRIBUTED_STORAGE_PREFERENCE'] = ['test']
|
self.test_app.config['DISTRIBUTED_STORAGE_PREFERENCE'] = ['test']
|
||||||
return self.test_app
|
return self.test_app
|
||||||
|
|
|
@ -2,6 +2,9 @@ def get_app_url(config):
|
||||||
""" Returns the application's URL, based on the given config. """
|
""" Returns the application's URL, based on the given config. """
|
||||||
return '%s://%s' % (config['PREFERRED_URL_SCHEME'], config['SERVER_HOSTNAME'])
|
return '%s://%s' % (config['PREFERRED_URL_SCHEME'], config['SERVER_HOSTNAME'])
|
||||||
|
|
||||||
|
def get_app_url_from_scheme_hostname(url_scheme_and_hostname):
|
||||||
|
""" Returns the application's URL, based on the given url scheme and hostname. """
|
||||||
|
return '%s://%s' % (url_scheme_and_hostname.url_scheme, url_scheme_and_hostname.hostname)
|
||||||
|
|
||||||
def slash_join(*args):
|
def slash_join(*args):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
|
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from data.users import LDAP_CERT_FILENAME
|
from data.users import LDAP_CERT_FILENAME
|
||||||
|
from util.config import URLSchemeAndHostname
|
||||||
|
|
||||||
from util.config.validators.validate_database import DatabaseValidator
|
from util.config.validators.validate_database import DatabaseValidator
|
||||||
from util.config.validators.validate_redis import RedisValidator
|
from util.config.validators.validate_redis import RedisValidator
|
||||||
|
@ -64,7 +65,7 @@ VALIDATORS = {
|
||||||
AppTokenAuthValidator.name: AppTokenAuthValidator.validate,
|
AppTokenAuthValidator.name: AppTokenAuthValidator.validate,
|
||||||
}
|
}
|
||||||
|
|
||||||
def validate_service_for_config(service, config, password=None, app=None):
|
def validate_service_for_config(service, validator_context):
|
||||||
""" 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 {
|
||||||
|
@ -72,7 +73,7 @@ def validate_service_for_config(service, config, password=None, app=None):
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
VALIDATORS[service](config, get_authenticated_user(), password, app)
|
VALIDATORS[service](validator_context)
|
||||||
return {
|
return {
|
||||||
'status': True
|
'status': True
|
||||||
}
|
}
|
||||||
|
@ -92,3 +93,28 @@ def is_valid_config_upload_filename(filename):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return any([filename.endswith(suffix) for suffix in CONFIG_FILE_SUFFIXES])
|
return any([filename.endswith(suffix) for suffix in CONFIG_FILE_SUFFIXES])
|
||||||
|
|
||||||
|
|
||||||
|
class ValidatorContext(object):
|
||||||
|
""" Context to run validators in, with any additional runtime configuration they need
|
||||||
|
"""
|
||||||
|
def __init__(self, config, user_password=None, http_client=None, context=None,
|
||||||
|
url_scheme_and_hostname=None, jwt_auth_max=None, registry_title=None, ip_resolver=None):
|
||||||
|
self.config = config
|
||||||
|
self.user = get_authenticated_user()
|
||||||
|
self.user_password = user_password
|
||||||
|
self.http_client = http_client
|
||||||
|
self.context = context
|
||||||
|
self.scheme_and_hostname = url_scheme_and_hostname
|
||||||
|
self.jwt_auth_max = jwt_auth_max
|
||||||
|
self.registry_title = registry_title
|
||||||
|
self.ip_resolver = ip_resolver
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_app(cls, config, user_password, app, ip_resolver):
|
||||||
|
cls(config, user_password, app.config['HTTP_CLIENT'], app.app_context,
|
||||||
|
URLSchemeAndHostname(app.config['PREFERRED_URL_SCHEME'], app.config['SERVER_HOSTNAME']),
|
||||||
|
app.config.get('JWT_AUTH_MAX_FRESH_S', 300), app.config['REGISTRY_TITLE'], ip_resolver)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,6 @@ class BaseValidator(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Raises Exception if failure to validate. """
|
""" Raises Exception if failure to validate. """
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -6,13 +6,15 @@ class AccessSettingsValidator(BaseValidator):
|
||||||
name = "access"
|
name = "access"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
|
config = validator_context.config
|
||||||
|
client = validator_context.http_client
|
||||||
|
|
||||||
if not config.get('FEATURE_DIRECT_LOGIN', True):
|
if not config.get('FEATURE_DIRECT_LOGIN', True):
|
||||||
# Make sure we have at least one OIDC enabled.
|
# Make sure we have at least one OIDC enabled.
|
||||||
github_login = config.get('FEATURE_GITHUB_LOGIN', False)
|
github_login = config.get('FEATURE_GITHUB_LOGIN', False)
|
||||||
google_login = config.get('FEATURE_GOOGLE_LOGIN', False)
|
google_login = config.get('FEATURE_GOOGLE_LOGIN', False)
|
||||||
|
|
||||||
client = app.config['HTTPCLIENT']
|
|
||||||
login_manager = OAuthLoginManager(config, client=client)
|
login_manager = OAuthLoginManager(config, client=client)
|
||||||
custom_oidc = [s for s in login_manager.services if isinstance(s, OIDCLoginService)]
|
custom_oidc = [s for s in login_manager.services if isinstance(s, OIDCLoginService)]
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,9 @@ class ActionLogArchivingValidator(BaseValidator):
|
||||||
name = "actionlogarchiving"
|
name = "actionlogarchiving"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
|
config = validator_context.config
|
||||||
|
|
||||||
""" Validates the action log archiving configuration. """
|
""" Validates the action log archiving configuration. """
|
||||||
if not config.get('FEATURE_ACTION_LOG_ROTATION', False):
|
if not config.get('FEATURE_ACTION_LOG_ROTATION', False):
|
||||||
return
|
return
|
||||||
|
|
|
@ -4,7 +4,9 @@ class AppTokenAuthValidator(BaseValidator):
|
||||||
name = "apptoken-auth"
|
name = "apptoken-auth"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
|
config = validator_context.config
|
||||||
|
|
||||||
if config.get('AUTHENTICATION_TYPE', 'Database') != 'AppToken':
|
if config.get('AUTHENTICATION_TYPE', 'Database') != 'AppToken':
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
from bitbucket import BitBucket
|
from bitbucket import BitBucket
|
||||||
|
|
||||||
from util import get_app_url
|
from util import get_app_url_from_scheme_hostname
|
||||||
from util.config.validators import BaseValidator, ConfigValidationException
|
from util.config.validators import BaseValidator, ConfigValidationException
|
||||||
|
|
||||||
class BitbucketTriggerValidator(BaseValidator):
|
class BitbucketTriggerValidator(BaseValidator):
|
||||||
name = "bitbucket-trigger"
|
name = "bitbucket-trigger"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates the config for BitBucket. """
|
""" Validates the config for BitBucket. """
|
||||||
|
config = validator_context.config
|
||||||
|
|
||||||
trigger_config = config.get('BITBUCKET_TRIGGER_CONFIG')
|
trigger_config = config.get('BITBUCKET_TRIGGER_CONFIG')
|
||||||
if not trigger_config:
|
if not trigger_config:
|
||||||
raise ConfigValidationException('Missing client ID and client secret')
|
raise ConfigValidationException('Missing client ID and client secret')
|
||||||
|
@ -21,7 +23,7 @@ class BitbucketTriggerValidator(BaseValidator):
|
||||||
|
|
||||||
key = trigger_config['CONSUMER_KEY']
|
key = trigger_config['CONSUMER_KEY']
|
||||||
secret = trigger_config['CONSUMER_SECRET']
|
secret = trigger_config['CONSUMER_SECRET']
|
||||||
callback_url = '%s/oauth1/bitbucket/callback/trigger/' % (get_app_url(app.config))
|
callback_url = '%s/oauth1/bitbucket/callback/trigger/' % (get_app_url(validator_context.scheme_and_hostname))
|
||||||
|
|
||||||
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()
|
||||||
|
|
|
@ -7,8 +7,10 @@ class DatabaseValidator(BaseValidator):
|
||||||
name = "database"
|
name = "database"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates connecting to the database. """
|
""" Validates connecting to the database. """
|
||||||
|
config = validator_context.config
|
||||||
|
|
||||||
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 OperationalError as ex:
|
except OperationalError as ex:
|
||||||
|
|
|
@ -7,9 +7,14 @@ class EmailValidator(BaseValidator):
|
||||||
name = "mail"
|
name = "mail"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates sending email. """
|
""" Validates sending email. """
|
||||||
with app.app_context():
|
config = validator_context.config
|
||||||
|
user = validator_context.user
|
||||||
|
app_context = validator_context.context
|
||||||
|
registry_title = validator_context.registry_title
|
||||||
|
|
||||||
|
with app_context():
|
||||||
test_app = Flask("mail-test-app")
|
test_app = Flask("mail-test-app")
|
||||||
test_app.config.update(config)
|
test_app.config.update(config)
|
||||||
test_app.config.update({
|
test_app.config.update({
|
||||||
|
@ -18,7 +23,7 @@ class EmailValidator(BaseValidator):
|
||||||
})
|
})
|
||||||
|
|
||||||
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" % registry_title,
|
||||||
sender=config.get('MAIL_DEFAULT_SENDER'))
|
sender=config.get('MAIL_DEFAULT_SENDER'))
|
||||||
test_msg.add_recipient(user.email)
|
test_msg.add_recipient(user.email)
|
||||||
test_mail.send(test_msg)
|
test_mail.send(test_msg)
|
||||||
|
|
|
@ -6,8 +6,11 @@ class BaseGitHubValidator(BaseValidator):
|
||||||
config_key = None
|
config_key = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates the OAuth credentials and API endpoint for a Github service. """
|
""" Validates the OAuth credentials and API endpoint for a Github service. """
|
||||||
|
config = validator_context.config
|
||||||
|
client = validator_context.http_client
|
||||||
|
|
||||||
github_config = config.get(cls.config_key)
|
github_config = config.get(cls.config_key)
|
||||||
if not github_config:
|
if not github_config:
|
||||||
raise ConfigValidationException('Missing GitHub client id and client secret')
|
raise ConfigValidationException('Missing GitHub client id and client secret')
|
||||||
|
@ -29,9 +32,8 @@ class BaseGitHubValidator(BaseValidator):
|
||||||
raise ConfigValidationException('Organization restriction must have at least one allowed ' +
|
raise ConfigValidationException('Organization restriction must have at least one allowed ' +
|
||||||
'organization')
|
'organization')
|
||||||
|
|
||||||
client = app.config['HTTPCLIENT']
|
|
||||||
oauth = GithubOAuthService(config, cls.config_key)
|
oauth = GithubOAuthService(config, cls.config_key)
|
||||||
result = oauth.validate_client_id_and_secret(client, app.config)
|
result = oauth.validate_client_id_and_secret(client)
|
||||||
if not result:
|
if not result:
|
||||||
raise ConfigValidationException('Invalid client id or client secret')
|
raise ConfigValidationException('Invalid client id or client secret')
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
from oauth.services.gitlab import GitLabOAuthService
|
from oauth.services.gitlab import GitLabOAuthService
|
||||||
from util.config import URLSchemeAndHostname
|
|
||||||
from util.config.validators import BaseValidator, ConfigValidationException
|
from util.config.validators import BaseValidator, ConfigValidationException
|
||||||
|
|
||||||
class GitLabTriggerValidator(BaseValidator):
|
class GitLabTriggerValidator(BaseValidator):
|
||||||
name = "gitlab-trigger"
|
name = "gitlab-trigger"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates the OAuth credentials and API endpoint for a GitLab service. """
|
""" Validates the OAuth credentials and API endpoint for a GitLab service. """
|
||||||
|
config = validator_context.config
|
||||||
|
url_scheme_and_hostname = validator_context.url_scheme_and_hostname
|
||||||
|
client = validator_context.http_client
|
||||||
|
|
||||||
github_config = config.get('GITLAB_TRIGGER_CONFIG')
|
github_config = config.get('GITLAB_TRIGGER_CONFIG')
|
||||||
if not github_config:
|
if not github_config:
|
||||||
raise ConfigValidationException('Missing GitLab client id and client secret')
|
raise ConfigValidationException('Missing GitLab client id and client secret')
|
||||||
|
@ -23,9 +26,7 @@ class GitLabTriggerValidator(BaseValidator):
|
||||||
if not github_config.get('CLIENT_SECRET'):
|
if not github_config.get('CLIENT_SECRET'):
|
||||||
raise ConfigValidationException('Missing Client Secret')
|
raise ConfigValidationException('Missing Client Secret')
|
||||||
|
|
||||||
client = app.config['HTTPCLIENT']
|
|
||||||
oauth = GitLabOAuthService(config, 'GITLAB_TRIGGER_CONFIG')
|
oauth = GitLabOAuthService(config, 'GITLAB_TRIGGER_CONFIG')
|
||||||
url_scheme_and_hostname = URLSchemeAndHostname(app.config['PREFERRED_URL_SCHEME'], app.config['SERVER_HOSTNAME'])
|
|
||||||
result = oauth.validate_client_id_and_secret(client, url_scheme_and_hostname)
|
result = oauth.validate_client_id_and_secret(client, url_scheme_and_hostname)
|
||||||
if not result:
|
if not result:
|
||||||
raise ConfigValidationException('Invalid client id or client secret')
|
raise ConfigValidationException('Invalid client id or client secret')
|
||||||
|
|
|
@ -5,8 +5,11 @@ class GoogleLoginValidator(BaseValidator):
|
||||||
name = "google-login"
|
name = "google-login"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates the Google Login client ID and secret. """
|
""" Validates the Google Login client ID and secret. """
|
||||||
|
config = validator_context.config
|
||||||
|
client = validator_context.http_client
|
||||||
|
|
||||||
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 ConfigValidationException('Missing client ID and client secret')
|
raise ConfigValidationException('Missing client ID and client secret')
|
||||||
|
@ -17,8 +20,8 @@ class GoogleLoginValidator(BaseValidator):
|
||||||
if not google_login_config.get('CLIENT_SECRET'):
|
if not google_login_config.get('CLIENT_SECRET'):
|
||||||
raise ConfigValidationException('Missing Client Secret')
|
raise ConfigValidationException('Missing Client Secret')
|
||||||
|
|
||||||
client = app.config['HTTPCLIENT']
|
|
||||||
oauth = GoogleOAuthService(config, 'GOOGLE_LOGIN_CONFIG')
|
oauth = GoogleOAuthService(config, 'GOOGLE_LOGIN_CONFIG')
|
||||||
result = oauth.validate_client_id_and_secret(client, app.config)
|
# TODO(sam): the google oauth doesn't need the app config, but when refactoring pass in the URLSchemeandHostname
|
||||||
|
result = oauth.validate_client_id_and_secret(client)
|
||||||
if not result:
|
if not result:
|
||||||
raise ConfigValidationException('Invalid client id or client secret')
|
raise ConfigValidationException('Invalid client id or client secret')
|
||||||
|
|
|
@ -6,8 +6,14 @@ class JWTAuthValidator(BaseValidator):
|
||||||
name = "jwt"
|
name = "jwt"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app, public_key_path=None):
|
def validate(cls, validator_context, public_key_path=None):
|
||||||
""" Validates the JWT authentication system. """
|
""" Validates the JWT authentication system. """
|
||||||
|
config = validator_context.config
|
||||||
|
user = validator_context.user
|
||||||
|
user_password = validator_context.user_password
|
||||||
|
http_client = validator_context.http_client
|
||||||
|
jwt_auth_max = validator_context.jwt_auth_max
|
||||||
|
|
||||||
if config.get('AUTHENTICATION_TYPE', 'Database') != 'JWT':
|
if config.get('AUTHENTICATION_TYPE', 'Database') != 'JWT':
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -27,8 +33,8 @@ class JWTAuthValidator(BaseValidator):
|
||||||
# the key cannot be found.
|
# the key cannot be found.
|
||||||
users = ExternalJWTAuthN(verify_endpoint, query_endpoint, getuser_endpoint, issuer,
|
users = ExternalJWTAuthN(verify_endpoint, query_endpoint, getuser_endpoint, issuer,
|
||||||
OVERRIDE_CONFIG_DIRECTORY,
|
OVERRIDE_CONFIG_DIRECTORY,
|
||||||
app.config['HTTPCLIENT'],
|
http_client,
|
||||||
app.config.get('JWT_AUTH_MAX_FRESH_S', 300),
|
jwt_auth_max,
|
||||||
public_key_path=public_key_path,
|
public_key_path=public_key_path,
|
||||||
requires_email=config.get('FEATURE_MAILING', True))
|
requires_email=config.get('FEATURE_MAILING', True))
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,12 @@ class KeystoneValidator(BaseValidator):
|
||||||
name = "keystone"
|
name = "keystone"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates the Keystone authentication system. """
|
""" Validates the Keystone authentication system. """
|
||||||
|
config = validator_context.config
|
||||||
|
user = validator_context.user
|
||||||
|
user_password = validator_context.user_password
|
||||||
|
|
||||||
if config.get('AUTHENTICATION_TYPE', 'Database') != 'Keystone':
|
if config.get('AUTHENTICATION_TYPE', 'Database') != 'Keystone':
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,11 @@ class LDAPValidator(BaseValidator):
|
||||||
name = "ldap"
|
name = "ldap"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates the LDAP connection. """
|
""" Validates the LDAP connection. """
|
||||||
|
config = validator_context.config
|
||||||
|
user = validator_context.user
|
||||||
|
user_password = validator_context.user_password
|
||||||
if config.get('AUTHENTICATION_TYPE', 'Database') != 'LDAP':
|
if config.get('AUTHENTICATION_TYPE', 'Database') != 'LDAP':
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,10 @@ class OIDCLoginValidator(BaseValidator):
|
||||||
name = "oidc-login"
|
name = "oidc-login"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
client = app.config['HTTPCLIENT']
|
config = validator_context.config
|
||||||
|
client = validator_context.http_client
|
||||||
|
|
||||||
login_manager = OAuthLoginManager(config, client=client)
|
login_manager = OAuthLoginManager(config, client=client)
|
||||||
for service in login_manager.services:
|
for service in login_manager.services:
|
||||||
if not isinstance(service, OIDCLoginService):
|
if not isinstance(service, OIDCLoginService):
|
||||||
|
|
|
@ -6,8 +6,10 @@ class RedisValidator(BaseValidator):
|
||||||
name = "redis"
|
name = "redis"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates connecting to redis. """
|
""" Validates connecting to redis. """
|
||||||
|
config = validator_context.config
|
||||||
|
|
||||||
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 ConfigValidationException('Missing redis hostname')
|
raise ConfigValidationException('Missing redis hostname')
|
||||||
|
|
|
@ -8,13 +8,16 @@ class SecurityScannerValidator(BaseValidator):
|
||||||
name = "security-scanner"
|
name = "security-scanner"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates the configuration for talking to a Quay Security Scanner. """
|
""" Validates the configuration for talking to a Quay Security Scanner. """
|
||||||
|
config = validator_context.config
|
||||||
|
client = validator_context.http_client
|
||||||
|
app = None #TODO(sam) validate with joey's pr about security scanner api
|
||||||
|
|
||||||
if not config.get('FEATURE_SECURITY_SCANNER', False):
|
if not config.get('FEATURE_SECURITY_SCANNER', False):
|
||||||
return
|
return
|
||||||
|
|
||||||
client = app.config['HTTPCLIENT']
|
api = SecurityScannerAPI(app.config, config, None, client=client, skip_validation=True)
|
||||||
api = SecurityScannerAPI(app, config, None, client=client, skip_validation=True)
|
|
||||||
|
|
||||||
if not config.get('TESTING', False):
|
if not config.get('TESTING', False):
|
||||||
# Generate a temporary Quay key to use for signing the outgoing requests.
|
# Generate a temporary Quay key to use for signing the outgoing requests.
|
||||||
|
|
|
@ -8,8 +8,10 @@ class SignerValidator(BaseValidator):
|
||||||
name = "signer"
|
name = "signer"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates the GPG public+private key pair used for signing converted ACIs. """
|
""" Validates the GPG public+private key pair used for signing converted ACIs. """
|
||||||
|
config = validator_context.config
|
||||||
|
|
||||||
if config.get('SIGNING_ENGINE') is None:
|
if config.get('SIGNING_ENGINE') is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,9 @@ class SSLValidator(BaseValidator):
|
||||||
name = "ssl"
|
name = "ssl"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates the SSL configuration (if enabled). """
|
""" Validates the SSL configuration (if enabled). """
|
||||||
|
config = validator_context.config
|
||||||
|
|
||||||
# Skip if non-SSL.
|
# Skip if non-SSL.
|
||||||
if config.get('PREFERRED_URL_SCHEME', 'http') != 'https':
|
if config.get('PREFERRED_URL_SCHEME', 'http') != 'https':
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
from _init import config_provider
|
from _init import config_provider
|
||||||
from storage import get_storage_driver
|
from storage import get_storage_driver
|
||||||
from util.config.validators import BaseValidator, ConfigValidationException
|
from util.config.validators import BaseValidator, ConfigValidationException
|
||||||
from util.ipresolver import NoopIPResolver
|
|
||||||
|
|
||||||
ip_resolver = NoopIPResolver()
|
|
||||||
|
|
||||||
|
|
||||||
class StorageValidator(BaseValidator):
|
class StorageValidator(BaseValidator):
|
||||||
name = "registry-storage"
|
name = "registry-storage"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates registry storage. """
|
""" Validates registry storage. """
|
||||||
|
config = validator_context.config
|
||||||
|
client = validator_context.http_client
|
||||||
|
ip_resolver = validator_context.ip_resolver
|
||||||
|
|
||||||
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, ip_resolver).items()
|
||||||
if not providers:
|
if not providers:
|
||||||
raise ConfigValidationException('Storage configuration required')
|
raise ConfigValidationException('Storage configuration required')
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ class StorageValidator(BaseValidator):
|
||||||
'with storage replication')
|
'with storage replication')
|
||||||
|
|
||||||
# Run validation on the driver.
|
# Run validation on the driver.
|
||||||
driver.validate(app.config['HTTPCLIENT'])
|
driver.validate(client)
|
||||||
|
|
||||||
# Run setup on the driver if the read/write succeeded.
|
# Run setup on the driver if the read/write succeeded.
|
||||||
driver.setup()
|
driver.setup()
|
||||||
|
@ -34,7 +35,7 @@ class StorageValidator(BaseValidator):
|
||||||
raise ConfigValidationException('Invalid storage configuration: %s: %s' % (name, msg))
|
raise ConfigValidationException('Invalid storage configuration: %s: %s' % (name, msg))
|
||||||
|
|
||||||
|
|
||||||
def _get_storage_providers(config):
|
def _get_storage_providers(config, ip_resolver):
|
||||||
storage_config = config.get('DISTRIBUTED_STORAGE_CONFIG', {})
|
storage_config = config.get('DISTRIBUTED_STORAGE_CONFIG', {})
|
||||||
drivers = {}
|
drivers = {}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,9 @@ class TimeMachineValidator(BaseValidator):
|
||||||
name = "time-machine"
|
name = "time-machine"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
|
config = validator_context.config
|
||||||
|
|
||||||
if not 'DEFAULT_TAG_EXPIRATION' in config:
|
if not 'DEFAULT_TAG_EXPIRATION' in config:
|
||||||
# Old style config
|
# Old style config
|
||||||
return
|
return
|
||||||
|
|
|
@ -11,15 +11,16 @@ class BittorrentValidator(BaseValidator):
|
||||||
name = "bittorrent"
|
name = "bittorrent"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config, user, user_password, app):
|
def validate(cls, validator_context):
|
||||||
""" Validates the configuration for using BitTorrent for downloads. """
|
""" Validates the configuration for using BitTorrent for downloads. """
|
||||||
|
config = validator_context.config
|
||||||
|
client = validator_context.http_client
|
||||||
|
|
||||||
announce_url = config.get('BITTORRENT_ANNOUNCE_URL')
|
announce_url = config.get('BITTORRENT_ANNOUNCE_URL')
|
||||||
if not announce_url:
|
if not announce_url:
|
||||||
raise ConfigValidationException('Missing 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']
|
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'info_hash': sha1('test').digest(),
|
'info_hash': sha1('test').digest(),
|
||||||
'peer_id': '-QUAY00-6wfG2wk6wWLc',
|
'peer_id': '-QUAY00-6wfG2wk6wWLc',
|
||||||
|
|
|
@ -107,3 +107,12 @@ def get_priority_for_index(index):
|
||||||
return priority
|
return priority
|
||||||
|
|
||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
|
|
||||||
|
|
||||||
|
def create_url_from_app(app):
|
||||||
|
"""
|
||||||
|
Higher order function that returns a function that when called, will generate a url for that given app
|
||||||
|
:param app: Flask app
|
||||||
|
:return:
|
||||||
|
type: Flask -> (str -> url)
|
||||||
|
"""
|
||||||
|
|
|
@ -16,7 +16,7 @@ from util import get_app_url, slash_join
|
||||||
from util.abchelpers import nooper
|
from util.abchelpers import nooper
|
||||||
from util.failover import failover, FailoverException
|
from util.failover import failover, FailoverException
|
||||||
from util.secscan.validator import SecurityConfigValidator
|
from util.secscan.validator import SecurityConfigValidator
|
||||||
from util.security.instancekeys import InstanceKeys
|
from util.security.instancekeys import InstanceKeys, instance_keys_context_from_app_config
|
||||||
from util.security.registry_jwt import generate_bearer_token, build_context_and_subject
|
from util.security.registry_jwt import generate_bearer_token, build_context_and_subject
|
||||||
|
|
||||||
from _init import CONF_DIR
|
from _init import CONF_DIR
|
||||||
|
@ -150,10 +150,10 @@ class NoopSecurityScannerAPI(SecurityScannerAPIInterface):
|
||||||
|
|
||||||
class ImplementedSecurityScannerAPI(SecurityScannerAPIInterface):
|
class ImplementedSecurityScannerAPI(SecurityScannerAPIInterface):
|
||||||
""" Helper class for talking to the Security Scan service (Clair). """
|
""" Helper class for talking to the Security Scan service (Clair). """
|
||||||
def __init__(self, app, config, storage, client=None):
|
def __init__(self, app_config, config, storage, client=None):
|
||||||
self._app = app
|
self._app_config = app_config
|
||||||
self._config = config
|
self._config = config
|
||||||
self._instance_keys = InstanceKeys(app)
|
self._instance_keys = InstanceKeys(instance_keys_context_from_app_config(app_config))
|
||||||
self._client = client or config['HTTPCLIENT']
|
self._client = client or config['HTTPCLIENT']
|
||||||
self._storage = storage
|
self._storage = storage
|
||||||
self._default_storage_locations = config['DISTRIBUTED_STORAGE_PREFERENCE']
|
self._default_storage_locations = config['DISTRIBUTED_STORAGE_PREFERENCE']
|
||||||
|
@ -183,7 +183,7 @@ class ImplementedSecurityScannerAPI(SecurityScannerAPIInterface):
|
||||||
repository_and_namespace = '/'.join([namespace_name, repo_name])
|
repository_and_namespace = '/'.join([namespace_name, repo_name])
|
||||||
|
|
||||||
# Generate the JWT which will authorize this
|
# Generate the JWT which will authorize this
|
||||||
audience = self._app.config['SERVER_HOSTNAME']
|
audience = self._app_config['SERVER_HOSTNAME']
|
||||||
context, subject = build_context_and_subject()
|
context, subject = build_context_and_subject()
|
||||||
access = [{
|
access = [{
|
||||||
'type': 'repository',
|
'type': 'repository',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from collections import namedtuple
|
||||||
from cachetools import lru_cache
|
from cachetools import lru_cache
|
||||||
from data import model
|
from data import model
|
||||||
from util.expiresdict import ExpiresDict, ExpiresEntry
|
from util.expiresdict import ExpiresDict, ExpiresEntry
|
||||||
|
@ -25,9 +26,10 @@ class InstanceKeys(object):
|
||||||
""" InstanceKeys defines a helper class for interacting with the Quay instance service keys
|
""" InstanceKeys defines a helper class for interacting with the Quay instance service keys
|
||||||
used for JWT signing of registry tokens as well as requests from Quay to other services
|
used for JWT signing of registry tokens as well as requests from Quay to other services
|
||||||
such as Clair. Each container will have a single registered instance key.
|
such as Clair. Each container will have a single registered instance key.
|
||||||
|
:param keys_context: InstanceKeysContext
|
||||||
"""
|
"""
|
||||||
def __init__(self, app):
|
def __init__(self, keys_context):
|
||||||
self.app = app
|
self.keys_context = keys_context
|
||||||
self.instance_keys = ExpiresDict(self._load_instance_keys)
|
self.instance_keys = ExpiresDict(self._load_instance_keys)
|
||||||
|
|
||||||
def clear_cache(self):
|
def clear_cache(self):
|
||||||
|
@ -45,24 +47,24 @@ class InstanceKeys(object):
|
||||||
@property
|
@property
|
||||||
def service_name(self):
|
def service_name(self):
|
||||||
""" Returns the name of the instance key's service (i.e. 'quay'). """
|
""" Returns the name of the instance key's service (i.e. 'quay'). """
|
||||||
return self.app.config['INSTANCE_SERVICE_KEY_SERVICE']
|
return self.keys_context.instance_key_service
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def service_key_expiration(self):
|
def service_key_expiration(self):
|
||||||
""" Returns the defined expiration for instance service keys, in minutes. """
|
""" Returns the defined expiration for instance service keys, in minutes. """
|
||||||
return self.app.config.get('INSTANCE_SERVICE_KEY_EXPIRATION', 120)
|
return self.keys_context.service_key_expiration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def local_key_id(self):
|
def local_key_id(self):
|
||||||
""" Returns the ID of the local instance service key. """
|
""" Returns the ID of the local instance service key. """
|
||||||
return _load_file_contents(self.app.config['INSTANCE_SERVICE_KEY_KID_LOCATION'])
|
return _load_file_contents(self.keys_context.service_key_kid_location)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def local_private_key(self):
|
def local_private_key(self):
|
||||||
""" Returns the private key of the local instance service key. """
|
""" Returns the private key of the local instance service key. """
|
||||||
return _load_file_contents(self.app.config['INSTANCE_SERVICE_KEY_LOCATION'])
|
return _load_file_contents(self.keys_context.service_key_location)
|
||||||
|
|
||||||
def get_service_key_public_key(self, kid):
|
def get_service_key_public_key(self, kid):
|
||||||
""" Returns the public key associated with the given instance service key or None if none. """
|
""" Returns the public key associated with the given instance service key or None if none. """
|
||||||
|
@ -77,3 +79,15 @@ def _load_file_contents(path):
|
||||||
""" Returns the contents of the specified file path. """
|
""" Returns the contents of the specified file path. """
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
InstanceKeysContext = namedtuple('InstanceKeysContext', ['instance_key_service',
|
||||||
|
'service_key_expiration',
|
||||||
|
'service_key_kid_location',
|
||||||
|
'service_key_location'])
|
||||||
|
|
||||||
|
def instance_keys_context_from_app_config(app_config):
|
||||||
|
return InstanceKeysContext(app_config['INSTANCE_SERVICE_KEY_SERVICE'],
|
||||||
|
app_config.get('INSTANCE_SERVICE_KEY_EXPIRATION', 120),
|
||||||
|
app_config['INSTANCE_SERVICE_KEY_KID_LOCATION'],
|
||||||
|
app_config['INSTANCE_SERVICE_KEY_LOCATION'])
|
||||||
|
|
|
@ -11,7 +11,7 @@ import requests
|
||||||
from data.database import CloseForLongOperation
|
from data.database import CloseForLongOperation
|
||||||
from util.abchelpers import nooper
|
from util.abchelpers import nooper
|
||||||
from util.failover import failover, FailoverException
|
from util.failover import failover, FailoverException
|
||||||
from util.security.instancekeys import InstanceKeys
|
from util.security.instancekeys import InstanceKeys, instance_keys_context_from_app_config
|
||||||
from util.security.registry_jwt import (build_context_and_subject, generate_bearer_token,
|
from util.security.registry_jwt import (build_context_and_subject, generate_bearer_token,
|
||||||
SIGNER_TUF_ROOT)
|
SIGNER_TUF_ROOT)
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ class NoopTUFMetadataAPI(TUFMetadataAPIInterface):
|
||||||
class ImplementedTUFMetadataAPI(TUFMetadataAPIInterface):
|
class ImplementedTUFMetadataAPI(TUFMetadataAPIInterface):
|
||||||
def __init__(self, app, config, client=None):
|
def __init__(self, app, config, client=None):
|
||||||
self._app = app
|
self._app = app
|
||||||
self._instance_keys = InstanceKeys(app)
|
self._instance_keys = InstanceKeys(instance_keys_context_from_app_config(app.config))
|
||||||
self._config = config
|
self._config = config
|
||||||
self._client = client or config['HTTPCLIENT']
|
self._client = client or config['HTTPCLIENT']
|
||||||
self._gun_prefix = config['TUF_GUN_PREFIX'] or config['SERVER_HOSTNAME']
|
self._gun_prefix = config['TUF_GUN_PREFIX'] or config['SERVER_HOSTNAME']
|
||||||
|
|
Reference in a new issue