mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
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>
101 lines
2.8 KiB
Python
101 lines
2.8 KiB
Python
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass
|
|
from typing import Optional, NamedTuple
|
|
|
|
|
|
class Resource(ABC):
|
|
"""
|
|
Base class for all resource types.
|
|
|
|
Each subclass represents a type of resource (e.g., TableResource, DatabaseResource).
|
|
The class itself carries metadata about the resource type.
|
|
Instances represent specific resources.
|
|
"""
|
|
|
|
# Class-level metadata (subclasses must define these)
|
|
name: str = None # e.g., "table", "database", "model"
|
|
parent_name: Optional[str] = None # e.g., "database" for tables
|
|
|
|
def __init__(self, parent: Optional[str] = None, child: Optional[str] = None):
|
|
"""
|
|
Create a resource instance.
|
|
|
|
Args:
|
|
parent: The parent identifier (meaning depends on resource type)
|
|
child: The child identifier (meaning depends on resource type)
|
|
"""
|
|
self.parent = parent
|
|
self.child = child
|
|
|
|
@classmethod
|
|
@abstractmethod
|
|
def resources_sql(cls) -> str:
|
|
"""
|
|
Return SQL query that returns all resources of this type.
|
|
|
|
Must return two columns: parent, child
|
|
"""
|
|
pass
|
|
|
|
def __str__(self) -> str:
|
|
if self.parent is None and self.child is None:
|
|
return f"{self.name}:*"
|
|
elif self.child is None:
|
|
return f"{self.name}:{self.parent}"
|
|
else:
|
|
return f"{self.name}:{self.parent}/{self.child}"
|
|
|
|
def __repr__(self) -> str:
|
|
parts = [f"{self.__class__.__name__}("]
|
|
args = []
|
|
if self.parent:
|
|
args.append(f"{self.parent!r}")
|
|
if self.child:
|
|
args.append(f"{self.child!r}")
|
|
parts.append(", ".join(args))
|
|
parts.append(")")
|
|
return "".join(parts)
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, Resource):
|
|
return False
|
|
return (
|
|
self.__class__ == other.__class__
|
|
and self.parent == other.parent
|
|
and self.child == other.child
|
|
)
|
|
|
|
def __hash__(self):
|
|
return hash((self.__class__, self.parent, self.child))
|
|
|
|
|
|
class AllowedResource(NamedTuple):
|
|
"""A resource with the reason it was allowed (for debugging)."""
|
|
|
|
resource: Resource
|
|
reason: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Action:
|
|
name: str
|
|
abbr: str | None
|
|
description: str | None
|
|
takes_parent: bool
|
|
takes_child: bool
|
|
resource_class: type[Resource]
|
|
|
|
|
|
# This is obsolete, replaced by Action and ResourceType
|
|
@dataclass
|
|
class Permission:
|
|
name: str
|
|
abbr: Optional[str]
|
|
description: Optional[str]
|
|
takes_database: bool
|
|
takes_resource: bool
|
|
default: bool
|
|
# This is deliberately undocumented: it's considered an internal
|
|
# implementation detail for view-table/view-database and should
|
|
# not be used by plugins as it may change in the future.
|
|
implies_can_view: bool = False
|