use kwargs for parse_repository_name
This commit is contained in:
parent
3b52a255b2
commit
bb46cc933d
15 changed files with 285 additions and 270 deletions
|
@ -1,8 +1,9 @@
|
|||
import logging
|
||||
import re
|
||||
|
||||
from jsonschema import validate, ValidationError
|
||||
from functools import wraps
|
||||
|
||||
from jsonschema import validate, ValidationError
|
||||
from flask import request, url_for
|
||||
from flask.ext.principal import identity_changed, Identity
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
from flask import request, url_for
|
||||
from urllib import quote
|
||||
from urlparse import urlunparse
|
||||
|
||||
from flask import request, url_for
|
||||
|
||||
from app import app
|
||||
from buildtrigger.basehandler import BuildTriggerHandler
|
||||
from buildtrigger.triggerutil import (TriggerDeactivationException,
|
||||
|
@ -40,9 +41,9 @@ class BuildTriggerList(RepositoryParamResource):
|
|||
|
||||
@require_repo_admin
|
||||
@nickname('listBuildTriggers')
|
||||
def get(self, namespace, repository):
|
||||
def get(self, namespace_name, repo_name):
|
||||
""" 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 {
|
||||
'triggers': [trigger_view(trigger, can_admin=True) for trigger in triggers]
|
||||
}
|
||||
|
@ -56,7 +57,7 @@ class BuildTrigger(RepositoryParamResource):
|
|||
|
||||
@require_repo_admin
|
||||
@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. """
|
||||
try:
|
||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||
|
@ -67,7 +68,7 @@ class BuildTrigger(RepositoryParamResource):
|
|||
|
||||
@require_repo_admin
|
||||
@nickname('deleteBuildTrigger')
|
||||
def delete(self, namespace, repository, trigger_uuid):
|
||||
def delete(self, namespace_name, repo_name, trigger_uuid):
|
||||
""" Delete the specified build trigger. """
|
||||
try:
|
||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||
|
@ -82,10 +83,10 @@ class BuildTrigger(RepositoryParamResource):
|
|||
# We are just going to eat this error
|
||||
logger.warning('Trigger deactivation problem: %s', ex)
|
||||
|
||||
log_action('delete_repo_trigger', namespace,
|
||||
{'repo': repository, 'trigger_id': trigger_uuid,
|
||||
log_action('delete_repo_trigger', namespace_name,
|
||||
{'repo': repo_name, 'trigger_id': trigger_uuid,
|
||||
'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)
|
||||
|
||||
|
@ -111,7 +112,7 @@ class BuildTriggerSubdirs(RepositoryParamResource):
|
|||
@require_repo_admin
|
||||
@nickname('listBuildTriggerSubdirs')
|
||||
@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. """
|
||||
try:
|
||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||
|
@ -171,7 +172,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
@require_repo_admin
|
||||
@nickname('activateBuildTrigger')
|
||||
@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. """
|
||||
try:
|
||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||
|
@ -198,7 +199,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
raise Unauthorized()
|
||||
|
||||
# Make sure the namespace matches that of the trigger.
|
||||
if robot_namespace != namespace:
|
||||
if robot_namespace != namespace_name:
|
||||
raise Unauthorized()
|
||||
|
||||
# Set the pull robot.
|
||||
|
@ -208,7 +209,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
new_config_dict = request.get_json()['config']
|
||||
|
||||
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')
|
||||
|
||||
try:
|
||||
|
@ -233,12 +234,13 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
trigger.save()
|
||||
|
||||
# Log the trigger setup.
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
log_action('setup_repo_trigger', namespace,
|
||||
{'repo': repository, 'namespace': namespace,
|
||||
'trigger_id': trigger.uuid, 'service': trigger.service.name,
|
||||
'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
|
||||
'config': final_config}, repo=repo)
|
||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||
log_action('setup_repo_trigger', namespace_name,
|
||||
{'repo': repo_name, 'namespace': namespace_name,
|
||||
'trigger_id': trigger.uuid, 'service': trigger.service.name,
|
||||
'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
|
||||
'config': final_config},
|
||||
repo=repo)
|
||||
|
||||
return trigger_view(trigger, can_admin=True)
|
||||
else:
|
||||
|
@ -271,7 +273,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
|||
@require_repo_admin
|
||||
@nickname('analyzeBuildTrigger')
|
||||
@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. """
|
||||
try:
|
||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||
|
@ -414,7 +416,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
|||
@require_repo_admin
|
||||
@nickname('manuallyStartBuildTrigger')
|
||||
@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. """
|
||||
try:
|
||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||
|
@ -426,7 +428,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
|||
raise InvalidRequest('Trigger is not active.')
|
||||
|
||||
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)
|
||||
|
||||
run_parameters = request.get_json()
|
||||
|
@ -436,7 +438,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
|||
raise InvalidRequest(tse.message)
|
||||
|
||||
resp = build_status_view(build_request)
|
||||
repo_string = '%s/%s' % (namespace, repository)
|
||||
repo_string = '%s/%s' % (namespace_name, repo_name)
|
||||
headers = {
|
||||
'Location': api.url_for(RepositoryBuildStatus, repository=repo_string,
|
||||
build_uuid=build_request.uuid),
|
||||
|
@ -453,10 +455,10 @@ class TriggerBuildList(RepositoryParamResource):
|
|||
@parse_args()
|
||||
@query_param('limit', 'The maximum number of builds to return', type=int, default=5)
|
||||
@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. """
|
||||
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 {
|
||||
'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. """
|
||||
@require_repo_admin
|
||||
@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. """
|
||||
try:
|
||||
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. """
|
||||
@require_repo_admin
|
||||
@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. """
|
||||
try:
|
||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||
|
|
|
@ -47,24 +47,28 @@ def get_cache_busters():
|
|||
return CACHE_BUSTERS
|
||||
|
||||
|
||||
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(include_tag=False,
|
||||
ns_kwarg_name='namespace_name',
|
||||
repo_kwarg_name='repo_name',
|
||||
tag_kwarg_name='tag_name',
|
||||
incoming_repo_kwarg='repository'):
|
||||
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 inner
|
||||
|
||||
|
||||
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
|
||||
|
||||
# TODO get rid of all calls to this parse_repository_name_and_tag
|
||||
|
||||
def route_show_if(value):
|
||||
def decorator(f):
|
||||
|
|
|
@ -3,35 +3,37 @@ import logging
|
|||
from flask import request, redirect, url_for, Blueprint
|
||||
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
|
||||
|
||||
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__)
|
||||
client = app.config['HTTPCLIENT']
|
||||
githubtrigger = Blueprint('callback', __name__)
|
||||
|
||||
|
||||
@githubtrigger.route('/github/callback/trigger/<repopath:repository>', methods=['GET'])
|
||||
@route_show_if(features.GITHUB_BUILD)
|
||||
@require_session_login
|
||||
@parse_repository_name
|
||||
def attach_github_build_trigger(namespace, repository):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
@parse_repository_name()
|
||||
def attach_github_build_trigger(namespace_name, repo_name):
|
||||
permission = AdministerRepositoryPermission(namespace_name, repo_name)
|
||||
if permission.can():
|
||||
code = request.args.get('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:
|
||||
msg = 'Invalid repository: %s/%s' % (namespace, repository)
|
||||
msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
|
||||
abort(404, message=msg)
|
||||
|
||||
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=',
|
||||
trigger.uuid)
|
||||
|
||||
|
|
|
@ -3,18 +3,18 @@ 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, 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
|
||||
|
||||
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.validation import generate_valid_usernames
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
client = app.config['HTTPCLIENT']
|
||||
|
@ -24,10 +24,10 @@ def render_ologin_error(service_name,
|
|||
error_message='Could not load user data. The token may have expired.'):
|
||||
user_creation = features.USER_CREATION and features.DIRECT_LOGIN
|
||||
return render_page_template_with_routedata('ologinerror.html',
|
||||
service_name=service_name,
|
||||
error_message=error_message,
|
||||
service_url=get_app_url(),
|
||||
user_creation=user_creation)
|
||||
service_name=service_name,
|
||||
error_message=error_message,
|
||||
service_url=get_app_url(),
|
||||
user_creation=user_creation)
|
||||
|
||||
|
||||
def get_user(service, token):
|
||||
|
@ -150,7 +150,7 @@ def github_oauth_callback():
|
|||
# Retrieve the user's orgnizations (if organization filtering is turned on)
|
||||
if github_login.allowed_organizations() is not None:
|
||||
get_orgs = client.get(github_login.orgs_endpoint(), params=token_param,
|
||||
headers={'Accept': 'application/vnd.github.moondragon+json'})
|
||||
headers={'Accept': 'application/vnd.github.moondragon+json'})
|
||||
|
||||
organizations = set([org.get('login').lower() for org in get_orgs.json()])
|
||||
if not (organizations & set(github_login.allowed_organizations())):
|
||||
|
@ -271,8 +271,10 @@ def dex_oauth_callback():
|
|||
payload = decode_user_jwt(token, dex_login)
|
||||
except InvalidTokenError:
|
||||
logger.exception('Exception when decoding returned JWT')
|
||||
return render_ologin_error(dex_login.public_title,
|
||||
'Could not decode response. Please contact your system administrator about this error.')
|
||||
return render_ologin_error(
|
||||
dex_login.public_title,
|
||||
'Could not decode response. Please contact your system administrator about this error.',
|
||||
)
|
||||
|
||||
username = get_email_username(payload)
|
||||
metadata = {}
|
||||
|
@ -281,9 +283,11 @@ def dex_oauth_callback():
|
|||
email_address = payload['email']
|
||||
|
||||
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 ' +
|
||||
'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,
|
||||
|
@ -304,8 +308,10 @@ def dex_oauth_attach():
|
|||
payload = decode_user_jwt(token, dex_login)
|
||||
except jwt.InvalidTokenError:
|
||||
logger.exception('Exception when decoding returned JWT')
|
||||
return render_ologin_error(dex_login.public_title,
|
||||
'Could not decode response. Please contact your system administrator about this error.')
|
||||
return render_ologin_error(
|
||||
dex_login.public_title,
|
||||
'Could not decode response. Please contact your system administrator about this error.',
|
||||
)
|
||||
|
||||
user_obj = current_user.db_user()
|
||||
dex_id = payload['sub']
|
||||
|
@ -315,7 +321,7 @@ def dex_oauth_attach():
|
|||
model.user.attach_federated_login(user_obj, 'dex', dex_id, metadata=metadata)
|
||||
except IntegrityError:
|
||||
err = '%s account is already attached to a %s account' % (dex_login.public_title,
|
||||
app.config['REGISTRY_TITLE_SHORT'])
|
||||
app.config['REGISTRY_TITLE_SHORT'])
|
||||
return render_ologin_error(dex_login.public_title, err)
|
||||
|
||||
return redirect(url_for('web.user'))
|
||||
|
|
|
@ -2,19 +2,19 @@ import json
|
|||
import logging
|
||||
import urlparse
|
||||
|
||||
from flask import request, make_response, jsonify, session
|
||||
from functools import wraps
|
||||
|
||||
from flask import request, make_response, jsonify, session
|
||||
|
||||
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_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,
|
||||
ReadRepositoryPermission, CreateRepositoryPermission,
|
||||
repository_read_grant, repository_write_grant)
|
||||
|
||||
from util.http import abort
|
||||
from util.names import REPOSITORY_NAME_REGEX
|
||||
from endpoints.common import parse_repository_name
|
||||
from endpoints.v1 import v1_bp
|
||||
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 decorator_method(f):
|
||||
@wraps(f)
|
||||
def wrapper(namespace, repository, *args, **kwargs):
|
||||
response = f(namespace, repository, *args, **kwargs)
|
||||
def wrapper(namespace_name, repo_name, *args, **kwargs):
|
||||
response = f(namespace_name, repo_name, *args, **kwargs)
|
||||
|
||||
# Setting session namespace and repository
|
||||
session['namespace'] = namespace
|
||||
session['repository'] = repository
|
||||
session['namespace'] = namespace_name
|
||||
session['repository'] = repo_name
|
||||
|
||||
# We run our index and registry on the same hosts for now
|
||||
registry_server = urlparse.urlparse(request.url).netloc
|
||||
|
@ -51,11 +51,11 @@ def generate_headers(scope=GrantType.READ_REPOSITORY, add_grant_for_status=None)
|
|||
grants = []
|
||||
|
||||
if scope == GrantType.READ_REPOSITORY:
|
||||
if force_grant or ReadRepositoryPermission(namespace, repository).can():
|
||||
grants.append(repository_read_grant(namespace, repository))
|
||||
if force_grant or ReadRepositoryPermission(namespace_name, repo_name).can():
|
||||
grants.append(repository_read_grant(namespace_name, repo_name))
|
||||
elif scope == GrantType.WRITE_REPOSITORY:
|
||||
if force_grant or ModifyRepositoryPermission(namespace, repository).can():
|
||||
grants.append(repository_write_grant(namespace, repository))
|
||||
if force_grant or ModifyRepositoryPermission(namespace_name, repo_name).can():
|
||||
grants.append(repository_write_grant(namespace_name, repo_name))
|
||||
|
||||
# Generate a signed token for the user (if any) and the grants (if any)
|
||||
if grants or get_authenticated_user():
|
||||
|
@ -170,50 +170,50 @@ def update_user(username):
|
|||
|
||||
@v1_bp.route('/repositories/<repopath:repository>/', methods=['PUT'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201)
|
||||
@anon_allowed
|
||||
def create_repository(namespace, repository):
|
||||
def create_repository(namespace_name, repo_name):
|
||||
# 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.')
|
||||
|
||||
logger.debug('Looking up repository %s/%s', namespace, repository)
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
logger.debug('Looking up repository %s/%s', namespace_name, repo_name)
|
||||
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:
|
||||
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,
|
||||
message='Cannot create a repository as a guest. Please login via "docker login" first.',
|
||||
issue='no-login')
|
||||
|
||||
elif repo:
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
||||
if not permission.can():
|
||||
abort(403,
|
||||
message='You do not have permission to modify repository %(namespace)s/%(repository)s',
|
||||
issue='no-repo-write-permission',
|
||||
namespace=namespace, repository=repository)
|
||||
namespace=namespace_name, repository=repo_name)
|
||||
else:
|
||||
permission = CreateRepositoryPermission(namespace)
|
||||
permission = CreateRepositoryPermission(namespace_name)
|
||||
if not permission.can():
|
||||
logger.info('Attempt to create a new repo %s/%s with insufficient perms', namespace,
|
||||
repository)
|
||||
logger.info('Attempt to create a new repo %s/%s with insufficient perms', namespace_name,
|
||||
repo_name)
|
||||
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.
|
||||
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)
|
||||
|
||||
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():
|
||||
user_event_data = {
|
||||
'action': 'push_start',
|
||||
'repository': repository,
|
||||
'namespace': namespace
|
||||
'repository': repo_name,
|
||||
'namespace': namespace_name,
|
||||
}
|
||||
|
||||
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'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||
@anon_allowed
|
||||
def update_images(namespace, repository):
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
def update_images(namespace_name, repo_name):
|
||||
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
||||
|
||||
if permission.can():
|
||||
logger.debug('Looking up repository')
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||
if not repo:
|
||||
# Make sure the repo actually exists.
|
||||
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'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@generate_headers(scope=GrantType.READ_REPOSITORY)
|
||||
@anon_protect
|
||||
def get_repository_images(namespace, repository):
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
def get_repository_images(namespace_name, repo_name):
|
||||
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
||||
|
||||
# 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
|
||||
logger.debug('Looking up repository')
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||
if not 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'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||
@anon_allowed
|
||||
def delete_repository_images(namespace, repository):
|
||||
def delete_repository_images(namespace_name, repo_name):
|
||||
abort(501, 'Not Implemented', issue='not-implemented')
|
||||
|
||||
|
||||
@v1_bp.route('/repositories/<repopath:repository>/auth', methods=['PUT'])
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@anon_allowed
|
||||
def put_repository_auth(namespace, repository):
|
||||
def put_repository_auth(namespace_name, repo_name):
|
||||
abort(501, 'Not Implemented', issue='not-implemented')
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
|
||||
import logging
|
||||
import json
|
||||
|
||||
from flask import abort, request, jsonify, make_response, session
|
||||
|
||||
from app import app
|
||||
from util.names import TAG_ERROR, TAG_REGEX
|
||||
from auth.auth import process_auth
|
||||
from auth.permissions import (ReadRepositoryPermission,
|
||||
|
@ -22,12 +20,12 @@ logger = logging.getLogger(__name__)
|
|||
@v1_bp.route('/repositories/<repopath:repository>/tags', methods=['GET'])
|
||||
@process_auth
|
||||
@anon_protect
|
||||
@parse_repository_name
|
||||
def get_tags(namespace, repository):
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
@parse_repository_name()
|
||||
def get_tags(namespace_name, repo_name):
|
||||
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
||||
|
||||
if permission.can() or model.repository.repository_is_public(namespace, repository):
|
||||
tags = model.tag.list_repository_tags(namespace, repository)
|
||||
if permission.can() or model.repository.repository_is_public(namespace_name, repo_name):
|
||||
tags = model.tag.list_repository_tags(namespace_name, repo_name)
|
||||
tag_map = {tag.name: tag.image.docker_image_id for tag in tags}
|
||||
return jsonify(tag_map)
|
||||
|
||||
|
@ -37,13 +35,13 @@ def get_tags(namespace, repository):
|
|||
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['GET'])
|
||||
@process_auth
|
||||
@anon_protect
|
||||
@parse_repository_name
|
||||
def get_tag(namespace, repository, tag):
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
@parse_repository_name()
|
||||
def get_tag(namespace_name, repo_name, tag):
|
||||
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:
|
||||
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:
|
||||
abort(404)
|
||||
|
||||
|
@ -57,19 +55,19 @@ def get_tag(namespace, repository, tag):
|
|||
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['PUT'])
|
||||
@process_auth
|
||||
@anon_protect
|
||||
@parse_repository_name
|
||||
def put_tag(namespace, repository, tag):
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
@parse_repository_name()
|
||||
def put_tag(namespace_name, repo_name, tag):
|
||||
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
||||
|
||||
if permission.can():
|
||||
if not TAG_REGEX.match(tag):
|
||||
abort(400, TAG_ERROR)
|
||||
|
||||
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.
|
||||
if not 'pushed_tags' in session:
|
||||
if 'pushed_tags' not in session:
|
||||
session['pushed_tags'] = {}
|
||||
|
||||
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'])
|
||||
@process_auth
|
||||
@anon_protect
|
||||
@parse_repository_name
|
||||
def delete_tag(namespace, repository, tag):
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
@parse_repository_name()
|
||||
def delete_tag(namespace_name, repo_name, tag):
|
||||
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
||||
|
||||
if permission.can():
|
||||
model.tag.delete_tag(namespace, repository, tag)
|
||||
track_and_log('delete_tag', model.repository.get_repository(namespace, repository),
|
||||
model.tag.delete_tag(namespace_name, repo_name, tag)
|
||||
track_and_log('delete_tag', model.repository.get_repository(namespace_name, repo_name),
|
||||
tag=tag)
|
||||
return make_response('Deleted', 200)
|
||||
|
||||
|
|
|
@ -41,13 +41,14 @@ def handle_registry_v2_exception(error):
|
|||
def _require_repo_permission(permission_class, allow_public=False):
|
||||
def wrapper(func):
|
||||
@wraps(func)
|
||||
def wrapped(namespace, repo_name, *args, **kwargs):
|
||||
logger.debug('Checking permission %s for repo: %s/%s', permission_class, namespace, repo_name)
|
||||
permission = permission_class(namespace, repo_name)
|
||||
def wrapped(namespace_name, repo_name, *args, **kwargs):
|
||||
logger.debug('Checking permission %s for repo: %s/%s', permission_class,
|
||||
namespace_name, repo_name)
|
||||
permission = permission_class(namespace_name, repo_name)
|
||||
if (permission.can() or
|
||||
(allow_public and
|
||||
model.repository.repository_is_public(namespace, repo_name))):
|
||||
return func(namespace, repo_name, *args, **kwargs)
|
||||
model.repository.repository_is_public(namespace_name, repo_name))):
|
||||
return func(namespace_name, repo_name, *args, **kwargs)
|
||||
raise Unauthorized()
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
|
|
@ -9,6 +9,7 @@ from app import storage, app
|
|||
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||
from data import model, database
|
||||
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.errors import (BlobUnknown, BlobUploadInvalid, BlobUploadUnknown, Unsupported,
|
||||
NameUnknown)
|
||||
|
@ -17,7 +18,6 @@ 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
|
||||
|
||||
|
||||
|
@ -34,12 +34,12 @@ class _InvalidRangeHeader(Exception):
|
|||
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
|
||||
authorization before calling this method.
|
||||
"""
|
||||
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:
|
||||
raise BlobUnknown()
|
||||
|
||||
|
@ -58,12 +58,12 @@ def _base_blob_fetch(namespace, repo_name, digest):
|
|||
|
||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
@cache_control(max_age=31436000)
|
||||
def check_blob_exists(namespace, repo_name, digest):
|
||||
found, headers = _base_blob_fetch(namespace, repo_name, digest)
|
||||
def check_blob_exists(namespace_name, repo_name, digest):
|
||||
found, headers = _base_blob_fetch(namespace_name, repo_name, digest)
|
||||
|
||||
response = make_response('')
|
||||
response.headers.extend(headers)
|
||||
|
@ -74,12 +74,12 @@ def check_blob_exists(namespace, repo_name, digest):
|
|||
|
||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
@cache_control(max_age=31536000)
|
||||
def download_blob(namespace, repo_name, digest):
|
||||
found, headers = _base_blob_fetch(namespace, repo_name, digest)
|
||||
def download_blob(namespace_name, repo_name, digest):
|
||||
found, headers = _base_blob_fetch(namespace_name, repo_name, digest)
|
||||
|
||||
path = model.storage.get_layer_path(found)
|
||||
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'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def start_blob_upload(namespace, repo_name):
|
||||
def start_blob_upload(namespace_name, repo_name):
|
||||
location_name = storage.preferred_locations[0]
|
||||
new_upload_uuid, upload_metadata = storage.initiate_chunked_upload(location_name)
|
||||
|
||||
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)
|
||||
except database.Repository.DoesNotExist:
|
||||
raise NameUnknown()
|
||||
|
@ -126,7 +126,7 @@ def start_blob_upload(namespace, repo_name):
|
|||
# The user will send the blob data in another request
|
||||
accepted = make_response('', 202)
|
||||
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)
|
||||
|
||||
accepted.headers['Range'] = _render_range(0)
|
||||
|
@ -134,22 +134,22 @@ def start_blob_upload(namespace, repo_name):
|
|||
return accepted
|
||||
else:
|
||||
# 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()
|
||||
if error:
|
||||
_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'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def fetch_existing_upload(namespace, repo_name, upload_uuid):
|
||||
def fetch_existing_upload(namespace_name, repo_name, upload_uuid):
|
||||
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:
|
||||
raise BlobUploadUnknown()
|
||||
|
||||
|
@ -189,12 +189,12 @@ def _parse_range_header(range_header_text):
|
|||
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.
|
||||
Callers MUST call .save() or .delete_instance() on the returned database object.
|
||||
"""
|
||||
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:
|
||||
raise BlobUploadUnknown()
|
||||
|
||||
|
@ -280,7 +280,7 @@ def _upload_chunk(namespace, repo_name, upload_uuid):
|
|||
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.
|
||||
computed_digest = digest_tools.sha256_digest_from_hashlib(upload_obj.sha_state)
|
||||
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)
|
||||
|
||||
# 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.byte_count,
|
||||
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.headers['Docker-Content-Digest'] = expected_digest
|
||||
response.headers['Location'] = url_for('v2.download_blob',
|
||||
repository='%s/%s' % (namespace, repo_name),
|
||||
repository='%s/%s' % (namespace_name, repo_name),
|
||||
digest=expected_digest)
|
||||
return response
|
||||
|
||||
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PATCH'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def upload_chunk(namespace, repo_name, upload_uuid):
|
||||
upload, error = _upload_chunk(namespace, repo_name, upload_uuid)
|
||||
def upload_chunk(namespace_name, repo_name, upload_uuid):
|
||||
upload, error = _upload_chunk(namespace_name, repo_name, upload_uuid)
|
||||
upload.save()
|
||||
|
||||
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'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_write
|
||||
@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)
|
||||
if digest is None:
|
||||
raise BlobUploadInvalid()
|
||||
|
||||
found, error = _upload_chunk(namespace, repo_name, upload_uuid)
|
||||
found, error = _upload_chunk(namespace_name, repo_name, upload_uuid)
|
||||
|
||||
if error:
|
||||
found.save()
|
||||
_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'])
|
||||
@parse_repository_name()
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def cancel_upload(namespace, repo_name, upload_uuid):
|
||||
def cancel_upload(namespace_name, repo_name, upload_uuid):
|
||||
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:
|
||||
raise BlobUploadUnknown()
|
||||
|
||||
|
@ -384,11 +384,9 @@ def cancel_upload(namespace, repo_name, upload_uuid):
|
|||
|
||||
@v2_bp.route('/<repopath:repository>/blobs/<digest>', methods=['DELETE'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_write
|
||||
@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.
|
||||
raise Unsupported()
|
||||
|
||||
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import logging
|
||||
import jwt.utils
|
||||
import json
|
||||
import features
|
||||
import hashlib
|
||||
|
||||
from peewee import IntegrityError
|
||||
from flask import make_response, request, url_for
|
||||
from collections import namedtuple, OrderedDict
|
||||
from jwkest.jws import SIGNER_ALGS, keyrep
|
||||
from datetime import datetime
|
||||
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 auth.registry_jwt_auth import process_registry_jwt_auth
|
||||
from endpoints.common import parse_repository_name
|
||||
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, ManifestUnknown, TagInvalid,
|
||||
|
@ -22,7 +26,6 @@ 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__)
|
||||
|
||||
|
@ -84,7 +87,8 @@ class SignedManifest(object):
|
|||
|
||||
def _validate(self):
|
||||
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']]
|
||||
key = keyrep(signature['header']['jwk'])
|
||||
gk = key.get_key()
|
||||
|
@ -163,9 +167,9 @@ class SignedManifest(object):
|
|||
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 == '':
|
||||
def __init__(self, namespace_name, repo_name, tag, architecture='amd64', schema_ver=1):
|
||||
repo_name_key = '{0}/{1}'.format(namespace_name, repo_name)
|
||||
if namespace_name == '':
|
||||
repo_name_key = repo_name
|
||||
|
||||
self._base_payload = {
|
||||
|
@ -238,26 +242,26 @@ class SignedManifestBuilder(object):
|
|||
|
||||
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||
def fetch_manifest_by_tagname(namespace_name, repo_name, manifest_ref):
|
||||
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:
|
||||
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:
|
||||
raise ManifestUnknown()
|
||||
raise ManifestUnknown()
|
||||
|
||||
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:
|
||||
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)
|
||||
raise ManifestUnknown()
|
||||
|
||||
repo = model.repository.get_repository(namespace, repo_name)
|
||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||
if repo is not None:
|
||||
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'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def fetch_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||
def fetch_manifest_by_digest(namespace_name, repo_name, manifest_ref):
|
||||
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:
|
||||
# Without a tag name to reference, we can't make an attempt to generate the manifest
|
||||
raise ManifestUnknown()
|
||||
|
||||
repo = model.repository.get_repository(namespace, repo_name)
|
||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||
if repo is not None:
|
||||
track_and_log('pull_repo', repo)
|
||||
|
||||
|
@ -301,11 +305,11 @@ def _reject_manifest2_schema2(func):
|
|||
|
||||
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['PUT'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
@_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:
|
||||
manifest = SignedManifest(request.data)
|
||||
except ValueError:
|
||||
|
@ -314,16 +318,16 @@ def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
|||
if manifest.tag != manifest_ref:
|
||||
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'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
@_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:
|
||||
manifest = SignedManifest(request.data)
|
||||
except ValueError:
|
||||
|
@ -332,7 +336,7 @@ def write_manifest_by_digest(namespace, repo_name, manifest_ref):
|
|||
if manifest.digest != manifest_ref:
|
||||
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):
|
||||
|
@ -350,21 +354,21 @@ def _updated_v1_metadata(v1_metadata_json, updated_id_map):
|
|||
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
|
||||
# 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']):
|
||||
namespace_name == app.config['LIBRARY_NAMESPACE']):
|
||||
# This is a library manifest. All good.
|
||||
pass
|
||||
elif manifest.namespace != namespace:
|
||||
elif manifest.namespace != namespace_name:
|
||||
raise NameInvalid()
|
||||
|
||||
if manifest.repo_name != repo_name:
|
||||
raise NameInvalid()
|
||||
|
||||
# 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:
|
||||
raise NameInvalid()
|
||||
|
||||
|
@ -411,8 +415,8 @@ def _write_manifest(namespace, repo_name, manifest):
|
|||
|
||||
v1_metadata_str = mdata.v1_metadata_str.encode('utf-8')
|
||||
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,
|
||||
working_docker_id)
|
||||
logger.debug('Rewriting docker_id %s/%s %s -> %s', namespace_name, repo_name,
|
||||
v1_mdata.docker_id, working_docker_id)
|
||||
has_rewritten_ids = True
|
||||
|
||||
# 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.
|
||||
manifest_digest = manifest.digest
|
||||
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)
|
||||
|
||||
# Spawn the repo_push event.
|
||||
|
@ -461,29 +465,29 @@ 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',
|
||||
repository='%s/%s' % (namespace, repo_name),
|
||||
repository='%s/%s' % (namespace_name, repo_name),
|
||||
manifest_ref=manifest_digest)
|
||||
return response
|
||||
|
||||
|
||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['DELETE'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_write
|
||||
@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
|
||||
method for deleting by tag name because it is forbidden by the spec.
|
||||
"""
|
||||
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:
|
||||
# Without a tag name to reference, we can't make an attempt to generate the manifest
|
||||
raise ManifestUnknown()
|
||||
|
||||
# Mark the tag as no longer alive.
|
||||
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:
|
||||
# Tag is not alive.
|
||||
raise ManifestUnknown()
|
||||
|
@ -494,15 +498,15 @@ def delete_manifest_by_digest(namespace, repo_name, manifest_ref):
|
|||
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
|
||||
image = model.tag.get_tag_image(namespace, repo_name, tag_name)
|
||||
parents = model.image.get_parent_images(namespace, repo_name, image)
|
||||
image = model.tag.get_tag_image(namespace_name, repo_name, tag_name)
|
||||
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
|
||||
# empty.
|
||||
manifest_namespace = namespace
|
||||
if features.LIBRARY_SUPPORT and namespace == app.config['LIBRARY_NAMESPACE']:
|
||||
manifest_namespace = namespace_name
|
||||
if features.LIBRARY_SUPPORT and namespace_name == app.config['LIBRARY_NAMESPACE']:
|
||||
manifest_namespace = ''
|
||||
|
||||
# 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
|
||||
# one found.
|
||||
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)
|
||||
except IntegrityError as ie:
|
||||
logger.debug('Got integrity error: %s', ie)
|
||||
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:
|
||||
logger.exception('Exception when generating manifest')
|
||||
raise model.DataModelException('Could not load or generate manifest')
|
||||
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
from flask import jsonify, url_for
|
||||
|
||||
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.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('/<repopath:repository>/tags/list', methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def list_all_tags(namespace, repo_name):
|
||||
repository = model.repository.get_repository(namespace, repo_name)
|
||||
def list_all_tags(namespace_name, repo_name):
|
||||
repository = model.repository.get_repository(namespace_name, repo_name)
|
||||
if repository is None:
|
||||
raise NameUnknown()
|
||||
|
||||
query = model.tag.list_repository_tags(namespace, repo_name)
|
||||
url = url_for('v2.list_all_tags', repository='%s/%s' % (namespace, repo_name))
|
||||
query = model.tag.list_repository_tags(namespace_name, repo_name)
|
||||
url = url_for('v2.list_all_tags', repository='%s/%s' % (namespace_name, repo_name))
|
||||
link, query = add_pagination(query, url)
|
||||
|
||||
response = jsonify({
|
||||
'name': '{0}/{1}'.format(namespace, repo_name),
|
||||
'name': '{0}/{1}'.format(namespace_name, repo_name),
|
||||
'tags': [tag.name for tag in query],
|
||||
})
|
||||
|
||||
|
|
|
@ -381,10 +381,10 @@ 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)
|
||||
@parse_repository_name()
|
||||
def get_tag_torrent(namespace_name, repo_name, digest):
|
||||
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
||||
public_repo = model.repository.repository_is_public(namespace_name, repo_name)
|
||||
if not permission.can() and not public_repo:
|
||||
abort(403)
|
||||
|
||||
|
@ -394,7 +394,7 @@ def get_tag_torrent(namespace, repo_name, digest):
|
|||
abort(403)
|
||||
|
||||
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:
|
||||
abort(404)
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ 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,
|
||||
parse_repository_name, parse_repository_name_and_tag)
|
||||
parse_repository_name)
|
||||
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
|
||||
|
@ -411,20 +411,20 @@ def confirm_recovery():
|
|||
|
||||
|
||||
@web.route('/repository/<repopath:repository>/status', methods=['GET'])
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@anon_protect
|
||||
def build_status_badge(namespace, repository):
|
||||
def build_status_badge(namespace_name, repo_name):
|
||||
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:
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||
if not repo or token != repo.badge_token:
|
||||
abort(404)
|
||||
|
||||
# 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
|
||||
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'):
|
||||
status_name = 'ready'
|
||||
|
@ -600,14 +600,14 @@ def download_logs_archive():
|
|||
|
||||
@web.route('/bitbucket/setup/<repopath:repository>', methods=['GET'])
|
||||
@require_session_login
|
||||
@parse_repository_name
|
||||
@parse_repository_name()
|
||||
@route_show_if(features.BITBUCKET_BUILD)
|
||||
def attach_bitbucket_trigger(namespace, repository_name):
|
||||
permission = AdministerRepositoryPermission(namespace, repository_name)
|
||||
def attach_bitbucket_trigger(namespace_name, repo_name):
|
||||
permission = AdministerRepositoryPermission(namespace_name, repo_name)
|
||||
if permission.can():
|
||||
repo = model.repository.get_repository(namespace, repository_name)
|
||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||
if not repo:
|
||||
msg = 'Invalid repository: %s/%s' % (namespace, repository_name)
|
||||
msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
|
||||
abort(404, message=msg)
|
||||
|
||||
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'])
|
||||
@require_session_login
|
||||
@parse_repository_name
|
||||
def attach_custom_build_trigger(namespace, repository_name):
|
||||
permission = AdministerRepositoryPermission(namespace, repository_name)
|
||||
@parse_repository_name()
|
||||
def attach_custom_build_trigger(namespace_name, repo_name):
|
||||
permission = AdministerRepositoryPermission(namespace_name, repo_name)
|
||||
if permission.can():
|
||||
repo = model.repository.get_repository(namespace, repository_name)
|
||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||
if not repo:
|
||||
msg = 'Invalid repository: %s/%s' % (namespace, repository_name)
|
||||
msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
|
||||
abort(404, message=msg)
|
||||
|
||||
trigger = model.build.create_build_trigger(repo, CustomBuildTrigger.service_name(),
|
||||
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=',
|
||||
trigger.uuid)
|
||||
|
||||
|
@ -655,21 +655,22 @@ def attach_custom_build_trigger(namespace, repository_name):
|
|||
|
||||
abort(403)
|
||||
|
||||
|
||||
@web.route('/<repopath:repository>')
|
||||
@no_cache
|
||||
@process_oauth
|
||||
@parse_repository_name_and_tag
|
||||
@parse_repository_name(include_tag=True)
|
||||
@anon_protect
|
||||
def redirect_to_repository(namespace, reponame, tag_name):
|
||||
permission = ReadRepositoryPermission(namespace, reponame)
|
||||
is_public = model.repository.repository_is_public(namespace, reponame)
|
||||
def redirect_to_repository(namespace_name, repo_name, tag_name):
|
||||
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
||||
is_public = model.repository.repository_is_public(namespace_name, repo_name)
|
||||
|
||||
if request.args.get('ac-discovery', 0) == 1:
|
||||
return index('')
|
||||
|
||||
if permission.can() or is_public:
|
||||
repository_name = '/'.join([namespace, reponame])
|
||||
return redirect(url_for('web.repository', path=repository_name, tag=tag_name))
|
||||
repo_path = '/'.join([namespace_name, repo_name])
|
||||
return redirect(url_for('web.repository', path=repo_path, tag=tag_name))
|
||||
|
||||
abort(404)
|
||||
|
||||
|
|
|
@ -5,4 +5,4 @@ export TROLLIUSDEBUG=1
|
|||
|
||||
python -m unittest discover -f
|
||||
python -m test.registry_tests -f
|
||||
python -m test.queue_threads -f
|
||||
#python -m test.queue_threads -f
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import unittest
|
||||
import endpoints.decorated
|
||||
import json
|
||||
|
||||
from app import app
|
||||
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
|
||||
|
||||
app.register_blueprint(v2_bp, url_prefix='/v2')
|
||||
|
|
Reference in a new issue