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; 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 // HTML escape function
function escapeHtml(text) { function escapeHtml(text) {
if (text === null || text === undefined) return ''; if (text === null || text === undefined) return '';

View file

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

View file

@ -66,7 +66,7 @@
{% endif %} {% endif %}
<div class="permission-form"> <div class="permission-form">
<form id="check-form"> <form id="check-form" method="get" action="{{ urls.path("-/check") }}">
<div class="form-section"> <div class="form-section">
<label for="action">Action (permission name):</label> <label for="action">Action (permission name):</label>
<select id="action" name="action" required> <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 // Populate form on initial load
(function() { (function() {
const params = populateFormFromURL(); 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 permissionSelect = document.getElementById('permission');
var resource1 = document.getElementById('resource_1'); var resource1 = document.getElementById('resource_1');
var resource2 = document.getElementById('resource_2'); var resource2 = document.getElementById('resource_2');
var resource1Section = resource1.closest('.form-section');
var resource2Section = resource2.closest('.form-section');
function updateResourceVisibility() { function updateResourceVisibility() {
var permission = permissionSelect.value; var permission = permissionSelect.value;
var {takes_parent, takes_child} = permissions[permission]; var {takes_parent, takes_child} = permissions[permission];
if (takes_parent) { resource1Section.style.display = takes_parent ? 'block' : 'none';
resource1.closest('p').style.display = 'block'; resource2Section.style.display = takes_child ? 'block' : 'none';
} else {
resource1.closest('p').style.display = 'none';
}
if (takes_child) {
resource2.closest('p').style.display = 'block';
} else {
resource2.closest('p').style.display = 'none';
}
} }
permissionSelect.addEventListener('change', updateResourceVisibility); permissionSelect.addEventListener('change', updateResourceVisibility);
updateResourceVisibility(); updateResourceVisibility();
@ -113,14 +107,21 @@ var debugResult = document.getElementById('debugResult');
debugPost.addEventListener('submit', function(ev) { debugPost.addEventListener('submit', function(ev) {
ev.preventDefault(); ev.preventDefault();
var formData = new FormData(debugPost); var formData = new FormData(debugPost);
console.log(formData);
fetch(debugPost.action, { fetch(debugPost.action, {
method: 'POST', method: 'POST',
body: new URLSearchParams(formData), body: new URLSearchParams(formData),
headers: {
'Accept': 'application/json'
}
}).then(function(response) { }).then(function(response) {
if (!response.ok) {
throw new Error('Request failed with status ' + response.status);
}
return response.json(); return response.json();
}).then(function(data) { }).then(function(data) {
debugResult.innerText = JSON.stringify(data, null, 4); debugResult.innerText = JSON.stringify(data, null, 4);
}).catch(function(error) {
debugResult.innerText = JSON.stringify({ error: error.message }, null, 4);
}); });
}); });
</script> </script>

View file

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

View file

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