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:
amitgandhinz 2014-11-17 16:05:52 -05:00
parent 46a408b8fd
commit 8091123422
15 changed files with 137 additions and 55 deletions

View File

@ -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')

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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(

View File

@ -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)

View File

@ -1 +1 @@
pecan==0.6.1
pecan==0.8.1

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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