Fix Docker Auth and our V2 registry paths to support library (i.e. namespace-less) repositories.

This support is placed behind a feature flag.
This commit is contained in:
Joseph Schorr 2016-01-21 15:40:51 -05:00
parent 06b0f756bd
commit e4ffaff869
37 changed files with 270 additions and 148 deletions

Binary file not shown.

View file

@ -138,7 +138,14 @@ _CLEAN_DATABASE_PATH = None
_JWK = RSAKey(key=RSA.generate(2048))
def get_full_contents(image_data):
def _get_repo_name(namespace, name):
if namespace == '':
return name
return '%s/%s' % (namespace, name)
def _get_full_contents(image_data):
if 'chunks' in image_data:
# Data is just for chunking; no need for a real TAR.
return image_data['contents']
@ -213,7 +220,8 @@ class RegistryTestCaseMixin(LiveServerTestCase):
self.csrf_token = self.conduct('GET', '/__test/csrf').text
def do_tag(self, namespace, repository, tag, image_id, expected_code=200):
self.conduct('PUT', '/v1/repositories/%s/%s/tags/%s' % (namespace, repository, tag),
repo_name = _get_repo_name(namespace, repository)
self.conduct('PUT', '/v1/repositories/%s/tags/%s' % (repo_name, tag),
data='"%s"' % image_id, expected_code=expected_code, auth='sig')
def conduct_api_login(self, username, password):
@ -221,8 +229,9 @@ class RegistryTestCaseMixin(LiveServerTestCase):
data=json.dumps(dict(username=username, password=password)),
headers={'Content-Type': 'application/json'})
def change_repo_visibility(self, repository, namespace, visibility):
self.conduct('POST', '/api/v1/repository/%s/%s/changevisibility' % (repository, namespace),
def change_repo_visibility(self, namespace, repository, visibility):
repo_name = _get_repo_name(namespace, repository)
self.conduct('POST', '/api/v1/repository/%s/changevisibility' % repo_name,
data=json.dumps(dict(visibility=visibility)),
headers={'Content-Type': 'application/json'})
@ -284,12 +293,13 @@ class V1RegistryPushMixin(V1RegistryMixin):
def do_push(self, namespace, repository, username, password, images=None, expected_code=201):
images = images or self._get_default_images()
auth = (username, password)
repo_name = _get_repo_name(namespace, repository)
# Ping!
self.v1_ping()
# PUT /v1/repositories/{namespace}/{repository}/
self.conduct('PUT', '/v1/repositories/%s/%s' % (namespace, repository),
self.conduct('PUT', '/v1/repositories/%s' % repo_name,
data=json.dumps(images), auth=auth,
expected_code=expected_code)
@ -310,7 +320,7 @@ class V1RegistryPushMixin(V1RegistryMixin):
data=json.dumps(image_json_data), auth='sig')
# PUT /v1/images/{imageID}/layer
layer_bytes = get_full_contents(image_data)
layer_bytes = _get_full_contents(image_data)
self.conduct('PUT', '/v1/images/%s/layer' % image_id,
data=StringIO(layer_bytes), auth='sig')
@ -325,7 +335,7 @@ class V1RegistryPushMixin(V1RegistryMixin):
self.do_tag(namespace, repository, 'latest', images[0]['id'])
# PUT /v1/repositories/{namespace}/{repository}/images
self.conduct('PUT', '/v1/repositories/%s/%s/images' % (namespace, repository),
self.conduct('PUT', '/v1/repositories/%s/images' % repo_name,
expected_code=204,
auth='sig')
@ -334,6 +344,7 @@ class V1RegistryPullMixin(V1RegistryMixin):
def do_pull(self, namespace, repository, username=None, password='password', expected_code=200,
images=None):
images = images or self._get_default_images()
repo_name = _get_repo_name(namespace, repository)
auth = None
if username:
@ -342,7 +353,7 @@ class V1RegistryPullMixin(V1RegistryMixin):
# Ping!
self.v1_ping()
prefix = '/v1/repositories/%s/%s/' % (namespace, repository)
prefix = '/v1/repositories/%s/' % repo_name
# GET /v1/repositories/{namespace}/{repository}/
self.conduct('GET', prefix + 'images', auth=auth, expected_code=expected_code)
@ -417,9 +428,11 @@ class V2RegistryMixin(BaseRegistryMixin):
def do_auth(self, username, password, namespace, repository, expected_code=200, scopes=[]):
auth = (username, password)
repo_name = _get_repo_name(namespace, repository)
params = {
'account': username,
'scope': 'repository:%s/%s:%s' % (namespace, repository, ','.join(scopes)),
'scope': 'repository:%s:%s' % (repo_name, ','.join(scopes)),
'service': app.config['SERVER_HOSTNAME'],
}
@ -439,6 +452,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
cancel=False, invalid=False, expected_manifest_code=202, expected_auth_code=200,
scopes=None):
images = images or self._get_default_images()
repo_name = _get_repo_name(namespace, repository)
# Ping!
self.v2_ping()
@ -456,7 +470,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
full_contents = {}
for image_data in images:
full_contents[image_data['id']] = get_full_contents(image_data)
full_contents[image_data['id']] = _get_full_contents(image_data)
checksum = 'sha256:' + hashlib.sha256(full_contents[image_data['id']]).hexdigest()
if invalid:
@ -476,11 +490,11 @@ class V2RegistryPushMixin(V2RegistryMixin):
# Layer data should not yet exist.
checksum = 'sha256:' + hashlib.sha256(layer_bytes).hexdigest()
self.conduct('HEAD', '/v2/%s/%s/blobs/%s' % (namespace, repository, checksum),
self.conduct('HEAD', '/v2/%s/blobs/%s' % (repo_name, checksum),
expected_code=404, auth='jwt')
# Start a new upload of the layer data.
response = self.conduct('POST', '/v2/%s/%s/blobs/uploads/' % (namespace, repository),
response = self.conduct('POST', '/v2/%s/blobs/uploads/' % repo_name,
expected_code=202, auth='jwt')
upload_uuid = response.headers['Docker-Upload-UUID']
@ -505,7 +519,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
return
# Retrieve the upload status at each point.
status_url = '/v2/%s/%s/blobs/uploads/%s' % (namespace, repository, upload_uuid)
status_url = '/v2/%s/blobs/uploads/%s' % (repo_name, upload_uuid)
response = self.conduct('GET', status_url, expected_code=204, auth='jwt',
headers=dict(host=self.get_server_url()))
self.assertEquals(response.headers['Docker-Upload-UUID'], upload_uuid)
@ -516,7 +530,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
auth='jwt')
# Ensure the upload was canceled.
status_url = '/v2/%s/%s/blobs/uploads/%s' % (namespace, repository, upload_uuid)
status_url = '/v2/%s/blobs/uploads/%s' % (repo_name, upload_uuid)
self.conduct('GET', status_url, expected_code=404, auth='jwt',
headers=dict(host=self.get_server_url()))
return
@ -529,14 +543,14 @@ class V2RegistryPushMixin(V2RegistryMixin):
checksums[image_id] = checksum
# Ensure the layer exists now.
response = self.conduct('HEAD', '/v2/%s/%s/blobs/%s' % (namespace, repository, checksum),
response = self.conduct('HEAD', '/v2/%s/blobs/%s' % (repo_name, checksum),
expected_code=200, auth='jwt')
self.assertEquals(response.headers['Docker-Content-Digest'], checksum)
self.assertEquals(response.headers['Content-Length'], str(len(layer_bytes)))
# Write the manifest.
put_code = 404 if invalid else expected_manifest_code
self.conduct('PUT', '/v2/%s/%s/manifests/%s' % (namespace, repository, tag_name),
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
data=manifest.bytes, expected_code=put_code,
headers={'Content-Type': 'application/json'}, auth='jwt')
@ -547,6 +561,7 @@ class V2RegistryPullMixin(V2RegistryMixin):
def do_pull(self, namespace, repository, username=None, password='password', expected_code=200,
manifest_id=None, expected_manifest_code=200, images=None):
images = images or self._get_default_images()
repo_name = _get_repo_name(namespace, repository)
# Ping!
self.v2_ping()
@ -559,7 +574,7 @@ class V2RegistryPullMixin(V2RegistryMixin):
# Retrieve the manifest for the tag or digest.
manifest_id = manifest_id or 'latest'
response = self.conduct('GET', '/v2/%s/%s/manifests/%s' % (namespace, repository, manifest_id),
response = self.conduct('GET', '/v2/%s/manifests/%s' % (repo_name, manifest_id),
auth='jwt', expected_code=expected_manifest_code)
if expected_manifest_code != 200:
return
@ -573,7 +588,7 @@ class V2RegistryPullMixin(V2RegistryMixin):
blobs = {}
for layer in manifest_data['fsLayers']:
blob_id = layer['blobSum']
result = self.conduct('GET', '/v2/%s/%s/blobs/%s' % (namespace, repository, blob_id),
result = self.conduct('GET', '/v2/%s/blobs/%s' % (repo_name, blob_id),
expected_code=200, auth='jwt')
blobs[blob_id] = result.content
@ -863,6 +878,17 @@ class RegistryTestsMixin(object):
self.do_pull('buynlarge', 'newrepo', 'devtable', 'password')
def test_library_repo(self):
self.do_push('', 'newrepo', 'devtable', 'password')
self.do_pull('', 'newrepo', 'devtable', 'password')
self.do_pull('library', 'newrepo', 'devtable', 'password')
def test_library_disabled(self):
with TestFeature(self, 'LIBRARY_SUPPORT', False):
self.do_push('library', 'newrepo', 'devtable', 'password')
self.do_pull('library', 'newrepo', 'devtable', 'password')
class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMixin,
RegistryTestCaseMixin, LiveServerTestCase):
""" Tests for V1 registry. """
@ -872,7 +898,7 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix
'id': 'onlyimagehere',
'contents': 'somecontents',
}]
self.do_push('public', 'newrepo/somesubrepo', 'public', 'password', images, expected_code=400)
self.do_push('public', 'newrepo/somesubrepo', 'public', 'password', images, expected_code=404)
def test_push_unicode_metadata(self):
self.conduct_api_login('devtable', 'password')
@ -896,7 +922,7 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix
self.do_push('public', 'newrepo', 'public', 'password', images)
self.do_tag('public', 'newrepo', '1', image_id)
self.do_tag('public', 'newrepo', 'x' * 128, image_id)
self.do_tag('public', 'newrepo', '', image_id, expected_code=400)
self.do_tag('public', 'newrepo', '', image_id, expected_code=404)
self.do_tag('public', 'newrepo', 'x' * 129, image_id, expected_code=400)
self.do_tag('public', 'newrepo', '.fail', image_id, expected_code=400)
self.do_tag('public', 'newrepo', '-fail', image_id, expected_code=400)
@ -1440,6 +1466,16 @@ class V2LoginTests(V2RegistryLoginMixin, LoginTests, RegistryTestCaseMixin, Base
def test_nouser_push_publicrepo(self):
self.do_login('', '', expected_code=401, scope='repository:public/publicrepo:push')
def test_library_invaliduser(self):
self.do_login('invaliduser', 'password', expected_code=401, scope='repository:librepo:pull,push')
def test_library_noaccess(self):
self.do_login('freshuser', 'password', expected_code=403, scope='repository:librepo:pull,push')
def test_library_access(self):
self.do_login('devtable', 'password', expect_success=200, scope='repository:librepo:pull,push')
if __name__ == '__main__':
unittest.main()

View file

@ -276,8 +276,7 @@ class IndexV2TestSpec(object):
return self
def get_url(self):
namespace, repo_name = parse_namespace_repository(self.repo_name)
return url_for(self.index_name, namespace=namespace, repo_name=repo_name, **self.kwargs)
return url_for(self.index_name, repository=self.repo_name, **self.kwargs)
def gen_basic_auth(self, username, password):
encoded = b64encode('%s:%s' % (username, password))

View file

@ -644,7 +644,7 @@ class TestConductSearch(ApiTestCase):
json = self.getJsonResponse(ConductSearch,
params=dict(query='owners'))
self.assertEquals(1, len(json['results']))
self.assertEquals(2, len(json['results']))
self.assertEquals(json['results'][0]['kind'], 'team')
self.assertEquals(json['results'][0]['name'], 'owners')

View file

@ -63,7 +63,7 @@ class _SpecTestBuilder(type):
session_vars = []
if test_spec.sess_repo:
ns, repo = parse_namespace_repository(test_spec.sess_repo)
ns, repo = parse_namespace_repository(test_spec.sess_repo, 'library')
session_vars.append(('namespace', ns))
session_vars.append(('repository', repo))

View file

@ -3,7 +3,6 @@ import endpoints.decorated
import json
from app import app
from util.names import parse_namespace_repository
from initdb import setup_database_for_testing, finished_database_for_testing
from specs import build_v2_index_specs
from endpoints.v2 import v2_bp