Request middleware conditionally require content-type

Change to the YAMLTranslator middleware to only require content-type
when the request should require it.  GET and DELETE should not, and
PUT and POST should only require it when there is a message body.

Change-Id: I8614ff077c4fc5b19b77e12a468e053c76733487
This commit is contained in:
Bryan Strassner 2017-11-06 09:52:20 -06:00 committed by Felipe Monteiro
parent ea727e7bbf
commit 7ff9a3b1ce
4 changed files with 138 additions and 22 deletions

View File

@ -118,20 +118,33 @@ class YAMLTranslator(HookableMiddlewareMixin, object):
def process_request(self, req, resp):
"""Performs content type enforcement on behalf of REST verbs."""
valid_content_types = ['application/x-yaml']
content_type = (req.content_type.split(';', 1)[0].strip()
# GET and DELETE should never carry a message body, and have
# no content type. Check for content-length or
# transfer-encoding to determine if a content-type header
# is required.
requires_content_type = (
req.method not in ['GET', 'DELETE'] and (
req.content_length is not None or
req.get_header('transfer-encoding') is not None
)
)
if requires_content_type:
content_type = (req.content_type.split(';', 1)[0].strip()
if req.content_type else '')
if not content_type:
raise falcon.HTTPMissingHeader('Content-Type')
elif content_type not in valid_content_types:
message = (
"Unexpected content type: {type}. Expected content types "
"are: {expected}."
).format(
type=six.b(req.content_type).decode('utf-8'),
expected=valid_content_types
)
raise falcon.HTTPUnsupportedMediaType(description=message)
if not content_type:
raise falcon.HTTPMissingHeader('Content-Type')
elif content_type not in valid_content_types:
message = (
"Unexpected content type: {type}. Expected content types "
"are: {expected}."
).format(
type=six.b(req.content_type).decode('utf-8'),
expected=valid_content_types
)
raise falcon.HTTPUnsupportedMediaType(description=message)
def process_response(self, req, resp, resource):
"""Converts responses to ``application/x-yaml`` content type."""

View File

@ -12,6 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import yaml
from falcon import testing as falcon_testing
from deckhand import service
@ -31,3 +34,13 @@ class BaseControllerTest(test_base.DeckhandWithDBTestCase,
# NOTE: allow_anonymous_access allows these unit tests to get around
# Keystone authentication.
self.useFixture(fixtures.ConfPatcher(allow_anonymous_access=True))
def _read_data(self, file_name):
# Reads data from a file in the resources directory
dir_path = os.path.dirname(os.path.realpath(__file__))
test_yaml_path = os.path.abspath(os.path.join(
dir_path, os.pardir, 'resources', file_name + '.yaml'))
with open(test_yaml_path, 'r') as yaml_file:
yaml_data = yaml_file.read()
self.data = yaml.safe_load(yaml_data)

View File

@ -22,21 +22,85 @@ from deckhand.tests.unit.control import base as test_base
class TestYAMLTranslator(test_base.BaseControllerTest):
def test_request_with_correct_content_type(self):
rules = {'deckhand:create_cleartext_documents': '@'}
self.policy.set_rules(rules)
self._read_data('sample_document_simple')
resp = self.app.simulate_put(
'/api/v1.0/buckets/b1/documents',
headers={'Content-Type': 'application/x-yaml'},
body=yaml.safe_dump(self.data),
)
self.assertEqual(200, resp.status_code)
def test_request_empty_put_and_content_type(self):
rules = {'deckhand:create_cleartext_documents': '@'}
self.policy.set_rules(rules)
resp = self.app.simulate_put(
'/api/v1.0/buckets/b1/documents',
headers={'Content-Type': 'application/x-yaml'},
)
self.assertEqual(200, resp.status_code)
def test_request_empty_put_and_no_content_type(self):
rules = {'deckhand:create_cleartext_documents': '@'}
self.policy.set_rules(rules)
resp = self.app.simulate_put(
'/api/v1.0/buckets/b1/documents',
)
self.assertEqual(200, resp.status_code)
def test_request_zero_length_put_and_no_content_type(self):
rules = {'deckhand:create_cleartext_documents': '@'}
self.policy.set_rules(rules)
resp = self.app.simulate_put(
'/api/v1.0/buckets/b1/documents',
body='',
)
self.assertEqual(200, resp.status_code)
def test_request_zero_length_put_and_content_type(self):
rules = {'deckhand:create_cleartext_documents': '@'}
self.policy.set_rules(rules)
resp = self.app.simulate_put(
'/api/v1.0/buckets/b1/documents',
headers={'Content-Type': 'application/x-yaml'},
body='',
)
self.assertEqual(200, resp.status_code)
def test_request_with_no_content_type_on_get(self):
resp = self.app.simulate_get(
'/versions', headers={'Content-Type': 'application/x-yaml'})
'/versions',
headers={})
self.assertEqual(200, resp.status_code)
def test_request_with_superfluous_content_type_on_get(self):
resp = self.app.simulate_get(
'/versions',
headers={'Content-Type': 'application/x-yaml'},
)
self.assertEqual(200, resp.status_code)
def test_request_with_correct_content_type_plus_encoding(self):
resp = self.app.simulate_get(
'/versions',
headers={'Content-Type': 'application/x-yaml;encoding=utf-8'})
rules = {'deckhand:create_cleartext_documents': '@'}
self.policy.set_rules(rules)
self._read_data('sample_document_simple')
resp = self.app.simulate_put(
'/api/v1.0/buckets/b1/documents',
headers={'Content-Type': 'application/x-yaml;encoding=utf-8'},
body=yaml.safe_dump(self.data),
)
self.assertEqual(200, resp.status_code)
class TestYAMLTranslatorNegative(test_base.BaseControllerTest):
def test_request_without_content_type_raises_exception(self):
resp = self.app.simulate_get('/versions')
self._read_data('sample_document_simple')
resp = self.app.simulate_put(
'/api/v1.0/buckets/b1/documents',
body=yaml.safe_dump(self.data),
)
self.assertEqual(400, resp.status_code)
expected = {
@ -60,8 +124,13 @@ class TestYAMLTranslatorNegative(test_base.BaseControllerTest):
self.assertEqual(expected, yaml.safe_load(resp.content))
def test_request_with_invalid_content_type_raises_exception(self):
resp = self.app.simulate_get(
'/versions', headers={'Content-Type': 'application/json'})
self._read_data('sample_document_simple')
resp = self.app.simulate_put(
'/api/v1.0/buckets/b1/documents',
headers={'Content-Type': 'application/json'},
body=yaml.safe_dump(self.data),
)
self.assertEqual(415, resp.status_code)
expected = {
@ -91,12 +160,16 @@ class TestYAMLTranslatorNegative(test_base.BaseControllerTest):
"""Only application/x-yaml should be supported, not application/yaml,
because it hasn't been registered as an official MIME type yet.
"""
resp = self.app.simulate_get(
'/versions', headers={'Content-Type': 'application/yaml'})
self._read_data('sample_document_simple')
resp = self.app.simulate_put(
'/api/v1.0/buckets/b1/documents',
headers={'Content-Type': 'application/yaml'},
body=yaml.safe_dump(self.data),
)
self.assertEqual(415, resp.status_code)
expected = {
'apiVersion': 'N/A',
'apiVersion': mock.ANY,
'code': '415 Unsupported Media Type',
'details': {
'errorCount': 1,

View File

@ -0,0 +1,17 @@
---
schema: armada/Manifest/v1.0
metadata:
schema: metadata/Document/v1.0
name: cluster-bootstrap
layeringDefinition:
abstract: false
layer: site
data:
release_prefix: ucp
chart_groups:
- kubernetes-proxy
- container-networking
- dns
- kubernetes
- kubernetes-rbac
...