Commit graph

2,759 commits

Author SHA1 Message Date
Simon Willison
30e2f9064b Remove implies_can_view logic from actor restrictions
Simplified restrictions_allow_action() to work on exact-match basis only.
Actor restrictions no longer use permission implication logic - if an actor
has view-table permission, they can view tables but NOT automatically
view-instance or view-database.

Updated test_restrictions_allow_action test cases to reflect new behavior:
- Removed test cases expecting view-table to imply view-instance
- Removed test cases expecting view-database to imply view-instance
- Removed test cases expecting execute-sql to imply view-instance/view-database
- Added test cases verifying exact matches work correctly
- Added test case verifying abbreviations work (es -> execute-sql)

This aligns actor restrictions with the new permission model where each
action is checked independently without hierarchical implications.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 15:38:07 -07:00
Simon Willison
e1582c1424 Fix actor restrictions to work with new actions system
- Updated restrictions_allow_action() to use datasette.actions instead of datasette.permissions
- Changed references from Permission to Action objects
- Updated takes_database checks to takes_parent
- Added get_action() method to Datasette class for looking up actions by name or abbreviation
- Integrated actor restriction checking into allowed() method
- Actor restrictions (_r in actor dict) are now properly enforced after SQL permission checks

This fixes tests in test_api_write.py where actors with restricted permissions
were incorrectly being granted access to actions outside their restrictions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 15:38:07 -07:00
Simon Willison
dc241e8691 Remove deprecated register_permissions hook
- Removed register_permissions hook definition from hookspecs.py
- Removed register_permissions implementation from default_permissions.py
- Removed pm.hook.register_permissions() call from app.py invoke_startup()
- The register_actions hook now serves as the sole mechanism for registering actions
- Removed Permission import from default_permissions.py as it's no longer needed

This completes the migration from the old register_permissions hook to the new
register_actions hook. All permission definitions should now use Action objects
via register_actions, and permission checking should use permission_resources_sql
to provide SQL-based permission rules.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 15:38:07 -07:00
Simon Willison
fe2084df66 Update test infrastructure to use register_actions hook
- Consolidated register_permissions and register_actions hooks in my_plugin.py
- Added permission_resources_sql hook to provide SQL-based permission rules
- Updated conftest.py to reference datasette.actions instead of datasette.permissions
- Updated fixtures.py to include permission_resources_sql hook and remove register_permissions
- Added backwards compatibility support for old datasette-register-permissions config
- Converted test actions (this_is_allowed, this_is_denied, etc.) to use permission_resources_sql

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 15:38:07 -07:00
Simon Willison
59ccf797c4 Rename register_permissions tests to register_actions
- Renamed test_hook_register_permissions to test_hook_register_actions
- Renamed test_hook_register_permissions_no_duplicates to test_hook_register_actions_no_duplicates
- Renamed test_hook_register_permissions_allows_identical_duplicates to test_hook_register_actions_allows_identical_duplicates
- Updated all tests to use Action objects instead of Permission objects
- Updated config structures from datasette-register-permissions to datasette-register-actions
- Changed assertions from ds.permissions to ds.actions
- Updated test_hook_permission_allowed to register custom actions

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 15:38:07 -07:00
Simon Willison
7aaff5e3d2 Update tests to use new allowed() method instead of permission_allowed() 2025-10-25 15:38:07 -07:00
Simon Willison
387afb0f69 Update tests to use simplified allowed() calls
- Removed explicit InstanceResource() parameters for instance-level checks
- Removed unused InstanceResource import
2025-10-25 15:38:07 -07:00
Simon Willison
cde1624d0a Update permission hooks to include source_plugin column and simplify menu_links
- Added source_plugin column to all permission SQL queries (required by new system)
- Removed unused InstanceResource import from default_menu_links.py
- Fixed SQL format to match (parent, child, allow, reason, source_plugin) schema
2025-10-25 15:38:07 -07:00
Simon Willison
a0659075a3 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
2025-10-25 15:38:07 -07:00
Simon Willison
224084facc Make allowed() and check_permission_for_resource keyword-only, add default resource
- 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
2025-10-25 15:38:07 -07:00
Simon Willison
235962cd35 just blacken-docs 2025-10-25 15:02:49 -07:00
Simon Willison
4be7eece8c just prettier, just format shortcuts 2025-10-25 09:04:04 -07:00
Simon Willison
4d03e8c12e Refactor AllowedResourcesView to use datasette.allowed_resources()
Refs https://github.com/simonw/datasette/issues/2527#issuecomment-3444586698
2025-10-24 12:21:48 -07:00
Simon Willison
e8b79970fb Implement also_requires to enforce view-database for execute-sql
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
2025-10-24 12:14:52 -07:00
Simon Willison
a2994cc5bb Remove automatic parameter namespacing from permission plugins
Simplifies the permission system by removing automatic parameter namespacing.
Plugins are now responsible for using unique parameter names. The recommended
convention is to prefix parameters with the plugin source name (e.g.,
:myplugin_user_id). System reserves :actor, :actor_id, :action, :filter_parent.

