diff --git a/datasette/filters.py b/datasette/filters.py index 67d4170b..7289c1dc 100644 --- a/datasette/filters.py +++ b/datasette/filters.py @@ -1,4 +1,5 @@ from datasette import hookimpl +from datasette.resources import DatabaseResource from datasette.views.base import DatasetteError from datasette.utils.asgi import BadRequest import json @@ -13,10 +14,10 @@ def where_filters(request, database, datasette): where_clauses = [] extra_wheres_for_ui = [] if "_where" in request.args: - if not await datasette.permission_allowed( - request.actor, - "execute-sql", - resource=database, + if not await datasette.allowed( + action="execute-sql", + resource=DatabaseResource(database=database), + actor=request.actor, default=True, ): raise DatasetteError("_where= is not allowed", status=403) diff --git a/datasette/views/database.py b/datasette/views/database.py index 280b9b81..8e68f29d 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -13,6 +13,7 @@ from typing import List from datasette.events import AlterTableEvent, CreateTableEvent, InsertRowsEvent from datasette.database import QueryInterrupted +from datasette.resources import DatabaseResource from datasette.utils import ( add_cors_headers, await_me_maybe, @@ -119,8 +120,8 @@ class DatabaseView(View): attached_databases = [d.name for d in await db.attached_databases()] - allow_execute_sql = await datasette.permission_allowed( - request.actor, "execute-sql", database + allow_execute_sql = await datasette.allowed( + action="execute-sql", resource=DatabaseResource(database=database), actor=request.actor ) json_data = { "database": database, @@ -729,8 +730,8 @@ class QueryView(View): path_with_format(request=request, format=key) ) - allow_execute_sql = await datasette.permission_allowed( - request.actor, "execute-sql", database + allow_execute_sql = await datasette.allowed( + action="execute-sql", resource=DatabaseResource(database=database), actor=request.actor ) show_hide_hidden = "" @@ -940,8 +941,8 @@ class TableCreateView(BaseView): database_name = db.name # Must have create-table permission - if not await self.ds.permission_allowed( - request.actor, "create-table", resource=database_name + if not await self.ds.allowed( + action="create-table", resource=DatabaseResource(database=database_name), actor=request.actor ): return _error(["Permission denied"], 403) @@ -977,8 +978,8 @@ class TableCreateView(BaseView): if replace: # Must have update-row permission - if not await self.ds.permission_allowed( - request.actor, "update-row", resource=database_name + if not await self.ds.allowed( + action="update-row", resource=DatabaseResource(database=database_name), actor=request.actor ): return _error(["Permission denied: need update-row"], 403) @@ -1001,8 +1002,8 @@ class TableCreateView(BaseView): if rows or row: # Must have insert-row permission - if not await self.ds.permission_allowed( - request.actor, "insert-row", resource=database_name + if not await self.ds.allowed( + action="insert-row", resource=DatabaseResource(database=database_name), actor=request.actor ): return _error(["Permission denied: need insert-row"], 403) @@ -1014,8 +1015,8 @@ class TableCreateView(BaseView): else: # alter=True only if they request it AND they have permission if data.get("alter"): - if not await self.ds.permission_allowed( - request.actor, "alter-table", resource=database_name + if not await self.ds.allowed( + action="alter-table", resource=DatabaseResource(database=database_name), actor=request.actor ): return _error(["Permission denied: need alter-table"], 403) alter = True diff --git a/datasette/views/index.py b/datasette/views/index.py index 2939f98e..0317e90b 100644 --- a/datasette/views/index.py +++ b/datasette/views/index.py @@ -177,8 +177,8 @@ class IndexView(BaseView): "databases": databases, "metadata": await self.ds.get_instance_metadata(), "datasette_version": __version__, - "private": not await self.ds.permission_allowed( - None, "view-instance" + "private": not await self.ds.allowed( + action="view-instance", actor=None ), "top_homepage": make_slot_function( "top_homepage", self.ds, request diff --git a/datasette/views/row.py b/datasette/views/row.py index f374fd94..28751048 100644 --- a/datasette/views/row.py +++ b/datasette/views/row.py @@ -1,6 +1,7 @@ from datasette.utils.asgi import NotFound, Forbidden, Response from datasette.database import QueryInterrupted from datasette.events import UpdateRowEvent, DeleteRowEvent +from datasette.resources import TableResource from .base import DataView, BaseView, _error from datasette.utils import ( await_me_maybe, @@ -184,8 +185,8 @@ async def _resolve_row_and_check_permission(datasette, request, permission): return False, _error(["Record not found: {}".format(e.pk_values)], 404) # Ensure user has permission to delete this row - if not await datasette.permission_allowed( - request.actor, permission, resource=(resolved.db.name, resolved.table) + if not await datasette.allowed( + action=permission, resource=TableResource(database=resolved.db.name, table=resolved.table), actor=request.actor ): return False, _error(["Permission denied"], 403) @@ -257,8 +258,8 @@ class RowUpdateView(BaseView): update = data["update"] alter = data.get("alter") - if alter and not await self.ds.permission_allowed( - request.actor, "alter-table", resource=(resolved.db.name, resolved.table) + if alter and not await self.ds.allowed( + action="alter-table", resource=TableResource(database=resolved.db.name, table=resolved.table), actor=request.actor ): return _error(["Permission denied for alter-table"], 403) diff --git a/datasette/views/special.py b/datasette/views/special.py index b67569d4..50fed0b0 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -1,6 +1,7 @@ import json import logging from datasette.events import LogoutEvent, LoginEvent, CreateTokenEvent +from datasette.resources import DatabaseResource, TableResource, InstanceResource from datasette.utils.asgi import Response, Forbidden from datasette.utils import ( actor_matches_allow, @@ -112,7 +113,7 @@ class PermissionsDebugView(BaseView): async def get(self, request): await self.ds.ensure_permissions(request.actor, ["view-instance"]) - if not await self.ds.permission_allowed(request.actor, "permissions-debug"): + if not await self.ds.allowed(action="permissions-debug", actor=request.actor): raise Forbidden("Permission denied") filter_ = request.args.get("filter") or "all" permission_checks = list(reversed(self.ds._permission_checks)) @@ -151,29 +152,31 @@ class PermissionsDebugView(BaseView): async def post(self, request): await self.ds.ensure_permissions(request.actor, ["view-instance"]) - if not await self.ds.permission_allowed(request.actor, "permissions-debug"): + if not await self.ds.allowed(action="permissions-debug", actor=request.actor): raise Forbidden("Permission denied") vars = await request.post_vars() actor = json.loads(vars["actor"]) permission = vars["permission"] resource_1 = vars["resource_1"] resource_2 = vars["resource_2"] - resource = [] - if resource_1: - resource.append(resource_1) - if resource_2: - resource.append(resource_2) - resource = tuple(resource) - if len(resource) == 1: - resource = resource[0] - result = await self.ds.permission_allowed( - actor, permission, resource, default="USE_DEFAULT" + # Convert to Resource object + if resource_1 and resource_2: + resource_obj = TableResource(database=resource_1, table=resource_2) + resource_for_response = (resource_1, resource_2) + elif resource_1: + resource_obj = DatabaseResource(database=resource_1) + resource_for_response = resource_1 + else: + resource_obj = InstanceResource() + resource_for_response = None + result = await self.ds.allowed( + action=permission, resource=resource_obj, actor=actor ) return Response.json( { "actor": actor, "permission": permission, - "resource": resource, + "resource": resource_for_response, "result": result, "default": self.ds.permissions[permission].default, } @@ -204,8 +207,8 @@ class AllowedResourcesView(BaseView): await self.ds.refresh_schemas() # Check if user has permissions-debug (to show sensitive fields) - has_debug_permission = await self.ds.permission_allowed( - request.actor, "permissions-debug" + has_debug_permission = await self.ds.allowed( + action="permissions-debug", actor=request.actor ) # Check if this is a request for JSON (has .json extension) @@ -360,7 +363,7 @@ class PermissionRulesView(BaseView): async def get(self, request): await self.ds.ensure_permissions(request.actor, ["view-instance"]) - if not await self.ds.permission_allowed(request.actor, "permissions-debug"): + if not await self.ds.allowed(action="permissions-debug", actor=request.actor): raise Forbidden("Permission denied") # Check if this is a request for JSON (has .json extension) @@ -401,8 +404,10 @@ class PermissionRulesView(BaseView): page_size = max_page_size offset = (page - 1) * page_size - union_sql, union_params = await self.ds._build_permission_rules_sql( - actor, action + from datasette.utils.actions_sql import build_permission_rules_sql + + union_sql, union_params = await build_permission_rules_sql( + self.ds, actor, action ) await self.ds.refresh_schemas() db = self.ds.get_internal_database() @@ -482,8 +487,8 @@ class PermissionCheckView(BaseView): async def get(self, request): # Check if user has permissions-debug (to show sensitive fields) - has_debug_permission = await self.ds.permission_allowed( - request.actor, "permissions-debug" + has_debug_permission = await self.ds.allowed( + action="permissions-debug", actor=request.actor ) # Check if this is a request for JSON (has .json extension) @@ -513,15 +518,19 @@ class PermissionCheckView(BaseView): {"error": "parent is required when child is provided"}, status=400 ) + # Convert to Resource object if parent and child: + resource_obj = TableResource(database=parent, table=child) resource = (parent, child) elif parent: + resource_obj = DatabaseResource(database=parent) resource = parent else: + resource_obj = InstanceResource() resource = None before_checks = len(self.ds._permission_checks) - allowed = await self.ds.permission_allowed_2(request.actor, action, resource) + allowed = await self.ds.allowed(action=action, resource=resource_obj, actor=request.actor) info = None if len(self.ds._permission_checks) > before_checks: @@ -642,8 +651,8 @@ class CreateTokenView(BaseView): for database in self.ds.databases.values(): if database.name == "_memory": continue - if not await self.ds.permission_allowed( - request.actor, "view-database", database.name + if not await self.ds.allowed( + action="view-database", resource=DatabaseResource(database=database.name), actor=request.actor ): continue hidden_tables = await database.hidden_table_names() @@ -651,10 +660,10 @@ class CreateTokenView(BaseView): for table in await database.table_names(): if table in hidden_tables: continue - if not await self.ds.permission_allowed( - request.actor, - "view-table", - resource=(database.name, table), + if not await self.ds.allowed( + action="view-table", + resource=TableResource(database=database.name, table=table), + actor=request.actor ): continue tables.append({"name": table, "encoded": tilde_encode(table)}) @@ -795,8 +804,8 @@ class ApiExplorerView(BaseView): if not db.is_mutable: continue - if await self.ds.permission_allowed( - request.actor, "insert-row", (name, table) + if await self.ds.allowed( + action="insert-row", resource=TableResource(database=name, table=table), actor=request.actor ): pks = await db.primary_keys(table) table_links.extend( @@ -831,8 +840,8 @@ class ApiExplorerView(BaseView): }, ] ) - if await self.ds.permission_allowed( - request.actor, "drop-table", (name, table) + if await self.ds.allowed( + action="drop-table", resource=TableResource(database=name, table=table), actor=request.actor ): table_links.append( { @@ -844,7 +853,7 @@ class ApiExplorerView(BaseView): ) database_links = [] if ( - await self.ds.permission_allowed(request.actor, "create-table", name) + await self.ds.allowed(action="create-table", resource=DatabaseResource(database=name), actor=request.actor) and db.is_mutable ): database_links.append( diff --git a/datasette/views/table.py b/datasette/views/table.py index 0a7e5265..1e9951f2 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -15,6 +15,7 @@ from datasette.events import ( UpsertRowsEvent, ) from datasette import tracer +from datasette.resources import DatabaseResource, TableResource from datasette.utils import ( add_cors_headers, await_me_maybe, @@ -449,11 +450,11 @@ class TableInsertView(BaseView): if upsert: # Must have insert-row AND upsert-row permissions if not ( - await self.ds.permission_allowed( - request.actor, "insert-row", resource=(database_name, table_name) + await self.ds.allowed( + action="insert-row", resource=TableResource(database=database_name, table=table_name), actor=request.actor ) - and await self.ds.permission_allowed( - request.actor, "update-row", resource=(database_name, table_name) + and await self.ds.allowed( + action="update-row", resource=TableResource(database=database_name, table=table_name), actor=request.actor ) ): return _error( @@ -461,8 +462,8 @@ class TableInsertView(BaseView): ) else: # Must have insert-row permission - if not await self.ds.permission_allowed( - request.actor, "insert-row", resource=(database_name, table_name) + if not await self.ds.allowed( + action="insert-row", resource=TableResource(database=database_name, table=table_name), actor=request.actor ): return _error(["Permission denied"], 403) @@ -491,16 +492,16 @@ class TableInsertView(BaseView): if upsert and (ignore or replace): return _error(["Upsert does not support ignore or replace"], 400) - if replace and not await self.ds.permission_allowed( - request.actor, "update-row", resource=(database_name, table_name) + if replace and not await self.ds.allowed( + action="update-row", resource=TableResource(database=database_name, table=table_name), actor=request.actor ): return _error(['Permission denied: need update-row to use "replace"'], 403) initial_schema = None if alter: # Must have alter-table permission - if not await self.ds.permission_allowed( - request.actor, "alter-table", resource=(database_name, table_name) + if not await self.ds.allowed( + action="alter-table", resource=TableResource(database=database_name, table=table_name), actor=request.actor ): return _error(["Permission denied for alter-table"], 403) # Track initial schema to check if it changed later @@ -627,8 +628,8 @@ class TableDropView(BaseView): db = self.ds.get_database(database_name) if not await db.table_exists(table_name): return _error(["Table not found: {}".format(table_name)], 404) - if not await self.ds.permission_allowed( - request.actor, "drop-table", resource=(database_name, table_name) + if not await self.ds.allowed( + action="drop-table", resource=TableResource(database=database_name, table=table_name), actor=request.actor ): return _error(["Permission denied"], 403) if not db.is_mutable: @@ -914,8 +915,8 @@ async def table_view_traced(datasette, request): "true" if datasette.setting("allow_facet") else "false" ), is_sortable=any(c["sortable"] for c in data["display_columns"]), - allow_execute_sql=await datasette.permission_allowed( - request.actor, "execute-sql", resolved.db.name + allow_execute_sql=await datasette.allowed( + action="execute-sql", resource=DatabaseResource(database=resolved.db.name), actor=request.actor ), query_ms=1.2, select_templates=[