Add the ability to blacklist v2 for specific versions

This commit is contained in:
Jake Moshenko 2015-12-15 16:21:06 -05:00
parent 4a84388f15
commit 766d60493f
7 changed files with 97 additions and 2 deletions

View file

@ -193,6 +193,10 @@ class DefaultConfig(object):
# Feature Flag: Whether the v2/ endpoint is visible # Feature Flag: Whether the v2/ endpoint is visible
FEATURE_ADVERTISE_V2 = True FEATURE_ADVERTISE_V2 = True
# Semver spec for which Docker versions we will blacklist
# Documentation: http://pythonhosted.org/semantic_version/reference.html#semantic_version.Spec
BLACKLIST_V2_SPEC = '<1.6.0'
BUILD_MANAGER = ('enterprise', {}) BUILD_MANAGER = ('enterprise', {})
DISTRIBUTED_STORAGE_CONFIG = { DISTRIBUTED_STORAGE_CONFIG = {

View file

@ -3,6 +3,7 @@ import logging
from flask import Blueprint, make_response, url_for, request, jsonify from flask import Blueprint, make_response, url_for, request, jsonify
from functools import wraps from functools import wraps
from urlparse import urlparse from urlparse import urlparse
from semantic_version import Spec
import features import features
@ -13,8 +14,10 @@ from auth.auth_context import get_grant_context
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission, from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
AdministerRepositoryPermission) AdministerRepositoryPermission)
from data import model from data import model
from app import app
from util.http import abort from util.http import abort
from util.saas.metricqueue import time_blueprint from util.saas.metricqueue import time_blueprint
from util.registry.dockerver import docker_version
from auth.registry_jwt_auth import process_registry_jwt_auth, get_auth_headers from auth.registry_jwt_auth import process_registry_jwt_auth, get_auth_headers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -75,6 +78,14 @@ def route_show_if(value):
@process_registry_jwt_auth @process_registry_jwt_auth
@anon_allowed @anon_allowed
def v2_support_enabled(): def v2_support_enabled():
docker_ver = docker_version(request.user_agent.string)
# Check if our version is one of the blacklisted versions, if we can't
# identify the version (None) we will fail open and assume that it is
# newer and therefore should not be blacklisted.
if Spec(app.config['BLACKLIST_V2_SPEC']).match(docker_ver) and docker_ver is not None:
abort(404)
response = make_response('true', 200) response = make_response('true', 200)
if get_grant_context() is None: if get_grant_context() is None:

View file

@ -58,3 +58,4 @@ rfc3987
jsonpath-rw jsonpath-rw
bintrees bintrees
redlock redlock
semantic-version

View file

@ -78,6 +78,7 @@ reportlab==2.7
requests==2.7.0 requests==2.7.0
requests-oauthlib==0.5.0 requests-oauthlib==0.5.0
rfc3987==1.3.4 rfc3987==1.3.4
semantic-version==2.4.2
simplejson==3.7.3 simplejson==3.7.3
six==1.9.0 six==1.9.0
SQLAlchemy==1.0.6 SQLAlchemy==1.0.6

View file

@ -181,13 +181,18 @@ class RegistryTestCaseMixin(LiveServerTestCase):
class BaseRegistryMixin(object): class BaseRegistryMixin(object):
def conduct(self, method, url, headers=None, data=None, auth=None, params=None, expected_code=200, def conduct(self, method, url, headers=None, data=None, auth=None, params=None, expected_code=200,
json_data=None): json_data=None, user_agent=None):
params = params or {} params = params or {}
params['_csrf_token'] = self.csrf_token params['_csrf_token'] = self.csrf_token
headers = headers or {} headers = headers or {}
auth_tuple = None auth_tuple = None
if user_agent is not None:
headers['User-Agent'] = user_agent
else:
headers['User-Agent'] = 'docker/1.9.1'
if self.docker_token: if self.docker_token:
headers['X-Docker-Token'] = self.docker_token headers['X-Docker-Token'] = self.docker_token
@ -1026,6 +1031,9 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
# Try to get tags before a repo exists. # Try to get tags before a repo exists.
self.conduct('GET', '/v2/devtable/doesnotexist/tags/list', auth='jwt', expected_code=401) self.conduct('GET', '/v2/devtable/doesnotexist/tags/list', auth='jwt', expected_code=401)
def test_one_five_blacklist(self):
self.conduct('GET', '/v2/', expected_code=404, user_agent='Go 1.1 package http')
class V1PushV2PullRegistryTests(V2RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMixin, class V1PushV2PullRegistryTests(V2RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMixin,
RegistryTestCaseMixin, LiveServerTestCase): RegistryTestCaseMixin, LiveServerTestCase):

View file

@ -1,9 +1,11 @@
import unittest import unittest
from itertools import islice from itertools import islice
from semantic_version import Version, Spec
from util.validation import generate_valid_usernames from util.validation import generate_valid_usernames
from util.registry.generatorfile import GeneratorFile from util.registry.generatorfile import GeneratorFile
from util.registry.dockerver import docker_version
class TestGeneratorFile(unittest.TestCase): class TestGeneratorFile(unittest.TestCase):
def sample_generator(self): def sample_generator(self):
@ -131,6 +133,47 @@ class TestUsernameGenerator(unittest.TestCase):
self.assertEquals('a003', generated_output[3]) self.assertEquals('a003', generated_output[3])
class TestDockerVersionParsing(unittest.TestCase):
def test_parsing(self):
tests_cases = [
('docker/1.6.0 go/go1.4.2 git-commit/1234567 kernel/4.2.0-18-generic os/linux arch/amd64',
Version('1.6.0')),
('docker/1.7.1 go/go1.4.2 kernel/4.1.7-15.23.amzn1.x86_64 os/linux arch/amd64',
Version('1.7.1')),
('docker/1.6.2 go/go1.4.2 git-commit/7c8fca2-dirty kernel/4.0.5 os/linux arch/amd64',
Version('1.6.2')),
('docker/1.9.0 go/go1.4.2 git-commit/76d6bc9 kernel/3.16.0-4-amd64 os/linux arch/amd64',
Version('1.9.0')),
('docker/1.9.1 go/go1.4.2 git-commit/a34a1d5 kernel/3.10.0-229.20.1.el7.x86_64 os/linux arch/amd64',
Version('1.9.1')),
('docker/1.8.2-circleci go/go1.4.2 git-commit/a8b52f5 kernel/3.13.0-71-generic os/linux arch/amd64',
Version('1.8.2')),
('Go 1.1 package http', Version('1.5.0')),
('curl', None),
('docker/1.8 stuff', Version('1.8.0')),
]
for ua_string, ver_info in tests_cases:
parsed_ver = docker_version(ua_string)
self.assertEquals(ver_info, parsed_ver)
def test_specs(self):
test_cases = [
# (Spec, no_match_case_list, matching_case_list)
(Spec('<1.6.0'), ['1.6.0', '1.6.1', '1.9.0', '100.5.2'], ['0.0.0', '1.5.99']),
(Spec('<1.9.0'), ['1.9.0', '100.5.2'], ['0.0.0', '1.5.99', '1.6.0', '1.6.1']),
(Spec('<1.6.0,>0.0.1'), ['1.6.0', '1.6.1', '1.9.0', '0.0.0'], ['1.5.99']),
]
for spec, no_match_cases, match_cases in test_cases:
for no_match_case in no_match_cases:
self.assertFalse(spec.match(Version(no_match_case)),
'Spec: %s Case: %s' % (spec, no_match_case))
for match_case in match_cases:
self.assertTrue(spec.match(Version(match_case)),
'Spec: %s Case: %s' % (spec, match_case))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View file

@ -0,0 +1,27 @@
import re
from semantic_version import Version
_USER_AGENT_SEARCH_REGEX = re.compile(r'docker\/([0-9]+(?:\.[0-9]+){1,2})')
_EXACT_1_5_USER_AGENT = re.compile(r'^Go 1\.1 package http$')
_ONE_FIVE_ZERO = '1.5.0'
def docker_version(user_agent_string):
""" Extract the Docker version from the user agent, taking special care to
handle the case of a 1.5 client requesting an auth token, which sends
a broken user agent. If we can not positively identify a version, return
None.
"""
# First search for a well defined semver portion in the UA header.
found_semver = _USER_AGENT_SEARCH_REGEX.search(user_agent_string)
if found_semver:
return Version(found_semver.group(1), partial=True)
# Check if we received the very specific header which represents a 1.5 request
# to the auth endpoints.
elif _EXACT_1_5_USER_AGENT.match(user_agent_string):
return Version(_ONE_FIVE_ZERO)
else:
return None