use kwargs for parse_repository_name

This commit is contained in:
Jimmy Zelinskie 2016-03-09 16:20:28 -05:00
parent 3b52a255b2
commit bb46cc933d
15 changed files with 285 additions and 270 deletions

View file

@ -1,8 +1,9 @@
import logging import logging
import re import re
from jsonschema import validate, ValidationError
from functools import wraps from functools import wraps
from jsonschema import validate, ValidationError
from flask import request, url_for from flask import request, url_for
from flask.ext.principal import identity_changed, Identity from flask.ext.principal import identity_changed, Identity
from cryptography.x509 import load_pem_x509_certificate from cryptography.x509 import load_pem_x509_certificate

View file

@ -3,10 +3,11 @@
import json import json
import logging import logging
from flask import request, url_for
from urllib import quote from urllib import quote
from urlparse import urlunparse from urlparse import urlunparse
from flask import request, url_for
from app import app from app import app
from buildtrigger.basehandler import BuildTriggerHandler from buildtrigger.basehandler import BuildTriggerHandler
from buildtrigger.triggerutil import (TriggerDeactivationException, from buildtrigger.triggerutil import (TriggerDeactivationException,
@ -40,9 +41,9 @@ class BuildTriggerList(RepositoryParamResource):
@require_repo_admin @require_repo_admin
@nickname('listBuildTriggers') @nickname('listBuildTriggers')
def get(self, namespace, repository): def get(self, namespace_name, repo_name):
""" List the triggers for the specified repository. """ """ List the triggers for the specified repository. """
triggers = model.build.list_build_triggers(namespace, repository) triggers = model.build.list_build_triggers(namespace_name, repo_name)
return { return {
'triggers': [trigger_view(trigger, can_admin=True) for trigger in triggers] 'triggers': [trigger_view(trigger, can_admin=True) for trigger in triggers]
} }
@ -56,7 +57,7 @@ class BuildTrigger(RepositoryParamResource):
@require_repo_admin @require_repo_admin
@nickname('getBuildTrigger') @nickname('getBuildTrigger')
def get(self, namespace, repository, trigger_uuid): def get(self, namespace_name, repo_name, trigger_uuid):
""" Get information for the specified build trigger. """ """ Get information for the specified build trigger. """
try: try:
trigger = model.build.get_build_trigger(trigger_uuid) trigger = model.build.get_build_trigger(trigger_uuid)
@ -67,7 +68,7 @@ class BuildTrigger(RepositoryParamResource):
@require_repo_admin @require_repo_admin
@nickname('deleteBuildTrigger') @nickname('deleteBuildTrigger')
def delete(self, namespace, repository, trigger_uuid): def delete(self, namespace_name, repo_name, trigger_uuid):
""" Delete the specified build trigger. """ """ Delete the specified build trigger. """
try: try:
trigger = model.build.get_build_trigger(trigger_uuid) trigger = model.build.get_build_trigger(trigger_uuid)
@ -82,10 +83,10 @@ class BuildTrigger(RepositoryParamResource):
# We are just going to eat this error # We are just going to eat this error
logger.warning('Trigger deactivation problem: %s', ex) logger.warning('Trigger deactivation problem: %s', ex)
log_action('delete_repo_trigger', namespace, log_action('delete_repo_trigger', namespace_name,
{'repo': repository, 'trigger_id': trigger_uuid, {'repo': repo_name, 'trigger_id': trigger_uuid,
'service': trigger.service.name}, 'service': trigger.service.name},
repo=model.repository.get_repository(namespace, repository)) repo=model.repository.get_repository(namespace_name, repo_name))
trigger.delete_instance(recursive=True) trigger.delete_instance(recursive=True)
@ -111,7 +112,7 @@ class BuildTriggerSubdirs(RepositoryParamResource):
@require_repo_admin @require_repo_admin
@nickname('listBuildTriggerSubdirs') @nickname('listBuildTriggerSubdirs')
@validate_json_request('BuildTriggerSubdirRequest') @validate_json_request('BuildTriggerSubdirRequest')
def post(self, namespace, repository, trigger_uuid): def post(self, namespace_name, repo_name, trigger_uuid):
""" List the subdirectories available for the specified build trigger and source. """ """ List the subdirectories available for the specified build trigger and source. """
try: try:
trigger = model.build.get_build_trigger(trigger_uuid) trigger = model.build.get_build_trigger(trigger_uuid)
@ -171,7 +172,7 @@ class BuildTriggerActivate(RepositoryParamResource):
@require_repo_admin @require_repo_admin
@nickname('activateBuildTrigger') @nickname('activateBuildTrigger')
@validate_json_request('BuildTriggerActivateRequest') @validate_json_request('BuildTriggerActivateRequest')
def post(self, namespace, repository, trigger_uuid): def post(self, namespace_name, repo_name, trigger_uuid):
""" Activate the specified build trigger. """ """ Activate the specified build trigger. """
try: try:
trigger = model.build.get_build_trigger(trigger_uuid) trigger = model.build.get_build_trigger(trigger_uuid)
@ -198,7 +199,7 @@ class BuildTriggerActivate(RepositoryParamResource):
raise Unauthorized() raise Unauthorized()
# Make sure the namespace matches that of the trigger. # Make sure the namespace matches that of the trigger.
if robot_namespace != namespace: if robot_namespace != namespace_name:
raise Unauthorized() raise Unauthorized()
# Set the pull robot. # Set the pull robot.
@ -208,7 +209,7 @@ class BuildTriggerActivate(RepositoryParamResource):
new_config_dict = request.get_json()['config'] new_config_dict = request.get_json()['config']
write_token_name = 'Build Trigger: %s' % trigger.service.name write_token_name = 'Build Trigger: %s' % trigger.service.name
write_token = model.token.create_delegate_token(namespace, repository, write_token_name, write_token = model.token.create_delegate_token(namespace_name, repo_name, write_token_name,
'write') 'write')
try: try:
@ -233,12 +234,13 @@ class BuildTriggerActivate(RepositoryParamResource):
trigger.save() trigger.save()
# Log the trigger setup. # Log the trigger setup.
repo = model.repository.get_repository(namespace, repository) repo = model.repository.get_repository(namespace_name, repo_name)
log_action('setup_repo_trigger', namespace, log_action('setup_repo_trigger', namespace_name,
{'repo': repository, 'namespace': namespace, {'repo': repo_name, 'namespace': namespace_name,
'trigger_id': trigger.uuid, 'service': trigger.service.name, 'trigger_id': trigger.uuid, 'service': trigger.service.name,
'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None, 'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
'config': final_config}, repo=repo) 'config': final_config},
repo=repo)
return trigger_view(trigger, can_admin=True) return trigger_view(trigger, can_admin=True)
else: else:
@ -271,7 +273,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
@require_repo_admin @require_repo_admin
@nickname('analyzeBuildTrigger') @nickname('analyzeBuildTrigger')
@validate_json_request('BuildTriggerAnalyzeRequest') @validate_json_request('BuildTriggerAnalyzeRequest')
def post(self, namespace, repository, trigger_uuid): def post(self, namespace_name, repo_name, trigger_uuid):
""" Analyze the specified build trigger configuration. """ """ Analyze the specified build trigger configuration. """
try: try:
trigger = model.build.get_build_trigger(trigger_uuid) trigger = model.build.get_build_trigger(trigger_uuid)
@ -414,7 +416,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
@require_repo_admin @require_repo_admin
@nickname('manuallyStartBuildTrigger') @nickname('manuallyStartBuildTrigger')
@validate_json_request('RunParameters') @validate_json_request('RunParameters')
def post(self, namespace, repository, trigger_uuid): def post(self, namespace_name, repo_name, trigger_uuid):
""" Manually start a build from the specified trigger. """ """ Manually start a build from the specified trigger. """
try: try:
trigger = model.build.get_build_trigger(trigger_uuid) trigger = model.build.get_build_trigger(trigger_uuid)
@ -426,7 +428,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
raise InvalidRequest('Trigger is not active.') raise InvalidRequest('Trigger is not active.')
try: try:
repo = model.repository.get_repository(namespace, repository) repo = model.repository.get_repository(namespace_name, repo_name)
pull_robot_name = model.build.get_pull_robot_name(trigger) pull_robot_name = model.build.get_pull_robot_name(trigger)
run_parameters = request.get_json() run_parameters = request.get_json()
@ -436,7 +438,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
raise InvalidRequest(tse.message) raise InvalidRequest(tse.message)
resp = build_status_view(build_request) resp = build_status_view(build_request)
repo_string = '%s/%s' % (namespace, repository) repo_string = '%s/%s' % (namespace_name, repo_name)
headers = { headers = {
'Location': api.url_for(RepositoryBuildStatus, repository=repo_string, 'Location': api.url_for(RepositoryBuildStatus, repository=repo_string,
build_uuid=build_request.uuid), build_uuid=build_request.uuid),
@ -453,10 +455,10 @@ class TriggerBuildList(RepositoryParamResource):
@parse_args() @parse_args()
@query_param('limit', 'The maximum number of builds to return', type=int, default=5) @query_param('limit', 'The maximum number of builds to return', type=int, default=5)
@nickname('listTriggerRecentBuilds') @nickname('listTriggerRecentBuilds')
def get(self, namespace, repository, trigger_uuid, parsed_args): def get(self, namespace_name, repo_name, trigger_uuid, parsed_args):
""" List the builds started by the specified trigger. """ """ List the builds started by the specified trigger. """
limit = parsed_args['limit'] limit = parsed_args['limit']
builds = model.build.list_trigger_builds(namespace, repository, trigger_uuid, limit) builds = model.build.list_trigger_builds(namespace_name, repo_name, trigger_uuid, limit)
return { return {
'builds': [build_status_view(bld) for bld in builds] 'builds': [build_status_view(bld) for bld in builds]
} }
@ -470,7 +472,7 @@ class BuildTriggerFieldValues(RepositoryParamResource):
""" Custom verb to fetch a values list for a particular field name. """ """ Custom verb to fetch a values list for a particular field name. """
@require_repo_admin @require_repo_admin
@nickname('listTriggerFieldValues') @nickname('listTriggerFieldValues')
def post(self, namespace, repository, trigger_uuid, field_name): def post(self, namespace_name, repo_name, trigger_uuid, field_name):
""" List the field values for a custom run field. """ """ List the field values for a custom run field. """
try: try:
trigger = model.build.get_build_trigger(trigger_uuid) trigger = model.build.get_build_trigger(trigger_uuid)
@ -501,7 +503,7 @@ class BuildTriggerSources(RepositoryParamResource):
""" Custom verb to fetch the list of build sources for the trigger config. """ """ Custom verb to fetch the list of build sources for the trigger config. """
@require_repo_admin @require_repo_admin
@nickname('listTriggerBuildSources') @nickname('listTriggerBuildSources')
def get(self, namespace, repository, trigger_uuid): def get(self, namespace_name, repo_name, trigger_uuid):
""" List the build sources for the trigger configuration thus far. """ """ List the build sources for the trigger configuration thus far. """
try: try:
trigger = model.build.get_build_trigger(trigger_uuid) trigger = model.build.get_build_trigger(trigger_uuid)

View file

@ -47,24 +47,28 @@ def get_cache_busters():
return CACHE_BUSTERS return CACHE_BUSTERS
def parse_repository_name(f): def parse_repository_name(include_tag=False,
@wraps(f) ns_kwarg_name='namespace_name',
def wrapper(repository, *args, **kwargs): repo_kwarg_name='repo_name',
lib_namespace = app.config['LIBRARY_NAMESPACE'] tag_kwarg_name='tag_name',
(namespace, repository) = parse_namespace_repository(repository, lib_namespace) incoming_repo_kwarg='repository'):
return f(namespace, repository, *args, **kwargs) def inner(func):
@wraps(func)
def wrapper(*args, **kwargs):
parsed_stuff = parse_namespace_repository(kwargs[incoming_repo_kwarg],
app.config['LIBRARY_NAMESPACE'],
include_tag=include_tag)
del kwargs[incoming_repo_kwarg]
kwargs[ns_kwarg_name] = parsed_stuff[0]
kwargs[repo_kwarg_name] = parsed_stuff[1]
if include_tag:
kwargs[tag_kwarg_name] = parsed_stuff[2]
return func(*args, **kwargs)
return wrapper return wrapper
return inner
def parse_repository_name_and_tag(f): # TODO get rid of all calls to this parse_repository_name_and_tag
@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
def route_show_if(value): def route_show_if(value):
def decorator(f): def decorator(f):

View file

@ -3,35 +3,37 @@ import logging
from flask import request, redirect, url_for, Blueprint from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user from flask.ext.login import current_user
from endpoints.common import route_show_if, parse_repository_name
from app import app, github_trigger
from data import model
from util.http import abort
from auth.permissions import AdministerRepositoryPermission
from auth.auth import require_session_login
import features import features
from app import app, github_trigger
from auth.auth import require_session_login
from auth.permissions import AdministerRepositoryPermission
from data import model
from endpoints.common import route_show_if, parse_repository_name
from util.http import abort
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
client = app.config['HTTPCLIENT'] client = app.config['HTTPCLIENT']
githubtrigger = Blueprint('callback', __name__) githubtrigger = Blueprint('callback', __name__)
@githubtrigger.route('/github/callback/trigger/<repopath:repository>', methods=['GET']) @githubtrigger.route('/github/callback/trigger/<repopath:repository>', methods=['GET'])
@route_show_if(features.GITHUB_BUILD) @route_show_if(features.GITHUB_BUILD)
@require_session_login @require_session_login
@parse_repository_name @parse_repository_name()
def attach_github_build_trigger(namespace, repository): def attach_github_build_trigger(namespace_name, repo_name):
permission = AdministerRepositoryPermission(namespace, repository) permission = AdministerRepositoryPermission(namespace_name, repo_name)
if permission.can(): if permission.can():
code = request.args.get('code') code = request.args.get('code')
token = github_trigger.exchange_code_for_token(app.config, client, code) token = github_trigger.exchange_code_for_token(app.config, client, code)
repo = model.repository.get_repository(namespace, repository) repo = model.repository.get_repository(namespace_name, repo_name)
if not repo: if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository) msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
abort(404, message=msg) abort(404, message=msg)
trigger = model.build.create_build_trigger(repo, 'github', token, current_user.db_user()) trigger = model.build.create_build_trigger(repo, 'github', token, current_user.db_user())
repo_path = '%s/%s' % (namespace, repository) repo_path = '%s/%s' % (namespace_name, repo_name)
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=', full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
trigger.uuid) trigger.uuid)

View file

@ -3,18 +3,18 @@ import requests
from flask import request, redirect, url_for, Blueprint from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user from flask.ext.login import current_user
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.validation import generate_valid_usernames
from util.http import abort
from auth.auth import require_session_login
from peewee import IntegrityError from peewee import IntegrityError
import features import features
from app import app, analytics, get_app_url, github_login, google_login, dex_login
from auth.auth import require_session_login
from data import model
from endpoints.common import common_login, route_show_if
from endpoints.web import render_page_template_with_routedata
from util.security.strictjwt import decode, InvalidTokenError from util.security.strictjwt import decode, InvalidTokenError
from util.validation import generate_valid_usernames
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
client = app.config['HTTPCLIENT'] client = app.config['HTTPCLIENT']
@ -271,8 +271,10 @@ def dex_oauth_callback():
payload = decode_user_jwt(token, dex_login) payload = decode_user_jwt(token, dex_login)
except InvalidTokenError: except InvalidTokenError:
logger.exception('Exception when decoding returned JWT') logger.exception('Exception when decoding returned JWT')
return render_ologin_error(dex_login.public_title, return render_ologin_error(
'Could not decode response. Please contact your system administrator about this error.') dex_login.public_title,
'Could not decode response. Please contact your system administrator about this error.',
)
username = get_email_username(payload) username = get_email_username(payload)
metadata = {} metadata = {}
@ -281,9 +283,11 @@ def dex_oauth_callback():
email_address = payload['email'] email_address = payload['email']
if not payload.get('email_verified', False): if not payload.get('email_verified', False):
return render_ologin_error(dex_login.public_title, return render_ologin_error(
dex_login.public_title,
'A verified e-mail address is required for login. Please verify your ' + 'A verified e-mail address is required for login. Please verify your ' +
'e-mail address in %s and try again.' % dex_login.public_title) 'e-mail address in %s and try again.' % dex_login.public_title,
)
return conduct_oauth_login(dex_login, dex_id, username, email_address, return conduct_oauth_login(dex_login, dex_id, username, email_address,
@ -304,8 +308,10 @@ def dex_oauth_attach():
payload = decode_user_jwt(token, dex_login) payload = decode_user_jwt(token, dex_login)
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
logger.exception('Exception when decoding returned JWT') logger.exception('Exception when decoding returned JWT')
return render_ologin_error(dex_login.public_title, return render_ologin_error(
'Could not decode response. Please contact your system administrator about this error.') dex_login.public_title,
'Could not decode response. Please contact your system administrator about this error.',
)
user_obj = current_user.db_user() user_obj = current_user.db_user()
dex_id = payload['sub'] dex_id = payload['sub']

View file

@ -2,19 +2,19 @@ import json
import logging import logging
import urlparse import urlparse
from flask import request, make_response, jsonify, session
from functools import wraps from functools import wraps
from flask import request, make_response, jsonify, session
from data import model from data import model
from app import app, authentication, userevents, storage from app import authentication, userevents
from auth.auth import process_auth, generate_signed_token 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 auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
from util.names import REPOSITORY_NAME_REGEX
from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission, from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
ReadRepositoryPermission, CreateRepositoryPermission, ReadRepositoryPermission, CreateRepositoryPermission,
repository_read_grant, repository_write_grant) repository_read_grant, repository_write_grant)
from util.http import abort from util.http import abort
from util.names import REPOSITORY_NAME_REGEX
from endpoints.common import parse_repository_name from endpoints.common import parse_repository_name
from endpoints.v1 import v1_bp from endpoints.v1 import v1_bp
from endpoints.trackhelper import track_and_log from endpoints.trackhelper import track_and_log
@ -33,12 +33,12 @@ class GrantType(object):
def generate_headers(scope=GrantType.READ_REPOSITORY, add_grant_for_status=None): def generate_headers(scope=GrantType.READ_REPOSITORY, add_grant_for_status=None):
def decorator_method(f): def decorator_method(f):
@wraps(f) @wraps(f)
def wrapper(namespace, repository, *args, **kwargs): def wrapper(namespace_name, repo_name, *args, **kwargs):
response = f(namespace, repository, *args, **kwargs) response = f(namespace_name, repo_name, *args, **kwargs)
# Setting session namespace and repository # Setting session namespace and repository
session['namespace'] = namespace session['namespace'] = namespace_name
session['repository'] = repository session['repository'] = repo_name
# We run our index and registry on the same hosts for now # We run our index and registry on the same hosts for now
registry_server = urlparse.urlparse(request.url).netloc registry_server = urlparse.urlparse(request.url).netloc
@ -51,11 +51,11 @@ def generate_headers(scope=GrantType.READ_REPOSITORY, add_grant_for_status=None)
grants = [] grants = []
if scope == GrantType.READ_REPOSITORY: if scope == GrantType.READ_REPOSITORY:
if force_grant or ReadRepositoryPermission(namespace, repository).can(): if force_grant or ReadRepositoryPermission(namespace_name, repo_name).can():
grants.append(repository_read_grant(namespace, repository)) grants.append(repository_read_grant(namespace_name, repo_name))
elif scope == GrantType.WRITE_REPOSITORY: elif scope == GrantType.WRITE_REPOSITORY:
if force_grant or ModifyRepositoryPermission(namespace, repository).can(): if force_grant or ModifyRepositoryPermission(namespace_name, repo_name).can():
grants.append(repository_write_grant(namespace, repository)) grants.append(repository_write_grant(namespace_name, repo_name))
# Generate a signed token for the user (if any) and the grants (if any) # Generate a signed token for the user (if any) and the grants (if any)
if grants or get_authenticated_user(): if grants or get_authenticated_user():
@ -170,50 +170,50 @@ def update_user(username):
@v1_bp.route('/repositories/<repopath:repository>/', methods=['PUT']) @v1_bp.route('/repositories/<repopath:repository>/', methods=['PUT'])
@process_auth @process_auth
@parse_repository_name @parse_repository_name()
@generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201) @generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201)
@anon_allowed @anon_allowed
def create_repository(namespace, repository): def create_repository(namespace_name, repo_name):
# Verify that the repository name is valid. # Verify that the repository name is valid.
if not REPOSITORY_NAME_REGEX.match(repository): if not REPOSITORY_NAME_REGEX.match(repo_name):
abort(400, message='Invalid repository name. Repository names cannot contain slashes.') abort(400, message='Invalid repository name. Repository names cannot contain slashes.')
logger.debug('Looking up repository %s/%s', namespace, repository) logger.debug('Looking up repository %s/%s', namespace_name, repo_name)
repo = model.repository.get_repository(namespace, repository) repo = model.repository.get_repository(namespace_name, repo_name)
logger.debug('Found repository %s/%s', namespace, repository) logger.debug('Found repository %s/%s', namespace_name, repo_name)
if not repo and get_authenticated_user() is None: if not repo and get_authenticated_user() is None:
logger.debug('Attempt to create repository %s/%s without user auth', namespace, repository) logger.debug('Attempt to create repository %s/%s without user auth', namespace_name, repo_name)
abort(401, abort(401,
message='Cannot create a repository as a guest. Please login via "docker login" first.', message='Cannot create a repository as a guest. Please login via "docker login" first.',
issue='no-login') issue='no-login')
elif repo: elif repo:
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace_name, repo_name)
if not permission.can(): if not permission.can():
abort(403, abort(403,
message='You do not have permission to modify repository %(namespace)s/%(repository)s', message='You do not have permission to modify repository %(namespace)s/%(repository)s',
issue='no-repo-write-permission', issue='no-repo-write-permission',
namespace=namespace, repository=repository) namespace=namespace_name, repository=repo_name)
else: else:
permission = CreateRepositoryPermission(namespace) permission = CreateRepositoryPermission(namespace_name)
if not permission.can(): if not permission.can():
logger.info('Attempt to create a new repo %s/%s with insufficient perms', namespace, logger.info('Attempt to create a new repo %s/%s with insufficient perms', namespace_name,
repository) repo_name)
msg = 'You do not have permission to create repositories in namespace "%(namespace)s"' msg = 'You do not have permission to create repositories in namespace "%(namespace)s"'
abort(403, message=msg, issue='no-create-permission', namespace=namespace) abort(403, message=msg, issue='no-create-permission', namespace=namespace_name)
# Attempt to create the new repository. # Attempt to create the new repository.
logger.debug('Creating repository %s/%s with owner: %s', namespace, repository, logger.debug('Creating repository %s/%s with owner: %s', namespace_name, repo_name,
get_authenticated_user().username) get_authenticated_user().username)
repo = model.repository.create_repository(namespace, repository, get_authenticated_user()) repo = model.repository.create_repository(namespace_name, repo_name, get_authenticated_user())
if get_authenticated_user(): if get_authenticated_user():
user_event_data = { user_event_data = {
'action': 'push_start', 'action': 'push_start',
'repository': repository, 'repository': repo_name,
'namespace': namespace 'namespace': namespace_name,
} }
event = userevents.get_event(get_authenticated_user().username) event = userevents.get_event(get_authenticated_user().username)
@ -224,15 +224,15 @@ def create_repository(namespace, repository):
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['PUT']) @v1_bp.route('/repositories/<repopath:repository>/images', methods=['PUT'])
@process_auth @process_auth
@parse_repository_name @parse_repository_name()
@generate_headers(scope=GrantType.WRITE_REPOSITORY) @generate_headers(scope=GrantType.WRITE_REPOSITORY)
@anon_allowed @anon_allowed
def update_images(namespace, repository): def update_images(namespace_name, repo_name):
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace_name, repo_name)
if permission.can(): if permission.can():
logger.debug('Looking up repository') logger.debug('Looking up repository')
repo = model.repository.get_repository(namespace, repository) repo = model.repository.get_repository(namespace_name, repo_name)
if not repo: if not repo:
# Make sure the repo actually exists. # Make sure the repo actually exists.
abort(404, message='Unknown repository', issue='unknown-repo') abort(404, message='Unknown repository', issue='unknown-repo')
@ -254,17 +254,17 @@ def update_images(namespace, repository):
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['GET']) @v1_bp.route('/repositories/<repopath:repository>/images', methods=['GET'])
@process_auth @process_auth
@parse_repository_name @parse_repository_name()
@generate_headers(scope=GrantType.READ_REPOSITORY) @generate_headers(scope=GrantType.READ_REPOSITORY)
@anon_protect @anon_protect
def get_repository_images(namespace, repository): def get_repository_images(namespace_name, repo_name):
permission = ReadRepositoryPermission(namespace, repository) permission = ReadRepositoryPermission(namespace_name, repo_name)
# TODO invalidate token? # TODO invalidate token?
if permission.can() or model.repository.repository_is_public(namespace, repository): if permission.can() or model.repository.repository_is_public(namespace_name, repo_name):
# We can't rely on permissions to tell us if a repo exists anymore # We can't rely on permissions to tell us if a repo exists anymore
logger.debug('Looking up repository') logger.debug('Looking up repository')
repo = model.repository.get_repository(namespace, repository) repo = model.repository.get_repository(namespace_name, repo_name)
if not repo: if not repo:
abort(404, message='Unknown repository', issue='unknown-repo') abort(404, message='Unknown repository', issue='unknown-repo')
@ -280,17 +280,17 @@ def get_repository_images(namespace, repository):
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['DELETE']) @v1_bp.route('/repositories/<repopath:repository>/images', methods=['DELETE'])
@process_auth @process_auth
@parse_repository_name @parse_repository_name()
@generate_headers(scope=GrantType.WRITE_REPOSITORY) @generate_headers(scope=GrantType.WRITE_REPOSITORY)
@anon_allowed @anon_allowed
def delete_repository_images(namespace, repository): def delete_repository_images(namespace_name, repo_name):
abort(501, 'Not Implemented', issue='not-implemented') abort(501, 'Not Implemented', issue='not-implemented')
@v1_bp.route('/repositories/<repopath:repository>/auth', methods=['PUT']) @v1_bp.route('/repositories/<repopath:repository>/auth', methods=['PUT'])
@parse_repository_name @parse_repository_name()
@anon_allowed @anon_allowed
def put_repository_auth(namespace, repository): def put_repository_auth(namespace_name, repo_name):
abort(501, 'Not Implemented', issue='not-implemented') abort(501, 'Not Implemented', issue='not-implemented')

View file

@ -1,10 +1,8 @@
import logging import logging
import json import json
from flask import abort, request, jsonify, make_response, session from flask import abort, request, jsonify, make_response, session
from app import app
from util.names import TAG_ERROR, TAG_REGEX from util.names import TAG_ERROR, TAG_REGEX
from auth.auth import process_auth from auth.auth import process_auth
from auth.permissions import (ReadRepositoryPermission, from auth.permissions import (ReadRepositoryPermission,
@ -22,12 +20,12 @@ logger = logging.getLogger(__name__)
@v1_bp.route('/repositories/<repopath:repository>/tags', methods=['GET']) @v1_bp.route('/repositories/<repopath:repository>/tags', methods=['GET'])
@process_auth @process_auth
@anon_protect @anon_protect
@parse_repository_name @parse_repository_name()
def get_tags(namespace, repository): def get_tags(namespace_name, repo_name):
permission = ReadRepositoryPermission(namespace, repository) permission = ReadRepositoryPermission(namespace_name, repo_name)
if permission.can() or model.repository.repository_is_public(namespace, repository): if permission.can() or model.repository.repository_is_public(namespace_name, repo_name):
tags = model.tag.list_repository_tags(namespace, repository) tags = model.tag.list_repository_tags(namespace_name, repo_name)
tag_map = {tag.name: tag.image.docker_image_id for tag in tags} tag_map = {tag.name: tag.image.docker_image_id for tag in tags}
return jsonify(tag_map) return jsonify(tag_map)
@ -37,13 +35,13 @@ def get_tags(namespace, repository):
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['GET']) @v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['GET'])
@process_auth @process_auth
@anon_protect @anon_protect
@parse_repository_name @parse_repository_name()
def get_tag(namespace, repository, tag): def get_tag(namespace_name, repo_name, tag):
permission = ReadRepositoryPermission(namespace, repository) permission = ReadRepositoryPermission(namespace_name, repo_name)
if permission.can() or model.repository.repository_is_public(namespace, repository): if permission.can() or model.repository.repository_is_public(namespace_name, repo_name):
try: try:
tag_image = model.tag.get_tag_image(namespace, repository, tag) tag_image = model.tag.get_tag_image(namespace_name, repo_name, tag)
except model.DataModelException: except model.DataModelException:
abort(404) abort(404)
@ -57,19 +55,19 @@ def get_tag(namespace, repository, tag):
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['PUT']) @v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['PUT'])
@process_auth @process_auth
@anon_protect @anon_protect
@parse_repository_name @parse_repository_name()
def put_tag(namespace, repository, tag): def put_tag(namespace_name, repo_name, tag):
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace_name, repo_name)
if permission.can(): if permission.can():
if not TAG_REGEX.match(tag): if not TAG_REGEX.match(tag):
abort(400, TAG_ERROR) abort(400, TAG_ERROR)
docker_image_id = json.loads(request.data) docker_image_id = json.loads(request.data)
model.tag.create_or_update_tag(namespace, repository, tag, docker_image_id) model.tag.create_or_update_tag(namespace_name, repo_name, tag, docker_image_id)
# Store the updated tag. # Store the updated tag.
if not 'pushed_tags' in session: if 'pushed_tags' not in session:
session['pushed_tags'] = {} session['pushed_tags'] = {}
session['pushed_tags'][tag] = docker_image_id session['pushed_tags'][tag] = docker_image_id
@ -82,13 +80,13 @@ def put_tag(namespace, repository, tag):
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['DELETE']) @v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['DELETE'])
@process_auth @process_auth
@anon_protect @anon_protect
@parse_repository_name @parse_repository_name()
def delete_tag(namespace, repository, tag): def delete_tag(namespace_name, repo_name, tag):
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace_name, repo_name)
if permission.can(): if permission.can():
model.tag.delete_tag(namespace, repository, tag) model.tag.delete_tag(namespace_name, repo_name, tag)
track_and_log('delete_tag', model.repository.get_repository(namespace, repository), track_and_log('delete_tag', model.repository.get_repository(namespace_name, repo_name),
tag=tag) tag=tag)
return make_response('Deleted', 200) return make_response('Deleted', 200)

View file

@ -41,13 +41,14 @@ def handle_registry_v2_exception(error):
def _require_repo_permission(permission_class, allow_public=False): def _require_repo_permission(permission_class, allow_public=False):
def wrapper(func): def wrapper(func):
@wraps(func) @wraps(func)
def wrapped(namespace, repo_name, *args, **kwargs): def wrapped(namespace_name, repo_name, *args, **kwargs):
logger.debug('Checking permission %s for repo: %s/%s', permission_class, namespace, repo_name) logger.debug('Checking permission %s for repo: %s/%s', permission_class,
permission = permission_class(namespace, repo_name) namespace_name, repo_name)
permission = permission_class(namespace_name, repo_name)
if (permission.can() or if (permission.can() or
(allow_public and (allow_public and
model.repository.repository_is_public(namespace, repo_name))): model.repository.repository_is_public(namespace_name, repo_name))):
return func(namespace, repo_name, *args, **kwargs) return func(namespace_name, repo_name, *args, **kwargs)
raise Unauthorized() raise Unauthorized()
return wrapped return wrapped
return wrapper return wrapper

View file

@ -9,6 +9,7 @@ from app import storage, app
from auth.registry_jwt_auth import process_registry_jwt_auth from auth.registry_jwt_auth import process_registry_jwt_auth
from data import model, database from data import model, database
from digest import digest_tools from digest import digest_tools
from endpoints.common import parse_repository_name
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write, get_input_stream from endpoints.v2 import v2_bp, require_repo_read, require_repo_write, get_input_stream
from endpoints.v2.errors import (BlobUnknown, BlobUploadInvalid, BlobUploadUnknown, Unsupported, from endpoints.v2.errors import (BlobUnknown, BlobUploadInvalid, BlobUploadUnknown, Unsupported,
NameUnknown) NameUnknown)
@ -17,7 +18,6 @@ from util.cache import cache_control
from util.registry.filelike import wrap_with_handler, StreamSlice from util.registry.filelike import wrap_with_handler, StreamSlice
from util.registry.gzipstream import calculate_size_handler from util.registry.gzipstream import calculate_size_handler
from util.registry.torrent import PieceHasher from util.registry.torrent import PieceHasher
from endpoints.common import parse_repository_name
from storage.basestorage import InvalidChunkException from storage.basestorage import InvalidChunkException
@ -34,12 +34,12 @@ class _InvalidRangeHeader(Exception):
pass pass
def _base_blob_fetch(namespace, repo_name, digest): def _base_blob_fetch(namespace_name, repo_name, digest):
""" Some work that is common to both GET and HEAD requests. Callers MUST check for proper """ Some work that is common to both GET and HEAD requests. Callers MUST check for proper
authorization before calling this method. authorization before calling this method.
""" """
try: try:
found = model.blob.get_repo_blob_by_digest(namespace, repo_name, digest) found = model.blob.get_repo_blob_by_digest(namespace_name, repo_name, digest)
except model.BlobDoesNotExist: except model.BlobDoesNotExist:
raise BlobUnknown() raise BlobUnknown()
@ -58,12 +58,12 @@ def _base_blob_fetch(namespace, repo_name, digest):
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD']) @v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_read @require_repo_read
@anon_protect @anon_protect
@cache_control(max_age=31436000) @cache_control(max_age=31436000)
def check_blob_exists(namespace, repo_name, digest): def check_blob_exists(namespace_name, repo_name, digest):
found, headers = _base_blob_fetch(namespace, repo_name, digest) found, headers = _base_blob_fetch(namespace_name, repo_name, digest)
response = make_response('') response = make_response('')
response.headers.extend(headers) response.headers.extend(headers)
@ -74,12 +74,12 @@ def check_blob_exists(namespace, repo_name, digest):
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET']) @v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_read @require_repo_read
@anon_protect @anon_protect
@cache_control(max_age=31536000) @cache_control(max_age=31536000)
def download_blob(namespace, repo_name, digest): def download_blob(namespace_name, repo_name, digest):
found, headers = _base_blob_fetch(namespace, repo_name, digest) found, headers = _base_blob_fetch(namespace_name, repo_name, digest)
path = model.storage.get_layer_path(found) path = model.storage.get_layer_path(found)
logger.debug('Looking up the direct download URL for path: %s', path) logger.debug('Looking up the direct download URL for path: %s', path)
@ -108,15 +108,15 @@ def _render_range(num_uploaded_bytes, with_bytes_prefix=True):
@v2_bp.route('/<repopath:repository>/blobs/uploads/', methods=['POST']) @v2_bp.route('/<repopath:repository>/blobs/uploads/', methods=['POST'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_write @require_repo_write
@anon_protect @anon_protect
def start_blob_upload(namespace, repo_name): def start_blob_upload(namespace_name, repo_name):
location_name = storage.preferred_locations[0] location_name = storage.preferred_locations[0]
new_upload_uuid, upload_metadata = storage.initiate_chunked_upload(location_name) new_upload_uuid, upload_metadata = storage.initiate_chunked_upload(location_name)
try: try:
model.blob.initiate_upload(namespace, repo_name, new_upload_uuid, location_name, model.blob.initiate_upload(namespace_name, repo_name, new_upload_uuid, location_name,
upload_metadata) upload_metadata)
except database.Repository.DoesNotExist: except database.Repository.DoesNotExist:
raise NameUnknown() raise NameUnknown()
@ -126,7 +126,7 @@ def start_blob_upload(namespace, repo_name):
# The user will send the blob data in another request # The user will send the blob data in another request
accepted = make_response('', 202) accepted = make_response('', 202)
accepted.headers['Location'] = url_for('v2.upload_chunk', accepted.headers['Location'] = url_for('v2.upload_chunk',
repository='%s/%s' % (namespace, repo_name), repository='%s/%s' % (namespace_name, repo_name),
upload_uuid=new_upload_uuid) upload_uuid=new_upload_uuid)
accepted.headers['Range'] = _render_range(0) accepted.headers['Range'] = _render_range(0)
@ -134,22 +134,22 @@ def start_blob_upload(namespace, repo_name):
return accepted return accepted
else: else:
# The user plans to send us the entire body right now # The user plans to send us the entire body right now
uploaded, error = _upload_chunk(namespace, repo_name, new_upload_uuid) uploaded, error = _upload_chunk(namespace_name, repo_name, new_upload_uuid)
uploaded.save() uploaded.save()
if error: if error:
_range_not_satisfiable(uploaded.byte_count) _range_not_satisfiable(uploaded.byte_count)
return _finish_upload(namespace, repo_name, uploaded, digest) return _finish_upload(namespace_name, repo_name, uploaded, digest)
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['GET']) @v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['GET'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_write @require_repo_write
@anon_protect @anon_protect
def fetch_existing_upload(namespace, repo_name, upload_uuid): def fetch_existing_upload(namespace_name, repo_name, upload_uuid):
try: try:
found = model.blob.get_blob_upload(namespace, repo_name, upload_uuid) found = model.blob.get_blob_upload(namespace_name, repo_name, upload_uuid)
except model.InvalidBlobUpload: except model.InvalidBlobUpload:
raise BlobUploadUnknown() raise BlobUploadUnknown()
@ -189,12 +189,12 @@ def _parse_range_header(range_header_text):
return (start, length) return (start, length)
def _upload_chunk(namespace, repo_name, upload_uuid): def _upload_chunk(namespace_name, repo_name, upload_uuid):
""" Common code among the various uploading paths for appending data to blobs. """ Common code among the various uploading paths for appending data to blobs.
Callers MUST call .save() or .delete_instance() on the returned database object. Callers MUST call .save() or .delete_instance() on the returned database object.
""" """
try: try:
found = model.blob.get_blob_upload(namespace, repo_name, upload_uuid) found = model.blob.get_blob_upload(namespace_name, repo_name, upload_uuid)
except model.InvalidBlobUpload: except model.InvalidBlobUpload:
raise BlobUploadUnknown() raise BlobUploadUnknown()
@ -280,7 +280,7 @@ def _upload_chunk(namespace, repo_name, upload_uuid):
return found, error return found, error
def _finish_upload(namespace, repo_name, upload_obj, expected_digest): def _finish_upload(namespace_name, repo_name, upload_obj, expected_digest):
# Verify that the digest's SHA matches that of the uploaded data. # Verify that the digest's SHA matches that of the uploaded data.
computed_digest = digest_tools.sha256_digest_from_hashlib(upload_obj.sha_state) computed_digest = digest_tools.sha256_digest_from_hashlib(upload_obj.sha_state)
if not digest_tools.digests_equal(computed_digest, expected_digest): if not digest_tools.digests_equal(computed_digest, expected_digest):
@ -303,7 +303,7 @@ def _finish_upload(namespace, repo_name, upload_obj, expected_digest):
final_blob_location, upload_obj.storage_metadata) final_blob_location, upload_obj.storage_metadata)
# Mark the blob as uploaded. # Mark the blob as uploaded.
blob_storage = model.blob.store_blob_record_and_temp_link(namespace, repo_name, expected_digest, blob_storage = model.blob.store_blob_record_and_temp_link(namespace_name, repo_name, expected_digest,
upload_obj.location, upload_obj.location,
upload_obj.byte_count, upload_obj.byte_count,
app.config['PUSH_TEMP_TAG_EXPIRATION_SEC'], app.config['PUSH_TEMP_TAG_EXPIRATION_SEC'],
@ -319,18 +319,18 @@ def _finish_upload(namespace, repo_name, upload_obj, expected_digest):
response = make_response('', 201) response = make_response('', 201)
response.headers['Docker-Content-Digest'] = expected_digest response.headers['Docker-Content-Digest'] = expected_digest
response.headers['Location'] = url_for('v2.download_blob', response.headers['Location'] = url_for('v2.download_blob',
repository='%s/%s' % (namespace, repo_name), repository='%s/%s' % (namespace_name, repo_name),
digest=expected_digest) digest=expected_digest)
return response return response
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PATCH']) @v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PATCH'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_write @require_repo_write
@anon_protect @anon_protect
def upload_chunk(namespace, repo_name, upload_uuid): def upload_chunk(namespace_name, repo_name, upload_uuid):
upload, error = _upload_chunk(namespace, repo_name, upload_uuid) upload, error = _upload_chunk(namespace_name, repo_name, upload_uuid)
upload.save() upload.save()
if error: if error:
@ -345,31 +345,31 @@ def upload_chunk(namespace, repo_name, upload_uuid):
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PUT']) @v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PUT'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_write @require_repo_write
@anon_protect @anon_protect
def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid): def monolithic_upload_or_last_chunk(namespace_name, repo_name, upload_uuid):
digest = request.args.get('digest', None) digest = request.args.get('digest', None)
if digest is None: if digest is None:
raise BlobUploadInvalid() raise BlobUploadInvalid()
found, error = _upload_chunk(namespace, repo_name, upload_uuid) found, error = _upload_chunk(namespace_name, repo_name, upload_uuid)
if error: if error:
found.save() found.save()
_range_not_satisfiable(found.byte_count) _range_not_satisfiable(found.byte_count)
return _finish_upload(namespace, repo_name, found, digest) return _finish_upload(namespace_name, repo_name, found, digest)
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['DELETE']) @v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['DELETE'])
@parse_repository_name()
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name
@require_repo_write @require_repo_write
@anon_protect @anon_protect
def cancel_upload(namespace, repo_name, upload_uuid): def cancel_upload(namespace_name, repo_name, upload_uuid):
try: try:
found = model.blob.get_blob_upload(namespace, repo_name, upload_uuid) found = model.blob.get_blob_upload(namespace_name, repo_name, upload_uuid)
except model.InvalidBlobUpload: except model.InvalidBlobUpload:
raise BlobUploadUnknown() raise BlobUploadUnknown()
@ -384,11 +384,9 @@ def cancel_upload(namespace, repo_name, upload_uuid):
@v2_bp.route('/<repopath:repository>/blobs/<digest>', methods=['DELETE']) @v2_bp.route('/<repopath:repository>/blobs/<digest>', methods=['DELETE'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_write @require_repo_write
@anon_protect @anon_protect
def delete_digest(namespace, repo_name, upload_uuid): def delete_digest(namespace_name, repo_name, upload_uuid):
# We do not support deleting arbitrary digests, as they break repo images. # We do not support deleting arbitrary digests, as they break repo images.
raise Unsupported() raise Unsupported()

View file

@ -1,18 +1,22 @@
import logging import logging
import jwt.utils
import json import json
import features
import hashlib import hashlib
from peewee import IntegrityError
from flask import make_response, request, url_for
from collections import namedtuple, OrderedDict from collections import namedtuple, OrderedDict
from jwkest.jws import SIGNER_ALGS, keyrep
from datetime import datetime from datetime import datetime
from functools import wraps from functools import wraps
import jwt.utils
from peewee import IntegrityError
from flask import make_response, request, url_for
from jwkest.jws import SIGNER_ALGS, keyrep
import features
from app import docker_v2_signing_key, app from app import docker_v2_signing_key, app
from auth.registry_jwt_auth import process_registry_jwt_auth from auth.registry_jwt_auth import process_registry_jwt_auth
from endpoints.common import parse_repository_name
from endpoints.decorators import anon_protect from endpoints.decorators import anon_protect
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write from endpoints.v2 import v2_bp, require_repo_read, require_repo_write
from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnknown, TagInvalid, from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnknown, TagInvalid,
@ -22,7 +26,6 @@ from endpoints.notificationhelper import spawn_notification
from digest import digest_tools from digest import digest_tools
from data import model from data import model
from data.database import RepositoryTag from data.database import RepositoryTag
from endpoints.common import parse_repository_name
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -84,7 +87,8 @@ class SignedManifest(object):
def _validate(self): def _validate(self):
for signature in self._signatures: for signature in self._signatures:
bytes_to_verify = '{0}.{1}'.format(signature['protected'], jwt.utils.base64url_encode(self.payload)) bytes_to_verify = '{0}.{1}'.format(signature['protected'],
jwt.utils.base64url_encode(self.payload))
signer = SIGNER_ALGS[signature['header']['alg']] signer = SIGNER_ALGS[signature['header']['alg']]
key = keyrep(signature['header']['jwk']) key = keyrep(signature['header']['jwk'])
gk = key.get_key() gk = key.get_key()
@ -163,9 +167,9 @@ class SignedManifest(object):
class SignedManifestBuilder(object): class SignedManifestBuilder(object):
""" Class which represents a manifest which is currently being built. """ Class which represents a manifest which is currently being built.
""" """
def __init__(self, namespace, repo_name, tag, architecture='amd64', schema_ver=1): def __init__(self, namespace_name, repo_name, tag, architecture='amd64', schema_ver=1):
repo_name_key = '{0}/{1}'.format(namespace, repo_name) repo_name_key = '{0}/{1}'.format(namespace_name, repo_name)
if namespace == '': if namespace_name == '':
repo_name_key = repo_name repo_name_key = repo_name
self._base_payload = { self._base_payload = {
@ -238,26 +242,26 @@ class SignedManifestBuilder(object):
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['GET']) @v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['GET'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_read @require_repo_read
@anon_protect @anon_protect
def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref): def fetch_manifest_by_tagname(namespace_name, repo_name, manifest_ref):
try: try:
manifest = model.tag.load_tag_manifest(namespace, repo_name, manifest_ref) manifest = model.tag.load_tag_manifest(namespace_name, repo_name, manifest_ref)
except model.InvalidManifestException: except model.InvalidManifestException:
try: try:
model.tag.get_active_tag(namespace, repo_name, manifest_ref) model.tag.get_active_tag(namespace_name, repo_name, manifest_ref)
except RepositoryTag.DoesNotExist: except RepositoryTag.DoesNotExist:
raise ManifestUnknown() raise ManifestUnknown()
try: try:
manifest = _generate_and_store_manifest(namespace, repo_name, manifest_ref) manifest = _generate_and_store_manifest(namespace_name, repo_name, manifest_ref)
except model.DataModelException: except model.DataModelException:
logger.exception('Exception when generating manifest for %s/%s:%s', namespace, repo_name, logger.exception('Exception when generating manifest for %s/%s:%s', namespace_name, repo_name,
manifest_ref) manifest_ref)
raise ManifestUnknown() raise ManifestUnknown()
repo = model.repository.get_repository(namespace, repo_name) repo = model.repository.get_repository(namespace_name, repo_name)
if repo is not None: if repo is not None:
track_and_log('pull_repo', repo, analytics_name='pull_repo_100x', analytics_sample=0.01) track_and_log('pull_repo', repo, analytics_name='pull_repo_100x', analytics_sample=0.01)
@ -269,17 +273,17 @@ def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['GET']) @v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['GET'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_read @require_repo_read
@anon_protect @anon_protect
def fetch_manifest_by_digest(namespace, repo_name, manifest_ref): def fetch_manifest_by_digest(namespace_name, repo_name, manifest_ref):
try: try:
manifest = model.tag.load_manifest_by_digest(namespace, repo_name, manifest_ref) manifest = model.tag.load_manifest_by_digest(namespace_name, repo_name, manifest_ref)
except model.InvalidManifestException: except model.InvalidManifestException:
# Without a tag name to reference, we can't make an attempt to generate the manifest # Without a tag name to reference, we can't make an attempt to generate the manifest
raise ManifestUnknown() raise ManifestUnknown()
repo = model.repository.get_repository(namespace, repo_name) repo = model.repository.get_repository(namespace_name, repo_name)
if repo is not None: if repo is not None:
track_and_log('pull_repo', repo) track_and_log('pull_repo', repo)
@ -301,11 +305,11 @@ def _reject_manifest2_schema2(func):
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['PUT']) @v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['PUT'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_write @require_repo_write
@anon_protect @anon_protect
@_reject_manifest2_schema2 @_reject_manifest2_schema2
def write_manifest_by_tagname(namespace, repo_name, manifest_ref): def write_manifest_by_tagname(namespace_name, repo_name, manifest_ref):
try: try:
manifest = SignedManifest(request.data) manifest = SignedManifest(request.data)
except ValueError: except ValueError:
@ -314,16 +318,16 @@ def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
if manifest.tag != manifest_ref: if manifest.tag != manifest_ref:
raise TagInvalid() raise TagInvalid()
return _write_manifest(namespace, repo_name, manifest) return _write_manifest(namespace_name, repo_name, manifest)
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['PUT']) @v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['PUT'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_write @require_repo_write
@anon_protect @anon_protect
@_reject_manifest2_schema2 @_reject_manifest2_schema2
def write_manifest_by_digest(namespace, repo_name, manifest_ref): def write_manifest_by_digest(namespace_name, repo_name, manifest_ref):
try: try:
manifest = SignedManifest(request.data) manifest = SignedManifest(request.data)
except ValueError: except ValueError:
@ -332,7 +336,7 @@ def write_manifest_by_digest(namespace, repo_name, manifest_ref):
if manifest.digest != manifest_ref: if manifest.digest != manifest_ref:
raise ManifestInvalid(detail={'message': 'manifest digest mismatch'}) raise ManifestInvalid(detail={'message': 'manifest digest mismatch'})
return _write_manifest(namespace, repo_name, manifest) return _write_manifest(namespace_name, repo_name, manifest)
def _updated_v1_metadata(v1_metadata_json, updated_id_map): def _updated_v1_metadata(v1_metadata_json, updated_id_map):
@ -350,21 +354,21 @@ def _updated_v1_metadata(v1_metadata_json, updated_id_map):
return json.dumps(parsed) return json.dumps(parsed)
def _write_manifest(namespace, repo_name, manifest): def _write_manifest(namespace_name, repo_name, manifest):
# Ensure that the manifest is for this repository. If the manifest's namespace is empty, then # 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. # it is for the library namespace and we need an extra check.
if (manifest.namespace == '' and features.LIBRARY_SUPPORT and if (manifest.namespace == '' and features.LIBRARY_SUPPORT and
namespace == app.config['LIBRARY_NAMESPACE']): namespace_name == app.config['LIBRARY_NAMESPACE']):
# This is a library manifest. All good. # This is a library manifest. All good.
pass pass
elif manifest.namespace != namespace: elif manifest.namespace != namespace_name:
raise NameInvalid() raise NameInvalid()
if manifest.repo_name != repo_name: if manifest.repo_name != repo_name:
raise NameInvalid() raise NameInvalid()
# Ensure that the repository exists. # Ensure that the repository exists.
repo = model.repository.get_repository(namespace, repo_name) repo = model.repository.get_repository(namespace_name, repo_name)
if repo is None: if repo is None:
raise NameInvalid() raise NameInvalid()
@ -411,8 +415,8 @@ def _write_manifest(namespace, repo_name, manifest):
v1_metadata_str = mdata.v1_metadata_str.encode('utf-8') v1_metadata_str = mdata.v1_metadata_str.encode('utf-8')
working_docker_id = hashlib.sha256(v1_metadata_str + '@' + digest_str).hexdigest() working_docker_id = hashlib.sha256(v1_metadata_str + '@' + digest_str).hexdigest()
logger.debug('Rewriting docker_id %s/%s %s -> %s', namespace, repo_name, v1_mdata.docker_id, logger.debug('Rewriting docker_id %s/%s %s -> %s', namespace_name, repo_name,
working_docker_id) v1_mdata.docker_id, working_docker_id)
has_rewritten_ids = True has_rewritten_ids = True
# Store the new docker id in the map # Store the new docker id in the map
@ -447,7 +451,7 @@ def _write_manifest(namespace, repo_name, manifest):
# Store the manifest pointing to the tag. # Store the manifest pointing to the tag.
manifest_digest = manifest.digest manifest_digest = manifest.digest
leaf_layer_id = images_map[layers[-1].v1_metadata.docker_id].docker_image_id leaf_layer_id = images_map[layers[-1].v1_metadata.docker_id].docker_image_id
model.tag.store_tag_manifest(namespace, repo_name, tag_name, leaf_layer_id, manifest_digest, model.tag.store_tag_manifest(namespace_name, repo_name, tag_name, leaf_layer_id, manifest_digest,
manifest.bytes) manifest.bytes)
# Spawn the repo_push event. # Spawn the repo_push event.
@ -461,29 +465,29 @@ def _write_manifest(namespace, repo_name, manifest):
response = make_response('OK', 202) response = make_response('OK', 202)
response.headers['Docker-Content-Digest'] = manifest_digest response.headers['Docker-Content-Digest'] = manifest_digest
response.headers['Location'] = url_for('v2.fetch_manifest_by_digest', response.headers['Location'] = url_for('v2.fetch_manifest_by_digest',
repository='%s/%s' % (namespace, repo_name), repository='%s/%s' % (namespace_name, repo_name),
manifest_ref=manifest_digest) manifest_ref=manifest_digest)
return response return response
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['DELETE']) @v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['DELETE'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_write @require_repo_write
@anon_protect @anon_protect
def delete_manifest_by_digest(namespace, repo_name, manifest_ref): def delete_manifest_by_digest(namespace_name, repo_name, manifest_ref):
""" Delete the manifest specified by the digest. Note: there is no equivalent """ Delete the manifest specified by the digest. Note: there is no equivalent
method for deleting by tag name because it is forbidden by the spec. method for deleting by tag name because it is forbidden by the spec.
""" """
try: try:
manifest = model.tag.load_manifest_by_digest(namespace, repo_name, manifest_ref) manifest = model.tag.load_manifest_by_digest(namespace_name, repo_name, manifest_ref)
except model.InvalidManifestException: except model.InvalidManifestException:
# Without a tag name to reference, we can't make an attempt to generate the manifest # Without a tag name to reference, we can't make an attempt to generate the manifest
raise ManifestUnknown() raise ManifestUnknown()
# Mark the tag as no longer alive. # Mark the tag as no longer alive.
try: try:
model.tag.delete_tag(namespace, repo_name, manifest.tag.name) model.tag.delete_tag(namespace_name, repo_name, manifest.tag.name)
except model.DataModelException: except model.DataModelException:
# Tag is not alive. # Tag is not alive.
raise ManifestUnknown() raise ManifestUnknown()
@ -494,15 +498,15 @@ def delete_manifest_by_digest(namespace, repo_name, manifest_ref):
return make_response('', 202) return make_response('', 202)
def _generate_and_store_manifest(namespace, repo_name, tag_name): def _generate_and_store_manifest(namespace_name, repo_name, tag_name):
# First look up the tag object and its ancestors # First look up the tag object and its ancestors
image = model.tag.get_tag_image(namespace, repo_name, tag_name) image = model.tag.get_tag_image(namespace_name, repo_name, tag_name)
parents = model.image.get_parent_images(namespace, repo_name, image) parents = model.image.get_parent_images(namespace_name, repo_name, image)
# If the manifest is being generated under the library namespace, then we make its namespace # If the manifest is being generated under the library namespace, then we make its namespace
# empty. # empty.
manifest_namespace = namespace manifest_namespace = namespace_name
if features.LIBRARY_SUPPORT and namespace == app.config['LIBRARY_NAMESPACE']: if features.LIBRARY_SUPPORT and namespace_name == app.config['LIBRARY_NAMESPACE']:
manifest_namespace = '' manifest_namespace = ''
# Create and populate the manifest builder # Create and populate the manifest builder
@ -520,13 +524,12 @@ def _generate_and_store_manifest(namespace, repo_name, tag_name):
# Write the manifest to the DB. If an existing manifest already exists, return the # Write the manifest to the DB. If an existing manifest already exists, return the
# one found. # one found.
try: try:
return model.tag.associate_generated_tag_manifest(namespace, repo_name, tag_name, return model.tag.associate_generated_tag_manifest(namespace_name, repo_name, tag_name,
manifest.digest, manifest.bytes) manifest.digest, manifest.bytes)
except IntegrityError as ie: except IntegrityError as ie:
logger.debug('Got integrity error: %s', ie) logger.debug('Got integrity error: %s', ie)
try: try:
return model.tag.load_tag_manifest(namespace, repo_name, tag_name) return model.tag.load_tag_manifest(namespace_name, repo_name, tag_name)
except model.InvalidManifestException: except model.InvalidManifestException:
logger.exception('Exception when generating manifest') logger.exception('Exception when generating manifest')
raise model.DataModelException('Could not load or generate manifest') raise model.DataModelException('Could not load or generate manifest')

View file

@ -1,29 +1,29 @@
from flask import jsonify, url_for from flask import jsonify, url_for
from auth.registry_jwt_auth import process_registry_jwt_auth from auth.registry_jwt_auth import process_registry_jwt_auth
from endpoints.common import parse_repository_name
from endpoints.v2 import v2_bp, require_repo_read from endpoints.v2 import v2_bp, require_repo_read
from endpoints.v2.errors import NameUnknown from endpoints.v2.errors import NameUnknown
from endpoints.v2.v2util import add_pagination from endpoints.v2.v2util import add_pagination
from endpoints.decorators import anon_protect from endpoints.decorators import anon_protect
from data import model from data import model
from endpoints.common import parse_repository_name
@v2_bp.route('/<repopath:repository>/tags/list', methods=['GET']) @v2_bp.route('/<repopath:repository>/tags/list', methods=['GET'])
@process_registry_jwt_auth @process_registry_jwt_auth
@parse_repository_name @parse_repository_name()
@require_repo_read @require_repo_read
@anon_protect @anon_protect
def list_all_tags(namespace, repo_name): def list_all_tags(namespace_name, repo_name):
repository = model.repository.get_repository(namespace, repo_name) repository = model.repository.get_repository(namespace_name, repo_name)
if repository is None: if repository is None:
raise NameUnknown() raise NameUnknown()
query = model.tag.list_repository_tags(namespace, repo_name) query = model.tag.list_repository_tags(namespace_name, repo_name)
url = url_for('v2.list_all_tags', repository='%s/%s' % (namespace, repo_name)) url = url_for('v2.list_all_tags', repository='%s/%s' % (namespace_name, repo_name))
link, query = add_pagination(query, url) link, query = add_pagination(query, url)
response = jsonify({ response = jsonify({
'name': '{0}/{1}'.format(namespace, repo_name), 'name': '{0}/{1}'.format(namespace_name, repo_name),
'tags': [tag.name for tag in query], 'tags': [tag.name for tag in query],
}) })

View file

@ -381,10 +381,10 @@ def get_squashed_tag(namespace, repository, tag):
@anon_protect @anon_protect
@verbs.route('/torrent{0}'.format(BLOB_DIGEST_ROUTE), methods=['GET']) @verbs.route('/torrent{0}'.format(BLOB_DIGEST_ROUTE), methods=['GET'])
@process_auth @process_auth
@parse_repository_name @parse_repository_name()
def get_tag_torrent(namespace, repo_name, digest): def get_tag_torrent(namespace_name, repo_name, digest):
permission = ReadRepositoryPermission(namespace, repo_name) permission = ReadRepositoryPermission(namespace_name, repo_name)
public_repo = model.repository.repository_is_public(namespace, repo_name) public_repo = model.repository.repository_is_public(namespace_name, repo_name)
if not permission.can() and not public_repo: if not permission.can() and not public_repo:
abort(403) abort(403)
@ -394,7 +394,7 @@ def get_tag_torrent(namespace, repo_name, digest):
abort(403) abort(403)
try: try:
blob = model.blob.get_repo_blob_by_digest(namespace, repo_name, digest) blob = model.blob.get_repo_blob_by_digest(namespace_name, repo_name, digest)
except model.BlobDoesNotExist: except model.BlobDoesNotExist:
abort(404) abort(404)

View file

@ -26,7 +26,7 @@ from data import model
from data.database import db from data.database import db
from endpoints.api.discovery import swagger_route_data 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) parse_repository_name)
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
from endpoints.decorators import anon_protect, anon_allowed from endpoints.decorators import anon_protect, anon_allowed
from health.healthcheck import get_healthchecker from health.healthcheck import get_healthchecker
@ -411,20 +411,20 @@ def confirm_recovery():
@web.route('/repository/<repopath:repository>/status', methods=['GET']) @web.route('/repository/<repopath:repository>/status', methods=['GET'])
@parse_repository_name @parse_repository_name()
@anon_protect @anon_protect
def build_status_badge(namespace, repository): def build_status_badge(namespace_name, repo_name):
token = request.args.get('token', None) token = request.args.get('token', None)
is_public = model.repository.repository_is_public(namespace, repository) is_public = model.repository.repository_is_public(namespace_name, repo_name)
if not is_public: if not is_public:
repo = model.repository.get_repository(namespace, repository) repo = model.repository.get_repository(namespace_name, repo_name)
if not repo or token != repo.badge_token: if not repo or token != repo.badge_token:
abort(404) abort(404)
# Lookup the tags for the repository. # Lookup the tags for the repository.
tags = model.tag.list_repository_tags(namespace, repository) tags = model.tag.list_repository_tags(namespace_name, repo_name)
is_empty = len(list(tags)) == 0 is_empty = len(list(tags)) == 0
recent_build = model.build.get_recent_repository_build(namespace, repository) recent_build = model.build.get_recent_repository_build(namespace_name, repo_name)
if not is_empty and (not recent_build or recent_build.phase == 'complete'): if not is_empty and (not recent_build or recent_build.phase == 'complete'):
status_name = 'ready' status_name = 'ready'
@ -600,14 +600,14 @@ def download_logs_archive():
@web.route('/bitbucket/setup/<repopath:repository>', methods=['GET']) @web.route('/bitbucket/setup/<repopath:repository>', methods=['GET'])
@require_session_login @require_session_login
@parse_repository_name @parse_repository_name()
@route_show_if(features.BITBUCKET_BUILD) @route_show_if(features.BITBUCKET_BUILD)
def attach_bitbucket_trigger(namespace, repository_name): def attach_bitbucket_trigger(namespace_name, repo_name):
permission = AdministerRepositoryPermission(namespace, repository_name) permission = AdministerRepositoryPermission(namespace_name, repo_name)
if permission.can(): if permission.can():
repo = model.repository.get_repository(namespace, repository_name) repo = model.repository.get_repository(namespace_name, repo_name)
if not repo: if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository_name) msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
abort(404, message=msg) abort(404, message=msg)
trigger = model.build.create_build_trigger(repo, BitbucketBuildTrigger.service_name(), None, trigger = model.build.create_build_trigger(repo, BitbucketBuildTrigger.service_name(), None,
@ -634,19 +634,19 @@ def attach_bitbucket_trigger(namespace, repository_name):
@web.route('/customtrigger/setup/<repopath:repository>', methods=['GET']) @web.route('/customtrigger/setup/<repopath:repository>', methods=['GET'])
@require_session_login @require_session_login
@parse_repository_name @parse_repository_name()
def attach_custom_build_trigger(namespace, repository_name): def attach_custom_build_trigger(namespace_name, repo_name):
permission = AdministerRepositoryPermission(namespace, repository_name) permission = AdministerRepositoryPermission(namespace_name, repo_name)
if permission.can(): if permission.can():
repo = model.repository.get_repository(namespace, repository_name) repo = model.repository.get_repository(namespace_name, repo_name)
if not repo: if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository_name) msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
abort(404, message=msg) abort(404, message=msg)
trigger = model.build.create_build_trigger(repo, CustomBuildTrigger.service_name(), trigger = model.build.create_build_trigger(repo, CustomBuildTrigger.service_name(),
None, current_user.db_user()) None, current_user.db_user())
repo_path = '%s/%s' % (namespace, repository_name) repo_path = '%s/%s' % (namespace_name, repo_name)
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=', full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
trigger.uuid) trigger.uuid)
@ -655,21 +655,22 @@ def attach_custom_build_trigger(namespace, repository_name):
abort(403) abort(403)
@web.route('/<repopath:repository>') @web.route('/<repopath:repository>')
@no_cache @no_cache
@process_oauth @process_oauth
@parse_repository_name_and_tag @parse_repository_name(include_tag=True)
@anon_protect @anon_protect
def redirect_to_repository(namespace, reponame, tag_name): def redirect_to_repository(namespace_name, repo_name, tag_name):
permission = ReadRepositoryPermission(namespace, reponame) permission = ReadRepositoryPermission(namespace_name, repo_name)
is_public = model.repository.repository_is_public(namespace, reponame) is_public = model.repository.repository_is_public(namespace_name, repo_name)
if request.args.get('ac-discovery', 0) == 1: if request.args.get('ac-discovery', 0) == 1:
return index('') return index('')
if permission.can() or is_public: if permission.can() or is_public:
repository_name = '/'.join([namespace, reponame]) repo_path = '/'.join([namespace_name, repo_name])
return redirect(url_for('web.repository', path=repository_name, tag=tag_name)) return redirect(url_for('web.repository', path=repo_path, tag=tag_name))
abort(404) abort(404)

View file

@ -5,4 +5,4 @@ export TROLLIUSDEBUG=1
python -m unittest discover -f python -m unittest discover -f
python -m test.registry_tests -f python -m test.registry_tests -f
python -m test.queue_threads -f #python -m test.queue_threads -f

View file

@ -1,10 +1,9 @@
import unittest import unittest
import endpoints.decorated
import json import json
from app import app from app import app
from initdb import setup_database_for_testing, finished_database_for_testing from initdb import setup_database_for_testing, finished_database_for_testing
from specs import build_v2_index_specs from test.specs import build_v2_index_specs
from endpoints.v2 import v2_bp from endpoints.v2 import v2_bp
app.register_blueprint(v2_bp, url_prefix='/v2') app.register_blueprint(v2_bp, url_prefix='/v2')