mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
skip_csrf(datasette, scope) plugin hook, refs #1377
This commit is contained in:
parent
4a3e8561ab
commit
b1fd24ac9f
8 changed files with 68 additions and 1 deletions
|
|
@ -1052,6 +1052,9 @@ class Datasette:
|
||||||
DatasetteRouter(self, routes),
|
DatasetteRouter(self, routes),
|
||||||
signing_secret=self._secret,
|
signing_secret=self._secret,
|
||||||
cookie_name="ds_csrftoken",
|
cookie_name="ds_csrftoken",
|
||||||
|
skip_if_scope=lambda scope: any(
|
||||||
|
pm.hook.skip_csrf(datasette=self, scope=scope)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if self.setting("trace_debug"):
|
if self.setting("trace_debug"):
|
||||||
asgi = AsgiTracer(asgi)
|
asgi = AsgiTracer(asgi)
|
||||||
|
|
|
||||||
|
|
@ -112,3 +112,8 @@ def table_actions(datasette, actor, database, table, request):
|
||||||
@hookspec
|
@hookspec
|
||||||
def database_actions(datasette, actor, database, request):
|
def database_actions(datasette, actor, database, request):
|
||||||
"""Links for the database actions menu"""
|
"""Links for the database actions menu"""
|
||||||
|
|
||||||
|
|
||||||
|
@hookspec
|
||||||
|
def skip_csrf(datasette, scope):
|
||||||
|
"""Mechanism for skipping CSRF checks for certain requests"""
|
||||||
|
|
|
||||||
|
|
@ -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() }}">
|
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
|
||||||
|
|
||||||
|
You can selectively disable CSRF protection using the :ref:`plugin_hook_skip_csrf` hook.
|
||||||
|
|
||||||
.. _internals_internal:
|
.. _internals_internal:
|
||||||
|
|
||||||
The _internal database
|
The _internal database
|
||||||
|
|
|
||||||
|
|
@ -1104,3 +1104,28 @@ database_actions(datasette, actor, database, request)
|
||||||
The current HTTP :ref:`internals_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.
|
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.
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -55,7 +55,7 @@ setup(
|
||||||
"uvicorn~=0.11",
|
"uvicorn~=0.11",
|
||||||
"aiofiles>=0.4,<0.8",
|
"aiofiles>=0.4,<0.8",
|
||||||
"janus>=0.4,<0.7",
|
"janus>=0.4,<0.7",
|
||||||
"asgi-csrf>=0.6",
|
"asgi-csrf>=0.9",
|
||||||
"PyYAML~=5.3",
|
"PyYAML~=5.3",
|
||||||
"mergedeep>=1.1.1,<1.4.0",
|
"mergedeep>=1.1.1,<1.4.0",
|
||||||
"itsdangerous>=1.1,<3.0",
|
"itsdangerous>=1.1,<3.0",
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ EXPECTED_PLUGINS = [
|
||||||
"register_magic_parameters",
|
"register_magic_parameters",
|
||||||
"register_routes",
|
"register_routes",
|
||||||
"render_cell",
|
"render_cell",
|
||||||
|
"skip_csrf",
|
||||||
"startup",
|
"startup",
|
||||||
"table_actions",
|
"table_actions",
|
||||||
],
|
],
|
||||||
|
|
@ -152,6 +153,7 @@ def make_app_client(
|
||||||
static_mounts=static_mounts,
|
static_mounts=static_mounts,
|
||||||
template_dir=template_dir,
|
template_dir=template_dir,
|
||||||
crossdb=crossdb,
|
crossdb=crossdb,
|
||||||
|
pdb=True,
|
||||||
)
|
)
|
||||||
ds.sqlite_functions.append(("sleep", 1, lambda n: time.sleep(float(n))))
|
ds.sqlite_functions.append(("sleep", 1, lambda n: time.sleep(float(n))))
|
||||||
yield TestClient(ds)
|
yield TestClient(ds)
|
||||||
|
|
|
||||||
|
|
@ -348,3 +348,8 @@ def database_actions(datasette, database, actor, request):
|
||||||
"label": label,
|
"label": label,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def skip_csrf(scope):
|
||||||
|
return scope["path"] == "/skip-csrf"
|
||||||
|
|
|
||||||
|
|
@ -825,3 +825,28 @@ def test_hook_database_actions(app_client):
|
||||||
assert get_table_actions_links(response_2.text) == [
|
assert get_table_actions_links(response_2.text) == [
|
||||||
{"label": "Database: fixtures - BOB", "href": "/"},
|
{"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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue