Skip to content

Commit 171cb55

Browse files
committed
Fix issue #60.
1 parent 2f033c1 commit 171cb55

File tree

3 files changed

+75
-1
lines changed

3 files changed

+75
-1
lines changed

CHANGES

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Version 1.0.0-dev
1515

1616
Not yet released.
1717

18+
- #60: added the ``hide_endpoints`` keyword argument to
19+
:meth:`APIManager.create_api_blueprint` to hide disallowed HTTP methods
20+
behind a :http:statuscode:`404` response instead of a :http:statuscode:`405`
21+
response.
1822
- #363 (partial solution): don't use ``COUNT`` on requests that don't require
1923
pagination.
2024
- #404: **Major overhaul of Flask-Restless to support JSON API**.

flask_restless/manager.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from uuid import uuid1
2727

2828
import flask
29+
from flask import abort
2930
from flask import request
3031
from flask import Blueprint
3132

@@ -369,7 +370,9 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS,
369370
serializer=None, deserializer=None,
370371
includes=None, allow_to_many_replacement=False,
371372
allow_delete_from_to_many_relationships=False,
372-
allow_client_generated_ids=False):
373+
allow_client_generated_ids=False,
374+
hide_disallowed_endpoints=False,
375+
hide_unauthenticated_endpoints=False):
373376
"""Creates and returns a ReSTful API interface as a blueprint, but does
374377
not register it on any :class:`flask.Flask` application.
375378
@@ -565,6 +568,19 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS,
565568
this be a UUID. This is ``False`` by default. For more information, see
566569
:ref:`creating`.
567570
571+
If `hide_disallowed_endpoints` is ``True``, requests to
572+
disallowed methods (that is, methods not specified in
573+
`methods`), which would normally yield a :http:statuscode:`405`
574+
response, will yield a :http:statuscode:`404` response
575+
instead. If `hide_unauthenticated_endpoints` is ``True``,
576+
requests to endpoints for which the user has not authenticated
577+
(as specified in the `authentication_required_for` and
578+
`authentication_function` arguments) will also be masked by
579+
:http:statuscode:`404` instead of :http:statuscode:`403`. These
580+
options may be used as a simple form of "security through
581+
obscurity", by (slightly) hindering users from discovering where
582+
an endpoint exists.
583+
568584
"""
569585
# Perform some sanity checks on the provided keyword arguments.
570586
if only is not None and exclude is not None:
@@ -727,9 +743,20 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS,
727743
blueprint.add_url_rule(eval_endpoint, methods=eval_methods,
728744
view_func=eval_api_view)
729745

746+
if hide_disallowed_endpoints:
747+
@blueprint.errorhandler(405)
748+
def return_404(error):
749+
abort(404)
750+
751+
if hide_unauthenticated_endpoints:
752+
@blueprint.errorhandler(403)
753+
def return_404(error):
754+
abort(404)
755+
730756
# Finally, record that this APIManager instance has created an API for
731757
# the specified model.
732758
self.created_apis_for[model] = APIInfo(collection_name, blueprint.name)
759+
733760
return blueprint
734761

735762
def create_api(self, *args, **kw):

tests/test_manager.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,49 @@ def test_model_for(self):
293293
self.manager.create_api(self.Person, collection_name='people')
294294
assert model_for('people') is self.Person
295295

296+
def test_hide_disallowed_endpoints(self):
297+
"""Tests that the `hide_disallowed_endpoints` and
298+
`hide_unauthenticated_endpoints` arguments correctly hide endpoints
299+
which would normally return a :http:statuscode:`405` or
300+
:http:statuscode:`403` with a :http:statuscode:`404`.
301+
302+
"""
303+
self.manager.create_api(self.Person, methods=['GET', 'POST'],
304+
hide_disallowed_endpoints=True)
305+
306+
class auth_func(object):
307+
x = 0
308+
def __call__(params):
309+
x += 1
310+
if x % 2 == 0:
311+
raise ProcessingException(status_code=403,
312+
message='Permission denied')
313+
return NO_CHANGE
314+
315+
self.manager.create_api(self.Person, methods=['GET', 'POST'],
316+
hide_unauthenticated_endpoints=True,
317+
preprocessors=dict(POST=[auth_func]),
318+
url_prefix='/auth')
319+
# first test disallowed functions
320+
response = self.app.get('/api/person')
321+
self.assertNotEqual(404, response.status_code)
322+
response = self.app.post('/api/person', data=dumps(dict(name='foo')))
323+
self.assertNotEqual(404, response.status_code)
324+
response = self.app.patch('/api/person/1',
325+
data=dumps(dict(name='bar')))
326+
self.assertEqual(404, response.status_code)
327+
response = self.app.put('/api/person/1', data=dumps(dict(name='bar')))
328+
self.assertEqual(404, response.status_code)
329+
response = self.app.delete('/api/person/1')
330+
self.assertEqual(404, response.status_code)
331+
# now test unauthenticated functions
332+
response = self.app.get('/auth/person')
333+
self.assertNotEqual(404, response.status_code)
334+
response = self.app.post('/auth/person', data=dumps(dict(name='foo')))
335+
self.assertNotEqual(404, response.status_code)
336+
response = self.app.post('/auth/person', data=dumps(dict(name='foo')))
337+
self.assertEqual(404, response.status_code)
338+
296339
@raises(ValueError)
297340
def test_model_for_nonexistent(self):
298341
"""Tests that attempting to get the model for a nonexistent collection

0 commit comments

Comments
 (0)