Merge pull request #2447 from jzelinskie/cnr-step2

CNR Step 2
This commit is contained in:
Jimmy Zelinskie 2017-03-22 18:45:51 -04:00 committed by GitHub
commit 3ccf3c5f33
40 changed files with 790 additions and 136 deletions

View file

@ -22,7 +22,7 @@ from auth.auth_context import get_authenticated_user, get_validated_oauth_token
from auth.process import process_oauth
from endpoints.csrf import csrf_protect
from endpoints.exception import (ApiException, Unauthorized, InvalidRequest, InvalidResponse,
FreshLoginRequired)
FreshLoginRequired, NotFound)
from endpoints.decorators import check_anon_protection
from util.metrics.metricqueue import time_decorator
from util.names import parse_namespace_repository
@ -200,6 +200,20 @@ class RepositoryParamResource(ApiResource):
method_decorators = [check_anon_protection, parse_repository_name]
def disallow_for_app_repositories(func):
@wraps(func)
def wrapped(self, namespace, repository, *args, **kwargs):
# Lookup the repository with the given namespace and name and ensure it is not an application
# repository.
repo = model.repository.get_repository(namespace, repository, kind_filter='application')
if repo:
abort(501)
return func(self, namespace, repository, *args, **kwargs)
return wrapped
def require_repo_permission(permission_class, scope, allow_public=False):
def wrapper(func):
@add_method_metadata('oauth2_scope', scope)

View file

@ -14,7 +14,7 @@ from buildtrigger.basehandler import BuildTriggerHandler
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
require_repo_read, require_repo_write, validate_json_request,
ApiResource, internal_only, format_date, api, path_param,
require_repo_admin, abort)
require_repo_admin, abort, disallow_for_app_repositories)
from endpoints.exception import Unauthorized, NotFound, InvalidRequest
from endpoints.building import start_build, PreparedBuild, MaximumBuildsQueuedException
from data import database
@ -200,6 +200,7 @@ class RepositoryBuildList(RepositoryParamResource):
@query_param('limit', 'The maximum number of builds to return', type=int, default=5)
@query_param('since', 'Returns all builds since the given unix timecode', type=int, default=None)
@nickname('getRepoBuilds')
@disallow_for_app_repositories
def get(self, namespace, repository, parsed_args):
""" Get the list of repository builds. """
limit = parsed_args.get('limit', 5)
@ -215,6 +216,7 @@ class RepositoryBuildList(RepositoryParamResource):
@require_repo_write
@nickname('requestRepoBuild')
@disallow_for_app_repositories
@validate_json_request('RepositoryBuildRequest')
def post(self, namespace, repository):
""" Request that a repository be built and pushed from the specified input. """
@ -315,6 +317,7 @@ class RepositoryBuildResource(RepositoryParamResource):
""" Resource for dealing with repository builds. """
@require_repo_read
@nickname('getRepoBuild')
@disallow_for_app_repositories
def get(self, namespace, repository, build_uuid):
""" Returns information about a build. """
try:
@ -329,6 +332,7 @@ class RepositoryBuildResource(RepositoryParamResource):
@require_repo_admin
@nickname('cancelRepoBuild')
@disallow_for_app_repositories
def delete(self, namespace, repository, build_uuid):
""" Cancels a repository build. """
try:
@ -352,6 +356,7 @@ class RepositoryBuildStatus(RepositoryParamResource):
""" Resource for dealing with repository build status. """
@require_repo_read
@nickname('getRepoBuildStatus')
@disallow_for_app_repositories
def get(self, namespace, repository, build_uuid):
""" Return the status for the builds specified by the build uuids. """
build = model.build.get_repository_build(build_uuid)
@ -392,6 +397,7 @@ class RepositoryBuildLogs(RepositoryParamResource):
""" Resource for loading repository build logs. """
@require_repo_write
@nickname('getRepoBuildLogs')
@disallow_for_app_repositories
def get(self, namespace, repository, build_uuid):
""" Return the build logs for the build specified by the build uuid. """
build = model.build.get_repository_build(build_uuid)

View file

@ -4,7 +4,7 @@ import json
from collections import defaultdict
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
format_date, path_param)
format_date, path_param, disallow_for_app_repositories)
from endpoints.exception import NotFound
from data import model
@ -49,6 +49,7 @@ class RepositoryImageList(RepositoryParamResource):
""" Resource for listing repository images. """
@require_repo_read
@nickname('listRepositoryImages')
@disallow_for_app_repositories
def get(self, namespace, repository):
""" List the images for the specified repository. """
repo = model.repository.get_repository(namespace, repository)
@ -89,6 +90,7 @@ class RepositoryImage(RepositoryParamResource):
""" Resource for handling repository images. """
@require_repo_read
@nickname('getImage')
@disallow_for_app_repositories
def get(self, namespace, repository, image_id):
""" Get the information available for the specified image. """
image = model.image.get_repo_image_extended(namespace, repository, image_id)

View file

@ -4,7 +4,8 @@ from app import label_validator
from flask import request
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
RepositoryParamResource, log_action, validate_json_request,
path_param, parse_args, query_param, truthy_bool, abort, api)
path_param, parse_args, query_param, truthy_bool, abort, api,
disallow_for_app_repositories)
from endpoints.exception import NotFound
from data import model
@ -59,6 +60,7 @@ class RepositoryManifestLabels(RepositoryParamResource):
@require_repo_read
@nickname('listManifestLabels')
@disallow_for_app_repositories
@parse_args()
@query_param('filter', 'If specified, only labels matching the given prefix will be returned',
type=str, default=None)
@ -75,6 +77,7 @@ class RepositoryManifestLabels(RepositoryParamResource):
@require_repo_write
@nickname('addManifestLabel')
@disallow_for_app_repositories
@validate_json_request('AddLabel')
def post(self, namespace, repository, manifestref):
""" Adds a new label into the tag manifest. """
@ -121,6 +124,7 @@ class ManageRepositoryManifestLabel(RepositoryParamResource):
""" Resource for managing the labels on a specific repository manifest. """
@require_repo_read
@nickname('getManifestLabel')
@disallow_for_app_repositories
def get(self, namespace, repository, manifestref, labelid):
""" Retrieves the label with the specific ID under the manifest. """
try:
@ -137,6 +141,7 @@ class ManageRepositoryManifestLabel(RepositoryParamResource):
@require_repo_write
@nickname('deleteManifestLabel')
@disallow_for_app_repositories
def delete(self, namespace, repository, manifestref, labelid):
""" Deletes an existing label from a manifest. """
try:

View file

@ -14,7 +14,7 @@ from endpoints.api import (truthy_bool, format_date, nickname, log_action, valid
require_repo_read, require_repo_write, require_repo_admin,
RepositoryParamResource, resource, query_param, parse_args, ApiResource,
request_error, require_scope, path_param, page_support, parse_args,
query_param, truthy_bool)
query_param, truthy_bool, disallow_for_app_repositories)
from endpoints.exception import Unauthorized, NotFound, InvalidRequest, ExceedsLicenseException
from endpoints.api.billing import lookup_allowed_private_repos, get_namespace_plan
from endpoints.api.subscribe import check_repository_usage
@ -77,6 +77,11 @@ class RepositoryList(ApiResource):
'type': 'string',
'description': 'Markdown encoded description for the repository',
},
'kind': {
'type': 'string',
'description': 'The kind of repository',
'enum': ['image', 'application'],
}
},
},
}
@ -111,7 +116,9 @@ class RepositoryList(ApiResource):
if not REPOSITORY_NAME_REGEX.match(repository_name):
raise InvalidRequest('Invalid repository name')
repo = model.repository.create_repository(namespace_name, repository_name, owner, visibility)
kind = req.get('kind', 'image')
repo = model.repository.create_repository(namespace_name, repository_name, owner, visibility,
repo_kind=kind)
repo.description = req['description']
repo.save()
@ -354,6 +361,7 @@ class Repository(RepositoryParamResource):
@require_repo_admin
@nickname('deleteRepository')
@disallow_for_app_repositories
def delete(self, namespace, repository):
""" Delete a repository. """
model.repository.purge_repository(namespace, repository)

View file

@ -7,7 +7,7 @@ from flask import request
from app import notification_queue
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, validate_json_request, request_error,
path_param)
path_param, disallow_for_app_repositories)
from endpoints.exception import NotFound
from endpoints.notificationevent import NotificationEvent
from endpoints.notificationmethod import (NotificationMethod,
@ -80,6 +80,7 @@ class RepositoryNotificationList(RepositoryParamResource):
@require_repo_admin
@nickname('createRepoNotification')
@disallow_for_app_repositories
@validate_json_request('NotificationCreateRequest')
def post(self, namespace, repository):
""" Create a new notification for the specified repository. """
@ -110,6 +111,7 @@ class RepositoryNotificationList(RepositoryParamResource):
@require_repo_admin
@nickname('listRepoNotifications')
@disallow_for_app_repositories
def get(self, namespace, repository):
""" List the notifications for the specified repository. """
notifications = model.notification.list_repo_notifications(namespace, repository)
@ -125,6 +127,7 @@ class RepositoryNotification(RepositoryParamResource):
""" Resource for dealing with specific notifications. """
@require_repo_admin
@nickname('getRepoNotification')
@disallow_for_app_repositories
def get(self, namespace, repository, uuid):
""" Get information for the specified notification. """
try:
@ -140,6 +143,7 @@ class RepositoryNotification(RepositoryParamResource):
@require_repo_admin
@nickname('deleteRepoNotification')
@disallow_for_app_repositories
def delete(self, namespace, repository, uuid):
""" Deletes the specified notification. """
deleted = model.notification.delete_repo_notification(namespace, repository, uuid)
@ -158,6 +162,7 @@ class TestRepositoryNotification(RepositoryParamResource):
""" Resource for queuing a test of a notification. """
@require_repo_admin
@nickname('testRepoNotification')
@disallow_for_app_repositories
def post(self, namespace, repository, uuid):
""" Queues a test notification for this repository. """
try:

View file

@ -7,7 +7,7 @@ from app import secscan_api
from data import model
from endpoints.api import (require_repo_read, path_param,
RepositoryParamResource, resource, nickname, show_if, parse_args,
query_param, truthy_bool)
query_param, truthy_bool, disallow_for_app_repositories)
from endpoints.exception import NotFound, DownstreamIssue
from endpoints.api.manifest import MANIFEST_DIGEST_ROUTE
from util.secscan.api import APIRequestFailure
@ -67,6 +67,7 @@ class RepositoryImageSecurity(RepositoryParamResource):
@require_repo_read
@nickname('getRepoImageSecurity')
@disallow_for_app_repositories
@parse_args()
@query_param('vulnerabilities', 'Include vulnerabilities informations', type=truthy_bool,
default=False)
@ -88,6 +89,7 @@ class RepositoryManifestSecurity(RepositoryParamResource):
@require_repo_read
@nickname('getRepoManifestSecurity')
@disallow_for_app_repositories
@parse_args()
@query_param('vulnerabilities', 'Include vulnerabilities informations', type=truthy_bool,
default=False)

