Merge pull request #2950 from coreos-inc/joseph.schorr/QS-99/fix-oci
Prevent 500s when receiving bad manifests or OCI manifests
This commit is contained in:
commit
b4df9fb805
4 changed files with 120 additions and 7 deletions
|
@ -18,7 +18,7 @@ from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnknown,
|
||||||
from endpoints.v2.labelhandlers import handle_label
|
from endpoints.v2.labelhandlers import handle_label
|
||||||
from image.docker import ManifestException
|
from image.docker import ManifestException
|
||||||
from image.docker.schema1 import DockerSchema1Manifest, DockerSchema1ManifestBuilder
|
from image.docker.schema1 import DockerSchema1Manifest, DockerSchema1ManifestBuilder
|
||||||
from image.docker.schema2 import DOCKER_SCHEMA2_CONTENT_TYPES
|
from image.docker.schema2 import DOCKER_SCHEMA2_CONTENT_TYPES, OCI_CONTENT_TYPES
|
||||||
from notifications import spawn_notification
|
from notifications import spawn_notification
|
||||||
from util.audit import track_and_log
|
from util.audit import track_and_log
|
||||||
from util.names import VALID_TAG_PATTERN
|
from util.names import VALID_TAG_PATTERN
|
||||||
|
@ -92,7 +92,7 @@ def fetch_manifest_by_digest(namespace_name, repo_name, manifest_ref):
|
||||||
def _reject_manifest2_schema2(func):
|
def _reject_manifest2_schema2(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
if request.content_type in DOCKER_SCHEMA2_CONTENT_TYPES:
|
if request.content_type in (DOCKER_SCHEMA2_CONTENT_TYPES | OCI_CONTENT_TYPES):
|
||||||
raise ManifestInvalid(detail={'message': 'manifest schema version not supported'},
|
raise ManifestInvalid(detail={'message': 'manifest schema version not supported'},
|
||||||
http_status_code=415)
|
http_status_code=415)
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
|
@ -11,6 +11,8 @@ import logging
|
||||||
from collections import namedtuple, OrderedDict
|
from collections import namedtuple, OrderedDict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from jsonschema import validate as validate_schema, ValidationError
|
||||||
|
|
||||||
from jwkest.jws import SIGNER_ALGS, keyrep
|
from jwkest.jws import SIGNER_ALGS, keyrep
|
||||||
from jwt.utils import base64url_encode, base64url_decode
|
from jwt.utils import base64url_encode, base64url_decode
|
||||||
|
|
||||||
|
@ -84,6 +86,73 @@ class Schema1V1Metadata(namedtuple('Schema1V1Metadata', ['image_id', 'parent_ima
|
||||||
|
|
||||||
|
|
||||||
class DockerSchema1Manifest(object):
|
class DockerSchema1Manifest(object):
|
||||||
|
METASCHEMA = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
DOCKER_SCHEMA1_SIGNATURES_KEY: {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
DOCKER_SCHEMA1_PROTECTED_KEY: {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
DOCKER_SCHEMA1_HEADER_KEY: {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'alg': {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'jwk': {
|
||||||
|
'type': 'object',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['alg', 'jwk'],
|
||||||
|
},
|
||||||
|
DOCKER_SCHEMA1_SIGNATURE_KEY: {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': [DOCKER_SCHEMA1_PROTECTED_KEY, DOCKER_SCHEMA1_HEADER_KEY,
|
||||||
|
DOCKER_SCHEMA1_SIGNATURE_KEY],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DOCKER_SCHEMA1_REPO_TAG_KEY: {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
DOCKER_SCHEMA1_REPO_NAME_KEY: {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
DOCKER_SCHEMA1_HISTORY_KEY: {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
DOCKER_SCHEMA1_V1_COMPAT_KEY: {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': [DOCKER_SCHEMA1_V1_COMPAT_KEY],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DOCKER_SCHEMA1_FS_LAYERS_KEY: {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
DOCKER_SCHEMA1_BLOB_SUM_KEY: {
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': [DOCKER_SCHEMA1_BLOB_SUM_KEY],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': [DOCKER_SCHEMA1_SIGNATURES_KEY, DOCKER_SCHEMA1_REPO_TAG_KEY,
|
||||||
|
DOCKER_SCHEMA1_REPO_NAME_KEY, DOCKER_SCHEMA1_FS_LAYERS_KEY,
|
||||||
|
DOCKER_SCHEMA1_HISTORY_KEY],
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, manifest_bytes, validate=True):
|
def __init__(self, manifest_bytes, validate=True):
|
||||||
self._layers = None
|
self._layers = None
|
||||||
self._bytes = manifest_bytes
|
self._bytes = manifest_bytes
|
||||||
|
@ -93,6 +162,11 @@ class DockerSchema1Manifest(object):
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
raise MalformedSchema1Manifest('malformed manifest data: %s' % ve)
|
raise MalformedSchema1Manifest('malformed manifest data: %s' % ve)
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_schema(self._parsed, DockerSchema1Manifest.METASCHEMA)
|
||||||
|
except ValidationError as ve:
|
||||||
|
raise MalformedSchema1Manifest('malformed manifest data: %s' % ve)
|
||||||
|
|
||||||
self._signatures = self._parsed[DOCKER_SCHEMA1_SIGNATURES_KEY]
|
self._signatures = self._parsed[DOCKER_SCHEMA1_SIGNATURES_KEY]
|
||||||
self._tag = self._parsed[DOCKER_SCHEMA1_REPO_TAG_KEY]
|
self._tag = self._parsed[DOCKER_SCHEMA1_REPO_TAG_KEY]
|
||||||
|
|
||||||
|
|
|
@ -7,5 +7,10 @@ https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-2.md
|
||||||
# Content Types
|
# Content Types
|
||||||
DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
|
DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
|
||||||
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE = 'application/vnd.docker.distribution.manifest.list.v2+json'
|
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE = 'application/vnd.docker.distribution.manifest.list.v2+json'
|
||||||
|
|
||||||
|
OCI_MANIFEST_CONTENT_TYPE = 'application/vnd.oci.image.manifest.v1+json'
|
||||||
|
OCI_MANIFESTLIST_CONTENT_TYPE = 'application/vnd.oci.image.index.v1+json'
|
||||||
|
|
||||||
DOCKER_SCHEMA2_CONTENT_TYPES = {DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE,
|
DOCKER_SCHEMA2_CONTENT_TYPES = {DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE,
|
||||||
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE}
|
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE}
|
||||||
|
OCI_CONTENT_TYPES = {OCI_MANIFEST_CONTENT_TYPE, OCI_MANIFESTLIST_CONTENT_TYPE}
|
||||||
|
|
|
@ -1366,11 +1366,11 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix
|
||||||
|
|
||||||
def test_users(self):
|
def test_users(self):
|
||||||
# Not logged in, should 404.
|
# Not logged in, should 404.
|
||||||
self.conduct('GET', '/v1/users', expected_code=404)
|
self.conduct('GET', '/v1/users/', expected_code=404)
|
||||||
|
|
||||||
# Try some logins.
|
# Try some logins.
|
||||||
self.conduct('POST', '/v1/users', json_data={'username': 'freshuser'}, expected_code=400)
|
self.conduct('POST', '/v1/users/', json_data={'username': 'freshuser'}, expected_code=400)
|
||||||
resp = self.conduct('POST', '/v1/users',
|
resp = self.conduct('POST', '/v1/users/',
|
||||||
json_data={'username': 'devtable', 'password': 'password'},
|
json_data={'username': 'devtable', 'password': 'password'},
|
||||||
expected_code=400)
|
expected_code=400)
|
||||||
|
|
||||||
|
@ -1623,6 +1623,40 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
||||||
headers={'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json'},
|
headers={'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json'},
|
||||||
auth='jwt')
|
auth='jwt')
|
||||||
|
|
||||||
|
def test_invalid_manifest(self):
|
||||||
|
namespace = 'devtable'
|
||||||
|
repository = 'somerepo'
|
||||||
|
tag_name = 'sometag'
|
||||||
|
|
||||||
|
repo_name = _get_repo_name(namespace, repository)
|
||||||
|
|
||||||
|
self.v2_ping()
|
||||||
|
self.do_auth('devtable', 'password', namespace, repository, scopes=['push', 'pull'])
|
||||||
|
|
||||||
|
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
||||||
|
data='{}', expected_code=400,
|
||||||
|
auth='jwt')
|
||||||
|
|
||||||
|
def test_oci_manifest_type(self):
|
||||||
|
namespace = 'devtable'
|
||||||
|
repository = 'somerepo'
|
||||||
|
tag_name = 'sometag'
|
||||||
|
|
||||||
|
repo_name = _get_repo_name(namespace, repository)
|
||||||
|
|
||||||
|
self.v2_ping()
|
||||||
|
self.do_auth('devtable', 'password', namespace, repository, scopes=['push', 'pull'])
|
||||||
|
|
||||||
|
# Build a fake manifest.
|
||||||
|
builder = DockerSchema1ManifestBuilder(namespace, repository, tag_name)
|
||||||
|
builder.add_layer('sha256:' + hashlib.sha256('invalid').hexdigest(), json.dumps({'id': 'foo'}))
|
||||||
|
manifest = builder.build(_JWK)
|
||||||
|
|
||||||
|
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
||||||
|
data=manifest.bytes, expected_code=415,
|
||||||
|
headers={'Content-Type': 'application/vnd.oci.image.manifest.v1+json'},
|
||||||
|
auth='jwt')
|
||||||
|
|
||||||
def test_invalid_blob(self):
|
def test_invalid_blob(self):
|
||||||
namespace = 'devtable'
|
namespace = 'devtable'
|
||||||
repository = 'somerepo'
|
repository = 'somerepo'
|
||||||
|
|
Reference in a new issue