From e4be95b16c99dd8d49fc6d89a684764bf731f1d9 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 30 Oct 2025 17:59:54 -0700 Subject: [PATCH] Update permissions documentation for new action system (#2551) --- datasette/app.py | 8 +- docs/authentication.rst | 235 ++++++++++++++++++++-------------------- docs/changelog.rst | 8 +- docs/internals.rst | 76 +++++++------ docs/json_api.rst | 10 +- tests/conftest.py | 8 +- 6 files changed, 177 insertions(+), 168 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index bfbf2360..15cf3495 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -1087,11 +1087,7 @@ class Datasette: # Validate that resource is a Resource object or None if resource is not None and not isinstance(resource, Resource): - raise TypeError( - f"resource must be a Resource object or None, not {type(resource).__name__}. " - f"Use DatabaseResource(database=...), TableResource(database=..., table=...), " - f"or QueryResource(database=..., query=...) instead." - ) + raise TypeError(f"resource must be a Resource subclass instance or None.") # Check if actor can see it if not await self.allowed(action=action, resource=resource, actor=actor): @@ -1122,7 +1118,7 @@ class Datasette: parent: Optional parent filter (e.g., database name) to limit results include_is_private: If True, include is_private column showing if anonymous cannot access - Returns a tuple of (query, params) that can be executed against the internal database. + Returns a tuple of (query: str, params: dict) that can be executed against the internal database. The query returns rows with (parent, child, reason) columns, plus is_private if requested. Example: diff --git a/docs/authentication.rst b/docs/authentication.rst index e658e78b..28fb76bb 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -6,18 +6,18 @@ Datasette doesn't require authentication by default. Any visitor to a Datasette instance can explore the full data and execute read-only SQL queries. -Datasette's plugin system can be used to add many different styles of authentication, such as user accounts, single sign-on or API keys. +Datasette can be configured to only allow authenticated users, or to control which databases, tables, and queries can be accessed by the public or by specific users. Datasette's plugin system can be used to add many different styles of authentication, such as user accounts, single sign-on or API keys. .. _authentication_actor: Actors ====== -Through plugins, Datasette can support both authenticated users (with cookies) and authenticated API agents (via authentication tokens). The word "actor" is used to cover both of these cases. +Through plugins, Datasette can support both authenticated users (with cookies) and authenticated API clients (via authentication tokens). The word "actor" is used to cover both of these cases. -Every request to Datasette has an associated actor value, available in the code as ``request.actor``. This can be ``None`` for unauthenticated requests, or a JSON compatible Python dictionary for authenticated users or API agents. +Every request to Datasette has an associated actor value, available in the code as ``request.actor``. This can be ``None`` for unauthenticated requests, or a JSON compatible Python dictionary for authenticated users or API clients. -The actor dictionary can be any shape - the design of that data structure is left up to the plugins. A useful convention is to include an ``"id"`` string, as demonstrated by the "root" actor below. +The actor dictionary can be any shape - the design of that data structure is left up to the plugins. Actors should always include a unique ``"id"`` string, as demonstrated by the "root" actor below. Plugins can use the :ref:`plugin_hook_actor_from_request` hook to implement custom logic for authenticating an actor based on the incoming HTTP request. @@ -32,19 +32,21 @@ The one exception is the "root" account, which you can sign into while using Dat The ``--root`` flag is designed for local development and testing. When you start Datasette with ``--root``, the root user automatically receives every permission, including: -* All view permissions (view-instance, view-database, view-table, etc.) -* All write permissions (insert-row, update-row, delete-row, create-table, alter-table, drop-table) -* Debug permissions (permissions-debug, debug-menu) +* All view permissions (``view-instance``, ``view-database``, ``view-table``, etc.) +* All write permissions (``insert-row``, ``update-row``, ``delete-row``, ``create-table``, ``alter-table``, ``drop-table``) +* Debug permissions (``permissions-debug``, ``debug-menu``) * Any custom permissions defined by plugins -.. warning:: - The ``--root`` flag should only be used for local development. Never use it in production or on publicly accessible servers. +If you add explicit deny rules in ``datasette.yaml`` those can still block the +root actor from specific databases or tables. + +The ``--root`` flag sets an internal ``root_enabled`` switch—without it, a signed-in user with ``{"id": "root"}`` is treated like any other actor. To sign in as root, start Datasette using the ``--root`` command-line option, like this:: datasette --root -:: +Datasette will output a single-use-only login URL on startup:: http://127.0.0.1:8001/-/auth-token?token=786fc524e0199d70dc9a581d851f466244e114ca92f33aa3b42a139e9388daa7 INFO: Started server process [25801] @@ -52,7 +54,7 @@ To sign in as root, start Datasette using the ``--root`` command-line option, li INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8001 (Press CTRL+C to quit) -The URL on the first line includes a one-use token which can be used to sign in as the "root" actor in your browser. Click on that link and then visit ``http://127.0.0.1:8001/-/actor`` to confirm that you are authenticated as an actor that looks like this: +Click on that link and then visit ``http://127.0.0.1:8001/-/actor`` to confirm that you are authenticated as an actor that looks like this: .. code-block:: json @@ -65,7 +67,7 @@ The URL on the first line includes a one-use token which can be used to sign in Permissions =========== -Datasette has an extensive permissions system built-in, which can be further extended and customized by plugins. +Datasette's permissions system is built around SQL queries. Datasette and its plugins construct SQL queries to resolve the list of resources that an actor cas access. The key question the permissions system answers is this: @@ -73,37 +75,47 @@ The key question the permissions system answers is this: **Actors** are :ref:`described above `. -An **action** is a string describing the action the actor would like to perform. A full list is :ref:`provided below ` - examples include ``view-table`` and ``execute-sql``. +An **action** is a string describing the action the actor would like to perform. A full list is :ref:`provided below ` - examples include ``view-table`` and ``execute-sql``. A **resource** is the item the actor wishes to interact with - for example a specific database or table. Some actions, such as ``permissions-debug``, are not associated with a particular resource. -Datasette's built-in view permissions (``view-database``, ``view-table`` etc) default to *allow* - unless you :ref:`configure additional permission rules ` unauthenticated users will be allowed to access content. +Datasette's built-in view actions (``view-database``, ``view-table`` etc) are allowed by Datasette's default configuration: unless you :ref:`configure additional permission rules ` unauthenticated users will be allowed to access content. -Permissions with potentially harmful effects should default to *deny*. Plugin authors should account for this when designing new plugins - for example, the `datasette-upload-csvs `__ plugin defaults to deny so that installations don't accidentally allow unauthenticated users to create new tables by uploading a CSV file. +Other actions, including those introduced by plugins, will default to *deny*. .. _authentication_permissions_explained: How permissions are resolved ---------------------------- -The :ref:`datasette.permission_allowed(actor, action, resource=None, default=...)` method is called to check if an actor is allowed to perform a specific action. +Datasette performs permission checks using the internal :ref:`datasette_allowed`, method which accepts keyword arguments for ``action``, ``resource`` and an optional ``actor``. -This method asks every plugin that implements the :ref:`plugin_hook_permission_allowed` hook if the actor is allowed to perform the action. +``resource`` should be an instance of the appropriate ``Resource`` subclass from :mod:`datasette.resources`—for example ``InstanceResource()``, ``DatabaseResource(database="...``)`` or ``TableResource(database="...", table="...")``. This defaults to ``InstanceResource()`` if not specified. -Each plugin can return ``True`` to indicate that the actor is allowed to perform the action, ``False`` if they are not allowed and ``None`` if the plugin has no opinion on the matter. +When a check runs Datasette gathers allow/deny rules from multiple sources and +compiles them into a SQL query. The resulting query describes all of the +resources an actor may access for that action, together with the reasons those +resources were allowed or denied. The combined sources are: -``False`` acts as a veto - if any plugin returns ``False`` then the permission check is denied. Otherwise, if any plugin returns ``True`` then the permission check is allowed. +* ``allow`` blocks configured in :ref:`datasette.yaml `. +* :ref:`Actor restrictions ` encoded into the actor dictionary or API token. +* The "root" user shortcut when ``--root`` (or :attr:`Datasette.root_enabled `) is active, replying ``True`` to all permission chucks unless configuration rules deny them at a more specific level. +* Any additional SQL provided by plugins implementing :ref:`plugin_hook_permission_resources_sql`. -The ``resource`` argument can be used to specify a specific resource that the action is being performed against. Some permissions, such as ``view-instance``, do not involve a resource. Others such as ``view-database`` have a resource that is a string naming the database. Permissions that take both a database name and the name of a table, view or canned query within that database use a resource that is a tuple of two strings, ``(database_name, resource_name)``. - -Plugins that implement the ``permission_allowed()`` hook can decide if they are going to consider the provided resource or not. +Datasette evaluates the SQL to determine if the requested ``resource`` is +included. Explicit deny rules returned by configuration or plugins will block +access even if other rules allowed it. .. _authentication_permissions_allow: Defining permissions with "allow" blocks ---------------------------------------- -The standard way to define permissions in Datasette is to use an ``"allow"`` block :ref:`in the datasette.yaml file `. This is a JSON document describing which actors are allowed to perform a permission. +One way to define permissions in Datasette is to use an ``"allow"`` block :ref:`in the datasette.yaml file `. This is a JSON document describing which actors are allowed to perform an action against a specific resource. + +Each ``allow`` block is compiled into SQL and combined with any +:ref:`plugin-provided rules ` to produce +the cascading allow/deny decisions that power :ref:`datasette_allowed`. The most basic form of allow block is this (`allow demo `__, `deny demo `__): @@ -425,7 +437,7 @@ You can control the following: * Access to specific tables and views * Access to specific :ref:`canned_queries` -If a user cannot access a specific database, they will not be able to access tables, views or queries within that database. If a user cannot access the instance they will not be able to access any of the databases, tables, views or queries. +If a user has permission to view a table they will be able to view that table, independent of if they have permission to view the database or instance that the table exists within. .. _authentication_permissions_instance: @@ -663,7 +675,7 @@ Controlling the ability to execute arbitrary SQL Datasette defaults to allowing any site visitor to execute their own custom SQL queries, for example using the form on `the database page `__ or by appending a ``?_where=`` parameter to the table page `like this `__. -Access to this ability is controlled by the :ref:`permissions_execute_sql` permission. +Access to this ability is controlled by the :ref:`actions_execute_sql` permission. The easiest way to disable arbitrary SQL queries is using the :ref:`default_allow_sql setting ` when you first start Datasette running. @@ -1027,9 +1039,25 @@ This example outputs the following:: Checking permissions in plugins =============================== -Datasette plugins can check if an actor has permission to perform an action using the :ref:`datasette.permission_allowed(...)` method. +Datasette plugins can check if an actor has permission to perform an action using :ref:`datasette_allowed`—for example:: -Datasette core performs a number of permission checks, :ref:`documented below `. Plugins can implement the :ref:`plugin_hook_permission_allowed` plugin hook to participate in decisions about whether an actor should be able to perform a specified action. + from datasette.resources import TableResource + + can_edit = await datasette.allowed( + action="update-row", + resource=TableResource(database="fixtures", table="facetable"), + actor=request.actor, + ) + +Use :ref:`datasette_ensure_permission` when you need to enforce a permission and +raise a ``Forbidden`` error automatically. + +Plugins that define new operations should return :class:`~datasette.permissions.Action` +objects from :ref:`plugin_register_actions` and can supply additional allow/deny +rules by returning :class:`~datasette.permissions.PermissionSQL` objects from the +:ref:`plugin_hook_permission_resources_sql` hook. Those rules are merged with +configuration ``allow`` blocks and actor restrictions to determine the final +result for each check. .. _authentication_actor_matches_allow: @@ -1049,12 +1077,14 @@ The currently authenticated actor is made available to plugins as ``request.acto .. _PermissionsDebugView: -The permissions debug tool -========================== +Permissions debug tools +======================= -The debug tool at ``/-/permissions`` is only available to the :ref:`authenticated root user ` (or any actor granted the ``permissions-debug`` action). +The debug tool at ``/-/permissions`` is available to any actor with the ``permissions-debug`` permission. By default this is just the :ref:`authenticated root user ` but you can open it up to all users by starting Datasette like this:: -It shows the thirty most recent permission checks that have been carried out by the Datasette instance. + datasette -s permissions.permissions-debug true data.db + +The page shows the permission checks that have been carried out by the Datasette instance. It also provides an interface for running hypothetical permission checks against a hypothetical actor. This is a useful way of confirming that your configured permissions work in the way you expect. @@ -1063,37 +1093,20 @@ This is designed to help administrators and plugin authors understand exactly ho .. _AllowedResourcesView: Allowed resources view -====================== +---------------------- -The ``/-/allowed`` endpoint displays resources that the current actor can access for a supplied ``action`` query string argument. +The ``/-/allowed`` endpoint displays resources that the current actor can access for a specified ``action``. This endpoint provides an interactive HTML form interface. Add ``.json`` to the URL path (e.g. ``/-/allowed.json``) to get the raw JSON response instead. Pass ``?action=view-table`` (or another action) to select the action. Optional ``parent=`` and ``child=`` query parameters can narrow the results to a specific database/table pair. -This endpoint is publicly accessible to help users understand their own permissions. However, potentially sensitive fields (``reason`` and ``source_plugin``) are only included in responses for users with the ``permissions-debug`` permission. - -Datasette includes helper endpoints for exploring the action-based permission resolver: - -``/-/allowed`` - Returns a paginated list of resources that the current actor is allowed to access for a given action. Pass ``?action=view-table`` (or another action) to select the action, and optional ``parent=``/``child=`` query parameters to narrow the results to a specific database/table pair. - -``/-/rules`` - Lists the raw permission rules (both allow and deny) contributing to each resource for the supplied action. This includes configuration-derived and plugin-provided rules. **Requires the permissions-debug permission** (only available to the root user by default). - -``/-/check`` - Evaluates whether the current actor can perform ``action`` against an optional ``parent``/``child`` resource tuple, returning the winning rule and reason. - -These endpoints work in conjunction with :ref:`plugin_hook_permission_resources_sql` and make it easier to verify that configuration allow blocks and plugins are behaving as intended. - -All three endpoints support both HTML and JSON responses. Visit the endpoint directly for an interactive HTML form interface, or add ``.json`` to the URL for a raw JSON response. - -**Security note:** The ``/-/check`` and ``/-/allowed`` endpoints are publicly accessible to help users understand their own permissions. However, potentially sensitive fields (``reason`` and ``source_plugin``) are only included in responses for users with the ``permissions-debug`` permission. The ``/-/rules`` endpoint requires the ``permissions-debug`` permission for all access. +This endpoint is publicly accessible to help users understand their own permissions. The potentially sensitive ``reason`` field is only shown to users with the ``permissions-debug`` permission - it shows the plugins and explanatory reasons that were responsible for each decision. .. _PermissionRulesView: Permission rules view -===================== +--------------------- The ``/-/rules`` endpoint displays all permission rules (both allow and deny) for each candidate resource for the requested action. @@ -1101,12 +1114,12 @@ This endpoint provides an interactive HTML form interface. Add ``.json`` to the Pass ``?action=`` as a query parameter to specify which action to check. -**Requires the permissions-debug permission** - this endpoint returns a 403 Forbidden error for users without this permission. +This endpoint requires the ``permissions-debug`` permission. .. _PermissionCheckView: Permission check view -===================== +--------------------- The ``/-/check`` endpoint evaluates a single action/resource pair and returns information indicating whether the access was allowed along with diagnostic information. @@ -1114,8 +1127,6 @@ This endpoint provides an interactive HTML form interface. Add ``.json`` to the Pass ``?action=`` to specify the action to check, and optional ``?parent=`` and ``?child=`` parameters to specify the resource. -This endpoint is publicly accessible to help users understand their own permissions. However, potentially sensitive fields (``reason`` and ``source_plugin``) are only included in responses for users with the ``permissions-debug`` permission. - .. _authentication_ds_actor: The ds_actor cookie @@ -1181,168 +1192,156 @@ The /-/logout page The page at ``/-/logout`` provides the ability to log out of a ``ds_actor`` cookie authentication session. -.. _permissions: +.. _actions: -Built-in permissions -==================== +Built-in actions +================ This section lists all of the permission checks that are carried out by Datasette core, along with the ``resource`` if it was passed. -.. _permissions_view_instance: +.. _actions_view_instance: view-instance ------------- Top level permission - Actor is allowed to view any pages within this instance, starting at https://latest.datasette.io/ -Default *allow*. - -.. _permissions_view_database: +.. _actions_view_database: view-database ------------- Actor is allowed to view a database page, e.g. https://latest.datasette.io/fixtures -``resource`` - string - The name of the database +``resource`` - ``datasette.permissions.DatabaseResource(database)`` + ``database`` is the name of the database (string) -Default *allow*. - -.. _permissions_view_database_download: +.. _actions_view_database_download: view-database-download ---------------------- Actor is allowed to download a database, e.g. https://latest.datasette.io/fixtures.db -``resource`` - string - The name of the database +``resource`` - ``datasette.resources.DatabaseResource(database)`` + ``database`` is the name of the database (string) -Default *allow*. - -.. _permissions_view_table: +.. _actions_view_table: view-table ---------- Actor is allowed to view a table (or view) page, e.g. https://latest.datasette.io/fixtures/complex_foreign_keys -``resource`` - tuple: (string, string) - The name of the database, then the name of the table +``resource`` - ``datasette.resources.TableResource(database, table)`` + ``database`` is the name of the database (string) -Default *allow*. + ``table`` is the name of the table (string) -.. _permissions_view_query: +.. _actions_view_query: view-query ---------- Actor is allowed to view (and execute) a :ref:`canned query ` page, e.g. https://latest.datasette.io/fixtures/pragma_cache_size - this includes executing :ref:`canned_queries_writable`. -``resource`` - tuple: (string, string) - The name of the database, then the name of the canned query +``resource`` - ``datasette.resources.QueryResource(database, query)`` + ``database`` is the name of the database (string) + + ``query`` is the name of the canned query (string) -Default *allow*. - -.. _permissions_insert_row: +.. _actions_insert_row: insert-row ---------- Actor is allowed to insert rows into a table. -``resource`` - tuple: (string, string) - The name of the database, then the name of the table +``resource`` - ``datasette.resources.TableResource(database, table)`` + ``database`` is the name of the database (string) -Default *deny*. + ``table`` is the name of the table (string) -.. _permissions_delete_row: +.. _actions_delete_row: delete-row ---------- Actor is allowed to delete rows from a table. -``resource`` - tuple: (string, string) - The name of the database, then the name of the table +``resource`` - ``datasette.resources.TableResource(database, table)`` + ``database`` is the name of the database (string) -Default *deny*. + ``table`` is the name of the table (string) -.. _permissions_update_row: +.. _actions_update_row: update-row ---------- Actor is allowed to update rows in a table. -``resource`` - tuple: (string, string) - The name of the database, then the name of the table +``resource`` - ``datasette.resources.TableResource(database, table)`` + ``database`` is the name of the database (string) -Default *deny*. + ``table`` is the name of the table (string) -.. _permissions_create_table: +.. _actions_create_table: create-table ------------ Actor is allowed to create a database table. -``resource`` - string - The name of the database +``resource`` - ``datasette.resources.DatabaseResource(database)`` + ``database`` is the name of the database (string) -Default *deny*. - -.. _permissions_alter_table: +.. _actions_alter_table: alter-table ----------- Actor is allowed to alter a database table. -``resource`` - tuple: (string, string) - The name of the database, then the name of the table +``resource`` - ``datasette.resources.TableResource(database, table)`` + ``database`` is the name of the database (string) -Default *deny*. + ``table`` is the name of the table (string) -.. _permissions_drop_table: +.. _actions_drop_table: drop-table ---------- Actor is allowed to drop a database table. -``resource`` - tuple: (string, string) - The name of the database, then the name of the table +``resource`` - ``datasette.resources.TableResource(database, table)`` + ``database`` is the name of the database (string) -Default *deny*. + ``table`` is the name of the table (string) -.. _permissions_execute_sql: +.. _actions_execute_sql: execute-sql ----------- -Actor is allowed to run arbitrary SQL queries against a specific database, e.g. https://latest.datasette.io/fixtures?sql=select+100 +Actor is allowed to run arbitrary SQL queries against a specific database, e.g. https://latest.datasette.io/fixtures/-/query?sql=select+100 -``resource`` - string - The name of the database +``resource`` - ``datasette.resources.DatabaseResource(database)`` + ``database`` is the name of the database (string) -Default *allow*. See also :ref:`the default_allow_sql setting `. +See also :ref:`the default_allow_sql setting `. -.. _permissions_permissions_debug: +.. _actions_permissions_debug: permissions-debug ----------------- -Actor is allowed to view the ``/-/permissions`` debug page. +Actor is allowed to view the ``/-/permissions`` debug tools. -Default *deny*. - -.. _permissions_debug_menu: +.. _actions_debug_menu: debug-menu ---------- Controls if the various debug pages are displayed in the navigation menu. - -Default *deny*. diff --git a/docs/changelog.rst b/docs/changelog.rst index 35b3c3ac..b9340492 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -188,7 +188,7 @@ This alpha release adds basic alter table support to the Datasette Write API and Alter table support for create, insert, upsert and update ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :ref:`JSON write API ` can now be used to apply simple alter table schema changes, provided the acting actor has the new :ref:`permissions_alter_table` permission. (:issue:`2101`) +The :ref:`JSON write API ` can now be used to apply simple alter table schema changes, provided the acting actor has the new :ref:`actions_alter_table` permission. (:issue:`2101`) The only alter operation supported so far is adding new columns to an existing table. @@ -203,12 +203,12 @@ Permissions fix for the upsert API The :ref:`/database/table/-/upsert API ` had a minor permissions bug, only affecting Datasette instances that had configured the ``insert-row`` and ``update-row`` permissions to apply to a specific table rather than the database or instance as a whole. Full details in issue :issue:`2262`. -To avoid similar mistakes in the future the :ref:`datasette.permission_allowed() ` method now specifies ``default=`` as a keyword-only argument. +To avoid similar mistakes in the future the ``datasette.permission_allowed()`` method now specifies ``default=`` as a keyword-only argument. Permission checks now consider opinions from every plugin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :ref:`datasette.permission_allowed() ` method previously consulted every plugin that implemented the :ref:`permission_allowed() ` plugin hook and obeyed the opinion of the last plugin to return a value. (:issue:`2275`) +The ``datasette.permission_allowed()`` method previously consulted every plugin that implemented the :ref:`permission_allowed() ` plugin hook and obeyed the opinion of the last plugin to return a value. (:issue:`2275`) Datasette now consults every plugin and checks to see if any of them returned ``False`` (the veto rule), and if none of them did, it then checks to see if any of them returned ``True``. @@ -1403,7 +1403,7 @@ Smaller changes - New :ref:`datasette.get_database() ` method. - Added ``_`` prefix to many private, undocumented methods of the Datasette class. (:issue:`576`) - Removed the ``db.get_outbound_foreign_keys()`` method which duplicated the behaviour of ``db.foreign_keys_for_table()``. -- New :ref:`await datasette.permission_allowed() ` method. +- New ``await datasette.permission_allowed()`` method. - ``/-/actor`` debugging endpoint for viewing the currently authenticated actor. - New ``request.cookies`` property. - ``/-/plugins`` endpoint now shows a list of hooks implemented by each plugin, e.g. https://latest.datasette.io/-/plugins?all=1 diff --git a/docs/internals.rst b/docs/internals.rst index 3f94f361..a0e2e5c8 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -342,33 +342,6 @@ If no plugins that implement that hook are installed, the default return value l "2": {"id": "2"} } -.. _datasette_permission_allowed: - -await .permission_allowed(actor, action, resource=None, default=...) --------------------------------------------------------------------- - -``actor`` - dictionary - The authenticated actor. This is usually ``request.actor``. - -``action`` - string - The name of the action that is being permission checked. - -``resource`` - string or tuple, optional - The resource, e.g. the name of the database, or a tuple of two strings containing the name of the database and the name of the table. Only some permissions apply to a resource. - -``default`` - optional: True, False or None - What value should be returned by default if nothing provides an opinion on this permission check. - Set to ``True`` for default allow or ``False`` for default deny. - If not specified the ``default`` from the ``Permission()`` tuple that was registered using :ref:`plugin_register_permissions` will be used. - -Check if the given actor has :ref:`permission ` to perform the given action on the given resource. - -Some permission checks are carried out against :ref:`rules defined in datasette.yaml `, while other custom permissions may be decided by plugins that implement the :ref:`plugin_hook_permission_allowed` plugin hook. - -If neither ``metadata.json`` nor any of the plugins provide an answer to the permission query the ``default`` argument will be returned. - -See :ref:`permissions` for a full list of permission actions included in Datasette core. - .. _datasette_allowed: await .allowed(\*, action, resource, actor=None) @@ -385,8 +358,6 @@ await .allowed(\*, action, resource, actor=None) This method checks if the given actor has permission to perform the given action on the given resource. All parameters must be passed as keyword arguments. -This is the modern resource-based permission checking method. It works with Resource objects that provide structured information about what is being accessed. - Example usage: .. code-block:: python @@ -414,7 +385,50 @@ Example usage: The method returns ``True`` if the permission is granted, ``False`` if denied. -For legacy string/tuple based permission checking, use :ref:`datasette_permission_allowed` instead. +.. _datasette_allowed_resources: + +await .allowed_resources(action, actor=None, \*, parent=None, include_is_private=False) +--------------------------------------------------------------------------------------- + +Returns a list of ``Resource`` objects that the actor can access for the +specified action. Each returned object is an instance of the action's +``resource_class`` and may include a ``.private`` attribute (when +``include_is_private=True``) to indicate that anonymous actors would be denied +access. + +Example:: + + tables = await datasette.allowed_resources( + "view-table", actor=request.actor, parent="fixtures" + ) + for table in tables: + print(table.parent, table.child) + +This method uses :ref:`datasette_allowed_resources_sql` under the hood and is an +efficient way to list the databases, tables or queries visible to a user. + +.. _datasette_allowed_resources_with_reasons: + +await .allowed_resources_with_reasons(action, actor=None) +--------------------------------------------------------- + +Returns a list of :class:`datasette.permissions.AllowedResource` tuples. Each tuple contains a ``Resource`` plus a list of strings describing the rules that granted access. This powers the debugging data shown by the ``/-/allowed`` endpoint and is helpful when building administrative tooling that needs to show why access was granted. + +.. _datasette_allowed_resources_sql: + +await .allowed_resources_sql(\*, action, actor=None, parent=None, include_is_private=False) +------------------------------------------------------------------------------------------- + +Builds the SQL query that Datasette uses to determine which resources an actor may access for a specific action. Returns a ``(sql: str, params: dict)`` tuple that can be executed against the internal ``catalog_*`` database tables. ``parent`` can be used to limit results to a specific database, and ``include_is_private`` adds a column indicating whether anonymous users would be denied access to that resource. + +Plugins that need to execute custom analysis over the raw allow/deny rules can use this helper to run the same query that powers the ``/-/allowed`` debugging interface. + +The SQL query built by this method will return the following columns: + +- ``parent``: The parent resource identifier (or NULL) +- ``child``: The child resource identifier (or NULL) +- ``reason``: The reason from the rule that granted access +- ``is_private``: (if ``include_is_private``) 1 if anonymous users cannot access, 0 otherwise .. _datasette_ensure_permission: @@ -422,7 +436,7 @@ await .ensure_permission(action, resource=None, actor=None) ----------------------------------------------------------- ``action`` - string - The action to check. See :ref:`permissions` for a list of available actions. + The action to check. See :ref:`actions` for a list of available actions. ``resource`` - Resource object (optional) The resource to check the permission against. Must be an instance of ``InstanceResource``, ``DatabaseResource``, or ``TableResource`` from the ``datasette.resources`` module. If omitted, defaults to ``InstanceResource()`` for instance-level permissions. diff --git a/docs/json_api.rst b/docs/json_api.rst index 3f696f39..3b9575de 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -623,7 +623,7 @@ Pass ``"ignore": true`` to ignore these errors and insert the other rows: Or you can pass ``"replace": true`` to replace any rows with conflicting primary keys with the new values. This requires the :ref:`permissions_update_row` permission. -Pass ``"alter: true`` to automatically add any missing columns to the table. This requires the :ref:`permissions_alter_table` permission. +Pass ``"alter: true`` to automatically add any missing columns to the table. This requires the :ref:`actions_alter_table` permission. .. _TableUpsertView: @@ -735,7 +735,7 @@ When using upsert you must provide the primary key column (or columns if the tab If your table does not have an explicit primary key you should pass the SQLite ``rowid`` key instead. -Pass ``"alter: true`` to automatically add any missing columns to the table. This requires the :ref:`permissions_alter_table` permission. +Pass ``"alter: true`` to automatically add any missing columns to the table. This requires the :ref:`actions_alter_table` permission. .. _RowUpdateView: @@ -792,7 +792,7 @@ The returned JSON will look like this: Any errors will return ``{"errors": ["... descriptive message ..."], "ok": false}``, and a ``400`` status code for a bad input or a ``403`` status code for an authentication or permission error. -Pass ``"alter: true`` to automatically add any missing columns to the table. This requires the :ref:`permissions_alter_table` permission. +Pass ``"alter: true`` to automatically add any missing columns to the table. This requires the :ref:`actions_alter_table` permission. .. _RowDeleteView: @@ -860,7 +860,7 @@ The JSON here describes the table that will be created: * ``pks`` can be used instead of ``pk`` to create a compound primary key. It should be a JSON list of column names to use in that primary key. * ``ignore`` can be set to ``true`` to ignore existing rows by primary key if the table already exists. * ``replace`` can be set to ``true`` to replace existing rows by primary key if the table already exists. This requires the :ref:`permissions_update_row` permission. -* ``alter`` can be set to ``true`` if you want to automatically add any missing columns to the table. This requires the :ref:`permissions_alter_table` permission. +* ``alter`` can be set to ``true`` if you want to automatically add any missing columns to the table. This requires the :ref:`actions_alter_table` permission. If the table is successfully created this will return a ``201`` status code and the following response: @@ -939,7 +939,7 @@ You can avoid this error by passing the same ``"ignore": true`` or ``"replace": To use the ``"replace": true`` option you will also need the :ref:`permissions_update_row` permission. -Pass ``"alter": true`` to automatically add any missing columns to the existing table that are present in the rows you are submitting. This requires the :ref:`permissions_alter_table` permission. +Pass ``"alter": true`` to automatically add any missing columns to the existing table that are present in the rows you are submitting. This requires the :ref:`actions_alter_table` permission. .. _TableDropView: diff --git a/tests/conftest.py b/tests/conftest.py index 31c45ed3..4797ab71 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -138,14 +138,14 @@ def restore_working_directory(tmpdir, request): @pytest.fixture(scope="session", autouse=True) -def check_permission_actions_are_documented(): +def check_actions_are_documented(): from datasette.plugins import pm content = ( pathlib.Path(__file__).parent.parent / "docs" / "authentication.rst" ).read_text() - permissions_re = re.compile(r"\.\. _permissions_([^\s:]+):") - documented_permission_actions = set(permissions_re.findall(content)).union( + permissions_re = re.compile(r"\.\. _actions_([^\s:]+):") + documented_actions = set(permissions_re.findall(content)).union( UNDOCUMENTED_PERMISSIONS ) @@ -160,7 +160,7 @@ def check_permission_actions_are_documented(): ) action = kwargs.get("action").replace("-", "_") assert ( - action in documented_permission_actions + action in documented_actions ), "Undocumented permission action: {}".format(action) pm.add_hookcall_monitoring(