Refactor check_visibility() to use Resource objects, refs #2537

Updated check_visibility() method signature to accept Resource objects
(DatabaseResource, TableResource, QueryResource) instead of plain strings
and tuples.

Changes:
- Updated check_visibility() signature to only accept Resource objects
- Added validation with helpful error message for incorrect types
- Updated all check_visibility() calls throughout the codebase:
  - datasette/views/database.py: Use DatabaseResource and QueryResource
  - datasette/views/special.py: Use DatabaseResource and TableResource
  - datasette/views/row.py: Use TableResource
  - datasette/views/table.py: Use TableResource
  - datasette/app.py: Use TableResource in expand_foreign_keys
- Updated tests to use Resource objects
- Updated documentation in docs/internals.rst:
  - Removed outdated permissions parameter
  - Updated examples to use Resource objects

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2025-10-26 09:39:51 -07:00
commit 95286fbb60
7 changed files with 34 additions and 47 deletions

View file

@ -1072,7 +1072,7 @@ class Datasette:
self,
actor: dict,
action: str,
resource: Optional[Union[str, Tuple[str, str]]] = None,
resource: Optional["Resource"] = None,
):
"""
Check if actor can see a resource and if it's private.
@ -1081,26 +1081,22 @@ class Datasette:
- visible: bool - can the actor see it?
- private: bool - if visible, can anonymous users NOT see it?
"""
# Convert old-style resource to Resource object
if resource is None:
resource_obj = None
elif isinstance(resource, str):
# Database resource
resource_obj = self.resource_for_action(action, parent=resource, child=None)
elif isinstance(resource, tuple) and len(resource) == 2:
# Database + child resource (table or query)
resource_obj = self.resource_for_action(
action, parent=resource[0], child=resource[1]
from datasette.permissions import Resource
# Validate that resource is a Resource object or None
if resource is not None and not isinstance(resource, Resource):
raise TypeError(
f"resource must be a Resource object or None, not {type(resource).__name__}. "
f"Use DatabaseResource(database=...), TableResource(database=..., table=...), "
f"or QueryResource(database=..., query=...) instead."
)
else:
resource_obj = None
# Check if actor can see it
if not await self.allowed(action=action, resource=resource_obj, actor=actor):
if not await self.allowed(action=action, resource=resource, actor=actor):
return False, False
# Check if anonymous user can see it (for "private" flag)
if not await self.allowed(action=action, resource=resource_obj, actor=None):
if not await self.allowed(action=action, resource=resource, actor=None):
# Actor can see it but anonymous cannot - it's private
return True, True
@ -1386,12 +1382,14 @@ class Datasette:
except IndexError:
return {}
# Ensure user has permission to view the referenced table
from datasette.resources import TableResource
other_table = fk["other_table"]
other_column = fk["other_column"]
visible, _ = await self.check_visibility(
actor,
action="view-table",
resource=(database, other_table),
resource=TableResource(database=database, table=other_table),
)
if not visible:
return {}

View file

@ -13,7 +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.resources import DatabaseResource, QueryResource
from datasette.utils import (
add_cors_headers,
await_me_maybe,
@ -51,7 +51,7 @@ class DatabaseView(View):
visible, private = await datasette.check_visibility(
request.actor,
action="view-database",
resource=database,
resource=DatabaseResource(database=database),
)
if not visible:
raise Forbidden("You do not have permission to view this database")
@ -541,7 +541,7 @@ class QueryView(View):
visible, private = await datasette.check_visibility(
request.actor,
action="view-query",
resource=(database, canned_query["name"]),
resource=QueryResource(database=database, query=canned_query["name"]),
)
if not visible:
raise Forbidden("You do not have permission to view this query")

View file

@ -29,7 +29,7 @@ class RowView(DataView):
visible, private = await self.ds.check_visibility(
request.actor,
action="view-table",
resource=(database, table),
resource=TableResource(database=database, table=table),
)
if not visible:
raise Forbidden("You do not have permission to view this table")

View file

@ -789,7 +789,9 @@ class ApiExplorerView(BaseView):
if name == "_internal":
continue
database_visible, _ = await self.ds.check_visibility(
request.actor, action="view-database", resource=name
request.actor,
action="view-database",
resource=DatabaseResource(database=name),
)
if not database_visible:
continue
@ -799,7 +801,7 @@ class ApiExplorerView(BaseView):
visible, _ = await self.ds.check_visibility(
request.actor,
action="view-table",
resource=(name, table),
resource=TableResource(database=name, table=table),
)
if not visible:
continue

View file

@ -978,7 +978,7 @@ async def table_view_data(
visible, private = await datasette.check_visibility(
request.actor,
action="view-table",
resource=(database_name, table_name),
resource=TableResource(database=database_name, table=table_name),
)
if not visible:
raise Forbidden("You do not have permission to view this table")