Skip to content

Commit

Permalink
Fix issue #60.
Browse files Browse the repository at this point in the history
  • Loading branch information
jfinkels committed Feb 16, 2016
1 parent 2f033c1 commit 171cb55
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand Down
29 changes: 28 additions & 1 deletion flask_restless/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from uuid import uuid1

import flask
from flask import abort
from flask import request
from flask import Blueprint

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
43 changes: 43 additions & 0 deletions tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 171cb55

Please sign in to comment.