Fix issue with Docker 1.8.3 and pulling public repos with no creds
We now return the valid subset of auth scopes requested. Adds a test for this case and adds testing of all returned JWTs in the V2 login tests
This commit is contained in:
parent
566a91f003
commit
8cd38569d6
5 changed files with 202 additions and 148 deletions
|
@ -9,7 +9,7 @@ import features
|
||||||
|
|
||||||
from app import metric_queue
|
from app import metric_queue
|
||||||
from endpoints.decorators import anon_protect, anon_allowed
|
from endpoints.decorators import anon_protect, anon_allowed
|
||||||
from endpoints.v2.errors import V2RegistryException
|
from endpoints.v2.errors import V2RegistryException, Unauthorized
|
||||||
from auth.auth_context import get_grant_context
|
from auth.auth_context import get_grant_context
|
||||||
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
||||||
AdministerRepositoryPermission)
|
AdministerRepositoryPermission)
|
||||||
|
@ -45,7 +45,7 @@ def _require_repo_permission(permission_class, allow_public=False):
|
||||||
(allow_public and
|
(allow_public and
|
||||||
model.repository.repository_is_public(namespace, repo_name))):
|
model.repository.repository_is_public(namespace, repo_name))):
|
||||||
return func(namespace, repo_name, *args, **kwargs)
|
return func(namespace, repo_name, *args, **kwargs)
|
||||||
raise abort(401)
|
raise Unauthorized()
|
||||||
return wrapped
|
return wrapped
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,12 @@ def generate_registry_jwt():
|
||||||
oauthtoken = get_validated_oauth_token()
|
oauthtoken = get_validated_oauth_token()
|
||||||
logger.debug('Authenticated OAuth token: %s', oauthtoken)
|
logger.debug('Authenticated OAuth token: %s', oauthtoken)
|
||||||
|
|
||||||
|
auth_credentials_sent = bool(request.headers.get('authorization', ''))
|
||||||
|
if auth_credentials_sent and not user and not token:
|
||||||
|
# The auth credentials sent for the user are invalid.
|
||||||
|
logger.debug('Invalid auth credentials')
|
||||||
|
abort(401)
|
||||||
|
|
||||||
access = []
|
access = []
|
||||||
user_event_data = {
|
user_event_data = {
|
||||||
'action': 'login',
|
'action': 'login',
|
||||||
|
@ -84,7 +90,7 @@ def generate_registry_jwt():
|
||||||
|
|
||||||
# Ensure that we are never creating an invalid repository.
|
# Ensure that we are never creating an invalid repository.
|
||||||
if not REPOSITORY_NAME_REGEX.match(reponame):
|
if not REPOSITORY_NAME_REGEX.match(reponame):
|
||||||
logger.debug('Found invalid repository name in auth flow: %v', reponame)
|
logger.debug('Found invalid repository name in auth flow: %s', reponame)
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
final_actions = []
|
final_actions = []
|
||||||
|
@ -92,38 +98,30 @@ def generate_registry_jwt():
|
||||||
if 'push' in actions:
|
if 'push' in actions:
|
||||||
# If there is no valid user or token, then the repository cannot be
|
# If there is no valid user or token, then the repository cannot be
|
||||||
# accessed.
|
# accessed.
|
||||||
if user is None and token is None:
|
if user is not None or token is not None:
|
||||||
logger.debug('No user and no token for requested "push" scope')
|
# Lookup the repository. If it exists, make sure the entity has modify
|
||||||
abort(401)
|
# permission. Otherwise, make sure the entity has create permission.
|
||||||
|
repo = model.repository.get_repository(namespace, reponame)
|
||||||
# Lookup the repository. If it exists, make sure the entity has modify
|
if repo:
|
||||||
# permission. Otherwise, make sure the entity has create permission.
|
if ModifyRepositoryPermission(namespace, reponame).can():
|
||||||
repo = model.repository.get_repository(namespace, reponame)
|
final_actions.append('push')
|
||||||
if repo:
|
else:
|
||||||
if not ModifyRepositoryPermission(namespace, reponame).can():
|
logger.debug('No permission to modify repository %s/%s', namespace, reponame)
|
||||||
logger.debug('No permission to modify repository %v/%v', namespace, reponame)
|
else:
|
||||||
abort(403)
|
if CreateRepositoryPermission(namespace).can() and user is not None:
|
||||||
else:
|
logger.debug('Creating repository: %s/%s', namespace, reponame)
|
||||||
if not CreateRepositoryPermission(namespace).can() or user is None:
|
model.repository.create_repository(namespace, reponame, user)
|
||||||
logger.debug('No permission to create repository %v/%v', namespace, reponame)
|
final_actions.append('push')
|
||||||
abort(403)
|
else:
|
||||||
|
logger.debug('No permission to create repository %s/%s', namespace, reponame)
|
||||||
logger.debug('Creating repository: %s/%s', namespace, reponame)
|
|
||||||
model.repository.create_repository(namespace, reponame, user)
|
|
||||||
|
|
||||||
final_actions.append('push')
|
|
||||||
|
|
||||||
if 'pull' in actions:
|
if 'pull' in actions:
|
||||||
# Grant pull if the user can read the repo or it is public. We also
|
# Grant pull if the user can read the repo or it is public.
|
||||||
# grant it if the user already has push, as they can clearly change
|
|
||||||
# the repository.
|
|
||||||
if (ReadRepositoryPermission(namespace, reponame).can() or
|
if (ReadRepositoryPermission(namespace, reponame).can() or
|
||||||
model.repository.repository_is_public(namespace, reponame) or
|
model.repository.repository_is_public(namespace, reponame)):
|
||||||
'push' in final_actions):
|
|
||||||
final_actions.append('pull')
|
final_actions.append('pull')
|
||||||
else:
|
else:
|
||||||
logger.debug('No permission to pull repository %v/%v', namespace, reponame)
|
logger.debug('No permission to pull repository %s/%s', namespace, reponame)
|
||||||
abort(403)
|
|
||||||
|
|
||||||
# Add the access for the JWT.
|
# Add the access for the JWT.
|
||||||
access.append({
|
access.append({
|
||||||
|
@ -137,6 +135,8 @@ def generate_registry_jwt():
|
||||||
user_action = 'push_start'
|
user_action = 'push_start'
|
||||||
elif 'pull' in final_actions:
|
elif 'pull' in final_actions:
|
||||||
user_action = 'pull_start'
|
user_action = 'pull_start'
|
||||||
|
else:
|
||||||
|
user_action = 'login'
|
||||||
|
|
||||||
user_event_data = {
|
user_event_data = {
|
||||||
'action': user_action,
|
'action': user_action,
|
||||||
|
|
|
@ -8,9 +8,12 @@ import resumablehashlib
|
||||||
import binascii
|
import binascii
|
||||||
|
|
||||||
import Crypto.Random
|
import Crypto.Random
|
||||||
|
from cachetools import lru_cache
|
||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from flask.blueprints import Blueprint
|
from flask.blueprints import Blueprint
|
||||||
from flask.ext.testing import LiveServerTestCase
|
from flask.ext.testing import LiveServerTestCase
|
||||||
|
from cryptography.x509 import load_pem_x509_certificate
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
from app import app, storage
|
from app import app, storage
|
||||||
from data.database import close_db_filter, configure
|
from data.database import close_db_filter, configure
|
||||||
|
@ -24,6 +27,7 @@ from initdb import wipe_database, initialize_database, populate_database
|
||||||
from endpoints.csrf import generate_csrf_token
|
from endpoints.csrf import generate_csrf_token
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from jsonschema import validate as validate_schema
|
from jsonschema import validate as validate_schema
|
||||||
|
from util.security import strictjwt
|
||||||
|
|
||||||
import endpoints.decorated
|
import endpoints.decorated
|
||||||
import json
|
import json
|
||||||
|
@ -137,6 +141,23 @@ _PORT_NUMBER = 5001
|
||||||
_CLEAN_DATABASE_PATH = None
|
_CLEAN_DATABASE_PATH = None
|
||||||
_JWK = RSAKey(key=RSA.generate(2048))
|
_JWK = RSAKey(key=RSA.generate(2048))
|
||||||
|
|
||||||
|
class FailureCodes:
|
||||||
|
""" Defines tuples representing the HTTP status codes for various errors. The tuple
|
||||||
|
is defined as ('errordescription', V1HTTPStatusCode, V2HTTPStatusCode). """
|
||||||
|
|
||||||
|
UNAUTHENTICATED = ('unauthenticated', 401, 401)
|
||||||
|
UNAUTHORIZED = ('unauthorized', 403, 401)
|
||||||
|
INVALID_REGISTRY = ('invalidregistry', 404, 404)
|
||||||
|
DOES_NOT_EXIST = ('doesnotexist', 404, 404)
|
||||||
|
INVALID_REQUEST = ('invalidrequest', 400, 400)
|
||||||
|
|
||||||
|
def _get_expected_code(expected_failure, version, success_status_code):
|
||||||
|
""" Returns the HTTP status code for the expected failure under the specified protocol version
|
||||||
|
(1 or 2). If none, returns the success status code. """
|
||||||
|
if not expected_failure:
|
||||||
|
return success_status_code
|
||||||
|
|
||||||
|
return expected_failure[version]
|
||||||
|
|
||||||
def _get_repo_name(namespace, name):
|
def _get_repo_name(namespace, name):
|
||||||
if namespace == '':
|
if namespace == '':
|
||||||
|
@ -290,7 +311,7 @@ class V1RegistryMixin(BaseRegistryMixin):
|
||||||
|
|
||||||
|
|
||||||
class V1RegistryPushMixin(V1RegistryMixin):
|
class V1RegistryPushMixin(V1RegistryMixin):
|
||||||
def do_push(self, namespace, repository, username, password, images=None, expected_code=201):
|
def do_push(self, namespace, repository, username, password, images=None, expect_failure=None):
|
||||||
images = images or self._get_default_images()
|
images = images or self._get_default_images()
|
||||||
auth = (username, password)
|
auth = (username, password)
|
||||||
repo_name = _get_repo_name(namespace, repository)
|
repo_name = _get_repo_name(namespace, repository)
|
||||||
|
@ -299,6 +320,7 @@ class V1RegistryPushMixin(V1RegistryMixin):
|
||||||
self.v1_ping()
|
self.v1_ping()
|
||||||
|
|
||||||
# PUT /v1/repositories/{namespace}/{repository}/
|
# PUT /v1/repositories/{namespace}/{repository}/
|
||||||
|
expected_code = _get_expected_code(expect_failure, 1, 201)
|
||||||
self.conduct('PUT', '/v1/repositories/%s' % repo_name,
|
self.conduct('PUT', '/v1/repositories/%s' % repo_name,
|
||||||
data=json.dumps(images), auth=auth,
|
data=json.dumps(images), auth=auth,
|
||||||
expected_code=expected_code)
|
expected_code=expected_code)
|
||||||
|
@ -341,7 +363,7 @@ class V1RegistryPushMixin(V1RegistryMixin):
|
||||||
|
|
||||||
|
|
||||||
class V1RegistryPullMixin(V1RegistryMixin):
|
class V1RegistryPullMixin(V1RegistryMixin):
|
||||||
def do_pull(self, namespace, repository, username=None, password='password', expected_code=200,
|
def do_pull(self, namespace, repository, username=None, password='password', expect_failure=None,
|
||||||
images=None):
|
images=None):
|
||||||
images = images or self._get_default_images()
|
images = images or self._get_default_images()
|
||||||
repo_name = _get_repo_name(namespace, repository)
|
repo_name = _get_repo_name(namespace, repository)
|
||||||
|
@ -356,6 +378,7 @@ class V1RegistryPullMixin(V1RegistryMixin):
|
||||||
prefix = '/v1/repositories/%s/' % repo_name
|
prefix = '/v1/repositories/%s/' % repo_name
|
||||||
|
|
||||||
# GET /v1/repositories/{namespace}/{repository}/
|
# GET /v1/repositories/{namespace}/{repository}/
|
||||||
|
expected_code = _get_expected_code(expect_failure, 1, 200)
|
||||||
self.conduct('GET', prefix + 'images', auth=auth, expected_code=expected_code)
|
self.conduct('GET', prefix + 'images', auth=auth, expected_code=expected_code)
|
||||||
if expected_code != 200:
|
if expected_code != 200:
|
||||||
return
|
return
|
||||||
|
@ -427,7 +450,10 @@ class V2RegistryMixin(BaseRegistryMixin):
|
||||||
|
|
||||||
|
|
||||||
def do_auth(self, username, password, namespace, repository, expected_code=200, scopes=[]):
|
def do_auth(self, username, password, namespace, repository, expected_code=200, scopes=[]):
|
||||||
auth = (username, password)
|
auth = None
|
||||||
|
if username and password:
|
||||||
|
auth = (username, password)
|
||||||
|
|
||||||
repo_name = _get_repo_name(namespace, repository)
|
repo_name = _get_repo_name(namespace, repository)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
|
@ -436,7 +462,7 @@ class V2RegistryMixin(BaseRegistryMixin):
|
||||||
'service': app.config['SERVER_HOSTNAME'],
|
'service': app.config['SERVER_HOSTNAME'],
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.conduct('GET', '/v2/auth', params=params, auth=(username, password),
|
response = self.conduct('GET', '/v2/auth', params=params, auth=auth,
|
||||||
expected_code=expected_code)
|
expected_code=expected_code)
|
||||||
|
|
||||||
if expected_code == 200:
|
if expected_code == 200:
|
||||||
|
@ -449,18 +475,21 @@ class V2RegistryMixin(BaseRegistryMixin):
|
||||||
|
|
||||||
class V2RegistryPushMixin(V2RegistryMixin):
|
class V2RegistryPushMixin(V2RegistryMixin):
|
||||||
def do_push(self, namespace, repository, username, password, images=None, tag_name=None,
|
def do_push(self, namespace, repository, username, password, images=None, tag_name=None,
|
||||||
cancel=False, invalid=False, expected_manifest_code=202, expected_auth_code=200,
|
cancel=False, invalid=False, expect_failure=None, scopes=None):
|
||||||
scopes=None):
|
|
||||||
images = images or self._get_default_images()
|
images = images or self._get_default_images()
|
||||||
repo_name = _get_repo_name(namespace, repository)
|
repo_name = _get_repo_name(namespace, repository)
|
||||||
|
|
||||||
# Ping!
|
# Ping!
|
||||||
self.v2_ping()
|
self.v2_ping()
|
||||||
|
|
||||||
# Auth.
|
# Auth. If the expected failure is an invalid registry, in V2 we'll receive that error from
|
||||||
|
# the auth endpoint first, rather than just the V2 requests below.
|
||||||
|
expected_auth_code = 200
|
||||||
|
if expect_failure == FailureCodes.INVALID_REGISTRY:
|
||||||
|
expected_auth_code = 400
|
||||||
|
|
||||||
self.do_auth(username, password, namespace, repository, scopes=scopes or ['push', 'pull'],
|
self.do_auth(username, password, namespace, repository, scopes=scopes or ['push', 'pull'],
|
||||||
expected_code=expected_auth_code)
|
expected_code=expected_auth_code)
|
||||||
|
|
||||||
if expected_auth_code != 200:
|
if expected_auth_code != 200:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -478,6 +507,8 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
||||||
|
|
||||||
builder.add_layer(checksum, json.dumps(image_data))
|
builder.add_layer(checksum, json.dumps(image_data))
|
||||||
|
|
||||||
|
expected_code = _get_expected_code(expect_failure, 2, 404)
|
||||||
|
|
||||||
# Build the manifest.
|
# Build the manifest.
|
||||||
manifest = builder.build(_JWK)
|
manifest = builder.build(_JWK)
|
||||||
|
|
||||||
|
@ -493,6 +524,11 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
||||||
self.conduct('HEAD', '/v2/%s/blobs/%s' % (repo_name, checksum),
|
self.conduct('HEAD', '/v2/%s/blobs/%s' % (repo_name, checksum),
|
||||||
expected_code=404, auth='jwt')
|
expected_code=404, auth='jwt')
|
||||||
|
|
||||||
|
# If we expected a non-404 status code, then the HEAD operation has failed and we cannot
|
||||||
|
# continue performing the push.
|
||||||
|
if expected_code != 404:
|
||||||
|
return
|
||||||
|
|
||||||
# Start a new upload of the layer data.
|
# Start a new upload of the layer data.
|
||||||
response = self.conduct('POST', '/v2/%s/blobs/uploads/' % repo_name,
|
response = self.conduct('POST', '/v2/%s/blobs/uploads/' % repo_name,
|
||||||
expected_code=202, auth='jwt')
|
expected_code=202, auth='jwt')
|
||||||
|
@ -548,8 +584,9 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
||||||
self.assertEquals(response.headers['Docker-Content-Digest'], checksum)
|
self.assertEquals(response.headers['Docker-Content-Digest'], checksum)
|
||||||
self.assertEquals(response.headers['Content-Length'], str(len(layer_bytes)))
|
self.assertEquals(response.headers['Content-Length'], str(len(layer_bytes)))
|
||||||
|
|
||||||
# Write the manifest.
|
# Write the manifest. If we expect it to be invalid, we expect a 404 code. Otherwise, we expect
|
||||||
put_code = 404 if invalid else expected_manifest_code
|
# a 202 response for success.
|
||||||
|
put_code = 404 if invalid else 202
|
||||||
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
||||||
data=manifest.bytes, expected_code=put_code,
|
data=manifest.bytes, expected_code=put_code,
|
||||||
headers={'Content-Type': 'application/json'}, auth='jwt')
|
headers={'Content-Type': 'application/json'}, auth='jwt')
|
||||||
|
@ -558,25 +595,32 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
||||||
|
|
||||||
|
|
||||||
class V2RegistryPullMixin(V2RegistryMixin):
|
class V2RegistryPullMixin(V2RegistryMixin):
|
||||||
def do_pull(self, namespace, repository, username=None, password='password', expected_code=200,
|
def do_pull(self, namespace, repository, username=None, password='password', expect_failure=None,
|
||||||
manifest_id=None, expected_manifest_code=200, images=None):
|
manifest_id=None, images=None):
|
||||||
images = images or self._get_default_images()
|
images = images or self._get_default_images()
|
||||||
repo_name = _get_repo_name(namespace, repository)
|
repo_name = _get_repo_name(namespace, repository)
|
||||||
|
|
||||||
# Ping!
|
# Ping!
|
||||||
self.v2_ping()
|
self.v2_ping()
|
||||||
|
|
||||||
# Auth.
|
# Auth. If the failure expected is unauthenticated, then the auth endpoint will 401 before
|
||||||
|
# we reach any of the registry operations.
|
||||||
|
expected_auth_code = 200
|
||||||
|
if expect_failure == FailureCodes.UNAUTHENTICATED:
|
||||||
|
expected_auth_code = 401
|
||||||
|
|
||||||
self.do_auth(username, password, namespace, repository, scopes=['pull'],
|
self.do_auth(username, password, namespace, repository, scopes=['pull'],
|
||||||
expected_code=expected_code)
|
expected_code=expected_auth_code)
|
||||||
if expected_code != 200:
|
if expected_auth_code != 200:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Retrieve the manifest for the tag or digest.
|
# Retrieve the manifest for the tag or digest.
|
||||||
manifest_id = manifest_id or 'latest'
|
manifest_id = manifest_id or 'latest'
|
||||||
|
|
||||||
|
expected_code = _get_expected_code(expect_failure, 2, 200)
|
||||||
response = self.conduct('GET', '/v2/%s/manifests/%s' % (repo_name, manifest_id),
|
response = self.conduct('GET', '/v2/%s/manifests/%s' % (repo_name, manifest_id),
|
||||||
auth='jwt', expected_code=expected_manifest_code)
|
auth='jwt', expected_code=expected_code)
|
||||||
if expected_manifest_code != 200:
|
if expected_code != 200:
|
||||||
return
|
return
|
||||||
|
|
||||||
manifest_data = json.loads(response.text)
|
manifest_data = json.loads(response.text)
|
||||||
|
@ -621,20 +665,23 @@ class V1RegistryLoginMixin(object):
|
||||||
|
|
||||||
|
|
||||||
class V2RegistryLoginMixin(object):
|
class V2RegistryLoginMixin(object):
|
||||||
def do_login(self, username, password, scope, expect_success=True, expected_code=None):
|
def do_login(self, username, password, scope, expect_success=True):
|
||||||
params = {
|
params = {
|
||||||
'account': username,
|
'account': username,
|
||||||
'scope': scope,
|
'scope': scope,
|
||||||
'service': app.config['SERVER_HOSTNAME'],
|
'service': app.config['SERVER_HOSTNAME'],
|
||||||
}
|
}
|
||||||
|
|
||||||
if expected_code is None:
|
if expect_success:
|
||||||
if expect_success:
|
expected_code = 200
|
||||||
expected_code = 200
|
else:
|
||||||
else:
|
expected_code = 401
|
||||||
expected_code = 403
|
|
||||||
|
|
||||||
response = self.conduct('GET', '/v2/auth', params=params, auth=(username, password),
|
auth = None
|
||||||
|
if username and password:
|
||||||
|
auth = (username, password)
|
||||||
|
|
||||||
|
response = self.conduct('GET', '/v2/auth', params=params, auth=auth,
|
||||||
expected_code=expected_code)
|
expected_code=expected_code)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -739,7 +786,9 @@ class RegistryTestsMixin(object):
|
||||||
|
|
||||||
# First try to pull the (currently private) repo anonymously, which should fail (since it is
|
# First try to pull the (currently private) repo anonymously, which should fail (since it is
|
||||||
# private)
|
# private)
|
||||||
self.do_pull('public', 'newrepo', expected_code=403)
|
self.do_pull('public', 'newrepo', expect_failure=FailureCodes.UNAUTHORIZED)
|
||||||
|
self.do_pull('public', 'newrepo', 'devtable', 'password',
|
||||||
|
expect_failure=FailureCodes.UNAUTHORIZED)
|
||||||
|
|
||||||
# Make the repository public.
|
# Make the repository public.
|
||||||
self.conduct_api_login('public', 'password')
|
self.conduct_api_login('public', 'password')
|
||||||
|
@ -757,7 +806,8 @@ class RegistryTestsMixin(object):
|
||||||
|
|
||||||
# First try to pull the (currently private) repo as devtable, which should fail as it belongs
|
# First try to pull the (currently private) repo as devtable, which should fail as it belongs
|
||||||
# to public.
|
# to public.
|
||||||
self.do_pull('public', 'newrepo', 'devtable', 'password', expected_code=403)
|
self.do_pull('public', 'newrepo', 'devtable', 'password',
|
||||||
|
expect_failure=FailureCodes.UNAUTHORIZED)
|
||||||
|
|
||||||
# Make the repository public.
|
# Make the repository public.
|
||||||
self.conduct_api_login('public', 'password')
|
self.conduct_api_login('public', 'password')
|
||||||
|
@ -775,7 +825,8 @@ class RegistryTestsMixin(object):
|
||||||
|
|
||||||
# First try to pull the (currently private) repo as public, which should fail as it belongs
|
# First try to pull the (currently private) repo as public, which should fail as it belongs
|
||||||
# to devtable.
|
# to devtable.
|
||||||
self.do_pull('devtable', 'newrepo', 'public', 'password', expected_code=403)
|
self.do_pull('devtable', 'newrepo', 'public', 'password',
|
||||||
|
expect_failure=FailureCodes.UNAUTHORIZED)
|
||||||
|
|
||||||
# Pull the repository as devtable, which should succeed because the repository is owned by
|
# Pull the repository as devtable, which should succeed because the repository is owned by
|
||||||
# devtable.
|
# devtable.
|
||||||
|
@ -791,7 +842,8 @@ class RegistryTestsMixin(object):
|
||||||
|
|
||||||
# First try to pull the (currently private) repo as devtable, which should fail as it belongs
|
# First try to pull the (currently private) repo as devtable, which should fail as it belongs
|
||||||
# to public.
|
# to public.
|
||||||
self.do_pull('public', 'newrepo', 'devtable', 'password', expected_code=403)
|
self.do_pull('public', 'newrepo', 'devtable', 'password',
|
||||||
|
expect_failure=FailureCodes.UNAUTHORIZED)
|
||||||
|
|
||||||
# Make the repository public.
|
# Make the repository public.
|
||||||
self.conduct_api_login('public', 'password')
|
self.conduct_api_login('public', 'password')
|
||||||
|
@ -811,7 +863,8 @@ class RegistryTestsMixin(object):
|
||||||
|
|
||||||
# First try to pull the (currently private) repo as devtable, which should fail as it belongs
|
# First try to pull the (currently private) repo as devtable, which should fail as it belongs
|
||||||
# to public.
|
# to public.
|
||||||
self.do_pull('public', 'newrepo', 'devtable', 'password', expected_code=403)
|
self.do_pull('public', 'newrepo', 'devtable', 'password',
|
||||||
|
expect_failure=FailureCodes.UNAUTHORIZED)
|
||||||
|
|
||||||
# Pull the repository as public, which should succeed because the repository is owned by public.
|
# Pull the repository as public, which should succeed because the repository is owned by public.
|
||||||
self.do_pull('public', 'newrepo', 'public', 'password')
|
self.do_pull('public', 'newrepo', 'public', 'password')
|
||||||
|
@ -826,7 +879,7 @@ class RegistryTestsMixin(object):
|
||||||
|
|
||||||
# First try to pull the (currently private) repo as anonymous, which should fail as it
|
# First try to pull the (currently private) repo as anonymous, which should fail as it
|
||||||
# is private.
|
# is private.
|
||||||
self.do_pull('public', 'newrepo', expected_code=401)
|
self.do_pull('public', 'newrepo', expect_failure=FailureCodes.UNAUTHENTICATED)
|
||||||
|
|
||||||
# Make the repository public.
|
# Make the repository public.
|
||||||
self.conduct_api_login('public', 'password')
|
self.conduct_api_login('public', 'password')
|
||||||
|
@ -835,7 +888,7 @@ class RegistryTestsMixin(object):
|
||||||
|
|
||||||
# Try again to pull the (currently public) repo as anonymous, which should fail as
|
# Try again to pull the (currently public) repo as anonymous, which should fail as
|
||||||
# anonymous access is disabled.
|
# anonymous access is disabled.
|
||||||
self.do_pull('public', 'newrepo', expected_code=401)
|
self.do_pull('public', 'newrepo', expect_failure=FailureCodes.UNAUTHENTICATED)
|
||||||
|
|
||||||
# Pull the repository as public, which should succeed because the repository is owned by public.
|
# Pull the repository as public, which should succeed because the repository is owned by public.
|
||||||
self.do_pull('public', 'newrepo', 'public', 'password')
|
self.do_pull('public', 'newrepo', 'public', 'password')
|
||||||
|
@ -898,7 +951,8 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix
|
||||||
'id': 'onlyimagehere',
|
'id': 'onlyimagehere',
|
||||||
'contents': 'somecontents',
|
'contents': 'somecontents',
|
||||||
}]
|
}]
|
||||||
self.do_push('public', 'newrepo/somesubrepo', 'public', 'password', images, expected_code=404)
|
self.do_push('public', 'newrepo/somesubrepo', 'public', 'password', images,
|
||||||
|
expect_failure=FailureCodes.INVALID_REGISTRY)
|
||||||
|
|
||||||
def test_push_unicode_metadata(self):
|
def test_push_unicode_metadata(self):
|
||||||
self.conduct_api_login('devtable', 'password')
|
self.conduct_api_login('devtable', 'password')
|
||||||
|
@ -948,7 +1002,7 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
||||||
|
|
||||||
# Ensure the tag no longer exists.
|
# Ensure the tag no longer exists.
|
||||||
self.do_pull('devtable', 'newrepo', 'devtable', 'password',
|
self.do_pull('devtable', 'newrepo', 'devtable', 'password',
|
||||||
expected_manifest_code=404)
|
expect_failure=FailureCodes.DOES_NOT_EXIST)
|
||||||
|
|
||||||
def test_push_only_push_scope(self):
|
def test_push_only_push_scope(self):
|
||||||
images = [{
|
images = [{
|
||||||
|
@ -959,15 +1013,6 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
||||||
self.do_push('devtable', 'somenewrepo', 'devtable', 'password', images,
|
self.do_push('devtable', 'somenewrepo', 'devtable', 'password', images,
|
||||||
scopes=['push'])
|
scopes=['push'])
|
||||||
|
|
||||||
def test_attempt_push_only_push_scope(self):
|
|
||||||
images = [{
|
|
||||||
'id': 'onlyimagehere',
|
|
||||||
'contents': 'foobar',
|
|
||||||
}]
|
|
||||||
|
|
||||||
self.do_push('public', 'somenewrepo', 'devtable', 'password', images,
|
|
||||||
scopes=['push'], expected_auth_code=403)
|
|
||||||
|
|
||||||
def test_push_reponame_with_slashes(self):
|
def test_push_reponame_with_slashes(self):
|
||||||
# Attempt to add a repository name with slashes. This should fail as we do not support it.
|
# Attempt to add a repository name with slashes. This should fail as we do not support it.
|
||||||
images = [{
|
images = [{
|
||||||
|
@ -976,7 +1021,7 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
||||||
}]
|
}]
|
||||||
|
|
||||||
self.do_push('public', 'newrepo/somesubrepo', 'devtable', 'password', images,
|
self.do_push('public', 'newrepo/somesubrepo', 'devtable', 'password', images,
|
||||||
expected_auth_code=400)
|
expect_failure=FailureCodes.INVALID_REGISTRY)
|
||||||
|
|
||||||
def test_invalid_push(self):
|
def test_invalid_push(self):
|
||||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', invalid=True)
|
self.do_push('devtable', 'newrepo', 'devtable', 'password', invalid=True)
|
||||||
|
@ -999,7 +1044,7 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
||||||
|
|
||||||
# Attempt to pull the invalid tag.
|
# Attempt to pull the invalid tag.
|
||||||
self.do_pull('devtable', 'newrepo', 'devtable', 'password', manifest_id='invalid',
|
self.do_pull('devtable', 'newrepo', 'devtable', 'password', manifest_id='invalid',
|
||||||
expected_manifest_code=404)
|
expect_failure=FailureCodes.INVALID_REGISTRY)
|
||||||
|
|
||||||
|
|
||||||
def test_partial_upload_below_5mb(self):
|
def test_partial_upload_below_5mb(self):
|
||||||
|
@ -1097,7 +1142,7 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
||||||
]
|
]
|
||||||
|
|
||||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images,
|
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images,
|
||||||
expected_manifest_code=400)
|
expect_failure=FailureCodes.INVALID_REQUEST)
|
||||||
|
|
||||||
def test_multiple_layers(self):
|
def test_multiple_layers(self):
|
||||||
# Push a manifest with multiple layers.
|
# Push a manifest with multiple layers.
|
||||||
|
@ -1116,7 +1161,8 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
||||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images)
|
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images)
|
||||||
|
|
||||||
def test_invalid_regname(self):
|
def test_invalid_regname(self):
|
||||||
self.do_push('devtable', 'this/is/a/repo', 'devtable', 'password', expected_auth_code=400)
|
self.do_push('devtable', 'this/is/a/repo', 'devtable', 'password',
|
||||||
|
expect_failure=FailureCodes.INVALID_REGISTRY)
|
||||||
|
|
||||||
def test_multiple_tags(self):
|
def test_multiple_tags(self):
|
||||||
latest_images = [
|
latest_images = [
|
||||||
|
@ -1432,49 +1478,108 @@ class V1LoginTests(V1RegistryLoginMixin, LoginTests, RegistryTestCaseMixin, Base
|
||||||
class V2LoginTests(V2RegistryLoginMixin, LoginTests, RegistryTestCaseMixin, BaseRegistryMixin, LiveServerTestCase):
|
class V2LoginTests(V2RegistryLoginMixin, LoginTests, RegistryTestCaseMixin, BaseRegistryMixin, LiveServerTestCase):
|
||||||
""" Tests for V2 login. """
|
""" Tests for V2 login. """
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def load_public_key(certificate_file_path):
|
||||||
|
with open(certificate_file_path) as cert_file:
|
||||||
|
cert_obj = load_pem_x509_certificate(cert_file.read(), default_backend())
|
||||||
|
return cert_obj.public_key()
|
||||||
|
|
||||||
|
|
||||||
|
def do_logincheck(self, username, password, scope, expected_actions=[], expect_success=True):
|
||||||
|
response = self.do_login(username, password, scope, expect_success=expect_success)
|
||||||
|
|
||||||
|
if not expect_success:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Validate the returned JWT.
|
||||||
|
encoded = response.json()['token']
|
||||||
|
|
||||||
|
expected_issuer = app.config['JWT_AUTH_TOKEN_ISSUER']
|
||||||
|
audience = app.config['SERVER_HOSTNAME']
|
||||||
|
|
||||||
|
max_signed_s = app.config.get('JWT_AUTH_MAX_FRESH_S', 3660)
|
||||||
|
certificate_file_path = app.config['JWT_AUTH_CERTIFICATE_PATH']
|
||||||
|
|
||||||
|
public_key = V2LoginTests.load_public_key(certificate_file_path)
|
||||||
|
|
||||||
|
max_exp = strictjwt.exp_max_s_option(max_signed_s)
|
||||||
|
payload = strictjwt.decode(encoded, public_key, algorithms=['RS256'], audience=audience,
|
||||||
|
issuer=expected_issuer, options=max_exp)
|
||||||
|
|
||||||
|
if scope is None:
|
||||||
|
self.assertEquals(0, len(payload['access']))
|
||||||
|
else:
|
||||||
|
self.assertEquals(1, len(payload['access']))
|
||||||
|
self.assertEquals(payload['access'][0]['actions'], expected_actions)
|
||||||
|
|
||||||
|
|
||||||
def test_nouser_noscope(self):
|
def test_nouser_noscope(self):
|
||||||
self.do_login('', '', expected_code=401, scope='')
|
self.do_logincheck('', '', expect_success=False, scope=None)
|
||||||
|
|
||||||
def test_validuser_unknownrepo(self):
|
def test_validuser_unknownrepo(self):
|
||||||
self.do_login('devtable', 'password', expect_success=False,
|
self.do_logincheck('devtable', 'password', expect_success=True,
|
||||||
scope='repository:invalidnamespace/simple:pull')
|
scope='repository:invalidnamespace/simple:pull',
|
||||||
|
expected_actions=[])
|
||||||
|
|
||||||
def test_validuser_unknownnamespacerepo(self):
|
def test_validuser_unknownnamespacerepo(self):
|
||||||
self.do_login('devtable', 'password', expect_success=True,
|
self.do_logincheck('devtable', 'password', expect_success=True,
|
||||||
scope='repository:devtable/newrepo:push')
|
scope='repository:devtable/newrepo:push',
|
||||||
|
expected_actions=['push'])
|
||||||
|
|
||||||
def test_validuser_noaccess(self):
|
def test_validuser_noaccess(self):
|
||||||
self.do_login('public', 'password', expect_success=False,
|
self.do_logincheck('public', 'password', expect_success=True,
|
||||||
scope='repository:devtable/simple:pull')
|
scope='repository:devtable/simple:pull',
|
||||||
|
expected_actions=[])
|
||||||
|
|
||||||
def test_validuser_noscope(self):
|
def test_validuser_noscope(self):
|
||||||
self.do_login('public', 'password', expect_success=True, scope=None)
|
self.do_logincheck('public', 'password', expect_success=True, scope=None)
|
||||||
|
|
||||||
def test_invaliduser_noscope(self):
|
def test_invaliduser_noscope(self):
|
||||||
self.do_login('invaliduser', 'invalidpass', expected_code=401, scope=None)
|
self.do_logincheck('invaliduser', 'invalidpass', expect_success=False, scope=None)
|
||||||
|
|
||||||
def test_invalidpassword_noscope(self):
|
def test_invalidpassword_noscope(self):
|
||||||
self.do_login('public', 'invalidpass', expected_code=401, scope=None)
|
self.do_logincheck('public', 'invalidpass', expect_success=False, scope=None)
|
||||||
|
|
||||||
def test_oauth_noaccess(self):
|
def test_oauth_noaccess(self):
|
||||||
self.do_login('$oauthtoken', 'test', expect_success=False,
|
self.do_logincheck('$oauthtoken', 'test', expect_success=True,
|
||||||
scope='repository:public/publicrepo:pull,push')
|
scope='repository:freshuser/unknownrepo:pull,push',
|
||||||
|
expected_actions=[])
|
||||||
|
|
||||||
|
def test_oauth_public(self):
|
||||||
|
self.do_logincheck('$oauthtoken', 'test', expect_success=True,
|
||||||
|
scope='repository:public/publicrepo:pull,push',
|
||||||
|
expected_actions=['pull'])
|
||||||
|
|
||||||
def test_nouser_pull_publicrepo(self):
|
def test_nouser_pull_publicrepo(self):
|
||||||
self.do_login('', '', expect_success=True, scope='repository:public/publicrepo:pull')
|
self.do_logincheck('', '', expect_success=True, scope='repository:public/publicrepo:pull',
|
||||||
|
expected_actions=['pull'])
|
||||||
|
|
||||||
def test_nouser_push_publicrepo(self):
|
def test_nouser_push_publicrepo(self):
|
||||||
self.do_login('', '', expected_code=401, scope='repository:public/publicrepo:push')
|
self.do_logincheck('', '', expect_success=True, scope='repository:public/publicrepo:push',
|
||||||
|
expected_actions=[])
|
||||||
|
|
||||||
def test_library_invaliduser(self):
|
def test_library_invaliduser(self):
|
||||||
self.do_login('invaliduser', 'password', expected_code=401, scope='repository:librepo:pull,push')
|
self.do_logincheck('invaliduser', 'password', expect_success=False,
|
||||||
|
scope='repository:librepo:pull,push')
|
||||||
|
|
||||||
def test_library_noaccess(self):
|
def test_library_noaccess(self):
|
||||||
self.do_login('freshuser', 'password', expected_code=403, scope='repository:librepo:pull,push')
|
self.do_logincheck('freshuser', 'password', expect_success=True,
|
||||||
|
scope='repository:librepo:pull,push',
|
||||||
|
expected_actions=[])
|
||||||
|
|
||||||
def test_library_access(self):
|
def test_library_access(self):
|
||||||
self.do_login('devtable', 'password', expect_success=200, scope='repository:librepo:pull,push')
|
self.do_logincheck('devtable', 'password', expect_success=True,
|
||||||
|
scope='repository:librepo:pull,push',
|
||||||
|
expected_actions=['push', 'pull'])
|
||||||
|
|
||||||
|
def test_nouser_pushpull_publicrepo(self):
|
||||||
|
# Note: Docker 1.8.3 will ask for both push and pull scopes at all times. For public pulls
|
||||||
|
# with no credentials, we were returning a 401. This test makes sure we get back just a pull
|
||||||
|
# token.
|
||||||
|
self.do_logincheck('', '', expect_success=True,
|
||||||
|
scope='repository:public/publicrepo:pull,push',
|
||||||
|
expected_actions=['pull'])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -253,21 +253,11 @@ class IndexV2TestSpec(object):
|
||||||
|
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
self.auth_no_access_code = 403
|
|
||||||
self.auth_read_code = 403
|
|
||||||
self.auth_admin_code = 403
|
|
||||||
|
|
||||||
self.anon_code = 401
|
self.anon_code = 401
|
||||||
self.no_access_code = 403
|
self.no_access_code = 403
|
||||||
self.read_code = 200
|
self.read_code = 200
|
||||||
self.admin_code = 200
|
self.admin_code = 200
|
||||||
|
|
||||||
def auth_status(self, auth_no_access_code=403, auth_read_code=200, auth_admin_code=200):
|
|
||||||
self.auth_no_access_code = auth_no_access_code
|
|
||||||
self.auth_read_code = auth_read_code
|
|
||||||
self.auth_admin_code = auth_admin_code
|
|
||||||
return self
|
|
||||||
|
|
||||||
def request_status(self, anon_code=401, no_access_code=403, read_code=200, admin_code=200):
|
def request_status(self, anon_code=401, no_access_code=403, read_code=200, admin_code=200):
|
||||||
self.anon_code = anon_code
|
self.anon_code = anon_code
|
||||||
self.no_access_code = no_access_code
|
self.no_access_code = no_access_code
|
||||||
|
@ -290,170 +280,131 @@ def build_v2_index_specs():
|
||||||
return [
|
return [
|
||||||
# v2.list_all_tags
|
# v2.list_all_tags
|
||||||
IndexV2TestSpec('v2.list_all_tags', 'GET', PUBLIC_REPO).
|
IndexV2TestSpec('v2.list_all_tags', 'GET', PUBLIC_REPO).
|
||||||
auth_status(200, 200, 200).
|
|
||||||
request_status(200, 200, 200, 200),
|
request_status(200, 200, 200, 200),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.list_all_tags', 'GET', PRIVATE_REPO).
|
IndexV2TestSpec('v2.list_all_tags', 'GET', PRIVATE_REPO).
|
||||||
auth_status(403, 200, 200).
|
|
||||||
request_status(401, 401, 200, 200),
|
request_status(401, 401, 200, 200),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.list_all_tags', 'GET', ORG_REPO).
|
IndexV2TestSpec('v2.list_all_tags', 'GET', ORG_REPO).
|
||||||
auth_status(403, 200, 200).
|
|
||||||
request_status(401, 401, 200, 200),
|
request_status(401, 401, 200, 200),
|
||||||
|
|
||||||
# v2.fetch_manifest_by_tagname
|
# v2.fetch_manifest_by_tagname
|
||||||
IndexV2TestSpec('v2.fetch_manifest_by_tagname', 'GET', PUBLIC_REPO, manifest_ref=FAKE_MANIFEST).
|
IndexV2TestSpec('v2.fetch_manifest_by_tagname', 'GET', PUBLIC_REPO, manifest_ref=FAKE_MANIFEST).
|
||||||
auth_status(200, 200, 200).
|
|
||||||
request_status(404, 404, 404, 404),
|
request_status(404, 404, 404, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.fetch_manifest_by_tagname', 'GET', PRIVATE_REPO, manifest_ref=FAKE_MANIFEST).
|
IndexV2TestSpec('v2.fetch_manifest_by_tagname', 'GET', PRIVATE_REPO, manifest_ref=FAKE_MANIFEST).
|
||||||
auth_status(403, 200, 200).
|
|
||||||
request_status(401, 401, 404, 404),
|
request_status(401, 401, 404, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.fetch_manifest_by_tagname', 'GET', ORG_REPO, manifest_ref=FAKE_MANIFEST).
|
IndexV2TestSpec('v2.fetch_manifest_by_tagname', 'GET', ORG_REPO, manifest_ref=FAKE_MANIFEST).
|
||||||
auth_status(403, 200, 200).
|
|
||||||
request_status(401, 401, 404, 404),
|
request_status(401, 401, 404, 404),
|
||||||
|
|
||||||
# v2.fetch_manifest_by_digest
|
# v2.fetch_manifest_by_digest
|
||||||
IndexV2TestSpec('v2.fetch_manifest_by_digest', 'GET', PUBLIC_REPO, manifest_ref=FAKE_DIGEST).
|
IndexV2TestSpec('v2.fetch_manifest_by_digest', 'GET', PUBLIC_REPO, manifest_ref=FAKE_DIGEST).
|
||||||
auth_status(200, 200, 200).
|
|
||||||
request_status(404, 404, 404, 404),
|
request_status(404, 404, 404, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.fetch_manifest_by_digest', 'GET', PRIVATE_REPO, manifest_ref=FAKE_DIGEST).
|
IndexV2TestSpec('v2.fetch_manifest_by_digest', 'GET', PRIVATE_REPO, manifest_ref=FAKE_DIGEST).
|
||||||
auth_status(403, 200, 200).
|
|
||||||
request_status(401, 401, 404, 404),
|
request_status(401, 401, 404, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.fetch_manifest_by_digest', 'GET', ORG_REPO, manifest_ref=FAKE_DIGEST).
|
IndexV2TestSpec('v2.fetch_manifest_by_digest', 'GET', ORG_REPO, manifest_ref=FAKE_DIGEST).
|
||||||
auth_status(403, 200, 200).
|
|
||||||
request_status(401, 401, 404, 404),
|
request_status(401, 401, 404, 404),
|
||||||
|
|
||||||
# v2.write_manifest_by_tagname
|
# v2.write_manifest_by_tagname
|
||||||
IndexV2TestSpec('v2.write_manifest_by_tagname', 'PUT', PUBLIC_REPO, manifest_ref=FAKE_MANIFEST).
|
IndexV2TestSpec('v2.write_manifest_by_tagname', 'PUT', PUBLIC_REPO, manifest_ref=FAKE_MANIFEST).
|
||||||
auth_status(403, 403, 403).
|
|
||||||
request_status(401, 401, 401, 401),
|
request_status(401, 401, 401, 401),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.write_manifest_by_tagname', 'PUT', PRIVATE_REPO, manifest_ref=FAKE_MANIFEST).
|
IndexV2TestSpec('v2.write_manifest_by_tagname', 'PUT', PRIVATE_REPO, manifest_ref=FAKE_MANIFEST).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 400),
|
request_status(401, 401, 401, 400),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.write_manifest_by_tagname', 'PUT', ORG_REPO, manifest_ref=FAKE_MANIFEST).
|
IndexV2TestSpec('v2.write_manifest_by_tagname', 'PUT', ORG_REPO, manifest_ref=FAKE_MANIFEST).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 400),
|
request_status(401, 401, 401, 400),
|
||||||
|
|
||||||
# v2.write_manifest_by_digest
|
# v2.write_manifest_by_digest
|
||||||
IndexV2TestSpec('v2.write_manifest_by_digest', 'PUT', PUBLIC_REPO, manifest_ref=FAKE_DIGEST).
|
IndexV2TestSpec('v2.write_manifest_by_digest', 'PUT', PUBLIC_REPO, manifest_ref=FAKE_DIGEST).
|
||||||
auth_status(403, 403, 403).
|
|
||||||
request_status(401, 401, 401, 401),
|
request_status(401, 401, 401, 401),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.write_manifest_by_digest', 'PUT', PRIVATE_REPO, manifest_ref=FAKE_DIGEST).
|
IndexV2TestSpec('v2.write_manifest_by_digest', 'PUT', PRIVATE_REPO, manifest_ref=FAKE_DIGEST).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 400),
|
request_status(401, 401, 401, 400),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.write_manifest_by_digest', 'PUT', ORG_REPO, manifest_ref=FAKE_DIGEST).
|
IndexV2TestSpec('v2.write_manifest_by_digest', 'PUT', ORG_REPO, manifest_ref=FAKE_DIGEST).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 400),
|
request_status(401, 401, 401, 400),
|
||||||
|
|
||||||
# v2.delete_manifest_by_digest
|
# v2.delete_manifest_by_digest
|
||||||
IndexV2TestSpec('v2.delete_manifest_by_digest', 'DELETE', PUBLIC_REPO, manifest_ref=FAKE_DIGEST).
|
IndexV2TestSpec('v2.delete_manifest_by_digest', 'DELETE', PUBLIC_REPO, manifest_ref=FAKE_DIGEST).
|
||||||
auth_status(403, 403, 403).
|
|
||||||
request_status(401, 401, 401, 401),
|
request_status(401, 401, 401, 401),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.delete_manifest_by_digest', 'DELETE', PRIVATE_REPO, manifest_ref=FAKE_DIGEST).
|
IndexV2TestSpec('v2.delete_manifest_by_digest', 'DELETE', PRIVATE_REPO, manifest_ref=FAKE_DIGEST).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 404),
|
request_status(401, 401, 401, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.delete_manifest_by_digest', 'DELETE', ORG_REPO, manifest_ref=FAKE_DIGEST).
|
IndexV2TestSpec('v2.delete_manifest_by_digest', 'DELETE', ORG_REPO, manifest_ref=FAKE_DIGEST).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 404),
|
request_status(401, 401, 401, 404),
|
||||||
|
|
||||||
# v2.check_blob_exists
|
# v2.check_blob_exists
|
||||||
IndexV2TestSpec('v2.check_blob_exists', 'HEAD', PUBLIC_REPO, digest=FAKE_DIGEST).
|
IndexV2TestSpec('v2.check_blob_exists', 'HEAD', PUBLIC_REPO, digest=FAKE_DIGEST).
|
||||||
auth_status(200, 200, 200).
|
|
||||||
request_status(404, 404, 404, 404),
|
request_status(404, 404, 404, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.check_blob_exists', 'HEAD', PRIVATE_REPO, digest=FAKE_DIGEST).
|
IndexV2TestSpec('v2.check_blob_exists', 'HEAD', PRIVATE_REPO, digest=FAKE_DIGEST).
|
||||||
auth_status(403, 200, 200).
|
|
||||||
request_status(401, 401, 404, 404),
|
request_status(401, 401, 404, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.check_blob_exists', 'HEAD', ORG_REPO, digest=FAKE_DIGEST).
|
IndexV2TestSpec('v2.check_blob_exists', 'HEAD', ORG_REPO, digest=FAKE_DIGEST).
|
||||||
auth_status(403, 200, 200).
|
|
||||||
request_status(401, 401, 404, 404),
|
request_status(401, 401, 404, 404),
|
||||||
|
|
||||||
# v2.download_blob
|
# v2.download_blob
|
||||||
IndexV2TestSpec('v2.download_blob', 'GET', PUBLIC_REPO, digest=FAKE_DIGEST).
|
IndexV2TestSpec('v2.download_blob', 'GET', PUBLIC_REPO, digest=FAKE_DIGEST).
|
||||||
auth_status(200, 200, 200).
|
|
||||||
request_status(404, 404, 404, 404),
|
request_status(404, 404, 404, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.download_blob', 'GET', PRIVATE_REPO, digest=FAKE_DIGEST).
|
IndexV2TestSpec('v2.download_blob', 'GET', PRIVATE_REPO, digest=FAKE_DIGEST).
|
||||||
auth_status(403, 200, 200).
|
|
||||||
request_status(401, 401, 404, 404),
|
request_status(401, 401, 404, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.download_blob', 'GET', ORG_REPO, digest=FAKE_DIGEST).
|
IndexV2TestSpec('v2.download_blob', 'GET', ORG_REPO, digest=FAKE_DIGEST).
|
||||||
auth_status(403, 200, 200).
|
|
||||||
request_status(401, 401, 404, 404),
|
request_status(401, 401, 404, 404),
|
||||||
|
|
||||||
# v2.start_blob_upload
|
# v2.start_blob_upload
|
||||||
IndexV2TestSpec('v2.start_blob_upload', 'POST', PUBLIC_REPO).
|
IndexV2TestSpec('v2.start_blob_upload', 'POST', PUBLIC_REPO).
|
||||||
auth_status(403, 403, 403).
|
|
||||||
request_status(401, 401, 401, 401),
|
request_status(401, 401, 401, 401),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.start_blob_upload', 'POST', PRIVATE_REPO).
|
IndexV2TestSpec('v2.start_blob_upload', 'POST', PRIVATE_REPO).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 202),
|
request_status(401, 401, 401, 202),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.start_blob_upload', 'POST', ORG_REPO).
|
IndexV2TestSpec('v2.start_blob_upload', 'POST', ORG_REPO).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 202),
|
request_status(401, 401, 401, 202),
|
||||||
|
|
||||||
# v2.fetch_existing_upload
|
# v2.fetch_existing_upload
|
||||||
IndexV2TestSpec('v2.fetch_existing_upload', 'GET', PUBLIC_REPO, 'push,pull', upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.fetch_existing_upload', 'GET', PUBLIC_REPO, 'push,pull', upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 403).
|
|
||||||
request_status(401, 401, 401, 401),
|
request_status(401, 401, 401, 401),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.fetch_existing_upload', 'GET', PRIVATE_REPO, 'push,pull', upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.fetch_existing_upload', 'GET', PRIVATE_REPO, 'push,pull', upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 404),
|
request_status(401, 401, 401, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.fetch_existing_upload', 'GET', ORG_REPO, 'push,pull', upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.fetch_existing_upload', 'GET', ORG_REPO, 'push,pull', upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 404),
|
request_status(401, 401, 401, 404),
|
||||||
|
|
||||||
# v2.upload_chunk
|
# v2.upload_chunk
|
||||||
IndexV2TestSpec('v2.upload_chunk', 'PATCH', PUBLIC_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.upload_chunk', 'PATCH', PUBLIC_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 403).
|
|
||||||
request_status(401, 401, 401, 401),
|
request_status(401, 401, 401, 401),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.upload_chunk', 'PATCH', PRIVATE_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.upload_chunk', 'PATCH', PRIVATE_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 404),
|
request_status(401, 401, 401, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.upload_chunk', 'PATCH', ORG_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.upload_chunk', 'PATCH', ORG_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 404),
|
request_status(401, 401, 401, 404),
|
||||||
|
|
||||||
# v2.monolithic_upload_or_last_chunk
|
# v2.monolithic_upload_or_last_chunk
|
||||||
IndexV2TestSpec('v2.monolithic_upload_or_last_chunk', 'PUT', PUBLIC_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.monolithic_upload_or_last_chunk', 'PUT', PUBLIC_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 403).
|
|
||||||
request_status(401, 401, 401, 401),
|
request_status(401, 401, 401, 401),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.monolithic_upload_or_last_chunk', 'PUT', PRIVATE_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.monolithic_upload_or_last_chunk', 'PUT', PRIVATE_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 400),
|
request_status(401, 401, 401, 400),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.monolithic_upload_or_last_chunk', 'PUT', ORG_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.monolithic_upload_or_last_chunk', 'PUT', ORG_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 400),
|
request_status(401, 401, 401, 400),
|
||||||
|
|
||||||
# v2.cancel_upload
|
# v2.cancel_upload
|
||||||
IndexV2TestSpec('v2.cancel_upload', 'DELETE', PUBLIC_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.cancel_upload', 'DELETE', PUBLIC_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 403).
|
|
||||||
request_status(401, 401, 401, 401),
|
request_status(401, 401, 401, 401),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.cancel_upload', 'DELETE', PRIVATE_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.cancel_upload', 'DELETE', PRIVATE_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 404),
|
request_status(401, 401, 401, 404),
|
||||||
|
|
||||||
IndexV2TestSpec('v2.cancel_upload', 'DELETE', ORG_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
IndexV2TestSpec('v2.cancel_upload', 'DELETE', ORG_REPO, upload_uuid=FAKE_UPLOAD_ID).
|
||||||
auth_status(403, 403, 200).
|
|
||||||
request_status(401, 401, 401, 404),
|
request_status(401, 401, 401, 404),
|
||||||
]
|
]
|
||||||
|
|
|
@ -31,7 +31,6 @@ class _SpecTestBuilder(type):
|
||||||
expected_index_status = getattr(test_spec, attrs['result_attr'])
|
expected_index_status = getattr(test_spec, attrs['result_attr'])
|
||||||
|
|
||||||
if attrs['auth_username']:
|
if attrs['auth_username']:
|
||||||
expected_auth_status = getattr(test_spec, 'auth_' + attrs['result_attr'])
|
|
||||||
|
|
||||||
# Get a signed JWT.
|
# Get a signed JWT.
|
||||||
username = attrs['auth_username']
|
username = attrs['auth_username']
|
||||||
|
@ -44,12 +43,11 @@ class _SpecTestBuilder(type):
|
||||||
headers=[('authorization', test_spec.gen_basic_auth(username, password))],
|
headers=[('authorization', test_spec.gen_basic_auth(username, password))],
|
||||||
query_string=query_string)
|
query_string=query_string)
|
||||||
|
|
||||||
msg = 'Auth failed for %s %s: got %s, expected: %s' % (
|
msg = 'Auth failed for %s %s: got %s, expected: 200' % (
|
||||||
test_spec.method_name, test_spec.index_name, arv.status_code, expected_auth_status)
|
test_spec.method_name, test_spec.index_name, arv.status_code)
|
||||||
self.assertEqual(arv.status_code, expected_auth_status, msg)
|
self.assertEqual(arv.status_code, 200, msg)
|
||||||
|
|
||||||
if arv.status_code == 200:
|
headers = [('authorization', 'Bearer ' + json.loads(arv.data)['token'])]
|
||||||
headers = [('authorization', 'Bearer ' + json.loads(arv.data)['token'])]
|
|
||||||
|
|
||||||
rv = c.open(url, headers=headers, method=test_spec.method_name)
|
rv = c.open(url, headers=headers, method=test_spec.method_name)
|
||||||
msg = '%s %s: got %s, expected: %s (auth: %s | headers %s)' % (test_spec.method_name,
|
msg = '%s %s: got %s, expected: %s (auth: %s | headers %s)' % (test_spec.method_name,
|
||||||
|
|
Reference in a new issue