View file

@ -4,7 +4,8 @@ from flask import request, abort
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
RepositoryParamResource, log_action, validate_json_request,
path_param, parse_args, query_param, truthy_bool)
path_param, parse_args, query_param, truthy_bool,
disallow_for_app_repositories)
from endpoints.exception import NotFound
from endpoints.api.image import image_view
from data import model
@ -18,6 +19,7 @@ class ListRepositoryTags(RepositoryParamResource):
""" Resource for listing full repository tag history, alive *and dead*. """
@require_repo_read
@disallow_for_app_repositories
@parse_args()
@query_param('specificTag', 'Filters the tags to the specific tag.', type=str, default='')
@query_param('limit', 'Limit to the number of results to return per page. Max 100.', type=int, default=50)
@ -82,6 +84,7 @@ class RepositoryTag(RepositoryParamResource):
}
@require_repo_write
@disallow_for_app_repositories
@nickname('changeTagImage')
@validate_json_request('MoveTag')
def put(self, namespace, repository, tag):
@ -116,6 +119,7 @@ class RepositoryTag(RepositoryParamResource):
return 'Updated', 201
@require_repo_write
@disallow_for_app_repositories
@nickname('deleteFullTag')
def delete(self, namespace, repository, tag):
""" Delete the specified repository tag. """
@ -136,6 +140,7 @@ class RepositoryTagImages(RepositoryParamResource):
""" Resource for listing the images in a specific repository tag. """
@require_repo_read
@nickname('listTagImages')
@disallow_for_app_repositories
@parse_args()
@query_param('owned', 'If specified, only images wholely owned by this tag are returned.',
type=truthy_bool, default=False)
@ -206,6 +211,7 @@ class RestoreTag(RepositoryParamResource):
}
@require_repo_write
@disallow_for_app_repositories
@nickname('restoreTag')
@validate_json_request('RestoreTag')
def post(self, namespace, repository, tag):

View file

View file

@ -0,0 +1,58 @@
import datetime
import json
from contextlib import contextmanager
from data import model
from endpoints.api import api
CSRF_TOKEN_KEY = '_csrf_token'
CSRF_TOKEN = '123csrfforme'
@contextmanager
def client_with_identity(auth_username, client):
with client.session_transaction() as sess:
if auth_username:
if auth_username is not None:
loaded = model.user.get_user(auth_username)
sess['user_id'] = loaded.uuid
sess['login_time'] = datetime.datetime.now()
sess[CSRF_TOKEN_KEY] = CSRF_TOKEN
yield client
with client.session_transaction() as sess:
sess['user_id'] = None
sess['login_time'] = None
sess[CSRF_TOKEN_KEY] = None
def add_csrf_param(params):
""" Returns a params dict with the CSRF parameter added. """
params = params or {}
params[CSRF_TOKEN_KEY] = CSRF_TOKEN
return params
def conduct_api_call(client, resource, method, params, body=None, expected_code=200):
""" Conducts an API call to the given resource via the given client, and ensures its returned
status matches the code given.
Returns the response.
"""
params = add_csrf_param(params)
final_url = api.url_for(resource, **params)
headers = {}
headers.update({"Content-Type": "application/json"})
if body is not None:
body = json.dumps(body)
rv = client.open(final_url, method=method, data=body, headers=headers)
msg = '%s %s: got %s expected: %s | %s' % (method, final_url, rv.status_code, expected_code,
rv.data)
assert rv.status_code == expected_code, msg
return rv

View file

@ -0,0 +1,80 @@
import pytest
from data import model
from endpoints.api.repository import Repository
from endpoints.api.build import (RepositoryBuildList, RepositoryBuildResource,
RepositoryBuildStatus, RepositoryBuildLogs)
from endpoints.api.image import RepositoryImageList, RepositoryImage
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
from endpoints.api.repositorynotification import (RepositoryNotification,
RepositoryNotificationList,
TestRepositoryNotification)
from endpoints.api.secscan import RepositoryImageSecurity, RepositoryManifestSecurity
from endpoints.api.tag import ListRepositoryTags, RepositoryTag, RepositoryTagImages, RestoreTag
from endpoints.api.trigger import (BuildTriggerList, BuildTrigger, BuildTriggerSubdirs,
BuildTriggerActivate, BuildTriggerAnalyze, ActivateBuildTrigger,
TriggerBuildList, BuildTriggerFieldValues, BuildTriggerSources,
BuildTriggerSourceNamespaces)
from endpoints.api.test.shared import client_with_identity, conduct_api_call
from endpoints.test.fixtures import app, appconfig, database_uri, init_db_path, sqlitedb_file
BUILD_ARGS = {'build_uuid': '1234'}
IMAGE_ARGS = {'imageid': '1234', 'image_id': 1234}
MANIFEST_ARGS = {'manifestref': 'sha256:abcd1234'}
LABEL_ARGS = {'manifestref': 'sha256:abcd1234', 'labelid': '1234'}
NOTIFICATION_ARGS = {'uuid': '1234'}
TAG_ARGS = {'tag': 'foobar'}
TRIGGER_ARGS = {'trigger_uuid': '1234'}
FIELD_ARGS = {'trigger_uuid': '1234', 'field_name': 'foobar'}
@pytest.mark.parametrize('resource, method, params', [
(Repository, 'delete', None),
(RepositoryBuildList, 'get', None),
(RepositoryBuildList, 'post', None),
(RepositoryBuildResource, 'get', BUILD_ARGS),
(RepositoryBuildResource, 'delete', BUILD_ARGS),
(RepositoryBuildStatus, 'get', BUILD_ARGS),
(RepositoryBuildLogs, 'get', BUILD_ARGS),
(RepositoryImageList, 'get', None),
(RepositoryImage, 'get', IMAGE_ARGS),
(RepositoryManifestLabels, 'get', MANIFEST_ARGS),
(RepositoryManifestLabels, 'post', MANIFEST_ARGS),
(ManageRepositoryManifestLabel, 'get', LABEL_ARGS),
(ManageRepositoryManifestLabel, 'delete', LABEL_ARGS),
(RepositoryNotificationList, 'get', None),
(RepositoryNotificationList, 'post', None),
(RepositoryNotification, 'get', NOTIFICATION_ARGS),
(RepositoryNotification, 'delete', NOTIFICATION_ARGS),
(TestRepositoryNotification, 'post', NOTIFICATION_ARGS),
(RepositoryImageSecurity, 'get', IMAGE_ARGS),
(RepositoryManifestSecurity, 'get', MANIFEST_ARGS),
(ListRepositoryTags, 'get', None),
(RepositoryTag, 'put', TAG_ARGS),
(RepositoryTag, 'delete', TAG_ARGS),
(RepositoryTagImages, 'get', TAG_ARGS),
(RestoreTag, 'post', TAG_ARGS),
(BuildTriggerList, 'get', None),
(BuildTrigger, 'get', TRIGGER_ARGS),
(BuildTrigger, 'delete', TRIGGER_ARGS),
(BuildTriggerSubdirs, 'post', TRIGGER_ARGS),
(BuildTriggerActivate, 'post', TRIGGER_ARGS),
(BuildTriggerAnalyze, 'post', TRIGGER_ARGS),
(ActivateBuildTrigger, 'post', TRIGGER_ARGS),
(TriggerBuildList, 'get', TRIGGER_ARGS),
(BuildTriggerFieldValues, 'post', FIELD_ARGS),
(BuildTriggerSources, 'post', TRIGGER_ARGS),
(BuildTriggerSourceNamespaces, 'get', TRIGGER_ARGS),
])
def test_disallowed_for_apps(resource, method, params, client):
namespace = 'devtable'
repository = 'someapprepo'
devtable = model.user.get_user('devtable')
model.repository.create_repository(namespace, repository, devtable, repo_kind='application')
params = params or {}
params['repository'] = '%s/%s' % (namespace, repository)
with client_with_identity('devtable', client) as cl:
conduct_api_call(cl, resource, method, params, None, 501)

View file

@ -1,44 +1,29 @@
import datetime
import pytest
from data import model
from endpoints.api import api
from endpoints.api.test.shared import client_with_identity, conduct_api_call
from endpoints.api.superuser import SuperUserRepositoryBuildLogs, SuperUserRepositoryBuildResource
from endpoints.api.superuser import SuperUserRepositoryBuildStatus
from endpoints.test.fixtures import app, appconfig, database_uri, init_db_path, sqlitedb_file
TEAM_PARAMS = {'orgname': 'buynlarge', 'teamname': 'owners'}
BUILD_PARAMS = {'build_uuid': 'test-1234'}
def client_with_identity(auth_username, client):
with client.session_transaction() as sess:
if auth_username:
if auth_username is not None:
loaded = model.user.get_user(auth_username)
sess['user_id'] = loaded.uuid
sess['login_time'] = datetime.datetime.now()
return client
@pytest.mark.parametrize('resource,method,params,body,identity,expected', [
(SuperUserRepositoryBuildLogs, 'GET', BUILD_PARAMS, None, None, 401),
(SuperUserRepositoryBuildLogs, 'GET', BUILD_PARAMS, None, 'freshuser', 403),
(SuperUserRepositoryBuildLogs, 'GET', BUILD_PARAMS, None, 'reader', 403),
(SuperUserRepositoryBuildLogs, 'GET', BUILD_PARAMS, None, 'devtable', 400),
(SuperUserRepositoryBuildStatus, 'GET', BUILD_PARAMS, None, None, 401),
(SuperUserRepositoryBuildStatus, 'GET', BUILD_PARAMS, None, 'freshuser', 403),
(SuperUserRepositoryBuildStatus, 'GET', BUILD_PARAMS, None, 'reader', 403),
(SuperUserRepositoryBuildStatus, 'GET', BUILD_PARAMS, None, 'devtable', 400),
@pytest.mark.parametrize('resource,identity,expected', [
(SuperUserRepositoryBuildLogs, None, 401),
(SuperUserRepositoryBuildLogs, 'freshuser', 403),
(SuperUserRepositoryBuildLogs, 'reader', 403),
(SuperUserRepositoryBuildLogs, 'devtable', 400),
(SuperUserRepositoryBuildStatus, None, 401),
(SuperUserRepositoryBuildStatus, 'freshuser', 403),
(SuperUserRepositoryBuildStatus, 'reader', 403),
(SuperUserRepositoryBuildStatus, 'devtable', 400),
(SuperUserRepositoryBuildResource, None, 401),
(SuperUserRepositoryBuildResource, 'freshuser', 403),
(SuperUserRepositoryBuildResource, 'reader', 403),
(SuperUserRepositoryBuildResource, 'devtable', 404),
(SuperUserRepositoryBuildResource, 'GET', BUILD_PARAMS, None, None, 401),
(SuperUserRepositoryBuildResource, 'GET', BUILD_PARAMS, None, 'freshuser', 403),
(SuperUserRepositoryBuildResource, 'GET', BUILD_PARAMS, None, 'reader', 403),
(SuperUserRepositoryBuildResource, 'GET', BUILD_PARAMS, None, 'devtable', 404),
])
def test_super_user_build_endpoints(resource, identity, expected, client):
cl = client_with_identity(identity, client)
final_url = api.url_for(resource, build_uuid='1234')
rv = cl.open(final_url)
msg = '%s %s: %s expected: %s' % ('GET', final_url, rv.status_code, expected)
assert rv.status_code == expected, msg
def test_api_security(resource, method, params, body, identity, expected, client):
with client_with_identity(identity, client) as cl:
conduct_api_call(cl, resource, method, params, body, expected)

View file

@ -15,7 +15,8 @@ from buildtrigger.triggerutil import (TriggerDeactivationException,
RepositoryReadException, TriggerStartException)
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, request_error, query_param, parse_args, internal_only,
validate_json_request, api, path_param, abort)
validate_json_request, api, path_param, abort,
disallow_for_app_repositories)
from endpoints.exception import NotFound, Unauthorized, InvalidRequest
from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus
from endpoints.building import start_build, MaximumBuildsQueuedException
@ -40,6 +41,7 @@ class BuildTriggerList(RepositoryParamResource):
""" Resource for listing repository build triggers. """
@require_repo_admin
@disallow_for_app_repositories
@nickname('listBuildTriggers')
def get(self, namespace_name, repo_name):
""" List the triggers for the specified repository. """
@ -56,6 +58,7 @@ class BuildTrigger(RepositoryParamResource):
""" Resource for managing specific build triggers. """
@require_repo_admin
@disallow_for_app_repositories
@nickname('getBuildTrigger')
def get(self, namespace_name, repo_name, trigger_uuid):
""" Get information for the specified build trigger. """
@ -67,6 +70,7 @@ class BuildTrigger(RepositoryParamResource):
return trigger_view(trigger, can_admin=True)
@require_repo_admin
@disallow_for_app_repositories
@nickname('deleteBuildTrigger')
def delete(self, namespace_name, repo_name, trigger_uuid):
""" Delete the specified build trigger. """
@ -110,6 +114,7 @@ class BuildTriggerSubdirs(RepositoryParamResource):
}
@require_repo_admin
@disallow_for_app_repositories
@nickname('listBuildTriggerSubdirs')
@validate_json_request('BuildTriggerSubdirRequest')
def post(self, namespace_name, repo_name, trigger_uuid):
@ -170,6 +175,7 @@ class BuildTriggerActivate(RepositoryParamResource):
}
@require_repo_admin
@disallow_for_app_repositories
@nickname('activateBuildTrigger')
@validate_json_request('BuildTriggerActivateRequest')
def post(self, namespace_name, repo_name, trigger_uuid):
@ -271,6 +277,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
}
@require_repo_admin
@disallow_for_app_repositories
@nickname('analyzeBuildTrigger')
@validate_json_request('BuildTriggerAnalyzeRequest')
def post(self, namespace_name, repo_name, trigger_uuid):
@ -420,6 +427,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
}
@require_repo_admin
@disallow_for_app_repositories
@nickname('manuallyStartBuildTrigger')
@validate_json_request('RunParameters')
def post(self, namespace_name, repo_name, trigger_uuid):
@ -460,6 +468,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
class TriggerBuildList(RepositoryParamResource):
""" Resource to represent builds that were activated from the specified trigger. """
@require_repo_admin
@disallow_for_app_repositories
@parse_args()
@query_param('limit', 'The maximum number of builds to return', type=int, default=5)
@nickname('listTriggerRecentBuilds')
@ -479,6 +488,7 @@ FIELD_VALUE_LIMIT = 30
class BuildTriggerFieldValues(RepositoryParamResource):
""" Custom verb to fetch a values list for a particular field name. """
@require_repo_admin
@disallow_for_app_repositories
@nickname('listTriggerFieldValues')
def post(self, namespace_name, repo_name, trigger_uuid, field_name):
""" List the field values for a custom run field. """
@ -522,6 +532,7 @@ class BuildTriggerSources(RepositoryParamResource):
}
@require_repo_admin
@disallow_for_app_repositories
@nickname('listTriggerBuildSources')
@validate_json_request('BuildTriggerSourcesRequest')
def post(self, namespace_name, repo_name, trigger_uuid):
@ -555,6 +566,7 @@ class BuildTriggerSources(RepositoryParamResource):
class BuildTriggerSourceNamespaces(RepositoryParamResource):
""" Custom verb to fetch the list of namespaces (orgs, projects, etc) for the trigger config. """
@require_repo_admin
@disallow_for_app_repositories
@nickname('listTriggerBuildSourceNamespaces')
def get(self, namespace_name, repo_name, trigger_uuid):
""" List the build sources for the trigger configuration thus far. """

