Skip to content

Commit

Permalink
Permission check testing tool, refs #1881
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Nov 3, 2022
1 parent 9b5a73b commit c51d924
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 1 deletion.
19 changes: 19 additions & 0 deletions datasette/permissions.py
@@ -0,0 +1,19 @@
import collections

Permission = collections.namedtuple(
"Permission", ("name", "abbr", "takes_database", "takes_table", "default")
)

PERMISSIONS = (
Permission("view-instance", "vi", False, False, True),
Permission("view-database", "vd", True, False, True),
Permission("view-database-download", "vdd", True, False, True),
Permission("view-table", "vt", True, True, True),
Permission("view-query", "vq", True, True, True),
Permission("insert-row", "ir", True, True, False),
Permission("delete-row", "dr", True, True, False),
Permission("drop-table", "dt", True, True, False),
Permission("execute-sql", "es", True, False, True),
Permission("permissions-debug", "pd", False, False, False),
Permission("debug-menu", "dm", False, False, False),
)
86 changes: 86 additions & 0 deletions datasette/templates/permissions_debug.html
Expand Up @@ -19,11 +19,97 @@
.check-action, .check-when, .check-result {
font-size: 1.3em;
}
textarea {
height: 10em;
width: 95%;
box-sizing: border-box;
padding: 0.5em;
border: 2px dotted black;
}
.two-col {
display: inline-block;
width: 48%;
}
.two-col label {
width: 48%;
}
@media only screen and (max-width: 576px) {
.two-col {
width: 100%;
}
}
</style>
{% endblock %}

{% block content %}

<h1>Permission check testing tool</h1>

<p>This tool lets you simulate an actor and a permission check for that actor.</p>

<form action="{{ urls.path('-/permissions') }}" id="debug-post" method="post" style="margin-bottom: 1em">
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
<div class="two-col">
<p><label>Actor</label></p>
<textarea name="actor">{% if actor_input %}{{ actor_input }}{% else %}{"id": "root"}{% endif %}</textarea>
</div>
<div class="two-col" style="vertical-align: top">
<p><label for="permission" style="display:block">Permission</label>
<select name="permission" id="permission">
{% for permission in permissions %}
<option value="{{ permission.0 }}">{{ permission.name }} (default {{ permission.default }})</option>
{% endfor %}
</select>
<p><label for="resource_1">Database name</label><input type="text" id="resource_1" name="resource_1"></p>
<p><label for="resource_2">Table or query name</label><input type="text" id="resource_2" name="resource_2"></p>
</div>
<div style="margin-top: 1em;">
<input type="submit" value="Simulate permission check">
</div>
<pre style="margin-top: 1em" id="debugResult"></pre>
</form>

<script>
var rawPerms = {{ permissions|tojson }};
var permissions = Object.fromEntries(rawPerms.map(([label, abbr, needs_resource_1, needs_resource_2, def]) => [label, {needs_resource_1, needs_resource_2, def}]))
var permissionSelect = document.getElementById('permission');
var resource1 = document.getElementById('resource_1');
var resource2 = document.getElementById('resource_2');
function updateResourceVisibility() {
var permission = permissionSelect.value;
var {needs_resource_1, needs_resource_2} = permissions[permission];
if (needs_resource_1) {
resource1.closest('p').style.display = 'block';
} else {
resource1.closest('p').style.display = 'none';
}
if (needs_resource_2) {
resource2.closest('p').style.display = 'block';
} else {
resource2.closest('p').style.display = 'none';
}
}
permissionSelect.addEventListener('change', updateResourceVisibility);
updateResourceVisibility();

// When #debug-post form is submitted, use fetch() to POST data
var debugPost = document.getElementById('debug-post');
var debugResult = document.getElementById('debugResult');
debugPost.addEventListener('submit', function(ev) {
ev.preventDefault();
var formData = new FormData(debugPost);
console.log(formData);
fetch(debugPost.action, {
method: 'POST',
body: new URLSearchParams(formData),
}).then(function(response) {
return response.json();
}).then(function(data) {
debugResult.innerText = JSON.stringify(data, null, 4);
});
});
</script>

<h1>Recent permissions checks</h1>

{% for check in permission_checks %}
Expand Down
34 changes: 33 additions & 1 deletion datasette/views/special.py
@@ -1,6 +1,8 @@
import json
from datasette.permissions import PERMISSIONS
from datasette.utils.asgi import Response, Forbidden
from datasette.utils import actor_matches_allow, add_cors_headers
from datasette.permissions import PERMISSIONS
from .base import BaseView
import secrets
import time
Expand Down Expand Up @@ -103,7 +105,37 @@ async def get(self, request):
["permissions_debug.html"],
request,
# list() avoids error if check is performed during template render:
{"permission_checks": list(reversed(self.ds._permission_checks))},
{
"permission_checks": list(reversed(self.ds._permission_checks)),
"permissions": PERMISSIONS,
},
)

async def post(self, request):
await self.ds.ensure_permissions(request.actor, ["view-instance"])
if not await self.ds.permission_allowed(request.actor, "permissions-debug"):
raise Forbidden("Permission denied")
vars = await request.post_vars()
actor = json.loads(vars["actor"])
permission = vars["permission"]
resource_1 = vars["resource_1"]
resource_2 = vars["resource_2"]
resource = []
if resource_1:
resource.append(resource_1)
if resource_2:
resource.append(resource_2)
resource = tuple(resource)
result = await self.ds.permission_allowed(
actor, permission, resource, default="USE_DEFAULT"
)
return Response.json(
{
"actor": actor,
"permission": permission,
"resource": resource,
"result": result,
}
)


Expand Down

0 comments on commit c51d924

Please sign in to comment.