mirror of
https://github.com/simonw/datasette.git
synced 2026-05-28 04:46:18 +02:00
Compare commits
1 commit
main
...
claude/deb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c635a1d99 |
15 changed files with 238 additions and 32 deletions
|
|
@ -1,4 +1,5 @@
|
|||
from datasette.permissions import Permission # noqa
|
||||
from datasette.permissions import DebugItem # noqa
|
||||
from datasette.version import __version_info__, __version__ # noqa
|
||||
from datasette.events import Event # noqa
|
||||
from datasette.utils.asgi import Forbidden, NotFound, Request, Response # noqa
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ from .views.index import IndexView
|
|||
from .views.special import (
|
||||
JsonDataView,
|
||||
PatternPortfolioView,
|
||||
DebugMenuView,
|
||||
AuthTokenView,
|
||||
ApiExplorerView,
|
||||
CreateTokenView,
|
||||
|
|
@ -1996,6 +1997,10 @@ class Datasette:
|
|||
AllowDebugView.as_view(self),
|
||||
r"/-/allow-debug$",
|
||||
)
|
||||
add_route(
|
||||
wrap_view(DebugMenuView, self),
|
||||
r"/-/debug$",
|
||||
)
|
||||
add_route(
|
||||
wrap_view(PatternPortfolioView, self),
|
||||
r"/-/patterns$",
|
||||
|
|
|
|||
95
datasette/default_debug_menu.py
Normal file
95
datasette/default_debug_menu.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from datasette import hookimpl
|
||||
from datasette.permissions import DebugItem
|
||||
|
||||
|
||||
@hookimpl
|
||||
def debug_menu(datasette, actor):
|
||||
async def inner():
|
||||
items = []
|
||||
|
||||
# Items visible to anyone
|
||||
items.append(
|
||||
DebugItem(
|
||||
title="Actor",
|
||||
description="Current authenticated actor",
|
||||
path="/-/actor",
|
||||
)
|
||||
)
|
||||
|
||||
# Items requiring view-instance
|
||||
if await datasette.allowed(action="view-instance", actor=actor):
|
||||
items.extend(
|
||||
[
|
||||
DebugItem(
|
||||
title="Databases",
|
||||
description="Connected databases",
|
||||
path="/-/databases",
|
||||
),
|
||||
DebugItem(
|
||||
title="Installed plugins",
|
||||
description="Plugins currently installed",
|
||||
path="/-/plugins",
|
||||
),
|
||||
DebugItem(
|
||||
title="Version info",
|
||||
description="Python, Datasette and SQLite versions",
|
||||
path="/-/versions",
|
||||
),
|
||||
DebugItem(
|
||||
title="Settings",
|
||||
description="Datasette configuration settings",
|
||||
path="/-/settings",
|
||||
),
|
||||
DebugItem(
|
||||
title="Config",
|
||||
description="Full configuration output",
|
||||
path="/-/config",
|
||||
),
|
||||
DebugItem(
|
||||
title="Threads",
|
||||
description="Active threads",
|
||||
path="/-/threads",
|
||||
),
|
||||
DebugItem(
|
||||
title="Messages",
|
||||
description="Debug the flash messaging system",
|
||||
path="/-/messages",
|
||||
),
|
||||
DebugItem(
|
||||
title="Pattern portfolio",
|
||||
description="Showcase of UI patterns and components",
|
||||
path="/-/patterns",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Items requiring permissions-debug
|
||||
if await datasette.allowed(action="permissions-debug", actor=actor):
|
||||
items.extend(
|
||||
[
|
||||
DebugItem(
|
||||
title="Permissions",
|
||||
description="Debug and test permission checks",
|
||||
path="/-/permissions",
|
||||
),
|
||||
DebugItem(
|
||||
title="Allow rules",
|
||||
description="Debug actor_matches_allow logic",
|
||||
path="/-/allow-debug",
|
||||
),
|
||||
DebugItem(
|
||||
title="Actions",
|
||||
description="Available permission actions",
|
||||
path="/-/actions",
|
||||
),
|
||||
DebugItem(
|
||||
title="Permission rules",
|
||||
description="Permission rules from all sources",
|
||||
path="/-/rules",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
return items
|
||||
|
||||
return inner
|
||||
|
|
@ -8,34 +8,7 @@ def menu_links(datasette, actor):
|
|||
return []
|
||||
|
||||
return [
|
||||
{"href": datasette.urls.path("/-/databases"), "label": "Databases"},
|
||||
{
|
||||
"href": datasette.urls.path("/-/plugins"),
|
||||
"label": "Installed plugins",
|
||||
},
|
||||
{
|
||||
"href": datasette.urls.path("/-/versions"),
|
||||
"label": "Version info",
|
||||
},
|
||||
{
|
||||
"href": datasette.urls.path("/-/settings"),
|
||||
"label": "Settings",
|
||||
},
|
||||
{
|
||||
"href": datasette.urls.path("/-/permissions"),
|
||||
"label": "Debug permissions",
|
||||
},
|
||||
{
|
||||
"href": datasette.urls.path("/-/messages"),
|
||||
"label": "Debug messages",
|
||||
},
|
||||
{
|
||||
"href": datasette.urls.path("/-/allow-debug"),
|
||||
"label": "Debug allow rules",
|
||||
},
|
||||
{"href": datasette.urls.path("/-/threads"), "label": "Debug threads"},
|
||||
{"href": datasette.urls.path("/-/actor"), "label": "Debug actor"},
|
||||
{"href": datasette.urls.path("/-/patterns"), "label": "Pattern portfolio"},
|
||||
{"href": datasette.urls.path("/-/debug"), "label": "Debug"},
|
||||
]
|
||||
|
||||
return inner
|
||||
|
|
|
|||
|
|
@ -137,6 +137,11 @@ def forbidden(datasette, request, message):
|
|||
"""Custom response for a 403 forbidden error"""
|
||||
|
||||
|
||||
@hookspec
|
||||
def debug_menu(datasette, actor, request):
|
||||
"""Return a list of DebugItem objects for the /-/debug page"""
|
||||
|
||||
|
||||
@hookspec
|
||||
def menu_links(datasette, actor, request):
|
||||
"""Links for the navigation menu"""
|
||||
|
|
|
|||
|
|
@ -122,6 +122,13 @@ class AllowedResource(NamedTuple):
|
|||
reason: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class DebugItem:
|
||||
title: str
|
||||
path: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class Action:
|
||||
name: str
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ DEFAULT_PLUGINS = (
|
|||
"datasette.default_magic_parameters",
|
||||
"datasette.blob_renderer",
|
||||
"datasette.default_menu_links",
|
||||
"datasette.default_debug_menu",
|
||||
"datasette.handle_exception",
|
||||
"datasette.forbidden",
|
||||
"datasette.events",
|
||||
|
|
|
|||
23
datasette/templates/debug.html
Normal file
23
datasette/templates/debug.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Debug{% endblock %}
|
||||
|
||||
{% block body_class %}debug{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Debug</h1>
|
||||
|
||||
{% if items %}
|
||||
<ul class="debug-items">
|
||||
{% for item in items %}
|
||||
<li>
|
||||
<a href="{{ urls.path(item.path) }}">{{ item.title }}</a>{% if item.description %}
|
||||
<p>{{ item.description }}</p>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No debug tools available.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -89,6 +89,30 @@ class PatternPortfolioView(View):
|
|||
)
|
||||
|
||||
|
||||
class DebugMenuView(View):
|
||||
async def get(self, request, datasette):
|
||||
from datasette.plugins import pm
|
||||
from datasette.utils import await_me_maybe
|
||||
|
||||
items = []
|
||||
for hook in pm.hook.debug_menu(
|
||||
datasette=datasette,
|
||||
actor=request.actor,
|
||||
request=request,
|
||||
):
|
||||
extra_items = await await_me_maybe(hook)
|
||||
if extra_items:
|
||||
items.extend(extra_items)
|
||||
return Response.html(
|
||||
await datasette.render_template(
|
||||
"debug.html",
|
||||
request=request,
|
||||
view_name="debug",
|
||||
context={"items": items},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AuthTokenView(BaseView):
|
||||
name = "auth_token"
|
||||
has_json_alternate = False
|
||||
|
|
|
|||
|
|
@ -1758,6 +1758,53 @@ This example will disable CSRF protection for that specific URL path:
|
|||
|
||||
If any of the currently active ``skip_csrf()`` plugin hooks return ``True``, CSRF protection will be skipped for the request.
|
||||
|
||||
.. _plugin_hook_debug_menu:
|
||||
|
||||
debug_menu(datasette, actor, request)
|
||||
-------------------------------------
|
||||
|
||||
``datasette`` - :ref:`internals_datasette`
|
||||
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries.
|
||||
|
||||
``actor`` - dictionary or None
|
||||
The currently authenticated :ref:`actor <authentication_actor>`.
|
||||
|
||||
``request`` - :ref:`internals_request` or None
|
||||
The current HTTP request. This can be ``None`` if the request object is not available.
|
||||
|
||||
This hook allows plugins to add items to the ``/-/debug`` page, which serves as a hub for all debug and diagnostic tools.
|
||||
|
||||
The hook should return a list of ``DebugItem`` objects (importable from ``datasette.permissions``). Each item has a ``title``, an optional ``description``, and a ``path`` to link to.
|
||||
|
||||
It can alternatively return an ``async def`` awaitable function which returns a list of ``DebugItem`` objects.
|
||||
|
||||
Hook implementations are responsible for checking permissions before returning items - only return items that the current actor should be able to see.
|
||||
|
||||
This example adds a debug item only if the actor has the ``view-instance`` permission:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from datasette import hookimpl
|
||||
from datasette.permissions import DebugItem
|
||||
|
||||
|
||||
@hookimpl
|
||||
def debug_menu(datasette, actor):
|
||||
async def inner():
|
||||
if not await datasette.allowed(
|
||||
action="view-instance", actor=actor
|
||||
):
|
||||
return []
|
||||
return [
|
||||
DebugItem(
|
||||
title="My debug tool",
|
||||
description="Custom diagnostic page",
|
||||
path="/-/my-debug-tool",
|
||||
)
|
||||
]
|
||||
|
||||
return inner
|
||||
|
||||
.. _plugin_hook_menu_links:
|
||||
|
||||
menu_links(datasette, actor, request)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ EXPECTED_PLUGINS = [
|
|||
"asgi_wrapper",
|
||||
"canned_queries",
|
||||
"database_actions",
|
||||
"debug_menu",
|
||||
"extra_body_script",
|
||||
"extra_css_urls",
|
||||
"extra_js_urls",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import asyncio
|
|||
from datasette import hookimpl
|
||||
from datasette.facets import Facet
|
||||
from datasette import tracer
|
||||
from datasette.permissions import Action
|
||||
from datasette.permissions import Action, DebugItem
|
||||
from datasette.resources import DatabaseResource
|
||||
from datasette.utils import path_with_added_args
|
||||
from datasette.utils.asgi import asgi_send_json, Response
|
||||
|
|
@ -354,6 +354,18 @@ def forbidden(datasette, request, message):
|
|||
return Response.redirect("/login?message=" + message)
|
||||
|
||||
|
||||
@hookimpl
|
||||
def debug_menu(datasette, actor, request):
|
||||
if actor:
|
||||
return [
|
||||
DebugItem(
|
||||
title="Test debug item",
|
||||
description="From test plugin",
|
||||
path="/-/plugins",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@hookimpl
|
||||
def menu_links(datasette, actor, request):
|
||||
if actor:
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ def documented_views():
|
|||
if first_word.endswith("View"):
|
||||
view_labels.add(first_word)
|
||||
# We deliberately don't document these:
|
||||
view_labels.update(("PatternPortfolioView", "AuthTokenView", "ApiExplorerView"))
|
||||
view_labels.update(("PatternPortfolioView", "AuthTokenView", "ApiExplorerView", "DebugMenuView"))
|
||||
return view_labels
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -955,8 +955,8 @@ def test_edit_sql_link_not_shown_if_user_lacks_permission(has_permission):
|
|||
"actor_id,should_have_links,should_not_have_links",
|
||||
[
|
||||
(None, None, None),
|
||||
("test", None, ["/-/permissions"]),
|
||||
("root", ["/-/permissions", "/-/allow-debug"], None),
|
||||
("test", None, ["/-/debug"]),
|
||||
("root", ["/-/debug"], None),
|
||||
],
|
||||
)
|
||||
async def test_navigation_menu_links(
|
||||
|
|
|
|||
|
|
@ -927,6 +927,18 @@ async def test_hook_handle_exception_custom_response(ds_client, param):
|
|||
assert response.text == param
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hook_debug_menu(ds_client):
|
||||
# Without authentication, no plugin debug items
|
||||
response = await ds_client.get("/-/debug")
|
||||
assert "Test debug item" not in response.text
|
||||
|
||||
# With authentication, plugin debug items appear
|
||||
response_2 = await ds_client.get("/-/debug?_bot=1")
|
||||
assert "Test debug item" in response_2.text
|
||||
assert "From test plugin" in response_2.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hook_menu_links(ds_client):
|
||||
def get_menu_links(html):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue