Made the X-Project_ID header mandatory in Poppy
Any middleware used (Keystone, Repose, etc) will be required to validate that the project_id is correct for the auth token used. This change also moves the instantiation of the hook that checks that the X-Project-ID header is mandatory to each controller instead of during the make_app. This allows the X-Project-ID context hook to only be checked on certain controllers (and ignored on ones like /ping where projectid is not needed. Change-Id: Ie2cfb9d76a0e53ba990332d1050cce634626b992 Closes-Bug: 1392818
This commit is contained in:
parent
46a408b8fd
commit
8091123422
|
@ -16,9 +16,11 @@
|
|||
import json
|
||||
|
||||
import pecan
|
||||
from pecan import hooks
|
||||
|
||||
from poppy.common import uri
|
||||
from poppy.transport.pecan.controllers import base
|
||||
from poppy.transport.pecan import hooks as poppy_hooks
|
||||
from poppy.transport.pecan.models.request import flavor as flavor_request
|
||||
from poppy.transport.pecan.models.response import flavor as flavor_response
|
||||
from poppy.transport.validators import helpers
|
||||
|
@ -28,7 +30,10 @@ from poppy.transport.validators.stoplight import helpers as stoplight_helpers
|
|||
from poppy.transport.validators.stoplight import rule
|
||||
|
||||
|
||||
class FlavorsController(base.Controller):
|
||||
class FlavorsController(base.Controller, hooks.HookController):
|
||||
|
||||
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
|
||||
|
||||
"""Flavors Controller."""
|
||||
|
||||
@pecan.expose('json')
|
||||
|
|
|
@ -14,12 +14,17 @@
|
|||
# limitations under the License.
|
||||
|
||||
import pecan
|
||||
from pecan import hooks
|
||||
|
||||
from poppy.transport.pecan.controllers import base
|
||||
from poppy.transport.pecan import hooks as poppy_hooks
|
||||
from poppy.transport.pecan.models.response import health as health_response
|
||||
|
||||
|
||||
class StorageHealthController(base.Controller):
|
||||
class StorageHealthController(base.Controller, hooks.HookController):
|
||||
|
||||
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
|
||||
|
||||
"""Storage Health Controller."""
|
||||
|
||||
@pecan.expose('json')
|
||||
|
@ -41,7 +46,9 @@ class StorageHealthController(base.Controller):
|
|||
pecan.response.status = 404
|
||||
|
||||
|
||||
class ProviderHealthController(base.Controller):
|
||||
class ProviderHealthController(base.Controller, hooks.HookController):
|
||||
|
||||
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
|
||||
|
||||
@pecan.expose('json')
|
||||
def get(self, provider_name):
|
||||
|
@ -56,7 +63,9 @@ class ProviderHealthController(base.Controller):
|
|||
pecan.response.status = 404
|
||||
|
||||
|
||||
class HealthController(base.Controller):
|
||||
class HealthController(base.Controller, hooks.HookController):
|
||||
|
||||
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
|
||||
|
||||
@pecan.expose('json')
|
||||
def get(self):
|
||||
|
|
|
@ -14,11 +14,15 @@
|
|||
# limitations under the License.
|
||||
|
||||
import pecan
|
||||
from pecan import hooks
|
||||
|
||||
from poppy.transport.pecan.controllers import base
|
||||
from poppy.transport.pecan import hooks as poppy_hooks
|
||||
|
||||
|
||||
class HomeController(base.Controller):
|
||||
class HomeController(base.Controller, hooks.HookController):
|
||||
|
||||
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
|
||||
|
||||
@pecan.expose('json')
|
||||
def get(self):
|
||||
|
|
|
@ -14,11 +14,17 @@
|
|||
# limitations under the License.
|
||||
|
||||
import pecan
|
||||
from pecan import hooks
|
||||
|
||||
from poppy.transport.pecan.controllers import base
|
||||
from poppy.transport.pecan import hooks as poppy_hooks
|
||||
|
||||
|
||||
class PingController(base.Controller):
|
||||
class PingController(base.Controller, hooks.HookController):
|
||||
|
||||
# no poppy_hooks.Context() required here as
|
||||
# project_id is not required to be submitted
|
||||
__hooks__ = [poppy_hooks.Error()]
|
||||
|
||||
@pecan.expose()
|
||||
def get(self):
|
||||
|
|
|
@ -17,9 +17,11 @@ import json
|
|||
|
||||
from oslo.config import cfg
|
||||
import pecan
|
||||
from pecan import hooks
|
||||
|
||||
from poppy.common import uri
|
||||
from poppy.transport.pecan.controllers import base
|
||||
from poppy.transport.pecan import hooks as poppy_hooks
|
||||
from poppy.transport.pecan.models.request import service as req_service_model
|
||||
from poppy.transport.pecan.models.response import link
|
||||
from poppy.transport.pecan.models.response import service as resp_service_model
|
||||
|
@ -37,7 +39,9 @@ LIMITS_OPTIONS = [
|
|||
LIMITS_GROUP = 'drivers:transport:limits'
|
||||
|
||||
|
||||
class ServiceAssetsController(base.Controller):
|
||||
class ServiceAssetsController(base.Controller, hooks.HookController):
|
||||
|
||||
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
|
||||
|
||||
@pecan.expose()
|
||||
def delete(self, service_name):
|
||||
|
@ -60,7 +64,9 @@ class ServiceAssetsController(base.Controller):
|
|||
pecan.response.status = 202
|
||||
|
||||
|
||||
class ServicesController(base.Controller):
|
||||
class ServicesController(base.Controller, hooks.HookController):
|
||||
|
||||
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
|
||||
|
||||
def __init__(self, driver):
|
||||
super(ServicesController, self).__init__(driver)
|
||||
|
|
|
@ -22,7 +22,6 @@ from poppy.openstack.common import log
|
|||
from poppy import transport
|
||||
from poppy.transport.pecan import controllers
|
||||
from poppy.transport.pecan.controllers import v1
|
||||
from poppy.transport.pecan import hooks
|
||||
|
||||
|
||||
_PECAN_OPTIONS = [
|
||||
|
@ -63,8 +62,7 @@ class PecanTransportDriver(transport.Driver):
|
|||
home_controller.add_controller('services', v1.Services(self))
|
||||
home_controller.add_controller('flavors', v1.Flavors(self))
|
||||
|
||||
pecan_hooks = [hooks.Context(), hooks.Error()]
|
||||
self._app = pecan.make_app(root_controller, hooks=pecan_hooks)
|
||||
self._app = pecan.make_app(root_controller)
|
||||
|
||||
def listen(self):
|
||||
LOG.info(
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import pecan
|
||||
from pecan import hooks
|
||||
|
||||
from poppy.openstack.common import context
|
||||
|
@ -28,7 +29,7 @@ class PoppyRequestContext(context.RequestContext):
|
|||
|
||||
class ContextHook(hooks.PecanHook):
|
||||
|
||||
def on_route(self, state):
|
||||
def before(self, state):
|
||||
context_kwargs = {}
|
||||
|
||||
if 'X-Project-ID' in state.request.headers:
|
||||
|
@ -37,24 +38,19 @@ class ContextHook(hooks.PecanHook):
|
|||
state.request.host_url +
|
||||
'/'.join(state.request.path.split('/')[0:2]))
|
||||
|
||||
if 'tenant' not in context_kwargs:
|
||||
# Didn't find the X-Project-Id header, pull from URL instead
|
||||
# Expects form /v1/{project_id}/path
|
||||
context_kwargs['tenant'] = state.request.path.split('/')[2]
|
||||
context_kwargs['base_url'] = (
|
||||
state.request.host_url +
|
||||
'/'.join(state.request.path.split('/')[0:3]))
|
||||
|
||||
if 'X-Auth-Token' in state.request.headers:
|
||||
context_kwargs['auth_token'] = (
|
||||
state.request.headers['X-Auth-Token']
|
||||
)
|
||||
|
||||
# if we still dont have a tenant, then return a 400
|
||||
if 'tenant' not in context_kwargs:
|
||||
pecan.abort(400, detail="The Project ID must be provided.")
|
||||
|
||||
request_context = PoppyRequestContext(**context_kwargs)
|
||||
state.request.context = request_context
|
||||
local.store.context = request_context
|
||||
|
||||
def before(self, state):
|
||||
'''Attach tenant_id as a member variable project_id to controller.'''
|
||||
state.controller.__self__.project_id = getattr(local.store.context,
|
||||
"tenant", None)
|
||||
|
|
|
@ -1 +1 @@
|
|||
pecan==0.6.1
|
||||
pecan==0.8.1
|
|
@ -28,8 +28,14 @@ from tests.functional.transport.pecan import base
|
|||
@ddt.ddt
|
||||
class FlavorControllerTest(base.FunctionalTest):
|
||||
|
||||
def setUp(self):
|
||||
super(FlavorControllerTest, self).setUp()
|
||||
|
||||
self.project_id = str(uuid.uuid1())
|
||||
|
||||
def test_get_all(self):
|
||||
response = self.app.get('/v1.0/flavors')
|
||||
response = self.app.get('/v1.0/flavors',
|
||||
headers={'X-Project-ID': self.project_id})
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
@ddt.file_data('data_create_flavor.json')
|
||||
|
@ -43,12 +49,13 @@ class FlavorControllerTest(base.FunctionalTest):
|
|||
mock_manager.get.return_value = mock_response
|
||||
|
||||
url = u'/v1.0/flavors/{0}'.format(uri.encode(value['id']))
|
||||
response = self.app.get(url)
|
||||
response = self.app.get(url, headers={'X-Project-ID': self.project_id})
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
def test_get_not_found(self):
|
||||
response = self.app.get('/v1.0/flavors/{0}'.format("non_exist"),
|
||||
headers={'X-Project-ID': self.project_id},
|
||||
status=404,
|
||||
expect_errors=True)
|
||||
|
||||
|
@ -59,7 +66,9 @@ class FlavorControllerTest(base.FunctionalTest):
|
|||
|
||||
response = self.app.post('/v1.0/flavors',
|
||||
params=json.dumps(value),
|
||||
headers={"Content-Type": "application/json"},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
'X-Project-ID': self.project_id},
|
||||
status=400,
|
||||
expect_errors=True)
|
||||
|
||||
|
@ -73,7 +82,9 @@ class FlavorControllerTest(base.FunctionalTest):
|
|||
# create with good data
|
||||
response = self.app.post('/v1.0/flavors',
|
||||
params=json.dumps(value),
|
||||
headers={"Content-Type": "application/json"},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
'X-Project-ID': self.project_id},
|
||||
expect_errors=True)
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
|
@ -85,10 +96,15 @@ class FlavorControllerTest(base.FunctionalTest):
|
|||
# create with good data
|
||||
response = self.app.post('/v1.0/flavors',
|
||||
params=json.dumps(value),
|
||||
headers={"Content-Type": "application/json"})
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
'X-Project-ID': self.project_id})
|
||||
self.assertEqual(201, response.status_code)
|
||||
|
||||
def test_delete(self):
|
||||
response = self.app.delete('/v1.0/flavors/{0}'.format(uuid.uuid1()))
|
||||
response = self.app.delete(
|
||||
'/v1.0/flavors/{0}'.format(uuid.uuid1()),
|
||||
headers={'X-Project-ID': self.project_id}
|
||||
)
|
||||
|
||||
self.assertEqual(204, response.status_code)
|
||||
|
|
|
@ -13,13 +13,20 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
|
||||
from poppy.common import util
|
||||
from tests.functional.transport.pecan import base
|
||||
|
||||
|
||||
class TestHealth(base.FunctionalTest):
|
||||
class HealthControllerTest(base.FunctionalTest):
|
||||
|
||||
def setUp(self):
|
||||
super(HealthControllerTest, self).setUp()
|
||||
|
||||
self.project_id = str(uuid.uuid1())
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_health(self, mock_requests):
|
||||
|
@ -27,7 +34,8 @@ class TestHealth(base.FunctionalTest):
|
|||
{'content': '', 'status_code': 200})
|
||||
mock_requests.return_value = response_object
|
||||
|
||||
response = self.app.get('/v1.0/health')
|
||||
response = self.app.get('/v1.0/health',
|
||||
headers={'X-Project-ID': self.project_id})
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
|
@ -36,16 +44,19 @@ class TestHealth(base.FunctionalTest):
|
|||
{'content': '', 'status_code': 200})
|
||||
mock_requests.return_value = response_object
|
||||
|
||||
response = self.app.get('/v1.0/health')
|
||||
response = self.app.get('/v1.0/health',
|
||||
headers={'X-Project-ID': self.project_id})
|
||||
for name in response.json['storage']:
|
||||
endpoint = '/v1.0/health/storage/{0}'.format(
|
||||
name)
|
||||
response = self.app.get(endpoint)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertIn('true', str(response.body))
|
||||
endpoint = '/v1.0/health/storage/{0}'.format(
|
||||
name)
|
||||
response = self.app.get(endpoint,
|
||||
headers={'X-Project-ID': self.project_id})
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertIn('true', str(response.body))
|
||||
|
||||
def test_get_unknown_storage(self):
|
||||
response = self.app.get('/v1.0/health/storage/unknown',
|
||||
headers={'X-Project-ID': self.project_id},
|
||||
expect_errors=True)
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
|
@ -55,15 +66,18 @@ class TestHealth(base.FunctionalTest):
|
|||
{'content': '', 'status_code': 200})
|
||||
mock_requests.return_value = response_object
|
||||
|
||||
response = self.app.get('/v1.0/health')
|
||||
response = self.app.get('/v1.0/health',
|
||||
headers={'X-Project-ID': self.project_id})
|
||||
for name in response.json['providers']:
|
||||
endpoint = '/v1.0/health/provider/{0}'.format(
|
||||
name)
|
||||
response = self.app.get(endpoint)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertIn('true', str(response.body))
|
||||
endpoint = '/v1.0/health/provider/{0}'.format(
|
||||
name)
|
||||
response = self.app.get(endpoint,
|
||||
headers={'X-Project-ID': self.project_id})
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertIn('true', str(response.body))
|
||||
|
||||
def test_get_unknown_provider(self):
|
||||
response = self.app.get('/v1.0/health/provider/unknown',
|
||||
headers={'X-Project-ID': self.project_id},
|
||||
expect_errors=True)
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
|
|
@ -13,15 +13,29 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import uuid
|
||||
|
||||
from poppy.manager.default import home
|
||||
from tests.functional.transport.pecan import base
|
||||
|
||||
|
||||
class HomeControllerTest(base.FunctionalTest):
|
||||
|
||||
def setUp(self):
|
||||
super(HomeControllerTest, self).setUp()
|
||||
|
||||
self.project_id = str(uuid.uuid1())
|
||||
|
||||
def test_get_all(self):
|
||||
response = self.app.get('/v1.0/00001')
|
||||
response = self.app.get('/v1.0/',
|
||||
headers={'X-Project-ID': self.project_id})
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
# Temporary until actual implementation
|
||||
self.assertEqual(home.JSON_HOME, response.json)
|
||||
|
||||
def test_get_without_project_id(self):
|
||||
response = self.app.get('/v1.0/',
|
||||
expect_errors=True)
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
|
|
|
@ -13,12 +13,21 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import uuid
|
||||
|
||||
from tests.functional.transport.pecan import base
|
||||
|
||||
|
||||
class TestPing(base.FunctionalTest):
|
||||
|
||||
def test_ping(self):
|
||||
response = self.app.get('/v1.0/ping')
|
||||
response = self.app.get('/v1.0/ping',
|
||||
headers={
|
||||
'X-Project-ID': str(uuid.uuid1())
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
def test_ping_no_project_id(self):
|
||||
response = self.app.get('/v1.0/ping')
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
|
|
@ -58,7 +58,10 @@ class ServiceControllerTest(base.FunctionalTest):
|
|||
}
|
||||
response = self.app.post('/v1.0/flavors',
|
||||
params=json.dumps(flavor_json),
|
||||
headers={"Content-Type": "application/json"})
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Project-ID": self.project_id})
|
||||
|
||||
self.assertEqual(201, response.status_code)
|
||||
|
||||
# create an initial service to be used by the tests
|
||||
|
@ -258,9 +261,10 @@ class ServiceControllerTest(base.FunctionalTest):
|
|||
expect_errors=True)
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
response = self.app.patch("/v1.0/" + self.project_id,
|
||||
response = self.app.patch("/v1.0/",
|
||||
headers={
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-Project-ID': self.project_id
|
||||
},
|
||||
expect_errors=True)
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
@ -285,7 +289,8 @@ class ServiceControllerTest(base.FunctionalTest):
|
|||
response = self.app.delete('/v1.0/%s/services/non_exist_service_name' %
|
||||
self.project_id,
|
||||
headers={
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-Project-ID': self.project_id
|
||||
},
|
||||
expect_errors=True)
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
|
|
@ -27,7 +27,7 @@ class ContextHookTest(base.FunctionalTest):
|
|||
self.headers = {'X-Auth-Token': str(uuid.uuid4())}
|
||||
|
||||
def test_project_id_in_header(self):
|
||||
self.headers['X-Project-Id'] = '000001'
|
||||
self.headers['X-Project-Id'] = str(uuid.uuid1())
|
||||
response = self.app.get('/v1.0', headers=self.headers)
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
@ -35,9 +35,9 @@ class ContextHookTest(base.FunctionalTest):
|
|||
# Temporary until actual implementation
|
||||
self.assertEqual(home.JSON_HOME, response.json)
|
||||
|
||||
def test_project_id_in_url(self):
|
||||
response = self.app.get('/v1.0/000001', headers=self.headers)
|
||||
def test_project_id_missing(self):
|
||||
response = self.app.get('/v1.0/',
|
||||
headers=self.headers,
|
||||
expect_errors=True)
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
# Temporary until actual implementation
|
||||
self.assertEqual(home.JSON_HOME, response.json)
|
||||
self.assertEqual(400, response.status_code)
|
||||
|
|
2
tox.ini
2
tox.ini
|
@ -18,7 +18,7 @@ deps = -r{toxinidir}/requirements/requirements.txt
|
|||
-r{toxinidir}/tests/test-requirements.txt
|
||||
commands = pip install git+https://github.com/stackforge/opencafe.git#egg=cafe
|
||||
pip install git+https://github.com/tonytan4ever/python-maxcdn.git#egg=maxcdn
|
||||
nosetests {posargs:--exclude=api}
|
||||
nosetests {posargs:--exclude=api --nologcapture}
|
||||
|
||||
[tox:jenkins]
|
||||
downloadcache = ~/cache/pip
|
||||
|
|
Loading…
Reference in New Issue