Added /-/permissions debug tool, closes #788

Also started the authentication.rst docs page, refs #786.

Part of authentication work, refs #699.
pull/703/head
Simon Willison 2020-05-31 22:00:36 -07:00
rodzic 57cf5139c5
commit dfdbdf378a
8 zmienionych plików z 152 dodań i 3 usunięć

Wyświetl plik

@ -1,5 +1,6 @@
import asyncio
import collections
import datetime
import hashlib
import itertools
import json
@ -24,7 +25,12 @@ import uvicorn
from .views.base import DatasetteError, ureg, AsgiRouter
from .views.database import DatabaseDownload, DatabaseView
from .views.index import IndexView
from .views.special import JsonDataView, PatternPortfolioView, AuthTokenView
from .views.special import (
JsonDataView,
PatternPortfolioView,
AuthTokenView,
PermissionsDebugView,
)
from .views.table import RowView, TableView
from .renderer import json_renderer
from .database import Database, QueryInterrupted
@ -283,6 +289,7 @@ class Datasette:
pm.hook.prepare_jinja2_environment(env=self.jinja_env)
self._register_renderers()
self.permission_checks = collections.deque(maxlen=30)
self._root_token = os.urandom(32).hex()
def sign(self, value, namespace="default"):
@ -420,6 +427,7 @@ class Datasette:
self, actor, action, resource_type=None, resource_identifier=None, default=False
):
"Check permissions using the permissions_allowed plugin hook"
result = None
for check in pm.hook.permission_allowed(
datasette=self,
actor=actor,
@ -432,8 +440,23 @@ class Datasette:
if asyncio.iscoroutine(check):
check = await check
if check is not None:
return check
return default
result = check
used_default = False
if result is None:
result = default
used_default = True
self.permission_checks.append(
{
"when": datetime.datetime.utcnow().isoformat(),
"actor": actor,
"action": action,
"resource_type": resource_type,
"resource_identifier": resource_identifier,
"used_default": used_default,
"result": result,
}
)
return result
async def execute(
self,
@ -782,6 +805,9 @@ class Datasette:
add_route(
AuthTokenView.as_asgi(self), r"/-/auth-token$",
)
add_route(
PermissionsDebugView.as_asgi(self), r"/-/permissions$",
)
add_route(
PatternPortfolioView.as_asgi(self), r"/-/patterns$",
)

Wyświetl plik

@ -0,0 +1,7 @@
from datasette import hookimpl
@hookimpl
def permission_allowed(actor, action, resource_type, resource_identifier):
if actor and actor.get("id") == "root" and action == "permissions-debug":
return True

Wyświetl plik

@ -10,6 +10,7 @@ DEFAULT_PLUGINS = (
"datasette.facets",
"datasette.sql_functions",
"datasette.actor_auth_cookie",
"datasette.default_permissions",
)
pm = pluggy.PluginManager("datasette")

Wyświetl plik

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}Debug permissions{% endblock %}
{% block extra_head %}
<style type="text/css">
.check-result-true {
color: green;
}
.check-result-false {
color: red;
}
.check h2 {
font-size: 1em
}
.check-action, .check-when, .check-result {
font-size: 1.3em;
}
</style>
{% endblock %}
{% block nav %}
<p class="crumbs">
<a href="{{ base_url }}">home</a>
</p>
{{ super() }}
{% endblock %}
{% block content %}
<h1>Recent permissions checks</h1>
{% for check in permission_checks %}
<div class="check">
<h2>
<span class="check-action">{{ check.action }}</span>
checked at
<span class="check-when">{{ check.when }}</span>
{% if check.result %}
<span class="check-result check-result-true"></span>
{% else %}
<span class="check-result check-result-false"></span>
{% endif %}
{% if check.used_default %}
<span class="check-used-default">(used default)</span>
{% endif %}
</h2>
<p><strong>Actor:</strong> {{ check.actor|tojson }}</p>
{% if check.resource_type %}
<p><strong>Resource:</strong> {{ check.resource_type }}: {{ check.resource_identifier }}</p>
{% endif %}
</div>
{% endfor %}
{% endblock %}

Wyświetl plik

@ -76,3 +76,21 @@ class AuthTokenView(BaseView):
return response
else:
return Response("Invalid token", status=403)
class PermissionsDebugView(BaseView):
name = "permissions_debug"
def __init__(self, datasette):
self.ds = datasette
async def get(self, request):
if not await self.ds.permission_allowed(
request.scope.get("actor"), "permissions-debug"
):
return Response("Permission denied", status=403)
return await self.render(
["permissions_debug.html"],
request,
{"permission_checks": reversed(self.ds.permission_checks)},
)

Wyświetl plik

@ -0,0 +1,18 @@
.. _authentication:
================================
Authentication and permissions
================================
Datasette's authentication system is currently under construction. Follow `issue 699 <https://github.com/simonw/datasette/issues/699>`__ to track the development of this feature.
.. _PermissionsDebugView:
Permissions Debug
=================
The debug tool at ``/-/permissions`` is only available to the root user.
It shows the thirty most recent permission checks that have been carried out by the Datasette instance.
This is designed to help administrators and plugin authors understand exactly how permission checks are being carried out, in order to effectively configure Datasette's permission system.

Wyświetl plik

@ -40,6 +40,7 @@ Contents
publish
json_api
sql_queries
authentication
performance
csv_export
facets

Wyświetl plik

@ -1,4 +1,5 @@
from .fixtures import app_client
from bs4 import BeautifulSoup as Soup
def test_auth_token(app_client):
@ -23,3 +24,25 @@ def test_actor_cookie(app_client):
cookie = app_client.ds.sign({"id": "test"}, "actor")
response = app_client.get("/", cookies={"ds_actor": cookie})
assert {"id": "test"} == app_client.ds._last_request.scope["actor"]
def test_permissions_debug(app_client):
assert 403 == app_client.get("/-/permissions").status
# With the cookie it should work
cookie = app_client.ds.sign({"id": "root"}, "actor")
response = app_client.get("/-/permissions", cookies={"ds_actor": cookie})
# Should show one failure and one success
soup = Soup(response.body, "html.parser")
check_divs = soup.findAll("div", {"class": "check"})
checks = [
{
"action": div.select_one(".check-action").text,
"result": bool(div.select(".check-result-true")),
"used_default": bool(div.select(".check-used-default")),
}
for div in check_divs
]
assert [
{"action": "permissions-debug", "result": True, "used_default": False},
{"action": "permissions-debug", "result": False, "used_default": True},
] == checks