From b86d389c8ea6eb3a95e9dfbf70cf6f5b60c29628 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 5 Mar 2019 16:50:56 -0500 Subject: [PATCH] Add ability to restrict V1 push behind a namespace whitelist Also enables the feature by default with an empty whitelist for QE Fixes https://jira.coreos.com/browse/QUAY-1342 --- config.py | 4 +++ config_app/config_endpoints/api/suconfig.py | 5 +-- .../core-config-setup/config-setup-tool.html | 30 ++++++++++++++++ endpoints/v1/__init__.py | 36 ++++++++++++++++++- endpoints/v1/index.py | 6 +++- endpoints/v1/registry.py | 5 ++- endpoints/v1/tag.py | 4 ++- util/config/configutil.py | 1 + util/config/schema.py | 15 +++++++- 9 files changed, 99 insertions(+), 7 deletions(-) diff --git a/config.py b/config.py index 15395fcb4..7a36e35f0 100644 --- a/config.py +++ b/config.py @@ -236,6 +236,10 @@ class DefaultConfig(ImmutableConfig): # Documentation: http://pythonhosted.org/semantic_version/reference.html#semantic_version.Spec BLACKLIST_V2_SPEC = '<1.6.0' + # Feature Flag: Whether to restrict V1 pushes to the whitelist. + FEATURE_RESTRICTED_V1_PUSH = False + V1_PUSH_WHITELIST = [] + # Feature Flag: Whether or not to rotate old action logs to storage. FEATURE_ACTION_LOG_ROTATION = False diff --git a/config_app/config_endpoints/api/suconfig.py b/config_app/config_endpoints/api/suconfig.py index 6e13ce638..7c9f18c09 100644 --- a/config_app/config_endpoints/api/suconfig.py +++ b/config_app/config_endpoints/api/suconfig.py @@ -89,7 +89,6 @@ class SuperUserRegistryStatus(ApiResource): @nickname('scRegistryStatus') def get(self): """ Returns the status of the registry. """ - # If there is no config file, we need to setup the database. if not config_provider.config_exists(): return { @@ -104,7 +103,9 @@ class SuperUserRegistryStatus(ApiResource): config = config_provider.get_config() if config and config['SETUP_COMPLETE']: - return 'config' + return { + 'status': 'config' + } return { 'status': 'create-superuser' if not database_has_users() else 'config' diff --git a/config_app/js/core-config-setup/config-setup-tool.html b/config_app/js/core-config-setup/config-setup-tool.html index 58d9894b6..269bc191d 100644 --- a/config_app/js/core-config-setup/config-setup-tool.html +++ b/config_app/js/core-config-setup/config-setup-tool.html @@ -1419,6 +1419,36 @@ + +
+
+ Registry Protocol Settings +
+
+
+ Docker V1 protocol support has been officially deprecated by Quay and support will be + removed in the next major version. It is strongly suggested to have this + flag enabled and to restrict access to V1 push. +
+
+ Restrict V1 Push Support +
+
+ If enabled, Docker V1 push protocol will only be supported by those namespaces whitelisted + below. This feature should be left on unless general usage of the older + Docker V1 protocol is necessary. +
+
+ Namespace whitelist: + +
+ The list of namespaces in which V1 push is still enabled. +
+
+
+
+
diff --git a/endpoints/v1/__init__.py b/endpoints/v1/__init__.py index f3ea259ff..b4f97e7e1 100644 --- a/endpoints/v1/__init__.py +++ b/endpoints/v1/__init__.py @@ -1,12 +1,20 @@ +import logging + +from functools import wraps + from flask import Blueprint, make_response -from app import metric_queue +import features + +from app import metric_queue, app from endpoints.decorators import anon_protect, anon_allowed from util.metrics.metricqueue import time_blueprint +from util.http import abort v1_bp = Blueprint('v1', __name__) time_blueprint(v1_bp, metric_queue) +logger = logging.getLogger(__name__) # Note: This is *not* part of the Docker index spec. This is here for our own health check, # since we have nginx handle the _ping below. @@ -26,6 +34,32 @@ def ping(): return response +def check_v1_push_enabled(namespace_name_kwarg='namespace_name'): + """ Decorator which checks if V1 push is enabled for the current namespace. The first argument + to the wrapped function must be the namespace name or there must be a kwarg with the + name `namespace_name`. + """ + def wrapper(wrapped): + @wraps(wrapped) + def decorated(*args, **kwargs): + if namespace_name_kwarg in kwargs: + namespace_name = kwargs[namespace_name_kwarg] + else: + namespace_name = args[0] + + if features.RESTRICTED_V1_PUSH: + whitelist = app.config.get('V1_PUSH_WHITELIST') or [] + logger.debug('V1 push is restricted to whitelist: %s', whitelist) + if namespace_name not in whitelist: + abort(405, + message=('V1 push support has been deprecated. To enable for this ' + + 'namespace, please contact support.')) + + return wrapped(*args, **kwargs) + return decorated + return wrapper + + from endpoints.v1 import ( index, registry, diff --git a/endpoints/v1/index.py b/endpoints/v1/index.py index 0c0d52945..f8655e76d 100644 --- a/endpoints/v1/index.py +++ b/endpoints/v1/index.py @@ -18,7 +18,7 @@ from data import model from data.registry_model import registry_model from data.registry_model.manifestbuilder import create_manifest_builder, lookup_manifest_builder from endpoints.decorators import anon_protect, anon_allowed, parse_repository_name -from endpoints.v1 import v1_bp +from endpoints.v1 import v1_bp, check_v1_push_enabled from notifications import spawn_notification from util.audit import track_and_log from util.http import abort @@ -165,6 +165,7 @@ def update_user(username): @v1_bp.route('/repositories//', methods=['PUT']) @process_auth @parse_repository_name() +@check_v1_push_enabled() @ensure_namespace_enabled @generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201) @anon_allowed @@ -229,6 +230,7 @@ def create_repository(namespace_name, repo_name): @v1_bp.route('/repositories//images', methods=['PUT']) @process_auth @parse_repository_name() +@check_v1_push_enabled() @ensure_namespace_enabled @generate_headers(scope=GrantType.WRITE_REPOSITORY) @anon_allowed @@ -295,6 +297,7 @@ def get_repository_images(namespace_name, repo_name): @v1_bp.route('/repositories//images', methods=['DELETE']) @process_auth @parse_repository_name() +@check_v1_push_enabled() @ensure_namespace_enabled @generate_headers(scope=GrantType.WRITE_REPOSITORY) @anon_allowed @@ -304,6 +307,7 @@ def delete_repository_images(namespace_name, repo_name): @v1_bp.route('/repositories//auth', methods=['PUT']) @parse_repository_name() +@check_v1_push_enabled() @ensure_namespace_enabled @anon_allowed def put_repository_auth(namespace_name, repo_name): diff --git a/endpoints/v1/registry.py b/endpoints/v1/registry.py index 0a75bff4a..551ba33eb 100644 --- a/endpoints/v1/registry.py +++ b/endpoints/v1/registry.py @@ -16,7 +16,7 @@ from data.registry_model import registry_model from data.registry_model.blobuploader import upload_blob, BlobUploadSettings, BlobUploadException from data.registry_model.manifestbuilder import lookup_manifest_builder from digest import checksums -from endpoints.v1 import v1_bp +from endpoints.v1 import v1_bp, check_v1_push_enabled from endpoints.v1.index import ensure_namespace_enabled from endpoints.decorators import anon_protect, check_region_blacklisted from util.http import abort, exact_abort @@ -149,6 +149,7 @@ def get_image_layer(namespace, repository, image_id, headers): @v1_bp.route('/images//layer', methods=['PUT']) @process_auth @extract_namespace_repo_from_session +@check_v1_push_enabled() @ensure_namespace_enabled @anon_protect def put_image_layer(namespace, repository, image_id): @@ -240,6 +241,7 @@ def put_image_layer(namespace, repository, image_id): @v1_bp.route('/images//checksum', methods=['PUT']) @process_auth @extract_namespace_repo_from_session +@check_v1_push_enabled() @ensure_namespace_enabled @anon_protect def put_image_checksum(namespace, repository, image_id): @@ -345,6 +347,7 @@ def get_image_ancestry(namespace, repository, image_id, headers): @v1_bp.route('/images//json', methods=['PUT']) @process_auth @extract_namespace_repo_from_session +@check_v1_push_enabled() @ensure_namespace_enabled @anon_protect def put_image_json(namespace, repository, image_id): diff --git a/endpoints/v1/tag.py b/endpoints/v1/tag.py index 61afaf6b5..bfe35f342 100644 --- a/endpoints/v1/tag.py +++ b/endpoints/v1/tag.py @@ -9,7 +9,7 @@ from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermissi from data.registry_model import registry_model from data.registry_model.manifestbuilder import lookup_manifest_builder from endpoints.decorators import anon_protect, parse_repository_name -from endpoints.v1 import v1_bp +from endpoints.v1 import v1_bp, check_v1_push_enabled from util.audit import track_and_log from util.names import TAG_ERROR, TAG_REGEX @@ -59,6 +59,7 @@ def get_tag(namespace_name, repo_name, tag): @process_auth @anon_protect @parse_repository_name() +@check_v1_push_enabled() def put_tag(namespace_name, repo_name, tag): permission = ModifyRepositoryPermission(namespace_name, repo_name) repository_ref = registry_model.lookup_repository(namespace_name, repo_name, kind_filter='image') @@ -98,6 +99,7 @@ def put_tag(namespace_name, repo_name, tag): @process_auth @anon_protect @parse_repository_name() +@check_v1_push_enabled() def delete_tag(namespace_name, repo_name, tag): permission = ModifyRepositoryPermission(namespace_name, repo_name) repository_ref = registry_model.lookup_repository(namespace_name, repo_name, kind_filter='image') diff --git a/util/config/configutil.py b/util/config/configutil.py index c9c037c11..555c74066 100644 --- a/util/config/configutil.py +++ b/util/config/configutil.py @@ -23,6 +23,7 @@ def add_enterprise_config_defaults(config_obj, current_secret_key): config_obj['FEATURE_APP_SPECIFIC_TOKENS'] = config_obj.get('FEATURE_APP_SPECIFIC_TOKENS', True) config_obj['FEATURE_PARTIAL_USER_AUTOCOMPLETE'] = config_obj.get('FEATURE_PARTIAL_USER_AUTOCOMPLETE', True) config_obj['FEATURE_USERNAME_CONFIRMATION'] = config_obj.get('FEATURE_USERNAME_CONFIRMATION', True) + config_obj['FEATURE_RESTRICTED_V1_PUSH'] = config_obj.get('FEATURE_RESTRICTED_V1_PUSH', True) # Default features that are off. config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False) diff --git a/util/config/schema.py b/util/config/schema.py index 8b6937aa7..24d127757 100644 --- a/util/config/schema.py +++ b/util/config/schema.py @@ -955,6 +955,19 @@ CONFIG_SCHEMA = { 'description': 'If set to true, users can confirm their generated usernames. Defaults to True', 'x-example': False, }, + + # Feature Flag: V1 push restriction. + 'FEATURE_RESTRICTED_V1_PUSH': { + 'type': 'boolean', + 'description': 'If set to true, only namespaces listed in V1_PUSH_WHITELIST support V1 push. Defaults to True', + 'x-example': False, + }, + + # Feature Flag: V1 push restriction. + 'V1_PUSH_WHITELIST': { + 'type': 'array', + 'description': 'The array of namespace names that support V1 push if FEATURE_RESTRICTED_V1_PUSH is set to true.', + 'x-example': ['some', 'namespaces'], + }, }, } -