Added /-/permissions debug tool, closes #788

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

Part of authentication work, refs #699.
This commit is contained in:
Simon Willison 2020-05-31 22:00:36 -07:00
commit dfdbdf378a
8 changed files with 152 additions and 3 deletions

View file

@ -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$",
)

View file

@ -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

View file

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

View file

@ -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 %}

View file

@ -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)},
)