diff --git a/weed/admin/dash/admin_data.go b/weed/admin/dash/admin_data.go index e58087840..7a9e50964 100644 --- a/weed/admin/dash/admin_data.go +++ b/weed/admin/dash/admin_data.go @@ -37,6 +37,7 @@ type ObjectStoreUser struct { AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` Permissions []string `json:"permissions"` + PolicyNames []string `json:"policy_names"` } type ObjectStoreUsersData struct { @@ -52,11 +53,13 @@ type CreateUserRequest struct { Email string `json:"email"` Actions []string `json:"actions"` GenerateKey bool `json:"generate_key"` + PolicyNames []string `json:"policy_names"` } type UpdateUserRequest struct { - Email string `json:"email"` - Actions []string `json:"actions"` + Email string `json:"email"` + Actions []string `json:"actions"` + PolicyNames []string `json:"policy_names"` } type UpdateUserPoliciesRequest struct { @@ -70,10 +73,11 @@ type AccessKeyInfo struct { } type UserDetails struct { - Username string `json:"username"` - Email string `json:"email"` - Actions []string `json:"actions"` - AccessKeys []AccessKeyInfo `json:"access_keys"` + Username string `json:"username"` + Email string `json:"email"` + Actions []string `json:"actions"` + PolicyNames []string `json:"policy_names"` + AccessKeys []AccessKeyInfo `json:"access_keys"` } type FilerNode struct { diff --git a/weed/admin/dash/user_management.go b/weed/admin/dash/user_management.go index 3a74a675f..38e1f316d 100644 --- a/weed/admin/dash/user_management.go +++ b/weed/admin/dash/user_management.go @@ -21,8 +21,9 @@ func (s *AdminServer) CreateObjectStoreUser(req CreateUserRequest) (*ObjectStore // Create new identity newIdentity := &iam_pb.Identity{ - Name: req.Username, - Actions: req.Actions, + Name: req.Username, + Actions: req.Actions, + PolicyNames: req.PolicyNames, } // Add account if email is provided @@ -63,6 +64,7 @@ func (s *AdminServer) CreateObjectStoreUser(req CreateUserRequest) (*ObjectStore AccessKey: accessKey, SecretKey: secretKey, Permissions: req.Actions, + PolicyNames: req.PolicyNames, } return user, nil @@ -91,12 +93,17 @@ func (s *AdminServer) UpdateObjectStoreUser(username string, req UpdateUserReque Account: identity.Account, Credentials: identity.Credentials, Actions: identity.Actions, + PolicyNames: identity.PolicyNames, } // Update actions if provided - if len(req.Actions) > 0 { + if req.Actions != nil { updatedIdentity.Actions = req.Actions } + // Always update policy names when present in request (even if empty to allow clearing) + if req.PolicyNames != nil { + updatedIdentity.PolicyNames = req.PolicyNames + } // Update email if provided if req.Email != "" { @@ -120,6 +127,7 @@ func (s *AdminServer) UpdateObjectStoreUser(username string, req UpdateUserReque Username: username, Email: req.Email, Permissions: updatedIdentity.Actions, + PolicyNames: updatedIdentity.PolicyNames, } // Get first access key for display @@ -169,8 +177,9 @@ func (s *AdminServer) GetObjectStoreUserDetails(username string) (*UserDetails, } details := &UserDetails{ - Username: username, - Actions: identity.Actions, + Username: username, + Actions: identity.Actions, + PolicyNames: identity.PolicyNames, } // Set email from account if available @@ -295,6 +304,7 @@ func (s *AdminServer) UpdateUserPolicies(username string, actions []string) erro Account: identity.Account, Credentials: identity.Credentials, Actions: actions, + PolicyNames: identity.PolicyNames, } // Update user using credential manager diff --git a/weed/admin/handlers/user_handlers.go b/weed/admin/handlers/user_handlers.go index ed280a0ad..89b07bf75 100644 --- a/weed/admin/handlers/user_handlers.go +++ b/weed/admin/handlers/user_handlers.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "net/http" "time" @@ -29,7 +30,12 @@ func (h *UserHandlers) ShowObjectStoreUsers(c *gin.Context) { usersData := h.getObjectStoreUsersData(c) // Render HTML template + // Add cache-control headers to prevent browser caching of inline JavaScript c.Header("Content-Type", "text/html") + c.Header("Cache-Control", "no-cache, no-store, must-revalidate") + c.Header("Pragma", "no-cache") + c.Header("Expires", "0") + c.Header("ETag", fmt.Sprintf("\"%d\"", time.Now().Unix())) usersComponent := app.ObjectStoreUsers(usersData) layoutComponent := layout.Layout(c, usersComponent) err := layoutComponent.Render(c.Request.Context(), c.Writer) diff --git a/weed/admin/static/js/admin.js b/weed/admin/static/js/admin.js index 1758cde82..805445024 100644 --- a/weed/admin/static/js/admin.js +++ b/weed/admin/static/js/admin.js @@ -4,12 +4,12 @@ let bucketToDelete = ''; // Initialize dashboard when DOM is loaded -document.addEventListener('DOMContentLoaded', function() { +document.addEventListener('DOMContentLoaded', function () { initializeDashboard(); initializeEventHandlers(); setupFormValidation(); setupFileManagerEventHandlers(); - + // Initialize delete button visibility on file browser page if (window.location.pathname === '/files') { updateDeleteSelectedButton(); @@ -19,16 +19,16 @@ document.addEventListener('DOMContentLoaded', function() { function initializeDashboard() { // Set up HTMX event listeners setupHTMXListeners(); - + // Initialize tooltips initializeTooltips(); - + // Set up periodic refresh setupAutoRefresh(); - + // Set active navigation setActiveNavigation(); - + // Set up submenu behavior setupSubmenuBehavior(); } @@ -36,17 +36,17 @@ function initializeDashboard() { // HTMX event listeners function setupHTMXListeners() { // Show loading indicator on requests - document.body.addEventListener('htmx:beforeRequest', function(evt) { + document.body.addEventListener('htmx:beforeRequest', function (evt) { showLoadingIndicator(); }); - + // Hide loading indicator on completion - document.body.addEventListener('htmx:afterRequest', function(evt) { + document.body.addEventListener('htmx:afterRequest', function (evt) { hideLoadingIndicator(); }); - + // Handle errors - document.body.addEventListener('htmx:responseError', function(evt) { + document.body.addEventListener('htmx:responseError', function (evt) { handleHTMXError(evt); }); } @@ -62,7 +62,7 @@ function initializeTooltips() { // Set up auto-refresh for dashboard data function setupAutoRefresh() { // Refresh dashboard data every 30 seconds - setInterval(function() { + setInterval(function () { if (window.location.pathname === '/dashboard') { htmx.trigger('#dashboard-content', 'refresh'); } @@ -73,11 +73,11 @@ function setupAutoRefresh() { function setActiveNavigation() { const currentPath = window.location.pathname; const navLinks = document.querySelectorAll('.sidebar .nav-link'); - - navLinks.forEach(function(link) { + + navLinks.forEach(function (link) { const href = link.getAttribute('href'); let isActive = false; - + if (href === currentPath) { isActive = true; } else if (currentPath === '/' && href === '/admin') { @@ -86,7 +86,7 @@ function setActiveNavigation() { isActive = true; } // Note: Removed the problematic cluster condition that was highlighting all submenu items - + if (isActive) { link.classList.add('active'); } else { @@ -98,13 +98,13 @@ function setActiveNavigation() { // Set up submenu behavior function setupSubmenuBehavior() { const currentPath = window.location.pathname; - + // If we're on a cluster page, expand the cluster submenu if (currentPath.startsWith('/cluster/')) { const clusterSubmenu = document.getElementById('clusterSubmenu'); if (clusterSubmenu) { clusterSubmenu.classList.add('show'); - + // Update the parent toggle button state const toggleButton = document.querySelector('[data-bs-target="#clusterSubmenu"]'); if (toggleButton) { @@ -113,13 +113,13 @@ function setupSubmenuBehavior() { } } } - + // If we're on an object store page, expand the object store submenu if (currentPath.startsWith('/object-store/')) { const objectStoreSubmenu = document.getElementById('objectStoreSubmenu'); if (objectStoreSubmenu) { objectStoreSubmenu.classList.add('show'); - + // Update the parent toggle button state const toggleButton = document.querySelector('[data-bs-target="#objectStoreSubmenu"]'); if (toggleButton) { @@ -128,13 +128,13 @@ function setupSubmenuBehavior() { } } } - + // If we're on a maintenance page, expand the maintenance submenu if (currentPath.startsWith('/maintenance')) { const maintenanceSubmenu = document.getElementById('maintenanceSubmenu'); if (maintenanceSubmenu) { maintenanceSubmenu.classList.add('show'); - + // Update the parent toggle button state const toggleButton = document.querySelector('[data-bs-target="#maintenanceSubmenu"]'); if (toggleButton) { @@ -143,41 +143,41 @@ function setupSubmenuBehavior() { } } } - + // Prevent submenu from collapsing when clicking on submenu items const clusterSubmenuLinks = document.querySelectorAll('#clusterSubmenu .nav-link'); - clusterSubmenuLinks.forEach(function(link) { - link.addEventListener('click', function(e) { + clusterSubmenuLinks.forEach(function (link) { + link.addEventListener('click', function (e) { // Don't prevent the navigation, just stop the collapse behavior e.stopPropagation(); }); }); - + const objectStoreSubmenuLinks = document.querySelectorAll('#objectStoreSubmenu .nav-link'); - objectStoreSubmenuLinks.forEach(function(link) { - link.addEventListener('click', function(e) { + objectStoreSubmenuLinks.forEach(function (link) { + link.addEventListener('click', function (e) { // Don't prevent the navigation, just stop the collapse behavior e.stopPropagation(); }); }); - + const maintenanceSubmenuLinks = document.querySelectorAll('#maintenanceSubmenu .nav-link'); - maintenanceSubmenuLinks.forEach(function(link) { - link.addEventListener('click', function(e) { + maintenanceSubmenuLinks.forEach(function (link) { + link.addEventListener('click', function (e) { // Don't prevent the navigation, just stop the collapse behavior e.stopPropagation(); }); }); - + // Handle the main cluster toggle const clusterToggle = document.querySelector('[data-bs-target="#clusterSubmenu"]'); if (clusterToggle) { - clusterToggle.addEventListener('click', function(e) { + clusterToggle.addEventListener('click', function (e) { e.preventDefault(); - + const submenu = document.getElementById('clusterSubmenu'); const isExpanded = submenu.classList.contains('show'); - + if (isExpanded) { // Collapse submenu.classList.remove('show'); @@ -191,16 +191,16 @@ function setupSubmenuBehavior() { } }); } - + // Handle the main object store toggle const objectStoreToggle = document.querySelector('[data-bs-target="#objectStoreSubmenu"]'); if (objectStoreToggle) { - objectStoreToggle.addEventListener('click', function(e) { + objectStoreToggle.addEventListener('click', function (e) { e.preventDefault(); - + const submenu = document.getElementById('objectStoreSubmenu'); const isExpanded = submenu.classList.contains('show'); - + if (isExpanded) { // Collapse submenu.classList.remove('show'); @@ -214,16 +214,16 @@ function setupSubmenuBehavior() { } }); } - + // Handle the main maintenance toggle const maintenanceToggle = document.querySelector('[data-bs-target="#maintenanceSubmenu"]'); if (maintenanceToggle) { - maintenanceToggle.addEventListener('click', function(e) { + maintenanceToggle.addEventListener('click', function (e) { e.preventDefault(); - + const submenu = document.getElementById('maintenanceSubmenu'); const isExpanded = submenu.classList.contains('show'); - + if (isExpanded) { // Collapse submenu.classList.remove('show'); @@ -245,7 +245,7 @@ function showLoadingIndicator() { if (indicator) { indicator.style.display = 'block'; } - + // Add loading class to body document.body.classList.add('loading'); } @@ -255,7 +255,7 @@ function hideLoadingIndicator() { if (indicator) { indicator.style.display = 'none'; } - + // Remove loading class from body document.body.classList.remove('loading'); } @@ -263,10 +263,10 @@ function hideLoadingIndicator() { // Handle HTMX errors function handleHTMXError(evt) { console.error('HTMX Request Error:', evt.detail); - + // Show error toast or message showErrorMessage('Request failed. Please try again.'); - + hideLoadingIndicator(); } @@ -278,7 +278,7 @@ function showErrorMessage(message) { toast.setAttribute('role', 'alert'); toast.setAttribute('aria-live', 'assertive'); toast.setAttribute('aria-atomic', 'true'); - + toast.innerHTML = `
@@ -288,7 +288,7 @@ function showErrorMessage(message) {
`; - + // Add to toast container or create one let toastContainer = document.getElementById('toast-container'); if (!toastContainer) { @@ -298,15 +298,15 @@ function showErrorMessage(message) { toastContainer.style.zIndex = '1055'; document.body.appendChild(toastContainer); } - + toastContainer.appendChild(toast); - + // Show toast const bsToast = new bootstrap.Toast(toast); bsToast.show(); - + // Remove toast element after it's hidden - toast.addEventListener('hidden.bs.toast', function() { + toast.addEventListener('hidden.bs.toast', function () { toast.remove(); }); } @@ -318,7 +318,7 @@ function showSuccessMessage(message) { toast.setAttribute('role', 'alert'); toast.setAttribute('aria-live', 'assertive'); toast.setAttribute('aria-atomic', 'true'); - + toast.innerHTML = `
@@ -328,7 +328,7 @@ function showSuccessMessage(message) {
`; - + let toastContainer = document.getElementById('toast-container'); if (!toastContainer) { toastContainer = document.createElement('div'); @@ -337,13 +337,13 @@ function showSuccessMessage(message) { toastContainer.style.zIndex = '1055'; document.body.appendChild(toastContainer); } - + toastContainer.appendChild(toast); - + const bsToast = new bootstrap.Toast(toast); bsToast.show(); - - toast.addEventListener('hidden.bs.toast', function() { + + toast.addEventListener('hidden.bs.toast', function () { toast.remove(); }); } @@ -351,13 +351,13 @@ function showSuccessMessage(message) { // Format bytes for display function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; - + const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; - + const i = Math.floor(Math.log(bytes) / Math.log(k)); - + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } @@ -380,7 +380,7 @@ function confirmAction(message, callback) { } // Global error handler -window.addEventListener('error', function(e) { +window.addEventListener('error', function (e) { console.error('Global error:', e.error); showErrorMessage('An unexpected error occurred.'); }); @@ -392,7 +392,7 @@ window.Dashboard = { formatBytes, formatNumber, confirmAction -}; +}; // Initialize event handlers function initializeEventHandlers() { @@ -403,13 +403,13 @@ function initializeEventHandlers() { } // Delete bucket buttons - document.addEventListener('click', function(e) { + document.addEventListener('click', function (e) { if (e.target.closest('.delete-bucket-btn')) { const button = e.target.closest('.delete-bucket-btn'); const bucketName = button.getAttribute('data-bucket-name'); confirmDeleteBucket(bucketName); } - + // Quota management buttons if (e.target.closest('.quota-btn')) { const button = e.target.closest('.quota-btn'); @@ -429,7 +429,7 @@ function initializeEventHandlers() { // Enable quota checkbox for create bucket form const enableQuotaCheckbox = document.getElementById('enableQuota'); if (enableQuotaCheckbox) { - enableQuotaCheckbox.addEventListener('change', function() { + enableQuotaCheckbox.addEventListener('change', function () { const quotaSettings = document.getElementById('quotaSettings'); if (this.checked) { quotaSettings.style.display = 'block'; @@ -442,7 +442,7 @@ function initializeEventHandlers() { // Enable quota checkbox for quota modal const quotaEnabledCheckbox = document.getElementById('quotaEnabled'); if (quotaEnabledCheckbox) { - quotaEnabledCheckbox.addEventListener('change', function() { + quotaEnabledCheckbox.addEventListener('change', function () { const quotaSizeSettings = document.getElementById('quotaSizeSettings'); if (this.checked) { quotaSizeSettings.style.display = 'block'; @@ -467,7 +467,7 @@ function setupFormValidation() { // Handle create bucket form submission async function handleCreateBucket(event) { event.preventDefault(); - + const form = event.target; const formData = new FormData(form); const bucketData = { @@ -492,14 +492,14 @@ async function handleCreateBucket(event) { if (response.ok) { // Success showAlert('success', `Bucket "${bucketData.name}" created successfully!`); - + // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('createBucketModal')); modal.hide(); - + // Reset form form.reset(); - + // Refresh the page after a short delay setTimeout(() => { location.reload(); @@ -519,7 +519,7 @@ function validateBucketName(event) { const input = event.target; const value = input.value; const isValid = /^[a-z0-9.-]+$/.test(value) && value.length >= 3 && value.length <= 63; - + if (value.length > 0 && !isValid) { input.setCustomValidity('Bucket name must contain only lowercase letters, numbers, dots, and hyphens (3-63 characters)'); } else { @@ -531,7 +531,7 @@ function validateBucketName(event) { function confirmDeleteBucket(bucketName) { bucketToDelete = bucketName; document.getElementById('deleteBucketName').textContent = bucketName; - + const modal = new bootstrap.Modal(document.getElementById('deleteBucketModal')); modal.show(); } @@ -552,11 +552,11 @@ async function deleteBucket() { if (response.ok) { // Success showAlert('success', `Bucket "${bucketToDelete}" deleted successfully!`); - + // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('deleteBucketModal')); modal.hide(); - + // Refresh the page after a short delay setTimeout(() => { location.reload(); @@ -588,7 +588,7 @@ function exportBucketList() { const data = rows.map(row => { const cells = row.querySelectorAll('td'); if (cells.length < 5) return null; // Skip empty state row - + return { name: cells[0].textContent.trim(), created: cells[1].textContent.trim(), @@ -639,7 +639,7 @@ function showAlert(type, message) { min-width: 300px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); `; - + alert.innerHTML = ` ${message} @@ -684,9 +684,9 @@ function exportVolumeServers() { showErrorMessage('No volume servers data to export'); return; } - + let csv = 'Server ID,Address,Data Center,Rack,Volumes,Capacity,Usage\n'; - + const rows = table.querySelectorAll('tbody tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); @@ -703,7 +703,7 @@ function exportVolumeServers() { csv += rowData.join(',') + '\n'; } }); - + downloadCSV(csv, 'seaweedfs-volume-servers.csv'); } @@ -714,7 +714,7 @@ function exportVolumes() { showErrorMessage('No volumes data to export'); return; } - + // Get headers from the table (dynamically handles conditional columns) const headerCells = table.querySelectorAll('thead th'); const headers = []; @@ -724,9 +724,9 @@ function exportVolumes() { headers.push(cell.textContent.trim()); } }); - + let csv = headers.join(',') + '\n'; - + const rows = table.querySelectorAll('tbody tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); @@ -735,9 +735,9 @@ function exportVolumes() { for (let i = 0; i < cells.length - 1; i++) { rowData.push(`"${cells[i].textContent.trim().replace(/"/g, '""')}"`); } - csv += rowData.join(',') + '\n'; + csv += rowData.join(',') + '\n'; }); - + downloadCSV(csv, 'seaweedfs-volumes.csv'); } @@ -854,15 +854,15 @@ function exportUsers() { showAlert('error', 'Users table not found'); return; } - + const rows = table.querySelectorAll('tbody tr'); if (rows.length === 0) { showErrorMessage('No users to export'); return; } - + let csvContent = 'Username,Email,Access Key,Status,Created,Last Login\n'; - + rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length >= 6) { @@ -872,11 +872,11 @@ function exportUsers() { const status = cells[3].textContent.trim(); const created = cells[4].textContent.trim(); const lastLogin = cells[5].textContent.trim(); - + csvContent += `"${username}","${email}","${accessKey}","${status}","${created}","${lastLogin}"\n`; } }); - + downloadCSV(csvContent, 'seaweedfs-users.csv'); } @@ -884,12 +884,12 @@ function exportUsers() { function confirmDeleteCollection(button) { const collectionName = button.getAttribute('data-collection-name'); document.getElementById('deleteCollectionName').textContent = collectionName; - + const modal = new bootstrap.Modal(document.getElementById('deleteCollectionModal')); modal.show(); - + // Set up confirm button - document.getElementById('confirmDeleteCollection').onclick = function() { + document.getElementById('confirmDeleteCollection').onclick = function () { deleteCollection(collectionName); }; } @@ -903,7 +903,7 @@ async function deleteCollection(collectionName) { 'Content-Type': 'application/json', } }); - + if (response.ok) { showSuccessMessage(`Collection "${collectionName}" deleted successfully`); // Hide modal @@ -929,7 +929,7 @@ async function deleteCollection(collectionName) { function downloadCSV(csvContent, filename) { const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); - + if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute('href', url); @@ -947,11 +947,11 @@ function downloadCSV(csvContent, filename) { function toggleSelectAll() { const selectAll = document.getElementById('selectAll'); const checkboxes = document.querySelectorAll('.file-checkbox'); - + checkboxes.forEach(checkbox => { checkbox.checked = selectAll.checked; }); - + updateDeleteSelectedButton(); } @@ -959,7 +959,7 @@ function toggleSelectAll() { function updateDeleteSelectedButton() { const checkboxes = document.querySelectorAll('.file-checkbox:checked'); const deleteBtn = document.getElementById('deleteSelectedBtn'); - + if (deleteBtn) { if (checkboxes.length > 0) { deleteBtn.style.display = 'inline-block'; @@ -975,7 +975,7 @@ function updateSelectAllCheckbox() { const selectAll = document.getElementById('selectAll'); const allCheckboxes = document.querySelectorAll('.file-checkbox'); const checkedCheckboxes = document.querySelectorAll('.file-checkbox:checked'); - + if (selectAll && allCheckboxes.length > 0) { if (checkedCheckboxes.length === 0) { selectAll.checked = false; @@ -999,17 +999,17 @@ function getSelectedFilePaths() { // Confirm delete selected files function confirmDeleteSelected() { const selectedPaths = getSelectedFilePaths(); - + if (selectedPaths.length === 0) { showAlert('warning', 'No files selected'); return; } - + const fileNames = selectedPaths.map(path => path.split('/').pop()).join(', '); - const message = selectedPaths.length === 1 + const message = selectedPaths.length === 1 ? `Are you sure you want to delete "${fileNames}"?` : `Are you sure you want to delete ${selectedPaths.length} selected items?\n\n${fileNames.substring(0, 200)}${fileNames.length > 200 ? '...' : ''}`; - + if (confirm(message)) { deleteSelectedFiles(selectedPaths); } @@ -1021,13 +1021,13 @@ async function deleteSelectedFiles(filePaths) { showAlert('warning', 'No files selected'); return; } - + // Disable the delete button during operation const deleteBtn = document.getElementById('deleteSelectedBtn'); const originalText = deleteBtn.innerHTML; deleteBtn.disabled = true; deleteBtn.innerHTML = 'Deleting...'; - + try { const response = await fetch('/api/files/delete-multiple', { method: 'DELETE', @@ -1039,7 +1039,7 @@ async function deleteSelectedFiles(filePaths) { if (response.ok) { const result = await response.json(); - + if (result.deleted > 0) { if (result.failed === 0) { showAlert('success', `Successfully deleted ${result.deleted} item(s)`); @@ -1049,7 +1049,7 @@ async function deleteSelectedFiles(filePaths) { console.warn('Deletion errors:', result.errors); } } - + // Reload the page to update the file list setTimeout(() => { window.location.reload(); @@ -1091,31 +1091,31 @@ function uploadFile() { async function submitCreateFolder() { const folderName = document.getElementById('folderName').value.trim(); const currentPath = document.getElementById('currentPath').value; - + if (!folderName) { showErrorMessage('Please enter a folder name'); return; } - + // Validate folder name if (folderName.includes('/') || folderName.includes('\\')) { showErrorMessage('Folder names cannot contain / or \\ characters'); return; } - + // Additional validation for reserved names const reservedNames = ['.', '..', 'CON', 'PRN', 'AUX', 'NUL']; if (reservedNames.includes(folderName.toUpperCase())) { showErrorMessage('This folder name is reserved and cannot be used'); return; } - + // Disable the button to prevent double submission const submitButton = document.querySelector('#createFolderModal .btn-primary'); const originalText = submitButton.innerHTML; submitButton.disabled = true; submitButton.innerHTML = 'Creating...'; - + try { const response = await fetch('/api/files/create-folder', { method: 'POST', @@ -1157,37 +1157,37 @@ async function submitCreateFolder() { async function submitUploadFile() { const fileInput = document.getElementById('fileInput'); const currentPath = document.getElementById('uploadPath').value; - + if (!fileInput.files || fileInput.files.length === 0) { showErrorMessage('Please select at least one file to upload'); return; } - + const files = Array.from(fileInput.files); const totalSize = files.reduce((sum, file) => sum + file.size, 0); - + // Validate total file size (limit to 500MB for admin interface) const maxSize = 500 * 1024 * 1024; // 500MB total if (totalSize > maxSize) { showErrorMessage('Total file size exceeds 500MB limit. Please select fewer or smaller files.'); return; } - + // Individual file size validation removed - no limit per file - + const formData = new FormData(); files.forEach(file => { formData.append('files', file); }); formData.append('path', currentPath); - + // Show progress bar and disable button const progressContainer = document.getElementById('uploadProgress'); const progressBar = progressContainer.querySelector('.progress-bar'); const uploadStatus = document.getElementById('uploadStatus'); const submitButton = document.querySelector('#uploadFileModal .btn-primary'); const originalText = submitButton.innerHTML; - + progressContainer.style.display = 'block'; progressBar.style.width = '0%'; progressBar.setAttribute('aria-valuenow', '0'); @@ -1195,12 +1195,12 @@ async function submitUploadFile() { uploadStatus.textContent = `Uploading ${files.length} file(s)...`; submitButton.disabled = true; submitButton.innerHTML = 'Uploading...'; - + try { const xhr = new XMLHttpRequest(); - + // Handle progress - xhr.upload.addEventListener('progress', function(e) { + xhr.upload.addEventListener('progress', function (e) { if (e.lengthComputable) { const percentComplete = Math.round((e.loaded / e.total) * 100); progressBar.style.width = percentComplete + '%'; @@ -1209,13 +1209,13 @@ async function submitUploadFile() { uploadStatus.textContent = `Uploading ${files.length} file(s)... ${percentComplete}%`; } }); - + // Handle completion - xhr.addEventListener('load', function() { + xhr.addEventListener('load', function () { if (xhr.status === 200) { try { const response = JSON.parse(xhr.responseText); - + if (response.uploaded > 0) { if (response.failed === 0) { showSuccessMessage(`Successfully uploaded ${response.uploaded} file(s)`); @@ -1226,7 +1226,7 @@ async function submitUploadFile() { console.warn('Upload errors:', response.errors); } } - + // Hide modal and refresh page const modal = bootstrap.Modal.getInstance(document.getElementById('uploadFileModal')); modal.hide(); @@ -1256,23 +1256,23 @@ async function submitUploadFile() { progressContainer.style.display = 'none'; } }); - + // Handle errors - xhr.addEventListener('error', function() { + xhr.addEventListener('error', function () { showErrorMessage('Failed to upload files. Please check your connection and try again.'); progressContainer.style.display = 'none'; }); - + // Handle abort - xhr.addEventListener('abort', function() { + xhr.addEventListener('abort', function () { showErrorMessage('File upload was cancelled.'); progressContainer.style.display = 'none'; }); - + // Send request xhr.open('POST', '/api/files/upload'); xhr.send(formData); - + } catch (error) { console.error('Upload error:', error); showErrorMessage('Failed to upload files. Please try again.'); @@ -1331,16 +1331,16 @@ function downloadFile(filePath) { async function viewFile(filePath) { try { const response = await fetch(`/api/files/view?path=${encodeURIComponent(filePath)}`); - + if (!response.ok) { const error = await response.json(); showAlert('error', `Failed to view file: ${error.error || 'Unknown error'}`); return; } - + const data = await response.json(); showFileViewer(data); - + } catch (error) { console.error('View file error:', error); showAlert('error', 'Failed to view file'); @@ -1351,16 +1351,16 @@ async function viewFile(filePath) { async function showProperties(filePath) { try { const response = await fetch(`/api/files/properties?path=${encodeURIComponent(filePath)}`); - + if (!response.ok) { const error = await response.json(); showAlert('error', `Failed to get file properties: ${error.error || 'Unknown error'}`); return; } - + const properties = await response.json(); showPropertiesModal(properties); - + } catch (error) { console.error('Properties error:', error); showAlert('error', 'Failed to get file properties'); @@ -1404,45 +1404,45 @@ function setupFileManagerEventHandlers() { // Handle Enter key in folder name input const folderNameInput = document.getElementById('folderName'); if (folderNameInput) { - folderNameInput.addEventListener('keypress', function(e) { + folderNameInput.addEventListener('keypress', function (e) { if (e.key === 'Enter') { e.preventDefault(); submitCreateFolder(); } }); } - + // Handle file selection change to show preview const fileInput = document.getElementById('fileInput'); if (fileInput) { - fileInput.addEventListener('change', function(e) { + fileInput.addEventListener('change', function (e) { updateFileListPreview(); }); } - + // Setup checkbox event listeners for file selection const checkboxes = document.querySelectorAll('.file-checkbox'); checkboxes.forEach(checkbox => { - checkbox.addEventListener('change', function() { + checkbox.addEventListener('change', function () { updateDeleteSelectedButton(); updateSelectAllCheckbox(); }); }); - + // Setup drag and drop for file uploads setupDragAndDrop(); - + // Clear form when modals are hidden const createFolderModal = document.getElementById('createFolderModal'); if (createFolderModal) { - createFolderModal.addEventListener('hidden.bs.modal', function() { + createFolderModal.addEventListener('hidden.bs.modal', function () { document.getElementById('folderName').value = ''; }); } - + const uploadFileModal = document.getElementById('uploadFileModal'); if (uploadFileModal) { - uploadFileModal.addEventListener('hidden.bs.modal', function() { + uploadFileModal.addEventListener('hidden.bs.modal', function () { const fileInput = document.getElementById('fileInput'); const progressContainer = document.getElementById('uploadProgress'); const fileListPreview = document.getElementById('fileListPreview'); @@ -1457,32 +1457,32 @@ function setupFileManagerEventHandlers() { function setupDragAndDrop() { const dropZone = document.querySelector('.card-body'); // Main file listing area const uploadModal = document.getElementById('uploadFileModal'); - + if (!dropZone || !uploadModal) return; - + // Prevent default drag behaviors ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, preventDefaults, false); document.body.addEventListener(eventName, preventDefaults, false); }); - + // Highlight drop zone when item is dragged over it ['dragenter', 'dragover'].forEach(eventName => { dropZone.addEventListener(eventName, highlight, false); }); - + ['dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, unhighlight, false); }); - + // Handle dropped files dropZone.addEventListener('drop', handleDrop, false); - + function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } - + function highlight(e) { dropZone.classList.add('drag-over'); // Add some visual feedback @@ -1514,7 +1514,7 @@ function setupDragAndDrop() { dropZone.appendChild(overlay); } } - + function unhighlight(e) { dropZone.classList.remove('drag-over'); const overlay = dropZone.querySelector('.drag-overlay'); @@ -1522,23 +1522,23 @@ function setupDragAndDrop() { overlay.remove(); } } - + function handleDrop(e) { const dt = e.dataTransfer; const files = dt.files; - + if (files.length > 0) { // Open upload modal and set files const fileInput = document.getElementById('fileInput'); if (fileInput) { // Create a new FileList-like object const fileArray = Array.from(files); - + // Set files to input (this is a bit tricky with file inputs) const dataTransfer = new DataTransfer(); fileArray.forEach(file => dataTransfer.items.add(file)); fileInput.files = dataTransfer.files; - + // Update preview and show modal updateFileListPreview(); const modal = new bootstrap.Modal(uploadModal); @@ -1553,20 +1553,20 @@ function updateFileListPreview() { const fileInput = document.getElementById('fileInput'); const fileListPreview = document.getElementById('fileListPreview'); const selectedFilesList = document.getElementById('selectedFilesList'); - + if (!fileInput.files || fileInput.files.length === 0) { fileListPreview.style.display = 'none'; return; } - + const files = Array.from(fileInput.files); const totalSize = files.reduce((sum, file) => sum + file.size, 0); - + let html = `
${files.length} file(s) selected Total: ${formatBytes(totalSize)}
`; - + files.forEach((file, index) => { const fileIcon = getFileIconByName(file.name); html += `
@@ -1577,7 +1577,7 @@ function updateFileListPreview() { ${formatBytes(file.size)}
`; }); - + selectedFilesList.innerHTML = html; fileListPreview.style.display = 'block'; } @@ -1585,7 +1585,7 @@ function updateFileListPreview() { // Get file icon based on file name/extension function getFileIconByName(fileName) { const ext = fileName.split('.').pop().toLowerCase(); - + switch (ext) { case 'jpg': case 'jpeg': @@ -1643,14 +1643,14 @@ function getFileIconByName(fileName) { function showQuotaModal(bucketName, currentQuotaMB, quotaEnabled) { document.getElementById('quotaBucketName').value = bucketName; document.getElementById('quotaEnabled').checked = quotaEnabled; - + // Convert quota to appropriate unit and set values const quotaBytes = currentQuotaMB * 1024 * 1024; // Convert MB to bytes const { size, unit } = convertBytesToBestUnit(quotaBytes); - + document.getElementById('quotaSizeMB').value = size; document.getElementById('quotaUnitMB').value = unit; - + // Show/hide quota size settings based on enabled state const quotaSizeSettings = document.getElementById('quotaSizeSettings'); if (quotaEnabled) { @@ -1658,7 +1658,7 @@ function showQuotaModal(bucketName, currentQuotaMB, quotaEnabled) { } else { quotaSizeSettings.style.display = 'none'; } - + const modal = new bootstrap.Modal(document.getElementById('manageQuotaModal')); modal.show(); } @@ -1668,17 +1668,17 @@ function convertBytesToBestUnit(bytes) { if (bytes === 0) { return { size: 0, unit: 'MB' }; } - + // Check if it's a clean TB value if (bytes >= 1024 * 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024 * 1024) === 0) { return { size: bytes / (1024 * 1024 * 1024 * 1024), unit: 'TB' }; } - + // Check if it's a clean GB value if (bytes >= 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024) === 0) { return { size: bytes / (1024 * 1024 * 1024), unit: 'GB' }; } - + // Default to MB return { size: bytes / (1024 * 1024), unit: 'MB' }; } @@ -1686,11 +1686,11 @@ function convertBytesToBestUnit(bytes) { // Handle quota update form submission async function handleUpdateQuota(event) { event.preventDefault(); - + const form = event.target; const formData = new FormData(form); const bucketName = document.getElementById('quotaBucketName').value; - + const quotaData = { quota_enabled: formData.get('quota_enabled') === 'on', quota_size: parseInt(formData.get('quota_size')) || 0, @@ -1711,11 +1711,11 @@ async function handleUpdateQuota(event) { if (response.ok) { // Success showAlert('success', `Quota for bucket "${bucketName}" updated successfully!`); - + // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('manageQuotaModal')); modal.hide(); - + // Refresh the page after a short delay setTimeout(() => { location.reload(); @@ -1735,7 +1735,7 @@ function showFileViewer(data) { const file = data.file; const content = data.content || ''; const viewable = data.viewable !== false; - + // Create modal HTML const modalHtml = `
`; - + // Remove existing modal if any const existingModal = document.getElementById('fileViewerModal'); if (existingModal) { existingModal.remove(); } - + // Add modal to DOM document.body.insertAdjacentHTML('beforeend', modalHtml); - + // Show modal const modal = new bootstrap.Modal(document.getElementById('fileViewerModal')); modal.show(); - + // Clean up when modal is hidden document.getElementById('fileViewerModal').addEventListener('hidden.bs.modal', function () { this.remove(); @@ -1853,7 +1853,7 @@ function getLanguageFromMime(mime, filename) { case 'text/sql': return 'sql'; case 'text/markdown': return 'markdown'; } - + // Fallback to file extension const ext = filename.split('.').pop().toLowerCase(); switch (ext) { @@ -1908,20 +1908,20 @@ function showPropertiesModal(properties) {
`; - + // Remove existing modal if any const existingModal = document.getElementById('propertiesModal'); if (existingModal) { existingModal.remove(); } - + // Add modal to DOM document.body.insertAdjacentHTML('beforeend', modalHtml); - + // Show modal const modal = new bootstrap.Modal(document.getElementById('propertiesModal')); modal.show(); - + // Clean up when modal is hidden document.getElementById('propertiesModal').addEventListener('hidden.bs.modal', function () { this.remove(); @@ -1939,14 +1939,14 @@ function createPropertiesContent(properties) { Full Path:${properties.full_path} Type:${properties.is_directory ? 'Directory' : 'File'} `; - + if (!properties.is_directory) { html += ` Size:${properties.size_formatted || formatBytes(properties.size || 0)} MIME Type:${properties.mime_type || 'Unknown'} `; } - + html += ` @@ -1954,14 +1954,14 @@ function createPropertiesContent(properties) {
Timestamps
`; - + if (properties.modified_time) { html += ``; } if (properties.created_time) { html += ``; } - + html += `
Modified:${properties.modified_time}
Created:${properties.created_time}
@@ -1974,7 +1974,7 @@ function createPropertiesContent(properties) { `; - + // Add TTL information if available if (properties.ttl_seconds && properties.ttl_seconds > 0) { html += ` @@ -1988,7 +1988,7 @@ function createPropertiesContent(properties) { `; } - + // Add chunk information if available if (properties.chunks && properties.chunks.length > 0) { html += ` @@ -2007,7 +2007,7 @@ function createPropertiesContent(properties) { `; - + properties.chunks.forEach(chunk => { html += ` @@ -2018,7 +2018,7 @@ function createPropertiesContent(properties) { `; }); - + html += ` @@ -2027,7 +2027,7 @@ function createPropertiesContent(properties) { `; } - + // Add extended attributes if available if (properties.extended && Object.keys(properties.extended).length > 0) { html += ` @@ -2036,18 +2036,18 @@ function createPropertiesContent(properties) {
Extended Attributes
`; - + Object.entries(properties.extended).forEach(([key, value]) => { html += ``; }); - + html += `
${key}:${value}
`; } - + return html; } @@ -2060,7 +2060,7 @@ function escapeHtml(text) { '"': '"', "'": ''' }; - return text.replace(/[&<>"']/g, function(m) { return map[m]; }); + return text.replace(/[&<>"']/g, function (m) { return map[m]; }); } // ============================================================================ @@ -2076,18 +2076,18 @@ let currentAccessKeysUser = ''; 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', @@ -2096,16 +2096,16 @@ async function handleCreateUser() { }, 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(); @@ -2123,22 +2123,30 @@ async function handleCreateUser() { 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 || ''; - + 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(); @@ -2154,16 +2162,21 @@ async function editUser(username) { 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) : []; + const userData = { email: formData.get('email'), - actions: selectedActions + actions: selectedActions, + policy_names: selectedPolicies }; - + try { const response = await fetch(`/api/users/${currentEditingUser}`, { method: 'PUT', @@ -2172,10 +2185,10 @@ async function handleUpdateUser() { }, body: JSON.stringify(userData) }); - + if (response.ok) { showSuccessMessage('User updated successfully'); - + // Close modal and refresh page const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal')); modal.hide(); @@ -2206,7 +2219,7 @@ async function deleteUserConfirmed(username) { const response = await fetch(`/api/users/${username}`, { method: 'DELETE' }); - + if (response.ok) { showSuccessMessage('User deleted successfully'); setTimeout(() => window.location.reload(), 1000); @@ -2225,10 +2238,10 @@ async function showUserDetails(username) { const response = await fetch(`/api/users/${username}`); if (response.ok) { const user = await response.json(); - + const content = createUserDetailsContent(user); document.getElementById('userDetailsContent').innerHTML = content; - + const modal = new bootstrap.Modal(document.getElementById('userDetailsModal')); modal.show(); } else { @@ -2259,17 +2272,17 @@ function createUserDetailsContent(user) {
Permissions
- ${user.actions && user.actions.length > 0 ? - user.actions.map(action => `${action}`).join('') : - 'No permissions assigned' - } + ${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

' - } + ${user.access_keys && user.access_keys.length > 0 ? + createAccessKeysTable(user.access_keys) : + '

No access keys

' + }
`; @@ -2301,9 +2314,9 @@ function createAccessKeysTable(accessKeys) { async function manageAccessKeys(username) { currentAccessKeysUser = username; document.getElementById('accessKeysUsername').textContent = username; - + await loadAccessKeys(username); - + const modal = new bootstrap.Modal(document.getElementById('accessKeysModal')); modal.show(); } @@ -2313,7 +2326,7 @@ async function loadAccessKeys(username) { 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 { @@ -2329,7 +2342,7 @@ function createAccessKeysManagementContent(accessKeys) { if (accessKeys.length === 0) { return '

No access keys found. Create one to get started.

'; } - + return `
@@ -2375,7 +2388,7 @@ async function createAccessKey() { showErrorMessage('No user selected'); return; } - + try { const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys`, { method: 'POST', @@ -2383,14 +2396,14 @@ async function createAccessKey() { '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 { @@ -2415,10 +2428,10 @@ async function deleteAccessKeyConfirmed(accessKeyId) { 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 { @@ -2456,7 +2469,7 @@ function showSecretKey(accessKey, secretKey) { `; - + showModal('Access Key Details', content); } @@ -2489,7 +2502,7 @@ function showNewAccessKeyModal(accessKeyData) { `; - + showModal('New Access Key Created', content); } @@ -2514,20 +2527,19 @@ function showModal(title, content) { `; - + // Add modal to body document.body.insertAdjacentHTML('beforeend', modalHtml); - + // Show modal const modal = new bootstrap.Modal(document.getElementById(modalId)); modal.show(); - + // Remove modal from DOM when hidden - document.getElementById(modalId).addEventListener('hidden.bs.modal', function() { + document.getElementById(modalId).addEventListener('hidden.bs.modal', function () { this.remove(); }); } - \ No newline at end of file diff --git a/weed/admin/view/app/object_store_users.templ b/weed/admin/view/app/object_store_users.templ index 686f57e1c..e93f39d75 100644 --- a/weed/admin/view/app/object_store_users.templ +++ b/weed/admin/view/app/object_store_users.templ @@ -223,6 +223,13 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { Hold Ctrl/Cmd to select multiple permissions +
+ + + Hold Ctrl/Cmd to select multiple policies +
+
+ + +
Create New User
Hold Ctrl/Cmd to select multiple permissions
Hold Ctrl/Cmd to select multiple policies
Edit User
User Details
Manage Access Keys
Access Keys for
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/iam/integration/iam_manager.go b/weed/iam/integration/iam_manager.go index 240c5fb96..caaa7f31d 100644 --- a/weed/iam/integration/iam_manager.go +++ b/weed/iam/integration/iam_manager.go @@ -77,6 +77,9 @@ type ActionRequest struct { // RequestContext contains additional request information RequestContext map[string]interface{} `json:"requestContext,omitempty"` + + // PolicyNames to evaluate (overrides role-based policies if present) + PolicyNames []string `json:"policyNames,omitempty"` } // NewIAMManager creates a new IAM manager @@ -281,14 +284,32 @@ func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest return false, fmt.Errorf("IAM manager not initialized") } - // Validate session token first (skip for OIDC tokens which are already validated) - if !isOIDCToken(request.SessionToken) { + // Validate session token if present (skip for OIDC tokens which are already validated, + // and skip for empty tokens which represent static access keys) + if request.SessionToken != "" && !isOIDCToken(request.SessionToken) { _, err := m.stsService.ValidateSessionToken(ctx, request.SessionToken) if err != nil { return false, fmt.Errorf("invalid session: %w", err) } } + // Create evaluation context + evalCtx := &policy.EvaluationContext{ + Principal: request.Principal, + Action: request.Action, + Resource: request.Resource, + RequestContext: request.RequestContext, + } + + // If explicit policy names are provided (e.g. from user identity), evaluate them directly + if len(request.PolicyNames) > 0 { + result, err := m.policyEngine.Evaluate(ctx, "", evalCtx, request.PolicyNames) + if err != nil { + return false, fmt.Errorf("policy evaluation failed: %w", err) + } + return result.Effect == policy.EffectAllow, nil + } + // Extract role name from principal ARN roleName := utils.ExtractRoleNameFromPrincipal(request.Principal) if roleName == "" { @@ -301,14 +322,6 @@ func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest return false, fmt.Errorf("role not found: %s", roleName) } - // Create evaluation context - evalCtx := &policy.EvaluationContext{ - Principal: request.Principal, - Action: request.Action, - Resource: request.Resource, - RequestContext: request.RequestContext, - } - // Evaluate policies attached to the role result, err := m.policyEngine.Evaluate(ctx, "", evalCtx, roleDef.AttachedPolicies) if err != nil { diff --git a/weed/pb/filer_pb/filer.pb.go b/weed/pb/filer_pb/filer.pb.go index 31de4e652..7e2cdedf3 100644 --- a/weed/pb/filer_pb/filer.pb.go +++ b/weed/pb/filer_pb/filer.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc v6.33.1 // source: filer.proto package filer_pb diff --git a/weed/pb/filer_pb/filer_grpc.pb.go b/weed/pb/filer_pb/filer_grpc.pb.go index bc892380c..e74a4c712 100644 --- a/weed/pb/filer_pb/filer_grpc.pb.go +++ b/weed/pb/filer_pb/filer_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v6.33.1 // source: filer.proto package filer_pb diff --git a/weed/pb/iam.proto b/weed/pb/iam.proto index 6720a0456..d485ce011 100644 --- a/weed/pb/iam.proto +++ b/weed/pb/iam.proto @@ -27,6 +27,7 @@ message Identity { Account account = 4; bool disabled = 5; // User status: false = enabled (default), true = disabled repeated string service_account_ids = 6; // IDs of service accounts owned by this user + repeated string policy_names = 7; } message Credential { diff --git a/weed/pb/iam_pb/iam.pb.go b/weed/pb/iam_pb/iam.pb.go index b40dc486a..4b0976410 100644 --- a/weed/pb/iam_pb/iam.pb.go +++ b/weed/pb/iam_pb/iam.pb.go @@ -89,6 +89,7 @@ type Identity struct { Account *Account `protobuf:"bytes,4,opt,name=account,proto3" json:"account,omitempty"` Disabled bool `protobuf:"varint,5,opt,name=disabled,proto3" json:"disabled,omitempty"` // User status: false = enabled (default), true = disabled ServiceAccountIds []string `protobuf:"bytes,6,rep,name=service_account_ids,json=serviceAccountIds,proto3" json:"service_account_ids,omitempty"` // IDs of service accounts owned by this user + PolicyNames []string `protobuf:"bytes,7,rep,name=policy_names,json=policyNames,proto3" json:"policy_names,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -165,6 +166,13 @@ func (x *Identity) GetServiceAccountIds() []string { return nil } +func (x *Identity) GetPolicyNames() []string { + if x != nil { + return x.PolicyNames + } + return nil +} + type Credential struct { state protoimpl.MessageState `protogen:"open.v1"` AccessKey string `protobuf:"bytes,1,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"` @@ -405,14 +413,15 @@ const file_iam_proto_rawDesc = "" + "identities\x18\x01 \x03(\v2\x10.iam_pb.IdentityR\n" + "identities\x12+\n" + "\baccounts\x18\x02 \x03(\v2\x0f.iam_pb.AccountR\baccounts\x12A\n" + - "\x10service_accounts\x18\x03 \x03(\v2\x16.iam_pb.ServiceAccountR\x0fserviceAccounts\"\xe5\x01\n" + + "\x10service_accounts\x18\x03 \x03(\v2\x16.iam_pb.ServiceAccountR\x0fserviceAccounts\"\x88\x02\n" + "\bIdentity\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x124\n" + "\vcredentials\x18\x02 \x03(\v2\x12.iam_pb.CredentialR\vcredentials\x12\x18\n" + "\aactions\x18\x03 \x03(\tR\aactions\x12)\n" + "\aaccount\x18\x04 \x01(\v2\x0f.iam_pb.AccountR\aaccount\x12\x1a\n" + "\bdisabled\x18\x05 \x01(\bR\bdisabled\x12.\n" + - "\x13service_account_ids\x18\x06 \x03(\tR\x11serviceAccountIds\"b\n" + + "\x13service_account_ids\x18\x06 \x03(\tR\x11serviceAccountIds\x12!\n" + + "\fpolicy_names\x18\a \x03(\tR\vpolicyNames\"b\n" + "\n" + "Credential\x12\x1d\n" + "\n" + diff --git a/weed/pb/master_pb/master.pb.go b/weed/pb/master_pb/master.pb.go index 8f0a54351..fc2ceec4c 100644 --- a/weed/pb/master_pb/master.pb.go +++ b/weed/pb/master_pb/master.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc v6.33.1 // source: master.proto package master_pb diff --git a/weed/pb/master_pb/master_grpc.pb.go b/weed/pb/master_pb/master_grpc.pb.go index ad8ac920d..441c2ffb1 100644 --- a/weed/pb/master_pb/master_grpc.pb.go +++ b/weed/pb/master_pb/master_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v6.33.1 // source: master.proto package master_pb diff --git a/weed/pb/mount_pb/mount.pb.go b/weed/pb/mount_pb/mount.pb.go index 0d9fde354..1299f6bec 100644 --- a/weed/pb/mount_pb/mount.pb.go +++ b/weed/pb/mount_pb/mount.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc v6.33.1 // source: mount.proto package mount_pb diff --git a/weed/pb/mount_pb/mount_grpc.pb.go b/weed/pb/mount_pb/mount_grpc.pb.go index 599a8807a..6a1281326 100644 --- a/weed/pb/mount_pb/mount_grpc.pb.go +++ b/weed/pb/mount_pb/mount_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v6.33.1 // source: mount.proto package mount_pb diff --git a/weed/pb/mq_agent_pb/mq_agent.pb.go b/weed/pb/mq_agent_pb/mq_agent.pb.go index bc321e957..155819280 100644 --- a/weed/pb/mq_agent_pb/mq_agent.pb.go +++ b/weed/pb/mq_agent_pb/mq_agent.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc v6.33.1 // source: mq_agent.proto package mq_agent_pb diff --git a/weed/pb/mq_agent_pb/mq_agent_grpc.pb.go b/weed/pb/mq_agent_pb/mq_agent_grpc.pb.go index 5b020bd73..62e323d13 100644 --- a/weed/pb/mq_agent_pb/mq_agent_grpc.pb.go +++ b/weed/pb/mq_agent_pb/mq_agent_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v6.33.1 // source: mq_agent.proto package mq_agent_pb diff --git a/weed/pb/mq_pb/mq_broker.pb.go b/weed/pb/mq_pb/mq_broker.pb.go index 7e7f706cb..88affa163 100644 --- a/weed/pb/mq_pb/mq_broker.pb.go +++ b/weed/pb/mq_pb/mq_broker.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc v6.33.1 // source: mq_broker.proto package mq_pb diff --git a/weed/pb/mq_pb/mq_broker_grpc.pb.go b/weed/pb/mq_pb/mq_broker_grpc.pb.go index 77ff7df52..1abefb75b 100644 --- a/weed/pb/mq_pb/mq_broker_grpc.pb.go +++ b/weed/pb/mq_pb/mq_broker_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v6.33.1 // source: mq_broker.proto package mq_pb diff --git a/weed/pb/remote_pb/remote.pb.go b/weed/pb/remote_pb/remote.pb.go index 8ca391d3d..f7b3866c5 100644 --- a/weed/pb/remote_pb/remote.pb.go +++ b/weed/pb/remote_pb/remote.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc v6.33.1 // source: remote.proto package remote_pb diff --git a/weed/pb/s3_pb/s3.pb.go b/weed/pb/s3_pb/s3.pb.go index 31b6c8e2e..67d6ef11d 100644 --- a/weed/pb/s3_pb/s3.pb.go +++ b/weed/pb/s3_pb/s3.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc v6.33.1 // source: s3.proto package s3_pb diff --git a/weed/pb/s3_pb/s3_grpc.pb.go b/weed/pb/s3_pb/s3_grpc.pb.go index 91f3138ce..f5f43f54f 100644 --- a/weed/pb/s3_pb/s3_grpc.pb.go +++ b/weed/pb/s3_pb/s3_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v6.33.1 // source: s3.proto package s3_pb diff --git a/weed/pb/schema_pb/mq_schema.pb.go b/weed/pb/schema_pb/mq_schema.pb.go index 7fbf4a4e6..45d04c7bd 100644 --- a/weed/pb/schema_pb/mq_schema.pb.go +++ b/weed/pb/schema_pb/mq_schema.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc v6.33.1 // source: mq_schema.proto package schema_pb diff --git a/weed/pb/volume_server_pb/volume_server.pb.go b/weed/pb/volume_server_pb/volume_server.pb.go index 27e791be5..7b174f8dd 100644 --- a/weed/pb/volume_server_pb/volume_server.pb.go +++ b/weed/pb/volume_server_pb/volume_server.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc v6.33.1 // source: volume_server.proto package volume_server_pb diff --git a/weed/pb/volume_server_pb/volume_server_grpc.pb.go b/weed/pb/volume_server_pb/volume_server_grpc.pb.go index f43cff84c..02818ab38 100644 --- a/weed/pb/volume_server_pb/volume_server_grpc.pb.go +++ b/weed/pb/volume_server_pb/volume_server_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v6.33.1 // source: volume_server.proto package volume_server_pb diff --git a/weed/pb/worker_pb/worker.pb.go b/weed/pb/worker_pb/worker.pb.go index 7ff5a8a36..f0bd661c1 100644 --- a/weed/pb/worker_pb/worker.pb.go +++ b/weed/pb/worker_pb/worker.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc v6.33.1 // source: worker.proto package worker_pb diff --git a/weed/pb/worker_pb/worker_grpc.pb.go b/weed/pb/worker_pb/worker_grpc.pb.go index 85bad96f4..0b0a9ad09 100644 --- a/weed/pb/worker_pb/worker_grpc.pb.go +++ b/weed/pb/worker_pb/worker_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v6.33.1 // source: worker.proto package worker_pb diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index 9076dc54f..57918fabe 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -66,8 +66,9 @@ type Identity struct { Account *Account Credentials []*Credential Actions []Action - PrincipalArn string // ARN for IAM authorization (e.g., "arn:aws:iam::account-id:user/username") - Disabled bool // User status: false = enabled (default), true = disabled + PolicyNames []string // Attached IAM policy names + PrincipalArn string // ARN for IAM authorization (e.g., "arn:aws:iam::account-id:user/username") + Disabled bool // User status: false = enabled (default), true = disabled } // Account represents a system user, a system user can @@ -310,6 +311,7 @@ func (iam *IdentityAccessManagement) loadS3ApiConfiguration(config *iam_pb.S3Api Actions: nil, PrincipalArn: generatePrincipalArn(ident.Name), Disabled: ident.Disabled, // false (default) = enabled, true = disabled + PolicyNames: ident.PolicyNames, } switch { case ident.Name == AccountAnonymous.Id: @@ -939,9 +941,10 @@ func (iam *IdentityAccessManagement) authenticateJWTWithIAM(r *http.Request) (*I // Convert IAMIdentity to existing Identity structure identity := &Identity{ - Name: iamIdentity.Name, - Account: iamIdentity.Account, - Actions: []Action{}, // Empty - authorization handled by policy engine + Name: iamIdentity.Name, + Account: iamIdentity.Account, + Actions: []Action{}, // Empty - authorization handled by policy engine + PolicyNames: iamIdentity.PolicyNames, } // Store session info in request headers for later authorization @@ -997,8 +1000,9 @@ func (iam *IdentityAccessManagement) authorizeWithIAM(r *http.Request, identity // Create IAMIdentity for authorization iamIdentity := &IAMIdentity{ - Name: identity.Name, - Account: identity.Account, + Name: identity.Name, + Account: identity.Account, + PolicyNames: identity.PolicyNames, } // Determine authorization path and configure identity diff --git a/weed/s3api/s3_iam_middleware.go b/weed/s3api/s3_iam_middleware.go index 22e7b2233..70f74508b 100644 --- a/weed/s3api/s3_iam_middleware.go +++ b/weed/s3api/s3_iam_middleware.go @@ -193,6 +193,7 @@ func (s3iam *S3IAMIntegration) AuthorizeAction(ctx context.Context, identity *IA Resource: resourceArn, SessionToken: identity.SessionToken, RequestContext: requestContext, + PolicyNames: identity.PolicyNames, } // Check if action is allowed using our policy engine @@ -214,6 +215,7 @@ type IAMIdentity struct { Principal string SessionToken string Account *Account + PolicyNames []string } // IsAdmin checks if the identity has admin privileges @@ -490,7 +492,8 @@ func (enhanced *EnhancedS3ApiServer) AuthenticateJWTRequest(r *http.Request) (*I Name: iamIdentity.Name, Account: iamIdentity.Account, // Note: Actions will be determined by policy evaluation - Actions: []Action{}, // Empty - authorization handled by policy engine + Actions: []Action{}, // Empty - authorization handled by policy engine + PolicyNames: iamIdentity.PolicyNames, } // Store session token for later authorization