diff --git a/datasette/app.py b/datasette/app.py index c12e0af0..2f89d17c 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -464,16 +464,11 @@ class Datasette: else: return [] - async def permission_allowed( - self, actor, action, resource_identifier=None, default=False - ): + async def permission_allowed(self, actor, action, resource=None, default=False): "Check permissions using the permissions_allowed plugin hook" result = None for check in pm.hook.permission_allowed( - datasette=self, - actor=actor, - action=action, - resource_identifier=resource_identifier, + datasette=self, actor=actor, action=action, resource=resource, ): if callable(check): check = check() @@ -490,7 +485,7 @@ class Datasette: "when": datetime.datetime.utcnow().isoformat(), "actor": actor, "action": action, - "resource_identifier": resource_identifier, + "resource": resource, "used_default": used_default, "result": result, } diff --git a/datasette/default_permissions.py b/datasette/default_permissions.py index d27704aa..e989c0fa 100644 --- a/datasette/default_permissions.py +++ b/datasette/default_permissions.py @@ -3,7 +3,7 @@ from datasette.utils import actor_matches_allow @hookimpl -def permission_allowed(datasette, actor, action, resource_identifier): +def permission_allowed(datasette, actor, action, resource): if action == "permissions-debug": if actor and actor.get("id") == "root": return True @@ -12,12 +12,12 @@ def permission_allowed(datasette, actor, action, resource_identifier): if allow is not None: return actor_matches_allow(actor, allow) elif action == "view-database": - database_allow = datasette.metadata("allow", database=resource_identifier) + database_allow = datasette.metadata("allow", database=resource) if database_allow is None: return True return actor_matches_allow(actor, database_allow) elif action == "view-table": - database, table = resource_identifier + database, table = resource tables = datasette.metadata("tables", database=database) or {} table_allow = (tables.get(table) or {}).get("allow") if table_allow is None: @@ -25,7 +25,7 @@ def permission_allowed(datasette, actor, action, resource_identifier): 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_identifier + database, query_name = resource queries_metadata = datasette.metadata("queries", database=database) assert query_name in queries_metadata if isinstance(queries_metadata[query_name], str): diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py index 3c202553..d5fd232f 100644 --- a/datasette/hookspecs.py +++ b/datasette/hookspecs.py @@ -66,5 +66,5 @@ def actor_from_request(datasette, request): @hookspec -def permission_allowed(datasette, actor, action, resource_identifier): +def permission_allowed(datasette, actor, action, resource): "Check if actor is allowed to perfom this action - return True, False or None" diff --git a/datasette/templates/permissions_debug.html b/datasette/templates/permissions_debug.html index 7d3ee712..d898ea8c 100644 --- a/datasette/templates/permissions_debug.html +++ b/datasette/templates/permissions_debug.html @@ -46,8 +46,8 @@ {% endif %}

Actor: {{ check.actor|tojson }}

- {% if check.resource_identifier %} -

Resource: {{ check.resource_identifier }}

+ {% if check.resource %} +

Resource: {{ check.resource }}

