Compare commits

...

3 commits

Author SHA1 Message Date
Simon Willison
6e35a6b4f7 Mark that test as xfail for the moment 2022-12-08 14:46:02 -08:00
Simon Willison
94be9953c5 Refactor default_permissions.py to help with implementation of #1636 2022-12-08 14:44:27 -08:00
Simon Willison
5140f4e815 Docs for permissions: in metadata, refs #1636 2022-12-08 14:07:01 -08:00
3 changed files with 186 additions and 46 deletions

View file

@ -9,6 +9,7 @@ import time
@hookimpl(tryfirst=True, specname="permission_allowed")
def permission_allowed_default(datasette, actor, action, resource):
async def inner():
# id=root gets some special permissions:
if action in (
"permissions-debug",
"debug-menu",
@ -20,45 +21,72 @@ def permission_allowed_default(datasette, actor, action, resource):
):
if actor and actor.get("id") == "root":
return True
elif action == "view-instance":
allow = datasette.metadata("allow")
if allow is not None:
return actor_matches_allow(actor, allow)
elif action == "view-database":
if resource == "_internal" and (actor is None or actor.get("id") != "root"):
return False
database_allow = datasette.metadata("allow", database=resource)
if database_allow is None:
return None
return actor_matches_allow(actor, database_allow)
elif action == "view-table":
database, table = resource
tables = datasette.metadata("tables", database=database) or {}
table_allow = (tables.get(table) or {}).get("allow")
if table_allow is None:
return None
return actor_matches_allow(actor, table_allow)
elif action == "view-query":
# Check if this query has a "allow" block in metadata
database, query_name = resource
query = await datasette.get_canned_query(database, query_name, actor)
assert query is not None
allow = query.get("allow")
if allow is None:
return None
return actor_matches_allow(actor, allow)
elif action == "execute-sql":
# Use allow_sql block from database block, or from top-level
database_allow_sql = datasette.metadata("allow_sql", database=resource)
if database_allow_sql is None:
database_allow_sql = datasette.metadata("allow_sql")
if database_allow_sql is None:
return None
return actor_matches_allow(actor, database_allow_sql)
# Resolve metadata view permissions
if action in (
"view-instance",
"view-database",
"view-table",
"view-query",
"execute-sql",
):
result = await _resolve_metadata_view_permissions(
datasette, actor, action, resource
)
if result is not None:
return result
# Check custom permissions: blocks
return await _resolve_metadata_permissions_blocks(
datasette, actor, action, resource
)
return inner
async def _resolve_metadata_permissions_blocks(datasette, actor, action, resource):
# Check custom permissions: blocks - not yet implemented
return None
async def _resolve_metadata_view_permissions(datasette, actor, action, resource):
if action == "view-instance":
allow = datasette.metadata("allow")
if allow is not None:
return actor_matches_allow(actor, allow)
elif action == "view-database":
if resource == "_internal" and (actor is None or actor.get("id") != "root"):
return False
database_allow = datasette.metadata("allow", database=resource)
if database_allow is None:
return None
return actor_matches_allow(actor, database_allow)
elif action == "view-table":
database, table = resource
tables = datasette.metadata("tables", database=database) or {}
table_allow = (tables.get(table) or {}).get("allow")
if table_allow is None:
return None
return actor_matches_allow(actor, table_allow)
elif action == "view-query":
# Check if this query has a "allow" block in metadata
database, query_name = resource
query = await datasette.get_canned_query(database, query_name, actor)
assert query is not None
allow = query.get("allow")
if allow is None:
return None
return actor_matches_allow(actor, allow)
elif action == "execute-sql":
# Use allow_sql block from database block, or from top-level
database_allow_sql = datasette.metadata("allow_sql", database=resource)
if database_allow_sql is None:
database_allow_sql = datasette.metadata("allow_sql")
if database_allow_sql is None:
return None
return actor_matches_allow(actor, database_allow_sql)
@hookimpl(specname="permission_allowed")
def permission_allowed_actor_restrictions(actor, action, resource):
if actor is None:

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

@ -1,3 +1,4 @@
import collections
from datasette.app import Datasette
from .fixtures import app_client, assert_permissions_checked, make_app_client
from bs4 import BeautifulSoup as Soup
@ -640,3 +641,49 @@ async def test_actor_restricted_permissions(
"result": expected_result,
}
assert response.json() == expected
PermMetadataTestCase = collections.namedtuple(
"PermMetadataTestCase",
"metadata,actor,action,resource,default,expected_result",
)
@pytest.mark.asyncio
@pytest.mark.xfail(reason="Not implemented yet")
@pytest.mark.parametrize(
"metadata,actor,action,resource,default,expected_result",
(
# Simple view-instance default=True example
PermMetadataTestCase(
metadata={},
actor=None,
action="view-instance",
resource=None,
default=True,
expected_result=True,
),
# debug-menu on root
PermMetadataTestCase(
metadata={"permissions": {"debug-menu": {"id": "user"}}},
actor={"id": "user"},
action="debug-menu",
resource=None,
default=False,
expected_result=True,
),
),
)
async def test_permissions_in_metadata(
perms_ds, metadata, actor, action, resource, default, expected_result
):
previous_metadata = perms_ds.metadata()
updated_metadata = copy.deepcopy(previous_metadata)
updated_metadata.update(metadata)
try:
result = await perms_ds.permission_allowed(
actor, action, resource, default=default
)
assert result == expected_result
finally:
perms_ds._metadata_local = previous_metadata