Add the ability to blacklist v2 for specific versions
This commit is contained in:
parent
4a84388f15
commit
766d60493f
7 changed files with 97 additions and 2 deletions
|
@ -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 = {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -58,3 +58,4 @@ rfc3987
|
||||||
jsonpath-rw
|
jsonpath-rw
|
||||||
bintrees
|
bintrees
|
||||||
redlock
|
redlock
|
||||||
|
semantic-version
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
27
util/registry/dockerver.py
Normal file
27
util/registry/dockerver.py
Normal 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
|
Reference in a new issue