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.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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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(

View file

@ -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=[