mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Add parent filter and include_is_private to allowed_resources()
Major improvements to the allowed_resources() API: 1. **parent filter**: Filter results to specific database in SQL, not Python - Avoids loading thousands of tables into Python memory - Filtering happens efficiently in SQLite 2. **include_is_private flag**: Detect private resources in single SQL query - Compares actor permissions vs anonymous permissions in SQL - LEFT JOIN between actor_allowed and anon_allowed CTEs - Returns is_private column: 1 if anonymous blocked, 0 otherwise - No individual check_visibility() calls needed 3. **Resource.private property**: Safe access with clear error messages - Raises AttributeError if accessed without include_is_private=True - Prevents accidental misuse of the property 4. **Database view optimization**: Use new API to eliminate redundant checks - Single bulk query replaces N individual permission checks - Private flag computed in SQL, not via check_visibility() calls - Views filtered from allowed_dict instead of checking db.view_names() All permission filtering now happens in SQLite where it belongs, with minimal data transferred to Python. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2620938661
commit
8674aaa392
4 changed files with 319 additions and 133 deletions
|
|
@ -1305,15 +1305,28 @@ class Datasette:
|
|||
*,
|
||||
action: str,
|
||||
actor: dict | None = None,
|
||||
parent: str | None = None,
|
||||
include_is_private: bool = False,
|
||||
) -> tuple[str, dict]:
|
||||
"""
|
||||
Build SQL query to get all resources the actor can access for the given action.
|
||||
|
||||
Args:
|
||||
action: The action name (e.g., "view-table")
|
||||
actor: The actor dict (or None for unauthenticated)
|
||||
parent: Optional parent filter (e.g., database name) to limit results
|
||||
include_is_private: If True, include is_private column showing if anonymous cannot access
|
||||
|
||||
Returns a tuple of (query, params) that can be executed against the internal database.
|
||||
The query returns rows with (parent, child, reason) columns.
|
||||
The query returns rows with (parent, child, reason) columns, plus is_private if requested.
|
||||
|
||||
Example:
|
||||
query, params = await datasette.allowed_resources_sql(action="view-table", actor=actor)
|
||||
query, params = await datasette.allowed_resources_sql(
|
||||
action="view-table",
|
||||
actor=actor,
|
||||
parent="mydb",
|
||||
include_is_private=True
|
||||
)
|
||||
result = await datasette.get_internal_database().execute(query, params)
|
||||
"""
|
||||
from datasette.utils.actions_sql import build_allowed_resources_sql
|
||||
|
|
@ -1322,12 +1335,17 @@ class Datasette:
|
|||
if not action_obj:
|
||||
raise ValueError(f"Unknown action: {action}")
|
||||
|
||||
return await build_allowed_resources_sql(self, actor, action)
|
||||
return await build_allowed_resources_sql(
|
||||
self, actor, action, parent=parent, include_is_private=include_is_private
|
||||
)
|
||||
|
||||
async def allowed_resources(
|
||||
self,
|
||||
action: str,
|
||||
actor: dict | None = None,
|
||||
*,
|
||||
parent: str | None = None,
|
||||
include_is_private: bool = False,
|
||||
) -> list["Resource"]:
|
||||
"""
|
||||
Return all resources the actor can access for the given action.
|
||||
|
|
@ -1335,10 +1353,25 @@ class Datasette:
|
|||
Uses SQL to filter resources based on cascading permission rules.
|
||||
Returns instances of the appropriate Resource subclass.
|
||||
|
||||
Args:
|
||||
action: The action name (e.g., "view-table")
|
||||
actor: The actor dict (or None for unauthenticated)
|
||||
parent: Optional parent filter (e.g., database name) to limit results
|
||||
include_is_private: If True, adds a .private attribute to each Resource
|
||||
|
||||
Example:
|
||||
# Get all tables
|
||||
tables = await datasette.allowed_resources("view-table", actor)
|
||||
for table in tables:
|
||||
print(f"{table.parent}/{table.child}")
|
||||
|
||||
# Get tables for specific database with private flag
|
||||
tables = await datasette.allowed_resources(
|
||||
"view-table", actor, parent="mydb", include_is_private=True
|
||||
)
|
||||
for table in tables:
|
||||
if table.private:
|
||||
print(f"{table.child} is private")
|
||||
"""
|
||||
from datasette.permissions import Resource
|
||||
|
||||
|
|
@ -1346,17 +1379,24 @@ class Datasette:
|
|||
if not action_obj:
|
||||
raise ValueError(f"Unknown action: {action}")
|
||||
|
||||
query, params = await self.allowed_resources_sql(action=action, actor=actor)
|
||||
query, params = await self.allowed_resources_sql(
|
||||
action=action,
|
||||
actor=actor,
|
||||
parent=parent,
|
||||
include_is_private=include_is_private,
|
||||
)
|
||||
result = await self.get_internal_database().execute(query, params)
|
||||
|
||||
# Instantiate the appropriate Resource subclass for each row
|
||||
resource_class = action_obj.resource_class
|
||||
resources = []
|
||||
for row in result.rows:
|
||||
# row[0]=parent, row[1]=child, row[2]=reason (ignored)
|
||||
# row[0]=parent, row[1]=child, row[2]=reason (ignored), row[3]=is_private (if requested)
|
||||
# Create instance directly with parent/child from base class
|
||||
resource = object.__new__(resource_class)
|
||||
Resource.__init__(resource, parent=row[0], child=row[1])
|
||||
if include_is_private:
|
||||
resource.private = bool(row[3])
|
||||
resources.append(resource)
|
||||
|
||||
return resources
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue