diff --git a/CHANGES b/CHANGES index 5b649778..999bd03e 100644 --- a/CHANGES +++ b/CHANGES @@ -24,6 +24,10 @@ Not yet released. - Removes `mimerender`_ as a dependency. - :issue:`7`: allows filtering before function evaluation. - :issue:`49`: deserializers now expect a complete JSON API document. +- :issue:`60`: added the ``hide_disallowed_endpoints`` keyword argument to + :meth:`APIManager.create_api_blueprint` to hide disallowed HTTP methods + behind a :http:status:`404` response instead of a :http:status:`405` + response. - :issue:`200`: be smarter about determining the ``collection_name`` for polymorphic models defined with single-table inheritance. - :issue:`253`: don't assign to callable attributes of models. @@ -60,10 +64,10 @@ Version 1.0.0b1 Released on April 2, 2016. - :issue:`255`: adds support for filtering by PostgreSQL network operators. -- :issue:`257`: ensures additional attributes specified by the user actually exist on - the model. -- :issue:`363` (partial solution): don't use ``COUNT`` on requests that don't require - pagination. +- :issue:`257`: ensures additional attributes specified by the user actually + exist on the model. +- :issue:`363` (partial solution): don't use ``COUNT`` on requests that don't + require pagination. - :issue:`404`: **Major overhaul of Flask-Restless to support JSON API**. - Increases minimum version requirement for ``python-dateutil`` to be strictly greater than 2.2 to avoid parsing bug. diff --git a/docs/installation.rst b/docs/installation.rst index 4ee78b2e..3ff95162 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -13,7 +13,7 @@ currently include versions 2.6, 2.7, 3.3, 3.4, and 3.5. Flask-Restless has the following dependencies (which will be automatically installed if you use ``pip``): -* `Flask`_ version 0.10 or greater +* `Flask`_ version 0.11 or greater * `SQLAlchemy`_ version 0.8 or greater * `python-dateutil`_ version strictly greater than 2.2 diff --git a/flask_restless/manager.py b/flask_restless/manager.py index c621631a..9e9f99b6 100644 --- a/flask_restless/manager.py +++ b/flask_restless/manager.py @@ -21,6 +21,7 @@ import sys from sqlalchemy.inspection import inspect +from flask import abort from flask import Blueprint from flask import url_for as flask_url_for @@ -453,7 +454,8 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS, serializer_class=None, deserializer_class=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): """Creates and returns a ReSTful API interface as a blueprint, but does not register it on any :class:`flask.Flask` application. @@ -648,6 +650,18 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS, this be a UUID. This is ``False`` by default. For more information, see :doc:`creating`. + If `hide_disallowed_endpoints` is ``True``, requests to + disallowed methods (that is, methods not specified in + `methods`), which would normally yield a :http:status:`405` + response, will yield a :http:status:`404` response instead. This + option may be used as a simple form of "security through + obscurity", by (slightly) hindering users from discovering where + an endpoint exists. + + .. versionchanged:: 1.0.0 + + The `hide_disallowed_endpoints` keyword argument is new. + """ # Perform some sanity checks on the provided keyword arguments. if only is not None and exclude is not None: @@ -827,11 +841,17 @@ 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) + # Finally, record that this APIManager instance has created an API for # the specified model. self.created_apis_for[model] = APIInfo(collection_name, blueprint.name, serializer, primary_key) self.models.add(model) + return blueprint def create_api(self, *args, **kw): diff --git a/requirements/install.txt b/requirements/install.txt index 06e39b47..66a1c664 100644 --- a/requirements/install.txt +++ b/requirements/install.txt @@ -1,4 +1,4 @@ -flask>=0.10 +flask>=0.11 flask-sqlalchemy>=0.10 sqlalchemy>=0.8 python-dateutil>2.2 diff --git a/setup.py b/setup.py index 6b52f0c1..2e0d76d2 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ #: The installation requirements for Flask-Restless. Flask-SQLAlchemy is not #: required, so the user must install it explicitly. -REQUIREMENTS = ['flask>=0.10', 'sqlalchemy>=0.8', 'python-dateutil>2.2'] +REQUIREMENTS = ['flask>=0.11', 'sqlalchemy>=0.8', 'python-dateutil>2.2'] #: The absolute path to this file. HERE = os.path.abspath(os.path.dirname(__file__)) diff --git a/tests/test_manager.py b/tests/test_manager.py index 566829fe..3f6a70ab 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -425,6 +425,29 @@ def test_disallowed_methods(self): response = func('/api/person') assert response.status_code == 405 + def test_hide_disallowed_endpoints(self): + """Test for hiding disallowed endpoints behind a 404 Not Found. + + Setting the `hide_disallowed_endpoints` keyword argument to True + should cause requests that would normally cause a + :http:status:`405` response to cause a :http:status:`404` + response instead. + + """ + self.manager.create_api(self.Person, hide_disallowed_endpoints=True) + + response = self.app.get('/api/person') + self.assertNotEqual(response.status_code, 404) + + response = self.app.post('/api/person') + self.assertEqual(response.status_code, 404) + response = self.app.patch('/api/person/1') + self.assertEqual(response.status_code, 404) + response = self.app.delete('/api/person/1') + self.assertEqual(response.status_code, 404) + response = self.app.put('/api/person/1') + self.assertEqual(response.status_code, 404) + def test_empty_collection_name(self): """Tests that calling :meth:`APIManager.create_api` with an empty collection name raises an exception.