Add ability for specific geographic regions to be blocked from pulling images within a namespace

This commit is contained in:
Joseph Schorr 2018-12-05 15:19:37 -05:00
parent c71a43a06c
commit c3710a6a5e
20 changed files with 257 additions and 37 deletions

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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)

View file

@ -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)