Skip to content

datasette.permission_allowed() should consider all checks, not just the last returned #2275

@simonw

Description

@simonw

Spotted this in the code for datasette.permission_allowed():

datasette/datasette/app.py

Lines 906 to 914 in 232a304

for check in pm.hook.permission_allowed(
datasette=self,
actor=actor,
action=action,
resource=resource,
):
check = await await_me_maybe(check)
if check is not None:
result = check

This is effectively saying that the permission hook which "wins" is the last one checked in the sequence.

This is actually new behaviour, introduced in dfdbdf3#diff-83d29b1fb42ebaa92e01122d8142807188ab7b6689697c9defdcdca60ce13bc5L435 - prior to that commit the logic looked like this instead:

datasette/datasette/app.py

Lines 426 to 436 in 57cf513

action=action,
resource_type=resource_type,
resource_identifier=resource_identifier,
):
if callable(check):
check = check()
if asyncio.iscoroutine(check):
check = await check
if check is not None:
return check
return default

So the original logic was first-non-Null-wins, and then I changed it to last-non-Null-wins in dfdbdf3 - but the commit message and issue for that change don't mention it, which makes me think this was a mistake:

I don't think first wins OR last wins are the right solution here, especially given that Pluggy plugin order is a little bit non-deterministic (aside from when plugins use tryfirst= and trylast=).

Instead, I think a better solution would be to run ALL of the hooks and then have these rules:

  • If any of them returned False then the answer is False (a veto rule)
  • Otherwise, if any returned True then the answer is True
  • If all returned None (no opinion) then the answer is whatever the default is for that permission

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions