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 @@
+
+
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'],
+ },
},
}
-