diff --git a/datasette/default_permissions.py b/datasette/default_permissions.py index ab2f6312..3c295470 100644 --- a/datasette/default_permissions.py +++ b/datasette/default_permissions.py @@ -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: diff --git a/docs/authentication.rst b/docs/authentication.rst index 5881143a..3dfd9f61 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -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 `. 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 ` 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 ` (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 ` (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: diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 4eb18cee..50237ea0 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -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