{% endif %} {% endfor %} diff --git a/datasette/utils/__init__.py b/datasette/utils/__init__.py index 257d1285..7c1f34e0 100644 --- a/datasette/utils/__init__.py +++ b/datasette/utils/__init__.py @@ -876,14 +876,14 @@ def actor_matches_allow(actor, allow): return False -async def check_visibility(datasette, actor, action, resource_identifier, default=True): +async def check_visibility(datasette, actor, action, resource, default=True): "Returns (visible, private) - visible = can you see it, private = can others see it too" visible = await datasette.permission_allowed( - actor, action, resource_identifier=resource_identifier, default=default, + actor, action, resource=resource, default=default, ) if not visible: return (False, False) private = not await datasette.permission_allowed( - None, action, resource_identifier=resource_identifier, default=default, + None, action, resource=resource, default=default, ) return visible, private diff --git a/datasette/views/base.py b/datasette/views/base.py index 2ca5e86a..f327c6cd 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -64,12 +64,9 @@ class BaseView(AsgiView): response.body = b"" return response - async def check_permission(self, request, action, resource_identifier=None): + async def check_permission(self, request, action, resource=None): ok = await self.ds.permission_allowed( - request.actor, - action, - resource_identifier=resource_identifier, - default=True, + request.actor, action, resource=resource, default=True, ) if not ok: raise Forbidden(action) diff --git a/datasette/views/database.py b/datasette/views/database.py index d562ecb1..e1b29c27 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -88,7 +88,7 @@ class DatabaseView(DataView): "views": views, "queries": canned_queries, "private": not await self.ds.permission_allowed( - None, "view-database", "database", database + None, "view-database", database ), }, { diff --git a/docs/authentication.rst b/docs/authentication.rst index 67112969..f5209dfc 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -159,7 +159,7 @@ This is designed to help administrators and plugin authors understand exactly ho Permissions =========== -This section lists all of the permission checks that are carried out by Datasette core, along with the ``resource_identifier`` if it was passed. +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: @@ -176,7 +176,7 @@ view-database Actor is allowed to view a database page, e.g. https://latest.datasette.io/fixtures -``resource_identifier`` - string +``resource`` - string The name of the database .. _permissions_view_database_download: @@ -186,7 +186,7 @@ view-database-download Actor is allowed to download a database, e.g. https://latest.datasette.io/fixtures.db -``resource_identifier`` - string +``resource`` - string The name of the database .. _permissions_view_table: @@ -196,7 +196,7 @@ view-table Actor is allowed to view a table (or view) page, e.g. https://latest.datasette.io/fixtures/complex_foreign_keys -``resource_identifier`` - tuple: (string, string) +``resource`` - tuple: (string, string) The name of the database, then the name of the table .. _permissions_view_query: @@ -206,7 +206,7 @@ view-query Actor is allowed to view a :ref:`canned query ` page, e.g. https://latest.datasette.io/fixtures/pragma_cache_size -``resource_identifier`` - string +``resource`` - string The name of the canned query .. _permissions_execute_sql: @@ -216,7 +216,7 @@ execute-sql Actor is allowed to run arbitrary SQL queries against a specific database, e.g. https://latest.datasette.io/fixtures?sql=select+100 -``resource_identifier`` - string +``resource`` - string The name of the database .. _permissions_permissions_debug: diff --git a/docs/internals.rst b/docs/internals.rst index 1d61b6cb..83dbd897 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -121,8 +121,8 @@ Renders a `Jinja template `__ usin .. _datasette_permission_allowed: -await .permission_allowed(actor, action, resource_identifier=None, default=False) ---------------------------------------------------------------------------------- +await .permission_allowed(actor, action, resource=None, default=False) +---------------------------------------------------------------------- ``actor`` - dictionary The authenticated actor. This is usually ``request.actor``. @@ -130,13 +130,15 @@ await .permission_allowed(actor, action, resource_identifier=None, default=False ``action`` - string The name of the action that is being permission checked. -``resource_identifier`` - string, optional - The resource identifier, e.g. the name of the table. +``resource`` - string, optional + The resource, e.g. the name of the table. Only some permissions apply to a resource. Check if the given actor has permission to perform the given action on the given resource. This uses plugins that implement the :ref:`plugin_permission_allowed` plugin hook to decide if the action is allowed or not. If none of the plugins express an opinion, the return value will be the ``default`` argument. This is deny, but you can pass ``default=True`` to default allow instead. +See :ref:`permissions` for a full list of permissions included in Datasette core. + .. _datasette_get_database: .get_database(name) diff --git a/docs/plugins.rst b/docs/plugins.rst index 118fab84..56041d0c 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -1005,7 +1005,7 @@ Instead of returning a dictionary, this function can return an awaitable functio .. _plugin_permission_allowed: -permission_allowed(datasette, actor, action, resource_identifier) +permission_allowed(datasette, actor, action, resource) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``datasette`` - :ref:`internals_datasette` @@ -1017,7 +1017,9 @@ permission_allowed(datasette, actor, action, resource_identifier) ``action`` - string The action to be performed, e.g. ``"edit-table"``. -``resource_identifier`` - string +``resource`` - string or None An identifier for the individual resource, e.g. the name of the table. Called to check that an actor has permission to perform an action on a resource. Can return ``True`` if the action is allowed, ``False`` if the action is not allowed or ``None`` if the plugin does not have an opinion one way or the other. + +See :ref:`permissions` for a full list of permissions included in Datasette core. diff --git a/tests/conftest.py b/tests/conftest.py index 7f1e9387..320aa45b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -70,8 +70,8 @@ def check_permission_actions_are_documented(): action = kwargs.get("action").replace("-", "_") assert ( action in documented_permission_actions - ), "Undocumented permission action: {}, resource_identifier: {}".format( - action, kwargs["resource_identifier"] + ), "Undocumented permission action: {}, resource: {}".format( + action, kwargs["resource"] ) pm.add_hookcall_monitoring( diff --git a/tests/fixtures.py b/tests/fixtures.py index 8210d34f..e9175b57 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -857,21 +857,18 @@ if __name__ == "__main__": def assert_permissions_checked(datasette, actions): - # actions is a list of "action" or (action, resource_identifier) tuples + # actions is a list of "action" or (action, resource) tuples for action in actions: if isinstance(action, str): - resource_identifier = None + resource = None else: - action, resource_identifier = action + action, resource = action assert [ pc for pc in datasette._permission_checks - if pc["action"] == action - and pc["resource_identifier"] == resource_identifier - ], """Missing expected permission check: action={}, resource_identifier={} + if pc["action"] == action and pc["resource"] == resource + ], """Missing expected permission check: action={}, resource={} Permission checks seen: {} """.format( - action, - resource_identifier, - json.dumps(list(datasette._permission_checks), indent=4), + action, resource, json.dumps(list(datasette._permission_checks), indent=4), )