- Remove _namespace_params() function from datasette/utils/permissions.py
- Update build_rules_union() to use plugin params directly
- Document parameter naming convention in plugin_hooks.rst
- Update example plugins to use prefixed parameters
- Add test_multiple_plugins_with_own_parameters() to verify convention works
2025-10-24 11:44:43 -07:00
Simon Willison
7c6bc0b902 Fix #2509: Settings-based deny rules now override root user privileges
The root user's permission_resources_sql hook was returning early with a
blanket "allow all" rule, preventing settings-based deny rules from being
considered. This caused /-/allowed and /-/rules endpoints to incorrectly
show resources that were denied via settings.

Changed permission_resources_sql to append root permissions to the rules
list instead of returning early, allowing config-based deny rules to be
evaluated. The SQL cascading logic correctly applies: deny rules at the
same depth beat allow rules, so database-level denies override root's
global-level allow.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 11:13:19 -07:00
Simon Willison
5138e95d69 Migrate homepage to use bulk allowed_resources() and fix NULL handling in SQL JOINs
- 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>
2025-10-24 10:32:18 -07:00
Simon Willison
8674aaa392 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>
2025-10-24 10:32:18 -07:00
Simon Willison
2620938661 Migrate /database view to use bulk allowed_resources()
Replace one-by-one permission checks with bulk allowed_resources() call:
- DatabaseView and QueryView now fetch all allowed tables once
- Filter views and tables using pre-fetched allowed_table_set
- Update TableResource.resources_sql() to include views from catalog_views

This improves performance by reducing permission checks from O(n) to O(1) per
table/view, where n is the number of tables in the database.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 10:32:18 -07:00
Simon Willison
23715d6c00 Error on startup if invalid setting types 2025-10-24 10:32:18 -07:00
Simon Willison
58ac5ccd6e Simplify types in datasette/permissions.py 2025-10-24 10:32:18 -07:00
Simon Willison
b311f735f9 Fix schema mismatch in empty result query
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
2025-10-24 10:32:18 -07:00
Simon Willison
79879b834a Address PR #2515 review comments
- 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
2025-10-24 10:32:18 -07:00
Simon Willison
a21a1b6c14 Ran blacken-docs 2025-10-24 10:32:18 -07:00
Simon Willison
c0b5ce04c3 Ran cog 2025-10-24 10:32:18 -07:00
Simon Willison
9172020535 Removed unneccessary isinstance(candidate, PermissionSQL) 2025-10-24 10:32:18 -07:00
Simon Willison
4d6730e3c4 Remove unused methods from Resource base class 2025-10-24 10:32:18 -07:00
Simon Willison
c7278c73f3 Ran latest prettier 2025-10-24 10:32:18 -07:00
Simon Willison
96d2e16e83 Use allowed_resources_sql() with CTE for table filtering 2025-10-24 10:32:18 -07:00
Simon Willison
eb5a95ee6e Rewrite tables endpoint to use SQL LIKE instead of Python regex 2025-10-24 10:32:18 -07:00
Simon Willison
8e47f99874 Fix /-/tables endpoint: add .json support and correct response format 2025-10-24 10:32:18 -07:00
Simon Willison
7d04211559 Fix test_tables_endpoint_config_database_allow by using unique database names 2025-10-24 10:32:18 -07:00
Simon Willison
d73b6f169f Add register_actions hook to test plugin and improve test 2025-10-24 10:32:18 -07:00
Simon Willison
130dad268d Fix test_navigation_menu_links by enabling root_enabled for root actor 2025-10-24 10:32:18 -07:00
Simon Willison
e333827687 permission_allowed_default_allow_sql 2025-10-24 10:32:18 -07:00
Simon Willison
8b098e4b3e Applied Black 2025-10-24 10:32:18 -07:00
Simon Willison
06af34240f Fix permission endpoint tests by resolving method signature conflicts
- Renamed internal allowed_resources_sql() to _build_permission_rules_sql()
  to avoid conflict with public method
