Completed upgrade guide, closes #2564

This commit is contained in:
Simon Willison 2025-11-01 18:57:56 -07:00
commit b8cee8768e

View file

@ -6,10 +6,10 @@ orphan: true
<!-- START UPGRADE 1.0a20 --> <!-- START UPGRADE 1.0a20 -->
Datasette 1.0a20 makes some breaking changes to Datasette's permission system. Plugins need to be updated if they use any of the following: 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 `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 `permission_allowed()` plugin hook - this should be upgraded to use `permission_resources_sql()`.
- The `datasette.permission_allowed()` internal method - this should be replaced with `datasette.allowed()` - The `datasette.permission_allowed()` internal method - this should be replaced with `datasette.allowed()`
- Logic that grants access to the `"root"` actor can be removed. - Logic that grants access to the `"root"` actor can be removed.
@ -24,47 +24,37 @@ Old code:
def register_permissions(datasette): def register_permissions(datasette):
return [ return [
Permission( Permission(
name="datasette-pins-write", name="explain-sql",
abbr=None, abbr=None,
description="Can pin, unpin, and re-order pins for datasette-pins", description="Can explain SQL queries",
takes_database=False, takes_database=True,
takes_resource=False, takes_resource=False,
default=False, default=False,
), ),
Permission( Permission(
name="datasette-pins-read", name="annotate-rows",
abbr=None, abbr=None,
description="Can read pinned items.", description="Can annotate rows",
takes_database=True,
takes_resource=True,
default=False,
),
Permission(
name="view-debug-info",
abbr=None,
description="Can view debug information",
takes_database=False, takes_database=False,
takes_resource=False, takes_resource=False,
default=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`: The new `Action` does not have a `default=` parameter.
```python Here's the equivalent new code:
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`:
```python ```python
from datasette import hookimpl
from datasette.permissions import Action from datasette.permissions import Action
from datasette.resources import DatabaseResource, TableResource from datasette.resources import DatabaseResource, TableResource
@ -72,21 +62,26 @@ from datasette.resources import DatabaseResource, TableResource
def register_actions(datasette): def register_actions(datasette):
return [ return [
Action( Action(
name="execute-sql", name="explain-sql",
abbr="es", abbr=None,
description="Execute SQL queries", description="Explain SQL queries",
resource_class=DatabaseResource, # Parent-level resource resource_class=DatabaseResource,
), ),
Action( Action(
name="insert-row", name="annotate-rows",
abbr="ir", abbr=None,
description="Insert rows", description="Annotate rows",
resource_class=TableResource, # Child-level resource resource_class=TableResource,
),
Action(
name="view-debug-info",
abbr=None,
description="View debug information",
), ),
] ]
``` ```
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. For actions that apply to specific resources (like databases or tables), specify the `resource_class` instead of `takes_parent` and `takes_child`. Note that `view-debug-info` does not specify a `resource_class` because it applies globally.
## permission_allowed() hook is replaced by permission_resources_sql() ## permission_allowed() hook is replaced by permission_resources_sql()
@ -110,6 +105,76 @@ A `.deny(reason="")` class method is also available.
For more complex permission checks consult the documentation for that plugin hook: 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> <https://docs.datasette.io/en/latest/plugin_hooks.html#permission-resources-sql-datasette-actor-action>
## Using datasette.allowed() to check permissions instead of datasette.permission_allowed()
The internal method `datasette.permission_allowed()` has been replaced by `datasette.allowed()`.
The old method looked like this:
```python
can_debug = await datasette.permission_allowed(
request.actor,
"view-debug-info",
)
can_explain_sql = await datasette.permission_allowed(
request.actor,
"explain-sql",
resource="database_name",
)
can_annotate_rows = await datasette.permission_allowed(
request.actor,
"annotate-rows",
resource=(database_name, table_name),
)
```
Note the confusing design here where `resource` could be either a string or a tuple depending on the permission being checked.
The new keyword-only design makes this a lot more clear:
```python
from datasette.resources import DatabaseResource, TableResource
can_debug = await datasette.allowed(
actor=request.actor,
action="view-debug-info",
)
can_explain_sql = await datasette.allowed(
actor=request.actor,
action="explain-sql",
resource=DatabaseResource(database_name),
)
can_annotate_rows = await datasette.allowed(
actor=request.actor,
action="annotate-rows",
resource=TableResource(database_name, table_name),
)
```
## Root user checks are no longer necessary
Some plugins would introduce their own custom permission and then ensure the `"root"` actor had access to it using a pattern like this:
```python
@hookimpl
def register_permissions(datasette):
return [
Permission(
name="upload-dbs",
abbr=None,
description="Upload SQLite database files",
takes_database=False,
takes_resource=False,
default=False,
)
]
@hookimpl
def permission_allowed(actor, action):
if action == "upload-dbs" and actor and actor.get("id") == "root":
return True
```
This is no longer necessary in Datasette 1.0a20 - the `"root"` actor automatically has all permissions when Datasette is started with the `datasette --root` option.
The `permission_allowed()` hook in this example can be entirely removed.
## Fixing async with httpx.AsyncClient(app=app) ## Fixing async with httpx.AsyncClient(app=app)
Some older plugins may use the following pattern in their tests, which is no longer supported: Some older plugins may use the following pattern in their tests, which is no longer supported:
@ -121,5 +186,5 @@ async with httpx.AsyncClient(app=app) as client:
The new pattern is to use `ds.client` like this: The new pattern is to use `ds.client` like this:
```python ```python
ds = Datasette([], memory=True) ds = Datasette([], memory=True)
response = ds.client.get("/path") response = await ds.client.get("/path")
``` ```