mirror of
https://github.com/simonw/datasette.git
synced 2026-06-07 09:36:57 +02: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>
78 lines
3.4 KiB
HTML
78 lines
3.4 KiB
HTML
{% import "_crumbs.html" as crumbs with context %}<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>{% block title %}{% endblock %}</title>
|
|
<link rel="stylesheet" href="{{ urls.static('app.css') }}?{{ app_css_hash }}">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
{% for url in extra_css_urls %}
|
|
<link rel="stylesheet" href="{{ url.url }}"{% if url.get("sri") %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}>
|
|
{% endfor %}
|
|
<script>window.datasetteVersion = '{{ datasette_version }}';</script>
|
|
<script src="{{ urls.static('datasette-manager.js') }}" defer></script>
|
|
{% for url in extra_js_urls %}
|
|
<script {% if url.module %}type="module" {% endif %}src="{{ url.url }}"{% if url.get("sri") %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}></script>
|
|
{% endfor %}
|
|
{%- if alternate_url_json -%}
|
|
<link rel="alternate" type="application/json+datasette" href="{{ alternate_url_json }}">
|
|
{%- endif -%}
|
|
{%- block extra_head %}{% endblock -%}
|
|
</head>
|
|
<body class="{% block body_class %}{% endblock %}">
|
|
<div class="not-footer">
|
|
<header class="hd"><nav>{% block nav %}{% block crumbs %}{{ crumbs.nav(request=request) }}{% endblock %}
|
|
{% set links = menu_links() %}{% if links or show_logout %}
|
|
<details class="nav-menu details-menu">
|
|
<summary><svg aria-labelledby="nav-menu-svg-title" role="img"
|
|
fill="currentColor" stroke="currentColor" xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 16 16" width="16" height="16">
|
|
<title id="nav-menu-svg-title">Menu</title>
|
|
<path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path>
|
|
</svg></summary>
|
|
<div class="nav-menu-inner">
|
|
{% if links %}
|
|
<ul>
|
|
{% for link in links %}
|
|
<li><a href="{{ link.href }}">{{ link.label }}</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
{% if show_logout %}
|
|
<form class="nav-menu-logout" action="{{ urls.logout() }}" method="post">
|
|
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
|
|
<button class="button-as-link">Log out</button>
|
|
</form>{% endif %}
|
|
</div>
|
|
</details>{% endif %}
|
|
{% if actor %}
|
|
<div class="actor">
|
|
<strong>{{ display_actor(actor) }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}</nav></header>
|
|
|
|
{% block messages %}
|
|
{% if show_messages %}
|
|
{% for message, message_type in show_messages() %}
|
|
<p class="message-{% if message_type == 1 %}info{% elif message_type == 2 %}warning{% elif message_type == 3 %}error{% endif %}">{{ message }}</p>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
<section class="content">
|
|
{% block content %}
|
|
{% endblock %}
|
|
</section>
|
|
</div>
|
|
<footer class="ft">{% block footer %}{% include "_footer.html" %}{% endblock %}</footer>
|
|
|
|
{% include "_close_open_menus.html" %}
|
|
|
|
{% for body_script in body_scripts %}
|
|
<script{% if body_script.module %} type="module"{% endif %}>{{ body_script.script }}</script>
|
|
{% endfor %}
|
|
|
|
{% if select_templates %}<!-- Templates considered: {{ select_templates|join(", ") }} -->{% endif %}
|
|
<script src="{{ urls.static('navigation-search.js') }}" defer></script>
|
|
<navigation-search url="/-/tables"></navigation-search>
|
|
</body>
|
|
</html>
|