{"ok": true, "next": null, "rows": [{"id": "upgrade-1.0a20:fixing-async-with-httpx-asyncclient-app-app", "page": "upgrade-1.0a20", "ref": "fixing-async-with-httpx-asyncclient-app-app", "title": "Fixing async with httpx.AsyncClient(app=app)", "content": "Some older plugins may use the following pattern in their tests, which is no longer supported: \n             app = Datasette([], memory=True).app()\nasync with httpx.AsyncClient(app=app) as client:\n    response = await client.get(\"http://localhost/path\")\n \n             The new pattern is to use  ds.client  like this: \n             ds = Datasette([], memory=True)\nresponse = await ds.client.get(\"/path\")", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade-1.0a20:migrating-from-metadata-to-config", "page": "upgrade-1.0a20", "ref": "migrating-from-metadata-to-config", "title": "Migrating from metadata= to config=", "content": "Datasette 1.0 separates metadata (titles, descriptions, licenses) from configuration (settings, plugins, queries, permissions). Plugin tests and code need to be updated accordingly.", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade-1.0a20:permission-allowed-hook-is-replaced-by-permission-resources-sql", "page": "upgrade-1.0a20", "ref": "permission-allowed-hook-is-replaced-by-permission-resources-sql", "title": "permission_allowed() hook is replaced by permission_resources_sql()", "content": "The following old code: \n             @hookimpl\ndef permission_allowed(action):\n    if action == \"permissions-debug\":\n        return True\n \n             Can be replaced by: \n             from datasette.permissions import PermissionSQL\n\n@hookimpl\ndef permission_resources_sql(action):\n    return PermissionSQL.allow(reason=\"datasette-allow-permissions-debug\")\n \n             A  .deny(reason=\"\")  class method is also available. \n             For more complex permission checks consult the documentation for that plugin hook:\n                 https://docs.datasette.io/en/latest/plugin_hooks.html#permission-resources-sql-datasette-actor-action", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[{\"href\": \"https://docs.datasette.io/en/latest/plugin_hooks.html#permission-resources-sql-datasette-actor-action\", \"label\": \"https://docs.datasette.io/en/latest/plugin_hooks.html#permission-resources-sql-datasette-actor-action\"}]"}, {"id": "upgrade-1.0a20:permissions-are-now-actions", "page": "upgrade-1.0a20", "ref": "permissions-are-now-actions", "title": "Permissions are now actions", "content": "The  register_permissions()  hook shoud be replaced with  register_actions() . \n             Old code: \n             @hookimpl\ndef register_permissions(datasette):\n    return [\n        Permission(\n            name=\"explain-sql\",\n            abbr=None,\n            description=\"Can explain SQL queries\",\n            takes_database=True,\n            takes_resource=False,\n            default=False,\n        ),\n        Permission(\n            name=\"annotate-rows\",\n            abbr=None,\n            description=\"Can annotate rows\",\n            takes_database=True,\n            takes_resource=True,\n            default=False,\n        ),\n        Permission(\n            name=\"view-debug-info\",\n            abbr=None,\n            description=\"Can view debug information\",\n            takes_database=False,\n            takes_resource=False,\n            default=False,\n        ),\n    ]\n \n             The new  Action  does not have a  default=  parameter. \n             Here's the equivalent new code: \n             from datasette import hookimpl\nfrom datasette.permissions import Action\nfrom datasette.resources import DatabaseResource, TableResource\n\n@hookimpl\ndef register_actions(datasette):\n    return [\n        Action(\n            name=\"explain-sql\",\n            description=\"Explain SQL queries\",\n            resource_class=DatabaseResource,\n        ),\n        Action(\n            name=\"annotate-rows\",\n            description=\"Annotate rows\",\n            resource_class=TableResource,\n        ),\n        Action(\n            name=\"view-debug-info\",\n            description=\"View debug information\",\n        ),\n    ]\n \n             The  abbr=  is now optional and defaults to  None . \n             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.", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade-1.0a20:root-enabled-instances-during-testing", "page": "upgrade-1.0a20", "ref": "root-enabled-instances-during-testing", "title": "Root-enabled instances during testing", "content": "When writing tests that exercise root-only functionality, make sure to set  datasette.root_enabled = True  on the  Datasette  instance. Root permissions are only granted automatically when Datasette is started with  datasette --root  or when the flag is enabled directly in tests.", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\", \"Root user checks are no longer necessary\"]", "references": "[]"}, {"id": "upgrade-1.0a20:root-user-checks-are-no-longer-necessary", "page": "upgrade-1.0a20", "ref": "root-user-checks-are-no-longer-necessary", "title": "Root user checks are no longer necessary", "content": "Some plugins would introduce their own custom permission and then ensure the  \"root\"  actor had access to it using a pattern like this: \n             @hookimpl\ndef register_permissions(datasette):\n    return [\n        Permission(\n            name=\"upload-dbs\",\n            abbr=None,\n            description=\"Upload SQLite database files\",\n            takes_database=False,\n            takes_resource=False,\n            default=False,\n        )\n    ]\n\n\n@hookimpl\ndef permission_allowed(actor, action):\n    if action == \"upload-dbs\" and actor and actor.get(\"id\") == \"root\":\n        return True\n \n             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. \n             The  permission_allowed()  hook in this example can be entirely removed.", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade-1.0a20:target-the-new-apis-exclusively", "page": "upgrade-1.0a20", "ref": "target-the-new-apis-exclusively", "title": "Target the new APIs exclusively", "content": "Datasette 1.0a20\u2019s permission system is substantially different from previous releases. Attempting to keep plugin code compatible with both the old  permission_allowed()  and the new  allowed()  interfaces leads to brittle workarounds. Prefer to adopt the 1.0a20 APIs ( register_actions ,  permission_resources_sql() , and  datasette.allowed() ) outright and drop legacy fallbacks.", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade-1.0a20:update-datasette-metadata-calls", "page": "upgrade-1.0a20", "ref": "update-datasette-metadata-calls", "title": "Update datasette.metadata() calls", "content": "The  datasette.metadata()  method has been removed. Use these methods instead: \n                 Old code: \n                 try:\n    title = datasette.metadata(database=database)[\"queries\"][query_name][\"title\"]\nexcept (KeyError, TypeError):\n    pass\n \n                 New code: \n                 try:\n    query_info = await datasette.get_canned_query(database, query_name, request.actor)\n    if query_info and \"title\" in query_info:\n        title = query_info[\"title\"]\nexcept (KeyError, TypeError):\n    pass", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\", \"Migrating from metadata= to config=\"]", "references": "[]"}, {"id": "upgrade-1.0a20:update-query-urls-in-tests", "page": "upgrade-1.0a20", "ref": "update-query-urls-in-tests", "title": "Update query URLs in tests", "content": "Datasette now redirects  ?sql=  parameters from database pages to the query view: \n                 Old code: \n                 response = await ds.client.get(\"/_memory.atom?sql=select+1\")\n \n                 New code: \n                 response = await ds.client.get(\"/_memory/-/query.atom?sql=select+1\")", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\", \"Migrating from metadata= to config=\"]", "references": "[]"}, {"id": "upgrade-1.0a20:update-render-functions-to-async", "page": "upgrade-1.0a20", "ref": "update-render-functions-to-async", "title": "Update render functions to async", "content": "If your plugin's render function needs to call  datasette.get_canned_query()  or other async Datasette methods, it must be declared as async: \n                 Old code: \n                 def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data):\n    # ...\n    if query_name:\n        title = datasette.metadata(database=database)[\"queries\"][query_name][\"title\"]\n \n                 New code: \n                 async def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data):\n    # ...\n    if query_name:\n        query_info = await datasette.get_canned_query(database, query_name, request.actor)\n        if query_info and \"title\" in query_info:\n            title = query_info[\"title\"]", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\", \"Migrating from metadata= to config=\"]", "references": "[]"}, {"id": "upgrade-1.0a20:update-test-constructors", "page": "upgrade-1.0a20", "ref": "update-test-constructors", "title": "Update test constructors", "content": "Old code: \n                 ds = Datasette(\n    memory=True,\n    metadata={\n        \"databases\": {\n            \"_memory\": {\"queries\": {\"my_query\": {\"sql\": \"select 1\", \"title\": \"My Query\"}}}\n        },\n        \"plugins\": {\n            \"my-plugin\": {\"setting\": \"value\"}\n        }\n    }\n)\n \n                 New code: \n                 ds = Datasette(\n    memory=True,\n    config={\n        \"databases\": {\n            \"_memory\": {\"queries\": {\"my_query\": {\"sql\": \"select 1\", \"title\": \"My Query\"}}}\n        },\n        \"plugins\": {\n            \"my-plugin\": {\"setting\": \"value\"}\n        }\n    }\n)", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\", \"Migrating from metadata= to config=\"]", "references": "[]"}, {"id": "upgrade-1.0a20:using-datasette-allowed-to-check-permissions-instead-of-datasette-permission-allowed", "page": "upgrade-1.0a20", "ref": "using-datasette-allowed-to-check-permissions-instead-of-datasette-permission-allowed", "title": "Using datasette.allowed() to check permissions instead of datasette.permission_allowed()", "content": "The internal method  datasette.permission_allowed()  has been replaced by  datasette.allowed() . \n             The old method looked like this: \n             can_debug = await datasette.permission_allowed(\n    request.actor,\n    \"view-debug-info\",\n)\ncan_explain_sql = await datasette.permission_allowed(\n    request.actor,\n    \"explain-sql\",\n    resource=\"database_name\",\n)\ncan_annotate_rows = await datasette.permission_allowed(\n    request.actor,\n    \"annotate-rows\",\n    resource=(database_name, table_name),\n)\n \n             Note the confusing design here where  resource  could be either a string or a tuple depending on the permission being checked. \n             The new keyword-only design makes this a lot more clear: \n             from datasette.resources import DatabaseResource, TableResource\ncan_debug = await datasette.allowed(\n    actor=request.actor,\n    action=\"view-debug-info\",\n)\ncan_explain_sql = await datasette.allowed(\n    actor=request.actor,\n    action=\"explain-sql\",\n    resource=DatabaseResource(database_name),\n)\ncan_annotate_rows = await datasette.allowed(\n    actor=request.actor,\n    action=\"annotate-rows\",\n    resource=TableResource(database_name, table_name),\n)", "breadcrumbs": "[\"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade_guide:breaking-changes", "page": "upgrade_guide", "ref": "breaking-changes", "title": "Breaking changes", "content": "The  skip_csrf  plugin hook has been removed.  Existing plugins that still declare a  skip_csrf  hookimpl will continue to load - pluggy silently ignores unknown hook names - but the hook is no longer consulted by core, so the flows it previously unlocked will now be blocked (or allowed) purely on the basis of the new header check. \n                             The new middleware already covers the common cases that  skip_csrf  was written for: \n                             \n                                 \n                                     Browser-initiated JSON POSTs automatically get  Sec-Fetch-Site: same-origin  and pass the check. \n                                 \n                                 \n                                     Non-browser API clients (curl,  requests , server-to-server scripts) do not send browser security headers and are passed through. \n                                 \n                                 \n                                     Requests with an explicit  Authorization: Bearer ...  header are exempt from the CSRF check (see above). \n                                 \n                             \n                             If your plugin previously used  skip_csrf  to accept cross-origin browser POSTs, replace that flow with an authentication mechanism that does  not  rely on ambient browser credentials. Safe patterns include: \n                             \n                                 \n                                     Requiring an  Authorization: Bearer ...  API token on the endpoint. \n                                 \n                                 \n                                     Requiring a non-ambient credential in the request body (a webhook secret, HMAC signature, signed capability URL, OAuth client credential, or similar). \n                                 \n                                 \n                                     Issuing a short-lived signed URL that encodes the actor, the action, and an expiry, and verifying the signature on request. \n                                 \n                             \n                             Do not rely on the  ds_csrftoken  cookie for your own plugin's security checks - Datasette no longer sets or validates it, and the  request.scope[\"csrftoken\"]()  compatibility shim now returns a fresh random value each request rather than the signed cookie-bound value it used to. \n                         \n                         \n                             The  asgi-csrf  dependency has been dropped.  Any plugin that imported from  asgi_csrf  directly will need to be updated. \n                         \n                         \n                             The  csrf_error.html  template now receives a  reason  context variable  instead of  message_id  and  message_name . Custom overrides of this template should be updated.", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\", \"CSRF protection is now header-based\"]", "references": "[]"}, {"id": "upgrade_guide:fixing-async-with-httpx-asyncclient-app-app", "page": "upgrade_guide", "ref": "fixing-async-with-httpx-asyncclient-app-app", "title": "Fixing async with httpx.AsyncClient(app=app)", "content": "Some older plugins may use the following pattern in their tests, which is no longer supported: \n                 app = Datasette([], memory=True).app()\nasync with httpx.AsyncClient(app=app) as client:\n    response = await client.get(\"http://localhost/path\")\n \n                 The new pattern is to use  ds.client  like this: \n                 ds = Datasette([], memory=True)\nresponse = await ds.client.get(\"/path\")", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade_guide:migrating-from-metadata-to-config", "page": "upgrade_guide", "ref": "migrating-from-metadata-to-config", "title": "Migrating from metadata= to config=", "content": "Datasette 1.0 separates metadata (titles, descriptions, licenses) from configuration (settings, plugins, queries, permissions). Plugin tests and code need to be updated accordingly.", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade_guide:permission-allowed-hook-is-replaced-by-permission-resources-sql", "page": "upgrade_guide", "ref": "permission-allowed-hook-is-replaced-by-permission-resources-sql", "title": "permission_allowed() hook is replaced by permission_resources_sql()", "content": "The following old code: \n                 @hookimpl\ndef permission_allowed(action):\n    if action == \"permissions-debug\":\n        return True\n \n                 Can be replaced by: \n                 from datasette.permissions import PermissionSQL\n\n@hookimpl\ndef permission_resources_sql(action):\n    return PermissionSQL.allow(reason=\"datasette-allow-permissions-debug\")\n \n                 A  .deny(reason=\"\")  class method is also available. \n                 For more complex permission checks consult the documentation for that plugin hook:\n                     https://docs.datasette.io/en/latest/plugin_hooks.html#permission-resources-sql-datasette-actor-action", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[{\"href\": \"https://docs.datasette.io/en/latest/plugin_hooks.html#permission-resources-sql-datasette-actor-action\", \"label\": \"https://docs.datasette.io/en/latest/plugin_hooks.html#permission-resources-sql-datasette-actor-action\"}]"}, {"id": "upgrade_guide:permissions-are-now-actions", "page": "upgrade_guide", "ref": "permissions-are-now-actions", "title": "Permissions are now actions", "content": "The  register_permissions()  hook shoud be replaced with  register_actions() . \n                 Old code: \n                 @hookimpl\ndef register_permissions(datasette):\n    return [\n        Permission(\n            name=\"explain-sql\",\n            abbr=None,\n            description=\"Can explain SQL queries\",\n            takes_database=True,\n            takes_resource=False,\n            default=False,\n        ),\n        Permission(\n            name=\"annotate-rows\",\n            abbr=None,\n            description=\"Can annotate rows\",\n            takes_database=True,\n            takes_resource=True,\n            default=False,\n        ),\n        Permission(\n            name=\"view-debug-info\",\n            abbr=None,\n            description=\"Can view debug information\",\n            takes_database=False,\n            takes_resource=False,\n            default=False,\n        ),\n    ]\n \n                 The new  Action  does not have a  default=  parameter. \n                 Here's the equivalent new code: \n                 from datasette import hookimpl\nfrom datasette.permissions import Action\nfrom datasette.resources import DatabaseResource, TableResource\n\n@hookimpl\ndef register_actions(datasette):\n    return [\n        Action(\n            name=\"explain-sql\",\n            description=\"Explain SQL queries\",\n            resource_class=DatabaseResource,\n        ),\n        Action(\n            name=\"annotate-rows\",\n            description=\"Annotate rows\",\n            resource_class=TableResource,\n        ),\n        Action(\n            name=\"view-debug-info\",\n            description=\"View debug information\",\n        ),\n    ]\n \n                 The  abbr=  is now optional and defaults to  None . \n                 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.", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade_guide:root-enabled-instances-during-testing", "page": "upgrade_guide", "ref": "root-enabled-instances-during-testing", "title": "Root-enabled instances during testing", "content": "When writing tests that exercise root-only functionality, make sure to set  datasette.root_enabled = True  on the  Datasette  instance. Root permissions are only granted automatically when Datasette is started with  datasette --root  or when the flag is enabled directly in tests.", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\", \"Root user checks are no longer necessary\"]", "references": "[]"}, {"id": "upgrade_guide:root-user-checks-are-no-longer-necessary", "page": "upgrade_guide", "ref": "root-user-checks-are-no-longer-necessary", "title": "Root user checks are no longer necessary", "content": "Some plugins would introduce their own custom permission and then ensure the  \"root\"  actor had access to it using a pattern like this: \n                 @hookimpl\ndef register_permissions(datasette):\n    return [\n        Permission(\n            name=\"upload-dbs\",\n            abbr=None,\n            description=\"Upload SQLite database files\",\n            takes_database=False,\n            takes_resource=False,\n            default=False,\n        )\n    ]\n\n\n@hookimpl\ndef permission_allowed(actor, action):\n    if action == \"upload-dbs\" and actor and actor.get(\"id\") == \"root\":\n        return True\n \n                 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. \n                 The  permission_allowed()  hook in this example can be entirely removed.", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade_guide:security-properties", "page": "upgrade_guide", "ref": "security-properties", "title": "Security properties", "content": "For defense-in-depth the  ds_actor  and  ds_messages  cookies continue to be set with  SameSite=Lax  (Datasette's long-standing default). This means a genuine cross-site POST from an attacker's page would arrive without the user's authentication cookie even if the header check somehow failed.", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\", \"CSRF protection is now header-based\"]", "references": "[]"}, {"id": "upgrade_guide:target-the-new-apis-exclusively", "page": "upgrade_guide", "ref": "target-the-new-apis-exclusively", "title": "Target the new APIs exclusively", "content": "Datasette 1.0a20\u2019s permission system is substantially different from previous releases. Attempting to keep plugin code compatible with both the old  permission_allowed()  and the new  allowed()  interfaces leads to brittle workarounds. Prefer to adopt the 1.0a20 APIs ( register_actions ,  permission_resources_sql() , and  datasette.allowed() ) outright and drop legacy fallbacks.", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade_guide:update-datasette-metadata-calls", "page": "upgrade_guide", "ref": "update-datasette-metadata-calls", "title": "Update datasette.metadata() calls", "content": "The  datasette.metadata()  method has been removed. Use these methods instead: \n                     Old code: \n                     try:\n    title = datasette.metadata(database=database)[\"queries\"][query_name][\"title\"]\nexcept (KeyError, TypeError):\n    pass\n \n                     New code: \n                     try:\n    query_info = await datasette.get_canned_query(database, query_name, request.actor)\n    if query_info and \"title\" in query_info:\n        title = query_info[\"title\"]\nexcept (KeyError, TypeError):\n    pass", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\", \"Migrating from metadata= to config=\"]", "references": "[]"}, {"id": "upgrade_guide:update-query-urls-in-tests", "page": "upgrade_guide", "ref": "update-query-urls-in-tests", "title": "Update query URLs in tests", "content": "Datasette now redirects  ?sql=  parameters from database pages to the query view: \n                     Old code: \n                     response = await ds.client.get(\"/_memory.atom?sql=select+1\")\n \n                     New code: \n                     response = await ds.client.get(\"/_memory/-/query.atom?sql=select+1\")", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\", \"Migrating from metadata= to config=\"]", "references": "[]"}, {"id": "upgrade_guide:update-render-functions-to-async", "page": "upgrade_guide", "ref": "update-render-functions-to-async", "title": "Update render functions to async", "content": "If your plugin's render function needs to call  datasette.get_canned_query()  or other async Datasette methods, it must be declared as async: \n                     Old code: \n                     def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data):\n    # ...\n    if query_name:\n        title = datasette.metadata(database=database)[\"queries\"][query_name][\"title\"]\n \n                     New code: \n                     async def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data):\n    # ...\n    if query_name:\n        query_info = await datasette.get_canned_query(database, query_name, request.actor)\n        if query_info and \"title\" in query_info:\n            title = query_info[\"title\"]", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\", \"Migrating from metadata= to config=\"]", "references": "[]"}, {"id": "upgrade_guide:update-test-constructors", "page": "upgrade_guide", "ref": "update-test-constructors", "title": "Update test constructors", "content": "Old code: \n                     ds = Datasette(\n    memory=True,\n    metadata={\n        \"databases\": {\n            \"_memory\": {\"queries\": {\"my_query\": {\"sql\": \"select 1\", \"title\": \"My Query\"}}}\n        },\n        \"plugins\": {\n            \"my-plugin\": {\"setting\": \"value\"}\n        }\n    }\n)\n \n                     New code: \n                     ds = Datasette(\n    memory=True,\n    config={\n        \"databases\": {\n            \"_memory\": {\"queries\": {\"my_query\": {\"sql\": \"select 1\", \"title\": \"My Query\"}}}\n        },\n        \"plugins\": {\n            \"my-plugin\": {\"setting\": \"value\"}\n        }\n    }\n)", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\", \"Migrating from metadata= to config=\"]", "references": "[]"}, {"id": "upgrade_guide:upgrade-guide-csrf", "page": "upgrade_guide", "ref": "upgrade-guide-csrf", "title": "CSRF protection is now header-based", "content": "Datasette's Cross-Site Request Forgery protection no longer uses tokens. The previous  asgi-csrf  mechanism - which set a  ds_csrftoken  cookie and required a matching  <input type=\"hidden\" name=\"csrftoken\">  in every form - has been replaced with an ASGI middleware that inspects the browser-set  Sec-Fetch-Site  and  Origin  headers, following the approach described in  Filippo Valsorda's research  and implemented in Go 1.25's  http.CrossOriginProtection . \n                 This works identically on HTTPS, HTTP, and localhost. Non-browser clients (curl, Python  requests , server-to-server scripts) do not send  Sec-Fetch-Site  or  Origin  and are passed through unchanged - CSRF is a browser-only attack. \n                 Requests that carry an explicit  Authorization: Bearer ...  header are also exempt from the CSRF check, because bearer tokens are not ambient browser credentials: a malicious cross-origin page cannot cause the browser to attach a target site's bearer token unless the attacker's JavaScript already possesses it. This exemption is narrow - it covers the  Bearer  scheme only, not  Basic  or  Digest  - and it does not depend on the  --cors  setting. The exemption is about CSRF classification, not browser read access; CORS still controls the latter.", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[{\"href\": \"https://words.filippo.io/csrf/\", \"label\": \"Filippo Valsorda's research\"}]"}, {"id": "upgrade_guide:upgrade-guide-v1-a25", "page": "upgrade_guide", "ref": "upgrade-guide-v1-a25", "title": "Datasette 1.0a25: ", "content": "datasette.create_token()  is now an  async  method (previously it was synchronous). The  restrict_all ,  restrict_database , and  restrict_resource  keyword arguments have been replaced by a single  restrictions  parameter that accepts a  TokenRestrictions  object. \n                 Old code: \n                 token = datasette.create_token(\n    actor_id=\"user1\",\n    restrict_all=[\"view-instance\", \"view-table\"],\n    restrict_database={\"docs\": [\"view-query\"]},\n    restrict_resource={\n        \"docs\": {\n            \"attachments\": [\"insert-row\", \"update-row\"]\n        }\n    },\n)\n \n                 New code: \n                 from datasette.tokens import TokenRestrictions\n\ntoken = await datasette.create_token(\n    actor_id=\"user1\",\n    restrictions=(\n        TokenRestrictions()\n        .allow_all(\"view-instance\")\n        .allow_all(\"view-table\")\n        .allow_database(\"docs\", \"view-query\")\n        .allow_resource(\"docs\", \"attachments\", \"insert-row\")\n        .allow_resource(\"docs\", \"attachments\", \"update-row\")\n    ),\n)\n \n                 The  datasette create-token  CLI command is unchanged.", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade_guide:using-datasette-allowed-to-check-permissions-instead-of-datasette-permission-allowed", "page": "upgrade_guide", "ref": "using-datasette-allowed-to-check-permissions-instead-of-datasette-permission-allowed", "title": "Using datasette.allowed() to check permissions instead of datasette.permission_allowed()", "content": "The internal method  datasette.permission_allowed()  has been replaced by  datasette.allowed() . \n                 The old method looked like this: \n                 can_debug = await datasette.permission_allowed(\n    request.actor,\n    \"view-debug-info\",\n)\ncan_explain_sql = await datasette.permission_allowed(\n    request.actor,\n    \"explain-sql\",\n    resource=\"database_name\",\n)\ncan_annotate_rows = await datasette.permission_allowed(\n    request.actor,\n    \"annotate-rows\",\n    resource=(database_name, table_name),\n)\n \n                 Note the confusing design here where  resource  could be either a string or a tuple depending on the permission being checked. \n                 The new keyword-only design makes this a lot more clear: \n                 from datasette.resources import DatabaseResource, TableResource\ncan_debug = await datasette.allowed(\n    actor=request.actor,\n    action=\"view-debug-info\",\n)\ncan_explain_sql = await datasette.allowed(\n    actor=request.actor,\n    action=\"explain-sql\",\n    resource=DatabaseResource(database_name),\n)\ncan_annotate_rows = await datasette.allowed(\n    actor=request.actor,\n    action=\"annotate-rows\",\n    resource=TableResource(database_name, table_name),\n)", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\"]", "references": "[]"}, {"id": "upgrade_guide:what-you-can-remove", "page": "upgrade_guide", "ref": "what-you-can-remove", "title": "What you can remove", "content": "You can now delete any of the following from your plugins and custom templates: \n                     \n                         \n                             Hidden CSRF form fields: \n                             <input type=\"hidden\" name=\"csrftoken\" value=\"{{ csrftoken() }}\">\n \n                             The  csrftoken()  template helper (and  request.scope[\"csrftoken\"]()  for plugins that call it from Python) still exists as a compatibility shim. It now returns a per-request random string rather than a cookie-bound signed value. Datasette no longer validates this token, and no  ds_csrftoken  cookie is set. \n                             Important for plugin authors:  if your plugin previously used  request.scope[\"csrftoken\"]()  or the  ds_csrftoken  cookie as a security primitive (for example, signing a URL and later comparing it to the cookie), the invariant that the token equals  request.cookies[\"ds_csrftoken\"]  no longer holds. Replace those flows with signed, short-lived action URLs or explicit non-ambient credentials. \n                         \n                         \n                             Manual CSRF token extraction in tests, e.g.: \n                             # No longer needed\ncsrftoken = response.cookies[\"ds_csrftoken\"]\ncookies[\"ds_csrftoken\"] = csrftoken\npost_data[\"csrftoken\"] = csrftoken\n \n                             The  ds_csrftoken  cookie is no longer set at all. The  csrftoken_from=  argument of the Datasette test client's  .post()  method is now a no-op and can be removed from your test code.", "breadcrumbs": "[\"Upgrade guide\", \"Datasette 1.0a20 plugin upgrade guide\", \"CSRF protection is now header-based\"]", "references": "[]"}], "truncated": false}