Merge pull request #3398 from quay/joseph.schorr/QUAY-1342/v1-push-flag
Add ability to restrict V1 push behind a namespace whitelist
This commit is contained in:
commit
1f7b614905
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
|
# Documentation: http://pythonhosted.org/semantic_version/reference.html#semantic_version.Spec
|
||||||
BLACKLIST_V2_SPEC = '<1.6.0'
|
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 Flag: Whether or not to rotate old action logs to storage.
|
||||||
FEATURE_ACTION_LOG_ROTATION = False
|
FEATURE_ACTION_LOG_ROTATION = False
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,6 @@ class SuperUserRegistryStatus(ApiResource):
|
||||||
@nickname('scRegistryStatus')
|
@nickname('scRegistryStatus')
|
||||||
def get(self):
|
def get(self):
|
||||||
""" Returns the status of the registry. """
|
""" Returns the status of the registry. """
|
||||||
|
|
||||||
# If there is no config file, we need to setup the database.
|
# If there is no config file, we need to setup the database.
|
||||||
if not config_provider.config_exists():
|
if not config_provider.config_exists():
|
||||||
return {
|
return {
|
||||||
|
@ -104,7 +103,9 @@ class SuperUserRegistryStatus(ApiResource):
|
||||||
|
|
||||||
config = config_provider.get_config()
|
config = config_provider.get_config()
|
||||||
if config and config['SETUP_COMPLETE']:
|
if config and config['SETUP_COMPLETE']:
|
||||||
return 'config'
|
return {
|
||||||
|
'status': 'config'
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'status': 'create-superuser' if not database_has_users() else 'config'
|
'status': 'create-superuser' if not database_has_users() else 'config'
|
||||||
|
|
|
@ -1419,6 +1419,36 @@
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- /Access settings -->
|
</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 -->
|
<!-- Build Support -->
|
||||||
<div class="co-panel">
|
<div class="co-panel">
|
||||||
<div class="co-panel-heading">
|
<div class="co-panel-heading">
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from flask import Blueprint, make_response
|
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 endpoints.decorators import anon_protect, anon_allowed
|
||||||
from util.metrics.metricqueue import time_blueprint
|
from util.metrics.metricqueue import time_blueprint
|
||||||
|
from util.http import abort
|
||||||
|
|
||||||
v1_bp = Blueprint('v1', __name__)
|
v1_bp = Blueprint('v1', __name__)
|
||||||
time_blueprint(v1_bp, metric_queue)
|
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,
|
# 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.
|
# since we have nginx handle the _ping below.
|
||||||
|
@ -26,6 +34,32 @@ def ping():
|
||||||
return response
|
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 (
|
from endpoints.v1 import (
|
||||||
index,
|
index,
|
||||||
registry,
|
registry,
|
||||||
|
|
|
@ -18,7 +18,7 @@ from data import model
|
||||||
from data.registry_model import registry_model
|
from data.registry_model import registry_model
|
||||||
from data.registry_model.manifestbuilder import create_manifest_builder, lookup_manifest_builder
|
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.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 notifications import spawn_notification
|
||||||
from util.audit import track_and_log
|
from util.audit import track_and_log
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
|
@ -165,6 +165,7 @@ def update_user(username):
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/', methods=['PUT'])
|
@v1_bp.route('/repositories/<repopath:repository>/', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name()
|
@parse_repository_name()
|
||||||
|
@check_v1_push_enabled()
|
||||||
@ensure_namespace_enabled
|
@ensure_namespace_enabled
|
||||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201)
|
@generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201)
|
||||||
@anon_allowed
|
@anon_allowed
|
||||||
|
@ -229,6 +230,7 @@ def create_repository(namespace_name, repo_name):
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['PUT'])
|
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name()
|
@parse_repository_name()
|
||||||
|
@check_v1_push_enabled()
|
||||||
@ensure_namespace_enabled
|
@ensure_namespace_enabled
|
||||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||||
@anon_allowed
|
@anon_allowed
|
||||||
|
@ -295,6 +297,7 @@ def get_repository_images(namespace_name, repo_name):
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['DELETE'])
|
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['DELETE'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name()
|
@parse_repository_name()
|
||||||
|
@check_v1_push_enabled()
|
||||||
@ensure_namespace_enabled
|
@ensure_namespace_enabled
|
||||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||||
@anon_allowed
|
@anon_allowed
|
||||||
|
@ -304,6 +307,7 @@ def delete_repository_images(namespace_name, repo_name):
|
||||||
|
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/auth', methods=['PUT'])
|
@v1_bp.route('/repositories/<repopath:repository>/auth', methods=['PUT'])
|
||||||
@parse_repository_name()
|
@parse_repository_name()
|
||||||
|
@check_v1_push_enabled()
|
||||||
@ensure_namespace_enabled
|
@ensure_namespace_enabled
|
||||||
@anon_allowed
|
@anon_allowed
|
||||||
def put_repository_auth(namespace_name, repo_name):
|
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.blobuploader import upload_blob, BlobUploadSettings, BlobUploadException
|
||||||
from data.registry_model.manifestbuilder import lookup_manifest_builder
|
from data.registry_model.manifestbuilder import lookup_manifest_builder
|
||||||
from digest import checksums
|
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.v1.index import ensure_namespace_enabled
|
||||||
from endpoints.decorators import anon_protect, check_region_blacklisted
|
from endpoints.decorators import anon_protect, check_region_blacklisted
|
||||||
from util.http import abort, exact_abort
|
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'])
|
@v1_bp.route('/images/<image_id>/layer', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
|
@check_v1_push_enabled()
|
||||||
@ensure_namespace_enabled
|
@ensure_namespace_enabled
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def put_image_layer(namespace, repository, image_id):
|
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'])
|
@v1_bp.route('/images/<image_id>/checksum', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
|
@check_v1_push_enabled()
|
||||||
@ensure_namespace_enabled
|
@ensure_namespace_enabled
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def put_image_checksum(namespace, repository, image_id):
|
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'])
|
@v1_bp.route('/images/<image_id>/json', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
|
@check_v1_push_enabled()
|
||||||
@ensure_namespace_enabled
|
@ensure_namespace_enabled
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def put_image_json(namespace, repository, image_id):
|
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 import registry_model
|
||||||
from data.registry_model.manifestbuilder import lookup_manifest_builder
|
from data.registry_model.manifestbuilder import lookup_manifest_builder
|
||||||
from endpoints.decorators import anon_protect, parse_repository_name
|
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.audit import track_and_log
|
||||||
from util.names import TAG_ERROR, TAG_REGEX
|
from util.names import TAG_ERROR, TAG_REGEX
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ def get_tag(namespace_name, repo_name, tag):
|
||||||
@process_auth
|
@process_auth
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@parse_repository_name()
|
@parse_repository_name()
|
||||||
|
@check_v1_push_enabled()
|
||||||
def put_tag(namespace_name, repo_name, tag):
|
def put_tag(namespace_name, repo_name, tag):
|
||||||
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
||||||
repository_ref = registry_model.lookup_repository(namespace_name, repo_name, kind_filter='image')
|
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
|
@process_auth
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@parse_repository_name()
|
@parse_repository_name()
|
||||||
|
@check_v1_push_enabled()
|
||||||
def delete_tag(namespace_name, repo_name, tag):
|
def delete_tag(namespace_name, repo_name, tag):
|
||||||
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
||||||
repository_ref = registry_model.lookup_repository(namespace_name, repo_name, kind_filter='image')
|
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_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_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_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.
|
# Default features that are off.
|
||||||
config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False)
|
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',
|
'description': 'If set to true, users can confirm their generated usernames. Defaults to True',
|
||||||
'x-example': False,
|
'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