- Made public allowed_resources_sql() keyword-only to prevent argument order bugs
- Fixed PermissionRulesView to use _build_permission_rules_sql() which returns
  full permission rules (with allow/deny) instead of filtered resources
- Fixed _build_permission_rules_sql() to pass actor dict to build_rules_union()
- Added actor_id extraction in AllowedResourcesView
- Added root_enabled=True to test fixture to grant permissions-debug to root user

All 51 tests in test_permission_endpoints.py now pass.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 10:32:18 -07:00
Simon Willison
7423c1a999 Fixed some more tests 2025-10-24 10:32:18 -07:00
Simon Willison
98493b7587 Fix permission_allowed_sql_bridge to not apply defaults, closes #2526
The bridge was incorrectly using the new allowed() method which applies
default allow rules. This caused actors without restrictions to get True
instead of USE_DEFAULT, breaking backward compatibility.

Fixed by:
- Removing the code that converted to resource objects and called allowed()
- Bridge now ONLY checks config-based rules via _config_permission_rules()
- Returns None when no config rules exist, allowing Permission.default to apply
- This maintains backward compatibility with the permission_allowed() API

All 177 permission tests now pass, including test_actor_restricted_permissions
and test_permissions_checked which were previously failing.
2025-10-24 10:32:18 -07:00
Simon Willison
8b5bf3e487 Mark test_permissions_checked database download test as xfail, refs #2526
The test expects ensure_permissions() to check all three permissions
(view-database-download, view-database, view-instance) but the current
implementation short-circuits after the first successful check.

Created issue #2526 to track the investigation of the expected behavior.
2025-10-24 10:32:18 -07:00
Simon Willison
2ed2849a14 Eliminate duplicate config checking by removing old permission_allowed hooks
- Removed permission_allowed_default() hook (checked config twice)
- Removed _resolve_config_view_permissions() and _resolve_config_permissions_blocks() helpers
- Added permission_allowed_sql_bridge() to bridge old permission_allowed() API to new SQL system
- Moved default_allow_sql setting check into permission_resources_sql()
- Made root-level allow blocks apply to all view-* actions (view-database, view-table, view-query)
- Added add_row_allow_block() helper for allow blocks that should deny when no match

This resolves the duplicate checking issue where config blocks were evaluated twice:
once in permission_allowed hooks and once in permission_resources_sql hooks.

Note: One test still failing (test_permissions_checked for database download) - needs investigation
2025-10-24 10:32:18 -07:00
Simon Willison
b8d26754df Document datasette.allowed(), PermissionSQL class, and SQL parameters
- Added documentation for datasette.allowed() method with keyword-only arguments
- Added comprehensive PermissionSQL class documentation with examples
- Documented the three SQL parameters available: :actor, :actor_id, :action
- Included examples of using json_extract() to access actor fields
- Explained permission resolution rules (specificity, deny over allow, implicit deny)
- Fixed RST formatting warnings (escaped asterisk, fixed underline length)
2025-10-24 10:32:18 -07:00
Simon Willison
c06e05b7db New --root mechanism with datasette.root_enabled, closes #2521 2025-10-24 10:32:18 -07:00
Simon Willison
65c427e4ee Ensure :actor, :actor_id and :action are all available to permissions SQL, closes #2520
- Updated build_rules_union() to accept actor as dict and provide :actor (JSON) and :actor_id
- Updated resolve_permissions_from_catalog() and resolve_permissions_with_candidates() to accept actor dict
- :actor is now the full actor dict as JSON (use json_extract() to access fields)
- :actor_id is the actor's id field for simple comparisons
- :action continues to be available as before
- Updated all call sites and tests to use new parameter format
- Added test demonstrating all three parameters working together
2025-10-24 10:32:18 -07:00
Simon Willison
b9c6e7a0f6 PluginSQL renamed to PermissionSQL, closes #2524 2025-10-24 10:32:18 -07:00
Simon Willison
159b9f3fec ds.allowed() is now keyword-argument only, closes #2519 2025-10-24 10:32:18 -07:00
Simon Willison
8e9916b286 Update allowed_resources_sql() and refactor allowed_resources() 2025-10-24 10:32:18 -07:00
Simon Willison
b1080e7d30 Moved Resource defaults to datasette/resources.py 2025-10-24 10:32:18 -07:00
Simon Willison
5b0baf7cd5 Ran prettier 2025-10-24 10:32:18 -07:00
Simon Willison
2b879e462f Implement resource-based permission system with SQL-driven access control
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>
2025-10-24 10:32:18 -07:00