datasette/docs/upgrade-1.0a20.md
Simon Willison 5705ce0d95
Move takes_child/takes_parent information from Action to Resource (#2567)
Simplified Action by moving takes_child/takes_parent logic to Resource

- Removed InstanceResource - global actions are now simply those with resource_class=None
- Resource.parent_class - Replaced parent_name: str with parent_class: type[Resource] | None for direct class references
- Simplified Action dataclass - No more redundant fields, everything is derived from the Resource class structure
- Validation - The __init_subclass__ method now checks parent_class.parent_class to enforce the 2-level hierarchy

Closes #2563
2025-11-01 11:35:08 -07:00

3.9 KiB

orphan
true

Datasette 1.0a20 plugin upgrade guide

Datasette 1.0a20 makes some breaking changes to Datasette's permission system. Plugins need to be updated if they use any of the following:

  • The register_permissions() plugin hook - this should be replaced with register_actions
  • The permission_allowed() plugin hook - this should be upgraded to permission_resources_sql().
  • The datasette.permission_allowed() internal method - this should be replaced with datasette.allowed()
  • Logic that grants access to the "root" actor can be removed.

Permissions are now actions

The register_permissions() hook shoud be replaced with register_actions().

Old code:

@hookimpl
def register_permissions(datasette):
    return [
        Permission(
            name="datasette-pins-write",
            abbr=None,
            description="Can pin, unpin, and re-order pins for datasette-pins",
            takes_database=False,
            takes_resource=False,
            default=False,
        ),
        Permission(
            name="datasette-pins-read",
            abbr=None,
            description="Can read pinned items.",
            takes_database=False,
            takes_resource=False,
            default=False,
        ),
    ]

The new Action does not have a default= parameter. For global actions (those that don't apply to specific resources), omit resource_class:

from datasette.permissions import Action

@hookimpl
def register_actions(datasette):
    return [
        Action(
            name="datasette-pins-write",
            abbr=None,
            description="Can pin, unpin, and re-order pins for datasette-pins",
        ),
        Action(
            name="datasette-pins-read",
            abbr=None,
            description="Can read pinned items.",
        ),
    ]

For actions that apply to specific resources (like databases or tables), specify the resource_class instead of takes_parent and takes_child:

from datasette.permissions import Action
from datasette.resources import DatabaseResource, TableResource

@hookimpl
def register_actions(datasette):
    return [
        Action(
            name="execute-sql",
            abbr="es",
            description="Execute SQL queries",
            resource_class=DatabaseResource,  # Parent-level resource
        ),
        Action(
            name="insert-row",
            abbr="ir",
            description="Insert rows",
            resource_class=TableResource,  # Child-level resource
        ),
    ]

The hierarchy information (whether an action takes parent/child parameters) is now derived from the Resource class hierarchy. Action has takes_parent and takes_child properties that are computed based on the resource_class and its parent_class attribute.

permission_allowed() hook is replaced by permission_resources_sql()

The following old code:

@hookimpl
def permission_allowed(action):
    if action == "permissions-debug":
        return True

Can be replaced by:

from datasette.permissions import PermissionSQL

@hookimpl
def permission_resources_sql(action):
    return PermissionSQL.allow(reason="datasette-allow-permissions-debug")

A .deny(reason="") class method is also available.

For more complex permission checks consult the documentation for that plugin hook: https://docs.datasette.io/en/latest/plugin_hooks.html#permission-resources-sql-datasette-actor-action

Fixing async with httpx.AsyncClient(app=app)

Some older plugins may use the following pattern in their tests, which is no longer supported:

app = Datasette([], memory=True).app()
async with httpx.AsyncClient(app=app) as client:
    response = await client.get("http://localhost/path")

The new pattern is to use ds.client like this:

ds = Datasette([], memory=True)
response = ds.client.get("/path")