mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
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:
parent
73014abe8b
commit
b018eb3171
6 changed files with 44 additions and 166 deletions
|
|
@ -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 '';
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue