kopia lustrzana https://github.com/simonw/datasette
Added /-/permissions debug tool, closes #788
Also started the authentication.rst docs page, refs #786. Part of authentication work, refs #699.pull/703/head
rodzic
57cf5139c5
commit
dfdbdf378a
|
|
@ -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$",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -10,6 +10,7 @@ DEFAULT_PLUGINS = (
|
|||
"datasette.facets",
|
||||
"datasette.sql_functions",
|
||||
"datasette.actor_auth_cookie",
|
||||
"datasette.default_permissions",
|
||||
)
|
||||
|
||||
pm = pluggy.PluginManager("datasette")
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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)},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -40,6 +40,7 @@ Contents
|
|||
publish
|
||||
json_api
|
||||
sql_queries
|
||||
authentication
|
||||
performance
|
||||
csv_export
|
||||
facets
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue