Merge pull request #1160 from coreos-inc/dockerv2authsucks
Fix Docker Auth and our V2 registry paths to support library (i.e. namespace-less) repositories.
This commit is contained in:
commit
566a91f003
37 changed files with 270 additions and 148 deletions
|
@ -17,13 +17,14 @@ from util.cache import cache_control
|
|||
from util.registry.filelike import wrap_with_handler, StreamSlice
|
||||
from util.registry.gzipstream import calculate_size_handler
|
||||
from util.registry.torrent import PieceHasher
|
||||
from endpoints.common import parse_repository_name
|
||||
from storage.basestorage import InvalidChunkException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
BASE_BLOB_ROUTE = '/<namespace>/<repo_name>/blobs/<regex("{0}"):digest>'
|
||||
BASE_BLOB_ROUTE = '/<repopath:repository>/blobs/<regex("{0}"):digest>'
|
||||
BLOB_DIGEST_ROUTE = BASE_BLOB_ROUTE.format(digest_tools.DIGEST_PATTERN)
|
||||
RANGE_HEADER_REGEX = re.compile(r'^bytes=([0-9]+)-([0-9]+)$')
|
||||
BLOB_CONTENT_TYPE = 'application/octet-stream'
|
||||
|
@ -57,6 +58,7 @@ def _base_blob_fetch(namespace, repo_name, digest):
|
|||
|
||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
@cache_control(max_age=31436000)
|
||||
|
@ -72,6 +74,7 @@ def check_blob_exists(namespace, repo_name, digest):
|
|||
|
||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
@cache_control(max_age=31536000)
|
||||
|
@ -103,8 +106,9 @@ def _render_range(num_uploaded_bytes, with_bytes_prefix=True):
|
|||
return '{0}0-{1}'.format('bytes=' if with_bytes_prefix else '', num_uploaded_bytes - 1)
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/', methods=['POST'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/', methods=['POST'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def start_blob_upload(namespace, repo_name):
|
||||
|
@ -121,8 +125,10 @@ def start_blob_upload(namespace, repo_name):
|
|||
if digest is None:
|
||||
# The user will send the blob data in another request
|
||||
accepted = make_response('', 202)
|
||||
accepted.headers['Location'] = url_for('v2.upload_chunk', namespace=namespace,
|
||||
repo_name=repo_name, upload_uuid=new_upload_uuid)
|
||||
accepted.headers['Location'] = url_for('v2.upload_chunk',
|
||||
repository='%s/%s' % (namespace, repo_name),
|
||||
upload_uuid=new_upload_uuid)
|
||||
|
||||
accepted.headers['Range'] = _render_range(0)
|
||||
accepted.headers['Docker-Upload-UUID'] = new_upload_uuid
|
||||
return accepted
|
||||
|
@ -136,8 +142,9 @@ def start_blob_upload(namespace, repo_name):
|
|||
return _finish_upload(namespace, repo_name, uploaded, digest)
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['GET'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def fetch_existing_upload(namespace, repo_name, upload_uuid):
|
||||
|
@ -311,13 +318,15 @@ def _finish_upload(namespace, repo_name, upload_obj, expected_digest):
|
|||
|
||||
response = make_response('', 201)
|
||||
response.headers['Docker-Content-Digest'] = expected_digest
|
||||
response.headers['Location'] = url_for('v2.download_blob', namespace=namespace,
|
||||
repo_name=repo_name, digest=expected_digest)
|
||||
response.headers['Location'] = url_for('v2.download_blob',
|
||||
repository='%s/%s' % (namespace, repo_name),
|
||||
digest=expected_digest)
|
||||
return response
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['PATCH'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PATCH'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def upload_chunk(namespace, repo_name, upload_uuid):
|
||||
|
@ -334,8 +343,9 @@ def upload_chunk(namespace, repo_name, upload_uuid):
|
|||
return accepted
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['PUT'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PUT'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid):
|
||||
|
@ -352,8 +362,9 @@ def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid):
|
|||
return _finish_upload(namespace, repo_name, found, digest)
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['DELETE'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['DELETE'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def cancel_upload(namespace, repo_name, upload_uuid):
|
||||
|
@ -371,8 +382,9 @@ def cancel_upload(namespace, repo_name, upload_uuid):
|
|||
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/<digest>', methods=['DELETE'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/<digest>', methods=['DELETE'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def delete_digest(namespace, repo_name, upload_uuid):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import jwt.utils
|
||||
import json
|
||||
import features
|
||||
|
||||
from peewee import IntegrityError
|
||||
from flask import make_response, request, url_for
|
||||
|
@ -8,25 +9,25 @@ from collections import namedtuple, OrderedDict
|
|||
from jwkest.jws import SIGNER_ALGS, keyrep
|
||||
from datetime import datetime
|
||||
|
||||
from app import docker_v2_signing_key
|
||||
from app import docker_v2_signing_key, app
|
||||
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||
from endpoints.decorators import anon_protect
|
||||
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write
|
||||
from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnverified,
|
||||
ManifestUnknown, TagInvalid, NameInvalid)
|
||||
from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnknown, TagInvalid,
|
||||
NameInvalid)
|
||||
from endpoints.trackhelper import track_and_log
|
||||
from endpoints.notificationhelper import spawn_notification
|
||||
from digest import digest_tools
|
||||
from data import model
|
||||
from data.database import RepositoryTag
|
||||
|
||||
from endpoints.common import parse_repository_name
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
VALID_TAG_PATTERN = r'[\w][\w.-]{0,127}'
|
||||
|
||||
BASE_MANIFEST_ROUTE = '/<namespace>/<repo_name>/manifests/<regex("{0}"):manifest_ref>'
|
||||
BASE_MANIFEST_ROUTE = '/<repopath:repository>/manifests/<regex("{0}"):manifest_ref>'
|
||||
MANIFEST_DIGEST_ROUTE = BASE_MANIFEST_ROUTE.format(digest_tools.DIGEST_PATTERN)
|
||||
MANIFEST_TAGNAME_ROUTE = BASE_MANIFEST_ROUTE.format(VALID_TAG_PATTERN)
|
||||
|
||||
|
@ -61,9 +62,17 @@ class SignedManifest(object):
|
|||
|
||||
self._parsed = json.loads(manifest_bytes)
|
||||
self._signatures = self._parsed[_SIGNATURES_KEY]
|
||||
self._namespace, self._repo_name = self._parsed[_REPO_NAME_KEY].split('/')
|
||||
self._tag = self._parsed[_REPO_TAG_KEY]
|
||||
|
||||
repo_name_tuple = self._parsed[_REPO_NAME_KEY].split('/')
|
||||
if len(repo_name_tuple) > 1:
|
||||
self._namespace, self._repo_name = repo_name_tuple
|
||||
elif len(repo_name_tuple) == 1:
|
||||
self._namespace = ''
|
||||
self._repo_name = repo_name_tuple[0]
|
||||
else:
|
||||
raise ValueError('repo_name has too many or too few pieces')
|
||||
|
||||
self._validate()
|
||||
|
||||
def _validate(self):
|
||||
|
@ -144,9 +153,13 @@ class SignedManifestBuilder(object):
|
|||
""" Class which represents a manifest which is currently being built.
|
||||
"""
|
||||
def __init__(self, namespace, repo_name, tag, architecture='amd64', schema_ver=1):
|
||||
repo_name_key = '{0}/{1}'.format(namespace, repo_name)
|
||||
if namespace == '':
|
||||
repo_name_key = repo_name
|
||||
|
||||
self._base_payload = {
|
||||
_REPO_TAG_KEY: tag,
|
||||
_REPO_NAME_KEY: '{0}/{1}'.format(namespace, repo_name),
|
||||
_REPO_NAME_KEY: repo_name_key,
|
||||
_ARCH_KEY: architecture,
|
||||
_SCHEMA_VER: schema_ver,
|
||||
}
|
||||
|
@ -213,6 +226,7 @@ class SignedManifestBuilder(object):
|
|||
|
||||
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||
|
@ -242,6 +256,7 @@ def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
|||
|
||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def fetch_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||
|
@ -262,12 +277,13 @@ def fetch_manifest_by_digest(namespace, repo_name, manifest_ref):
|
|||
|
||||
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['PUT'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||
try:
|
||||
manifest = SignedManifest(request.data)
|
||||
except ValueError:
|
||||
except ValueError as ve:
|
||||
raise ManifestInvalid()
|
||||
|
||||
if manifest.tag != manifest_ref:
|
||||
|
@ -278,6 +294,7 @@ def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
|||
|
||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['PUT'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def write_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||
|
@ -293,8 +310,16 @@ def write_manifest_by_digest(namespace, repo_name, manifest_ref):
|
|||
|
||||
|
||||
def _write_manifest(namespace, repo_name, manifest):
|
||||
# Ensure that the manifest is for this repository.
|
||||
if manifest.namespace != namespace or manifest.repo_name != repo_name:
|
||||
# Ensure that the manifest is for this repository. If the manifest's namespace is empty, then
|
||||
# it is for the library namespace and we need an extra check.
|
||||
if (manifest.namespace == '' and features.LIBRARY_SUPPORT and
|
||||
namespace == app.config['LIBRARY_NAMESPACE']):
|
||||
# This is a library manifest. All good.
|
||||
pass
|
||||
elif manifest.namespace != namespace:
|
||||
raise NameInvalid()
|
||||
|
||||
if manifest.repo_name != repo_name:
|
||||
raise NameInvalid()
|
||||
|
||||
# Ensure that the repository exists.
|
||||
|
@ -369,13 +394,15 @@ def _write_manifest(namespace, repo_name, manifest):
|
|||
|
||||
response = make_response('OK', 202)
|
||||
response.headers['Docker-Content-Digest'] = manifest_digest
|
||||
response.headers['Location'] = url_for('v2.fetch_manifest_by_digest', namespace=namespace,
|
||||
repo_name=repo_name, manifest_ref=manifest_digest)
|
||||
response.headers['Location'] = url_for('v2.fetch_manifest_by_digest',
|
||||
repository='%s/%s' % (namespace, repo_name),
|
||||
manifest_ref=manifest_digest)
|
||||
return response
|
||||
|
||||
|
||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['DELETE'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def delete_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||
|
@ -406,8 +433,14 @@ def _generate_and_store_manifest(namespace, repo_name, tag_name):
|
|||
image = model.tag.get_tag_image(namespace, repo_name, tag_name)
|
||||
parents = model.image.get_parent_images(namespace, repo_name, image)
|
||||
|
||||
# If the manifest is being generated under the library namespace, then we make its namespace
|
||||
# empty.
|
||||
manifest_namespace = namespace
|
||||
if features.LIBRARY_SUPPORT and namespace == app.config['LIBRARY_NAMESPACE']:
|
||||
manifest_namespace = ''
|
||||
|
||||
# Create and populate the manifest builder
|
||||
builder = SignedManifestBuilder(namespace, repo_name, tag_name)
|
||||
builder = SignedManifestBuilder(manifest_namespace, repo_name, tag_name)
|
||||
|
||||
# Add the leaf layer
|
||||
builder.add_layer(image.storage.content_checksum, image.v1_json_metadata)
|
||||
|
|
|
@ -6,9 +6,11 @@ from endpoints.v2.errors import NameUnknown
|
|||
from endpoints.v2.v2util import add_pagination
|
||||
from endpoints.decorators import anon_protect
|
||||
from data import model
|
||||
from endpoints.common import parse_repository_name
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/tags/list', methods=['GET'])
|
||||
@v2_bp.route('/<repopath:repository>/tags/list', methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def list_all_tags(namespace, repo_name):
|
||||
|
@ -17,8 +19,7 @@ def list_all_tags(namespace, repo_name):
|
|||
raise NameUnknown()
|
||||
|
||||
query = model.tag.list_repository_tags(namespace, repo_name)
|
||||
|
||||
url = url_for('v2.list_all_tags', namespace=namespace, repo_name=repo_name)
|
||||
url = url_for('v2.list_all_tags', repository='%s/%s' % (namespace, repo_name))
|
||||
link, query = add_pagination(query, url)
|
||||
|
||||
response = jsonify({
|
||||
|
|
|
@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
TOKEN_VALIDITY_LIFETIME_S = 60 * 60 # 1 hour
|
||||
SCOPE_REGEX = re.compile(
|
||||
r'^repository:([\.a-zA-Z0-9_\-]+/[\.a-zA-Z0-9_\-]+):(((push|pull|\*),)*(push|pull|\*))$'
|
||||
r'^repository:(([\.a-zA-Z0-9_\-]+/)?[\.a-zA-Z0-9_\-]+):(((push|pull|\*),)*(push|pull|\*))$'
|
||||
)
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
|
@ -77,9 +77,10 @@ def generate_registry_jwt():
|
|||
logger.debug('Match: %s', match.groups())
|
||||
|
||||
namespace_and_repo = match.group(1)
|
||||
actions = match.group(2).split(',')
|
||||
actions = match.group(3).split(',')
|
||||
|
||||
namespace, reponame = parse_namespace_repository(namespace_and_repo)
|
||||
lib_namespace = app.config['LIBRARY_NAMESPACE']
|
||||
namespace, reponame = parse_namespace_repository(namespace_and_repo, lib_namespace)
|
||||
|
||||
# Ensure that we are never creating an invalid repository.
|
||||
if not REPOSITORY_NAME_REGEX.match(reponame):
|
||||
|
|
Reference in a new issue