View file

@ -29,6 +29,9 @@ class MaximumBuildsQueuedException(Exception):
def start_build(repository, prepared_build, pull_robot_name=None):
if repository.kind.name != 'image':
raise Exception('Attempt to start a build for application repository %s' % repository.id)
if MAX_BUILD_QUEUE_RATE_ITEMS > 0 and MAX_BUILD_QUEUE_RATE_SECS > 0:
queue_item_canonical_name = [repository.namespace_user.username, repository.name]
now = datetime.utcnow()

View file

@ -31,6 +31,8 @@ def attach_github_build_trigger(namespace_name, repo_name):
if not repo:
msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
abort(404, message=msg)
elif repo.kind.name != 'image':
abort(501)
trigger = model.build.create_build_trigger(repo, 'github', token, current_user.db_user())
repo_path = '%s/%s' % (namespace_name, repo_name)

View file

@ -44,6 +44,8 @@ def attach_gitlab_build_trigger():
if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository)
abort(404, message=msg)
elif repo.kind.name != 'image':
abort(501)
trigger = model.build.create_build_trigger(repo, 'gitlab', token, current_user.db_user())
repo_path = '%s/%s' % (namespace, repository)

View file

@ -182,6 +182,10 @@ def create_repository(namespace_name, repo_name):
message='You do not have permission to modify repository %(namespace)s/%(repository)s',
issue='no-repo-write-permission',
namespace=namespace_name, repository=repo_name)
elif repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, namespace=namespace_name)
else:
create_perm = CreateRepositoryPermission(namespace_name)
if not create_perm.can():
@ -223,6 +227,9 @@ def update_images(namespace_name, repo_name):
if not repo:
# Make sure the repo actually exists.
abort(404, message='Unknown repository', issue='unknown-repo')
elif repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, namespace=namespace_name)
# Generate a job for each notification that has been added to this repo
logger.debug('Adding notifications for repository')
@ -255,6 +262,9 @@ def get_repository_images(namespace_name, repo_name):
repo = model.get_repository(namespace_name, repo_name)
if not repo:
abort(404, message='Unknown repository', issue='unknown-repo')
elif repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, namespace=namespace_name)
logger.debug('Building repository image response')
resp = make_response(json.dumps([]), 200)

View file

@ -83,6 +83,11 @@ def head_image_layer(namespace, repository, image_id, headers):
logger.debug('Checking repo permissions')
if permission.can() or model.repository_is_public(namespace, repository):
repo = model.get_repository(namespace, repository)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, image_id=image_id)
logger.debug('Looking up placement locations')
locations, _ = model.placement_locations_and_path_docker_v1(namespace, repository, image_id)
if locations is None:
@ -116,6 +121,11 @@ def get_image_layer(namespace, repository, image_id, headers):
logger.debug('Checking repo permissions')
if permission.can() or model.repository_is_public(namespace, repository):
repo = model.get_repository(namespace, repository)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, image_id=image_id)
logger.debug('Looking up placement locations and path')
locations, path = model.placement_locations_and_path_docker_v1(namespace, repository, image_id)
if not locations or not path:
@ -151,6 +161,11 @@ def put_image_layer(namespace, repository, image_id):
if not permission.can():
abort(403)
repo = model.get_repository(namespace, repository)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, image_id=image_id)
logger.debug('Retrieving image')
if model.storage_exists(namespace, repository, image_id):
exact_abort(409, 'Image already exists')
@ -255,6 +270,11 @@ def put_image_checksum(namespace, repository, image_id):
if not permission.can():
abort(403)
repo = model.get_repository(namespace, repository)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, image_id=image_id)
# Docker Version < 0.10 (tarsum+sha):
old_checksum = request.headers.get('X-Docker-Checksum')
@ -324,6 +344,11 @@ def get_image_json(namespace, repository, image_id, headers):
if not permission.can() and not model.repository_is_public(namespace, repository):
abort(403)
repo = model.get_repository(namespace, repository)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, image_id=image_id)
logger.debug('Looking up repo image')
v1_metadata = model.docker_v1_metadata(namespace, repository, image_id)
if v1_metadata is None:
@ -353,6 +378,11 @@ def get_image_ancestry(namespace, repository, image_id, headers):
if not permission.can() and not model.repository_is_public(namespace, repository):
abort(403)
repo = model.get_repository(namespace, repository)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, image_id=image_id)
ancestry_docker_ids = model.image_ancestry(namespace, repository, image_id)
if ancestry_docker_ids is None:
abort(404, 'Image %(image_id)s not found', issue='unknown-image', image_id=image_id)
@ -373,6 +403,11 @@ def put_image_json(namespace, repository, image_id):
if not permission.can():
abort(403)
repo = model.get_repository(namespace, repository)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, image_id=image_id)
logger.debug('Parsing image JSON')
try:
uploaded_metadata = request.data

View file

@ -27,6 +27,11 @@ def get_tags(namespace_name, repo_name):
permission = ReadRepositoryPermission(namespace_name, repo_name)
if permission.can() or model.repository_is_public(namespace_name, repo_name):
repo = model.get_repository(namespace_name, repo_name)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, namespace=namespace_name)
tags = model.list_tags(namespace_name, repo_name)
tag_map = {tag.name: tag.image.docker_image_id for tag in tags}
return jsonify(tag_map)
@ -42,6 +47,11 @@ def get_tag(namespace_name, repo_name, tag):
permission = ReadRepositoryPermission(namespace_name, repo_name)
if permission.can() or model.repository_is_public(namespace_name, repo_name):
repo = model.get_repository(namespace_name, repo_name)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, namespace=namespace_name)
image_id = model.find_image_id_by_tag(namespace_name, repo_name, tag)
if image_id is None:
abort(404)
@ -64,6 +74,11 @@ def put_tag(namespace_name, repo_name, tag):
if not TAG_REGEX.match(tag):
abort(400, TAG_ERROR)
repo = model.get_repository(namespace_name, repo_name)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, namespace=namespace_name)
image_id = json.loads(request.data)
model.create_or_update_tag(namespace_name, repo_name, image_id, tag)
@ -86,6 +101,11 @@ def delete_tag(namespace_name, repo_name, tag):
permission = ModifyRepositoryPermission(namespace_name, repo_name)
if permission.can():
repo = model.get_repository(namespace_name, repo_name)
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
abort(405, message=msg, namespace=namespace_name)
model.delete_tag(namespace_name, repo_name, tag)
track_and_log('delete_tag', model.get_repository(namespace_name, repo_name), tag=tag)
return make_response('Deleted', 200)

View file

@ -15,9 +15,9 @@ from auth.auth_context import get_grant_context
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
AdministerRepositoryPermission)
from auth.registry_jwt_auth import process_registry_jwt_auth, get_auth_headers
from data import model
from data.interfaces.v2 import pre_oci_model as model
from endpoints.decorators import anon_protect, anon_allowed
from endpoints.v2.errors import V2RegistryException, Unauthorized
from endpoints.v2.errors import V2RegistryException, Unauthorized, Unsupported, NameUnknown
from util.http import abort
from util.metrics.metricqueue import time_blueprint
from util.registry.dockerver import docker_version
@ -96,13 +96,21 @@ def _require_repo_permission(permission_class, scopes=None, allow_public=False):
def wrapped(namespace_name, repo_name, *args, **kwargs):
logger.debug('Checking permission %s for repo: %s/%s', permission_class,
namespace_name, repo_name)
repository = namespace_name + '/' + repo_name
repo = model.get_repository(namespace_name, repo_name)
if repo is None:
raise Unauthorized(repository=repository, scopes=scopes)
permission = permission_class(namespace_name, repo_name)
if (permission.can() or
(allow_public and
model.repository.repository_is_public(namespace_name, repo_name))):
repo.is_public)):
if repo.kind != 'image':
msg = 'This repository is for managing %s resources and not container images.' % repo.kind
raise Unsupported(detail=msg)
return func(namespace_name, repo_name, *args, **kwargs)
repository = namespace_name + '/' + repo_name
raise Unauthorized(repository=repository, scopes=scopes)
return wrapped
return wrapper

View file

