skip_csrf(datasette, scope) plugin hook, refs #1377

This commit is contained in:
Simon Willison 2021-06-23 15:39:52 -07:00
commit b1fd24ac9f
8 changed files with 68 additions and 1 deletions

View file

@ -1052,6 +1052,9 @@ class Datasette:
DatasetteRouter(self, routes),
signing_secret=self._secret,
cookie_name="ds_csrftoken",
skip_if_scope=lambda scope: any(
pm.hook.skip_csrf(datasette=self, scope=scope)
),
)
if self.setting("trace_debug"):
asgi = AsgiTracer(asgi)

View file

@ -112,3 +112,8 @@ def table_actions(datasette, actor, database, table, request):
@hookspec
def database_actions(datasette, actor, database, request):
"""Links for the database actions menu"""
@hookspec
def skip_csrf(datasette, scope):
"""Mechanism for skipping CSRF checks for certain requests"""

View file

@ -778,6 +778,8 @@ If your plugin implements a ``<form method="POST">`` anywhere you will need to i
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
You can selectively disable CSRF protection using the :ref:`plugin_hook_skip_csrf` hook.
.. _internals_internal:
The _internal database

View file

@ -1104,3 +1104,28 @@ database_actions(datasette, actor, database, request)
The current HTTP :ref:`internals_request`.
This hook is similar to :ref:`plugin_hook_table_actions` but populates an actions menu on the database page.
.. _plugin_hook_skip_csrf:
skip_csrf(datasette, scope)
---------------------------
``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries.
``scope`` - dictionary
The `ASGI scope <https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope>`__ for the incoming HTTP request.
This hook can be used to skip :ref:`internals_csrf` for a specific incoming request. For example, you might have a custom path at ``/submit-comment`` which is designed to accept comments from anywhere, whether or not the incoming request originated on the site and has an accompanying CSRF token.
This example will disable CSRF protection for that specific URL path:
.. code-block:: python
from datasette import hookimpl
@hookimpl
def skip_csrf(scope):
return scope["path"] == "/submit-comment"
If any of the currently active ``skip_csrf()`` plugin hooks return ``True``, CSRF protection will be skipped for the request.

View file

@ -55,7 +55,7 @@ setup(
"uvicorn~=0.11",
"aiofiles>=0.4,<0.8",
"janus>=0.4,<0.7",
"asgi-csrf>=0.6",
"asgi-csrf>=0.9",
"PyYAML~=5.3",
"mergedeep>=1.1.1,<1.4.0",
"itsdangerous>=1.1,<3.0",

View file

@ -52,6 +52,7 @@ EXPECTED_PLUGINS = [
"register_magic_parameters",
"register_routes",
"render_cell",
"skip_csrf",
"startup",
"table_actions",
],
@ -152,6 +153,7 @@ def make_app_client(
static_mounts=static_mounts,
template_dir=template_dir,
crossdb=crossdb,
pdb=True,
)
ds.sqlite_functions.append(("sleep", 1, lambda n: time.sleep(float(n))))
yield TestClient(ds)

View file

@ -348,3 +348,8 @@ def database_actions(datasette, database, actor, request):
"label": label,
}
]
@hookimpl
def skip_csrf(scope):
return scope["path"] == "/skip-csrf"

View file

@ -825,3 +825,28 @@ def test_hook_database_actions(app_client):
assert get_table_actions_links(response_2.text) == [
{"label": "Database: fixtures - BOB", "href": "/"},
]
def test_hook_skip_csrf(app_client):
cookie = app_client.actor_cookie({"id": "test"})
csrf_response = app_client.post(
"/post/",
post_data={"this is": "post data"},
csrftoken_from=True,
cookies={"ds_actor": cookie},
)
assert csrf_response.status == 200
missing_csrf_response = app_client.post(
"/post/", post_data={"this is": "post data"}, cookies={"ds_actor": cookie}
)
assert missing_csrf_response.status == 403
# But "/skip-csrf" should allow
allow_csrf_response = app_client.post(
"/skip-csrf", post_data={"this is": "post data"}, cookies={"ds_actor": cookie}
)
assert allow_csrf_response.status == 405 # Method not allowed
# /skip-csrf-2 should not
second_missing_csrf_response = app_client.post(
"/skip-csrf-2", post_data={"this is": "post data"}, cookies={"ds_actor": cookie}
)
assert second_missing_csrf_response.status == 403