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
This commit is contained in:
		
							parent
							
								
									d3dd2f7b7c
								
							
						
					
					
						commit
						b86d389c8e
					
				
					 9 changed files with 99 additions and 7 deletions
				
			
		|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -1419,6 +1419,36 @@ | |||
|       </div> | ||||
|     </div> <!-- /Access settings --> | ||||
| 
 | ||||
|     <!-- Registry settings --> | ||||
|     <div class="co-panel"> | ||||
|       <div class="co-panel-heading"> | ||||
|         <i class="fas fa-cog"></i> Registry Protocol Settings | ||||
|       </div> | ||||
|       <div class="co-panel-body"> | ||||
|         <div class="co-alert co-alert-warning" style="margin-bottom: 20px"> | ||||
|           Docker V1 protocol support has been <strong>officially deprecated</strong> by Quay and support will be | ||||
|           removed in the next major version. It is <strongly>strongly</strongly> suggested to have this | ||||
|           flag enabled and to restrict access to V1 push. | ||||
|         </div> | ||||
|         <div class="config-bool-field" binding="config.FEATURE_RESTRICTED_V1_PUSH"> | ||||
|           Restrict V1 Push Support | ||||
|         </div> | ||||
|         <div class="help-text"> | ||||
|           If enabled, Docker V1 push protocol will only be supported by those namespaces whitelisted | ||||
|           below. This feature should be left on unless <strong>general usage</strong> of the older | ||||
|           Docker V1 protocol is necessary. | ||||
|         </div> | ||||
|         <div ng-if="config.FEATURE_RESTRICTED_V1_PUSH" style="margin-top: 20px;"> | ||||
|           <strong>Namespace whitelist:</strong> | ||||
|           <span class="config-list-field" item-title="Namespace" binding="config.V1_PUSH_WHITELIST" | ||||
|                 item-pattern="[a-z0-9-]"></span> | ||||
|           <div class="help-text"> | ||||
|             The list of namespaces in which V1 push is still enabled. | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Build Support --> | ||||
|     <div class="co-panel"> | ||||
|       <div class="co-panel-heading"> | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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/<repopath:repository>/', 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/<repopath:repository>/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/<repopath:repository>/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/<repopath:repository>/auth', methods=['PUT']) | ||||
| @parse_repository_name() | ||||
| @check_v1_push_enabled() | ||||
| @ensure_namespace_enabled | ||||
| @anon_allowed | ||||
| def put_repository_auth(namespace_name, repo_name): | ||||
|  |  | |||
|  | @ -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/<image_id>/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/<image_id>/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/<image_id>/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): | ||||
|  |  | |||
|  | @ -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') | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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'], | ||||
|     }, | ||||
|  }, | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Reference in a new issue