register_permissions() plugin hook (#1940)

* Docs for permissions: in metadata, refs #1636
* Refactor default_permissions.py to help with implementation of #1636
* register_permissions() plugin hook, closes #1939 - also refs #1938
* Tests for register_permissions() hook, refs #1939
* Documentation for datasette.permissions, refs #1939
* permission_allowed() falls back on Permission.default, refs #1939
* Raise StartupError on duplicate permissions
* Allow dupe permisisons if exact matches
This commit is contained in:
Simon Willison 2022-12-12 18:05:54 -08:00 committed by GitHub
commit 8bf06a76b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 513 additions and 88 deletions

View file

@ -185,8 +185,14 @@ The ``/-/allow-debug`` tool lets you try out different ``"action"`` blocks agai
.. _authentication_permissions_metadata:
Configuring permissions in metadata.json
========================================
Access permissions in metadata
==============================
There are two ways to configure permissions using ``metadata.json`` (or ``metadata.yaml``).
For simple visibility permissions you can use ``"allow"`` blocks in the root, database, table and query sections.
For other permissions you can use a ``"permissions"`` block, described :ref:`in the next section <blah>`.
You can limit who is allowed to view different parts of your Datasette instance using ``"allow"`` keys in your :ref:`metadata` configuration.
@ -201,8 +207,8 @@ If a user cannot access a specific database, they will not be able to access tab
.. _authentication_permissions_instance:
Controlling access to an instance
---------------------------------
Access to an instance
---------------------
Here's how to restrict access to your entire Datasette instance to just the ``"id": "root"`` user:
@ -228,8 +234,8 @@ One reason to do this is if you are using a Datasette plugin - such as `datasett
.. _authentication_permissions_database:
Controlling access to specific databases
----------------------------------------
Access to specific databases
----------------------------
To limit access to a specific ``private.db`` database to just authenticated users, use the ``"allow"`` block like this:
@ -247,8 +253,8 @@ To limit access to a specific ``private.db`` database to just authenticated user
.. _authentication_permissions_table:
Controlling access to specific tables and views
-----------------------------------------------
Access to specific tables and views
-----------------------------------
To limit access to the ``users`` table in your ``bakery.db`` database:
@ -277,8 +283,8 @@ This works for SQL views as well - you can list their names in the ``"tables"``
.. _authentication_permissions_query:
Controlling access to specific canned queries
---------------------------------------------
Access to specific canned queries
---------------------------------
:ref:`canned_queries` allow you to configure named SQL queries in your ``metadata.json`` that can be executed by users. These queries can be set up to both read and write to the database, so controlling who can execute them can be important.
@ -333,6 +339,63 @@ To limit this ability for just one specific database, use this:
}
}
.. _authentication_permissions_other:
Other permissions in metadata
=============================
For all other permissions, you can use one or more ``"permissions"`` blocks in your metadata.
To grant access to the :ref:`permissions debug tool <PermissionsDebugView>` to all signed in users you can grant ``permissions-debug`` to any actor with an ``id`` matching the wildcard ``*`` by adding this a the root of your metadata:
.. code-block:: json
{
"permissions": {
"debug-menu": {
"id": "*"
}
}
}
To grant ``create-table`` to the user with ``id`` of ``editor`` for the ``docs`` database:
.. code-block:: json
{
"databases": {
"docs": {
"permissions": {
"create-table": {
"id": "editor"
}
}
}
}
}
And for ``insert-row`` against the ``reports`` table in that ``docs`` database:
.. code-block:: json
{
"databases": {
"docs": {
"tables": {
"reports": {
"permissions": {
"insert-row": {
"id": "editor"
}
}
}
}
}
}
}
The :ref:`PermissionsDebugView` can be useful for helping test permissions that you have configured in this way.
.. _CreateTokenView:
API Tokens
@ -423,10 +486,12 @@ The currently authenticated actor is made available to plugins as ``request.acto
The permissions debug tool
==========================
The debug tool at ``/-/permissions`` is only available to the :ref:`authenticated root user <authentication_root>` (or any actor granted the ``permissions-debug`` action according to a plugin).
The debug tool at ``/-/permissions`` is only available to the :ref:`authenticated root user <authentication_root>` (or any actor granted the ``permissions-debug`` action).
It shows the thirty most recent 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.
This is designed to help administrators and plugin authors understand exactly how permission checks are being carried out, in order to effectively configure Datasette's permission system.
.. _authentication_ds_actor:

View file

@ -273,6 +273,15 @@ The dictionary keys are the name of the database that is used in the URL - e.g.
All databases are listed, irrespective of user permissions. This means that the ``_internal`` database will always be listed here.
.. _datasette_permissions:
.permissions
------------
Property exposing a dictionary of permissions that have been registered using the :ref:`plugin_register_permissions` plugin hook.
The dictionary keys are the permission names - e.g. ``view-instance`` - and the values are ``Permission()`` named tuples describing the permission. Here is a :ref:`description of that tuple <plugin_register_permissions>`.
.. _datasette_plugin_config:
.plugin_config(plugin_name, database=None, table=None)
@ -315,8 +324,8 @@ Renders a `Jinja template <https://jinja.palletsprojects.com/en/2.11.x/>`__ usin
.. _datasette_permission_allowed:
await .permission_allowed(actor, action, resource=None, default=False)
----------------------------------------------------------------------
await .permission_allowed(actor, action, resource=None, default=...)
--------------------------------------------------------------------
``actor`` - dictionary
The authenticated actor. This is usually ``request.actor``.
@ -327,8 +336,10 @@ await .permission_allowed(actor, action, resource=None, default=False)
``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 or False
Should this permission check be default allow or default deny.
``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 <authentication_permissions>` to perform the given action on the given resource.

View file

@ -760,6 +760,53 @@ The plugin hook can then be used to register the new facet class like this:
def register_facet_classes():
return [SpecialFacet]
.. _plugin_register_permissions:
register_permissions(datasette)
--------------------------------
If your plugin needs to register additional permissions unique to that plugin - ``upload-csvs`` for example - you can return a list of those permissions from this hook.
.. code-block:: python
from datasette import hookimpl, Permission
@hookimpl
def register_permissions(datasette):
return [
Permission(
name="upload-csvs",
abbr=None,
description="Upload CSV files",
takes_database=True,
takes_resource=False,
default=False,
)
]
The fields of the ``Permission`` named tuple are as follows:
``name``
The name of the permission, e.g. ``upload-csvs``. This should be unique across all plugins that the user might have installed, so choose carefully.
``abbr``
An abbreviation of the permission, e.g. ``uc``. This is optional - you can set it to ``None`` if you do not want to pick an abbreviation. Since this needs to be unique across all installed plugins it's best not to specify an abbreviation at all. If an abbreviation is provided it will be used when creating restricted signed API tokens.
``description``
A human-readable description of what the permission lets you do. Should make sense as the second part of a sentence that starts "A user with this permission can ...".
``takes_database``
``True`` if this permission can be granted on a per-database basis, ``False`` if it is only valid at the overall Datasette instance level.
``takes_resource``
``True`` if this permission can be granted on a per-resource basis. A resource is a database table, SQL view or :ref:`canned query <canned_queries>`.
``default``
The default value for this permission if it is not explicitly granted to a user. ``True`` means the permission is granted by default, ``False`` means it is not.
This should only be ``True`` if you want anonymous users to be able to take this action.
.. _plugin_asgi_wrapper:
asgi_wrapper(datasette)

View file

@ -154,6 +154,7 @@ If you run ``datasette plugins --all`` it will include default plugins that ship
"actor_from_request",
"permission_allowed",
"register_commands",
"register_permissions",
"skip_csrf"
]
},