mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Implemented actor_from_request with tests, refs #699
Also added datasette argument to permission_allowed hook
This commit is contained in:
parent
060a56735c
commit
461c82838d
6 changed files with 80 additions and 2 deletions
|
|
@ -798,7 +798,18 @@ class DatasetteRouter(AsgiRouter):
|
||||||
and scope.get("scheme") != "https"
|
and scope.get("scheme") != "https"
|
||||||
):
|
):
|
||||||
scope = dict(scope, scheme="https")
|
scope = dict(scope, scheme="https")
|
||||||
return await super().route_path(scope, receive, send, path)
|
# Handle authentication
|
||||||
|
actor = None
|
||||||
|
for actor in pm.hook.actor_from_request(
|
||||||
|
datasette=self.ds, request=Request(scope, receive)
|
||||||
|
):
|
||||||
|
if callable(actor):
|
||||||
|
actor = actor()
|
||||||
|
if asyncio.iscoroutine(actor):
|
||||||
|
actor = await actor
|
||||||
|
if actor:
|
||||||
|
break
|
||||||
|
return await super().route_path(dict(scope, actor=actor), receive, send, path)
|
||||||
|
|
||||||
async def handle_404(self, scope, receive, send, exception=None):
|
async def handle_404(self, scope, receive, send, exception=None):
|
||||||
# If URL has a trailing slash, redirect to URL without it
|
# If URL has a trailing slash, redirect to URL without it
|
||||||
|
|
|
||||||
|
|
@ -66,5 +66,5 @@ def actor_from_request(datasette, request):
|
||||||
|
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
def permission_allowed(actor, action, resource_type, resource_identifier):
|
def permission_allowed(datasette, actor, action, resource_type, resource_identifier):
|
||||||
"Check if actor is allowed to perfom this action - return True, False or None"
|
"Check if actor is allowed to perfom this action - return True, False or None"
|
||||||
|
|
|
||||||
|
|
@ -957,6 +957,29 @@ This is part of Datasette's authentication and permissions system. The function
|
||||||
|
|
||||||
If it cannot authenticate an actor, it should return ``None``. Otherwise it should return a dictionary representing that actor.
|
If it cannot authenticate an actor, it should return ``None``. Otherwise it should return a dictionary representing that actor.
|
||||||
|
|
||||||
|
Instead of returning a dictionary, this function can return an awaitable function which itself returns either ``None`` or a dictionary. This is useful for authentication functions that need to make a database query - for example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from datasette import hookimpl
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def actor_from_request(datasette, request):
|
||||||
|
async def inner():
|
||||||
|
token = request.args.get("_token")
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
# Look up ?_token=xxx in sessions table
|
||||||
|
result = await datasette.get_database().execute(
|
||||||
|
"select count(*) from sessions where token = ?", [token]
|
||||||
|
)
|
||||||
|
if result.first()[0]:
|
||||||
|
return {"token": token}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
.. _plugin_permission_allowed:
|
.. _plugin_permission_allowed:
|
||||||
|
|
||||||
permission_allowed(datasette, actor, action, resource_type, resource_identifier)
|
permission_allowed(datasette, actor, action, resource_type, resource_identifier)
|
||||||
|
|
|
||||||
|
|
@ -126,3 +126,11 @@ class DummyFacet(Facet):
|
||||||
facet_results = {}
|
facet_results = {}
|
||||||
facets_timed_out = []
|
facets_timed_out = []
|
||||||
return facet_results, facets_timed_out
|
return facet_results, facets_timed_out
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def actor_from_request(datasette, request):
|
||||||
|
if request.args.get("_bot"):
|
||||||
|
return {"id": "bot"}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -95,3 +95,15 @@ def asgi_wrapper(datasette):
|
||||||
return add_x_databases_header
|
return add_x_databases_header
|
||||||
|
|
||||||
return wrap_with_databases_header
|
return wrap_with_databases_header
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def actor_from_request(datasette, request):
|
||||||
|
async def inner():
|
||||||
|
if request.args.get("_bot2"):
|
||||||
|
result = await datasette.get_database().execute("select 1 + 1")
|
||||||
|
return {"id": "bot2", "1+1": result.first()[0]}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
|
||||||
|
|
@ -503,3 +503,27 @@ def test_register_facet_classes(app_client):
|
||||||
"toggle_url": "http://localhost/fixtures/compound_three_primary_keys.json?_dummy_facet=1&_facet=pk3",
|
"toggle_url": "http://localhost/fixtures/compound_three_primary_keys.json?_dummy_facet=1&_facet=pk3",
|
||||||
},
|
},
|
||||||
] == data["suggested_facets"]
|
] == data["suggested_facets"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_actor_from_request(app_client):
|
||||||
|
app_client.get("/")
|
||||||
|
# Should have no actor
|
||||||
|
assert None == app_client.ds._last_request.scope["actor"]
|
||||||
|
app_client.get("/?_bot=1")
|
||||||
|
# Should have bot actor
|
||||||
|
assert {"id": "bot"} == app_client.ds._last_request.scope["actor"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_actor_from_request_async(app_client):
|
||||||
|
app_client.get("/")
|
||||||
|
# Should have no actor
|
||||||
|
assert None == app_client.ds._last_request.scope["actor"]
|
||||||
|
app_client.get("/?_bot2=1")
|
||||||
|
# Should have bot2 actor
|
||||||
|
assert {"id": "bot2", "1+1": 2} == app_client.ds._last_request.scope["actor"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail
|
||||||
|
def test_permission_allowed(app_client):
|
||||||
|
# TODO
|
||||||
|
assert False
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue