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

pull/1368/head
Simon Willison 2021-06-23 15:39:52 -07:00
rodzic 4a3e8561ab
commit b1fd24ac9f
8 zmienionych plików z 68 dodań i 1 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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"""

Wyświetl plik

@ -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

Wyświetl plik

@ -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.

Wyświetl plik

@ -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",

Wyświetl plik

@ -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)

Wyświetl plik

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

Wyświetl plik

@ -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