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
This commit is contained in:
Simon Willison 2025-11-01 11:35:08 -07:00 committed by GitHub
commit 5705ce0d95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 417 additions and 186 deletions

View file

@ -883,24 +883,18 @@ Actions define what operations can be performed on resources (like viewing a tab
name="list-documents",
abbr="ld",
description="List documents in a collection",
takes_parent=True,
takes_child=False,
resource_class=DocumentCollectionResource,
),
Action(
name="view-document",
abbr="vdoc",
description="View document",
takes_parent=True,
takes_child=True,
resource_class=DocumentResource,
),
Action(
name="edit-document",
abbr="edoc",
description="Edit document",
takes_parent=True,
takes_child=True,
resource_class=DocumentResource,
),
]
@ -916,26 +910,20 @@ The fields of the ``Action`` dataclass are as follows:
``description`` - string or None
A human-readable description of what the action allows you to do.
``takes_parent`` - boolean
``True`` if this action requires a parent identifier (like a database name).
``takes_child`` - boolean
``True`` if this action requires a child identifier (like a table or document name).
``resource_class`` - type[Resource]
The Resource subclass that defines what kind of resource this action applies to. Your Resource subclass must:
``resource_class`` - type[Resource] or None
The Resource subclass that defines what kind of resource this action applies to. Omit this (or set to ``None``) for global actions that apply only at the instance level with no associated resources (like ``debug-menu`` or ``permissions-debug``). Your Resource subclass must:
- Define a ``name`` class attribute (e.g., ``"document"``)
- Optionally define a ``parent_name`` class attribute (e.g., ``"collection"``)
- Define a ``parent_class`` class attribute (``None`` for top-level resources like databases, or the parent ``Resource`` subclass for child resources)
- Implement a ``resources_sql()`` classmethod that returns SQL returning all resources as ``(parent, child)`` columns
- Have an ``__init__`` method that accepts appropriate parameters and calls ``super().__init__(parent=..., child=...)``
The ``resources_sql()`` method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``resources_sql()`` classmethod is crucial to Datasette's permission system. It returns a SQL query that lists all resources of that type that exist in the system.
The ``resources_sql()`` classmethod returns a SQL query that lists all resources of that type that exist in the system.
This SQL query is used by Datasette to efficiently check permissions across multiple resources at once. When a user requests a list of resources (like tables, documents, or other entities), Datasette uses this SQL to:
This query is used by Datasette to efficiently check permissions across multiple resources at once. When a user requests a list of resources (like tables, documents, or other entities), Datasette uses this SQL to:
1. Get all resources of this type from your data catalog
2. Combine it with permission rules from the ``permission_resources_sql`` hook

View file

@ -41,7 +41,7 @@ def register_permissions(datasette):
),
]
```
The new `Action` does not have a `default=` parameter, and `takes_database` and `takes_resource` have been renamed to `takes_parent` and `takes_child. The new code would look like this:
The new `Action` does not have a `default=` parameter. For global actions (those that don't apply to specific resources), omit `resource_class`:
```python
from datasette.permissions import Action
@ -53,21 +53,41 @@ def register_actions(datasette):
name="datasette-pins-write",
abbr=None,
description="Can pin, unpin, and re-order pins for datasette-pins",
takes_parent=False,
takes_child=False,
default=False,
),
Action(
name="datasette-pins-read",
abbr=None,
description="Can read pinned items.",
takes_parent=False,
takes_child=False,
default=False,
),
]
```
For actions that apply to specific resources (like databases or tables), specify the `resource_class` instead of `takes_parent` and `takes_child`:
```python
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: