- Made allowed() accept resource=None with InstanceResource() as default
- Made both functions keyword-argument only
- Added logging to _permission_checks for debug endpoints
- Fixed check_permission_for_resource to handle empty params correctly
- Created build_permission_rules_sql() helper function for debug views
Adds Action.also_requires field to specify dependencies between permissions.
When an action has also_requires set, users must have permission for BOTH
the main action AND the required action on a resource.
Applies this to execute-sql, which now requires view-database permission.
This prevents the illogical scenario where users can execute SQL on a
database they cannot view.
Changes:
- Add also_requires field to Action dataclass in datasette/permissions.py
- Update execute-sql action with also_requires="view-database"
- Implement also_requires handling in build_allowed_resources_sql()
- Implement also_requires handling in AllowedResourcesView endpoint
- Add test verifying execute-sql requires view-database permission
Fixes#2527
- Updated IndexView in datasette/views/index.py to fetch all allowed databases and tables
in bulk upfront using allowed_resources() instead of calling check_visibility() for each
database, table, and view individually
- Fixed SQL bug in build_allowed_resources_sql() where USING (parent, child) clauses failed
for database resources because NULL = NULL evaluates to NULL in SQL, not TRUE
- Changed all INNER JOINs to use explicit ON conditions with NULL-safe comparisons:
ON b.parent = x.parent AND (b.child = x.child OR (b.child IS NULL AND x.child IS NULL))
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
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>
When no permission rules exist, the query was returning 2 columns (parent, child)
but the function contract specifies 3 columns (parent, child, reason). This could
cause schema mismatches in consuming code.
Added 'NULL AS reason' to match the documented 3-column schema.
Added regression test that verifies the schema has 3 columns even when no
permission rules are returned. The test fails without the fix (showing only
2 columns) and passes with it.
Thanks to @asg017 for catching this
- Add URL to sqlite-permissions-poc in module docstring
- Replace Optional with | None for modern Python syntax
- Add Datasette type annotations
- Add SQL comment explaining cascading permission logic
- Refactor duplicated plugin result processing into helper function
This introduces a new hierarchical permission system that uses SQL queries
for efficient permission checking across resources. The system replaces the
older permission_allowed() pattern with a more flexible resource-based
approach.
Core changes:
- New Resource ABC and Action dataclass in datasette/permissions.py
* Resources represent hierarchical entities (instance, database, table)
* Each resource type implements resources_sql() to list all instances
* Actions define operations on resources with cascading rules
- New plugin hook: register_actions(datasette)
* Plugins register actions with their associated resource types
* Replaces register_permissions() and register_resource_types()
* See docs/plugin_hooks.rst for full documentation
- Three new Datasette methods for permission checks:
* allowed_resources(action, actor) - returns list[Resource]
* allowed_resources_with_reasons(action, actor) - for debugging
* allowed(action, resource, actor) - checks single resource
* All use SQL for filtering, never Python iteration
- New /-/tables endpoint (TablesView)
* Returns JSON list of tables user can view
* Supports ?q= parameter for regex filtering
* Format: {"matches": [{"name": "db/table", "url": "/db/table"}]}
* Respects all permission rules from configuration and plugins
- SQL-based permission evaluation (datasette/utils/actions_sql.py)
* Cascading rules: child-level → parent-level → global-level
* DENY beats ALLOW at same specificity
* Uses CTEs for efficient SQL-only filtering
* Combines permission_resources_sql() hook results
- Default actions in datasette/default_actions.py
* InstanceResource, DatabaseResource, TableResource, QueryResource
* Core actions: view-instance, view-database, view-table, etc.
- Fixed default_permissions.py to handle database-level allow blocks
* Now creates parent-level rules for view-table action
* Fixes: datasette ... -s databases.fixtures.allow.id root
Documentation:
- Comprehensive register_actions() hook documentation
- Detailed resources_sql() method explanation
- /-/tables endpoint documentation in docs/introspection.rst
- Deprecated register_permissions() with migration guide
Tests:
- tests/test_actions_sql.py: 7 tests for core permission API
- tests/test_tables_endpoint.py: 13 tests for /-/tables endpoint
- All 118 documentation tests pass
- Tests verify SQL does filtering (not Python)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>