Implemented actor_from_request with tests, refs #699

Also added datasette argument to permission_allowed hook
This commit is contained in:
Simon Willison 2020-05-30 15:06:33 -07:00
commit 461c82838d
6 changed files with 80 additions and 2 deletions

View file

@ -798,7 +798,18 @@ class DatasetteRouter(AsgiRouter):
and scope.get("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):
# If URL has a trailing slash, redirect to URL without it

View file

@ -66,5 +66,5 @@ def actor_from_request(datasette, request):
@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"

View file

@ -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.
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:
permission_allowed(datasette, actor, action, resource_type, resource_identifier)

View file

@ -126,3 +126,11 @@ class DummyFacet(Facet):
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

View file

@ -95,3 +95,15 @@ def asgi_wrapper(datasette):
return add_x_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

View file

@ -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",
},
] == 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