From dbde8983a71f41c760d0a0739c5f96f495bef827 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sat, 17 Jan 2026 12:54:21 -0800 Subject: [PATCH] Fix bucket permission persistence in Admin UI (#8049) Fix bucket permission persistence and security issues (#7226) Security Fixes: - Fix XSS vulnerability in showModal by using DOM methods instead of template strings for title - Add escapeHtmlForAttribute helper to properly escape all HTML entities (&, <, >, ", ') - Fix XSS in showSecretKey and showNewAccessKeyModal by using proper HTML escaping - Fix XSS in createAccessKeysContent by replacing inline onclick with data attributes and event delegation Code Cleanup: - Remove debug label "(DEBUG)" from page header - Remove debug console.log statements from buildBucketPermissionsNew - Remove dead functions: addBucketPermissionRow, removeBucketPermissionRow, parseBucketPermissions, buildBucketPermissions Validation Improvements: - Add validation in handleUpdateUser to prevent empty permissions submission - Update buildBucketPermissionsNew to return null when no buckets selected (instead of empty array) - Add proper error messages for validation failures UI Improvements: - Enhanced access key management with proper modals and copy buttons - Improved copy-to-clipboard functionality with fallbacks Fixes #7226 --- weed/admin/static/js/admin.js | 509 ++++-------------- weed/admin/view/app/object_store_users.templ | 371 ++++++++++++- .../view/app/object_store_users_templ.go | 2 +- 3 files changed, 465 insertions(+), 417 deletions(-) diff --git a/weed/admin/static/js/admin.js b/weed/admin/static/js/admin.js index 5d74f6e40..6dc11380c 100644 --- a/weed/admin/static/js/admin.js +++ b/weed/admin/static/js/admin.js @@ -2095,388 +2095,95 @@ function escapeHtml(text) { return text.replace(/[&<>"']/g, function (m) { return map[m]; }); } + // ============================================================================ -// USER MANAGEMENT FUNCTIONS +// SHARED MODAL UTILITIES FOR ACCESS KEY MANAGEMENT // ============================================================================ -// Global variables for user management -let currentEditingUser = ''; -let currentAccessKeysUser = ''; - -// User Management Functions - -async function handleCreateUser() { - const form = document.getElementById('createUserForm'); - const formData = new FormData(form); - - // Get selected actions - const actionsSelect = document.getElementById('actions'); - const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value); - - const userData = { - username: formData.get('username'), - email: formData.get('email'), - actions: selectedActions, - generate_key: formData.get('generateKey') === 'on' - }; - - try { - const response = await fetch('/api/users', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(userData) - }); - - if (response.ok) { - const result = await response.json(); - showSuccessMessage('User created successfully'); - - // Show the created access key if generated - if (result.user && result.user.access_key) { - showNewAccessKeyModal(result.user); - } - - // Close modal and refresh page - const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal')); - modal.hide(); - form.reset(); - setTimeout(() => window.location.reload(), 1000); - } else { - const error = await response.json(); - showErrorMessage('Failed to create user: ' + (error.error || 'Unknown error')); - } - } catch (error) { - console.error('Error creating user:', error); - showErrorMessage('Failed to create user: ' + error.message); - } +// HTML escaping helper to prevent XSS +function escapeHtmlForAttribute(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML.replace(/"/g, '"').replace(/'/g, '''); } -async function editUser(username) { - currentEditingUser = username; - - try { - const response = await fetch(`/api/users/${username}`); - if (response.ok) { - const user = await response.json(); - - // Populate edit form - document.getElementById('editUsername').value = username; - document.getElementById('editEmail').value = user.email || ''; - - // Set selected actions - const actionsSelect = document.getElementById('editActions'); - Array.from(actionsSelect.options).forEach(option => { - option.selected = user.actions && user.actions.includes(option.value); - }); - - // Set selected policies - const policiesSelect = document.getElementById('editPolicies'); - if (policiesSelect) { - Array.from(policiesSelect.options).forEach(option => { - option.selected = user.policy_names && user.policy_names.includes(option.value); - }); - } - - // Show modal - const modal = new bootstrap.Modal(document.getElementById('editUserModal')); - modal.show(); - } else { - showErrorMessage('Failed to load user details'); - } - } catch (error) { - console.error('Error loading user:', error); - showErrorMessage('Failed to load user details'); - } -} - -async function handleUpdateUser() { - const form = document.getElementById('editUserForm'); - const formData = new FormData(form); - - // Get selected actions - const actionsSelect = document.getElementById('editActions'); - const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value); - - // Get selected policies - const policiesSelect = document.getElementById('editPolicies'); - const selectedPolicies = policiesSelect ? Array.from(policiesSelect.selectedOptions).map(option => option.value) : []; +function showModal(title, content) { + // Create a dynamic modal + const modalId = 'dynamicModal_' + Date.now(); - const userData = { - email: formData.get('email'), - actions: selectedActions, - policy_names: selectedPolicies - }; + // Create modal structure using DOM to prevent XSS in title + const modalDiv = document.createElement('div'); + modalDiv.className = 'modal fade'; + modalDiv.id = modalId; + modalDiv.setAttribute('tabindex', '-1'); + modalDiv.setAttribute('role', 'dialog'); - try { - const response = await fetch(`/api/users/${currentEditingUser}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(userData) - }); + const modalDialog = document.createElement('div'); + modalDialog.className = 'modal-dialog'; + modalDialog.setAttribute('role', 'document'); - if (response.ok) { - showSuccessMessage('User updated successfully'); + const modalContent = document.createElement('div'); + modalContent.className = 'modal-content'; - // Close modal and refresh page - const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal')); - modal.hide(); - setTimeout(() => window.location.reload(), 1000); - } else { - const error = await response.json(); - showErrorMessage('Failed to update user: ' + (error.error || 'Unknown error')); - } - } catch (error) { - console.error('Error updating user:', error); - showErrorMessage('Failed to update user: ' + error.message); - } -} + // Header + const modalHeader = document.createElement('div'); + modalHeader.className = 'modal-header'; -function confirmDeleteUser(username) { - confirmAction( - `Are you sure you want to delete user "${username}"? This action cannot be undone.`, - () => deleteUserConfirmed(username) - ); -} + const modalTitle = document.createElement('h5'); + modalTitle.className = 'modal-title'; + modalTitle.textContent = title; // Safe - uses textContent -function deleteUser(username) { - confirmDeleteUser(username); -} + const closeButton = document.createElement('button'); + closeButton.type = 'button'; + closeButton.className = 'btn-close'; + closeButton.setAttribute('data-bs-dismiss', 'modal'); -async function deleteUserConfirmed(username) { - try { - const response = await fetch(`/api/users/${username}`, { - method: 'DELETE' - }); + modalHeader.appendChild(modalTitle); + modalHeader.appendChild(closeButton); - if (response.ok) { - showSuccessMessage('User deleted successfully'); - setTimeout(() => window.location.reload(), 1000); - } else { - const error = await response.json(); - showErrorMessage('Failed to delete user: ' + (error.error || 'Unknown error')); - } - } catch (error) { - console.error('Error deleting user:', error); - showErrorMessage('Failed to delete user: ' + error.message); - } -} + // Body (content may contain HTML, so use innerHTML) + const modalBody = document.createElement('div'); + modalBody.className = 'modal-body'; + modalBody.innerHTML = content; -async function showUserDetails(username) { - try { - const response = await fetch(`/api/users/${username}`); - if (response.ok) { - const user = await response.json(); + // Footer + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; - const content = createUserDetailsContent(user); - document.getElementById('userDetailsContent').innerHTML = content; + const closeFooterButton = document.createElement('button'); + closeFooterButton.type = 'button'; + closeFooterButton.className = 'btn btn-secondary'; + closeFooterButton.setAttribute('data-bs-dismiss', 'modal'); + closeFooterButton.textContent = 'Close'; - const modal = new bootstrap.Modal(document.getElementById('userDetailsModal')); - modal.show(); - } else { - showErrorMessage('Failed to load user details'); - } - } catch (error) { - console.error('Error loading user details:', error); - showErrorMessage('Failed to load user details'); - } -} + modalFooter.appendChild(closeFooterButton); -function createUserDetailsContent(user) { - return ` -
-
-
Basic Information
- - - - - - - - - -
Username:${escapeHtml(user.username)}
Email:${escapeHtml(user.email || 'Not set')}
-
-
-
Permissions
-
- ${user.actions && user.actions.length > 0 ? - user.actions.map(action => `${action}`).join('') : - 'No permissions assigned' - } -
- -
Access Keys
- ${user.access_keys && user.access_keys.length > 0 ? - createAccessKeysTable(user.access_keys) : - '

No access keys

' - } -
-
- `; -} + // Assemble modal + modalContent.appendChild(modalHeader); + modalContent.appendChild(modalBody); + modalContent.appendChild(modalFooter); + modalDialog.appendChild(modalContent); + modalDiv.appendChild(modalDialog); -function createAccessKeysTable(accessKeys) { - return ` -
- - - - - - - - - ${accessKeys.map(key => ` - - - - - `).join('')} - -
Access KeyCreated
${key.access_key}${new Date(key.created_at).toLocaleDateString()}
-
- `; -} - -async function manageAccessKeys(username) { - currentAccessKeysUser = username; - document.getElementById('accessKeysUsername').textContent = username; - - await loadAccessKeys(username); + // Add modal to body + document.body.appendChild(modalDiv); - const modal = new bootstrap.Modal(document.getElementById('accessKeysModal')); + // Show modal + const modal = new bootstrap.Modal(document.getElementById(modalId)); modal.show(); -} - -async function loadAccessKeys(username) { - try { - const response = await fetch(`/api/users/${username}`); - if (response.ok) { - const user = await response.json(); - - const content = createAccessKeysManagementContent(user.access_keys || []); - document.getElementById('accessKeysContent').innerHTML = content; - } else { - document.getElementById('accessKeysContent').innerHTML = '

Failed to load access keys

'; - } - } catch (error) { - console.error('Error loading access keys:', error); - document.getElementById('accessKeysContent').innerHTML = '

Error loading access keys

'; - } -} - -function createAccessKeysManagementContent(accessKeys) { - if (accessKeys.length === 0) { - return '

No access keys found. Create one to get started.

'; - } - - return ` -
- - - - - - - - - - - ${accessKeys.map(key => ` - - - - - - - `).join('')} - -
Access KeySecret KeyCreatedActions
- ${key.access_key} - - - •••••••••••••••• - - ${new Date(key.created_at).toLocaleDateString()} - -
-
- `; -} - -async function createAccessKey() { - if (!currentAccessKeysUser) { - showErrorMessage('No user selected'); - return; - } - - try { - const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - } - }); - - if (response.ok) { - const result = await response.json(); - showSuccessMessage('Access key created successfully'); - - // Show the new access key - showNewAccessKeyModal(result.access_key); - - // Reload access keys - await loadAccessKeys(currentAccessKeysUser); - } else { - const error = await response.json(); - showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error')); - } - } catch (error) { - console.error('Error creating access key:', error); - showErrorMessage('Failed to create access key: ' + error.message); - } -} - -function confirmDeleteAccessKey(accessKeyId) { - confirmAction( - `Are you sure you want to delete access key "${accessKeyId}"? This action cannot be undone.`, - () => deleteAccessKeyConfirmed(accessKeyId) - ); -} - -async function deleteAccessKeyConfirmed(accessKeyId) { - try { - const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys/${accessKeyId}`, { - method: 'DELETE' - }); - - if (response.ok) { - showSuccessMessage('Access key deleted successfully'); - // Reload access keys - await loadAccessKeys(currentAccessKeysUser); - } else { - const error = await response.json(); - showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error')); - } - } catch (error) { - console.error('Error deleting access key:', error); - showErrorMessage('Failed to delete access key: ' + error.message); - } + // Remove modal from DOM when hidden + document.getElementById(modalId).addEventListener('hidden.bs.modal', function () { + this.remove(); + }); } function showSecretKey(accessKey, secretKey) { + const modalId = 'secretKeyModal_' + Date.now(); + const escapedAccessKey = escapeHtmlForAttribute(accessKey); + const escapedSecretKey = escapeHtmlForAttribute(secretKey); + const content = `
@@ -2485,8 +2192,8 @@ function showSecretKey(accessKey, secretKey) {
- -
@@ -2494,8 +2201,8 @@ function showSecretKey(accessKey, secretKey) {
- -
@@ -2506,20 +2213,20 @@ function showSecretKey(accessKey, secretKey) { } function showNewAccessKeyModal(accessKeyData) { + const modalId = 'newKeyModal_' + Date.now(); + const escapedAccessKey = escapeHtmlForAttribute(accessKeyData.access_key); + const escapedSecretKey = escapeHtmlForAttribute(accessKeyData.secret_key); + const content = `
Success! Your new access key has been created.
-
- - Important: These credentials provide access to your object storage. Keep them secure and don't share them. You can view them again through the user management interface if needed. -
- -
@@ -2527,8 +2234,8 @@ function showNewAccessKeyModal(accessKeyData) {
- -
@@ -2538,40 +2245,32 @@ function showNewAccessKeyModal(accessKeyData) { showModal('New Access Key Created', content); } -function showModal(title, content) { - // Create a dynamic modal - const modalId = 'dynamicModal_' + Date.now(); - const modalHtml = ` - - `; - - // Add modal to body - document.body.insertAdjacentHTML('beforeend', modalHtml); - - // Show modal - const modal = new bootstrap.Modal(document.getElementById(modalId)); - modal.show(); +// Helper function to copy from an input field +function copyFromInput(inputId) { + const input = document.getElementById(inputId); + if (input) { + input.select(); + input.setSelectionRange(0, 99999); // For mobile devices - // Remove modal from DOM when hidden - document.getElementById(modalId).addEventListener('hidden.bs.modal', function () { - this.remove(); - }); + try { + const successful = document.execCommand('copy'); + if (successful) { + showAlert('success', 'Copied to clipboard!'); + } else { + // Try modern clipboard API as fallback + navigator.clipboard.writeText(input.value).then(() => { + showAlert('success', 'Copied to clipboard!'); + }).catch(() => { + showAlert('danger', 'Failed to copy'); + }); + } + } catch (err) { + // Try modern clipboard API as fallback + navigator.clipboard.writeText(input.value).then(() => { + showAlert('success', 'Copied to clipboard!'); + }).catch(() => { + showAlert('danger', 'Failed to copy'); + }); + } + } } - - - diff --git a/weed/admin/view/app/object_store_users.templ b/weed/admin/view/app/object_store_users.templ index e93f39d75..0f9a2d693 100644 --- a/weed/admin/view/app/object_store_users.templ +++ b/weed/admin/view/app/object_store_users.templ @@ -223,6 +223,30 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { Hold Ctrl/Cmd to select multiple permissions
+
+ + Apply selected permissions to specific buckets or all buckets + +
+ + +
+
+ + +
+ + +
+
+ + Apply selected permissions to specific buckets or all buckets + +
+ + +
+
+ + +
+ + +
Hold Ctrl/Cmd to select multiple permissions
Hold Ctrl/Cmd to select multiple policies
Edit User
User Details
Manage Access Keys
Access Keys for
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Create New User
Hold Ctrl/Cmd to select multiple permissions
Apply selected permissions to specific buckets or all buckets
Hold Ctrl/Cmd to select multiple buckets
Hold Ctrl/Cmd to select multiple policies
Edit User
Apply selected permissions to specific buckets or all buckets
Hold Ctrl/Cmd to select multiple buckets
User Details
Manage Access Keys
Access Keys for
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }