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:
josephschorr 2016-01-22 16:00:30 -05:00
commit 566a91f003
37 changed files with 270 additions and 148 deletions

View file

@ -227,7 +227,7 @@ def parse_args(func):
def parse_repository_name(func):
@wraps(func)
def wrapper(repository, *args, **kwargs):
(namespace, repository) = parse_namespace_repository(repository)
(namespace, repository) = parse_namespace_repository(repository, app.config['LIBRARY_NAMESPACE'])
return func(namespace, repository, *args, **kwargs)
return wrapper

View file

@ -145,7 +145,7 @@ def build_status_view(build_obj):
return resp
@resource('/v1/repository/<repopath:repository>/build/')
@resource('/v1/repository/<apirepopath:repository>/build/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryBuildList(RepositoryParamResource):
""" Resource related to creating and listing repository builds. """
@ -288,7 +288,7 @@ class RepositoryBuildList(RepositoryParamResource):
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>')
@resource('/v1/repository/<apirepopath:repository>/build/<build_uuid>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('build_uuid', 'The UUID of the build')
class RepositoryBuildResource(RepositoryParamResource):
@ -322,7 +322,7 @@ class RepositoryBuildResource(RepositoryParamResource):
raise InvalidRequest('Build is currently running or has finished')
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/status')
@resource('/v1/repository/<apirepopath:repository>/build/<build_uuid>/status')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('build_uuid', 'The UUID of the build')
class RepositoryBuildStatus(RepositoryParamResource):
@ -339,7 +339,7 @@ class RepositoryBuildStatus(RepositoryParamResource):
return build_status_view(build)
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/logs')
@resource('/v1/repository/<apirepopath:repository>/build/<build_uuid>/logs')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('build_uuid', 'The UUID of the build')
class RepositoryBuildLogs(RepositoryParamResource):

View file

@ -43,7 +43,7 @@ def historical_image_view(image, image_map):
return normal_view
@resource('/v1/repository/<repopath:repository>/image/')
@resource('/v1/repository/<apirepopath:repository>/image/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryImageList(RepositoryParamResource):
""" Resource for listing repository images. """
@ -82,7 +82,7 @@ class RepositoryImageList(RepositoryParamResource):
}
@resource('/v1/repository/<repopath:repository>/image/<image_id>')
@resource('/v1/repository/<apirepopath:repository>/image/<image_id>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('image_id', 'The Docker image ID')
class RepositoryImage(RepositoryParamResource):

View file

@ -106,7 +106,7 @@ def get_aggregate_logs(start_time, end_time, performer_name=None, repository=Non
}
@resource('/v1/repository/<repopath:repository>/logs')
@resource('/v1/repository/<apirepopath:repository>/logs')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryLogs(RepositoryParamResource):
""" Resource for fetching logs for the specific repository. """
@ -175,7 +175,7 @@ class OrgLogs(ApiResource):
raise Unauthorized()
@resource('/v1/repository/<repopath:repository>/aggregatelogs')
@resource('/v1/repository/<apirepopath:repository>/aggregatelogs')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryAggregateLogs(RepositoryParamResource):
""" Resource for fetching aggregated logs for the specific repository. """

View file

@ -38,7 +38,7 @@ def wrap_role_view_team(role_json, team):
return role_json
@resource('/v1/repository/<repopath:repository>/permissions/team/')
@resource('/v1/repository/<apirepopath:repository>/permissions/team/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryTeamPermissionList(RepositoryParamResource):
""" Resource for repository team permissions. """
@ -57,7 +57,7 @@ class RepositoryTeamPermissionList(RepositoryParamResource):
}
@resource('/v1/repository/<repopath:repository>/permissions/user/')
@resource('/v1/repository/<apirepopath:repository>/permissions/user/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryUserPermissionList(RepositoryParamResource):
""" Resource for repository user permissions. """
@ -97,7 +97,7 @@ class RepositoryUserPermissionList(RepositoryParamResource):
}
@resource('/v1/repository/<repopath:repository>/permissions/user/<username>/transitive')
@resource('/v1/repository/<apirepopath:repository>/permissions/user/<username>/transitive')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('username', 'The username of the user to which the permissions apply')
class RepositoryUserTransitivePermission(RepositoryParamResource):
@ -121,7 +121,7 @@ class RepositoryUserTransitivePermission(RepositoryParamResource):
}
@resource('/v1/repository/<repopath:repository>/permissions/user/<username>')
@resource('/v1/repository/<apirepopath:repository>/permissions/user/<username>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('username', 'The username of the user to which the permission applies')
class RepositoryUserPermission(RepositoryParamResource):
@ -215,7 +215,7 @@ class RepositoryUserPermission(RepositoryParamResource):
return 'Deleted', 204
@resource('/v1/repository/<repopath:repository>/permissions/team/<teamname>')
@resource('/v1/repository/<apirepopath:repository>/permissions/team/<teamname>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('teamname', 'The name of the team to which the permission applies')
class RepositoryTeamPermission(RepositoryParamResource):

View file

@ -30,7 +30,7 @@ def record_view(record):
@internal_only
@show_if(features.MAILING)
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
@resource('/v1/repository/<apirepopath:repository>/authorizedemail/<email>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('email', 'The e-mail address')
class RepositoryAuthorizedEmail(RepositoryParamResource):

View file

@ -225,7 +225,7 @@ class RepositoryList(ApiResource):
}
@resource('/v1/repository/<repopath:repository>')
@resource('/v1/repository/<apirepopath:repository>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class Repository(RepositoryParamResource):
"""Operations for managing a specific repository."""
@ -339,7 +339,7 @@ class Repository(RepositoryParamResource):
return 'Deleted', 204
@resource('/v1/repository/<repopath:repository>/changevisibility')
@resource('/v1/repository/<apirepopath:repository>/changevisibility')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryVisibility(RepositoryParamResource):
""" Custom verb for changing the visibility of the repository. """

View file

@ -38,7 +38,7 @@ def notification_view(note):
}
@resource('/v1/repository/<repopath:repository>/notification/')
@resource('/v1/repository/<apirepopath:repository>/notification/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryNotificationList(RepositoryParamResource):
""" Resource for dealing with listing and creating notifications on a repository. """
@ -116,7 +116,7 @@ class RepositoryNotificationList(RepositoryParamResource):
}
@resource('/v1/repository/<repopath:repository>/notification/<uuid>')
@resource('/v1/repository/<apirepopath:repository>/notification/<uuid>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('uuid', 'The UUID of the notification')
class RepositoryNotification(RepositoryParamResource):
@ -149,7 +149,7 @@ class RepositoryNotification(RepositoryParamResource):
return 'No Content', 204
@resource('/v1/repository/<repopath:repository>/notification/<uuid>/test')
@resource('/v1/repository/<apirepopath:repository>/notification/<uuid>/test')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('uuid', 'The UUID of the notification')
class TestRepositoryNotification(RepositoryParamResource):

View file

@ -20,7 +20,7 @@ def token_view(token_obj):
}
@resource('/v1/repository/<repopath:repository>/tokens/')
@resource('/v1/repository/<apirepopath:repository>/tokens/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryTokenList(RepositoryParamResource):
""" Resource for creating and listing repository tokens. """
@ -66,7 +66,7 @@ class RepositoryTokenList(RepositoryParamResource):
return token_view(token), 201
@resource('/v1/repository/<repopath:repository>/tokens/<code>')
@resource('/v1/repository/<apirepopath:repository>/tokens/<code>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('code', 'The token code')
class RepositoryToken(RepositoryParamResource):

View file

@ -54,7 +54,7 @@ def _get_status(repo_image):
@show_if(features.SECURITY_SCANNER)
@resource('/v1/repository/<repopath:repository>/image/<imageid>/vulnerabilities')
@resource('/v1/repository/<apirepopath:repository>/image/<imageid>/vulnerabilities')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('imageid', 'The image ID')
class RepositoryImageVulnerabilities(RepositoryParamResource):
@ -89,7 +89,7 @@ class RepositoryImageVulnerabilities(RepositoryParamResource):
@show_if(features.SECURITY_SCANNER)
@resource('/v1/repository/<repopath:repository>/image/<imageid>/packages')
@resource('/v1/repository/<apirepopath:repository>/image/<imageid>/packages')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('imageid', 'The image ID')
class RepositoryImagePackages(RepositoryParamResource):

View file

@ -11,7 +11,7 @@ from auth.auth_context import get_authenticated_user
from util.names import TAG_ERROR, TAG_REGEX
@resource('/v1/repository/<repopath:repository>/tag/')
@resource('/v1/repository/<apirepopath:repository>/tag/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class ListRepositoryTags(RepositoryParamResource):
""" Resource for listing full repository tag history, alive *and dead*. """
@ -60,7 +60,7 @@ class ListRepositoryTags(RepositoryParamResource):
}
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
@resource('/v1/repository/<apirepopath:repository>/tag/<tag>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('tag', 'The name of the tag')
class RepositoryTag(RepositoryParamResource):
@ -128,7 +128,7 @@ class RepositoryTag(RepositoryParamResource):
return 'Deleted', 204
@resource('/v1/repository/<repopath:repository>/tag/<tag>/images')
@resource('/v1/repository/<apirepopath:repository>/tag/<tag>/images')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('tag', 'The name of the tag')
class RepositoryTagImages(RepositoryParamResource):
@ -179,7 +179,7 @@ class RepositoryTagImages(RepositoryParamResource):
@resource('/v1/repository/<repopath:repository>/tag/<tag>/revert')
@resource('/v1/repository/<apirepopath:repository>/tag/<tag>/revert')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('tag', 'The name of the tag')
class RevertTag(RepositoryParamResource):

View file

@ -33,7 +33,7 @@ def _prepare_webhook_url(scheme, username, password, hostname, path):
return urlunparse((scheme, auth_hostname, path, '', '', ''))
@resource('/v1/repository/<repopath:repository>/trigger/')
@resource('/v1/repository/<apirepopath:repository>/trigger/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class BuildTriggerList(RepositoryParamResource):
""" Resource for listing repository build triggers. """
@ -48,7 +48,7 @@ class BuildTriggerList(RepositoryParamResource):
}
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>')
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
class BuildTrigger(RepositoryParamResource):
@ -95,7 +95,7 @@ class BuildTrigger(RepositoryParamResource):
return 'No Content', 204
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/subdir')
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/subdir')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
@internal_only
@ -143,7 +143,7 @@ class BuildTriggerSubdirs(RepositoryParamResource):
raise Unauthorized()
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/activate')
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/activate')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
class BuildTriggerActivate(RepositoryParamResource):
@ -245,7 +245,7 @@ class BuildTriggerActivate(RepositoryParamResource):
raise Unauthorized()
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/analyze')
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/analyze')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
@internal_only
@ -384,7 +384,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
raise NotFound()
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/start')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
class ActivateBuildTrigger(RepositoryParamResource):
@ -444,7 +444,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
return resp, 201, headers
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/builds')
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/builds')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
class TriggerBuildList(RepositoryParamResource):
@ -464,7 +464,7 @@ class TriggerBuildList(RepositoryParamResource):
FIELD_VALUE_LIMIT = 30
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/fields/<field_name>')
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/fields/<field_name>')
@internal_only
class BuildTriggerFieldValues(RepositoryParamResource):
""" Custom verb to fetch a values list for a particular field name. """
@ -493,7 +493,7 @@ class BuildTriggerFieldValues(RepositoryParamResource):
raise Unauthorized()
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/sources')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
@internal_only

View file

@ -876,7 +876,7 @@ class StarredRepositoryList(ApiResource):
}, 201
@resource('/v1/user/starred/<repopath:repository>')
@resource('/v1/user/starred/<apirepopath:repository>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class StarredRepository(RepositoryParamResource):
""" Operations for managing a specific starred repository. """

View file

@ -8,7 +8,6 @@ from buildtrigger.bitbuckethandler import BitbucketBuildTrigger
from endpoints.common import route_show_if
from app import app
from data import model
from util.names import parse_repository_name
from util.http import abort
from auth.auth import require_session_login

View file

@ -23,6 +23,7 @@ from functools import wraps
from config import frontend_visible_config
from external_libraries import get_external_javascript, get_external_css
from util.secscan.api import PRIORITY_LEVELS
from util.names import parse_namespace_repository
import features
@ -47,11 +48,24 @@ def get_cache_busters():
return CACHE_BUSTERS
class RepoPathConverter(BaseConverter):
regex = '[\.a-zA-Z0-9_\-]+/[\.a-zA-Z0-9_\-]+'
weight = 200
def parse_repository_name(f):
@wraps(f)
def wrapper(repository, *args, **kwargs):
lib_namespace = app.config['LIBRARY_NAMESPACE']
(namespace, repository) = parse_namespace_repository(repository, lib_namespace)
return f(namespace, repository, *args, **kwargs)
return wrapper
def parse_repository_name_and_tag(f):
@wraps(f)
def wrapper(repository, *args, **kwargs):
lib_namespace = app.config['LIBRARY_NAMESPACE']
namespace, repository, tag = parse_namespace_repository(repository, lib_namespace,
include_tag=True)
return f(namespace, repository, tag, *args, **kwargs)
return wrapper
app.url_map.converters['repopath'] = RepoPathConverter
def route_show_if(value):
def decorator(f):

View file

@ -3,10 +3,9 @@ import logging
from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user
from endpoints.common import route_show_if
from endpoints.common import route_show_if, parse_repository_name
from app import app, github_trigger
from data import model
from util.names import parse_repository_name
from util.http import abort
from auth.permissions import AdministerRepositoryPermission
from auth.auth import require_session_login
@ -17,7 +16,7 @@ logger = logging.getLogger(__name__)
client = app.config['HTTPCLIENT']
githubtrigger = Blueprint('callback', __name__)
@githubtrigger.route('/github/callback/trigger/<path:repository>', methods=['GET'])
@githubtrigger.route('/github/callback/trigger/<repopath:repository>', methods=['GET'])
@route_show_if(features.GITHUB_BUILD)
@require_session_login
@parse_repository_name

View file

@ -4,11 +4,10 @@ import requests
from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user
from endpoints.common import common_login, route_show_if
from endpoints.common import common_login, route_show_if, parse_repository_name
from endpoints.web import render_page_template_with_routedata
from app import app, analytics, get_app_url, github_login, google_login, dex_login
from data import model
from util.names import parse_repository_name
from util.validation import generate_valid_usernames
from util.http import abort
from auth.auth import require_session_login

View file

@ -9,12 +9,13 @@ from data import model
from app import app, authentication, userevents, storage
from auth.auth import process_auth, generate_signed_token
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
from util.names import parse_repository_name, REPOSITORY_NAME_REGEX
from util.names import REPOSITORY_NAME_REGEX
from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
ReadRepositoryPermission, CreateRepositoryPermission,
repository_read_grant, repository_write_grant)
from util.http import abort
from endpoints.common import parse_repository_name
from endpoints.v1 import v1_bp
from endpoints.trackhelper import track_and_log
from endpoints.notificationhelper import spawn_notification
@ -167,7 +168,7 @@ def update_user(username):
abort(403)
@v1_bp.route('/repositories/<path:repository>', methods=['PUT'])
@v1_bp.route('/repositories/<repopath:repository>', methods=['PUT'])
@process_auth
@parse_repository_name
@generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201)
@ -221,7 +222,7 @@ def create_repository(namespace, repository):
return make_response('Created', 201)
@v1_bp.route('/repositories/<path:repository>/images', methods=['PUT'])
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['PUT'])
@process_auth
@parse_repository_name
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
@ -251,7 +252,7 @@ def update_images(namespace, repository):
abort(403)
@v1_bp.route('/repositories/<path:repository>/images', methods=['GET'])
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['GET'])
@process_auth
@parse_repository_name
@generate_headers(scope=GrantType.READ_REPOSITORY)
@ -277,7 +278,7 @@ def get_repository_images(namespace, repository):
abort(403)
@v1_bp.route('/repositories/<path:repository>/images', methods=['DELETE'])
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['DELETE'])
@process_auth
@parse_repository_name
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
@ -286,7 +287,7 @@ def delete_repository_images(namespace, repository):
abort(501, 'Not Implemented', issue='not-implemented')
@v1_bp.route('/repositories/<path:repository>/auth', methods=['PUT'])
@v1_bp.route('/repositories/<repopath:repository>/auth', methods=['PUT'])
@parse_repository_name
@anon_allowed
def put_repository_auth(namespace, repository):

View file

@ -5,11 +5,12 @@ import json
from flask import abort, request, jsonify, make_response, session
from app import app
from util.names import TAG_ERROR, TAG_REGEX, parse_repository_name
from util.names import TAG_ERROR, TAG_REGEX
from auth.auth import process_auth
from auth.permissions import (ReadRepositoryPermission,
ModifyRepositoryPermission)
from data import model
from endpoints.common import parse_repository_name
from endpoints.decorators import anon_protect
from endpoints.v1 import v1_bp
from endpoints.trackhelper import track_and_log
@ -18,7 +19,7 @@ from endpoints.trackhelper import track_and_log
logger = logging.getLogger(__name__)
@v1_bp.route('/repositories/<path:repository>/tags', methods=['GET'])
@v1_bp.route('/repositories/<repopath:repository>/tags', methods=['GET'])
@process_auth
@anon_protect
@parse_repository_name
@ -33,7 +34,7 @@ def get_tags(namespace, repository):
abort(403)
@v1_bp.route('/repositories/<path:repository>/tags/<tag>', methods=['GET'])
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['GET'])
@process_auth
@anon_protect
@parse_repository_name
@ -53,7 +54,7 @@ def get_tag(namespace, repository, tag):
abort(403)
@v1_bp.route('/repositories/<path:repository>/tags/<tag>', methods=['PUT'])
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['PUT'])
@process_auth
@anon_protect
@parse_repository_name
@ -78,7 +79,7 @@ def put_tag(namespace, repository, tag):
abort(403)
@v1_bp.route('/repositories/<path:repository>/tags/<tag>', methods=['DELETE'])
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['DELETE'])
@process_auth
@anon_protect
@parse_repository_name

View file

@ -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):

View file

@ -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)

View file

@ -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({

View file

@ -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):

View file

@ -22,7 +22,7 @@ from formats.squashed import SquashedDockerImage
from formats.aci import ACIImage
from storage import Storage
from endpoints.v2.blob import BLOB_DIGEST_ROUTE
from endpoints.common import route_show_if
from endpoints.common import route_show_if, parse_repository_name
verbs = Blueprint('verbs', __name__)
@ -376,6 +376,7 @@ def get_squashed_tag(namespace, repository, tag):
@anon_protect
@verbs.route('/torrent{0}'.format(BLOB_DIGEST_ROUTE), methods=['GET'])
@process_auth
@parse_repository_name
def get_tag_torrent(namespace, repo_name, digest):
permission = ReadRepositoryPermission(namespace, repo_name)
public_repo = model.repository.repository_is_public(namespace, repo_name)

View file

@ -25,14 +25,14 @@ from buildtrigger.triggerutil import TriggerProviderException
from data import model
from data.database import db
from endpoints.api.discovery import swagger_route_data
from endpoints.common import common_login, render_page_template, route_show_if, param_required
from endpoints.common import (common_login, render_page_template, route_show_if, param_required,
parse_repository_name, parse_repository_name_and_tag)
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
from endpoints.decorators import anon_protect, anon_allowed
from health.healthcheck import get_healthchecker
from util.cache import no_cache, cache_control
from util.headers import parse_basic_auth
from util.invoice import renderInvoiceToPdf
from util.names import parse_repository_name, parse_repository_name_and_tag
from util.seo import render_snapshot
from util.systemlogs import build_logs_archive
from util.useremails import send_email_changed
@ -409,7 +409,7 @@ def confirm_recovery():
abort(403)
@web.route('/repository/<path:repository>/status', methods=['GET'])
@web.route('/repository/<repopath:repository>/status', methods=['GET'])
@parse_repository_name
@anon_protect
def build_status_badge(namespace, repository):
@ -597,7 +597,7 @@ def download_logs_archive():
abort(403)
@web.route('/bitbucket/setup/<path:repository>', methods=['GET'])
@web.route('/bitbucket/setup/<repopath:repository>', methods=['GET'])
@require_session_login
@parse_repository_name
@route_show_if(features.BITBUCKET_BUILD)
@ -630,7 +630,7 @@ def attach_bitbucket_trigger(namespace, repository_name):
abort(403)
@web.route('/customtrigger/setup/<path:repository>', methods=['GET'])
@web.route('/customtrigger/setup/<repopath:repository>', methods=['GET'])
@require_session_login
@parse_repository_name
def attach_custom_build_trigger(namespace, repository_name):
@ -653,7 +653,7 @@ def attach_custom_build_trigger(namespace, repository_name):
abort(403)
@web.route('/<path:repository>')
@web.route('/<repopath:repository>')
@no_cache
@process_oauth
@parse_repository_name_and_tag

View file

@ -67,7 +67,7 @@ def stripe_webhook():
return make_response('Okay')
@webhooks.route('/push/<path:repository>/trigger/<trigger_uuid>', methods=['POST'])
@webhooks.route('/push/<repopath:repository>/trigger/<trigger_uuid>', methods=['POST'])
@webhooks.route('/push/trigger/<trigger_uuid>', methods=['POST'], defaults={'repository': ''})
@process_auth
def build_trigger_webhook(trigger_uuid, **kwargs):