From 171cb55def21c57e441ccdcadd12954789703ccc Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Thu, 3 May 2012 22:45:18 -0400 Subject: [PATCH] Fix issue #60. --- CHANGES | 4 ++++ flask_restless/manager.py | 29 +++++++++++++++++++++++++- tests/test_manager.py | 43 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 9e03edf5..2a1df1d1 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,10 @@ Version 1.0.0-dev Not yet released. +- #60: added the ``hide_endpoints`` keyword argument to + :meth:`APIManager.create_api_blueprint` to hide disallowed HTTP methods + behind a :http:statuscode:`404` response instead of a :http:statuscode:`405` + response. - #363 (partial solution): don't use ``COUNT`` on requests that don't require pagination. - #404: **Major overhaul of Flask-Restless to support JSON API**. diff --git a/flask_restless/manager.py b/flask_restless/manager.py index 089d7e78..391df8d4 100644 --- a/flask_restless/manager.py +++ b/flask_restless/manager.py @@ -26,6 +26,7 @@ from uuid import uuid1 import flask +from flask import abort from flask import request from flask import Blueprint @@ -369,7 +370,9 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS, serializer=None, deserializer=None, includes=None, allow_to_many_replacement=False, allow_delete_from_to_many_relationships=False, - allow_client_generated_ids=False): + allow_client_generated_ids=False, + hide_disallowed_endpoints=False, + hide_unauthenticated_endpoints=False): """Creates and returns a ReSTful API interface as a blueprint, but does not register it on any :class:`flask.Flask` application. @@ -565,6 +568,19 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS, this be a UUID. This is ``False`` by default. For more information, see :ref:`creating`. + If `hide_disallowed_endpoints` is ``True``, requests to + disallowed methods (that is, methods not specified in + `methods`), which would normally yield a :http:statuscode:`405` + response, will yield a :http:statuscode:`404` response + instead. If `hide_unauthenticated_endpoints` is ``True``, + requests to endpoints for which the user has not authenticated + (as specified in the `authentication_required_for` and + `authentication_function` arguments) will also be masked by + :http:statuscode:`404` instead of :http:statuscode:`403`. These + options may be used as a simple form of "security through + obscurity", by (slightly) hindering users from discovering where + an endpoint exists. + """ # Perform some sanity checks on the provided keyword arguments. if only is not None and exclude is not None: @@ -727,9 +743,20 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS, blueprint.add_url_rule(eval_endpoint, methods=eval_methods, view_func=eval_api_view) + if hide_disallowed_endpoints: + @blueprint.errorhandler(405) + def return_404(error): + abort(404) + + if hide_unauthenticated_endpoints: + @blueprint.errorhandler(403) + def return_404(error): + abort(404) + # Finally, record that this APIManager instance has created an API for # the specified model. self.created_apis_for[model] = APIInfo(collection_name, blueprint.name) + return blueprint def create_api(self, *args, **kw): diff --git a/tests/test_manager.py b/tests/test_manager.py index ad15716a..18510db6 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -293,6 +293,49 @@ def test_model_for(self): self.manager.create_api(self.Person, collection_name='people') assert model_for('people') is self.Person + def test_hide_disallowed_endpoints(self): + """Tests that the `hide_disallowed_endpoints` and + `hide_unauthenticated_endpoints` arguments correctly hide endpoints + which would normally return a :http:statuscode:`405` or + :http:statuscode:`403` with a :http:statuscode:`404`. + + """ + self.manager.create_api(self.Person, methods=['GET', 'POST'], + hide_disallowed_endpoints=True) + + class auth_func(object): + x = 0 + def __call__(params): + x += 1 + if x % 2 == 0: + raise ProcessingException(status_code=403, + message='Permission denied') + return NO_CHANGE + + self.manager.create_api(self.Person, methods=['GET', 'POST'], + hide_unauthenticated_endpoints=True, + preprocessors=dict(POST=[auth_func]), + url_prefix='/auth') + # first test disallowed functions + response = self.app.get('/api/person') + self.assertNotEqual(404, response.status_code) + response = self.app.post('/api/person', data=dumps(dict(name='foo'))) + self.assertNotEqual(404, response.status_code) + response = self.app.patch('/api/person/1', + data=dumps(dict(name='bar'))) + self.assertEqual(404, response.status_code) + response = self.app.put('/api/person/1', data=dumps(dict(name='bar'))) + self.assertEqual(404, response.status_code) + response = self.app.delete('/api/person/1') + self.assertEqual(404, response.status_code) + # now test unauthenticated functions + response = self.app.get('/auth/person') + self.assertNotEqual(404, response.status_code) + response = self.app.post('/auth/person', data=dumps(dict(name='foo'))) + self.assertNotEqual(404, response.status_code) + response = self.app.post('/auth/person', data=dumps(dict(name='foo'))) + self.assertEqual(404, response.status_code) + @raises(ValueError) def test_model_for_nonexistent(self): """Tests that attempting to get the model for a nonexistent collection