@ -3,7 +3,6 @@ from flask import jsonify
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, paginate
from endpoints.v2.errors import NameUnknown
from endpoints.decorators import anon_protect
from data.interfaces.v2 import pre_oci_model as model
@ -14,10 +13,6 @@ from data.interfaces.v2 import pre_oci_model as model
@anon_protect
@paginate()
def list_all_tags(namespace_name, repo_name, limit, offset, pagination_callback):
repo = model.get_repository(namespace_name, repo_name)
if repo is None:
raise NameUnknown()
tags = model.repository_tags(namespace_name, repo_name, limit, offset)
response = jsonify({
'name': '{0}/{1}'.format(namespace_name, repo_name),

View file

@ -91,15 +91,25 @@ def generate_registry_jwt():
final_actions = []
repo = model.get_repository(namespace, reponame)
repo_is_public = repo is not None and repo.is_public
invalid_repo_message = ''
if repo is not None and repo.kind != 'image':
invalid_repo_message = (('This repository is for managing %s resources ' +
'and not container images.') % repo.kind)
if 'push' in actions:
# If there is no valid user or token, then the repository cannot be
# accessed.
if user is not None or token is not None:
# Lookup the repository. If it exists, make sure the entity has modify
# permission. Otherwise, make sure the entity has create permission.
repo = model.get_repository(namespace, reponame)
if repo:
if ModifyRepositoryPermission(namespace, reponame).can():
if repo.kind != 'image':
abort(405, invalid_repo_message)
final_actions.append('push')
else:
logger.debug('No permission to modify repository %s/%s', namespace, reponame)
@ -113,19 +123,24 @@ def generate_registry_jwt():
if 'pull' in actions:
# Grant pull if the user can read the repo or it is public.
if (ReadRepositoryPermission(namespace, reponame).can() or
model.repository_is_public(namespace, reponame)):
if ReadRepositoryPermission(namespace, reponame).can() or repo_is_public:
if repo is not None and repo.kind != 'image':
abort(405, invalid_repo_message)
final_actions.append('pull')
else:
logger.debug('No permission to pull repository %s/%s', namespace, reponame)
if '*' in actions:
# Grant * user is admin
if (AdministerRepositoryPermission(namespace, reponame).can()):
if AdministerRepositoryPermission(namespace, reponame).can():
if repo is not None and repo.kind != 'image':
abort(405, invalid_repo_message)
final_actions.append('*')
else:
logger.debug("No permission to administer repository %s/%s", namespace, reponame)
# Add the access for the JWT.
access.append({
'type': 'repository',

View file

@ -154,29 +154,35 @@ def _torrent_repo_verb(repo_image, tag, verb, **kwargs):
abort(406)
# Return the torrent.
public_repo = model.repository_is_public(repo_image.repository.namespace_name,
repo_image.repository.name)
torrent = _torrent_for_blob(derived_image.blob, public_repo)
repo = model.get_repository(repo_image.repository.namespace_name,
repo_image.repository.name)
repo_is_public = repo is not None and repo.is_public
torrent = _torrent_for_blob(derived_image.blob, repo_is_public)
# Log the action.
track_and_log('repo_verb', repo_image.repository, tag=tag, verb=verb, torrent=True, **kwargs)
return torrent
def _verify_repo_verb(_, namespace, repository, tag, verb, checker=None):
permission = ReadRepositoryPermission(namespace, repository)
if not permission.can() and not model.repository_is_public(namespace, repository):
def _verify_repo_verb(_, namespace, repo_name, tag, verb, checker=None):
permission = ReadRepositoryPermission(namespace, repo_name)
repo = model.get_repository(namespace, repo_name)
repo_is_public = repo is not None and repo.is_public
if not permission.can() and not repo_is_public:
abort(403)
# Lookup the requested tag.
tag_image = model.get_tag_image(namespace, repository, tag)
tag_image = model.get_tag_image(namespace, repo_name, tag)
if tag_image is None:
abort(404)
if repo is not None and repo.kind != 'image':
abort(405)
# If there is a data checker, call it first.
if checker is not None:
if not checker(tag_image):
logger.debug('Check mismatch on %s/%s:%s, verb %s', namespace, repository, tag, verb)
logger.debug('Check mismatch on %s/%s:%s, verb %s', namespace, repo_name, tag, verb)
abort(404)
return tag_image
@ -345,19 +351,24 @@ def get_squashed_tag(namespace, repository, tag):
@process_auth
@parse_repository_name()
def get_tag_torrent(namespace_name, repo_name, digest):
repo = model.get_repository(namespace_name, repo_name)
repo_is_public = repo is not None and repo.is_public
permission = ReadRepositoryPermission(namespace_name, repo_name)
public_repo = model.repository_is_public(namespace_name, repo_name)
if not permission.can() and not public_repo:
if not permission.can() and not repo_is_public:
abort(403)
user = get_authenticated_user()
if user is None and not public_repo:
if user is None and not repo_is_public:
# We can not generate a private torrent cluster without a user uuid (e.g. token auth)
abort(403)
if repo is not None and repo.kind != 'image':
abort(405)
blob = model.get_repo_blob_by_digest(namespace_name, repo_name, digest)
if blob is None:
abort(404)
metric_queue.repository_pull.Inc(labelvalues=[namespace_name, repo_name, 'torrent', True])
return _torrent_for_blob(blob, public_repo)
return _torrent_for_blob(blob, repo_is_public)

View file

@ -426,9 +426,12 @@ def confirm_recovery():
@anon_protect
def build_status_badge(namespace_name, repo_name):
token = request.args.get('token', None)
repo = model.repository.get_repository(namespace_name, repo_name)
if repo and repo.kind.name != 'image':
abort(404)
is_public = model.repository.repository_is_public(namespace_name, repo_name)
if not is_public:
repo = model.repository.get_repository(namespace_name, repo_name)
if not repo or token != repo.badge_token:
abort(404)
@ -628,6 +631,8 @@ def attach_bitbucket_trigger(namespace_name, repo_name):
if not repo:
msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
abort(404, message=msg)
elif repo.kind.name != 'image':
abort(501)
trigger = model.build.create_build_trigger(repo, BitbucketBuildTrigger.service_name(), None,
current_user.db_user())
@ -661,6 +666,8 @@ def attach_custom_build_trigger(namespace_name, repo_name):
if not repo:
msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
abort(404, message=msg)
elif repo.kind.name != 'image':
abort(501)
trigger = model.build.create_build_trigger(repo, CustomBuildTrigger.service_name(),
None, current_user.db_user())