Add ability for specific geographic regions to be blocked from pulling images within a namespace
This commit is contained in:
parent
c71a43a06c
commit
c3710a6a5e
20 changed files with 257 additions and 37 deletions
|
@ -18,7 +18,7 @@ from endpoints.appr import appr_bp, require_app_repo_read, require_app_repo_writ
|
|||
from endpoints.appr.cnr_backend import Blob, Channel, Package, User
|
||||
from endpoints.appr.decorators import disallow_for_image_repository
|
||||
from endpoints.appr.models_cnr import model
|
||||
from endpoints.decorators import anon_allowed, anon_protect
|
||||
from endpoints.decorators import anon_allowed, anon_protect, check_region_blacklisted
|
||||
from util.names import REPOSITORY_NAME_REGEX, TAG_REGEX
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -71,6 +71,7 @@ def login():
|
|||
strict_slashes=False,)
|
||||
@process_auth
|
||||
@require_app_repo_read
|
||||
@check_region_blacklisted(namespace_name_kwarg='namespace')
|
||||
@anon_protect
|
||||
def blobs(namespace, package_name, digest):
|
||||
reponame = repo_name(namespace, package_name)
|
||||
|
@ -114,6 +115,7 @@ def delete_package(namespace, package_name, release, media_type):
|
|||
methods=['GET'], strict_slashes=False)
|
||||
@process_auth
|
||||
@require_app_repo_read
|
||||
@check_region_blacklisted(namespace_name_kwarg='namespace')
|
||||
@anon_protect
|
||||
def show_package(namespace, package_name, release, media_type):
|
||||
reponame = repo_name(namespace, package_name)
|
||||
|
@ -152,6 +154,7 @@ def show_package_release_manifests(namespace, package_name, release):
|
|||
strict_slashes=False,)
|
||||
@process_auth
|
||||
@require_app_repo_read
|
||||
@check_region_blacklisted(namespace_name_kwarg='namespace')
|
||||
@anon_protect
|
||||
def pull(namespace, package_name, release, media_type):
|
||||
logger.debug('Pull of release %s of app repository %s/%s', release, namespace, package_name)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
""" Various decorators for endpoint and API handlers. """
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from functools import wraps
|
||||
|
@ -7,8 +8,9 @@ from flask import abort, request, make_response
|
|||
|
||||
import features
|
||||
|
||||
from app import app
|
||||
from app import app, ip_resolver, model_cache
|
||||
from auth.auth_context import get_authenticated_context
|
||||
from data.registry_model import registry_model
|
||||
from util.names import parse_namespace_repository, ImplicitLibraryNamespaceNotAllowed
|
||||
from util.http import abort
|
||||
|
||||
|
@ -122,3 +124,40 @@ def require_xhr_from_browser(func):
|
|||
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def check_region_blacklisted(error_class=None, namespace_name_kwarg=None):
|
||||
""" Decorator which checks if the incoming request is from a region geo IP blocked
|
||||
for the current namespace. The first argument to the wrapped function must be
|
||||
the namespace name.
|
||||
"""
|
||||
def wrapper(wrapped):
|
||||
@wraps(wrapped)
|
||||
def decorated(*args, **kwargs):
|
||||
if namespace_name_kwarg:
|
||||
namespace_name = kwargs[namespace_name_kwarg]
|
||||
else:
|
||||
namespace_name = args[0]
|
||||
|
||||
region_blacklist = registry_model.get_cached_namespace_region_blacklist(model_cache,
|
||||
namespace_name)
|
||||
if region_blacklist:
|
||||
# Resolve the IP information and block if on the namespace's blacklist.
|
||||
remote_addr = request.remote_addr
|
||||
if os.getenv('TEST', 'false').lower() == 'true':
|
||||
remote_addr = request.headers.get('X-Override-Remote-Addr-For-Testing', remote_addr)
|
||||
|
||||
resolved_ip_info = ip_resolver.resolve_ip(remote_addr)
|
||||
logger.debug('Resolved IP information for IP %s: %s', remote_addr, resolved_ip_info)
|
||||
|
||||
if (resolved_ip_info and
|
||||
resolved_ip_info.country_iso_code and
|
||||
resolved_ip_info.country_iso_code in region_blacklist):
|
||||
if error_class:
|
||||
raise error_class()
|
||||
|
||||
abort(403, 'Pulls of this data have been restricted geographically')
|
||||
|
||||
return wrapped(*args, **kwargs)
|
||||
return decorated
|
||||
return wrapper
|
||||
|
|
|
@ -18,7 +18,7 @@ from data.registry_model.manifestbuilder import lookup_manifest_builder
|
|||
from digest import checksums
|
||||
from endpoints.v1 import v1_bp
|
||||
from endpoints.v1.index import ensure_namespace_enabled
|
||||
from endpoints.decorators import anon_protect
|
||||
from endpoints.decorators import anon_protect, check_region_blacklisted
|
||||
from util.http import abort, exact_abort
|
||||
from util.registry.replication import queue_storage_replication
|
||||
|
||||
|
@ -109,6 +109,7 @@ def head_image_layer(namespace, repository, image_id, headers):
|
|||
@ensure_namespace_enabled
|
||||
@require_completion
|
||||
@set_cache_headers
|
||||
@check_region_blacklisted()
|
||||
@anon_protect
|
||||
def get_image_layer(namespace, repository, image_id, headers):
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
|
|
|
@ -13,11 +13,11 @@ from data.registry_model.blobuploader import (create_blob_upload, retrieve_blob_
|
|||
BlobUploadException, BlobTooLargeException,
|
||||
BlobRangeMismatchException)
|
||||
from digest import digest_tools
|
||||
from endpoints.decorators import anon_protect, parse_repository_name
|
||||
from endpoints.decorators import anon_protect, parse_repository_name, check_region_blacklisted
|
||||
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write, get_input_stream
|
||||
from endpoints.v2.errors import (
|
||||
BlobUnknown, BlobUploadInvalid, BlobUploadUnknown, Unsupported, NameUnknown, LayerTooLarge,
|
||||
InvalidRequest)
|
||||
InvalidRequest, BlobDownloadGeoBlocked)
|
||||
from util.cache import cache_control
|
||||
from util.names import parse_namespace_repository
|
||||
|
||||
|
@ -65,6 +65,7 @@ def check_blob_exists(namespace_name, repo_name, digest):
|
|||
@process_registry_jwt_auth(scopes=['pull'])
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
@check_region_blacklisted(BlobDownloadGeoBlocked)
|
||||
@cache_control(max_age=31536000)
|
||||
def download_blob(namespace_name, repo_name, digest):
|
||||
# Find the blob.
|
||||
|
|
|
@ -144,3 +144,10 @@ class NamespaceDisabled(V2RegistryException):
|
|||
def __init__(self, message=None):
|
||||
message = message or 'This namespace is disabled. Please contact your system administrator.'
|
||||
super(NamespaceDisabled, self).__init__('NAMESPACE_DISABLED', message, {}, 400)
|
||||
|
||||
|
||||
class BlobDownloadGeoBlocked(V2RegistryException):
|
||||
def __init__(self, detail=None):
|
||||
message = ('The region from which you are pulling has been geo-ip blocked. ' +
|
||||
'Please contact the namespace owner.')
|
||||
super(BlobDownloadGeoBlocked, self).__init__('BLOB_DOWNLOAD_GEO_BLOCKED', message, detail, 403)
|
||||
|
|
|
@ -13,7 +13,8 @@ from auth.permissions import ReadRepositoryPermission
|
|||
from data import database
|
||||
from data import model
|
||||
from data.registry_model import registry_model
|
||||
from endpoints.decorators import anon_protect, anon_allowed, route_show_if, parse_repository_name
|
||||
from endpoints.decorators import (anon_protect, anon_allowed, route_show_if, parse_repository_name,
|
||||
check_region_blacklisted)
|
||||
from endpoints.v2.blob import BLOB_DIGEST_ROUTE
|
||||
from image.appc import AppCImageFormatter
|
||||
from image.docker import ManifestException
|
||||
|
@ -273,6 +274,7 @@ def _repo_verb_signature(namespace, repository, tag_name, verb, checker=None, **
|
|||
return make_response(signature_value)
|
||||
|
||||
|
||||
@check_region_blacklisted()
|
||||
def _repo_verb(namespace, repository, tag_name, verb, formatter, sign=False, checker=None,
|
||||
**kwargs):
|
||||
# Verify that the image exists and that we have access to it.
|
||||
|
@ -444,6 +446,7 @@ def get_squashed_tag(namespace, repository, tag):
|
|||
@verbs.route('/torrent{0}'.format(BLOB_DIGEST_ROUTE), methods=['GET'])
|
||||
@process_auth
|
||||
@parse_repository_name()
|
||||
@check_region_blacklisted(namespace_name_kwarg='namespace_name')
|
||||
def get_tag_torrent(namespace_name, repo_name, digest):
|
||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||
repo_is_public = repo is not None and model.repository.is_repository_public(repo)
|
||||
|
|
Reference in a new issue