mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-20 22:27:04 +00:00
object_store_users: fix specific bucket admin permission (#9014)
Fix an issue where seleting Sepecific Buckets with Admin permission while creating/editing an object store user would grant Admin permission on all buckets
This commit is contained in:
@@ -16,8 +16,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
<p class="mb-0 text-muted">Manage S3 API users and their access credentials</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-primary"
|
||||
data-bs-toggle="modal"
|
||||
<button type="button" class="btn btn-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#createUserModal">
|
||||
<i class="fas fa-plus me-1"></i>Create User
|
||||
</button>
|
||||
@@ -269,7 +269,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
<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">
|
||||
@@ -282,7 +282,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
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 -->
|
||||
@@ -376,7 +376,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
<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="editBucketScope" id="editAllBuckets" value="all" checked onchange="toggleBucketList('edit')">
|
||||
<label class="form-check-label" for="editAllBuckets">
|
||||
@@ -389,7 +389,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
Specific Buckets
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="editBucketSelectionList" class="mt-2" style="display: none;">
|
||||
<select multiple class="form-select" id="editSelectedBuckets" size="5">
|
||||
<!-- Options loaded dynamically -->
|
||||
@@ -505,15 +505,15 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
const STATUS_INACTIVE = 'Inactive';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
|
||||
// Event delegation for user action buttons
|
||||
document.addEventListener('click', function(e) {
|
||||
const button = e.target.closest('[data-action]');
|
||||
if (!button) return;
|
||||
|
||||
|
||||
const action = button.getAttribute('data-action');
|
||||
const username = button.getAttribute('data-username');
|
||||
|
||||
|
||||
switch (action) {
|
||||
case 'show-user-details':
|
||||
showUserDetails(username);
|
||||
@@ -553,7 +553,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
|
||||
// Load policies for dropdowns
|
||||
loadPolicies();
|
||||
|
||||
|
||||
// Load buckets for bucket permissions
|
||||
loadBuckets();
|
||||
});
|
||||
@@ -627,20 +627,20 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const policies = data.policies || [];
|
||||
|
||||
|
||||
const createSelect = document.getElementById('policies');
|
||||
const editSelect = document.getElementById('editPolicies');
|
||||
|
||||
|
||||
// Check if elements exist
|
||||
if (!createSelect || !editSelect) {
|
||||
console.warn('Policy select elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Clear existing options
|
||||
createSelect.innerHTML = '';
|
||||
editSelect.innerHTML = '';
|
||||
|
||||
|
||||
if (policies && policies.length > 0) {
|
||||
policies.forEach(policy => {
|
||||
const option = document.createElement('option');
|
||||
@@ -671,7 +671,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
mode = mode || 'create';
|
||||
const adminCheckbox = document.getElementById(mode === 'edit' ? 'editBucketAdmin' : 'bucketAdmin');
|
||||
const permissionFields = document.getElementById(mode === 'edit' ? 'editBucketPermissionFields' : 'bucketPermissionFields');
|
||||
|
||||
|
||||
if (adminCheckbox && permissionFields) {
|
||||
permissionFields.style.display = adminCheckbox.checked ? 'none' : 'block';
|
||||
}
|
||||
@@ -682,7 +682,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
mode = mode || 'create';
|
||||
const specificRadio = document.getElementById(mode === 'edit' ? 'editSpecificBuckets' : 'specificBuckets');
|
||||
const bucketList = document.getElementById(mode === 'edit' ? 'editBucketSelectionList' : 'bucketSelectionList');
|
||||
|
||||
|
||||
if (specificRadio && bucketList) {
|
||||
bucketList.style.display = specificRadio.checked ? 'block' : 'none';
|
||||
}
|
||||
@@ -710,7 +710,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
function populateBucketSelections() {
|
||||
const createSelect = document.getElementById('selectedBuckets');
|
||||
const editSelect = document.getElementById('editSelectedBuckets');
|
||||
|
||||
|
||||
[createSelect, editSelect].forEach(select => {
|
||||
if (select) {
|
||||
select.innerHTML = '';
|
||||
@@ -732,17 +732,17 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
applyToAll: false,
|
||||
specificBuckets: []
|
||||
};
|
||||
|
||||
|
||||
// Check if user has Admin permission
|
||||
if (actions.includes('Admin')) {
|
||||
result.isAdmin = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Separate bucket-scoped from global actions
|
||||
const bucketActions = [];
|
||||
const globalBucketPerms = [];
|
||||
|
||||
|
||||
actions.forEach(action => {
|
||||
if (action.startsWith('s3tables:')) {
|
||||
const actionValue = action.slice('s3tables:'.length);
|
||||
@@ -767,7 +767,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
globalBucketPerms.push(action);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// If we have global bucket permissions (no colon), they apply to all buckets
|
||||
if (globalBucketPerms.length > 0) {
|
||||
result.permissions = globalBucketPerms;
|
||||
@@ -776,12 +776,12 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
// Get unique permissions and buckets
|
||||
const perms = [...new Set(bucketActions.map(ba => ba.permission))];
|
||||
const buckets = [...new Set(bucketActions.map(ba => ba.bucketId))];
|
||||
|
||||
|
||||
result.permissions = perms;
|
||||
result.applyToAll = false;
|
||||
result.specificBuckets = buckets;
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -805,34 +805,34 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
mode = mode || 'create';
|
||||
const selectId = mode === 'edit' ? 'editActions' : 'actions';
|
||||
const permSelect = document.getElementById(selectId);
|
||||
|
||||
|
||||
if (!permSelect) return [];
|
||||
|
||||
|
||||
// Get selected permissions from the original multi-select
|
||||
const selectedPerms = Array.from(permSelect.selectedOptions).map(opt => opt.value);
|
||||
|
||||
|
||||
const hasAdmin = selectedPerms.includes('Admin');
|
||||
const hasS3TablesAdmin = selectedPerms.includes('S3TablesAdmin');
|
||||
|
||||
|
||||
if (selectedPerms.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// Check if applying to all buckets or specific ones
|
||||
// Use querySelector to find the checked radio button by name group
|
||||
const scopeName = mode === 'edit' ? 'editBucketScope' : 'bucketScope';
|
||||
|
||||
|
||||
// Try multiple methods to find the checked radio
|
||||
let checkedRadio = document.querySelector(`input[name="${scopeName}"]:checked`);
|
||||
|
||||
|
||||
// Fallback: check both radio buttons explicitly
|
||||
if (!checkedRadio) {
|
||||
const allBucketsId = mode === 'edit' ? 'editAllBuckets' : 'allBuckets';
|
||||
const specificBucketsId = mode === 'edit' ? 'editSpecificBuckets' : 'specificBuckets';
|
||||
|
||||
|
||||
const allBucketsRadio = document.getElementById(allBucketsId);
|
||||
const specificBucketsRadio = document.getElementById(specificBucketsId);
|
||||
|
||||
|
||||
if (specificBucketsRadio && specificBucketsRadio.checked) {
|
||||
checkedRadio = specificBucketsRadio;
|
||||
} else if (allBucketsRadio && allBucketsRadio.checked) {
|
||||
@@ -867,18 +867,21 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
// Get selected specific buckets
|
||||
const bucketSelect = document.getElementById(mode === 'edit' ? 'editSelectedBuckets' : 'selectedBuckets');
|
||||
if (!bucketSelect) return null;
|
||||
|
||||
|
||||
const selectedBuckets = [...new Set(Array.from(bucketSelect.selectedOptions).map(opt => opt.value))];
|
||||
|
||||
|
||||
// Return null to signal validation failure if no buckets selected
|
||||
if (selectedBuckets.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Build bucket-scoped permissions
|
||||
const actions = [];
|
||||
if (hasAdmin) {
|
||||
actions.push('Admin');
|
||||
selectedBuckets.forEach((bucket) => {
|
||||
const bucketInfo = parseBucketOptionValue(bucket);
|
||||
actions.push("Admin:" + bucketInfo.name);
|
||||
});
|
||||
}
|
||||
if (hasS3TablesAdmin) {
|
||||
actions.push('s3tables:*');
|
||||
@@ -898,7 +901,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return [...new Set(actions)];
|
||||
}
|
||||
}
|
||||
@@ -929,11 +932,11 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
const response = await fetch(`/api/users/${encodedUsername}`);
|
||||
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 => {
|
||||
@@ -947,11 +950,11 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
option.selected = user.policy_names && user.policy_names.includes(option.value);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Populate bucket permissions using original permissions dropdown
|
||||
if (user.actions && user.actions.length > 0) {
|
||||
const bucketPerms = parseBucketPermissions(user.actions);
|
||||
|
||||
|
||||
// Set permissions in the original multi-select
|
||||
const actionsSelect = document.getElementById('editActions');
|
||||
if (actionsSelect) {
|
||||
@@ -963,18 +966,18 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Set bucket scope (all or specific)
|
||||
const allBucketsRadio = document.getElementById('editAllBuckets');
|
||||
const specificBucketsRadio = document.getElementById('editSpecificBuckets');
|
||||
|
||||
|
||||
if (!bucketPerms.isAdmin) {
|
||||
if (bucketPerms.applyToAll) {
|
||||
if (allBucketsRadio) allBucketsRadio.checked = true;
|
||||
} else if (bucketPerms.specificBuckets.length > 0) {
|
||||
if (specificBucketsRadio) specificBucketsRadio.checked = true;
|
||||
toggleBucketList('edit');
|
||||
|
||||
|
||||
// Select specific buckets
|
||||
const bucketSelect = document.getElementById('editSelectedBuckets');
|
||||
if (bucketSelect) {
|
||||
@@ -985,7 +988,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Populate groups
|
||||
await populateEditUserGroups(username);
|
||||
|
||||
@@ -1029,7 +1032,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
const response = await fetch(`/api/users/${encodedUsername}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
showSuccessMessage('User deleted successfully');
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
@@ -1048,10 +1051,10 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
async function handleCreateUser() {
|
||||
const form = document.getElementById('createUserForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
|
||||
// Get permissions with bucket scope applied
|
||||
const allActions = buildBucketPermissions('create');
|
||||
|
||||
|
||||
if (allActions === null) {
|
||||
showAlert('Please select at least one bucket when using specific bucket permissions', 'error');
|
||||
return;
|
||||
@@ -1061,7 +1064,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
showAlert('At least one permission must be selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const userData = {
|
||||
username: formData.get('username'),
|
||||
email: formData.get('email'),
|
||||
@@ -1069,7 +1072,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
policy_names: Array.from(document.getElementById('policies').selectedOptions).map(option => option.value),
|
||||
generate_key: document.getElementById('generateKey').checked
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
@@ -1078,16 +1081,16 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
},
|
||||
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();
|
||||
@@ -1210,28 +1213,28 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
showAlert('Username is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get permissions with bucket scope applied
|
||||
const allActions = buildBucketPermissions('edit');
|
||||
|
||||
|
||||
// Check for null (validation failure from buildBucketPermissions)
|
||||
if (allActions === null) {
|
||||
showAlert('Please select at least one bucket when using specific bucket permissions', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Validate that permissions are not empty
|
||||
if (!allActions || allActions.length === 0) {
|
||||
showAlert('At least one permission must be selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const userData = {
|
||||
email: document.getElementById('editEmail').value,
|
||||
actions: allActions,
|
||||
policy_names: Array.from(document.getElementById('editPolicies').selectedOptions).map(option => option.value)
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
const encodedUsername = encodeURIComponent(username);
|
||||
const response = await fetch(`/api/users/${encodedUsername}`, {
|
||||
@@ -1241,10 +1244,10 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
},
|
||||
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();
|
||||
@@ -1321,12 +1324,12 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
if (!user.access_keys || user.access_keys.length === 0) {
|
||||
return '<p class="text-muted">No access keys available</p>';
|
||||
}
|
||||
|
||||
|
||||
var keysHtml = '<div class="table-responsive">';
|
||||
keysHtml += '<table class="table table-sm">';
|
||||
keysHtml += '<thead><tr><th>Access Key</th><th>Status</th><th>Actions</th></tr></thead>';
|
||||
keysHtml += '<tbody>';
|
||||
|
||||
|
||||
user.access_keys.forEach(function(key) {
|
||||
keysHtml += '<tr>';
|
||||
keysHtml += '<td><code>' + escapeHtml(key.access_key) + '</code></td>';
|
||||
@@ -1348,11 +1351,11 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
keysHtml += '</td>';
|
||||
keysHtml += '</tr>';
|
||||
});
|
||||
|
||||
|
||||
keysHtml += '</tbody>';
|
||||
keysHtml += '</table>';
|
||||
keysHtml += '</div>';
|
||||
|
||||
|
||||
// Add delegated event listener for view secret buttons
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.view-secret-btn').forEach(btn => {
|
||||
@@ -1363,7 +1366,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
});
|
||||
});
|
||||
}, 100);
|
||||
|
||||
|
||||
return keysHtml;
|
||||
}
|
||||
|
||||
@@ -1514,10 +1517,10 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
const response = await fetch(`/api/users/${encodedUsername}/access-keys/${encodedAccessKey}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
showSuccessMessage('Access key deleted successfully');
|
||||
|
||||
|
||||
// Refresh access keys display
|
||||
refreshAccessKeysList(username);
|
||||
} else {
|
||||
@@ -1552,4 +1555,4 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
}
|
||||
|
||||
// Helper functions for template
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user