Add access key status management to Admin UI (#8050)

* Add access key status management to Admin UI

- Add Status field to AccessKeyInfo struct
- Implement UpdateAccessKeyStatus API endpoint
- Add status dropdown in access keys modal
- Fix modal backdrop issue by using refreshAccessKeysList helper
- Status can be toggled between Active and Inactive

* Replace magic strings with constants for access key status

- Define AccessKeyStatusActive and AccessKeyStatusInactive constants in admin_data.go
- Define STATUS_ACTIVE and STATUS_INACTIVE constants in JavaScript
- Replace all hardcoded 'Active' and 'Inactive' strings with constants
- Update error messages to use constants for consistency

* Remove duplicate manageAccessKeys function definition

* Add security improvements to access key status management

- Add status validation in UpdateAccessKeyStatus to prevent invalid values
- Fix XSS vulnerability by replacing inline onchange with data attributes
- Add delegated event listener for status select changes
- Add URL encoding to API request path segments
This commit is contained in:
Chris Lu authored and GitHub committed 2026-01-17 18:18:32 -08:00
1 parent dbde8983a7
commit 6bc5a64a98
6 files changed
+159 -12

No files matched your search

+63 -11
View File
@@ -396,6 +396,10 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
<!-- JavaScript for user management -->
<script>
// Access key status constants
const STATUS_ACTIVE = 'Active';
const STATUS_INACTIVE = 'Inactive';
document.addEventListener('DOMContentLoaded', function() {
// Event delegation for user action buttons
@@ -432,6 +436,17 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
}
});
// Event delegation for access key status changes
document.addEventListener('change', function(e) {
const statusSelect = e.target.closest('.access-key-status-select');
if (statusSelect) {
const username = statusSelect.getAttribute('data-username');
const accessKey = statusSelect.getAttribute('data-access-key');
const newStatus = statusSelect.value;
updateAccessKeyStatus(username, accessKey, newStatus);
}
});
// Load policies for dropdowns
loadPolicies();
@@ -973,7 +988,12 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
user.access_keys.forEach(function(key) {
keysHtml += '<tr>';
keysHtml += '<td><code>' + escapeHtml(key.access_key) + '</code></td>';
keysHtml += '<td><span class="badge bg-success">Active</span></td>';
keysHtml += '<td>';
keysHtml += '<select class="form-select form-select-sm access-key-status-select" data-username="' + escapeHtml(user.username) + '" data-access-key="' + escapeHtml(key.access_key) + '" style="width: 110px;">';
keysHtml += '<option value="' + STATUS_ACTIVE + '" ' + (key.status === STATUS_ACTIVE || !key.status ? 'selected' : '') + '>' + STATUS_ACTIVE + '</option>';
keysHtml += '<option value="' + STATUS_INACTIVE + '" ' + (key.status === STATUS_INACTIVE ? 'selected' : '') + '>' + STATUS_INACTIVE + '</option>';
keysHtml += '</select>';
keysHtml += '</td>';
keysHtml += '<td>';
// Add "View Secret" button with data attributes
keysHtml += '<button class="btn btn-outline-secondary btn-sm me-2 view-secret-btn" data-access-key="' + escapeHtml(key.access_key) + '" data-secret-key="' + escapeHtml(key.secret_key) + '">';
@@ -1005,6 +1025,46 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
return keysHtml;
}
// Refresh access keys list content
async function refreshAccessKeysList(username) {
try {
const response = await fetch(`/api/users/${username}`);
if (response.ok) {
const user = await response.json();
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
}
} catch (error) {
console.error('Error refreshing access keys:', error);
}
}
// Update access key status
async function updateAccessKeyStatus(username, accessKey, status) {
try {
const response = await fetch(`/api/users/${encodeURIComponent(username)}/access-keys/${encodeURIComponent(accessKey)}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: status })
});
if (response.ok) {
showSuccessMessage('Access key status updated successfully');
// Refresh access keys display without toggling modal
refreshAccessKeysList(username);
} else {
const error = await response.json();
showErrorMessage('Failed to update access key status: ' + (error.error || 'Unknown error'));
refreshAccessKeysList(username);
}
} catch (error) {
console.error('Error updating access key status:', error);
showErrorMessage('Failed to update access key status: ' + error.message);
refreshAccessKeysList(username);
}
}
// Create new access key
async function createAccessKey() {
const username = document.getElementById('accessKeysUsername').textContent;
@@ -1029,11 +1089,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
showSuccessMessage('Access key created successfully');
// Refresh access keys display
const userResponse = await fetch(`/api/users/${username}`);
if (userResponse.ok) {
const user = await userResponse.json();
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
}
refreshAccessKeysList(username);
} else {
const error = await response.json();
showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error'));
@@ -1056,11 +1112,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
showSuccessMessage('Access key deleted successfully');
// Refresh access keys display
const userResponse = await fetch(`/api/users/${username}`);
if (userResponse.ok) {
const user = await userResponse.json();
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
}
refreshAccessKeysList(username);
} else {
const error = await response.json();
showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error'));
@@ -193,7 +193,7 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</small></div></div></div><!-- Create User Modal --><div class=\"modal fade\" id=\"createUserModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user-plus me-2\"></i>Create New User</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"createUserForm\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username *</label> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"email\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" id=\"email\" name=\"email\"></div><div class=\"mb-3\"><label for=\"actions\" class=\"form-label\">Permissions</label> <select multiple class=\"form-control\" id=\"actions\" name=\"actions\" size=\"10\"><option value=\"Admin\">Admin (Full Access)</option> <option value=\"Read\">Read</option> <option value=\"Write\">Write</option> <option value=\"List\">List</option> <option value=\"Tagging\">Tagging</option> <optgroup label=\"Object Lock Permissions\"><option value=\"BypassGovernanceRetention\">Bypass Governance Retention</option> <option value=\"GetObjectRetention\">Get Object Retention</option> <option value=\"PutObjectRetention\">Put Object Retention</option> <option value=\"GetObjectLegalHold\">Get Object Legal Hold</option> <option value=\"PutObjectLegalHold\">Put Object Legal Hold</option> <option value=\"GetBucketObjectLockConfiguration\">Get Bucket Object Lock Configuration</option> <option value=\"PutBucketObjectLockConfiguration\">Put Bucket Object Lock Configuration</option></optgroup></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple permissions</small></div><div class=\"mb-3\"><label class=\"form-label\">Bucket Scope</label> <small class=\"form-text text-muted d-block mb-2\">Apply selected permissions to specific buckets or all buckets</small><div class=\"form-check mb-2\"><input class=\"form-check-input\" type=\"radio\" name=\"bucketScope\" id=\"allBuckets\" value=\"all\" checked onchange=\"toggleBucketList()\"> <label class=\"form-check-label\" for=\"allBuckets\">All Buckets</label></div><div class=\"form-check mb-2\"><input class=\"form-check-input\" type=\"radio\" name=\"bucketScope\" id=\"specificBuckets\" value=\"specific\" onchange=\"toggleBucketList()\"> <label class=\"form-check-label\" for=\"specificBuckets\">Specific Buckets</label></div><div id=\"bucketSelectionList\" class=\"mt-2\" style=\"display: none;\"><select multiple class=\"form-select\" id=\"selectedBuckets\" size=\"5\"><!-- Options loaded dynamically --></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple buckets</small></div></div><div class=\"mb-3\"><label for=\"policies\" class=\"form-label\">Attached Policies</label> <select multiple class=\"form-control\" id=\"policies\" name=\"policies\" size=\"5\"><!-- Options loaded dynamically --></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple policies</small></div><div class=\"mb-3 form-check\"><input type=\"checkbox\" class=\"form-check-input\" id=\"generateKey\" name=\"generateKey\" checked> <label class=\"form-check-label\" for=\"generateKey\">Generate access key automatically</label></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"handleCreateUser()\">Create User</button></div></div></div></div><!-- Edit User Modal --><div class=\"modal fade\" id=\"editUserModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user-edit me-2\"></i>Edit User</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"editUserForm\"><input type=\"hidden\" id=\"editUsername\" name=\"username\"><div class=\"mb-3\"><label for=\"editEmail\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" id=\"editEmail\" name=\"email\"></div><div class=\"mb-3\"><label for=\"editActions\" class=\"form-label\">Permissions</label> <select multiple class=\"form-control\" id=\"editActions\" name=\"actions\" size=\"10\"><option value=\"Admin\">Admin (Full Access)</option> <option value=\"Read\">Read</option> <option value=\"Write\">Write</option> <option value=\"List\">List</option> <option value=\"Tagging\">Tagging</option> <optgroup label=\"Object Lock Permissions\"><option value=\"BypassGovernanceRetention\">Bypass Governance Retention</option> <option value=\"GetObjectRetention\">Get Object Retention</option> <option value=\"PutObjectRLine truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</small></div></div></div><!-- Create User Modal --><div class=\"modal fade\" id=\"createUserModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user-plus me-2\"></i>Create New User</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"createUserForm\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username *</label> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"email\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" id=\"email\" name=\"email\"></div><div class=\"mb-3\"><label for=\"actions\" class=\"form-label\">Permissions</label> <select multiple class=\"form-control\" id=\"actions\" name=\"actions\" size=\"10\"><option value=\"Admin\">Admin (Full Access)</option> <option value=\"Read\">Read</option> <option value=\"Write\">Write</option> <option value=\"List\">List</option> <option value=\"Tagging\">Tagging</option> <optgroup label=\"Object Lock Permissions\"><option value=\"BypassGovernanceRetention\">Bypass Governance Retention</option> <option value=\"GetObjectRetention\">Get Object Retention</option> <option value=\"PutObjectRetention\">Put Object Retention</option> <option value=\"GetObjectLegalHold\">Get Object Legal Hold</option> <option value=\"PutObjectLegalHold\">Put Object Legal Hold</option> <option value=\"GetBucketObjectLockConfiguration\">Get Bucket Object Lock Configuration</option> <option value=\"PutBucketObjectLockConfiguration\">Put Bucket Object Lock Configuration</option></optgroup></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple permissions</small></div><div class=\"mb-3\"><label class=\"form-label\">Bucket Scope</label> <small class=\"form-text text-muted d-block mb-2\">Apply selected permissions to specific buckets or all buckets</small><div class=\"form-check mb-2\"><input class=\"form-check-input\" type=\"radio\" name=\"bucketScope\" id=\"allBuckets\" value=\"all\" checked onchange=\"toggleBucketList()\"> <label class=\"form-check-label\" for=\"allBuckets\">All Buckets</label></div><div class=\"form-check mb-2\"><input class=\"form-check-input\" type=\"radio\" name=\"bucketScope\" id=\"specificBuckets\" value=\"specific\" onchange=\"toggleBucketList()\"> <label class=\"form-check-label\" for=\"specificBuckets\">Specific Buckets</label></div><div id=\"bucketSelectionList\" class=\"mt-2\" style=\"display: none;\"><select multiple class=\"form-select\" id=\"selectedBuckets\" size=\"5\"><!-- Options loaded dynamically --></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple buckets</small></div></div><div class=\"mb-3\"><label for=\"policies\" class=\"form-label\">Attached Policies</label> <select multiple class=\"form-control\" id=\"policies\" name=\"policies\" size=\"5\"><!-- Options loaded dynamically --></select> <small class=\"form-text text-muted\">Hold Ctrl/Cmd to select multiple policies</small></div><div class=\"mb-3 form-check\"><input type=\"checkbox\" class=\"form-check-input\" id=\"generateKey\" name=\"generateKey\" checked> <label class=\"form-check-label\" for=\"generateKey\">Generate access key automatically</label></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"handleCreateUser()\">Create User</button></div></div></div></div><!-- Edit User Modal --><div class=\"modal fade\" id=\"editUserModal\" tabindex=\"-1\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"fas fa-user-edit me-2\"></i>Edit User</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\"></button></div><div class=\"modal-body\"><form id=\"editUserForm\"><input type=\"hidden\" id=\"editUsername\" name=\"username\"><div class=\"mb-3\"><label for=\"editEmail\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" id=\"editEmail\" name=\"email\"></div><div class=\"mb-3\"><label for=\"editActions\" class=\"form-label\">Permissions</label> <select multiple class=\"form-control\" id=\"editActions\" name=\"actions\" size=\"10\"><option value=\"Admin\">Admin (Full Access)</option> <option value=\"Read\">Read</option> <option value=\"Write\">Write</option> <option value=\"List\">List</option> <option value=\"Tagging\">Tagging</option> <optgroup label=\"Object Lock Permissions\"><option value=\"BypassGovernanceRetention\">Bypass Governance Retention</option> <option value=\"GetObjectRetention\">Get Object Retention</option> <option value=\"PutObjectRLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}