Migrate all view files to use new allowed() method with Resource objects

- Converted all permission_allowed() calls to allowed()
- Use proper Resource objects (InstanceResource, DatabaseResource, TableResource)
- Removed explicit InstanceResource() parameters where default applies
- Updated PermissionRulesView to use build_permission_rules_sql() helper
This commit is contained in:
Simon Willison 2025-10-24 13:53:50 -07:00
commit a0659075a3
6 changed files with 81 additions and 68 deletions

View file

@ -1,4 +1,5 @@
from datasette import hookimpl from datasette import hookimpl
from datasette.resources import DatabaseResource
from datasette.views.base import DatasetteError from datasette.views.base import DatasetteError
from datasette.utils.asgi import BadRequest from datasette.utils.asgi import BadRequest
import json import json
@ -13,10 +14,10 @@ def where_filters(request, database, datasette):
where_clauses = [] where_clauses = []
extra_wheres_for_ui = [] extra_wheres_for_ui = []
if "_where" in request.args: if "_where" in request.args:
if not await datasette.permission_allowed( if not await datasette.allowed(
request.actor, action="execute-sql",
"execute-sql", resource=DatabaseResource(database=database),
resource=database, actor=request.actor,
default=True, default=True,
): ):
raise DatasetteError("_where= is not allowed", status=403) raise DatasetteError("_where= is not allowed", status=403)

View file

@ -13,6 +13,7 @@ from typing import List
from datasette.events import AlterTableEvent, CreateTableEvent, InsertRowsEvent from datasette.events import AlterTableEvent, CreateTableEvent, InsertRowsEvent
from datasette.database import QueryInterrupted from datasette.database import QueryInterrupted
from datasette.resources import DatabaseResource
from datasette.utils import ( from datasette.utils import (
add_cors_headers, add_cors_headers,
await_me_maybe, await_me_maybe,
@ -119,8 +120,8 @@ class DatabaseView(View):
attached_databases = [d.name for d in await db.attached_databases()] attached_databases = [d.name for d in await db.attached_databases()]
allow_execute_sql = await datasette.permission_allowed( allow_execute_sql = await datasette.allowed(
request.actor, "execute-sql", database action="execute-sql", resource=DatabaseResource(database=database), actor=request.actor
) )
json_data = { json_data = {
"database": database, "database": database,
@ -729,8 +730,8 @@ class QueryView(View):
path_with_format(request=request, format=key) path_with_format(request=request, format=key)
) )
allow_execute_sql = await datasette.permission_allowed( allow_execute_sql = await datasette.allowed(
request.actor, "execute-sql", database action="execute-sql", resource=DatabaseResource(database=database), actor=request.actor
) )
show_hide_hidden = "" show_hide_hidden = ""
@ -940,8 +941,8 @@ class TableCreateView(BaseView):
database_name = db.name database_name = db.name
# Must have create-table permission # Must have create-table permission
if not await self.ds.permission_allowed( if not await self.ds.allowed(
request.actor, "create-table", resource=database_name action="create-table", resource=DatabaseResource(database=database_name), actor=request.actor
): ):
return _error(["Permission denied"], 403) return _error(["Permission denied"], 403)
@ -977,8 +978,8 @@ class TableCreateView(BaseView):
if replace: if replace:
# Must have update-row permission # Must have update-row permission
if not await self.ds.permission_allowed( if not await self.ds.allowed(
request.actor, "update-row", resource=database_name action="update-row", resource=DatabaseResource(database=database_name), actor=request.actor
): ):
return _error(["Permission denied: need update-row"], 403) return _error(["Permission denied: need update-row"], 403)
@ -1001,8 +1002,8 @@ class TableCreateView(BaseView):
if rows or row: if rows or row:
# Must have insert-row permission # Must have insert-row permission
if not await self.ds.permission_allowed( if not await self.ds.allowed(
request.actor, "insert-row", resource=database_name action="insert-row", resource=DatabaseResource(database=database_name), actor=request.actor
): ):
return _error(["Permission denied: need insert-row"], 403) return _error(["Permission denied: need insert-row"], 403)
@ -1014,8 +1015,8 @@ class TableCreateView(BaseView):
else: else:
# alter=True only if they request it AND they have permission # alter=True only if they request it AND they have permission
if data.get("alter"): if data.get("alter"):
if not await self.ds.permission_allowed( if not await self.ds.allowed(
request.actor, "alter-table", resource=database_name action="alter-table", resource=DatabaseResource(database=database_name), actor=request.actor
): ):
return _error(["Permission denied: need alter-table"], 403) return _error(["Permission denied: need alter-table"], 403)
alter = True alter = True

View file

@ -177,8 +177,8 @@ class IndexView(BaseView):
"databases": databases, "databases": databases,
"metadata": await self.ds.get_instance_metadata(), "metadata": await self.ds.get_instance_metadata(),
"datasette_version": __version__, "datasette_version": __version__,
"private": not await self.ds.permission_allowed( "private": not await self.ds.allowed(
None, "view-instance" action="view-instance", actor=None
), ),
"top_homepage": make_slot_function( "top_homepage": make_slot_function(
"top_homepage", self.ds, request "top_homepage", self.ds, request

View file

@ -1,6 +1,7 @@
from datasette.utils.asgi import NotFound, Forbidden, Response from datasette.utils.asgi import NotFound, Forbidden, Response
from datasette.database import QueryInterrupted from datasette.database import QueryInterrupted
from datasette.events import UpdateRowEvent, DeleteRowEvent from datasette.events import UpdateRowEvent, DeleteRowEvent
from datasette.resources import TableResource
from .base import DataView, BaseView, _error from .base import DataView, BaseView, _error
from datasette.utils import ( from datasette.utils import (
await_me_maybe, 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) return False, _error(["Record not found: {}".format(e.pk_values)], 404)
# Ensure user has permission to delete this row # Ensure user has permission to delete this row
if not await datasette.permission_allowed( if not await datasette.allowed(
request.actor, permission, resource=(resolved.db.name, resolved.table) action=permission, resource=TableResource(database=resolved.db.name, table=resolved.table), actor=request.actor
): ):
return False, _error(["Permission denied"], 403) return False, _error(["Permission denied"], 403)
@ -257,8 +258,8 @@ class RowUpdateView(BaseView):
update = data["update"] update = data["update"]
alter = data.get("alter") alter = data.get("alter")
if alter and not await self.ds.permission_allowed( if alter and not await self.ds.allowed(
request.actor, "alter-table", resource=(resolved.db.name, resolved.table) action="alter-table", resource=TableResource(database=resolved.db.name, table=resolved.table), actor=request.actor
): ):
return _error(["Permission denied for alter-table"], 403) return _error(["Permission denied for alter-table"], 403)

View file

@ -1,6 +1,7 @@
import json import json
import logging import logging
from datasette.events import LogoutEvent, LoginEvent, CreateTokenEvent from datasette.events import LogoutEvent, LoginEvent, CreateTokenEvent
from datasette.resources import DatabaseResource, TableResource, InstanceResource
from datasette.utils.asgi import Response, Forbidden from datasette.utils.asgi import Response, Forbidden
from datasette.utils import ( from datasette.utils import (
actor_matches_allow, actor_matches_allow,
@ -112,7 +113,7 @@ class PermissionsDebugView(BaseView):
async def get(self, request): async def get(self, request):
await self.ds.ensure_permissions(request.actor, ["view-instance"]) 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") raise Forbidden("Permission denied")
filter_ = request.args.get("filter") or "all" filter_ = request.args.get("filter") or "all"
permission_checks = list(reversed(self.ds._permission_checks)) permission_checks = list(reversed(self.ds._permission_checks))
@ -151,29 +152,31 @@ class PermissionsDebugView(BaseView):
async def post(self, request): async def post(self, request):
await self.ds.ensure_permissions(request.actor, ["view-instance"]) 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") raise Forbidden("Permission denied")
vars = await request.post_vars() vars = await request.post_vars()
actor = json.loads(vars["actor"]) actor = json.loads(vars["actor"])
permission = vars["permission"] permission = vars["permission"]
resource_1 = vars["resource_1"] resource_1 = vars["resource_1"]
resource_2 = vars["resource_2"] resource_2 = vars["resource_2"]
resource = [] # Convert to Resource object
if resource_1: if resource_1 and resource_2:
resource.append(resource_1) resource_obj = TableResource(database=resource_1, table=resource_2)
if resource_2: resource_for_response = (resource_1, resource_2)
resource.append(resource_2) elif resource_1:
resource = tuple(resource) resource_obj = DatabaseResource(database=resource_1)
if len(resource) == 1: resource_for_response = resource_1
resource = resource[0] else:
result = await self.ds.permission_allowed( resource_obj = InstanceResource()
actor, permission, resource, default="USE_DEFAULT" resource_for_response = None
result = await self.ds.allowed(
action=permission, resource=resource_obj, actor=actor
) )
return Response.json( return Response.json(
{ {
"actor": actor, "actor": actor,
"permission": permission, "permission": permission,
"resource": resource, "resource": resource_for_response,
"result": result, "result": result,
"default": self.ds.permissions[permission].default, "default": self.ds.permissions[permission].default,
} }
@ -204,8 +207,8 @@ class AllowedResourcesView(BaseView):
await self.ds.refresh_schemas() await self.ds.refresh_schemas()
# Check if user has permissions-debug (to show sensitive fields) # Check if user has permissions-debug (to show sensitive fields)
has_debug_permission = await self.ds.permission_allowed( has_debug_permission = await self.ds.allowed(
request.actor, "permissions-debug" action="permissions-debug", actor=request.actor
) )
# Check if this is a request for JSON (has .json extension) # Check if this is a request for JSON (has .json extension)
@ -360,7 +363,7 @@ class PermissionRulesView(BaseView):
async def get(self, request): async def get(self, request):
await self.ds.ensure_permissions(request.actor, ["view-instance"]) 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") raise Forbidden("Permission denied")
# Check if this is a request for JSON (has .json extension) # Check if this is a request for JSON (has .json extension)
@ -401,8 +404,10 @@ class PermissionRulesView(BaseView):
page_size = max_page_size page_size = max_page_size
offset = (page - 1) * page_size offset = (page - 1) * page_size
union_sql, union_params = await self.ds._build_permission_rules_sql( from datasette.utils.actions_sql import build_permission_rules_sql
actor, action
union_sql, union_params = await build_permission_rules_sql(
self.ds, actor, action
) )
await self.ds.refresh_schemas() await self.ds.refresh_schemas()
db = self.ds.get_internal_database() db = self.ds.get_internal_database()
@ -482,8 +487,8 @@ class PermissionCheckView(BaseView):
async def get(self, request): async def get(self, request):
# Check if user has permissions-debug (to show sensitive fields) # Check if user has permissions-debug (to show sensitive fields)
has_debug_permission = await self.ds.permission_allowed( has_debug_permission = await self.ds.allowed(
request.actor, "permissions-debug" action="permissions-debug", actor=request.actor
) )
# Check if this is a request for JSON (has .json extension) # 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 {"error": "parent is required when child is provided"}, status=400
) )
# Convert to Resource object
if parent and child: if parent and child:
resource_obj = TableResource(database=parent, table=child)
resource = (parent, child) resource = (parent, child)
elif parent: elif parent:
resource_obj = DatabaseResource(database=parent)
resource = parent resource = parent
else: else:
resource_obj = InstanceResource()
resource = None resource = None
before_checks = len(self.ds._permission_checks) 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 info = None
if len(self.ds._permission_checks) > before_checks: if len(self.ds._permission_checks) > before_checks:
@ -642,8 +651,8 @@ class CreateTokenView(BaseView):
for database in self.ds.databases.values(): for database in self.ds.databases.values():
if database.name == "_memory": if database.name == "_memory":
continue continue
if not await self.ds.permission_allowed( if not await self.ds.allowed(
request.actor, "view-database", database.name action="view-database", resource=DatabaseResource(database=database.name), actor=request.actor
): ):
continue continue
hidden_tables = await database.hidden_table_names() hidden_tables = await database.hidden_table_names()
@ -651,10 +660,10 @@ class CreateTokenView(BaseView):
for table in await database.table_names(): for table in await database.table_names():
if table in hidden_tables: if table in hidden_tables:
continue continue
if not await self.ds.permission_allowed( if not await self.ds.allowed(
request.actor, action="view-table",
"view-table", resource=TableResource(database=database.name, table=table),
resource=(database.name, table), actor=request.actor
): ):
continue continue
tables.append({"name": table, "encoded": tilde_encode(table)}) tables.append({"name": table, "encoded": tilde_encode(table)})
@ -795,8 +804,8 @@ class ApiExplorerView(BaseView):
if not db.is_mutable: if not db.is_mutable:
continue continue
if await self.ds.permission_allowed( if await self.ds.allowed(
request.actor, "insert-row", (name, table) action="insert-row", resource=TableResource(database=name, table=table), actor=request.actor
): ):
pks = await db.primary_keys(table) pks = await db.primary_keys(table)
table_links.extend( table_links.extend(
@ -831,8 +840,8 @@ class ApiExplorerView(BaseView):
}, },
] ]
) )
if await self.ds.permission_allowed( if await self.ds.allowed(
request.actor, "drop-table", (name, table) action="drop-table", resource=TableResource(database=name, table=table), actor=request.actor
): ):
table_links.append( table_links.append(
{ {
@ -844,7 +853,7 @@ class ApiExplorerView(BaseView):
) )
database_links = [] database_links = []
if ( 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 and db.is_mutable
): ):
database_links.append( database_links.append(

View file

@ -15,6 +15,7 @@ from datasette.events import (
UpsertRowsEvent, UpsertRowsEvent,
) )
from datasette import tracer from datasette import tracer
from datasette.resources import DatabaseResource, TableResource
from datasette.utils import ( from datasette.utils import (
add_cors_headers, add_cors_headers,
await_me_maybe, await_me_maybe,
@ -449,11 +450,11 @@ class TableInsertView(BaseView):
if upsert: if upsert:
# Must have insert-row AND upsert-row permissions # Must have insert-row AND upsert-row permissions
if not ( if not (
await self.ds.permission_allowed( await self.ds.allowed(
request.actor, "insert-row", resource=(database_name, table_name) action="insert-row", resource=TableResource(database=database_name, table=table_name), actor=request.actor
) )
and await self.ds.permission_allowed( and await self.ds.allowed(
request.actor, "update-row", resource=(database_name, table_name) action="update-row", resource=TableResource(database=database_name, table=table_name), actor=request.actor
) )
): ):
return _error( return _error(
@ -461,8 +462,8 @@ class TableInsertView(BaseView):
) )
else: else:
# Must have insert-row permission # Must have insert-row permission
if not await self.ds.permission_allowed( if not await self.ds.allowed(
request.actor, "insert-row", resource=(database_name, table_name) action="insert-row", resource=TableResource(database=database_name, table=table_name), actor=request.actor
): ):
return _error(["Permission denied"], 403) return _error(["Permission denied"], 403)
@ -491,16 +492,16 @@ class TableInsertView(BaseView):
if upsert and (ignore or replace): if upsert and (ignore or replace):
return _error(["Upsert does not support ignore or replace"], 400) return _error(["Upsert does not support ignore or replace"], 400)
if replace and not await self.ds.permission_allowed( if replace and not await self.ds.allowed(
request.actor, "update-row", resource=(database_name, table_name) 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) return _error(['Permission denied: need update-row to use "replace"'], 403)
initial_schema = None initial_schema = None
if alter: if alter:
# Must have alter-table permission # Must have alter-table permission
if not await self.ds.permission_allowed( if not await self.ds.allowed(
request.actor, "alter-table", resource=(database_name, table_name) action="alter-table", resource=TableResource(database=database_name, table=table_name), actor=request.actor
): ):
return _error(["Permission denied for alter-table"], 403) return _error(["Permission denied for alter-table"], 403)
# Track initial schema to check if it changed later # Track initial schema to check if it changed later
@ -627,8 +628,8 @@ class TableDropView(BaseView):
db = self.ds.get_database(database_name) db = self.ds.get_database(database_name)
if not await db.table_exists(table_name): if not await db.table_exists(table_name):
return _error(["Table not found: {}".format(table_name)], 404) return _error(["Table not found: {}".format(table_name)], 404)
if not await self.ds.permission_allowed( if not await self.ds.allowed(
request.actor, "drop-table", resource=(database_name, table_name) action="drop-table", resource=TableResource(database=database_name, table=table_name), actor=request.actor
): ):
return _error(["Permission denied"], 403) return _error(["Permission denied"], 403)
if not db.is_mutable: if not db.is_mutable:
@ -914,8 +915,8 @@ async def table_view_traced(datasette, request):
"true" if datasette.setting("allow_facet") else "false" "true" if datasette.setting("allow_facet") else "false"
), ),
is_sortable=any(c["sortable"] for c in data["display_columns"]), is_sortable=any(c["sortable"] for c in data["display_columns"]),
allow_execute_sql=await datasette.permission_allowed( allow_execute_sql=await datasette.allowed(
request.actor, "execute-sql", resolved.db.name action="execute-sql", resource=DatabaseResource(database=resolved.db.name), actor=request.actor
), ),
query_ms=1.2, query_ms=1.2,
select_templates=[ select_templates=[