Simplified the code for the permission debug pages

Decided not to use as much JavaScript

Used Codex CLI for this. Refs #2543
This commit is contained in:
Simon Willison 2025-10-26 17:28:59 -07:00
commit b018eb3171
6 changed files with 44 additions and 166 deletions

View file

@ -40,26 +40,6 @@ function populateFormFromURL() {
return params;
}
// Update URL with current form values
function updateURL(formId, page = 1) {
const form = document.getElementById(formId);
const formData = new FormData(form);
const params = new URLSearchParams();
for (const [key, value] of formData.entries()) {
if (value) {
params.append(key, value);
}
}
if (page > 1) {
params.set('page', page.toString());
}
const newURL = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
window.history.pushState({}, '', newURL);
}
// HTML escape function
function escapeHtml(text) {
if (text === null || text === undefined) return '';

View file

@ -23,7 +23,7 @@
{% endif %}
<div class="permission-form">
<form id="allowed-form">
<form id="allowed-form" method="get" action="{{ urls.path("-/allowed") }}">
<div class="form-section">
<label for="action">Action (permission name):</label>
<select id="action" name="action" required>
@ -83,23 +83,6 @@ const resultsCount = document.getElementById('results-count');
const pagination = document.getElementById('pagination');
const submitBtn = document.getElementById('submit-btn');
const hasDebugPermission = {{ 'true' if has_debug_permission else 'false' }};
let currentData = null;
form.addEventListener('submit', async (ev) => {
ev.preventDefault();
updateURL('allowed-form', 1);
await fetchResults(1, false);
});
// Handle browser back/forward
window.addEventListener('popstate', () => {
const params = populateFormFromURL();
const action = params.get('action');
const page = params.get('page');
if (action) {
fetchResults(page ? parseInt(page) : 1, false);
}
});
// Populate form on initial load
(function() {
@ -107,11 +90,11 @@ window.addEventListener('popstate', () => {
const action = params.get('action');
const page = params.get('page');
if (action) {
fetchResults(page ? parseInt(page) : 1, false);
fetchResults(page ? parseInt(page) : 1);
}
})();
async function fetchResults(page = 1, updateHistory = true) {
async function fetchResults(page = 1) {
submitBtn.disabled = true;
submitBtn.textContent = 'Loading...';
@ -139,7 +122,6 @@ async function fetchResults(page = 1, updateHistory = true) {
const data = await response.json();
if (response.ok) {
currentData = data;
displayResults(data);
} else {
displayError(data);
@ -198,13 +180,8 @@ function displayResults(data) {
if (data.previous_url || data.next_url) {
if (data.previous_url) {
const prevLink = document.createElement('a');
prevLink.href = '#';
prevLink.href = data.previous_url;
prevLink.textContent = '← Previous';
prevLink.addEventListener('click', (e) => {
e.preventDefault();
updateURL('allowed-form', data.page - 1);
fetchResults(data.page - 1, false);
});
pagination.appendChild(prevLink);
}
@ -214,13 +191,8 @@ function displayResults(data) {
if (data.next_url) {
const nextLink = document.createElement('a');
nextLink.href = '#';
nextLink.href = data.next_url;
nextLink.textContent = 'Next →';
nextLink.addEventListener('click', (e) => {
e.preventDefault();
updateURL('allowed-form', data.page + 1);
fetchResults(data.page + 1, false);
});
pagination.appendChild(nextLink);
}
}

View file

@ -66,7 +66,7 @@
{% endif %}
<div class="permission-form">
<form id="check-form">
<form id="check-form" method="get" action="{{ urls.path("-/check") }}">
<div class="form-section">
<label for="action">Action (permission name):</label>
<select id="action" name="action" required>
@ -159,21 +159,6 @@ async function performCheck() {
}
}
form.addEventListener('submit', async (ev) => {
ev.preventDefault();
updateURL('check-form');
await performCheck();
});
// Handle browser back/forward
window.addEventListener('popstate', () => {
const params = populateFormFromURL();
const action = params.get('action');
if (action) {
performCheck();
}
});
// Populate form on initial load
(function() {
const params = populateFormFromURL();

View file

@ -90,19 +90,13 @@ var permissions = Object.fromEntries(rawPerms.map(p => [p.name, p]));
var permissionSelect = document.getElementById('permission');
var resource1 = document.getElementById('resource_1');
var resource2 = document.getElementById('resource_2');
var resource1Section = resource1.closest('.form-section');
var resource2Section = resource2.closest('.form-section');
function updateResourceVisibility() {
var permission = permissionSelect.value;
var {takes_parent, takes_child} = permissions[permission];
if (takes_parent) {
resource1.closest('p').style.display = 'block';
} else {
resource1.closest('p').style.display = 'none';
}
if (takes_child) {
resource2.closest('p').style.display = 'block';
} else {
resource2.closest('p').style.display = 'none';
}
resource1Section.style.display = takes_parent ? 'block' : 'none';
resource2Section.style.display = takes_child ? 'block' : 'none';
}
permissionSelect.addEventListener('change', updateResourceVisibility);
updateResourceVisibility();
@ -113,14 +107,21 @@ 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),
headers: {
'Accept': 'application/json'
}
}).then(function(response) {
if (!response.ok) {
throw new Error('Request failed with status ' + response.status);
}
return response.json();
}).then(function(data) {
debugResult.innerText = JSON.stringify(data, null, 4);
}).catch(function(error) {
debugResult.innerText = JSON.stringify({ error: error.message }, null, 4);
});
});
</script>

View file

@ -23,7 +23,7 @@
{% endif %}
<div class="permission-form">
<form id="rules-form">
<form id="rules-form" method="get" action="{{ urls.path("-/rules") }}">
<div class="form-section">
<label for="action">Action (permission name):</label>
<select id="action" name="action" required>
@ -70,23 +70,6 @@ const resultsContent = document.getElementById('results-content');
const resultsCount = document.getElementById('results-count');
const pagination = document.getElementById('pagination');
const submitBtn = document.getElementById('submit-btn');
let currentData = null;
form.addEventListener('submit', async (ev) => {
ev.preventDefault();
updateURL('rules-form', 1);
await fetchResults(1, false);
});
// Handle browser back/forward
window.addEventListener('popstate', () => {
const params = populateFormFromURL();
const action = params.get('action');
const page = params.get('page');
if (action) {
fetchResults(page ? parseInt(page) : 1, false);
}
});
// Populate form on initial load
(function() {
@ -94,11 +77,11 @@ window.addEventListener('popstate', () => {
const action = params.get('action');
const page = params.get('page');
if (action) {
fetchResults(page ? parseInt(page) : 1, false);
fetchResults(page ? parseInt(page) : 1);
}
})();
async function fetchResults(page = 1, updateHistory = true) {
async function fetchResults(page = 1) {
submitBtn.disabled = true;
submitBtn.textContent = 'Loading...';
@ -126,7 +109,6 @@ async function fetchResults(page = 1, updateHistory = true) {
const data = await response.json();
if (response.ok) {
currentData = data;
displayResults(data);
} else {
displayError(data);
@ -183,13 +165,8 @@ function displayResults(data) {
if (data.previous_url || data.next_url) {
if (data.previous_url) {
const prevLink = document.createElement('a');
prevLink.href = '#';
prevLink.href = data.previous_url;
prevLink.textContent = '← Previous';
prevLink.addEventListener('click', (e) => {
e.preventDefault();
updateURL('rules-form', data.page - 1);
fetchResults(data.page - 1, false);
});
pagination.appendChild(prevLink);
}
@ -199,22 +176,14 @@ function displayResults(data) {
if (data.next_url) {
const nextLink = document.createElement('a');
nextLink.href = '#';
nextLink.href = data.next_url;
nextLink.textContent = 'Next →';
nextLink.addEventListener('click', (e) => {
e.preventDefault();
updateURL('rules-form', data.page + 1);
fetchResults(data.page + 1, false);
});
pagination.appendChild(nextLink);
}
}
// Update raw JSON
document.getElementById('raw-json').innerHTML = jsonFormatHighlight(data);
// Scroll to results
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function displayError(data) {
@ -225,8 +194,6 @@ function displayError(data) {
resultsContent.innerHTML = `<div class="error-message">Error: ${escapeHtml(data.error || 'Unknown error')}</div>`;
document.getElementById('raw-json').innerHTML = jsonFormatHighlight(data);
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
</script>

View file

@ -180,35 +180,13 @@ class PermissionsDebugView(BaseView):
vars = await request.post_vars()
actor = json.loads(vars["actor"])
permission = vars["permission"]
resource_1 = vars["resource_1"]
resource_2 = vars["resource_2"]
parent = vars.get("resource_1") or None
child = vars.get("resource_2") or None
# Use the action's properties to create the appropriate resource object
action = self.ds.actions.get(permission)
if not action:
return Response.json({"error": f"Unknown action: {permission}"}, status=400)
if action.takes_parent and action.takes_child:
resource_obj = action.resource_class(database=resource_1, table=resource_2)
resource_for_response = (resource_1, resource_2)
elif action.takes_parent:
resource_obj = action.resource_class(database=resource_1)
resource_for_response = resource_1
else:
resource_obj = action.resource_class()
resource_for_response = None
result = await self.ds.allowed(
action=permission, resource=resource_obj, actor=actor
)
return Response.json(
{
"actor": actor,
"permission": permission,
"resource": resource_for_response,
"result": result,
}
response, status = await _check_permission_for_actor(
self.ds, permission, parent, child, actor
)
return Response.json(response, status=status)
class AllowedResourcesView(BaseView):
@ -255,34 +233,35 @@ class AllowedResourcesView(BaseView):
},
)
# JSON API - action parameter is required
payload, status = await self._allowed_payload(request, has_debug_permission)
headers = {}
if self.ds.cors:
add_cors_headers(headers)
return Response.json(payload, status=status, headers=headers)
async def _allowed_payload(self, request, has_debug_permission):
action = request.args.get("action")
if not action:
return Response.json({"error": "action parameter is required"}, status=400)
return {"error": "action parameter is required"}, 400
if action not in self.ds.actions:
return Response.json({"error": f"Unknown action: {action}"}, status=404)
return {"error": f"Unknown action: {action}"}, 404
actor = request.actor if isinstance(request.actor, dict) else None
actor_id = actor.get("id") if actor else None
parent_filter = request.args.get("parent")
child_filter = request.args.get("child")
if child_filter and not parent_filter:
return Response.json(
{"error": "parent must be provided when child is specified"},
status=400,
)
return {"error": "parent must be provided when child is specified"}, 400
try:
page = int(request.args.get("page", "1"))
page_size = int(request.args.get("page_size", "50"))
except ValueError:
return Response.json(
{"error": "page and page_size must be integers"}, status=400
)
return {"error": "page and page_size must be integers"}, 400
if page < 1:
return Response.json({"error": "page must be >= 1"}, status=400)
return {"error": "page must be >= 1"}, 400
if page_size < 1:
return Response.json({"error": "page_size must be >= 1"}, status=400)
return {"error": "page_size must be >= 1"}, 400
max_page_size = 200
if page_size > max_page_size:
page_size = max_page_size
@ -304,10 +283,7 @@ class AllowedResourcesView(BaseView):
)
except Exception:
# If catalog tables don't exist yet, return empty results
headers = {}
if self.ds.cors:
add_cors_headers(headers)
return Response.json(
return (
{
"action": action,
"actor_id": actor_id,
@ -316,7 +292,7 @@ class AllowedResourcesView(BaseView):
"total": 0,
"items": [],
},
headers=headers,
200,
)
# Convert to list of dicts with resource path
@ -396,10 +372,7 @@ class AllowedResourcesView(BaseView):
if page > 1:
response["previous_url"] = build_page_url(page - 1)
headers = {}
if self.ds.cors:
add_cors_headers(headers)
return Response.json(response, headers=headers)
return response, 200
class PermissionRulesView(BaseView):