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_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', {})
DISTRIBUTED_STORAGE_CONFIG = {

View file

@ -3,6 +3,7 @@ import logging
from flask import Blueprint, make_response, url_for, request, jsonify
from functools import wraps
from urlparse import urlparse
from semantic_version import Spec
import features
@ -13,8 +14,10 @@ from auth.auth_context import get_grant_context
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
AdministerRepositoryPermission)
from data import model
from app import app
from util.http import abort
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
logger = logging.getLogger(__name__)
@ -75,6 +78,14 @@ def route_show_if(value):
@process_registry_jwt_auth
@anon_allowed
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)
if get_grant_context() is None:

View file

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

View file

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

View file

@ -181,13 +181,18 @@ class RegistryTestCaseMixin(LiveServerTestCase):
class BaseRegistryMixin(object):
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['_csrf_token'] = self.csrf_token
headers = headers or {}
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:
headers['X-Docker-Token'] = self.docker_token
@ -1026,6 +1031,9 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
# Try to get tags before a repo exists.
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,
RegistryTestCaseMixin, LiveServerTestCase):

View file

@ -1,9 +1,11 @@
import unittest
from itertools import islice
from semantic_version import Version, Spec
from util.validation import generate_valid_usernames
from util.registry.generatorfile import GeneratorFile
from util.registry.dockerver import docker_version
class TestGeneratorFile(unittest.TestCase):
def sample_generator(self):
@ -131,6 +133,47 @@ class TestUsernameGenerator(unittest.TestCase):
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__':
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