* Neater design for PermissionSQL class, refs #2556
- source is now automatically set to the source plugin
- params is optional
* PermissionSQL.allow() and PermissionSQL.deny() shortcuts
Closes#2556
* Filter out temp database from attached_databases()
Refs https://github.com/simonw/datasette/issues/2557#issuecomment-3470510837
* Ported setup.py to pyproject.toml, refs #2553
* Make fixtures tests less flaky
The in-memory fixtures table was being shared between different
instances of the test client, leading to occasional errors when
running the full test suite.
The execute_write_script() method was causing SQLite database locking
errors when multiple executescript() calls ran in quick succession.
Root cause: SQLite's executescript() method has special behavior - it
implicitly commits any pending transaction and operates in autocommit
mode. However, execute_write_script() was passing these calls through
execute_write_fn() with the default transaction=True, which wrapped
the executescript() call in a transaction context (with conn:).
This created a conflict where sequential executescript() calls would
cause the second call to fail with "OperationalError: database table
is locked: sqlite_master" because the sqlite_master table was still
locked from the first operation's implicit commit.
Fix: Pass transaction=False to execute_write_fn() since executescript()
manages its own transactions and should not be wrapped in an additional
transaction context.
This was causing test_hook_extra_body_script to fail because the
internal database initialization (which calls executescript twice in
succession) would fail, preventing the application from rendering
pages correctly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This adds a new endpoint at /-/actions that lists all registered actions
in the permission system. The endpoint supports both JSON and HTML output.
Changes:
- Added _actions() method to Datasette class to return action list
- Added route for /-/actions with JsonDataView
- Created actions.html template for nice HTML display
- Added template parameter to JsonDataView for custom templates
- Moved respond_json_or_html from BaseView to JsonDataView
- Added test for the new endpoint
The endpoint requires view-instance permission and provides details about
each action including name, abbreviation, description, resource class,
and parent/child requirements.
Closes#2547
Co-Authored-By: Claude <noreply@anthropic.com>
This fixes issues introduced by the ruff commit e57f391a which converted
Optional[x] to x | None:
- Fixed datasette/app.py line 1024: Dict[id | str, Dict] -> Dict[int | str, Dict]
(was using id built-in function instead of int type)
- Fixed datasette/app.py line 1074: Optional["Resource"] -> "Resource" | None
- Added 'from __future__ import annotations' for Python 3.10 compatibility
- Added TYPE_CHECKING blocks to avoid circular imports
- Removed dead code (unused variable assignments) from cli.py and views
- Removed unused imports flagged by ruff across multiple files
- Fixed test fixtures: moved app_client fixture imports to conftest.py
(fixed 71 test errors caused by fixtures not being registered)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The /-/check endpoint now requires the permissions-debug permission
to access. This prevents unauthorized users from probing the permission
system. Administrators can grant this permission to specific users or
anonymous users if they want to allow open access.
Added test to verify anonymous and regular users are denied access,
while root user (who has all permissions) can access the endpoint.
Closes#2546
Added test_rst_heading_underlines_match_title_length() to verify that RST
heading underlines match their title lengths. The test properly handles:
- Overline+underline style headings (skips validation for those)
- Empty lines before underlines (ignores them)
- Minimum 5-character underline length (avoids false positives)
Running this test identified 14 heading underline mismatches which have
been fixed across 5 documentation files:
- docs/authentication.rst (3 headings)
- docs/plugin_hooks.rst (4 headings)
- docs/internals.rst (5 headings)
- docs/deploying.rst (1 heading)
- docs/changelog.rst (1 heading)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
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>
The JavaScript was still referencing the old field names takes_database
and takes_resource instead of the new takes_parent and takes_child. This
caused the resource input fields to not show/hide properly when selecting
different permission actions.
- Modified /-/allowed to show all reasons that grant access to a resource
- Changed from MAX(reason) to json_group_array() in SQL to collect all reasons
- Reasons now displayed as JSON arrays in both HTML and JSON responses
- Only show Reason column to users with permissions-debug permission
- Removed obsolete "Source Plugin" column from /-/rules interface
- Updated allowed_resources_with_reasons() to parse and return reason lists
- Fixed alert() on /-/allowed by replacing with disabled input state
- Renamed test_tables_endpoint.py to test_allowed_resources.py to better
reflect that it tests the allowed_resources() API, not the HTTP endpoint
- Removed three outdated tests from test_search_tables.py that expected
the old behavior where /-/tables.json with no query returned empty results
- The new behavior (from commit bda69ff1) returns all tables with pagination
when no query is provided
Fixes test failures in CI from PR #2539🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Actor restrictions (_r) now integrate with the SQL permission layer via
the permission_resources_sql() hook instead of acting as a post-filter.
This fixes the issue where allowed_resources() didn't respect restrictions,
causing incorrect database/table listings at /.json and /database.json
endpoints for restricted actors.
Key changes:
- Add _restriction_permission_rules() function to generate SQL rules from _r
- Restrictions create global DENY + specific ALLOW rules using allowlist
- Restrictions act as gating filter BEFORE config/root/default permissions
- Remove post-filter check from allowed() method (now redundant)
- Skip default allow rules when actor has restrictions
- Add comprehensive tests for restriction filtering behavior
The cascading permission logic (child → parent → global) ensures that
allowlisted resources override the global deny, while non-allowlisted
resources are blocked.
Closes#2534
The self.permissions dictionary was declared in __init__ but never
populated - only self.actions gets populated during startup.
The get_permission() method was unused legacy code that tried to look
up permissions from the empty self.permissions dictionary.
Changes:
- Removed self.permissions = {} from Datasette.__init__
- Removed get_permission() method (unused)
- Renamed test_get_permission → test_get_action to match actual method being tested
All tests pass, confirming these were unused artifacts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The create-token CLI command was checking ds.permissions.get(action)
instead of ds.actions.get(action) when validating action names. This
caused false "Unknown permission" warnings for valid actions like
"debug-menu".
This is the same bug we fixed in app.py:685. The Action objects are
stored in ds.actions, not ds.permissions.
The warnings were being printed to stderr (correctly) but CliRunner
mixes stderr and stdout, so the warnings contaminated the token output,
causing token authentication to fail in tests.
Fixes all 6 test_cli_create_token tests.
Refs #2534🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated test expectations to match the actual /-/permissions POST endpoint:
1. **Resource format**: Changed from empty list `[]` to `None` when no resources,
and from tuple `(a, b)` to list `[a, b]` for two resources (JSON serialization)
2. **Result values**: Changed from sentinel "USE_DEFAULT" to actual boolean True/False
3. **also_requires dependencies**: Fixed tests for actions with dependencies:
- view-database-download now requires both "vdd" and "vd" in restrictions
- execute-sql now requires both "es" and "vd" in restrictions
4. **No upward cascading**: view-database does NOT grant view-instance
(changed expected result from True to False)
All 20 test_actor_restricted_permissions test cases now pass.
Refs #2534🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
These tests were expecting an old API behavior from the /-/permissions debug endpoint
that no longer exists. The tests expect:
- A "default" field in the response (removed when migrating to new permission system)
- "USE_DEFAULT" sentinel values instead of actual True/False results
- Empty list `[]` for no resource instead of `None`
The /-/permissions POST endpoint was updated (views/special.py:151-185) to return
simpler responses without the "default" field, but these tests weren't updated to match.
These tests need to be rewritten to test the new permission system correctly.
Refs #2534
Fixed two bugs preventing the create token UI and tests from working:
1. **Template variable mismatch**: create_token.html was using undefined variables
- Changed `all_permissions` → `all_actions`
- Changed `database_permissions` → `database_actions`
- Changed `resource_permissions` → `child_actions`
These match what CreateTokenView.shared() actually provides to the template.
2. **Action abbreviation bug**: app.py:685 was checking the wrong dictionary
- Changed `self.permissions.get(action)` → `self.actions.get(action)`
The abbreviate_action() function needs to look up Action objects (which have
the `abbr` attribute), not Permission objects. This bug prevented action names
like "view-instance" from being abbreviated to "vi" in token restrictions.
Refs #2534🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The test was expecting upward permission cascading (e.g., view-table permission
granting view-database access), but the actual implementation in
restrictions_allow_action() uses exact-match, non-cascading checks.
Updated 5 test cases to expect 403 (Forbidden) instead of 200 when:
- Actor has view-database permission but accesses instance page
- Actor has database-level view-table permission but accesses instance/database pages
- Actor has table-level view-table permission but accesses instance/database pages
This matches the documented behavior: "Restrictions work on an exact-match basis:
if an actor has view-table permission, they can view tables, but NOT automatically
view-instance or view-database."
Refs #2534https://github.com/simonw/datasette/issues/2534#issuecomment-3447774464🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When a database has an "allow" block in the configuration, it should
apply to all queries in that database, not just tables and the database
itself. This fix ensures that queries respect database-level access
controls.
This fixes the test_padlocks_on_database_page test which expects
plugin-defined queries (from_async_hook, from_hook) to show padlock
indicators when the database has restricted access.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added a new helper method resource_for_action() that creates Resource
instances for a given action by looking up the action's resource_class.
This eliminates the ugly object.__new__() pattern throughout the codebase.
Refactored all places that were using object.__new__() to create Resource
instances:
- check_visibility()
- allowed_resources()
- allowed_resources_with_reasons()
Also refactored database view to use allowed_resources() with
include_is_private=True to get canned queries, rather than manually
checking each one.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated check_visibility() to use the action's resource_class to determine
the correct Resource type to instantiate, rather than hardcoding based on
the action name. This follows the pattern used elsewhere in the codebase
and properly supports QueryResource for view-query actions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This change integrates canned queries with Datasette's new SQL-based
permissions system by making the following changes:
1. **Default canned_queries plugin hook**: Added a new hookimpl in
default_permissions.py that returns canned queries from datasette
configuration. This extracts config-reading logic into a plugin hook,
allowing QueryResource to discover all queries.
2. **Async resources_sql()**: Converted Resource.resources_sql() from a
synchronous class method returning a string to an async method that
receives the datasette instance. This allows QueryResource to call
plugin hooks and query the database.
3. **QueryResource implementation**: Implemented QueryResource.resources_sql()
to gather all canned queries by:
- Querying catalog_databases for all databases
- Calling canned_queries hooks for each database with actor=None
- Building a UNION ALL SQL query of all (database, query_name) pairs
- Properly escaping single quotes in resource names
4. **Simplified get_canned_queries()**: Removed config-reading logic since
it's now handled by the default plugin hook.
5. **Added view-query to default allow**: Added "view-query" to the
default_allow_actions set so canned queries are accessible by default.
6. **Removed xfail markers**: Removed test xfail markers from:
- tests/test_canned_queries.py (entire module)
- tests/test_html.py (2 tests)
- tests/test_permissions.py (1 test)
- tests/test_plugins.py (1 test)
All canned query tests now pass with the new permission system.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated the assert_permissions_checked() helper function to work with the
new PermissionCheck dataclass instead of dictionaries. The function now:
- Uses dataclass attributes (pc.action) instead of dict subscripting
- Converts parent/child to old resource format for comparison
- Updates error message formatting to show dataclass fields
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Instead of logging permission checks as dicts with a 'resource' key,
use a typed dataclass with separate parent and child fields.
Changes:
- Created PermissionCheck dataclass in app.py
- Updated permission check logging to use dataclass
- Updated PermissionsDebugView to use dataclass attributes
- Updated PermissionCheckView to check parent/child instead of resource
- Updated permissions_debug.html template to display parent/child
- Updated test expectations to use dataclass attributes
This provides better type safety and cleaner separation between
parent and child resource identifiers.
Changes:
- Use allowed_resources() instead of manual iteration with allowed() checks
- Rename all_permissions → all_actions
- Rename database_permissions → database_actions
- Rename resource_permissions → child_actions
- Update to use takes_parent/takes_child instead of takes_database/takes_resource
This makes the code more efficient (bulk permission checking) and uses
consistent naming throughout.