datasette/datasette/resources.py
Simon Willison 5705ce0d95
Move takes_child/takes_parent information from Action to Resource (#2567)
Simplified Action by moving takes_child/takes_parent logic to Resource

- Removed InstanceResource - global actions are now simply those with resource_class=None
- Resource.parent_class - Replaced parent_name: str with parent_class: type[Resource] | None for direct class references
- Simplified Action dataclass - No more redundant fields, everything is derived from the Resource class structure
- Validation - The __init_subclass__ method now checks parent_class.parent_class to enforce the 2-level hierarchy

Closes #2563
2025-11-01 11:35:08 -07:00

90 lines
2.8 KiB
Python

"""Core resource types for Datasette's permission system."""
from datasette.permissions import Resource
class DatabaseResource(Resource):
"""A database in Datasette."""
name = "database"
parent_class = None # Top of the resource hierarchy
def __init__(self, database: str):
super().__init__(parent=database, child=None)
@classmethod
async def resources_sql(cls, datasette) -> str:
return """
SELECT database_name AS parent, NULL AS child
FROM catalog_databases
"""
class TableResource(Resource):
"""A table in a database."""
name = "table"
parent_class = DatabaseResource
def __init__(self, database: str, table: str):
super().__init__(parent=database, child=table)
@classmethod
async def resources_sql(cls, datasette) -> str:
return """
SELECT database_name AS parent, table_name AS child
FROM catalog_tables
UNION ALL
SELECT database_name AS parent, view_name AS child
FROM catalog_views
"""
class QueryResource(Resource):
"""A canned query in a database."""
name = "query"
parent_class = DatabaseResource
def __init__(self, database: str, query: str):
super().__init__(parent=database, child=query)
@classmethod
async def resources_sql(cls, datasette) -> str:
from datasette.plugins import pm
from datasette.utils import await_me_maybe
# Get all databases from catalog
db = datasette.get_internal_database()
result = await db.execute("SELECT database_name FROM catalog_databases")
databases = [row[0] for row in result.rows]
# Gather all canned queries from all databases
query_pairs = []
for database_name in databases:
# Call the hook to get queries (including from config via default plugin)
for queries_result in pm.hook.canned_queries(
datasette=datasette,
database=database_name,
actor=None, # Get ALL queries for resource enumeration
):
queries = await await_me_maybe(queries_result)
if queries:
for query_name in queries.keys():
query_pairs.append((database_name, query_name))
# Build SQL
if not query_pairs:
return "SELECT NULL AS parent, NULL AS child WHERE 0"
# Generate UNION ALL query
selects = []
for db_name, query_name in query_pairs:
# Escape single quotes by doubling them
db_escaped = db_name.replace("'", "''")
query_escaped = query_name.replace("'", "''")
selects.append(
f"SELECT '{db_escaped}' AS parent, '{query_escaped}' AS child"
)
return " UNION ALL ".join(selects)