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:
Moray Baruh
2026-04-10 18:10:05 -07:00
committed by GitHub
parent c390448906
commit 41ff105f47
2 changed files with 72 additions and 69 deletions
+71 -68
View File
@@ -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