* Implement Policy Attachment support for Object Store Users
- Added policy_names field to iam.proto and regenerated protos.
- Updated S3 API and IAM integration to support direct policy evaluation for users.
- Enhanced Admin UI to allow attaching policies to users via modals.
- Renamed 'policies' to 'policy_names' to clarify that it stores identifiers.
- Fixed syntax error in user_management.go.
* Fix policy dropdown not populating
The API returns {policies: [...]} but JavaScript was treating response as direct array.
Updated loadPolicies() to correctly access data.policies property.
* Add null safety checks for policy dropdowns
Added checks to prevent "undefined" errors when:
- Policy select elements don't exist
- Policy dropdowns haven't loaded yet
- User is being edited before policies are loaded
* Fix policy dropdown by using correct JSON field name
JSON response has lowercase 'name' field but JavaScript was accessing 'Name'.
Changed policy.Name to policy.name to match the IAMPolicy JSON structure.
* Fix policy names not being saved on user update
Changed condition from len(req.PolicyNames) > 0 to req.PolicyNames != nil
to ensure policy names are always updated when present in the request,
even if it's an empty array (to allow clearing policies).
* Add debug logging for policy names update flow
Added console.log in frontend and glog in backend to trace
policy_names data through the update process.
* Temporarily disable auto-reload for debugging
Commented out window.location.reload() so console logs are visible
when updating a user.
* Add detailed debug logging and alert for policy selection
Added console.log for each step and an alert to show policy_names value
to help diagnose why it's not being included in the request.
* Regenerate templ files for object_store_users
Ran templ generate to ensure _templ.go files are up to date with
the latest .templ changes including debug logging.
* Remove debug logging and restore normal functionality
Cleaned up temporary debug code (console.log and alert statements)
and re-enabled automatic page reload after user update.
* Add step-by-step alert debugging for policy update
Added 5 alert checkpoints to trace policy data through the update flow:
1. Check if policiesSelect element exists
2. Show selected policy values
3. Show userData.policy_names
4. Show full request body
5. Confirm server response
Temporarily disabled auto-reload to see alerts.
* Add version check alert on page load
Added alert on DOMContentLoaded to verify new JavaScript is being executed
and not cached by the browser.
* Compile templates using make
Ran make to compile all template files and install the weed binary.
* Add button click detection and make handleUpdateUser global
- Added inline alert on button click to verify click is detected
- Made handleUpdateUser a window-level function to ensure it's accessible
- Added alert at start of handleUpdateUser function
* Fix handleUpdateUser scope issue - remove duplicate definition
Removed duplicate function definition that was inside DOMContentLoaded.
Now handleUpdateUser is defined only once in global scope (line 383)
making it accessible when button onclick fires.
* Remove all duplicate handleUpdateUser definitions
Now handleUpdateUser is defined only once at the very top of the script
block (line 352), before DOMContentLoaded, ensuring it's available when
the button onclick fires.
* Add function existence check and error catching
Added alerts to check if handleUpdateUser is defined and wrapped
the function call in try-catch to capture any JavaScript errors.
Also added console.log statements to verify function definition.
* Simplify handleUpdateUser to non-async for testing
Removed async/await and added early return to test if function
can be called at all. This will help identify if async is causing
the issue.
* Add cache-control headers to prevent browser caching
Added no-cache headers to ShowObjectStoreUsers handler to prevent
aggressive browser caching of inline JavaScript in the HTML page.
* Fix syntax error - make handleUpdateUser async
Changed function back to async to fix 'await is only valid in async functions' error.
The cache-control headers are working - browser is now loading new code.
* Update version check to v3 to verify cache busting
Changed version alert to 'v3 - WITH EARLY RETURN' to confirm
the new code with early return statement is being loaded.
* Remove all debug code - clean implementation
Removed all alerts, console.logs, and test code.
Implemented clean policy update functionality with proper error handling.
* Add ETag header for cache-busting and update walkthrough
* Fix policy pre-selection in Edit User modal
- Updated admin.js editUser function to pre-select policies
- Root cause: duplicate editUser in admin.js overwrote inline version
- Added policy pre-selection logic to match inline template
- Verified working in browser: policies now pre-select correctly
* Fix policy persistence in handleUpdateUser
- Added policy_names field to userData payload in handleUpdateUser
- Policies were being lost because handleUpdateUser only sent email and actions
- Now collects selected policies from editPolicies dropdown
- Verified working: policies persist correctly across updates
* Fix XSS vulnerability in access keys display
- Escape HTML in access key display using escapeHtml utility
- Replace inline onclick handlers with data attributes
- Add event delegation for delete access key buttons
- Prevents script injection via malicious access key values
* Fix additional XSS vulnerabilities in user details display
- Escape HTML in actions badges (line 626)
- Escape HTML in policy_names badges (line 636)
- Prevents script injection via malicious action or policy names
* Fix XSS vulnerability in loadPolicies function
- Replace innerHTML string concatenation with DOM API
- Use createElement and textContent for safe policy name insertion
- Prevents script injection via malicious policy names
- Apply same pattern to both create and edit select elements
* Remove debug logging from UpdateObjectStoreUser
- Removed glog.V(0) debug statements
- Clean up temporary debugging code before production
* Remove duplicate handleUpdateUser function
- Removed inline handleUpdateUser that duplicated admin.js logic
- Removed debug console.log statement
- admin.js version is now the single source of truth
- Eliminates maintenance burden of keeping two versions in sync
* Refine user management and address code review feedback
- Preserve PolicyNames in UpdateUserPolicies
- Allow clearing actions in UpdateObjectStoreUser by checking for nil
- Remove version comment from object_store_users.templ
- Refactor loadPolicies for DRYness using cloneNode while keeping DOM API security
* IAM Authorization for Static Access Keys
* verified XSS Fixes in Templates
* fix div
748 lines
37 KiB
Plaintext
748 lines
37 KiB
Plaintext
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
)
|
|
|
|
templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-0 text-gray-800">
|
|
<i class="fas fa-users me-2"></i>Object Store Users
|
|
</h1>
|
|
<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"
|
|
data-bs-target="#createUserModal">
|
|
<i class="fas fa-plus me-1"></i>Create User
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card border-left-primary shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
|
Total Users
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalUsers)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-users fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card border-left-success shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
|
Total Users
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", len(data.Users))}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-user-check fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card border-left-info shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
|
Last Updated
|
|
</div>
|
|
<div class="h6 mb-0 font-weight-bold text-gray-800">
|
|
{data.LastUpdated.Format("15:04")}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-clock fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Users Table -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-users me-2"></i>Object Store Users
|
|
</h6>
|
|
<div class="dropdown no-arrow">
|
|
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i>
|
|
</a>
|
|
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in">
|
|
<div class="dropdown-header">Actions:</div>
|
|
<a class="dropdown-item" href="#" onclick="exportUsers()">
|
|
<i class="fas fa-download me-2"></i>Export List
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover" width="100%" cellspacing="0" id="usersTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Username</th>
|
|
<th>Email</th>
|
|
<th>Access Key</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, user := range data.Users {
|
|
<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<i class="fas fa-user me-2 text-muted"></i>
|
|
<strong>{user.Username}</strong>
|
|
</div>
|
|
</td>
|
|
<td>{user.Email}</td>
|
|
<td>
|
|
<code class="text-muted">{user.AccessKey}</code>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button type="button" class="btn btn-outline-info"
|
|
data-action="show-user-details" data-username={ user.Username }>
|
|
<i class="fas fa-info-circle"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary"
|
|
data-action="edit-user" data-username={ user.Username }>
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary"
|
|
data-action="manage-access-keys" data-username={ user.Username }>
|
|
<i class="fas fa-key"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger"
|
|
data-action="delete-user" data-username={ user.Username }>
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
if len(data.Users) == 0 {
|
|
<tr>
|
|
<td colspan="4" class="text-center text-muted py-4">
|
|
<i class="fas fa-users fa-3x mb-3 text-muted"></i>
|
|
<div>
|
|
<h5>No users found</h5>
|
|
<p>Create your first object store user to get started.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Last Updated -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<small class="text-muted">
|
|
<i class="fas fa-clock me-1"></i>
|
|
Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create User Modal -->
|
|
<div class="modal fade" id="createUserModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-user-plus me-2"></i>Create New User
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="createUserForm">
|
|
<div class="mb-3">
|
|
<label for="username" class="form-label">Username *</label>
|
|
<input type="text" class="form-control" id="username" name="username" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="email" class="form-label">Email</label>
|
|
<input type="email" class="form-control" id="email" name="email">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="actions" class="form-label">Permissions</label>
|
|
<select multiple class="form-control" id="actions" name="actions" size="10">
|
|
<option value="Admin">Admin (Full Access)</option>
|
|
<option value="Read">Read</option>
|
|
<option value="Write">Write</option>
|
|
<option value="List">List</option>
|
|
<option value="Tagging">Tagging</option>
|
|
<optgroup label="Object Lock Permissions">
|
|
<option value="BypassGovernanceRetention">Bypass Governance Retention</option>
|
|
<option value="GetObjectRetention">Get Object Retention</option>
|
|
<option value="PutObjectRetention">Put Object Retention</option>
|
|
<option value="GetObjectLegalHold">Get Object Legal Hold</option>
|
|
<option value="PutObjectLegalHold">Put Object Legal Hold</option>
|
|
<option value="GetBucketObjectLockConfiguration">Get Bucket Object Lock Configuration</option>
|
|
<option value="PutBucketObjectLockConfiguration">Put Bucket Object Lock Configuration</option>
|
|
</optgroup>
|
|
</select>
|
|
<small class="form-text text-muted">Hold Ctrl/Cmd to select multiple permissions</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="policies" class="form-label">Attached Policies</label>
|
|
<select multiple class="form-control" id="policies" name="policies" size="5">
|
|
<!-- Options loaded dynamically -->
|
|
</select>
|
|
<small class="form-text text-muted">Hold Ctrl/Cmd to select multiple policies</small>
|
|
</div>
|
|
<div class="mb-3 form-check">
|
|
<input type="checkbox" class="form-check-input" id="generateKey" name="generateKey" checked>
|
|
<label class="form-check-label" for="generateKey">
|
|
Generate access key automatically
|
|
</label>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="handleCreateUser()">Create User</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit User Modal -->
|
|
<div class="modal fade" id="editUserModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-user-edit me-2"></i>Edit User
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editUserForm">
|
|
<input type="hidden" id="editUsername" name="username">
|
|
<div class="mb-3">
|
|
<label for="editEmail" class="form-label">Email</label>
|
|
<input type="email" class="form-control" id="editEmail" name="email">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="editActions" class="form-label">Permissions</label>
|
|
<select multiple class="form-control" id="editActions" name="actions" size="10">
|
|
<option value="Admin">Admin (Full Access)</option>
|
|
<option value="Read">Read</option>
|
|
<option value="Write">Write</option>
|
|
<option value="List">List</option>
|
|
<option value="Tagging">Tagging</option>
|
|
<optgroup label="Object Lock Permissions">
|
|
<option value="BypassGovernanceRetention">Bypass Governance Retention</option>
|
|
<option value="GetObjectRetention">Get Object Retention</option>
|
|
<option value="PutObjectRetention">Put Object Retention</option>
|
|
<option value="GetObjectLegalHold">Get Object Legal Hold</option>
|
|
<option value="PutObjectLegalHold">Put Object Legal Hold</option>
|
|
<option value="GetBucketObjectLockConfiguration">Get Bucket Object Lock Configuration</option>
|
|
<option value="PutBucketObjectLockConfiguration">Put Bucket Object Lock Configuration</option>
|
|
</optgroup>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="editPolicies" class="form-label">Attached Policies</label>
|
|
<select multiple class="form-control" id="editPolicies" name="policies" size="5">
|
|
<!-- Options loaded dynamically -->
|
|
</select>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="handleUpdateUser()">Update User</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Details Modal -->
|
|
<div class="modal fade" id="userDetailsModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-user me-2"></i>User Details
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="userDetailsContent">
|
|
<!-- Content will be loaded dynamically -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Access Keys Management Modal -->
|
|
<div class="modal fade" id="accessKeysModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-key me-2"></i>Manage Access Keys
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6>Access Keys for <span id="accessKeysUsername"></span></h6>
|
|
<button type="button" class="btn btn-primary btn-sm" onclick="createAccessKey()">
|
|
<i class="fas fa-plus me-1"></i>Create New Key
|
|
</button>
|
|
</div>
|
|
<div id="accessKeysContent">
|
|
<!-- Content will be loaded dynamically -->
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JavaScript for user management -->
|
|
<script>
|
|
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);
|
|
break;
|
|
case 'edit-user':
|
|
editUser(username);
|
|
break;
|
|
case 'manage-access-keys':
|
|
manageAccessKeys(username);
|
|
break;
|
|
case 'delete-user':
|
|
deleteUser(username);
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Event delegation for delete access key buttons
|
|
document.addEventListener('click', function(e) {
|
|
const deleteBtn = e.target.closest('.delete-access-key-btn');
|
|
if (deleteBtn) {
|
|
const username = deleteBtn.getAttribute('data-username');
|
|
const accessKey = deleteBtn.getAttribute('data-access-key');
|
|
deleteAccessKey(username, accessKey);
|
|
}
|
|
});
|
|
|
|
// Load policies for dropdowns
|
|
loadPolicies();
|
|
});
|
|
|
|
// Load policies
|
|
async function loadPolicies() {
|
|
try {
|
|
const response = await fetch('/api/object-store/policies');
|
|
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');
|
|
option.value = policy.name;
|
|
option.textContent = policy.name;
|
|
createSelect.appendChild(option);
|
|
editSelect.appendChild(option.cloneNode(true));
|
|
});
|
|
} else {
|
|
const emptyOption = document.createElement('option');
|
|
emptyOption.disabled = true;
|
|
emptyOption.textContent = 'No policies available';
|
|
createSelect.appendChild(emptyOption);
|
|
editSelect.appendChild(emptyOption.cloneNode(true));
|
|
}
|
|
} else {
|
|
const error = await response.json().catch(() => ({}));
|
|
showErrorMessage('Failed to load policies: ' + (error.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading policies:', error);
|
|
showErrorMessage('Failed to load policies: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Show user details modal
|
|
async function showUserDetails(username) {
|
|
try {
|
|
const response = await fetch(`/api/users/${username}`);
|
|
if (response.ok) {
|
|
const user = await response.json();
|
|
document.getElementById('userDetailsContent').innerHTML = createUserDetailsContent(user);
|
|
const modal = new bootstrap.Modal(document.getElementById('userDetailsModal'));
|
|
modal.show();
|
|
} else {
|
|
showErrorMessage('Failed to load user details');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading user details:', error);
|
|
showErrorMessage('Failed to load user details');
|
|
}
|
|
}
|
|
|
|
// Edit user function
|
|
async function editUser(username) {
|
|
try {
|
|
const response = await fetch(`/api/users/${username}`);
|
|
if (response.ok) {
|
|
const user = await response.json();
|
|
|
|
// Populate edit form
|
|
document.getElementById('editUsername').value = username;
|
|
document.getElementById('editEmail').value = user.email || '';
|
|
|
|
// Set selected actions
|
|
const actionsSelect = document.getElementById('editActions');
|
|
Array.from(actionsSelect.options).forEach(option => {
|
|
option.selected = user.actions && user.actions.includes(option.value);
|
|
});
|
|
|
|
// Set selected policies
|
|
const policiesSelect = document.getElementById('editPolicies');
|
|
if (policiesSelect) {
|
|
Array.from(policiesSelect.options).forEach(option => {
|
|
option.selected = user.policy_names && user.policy_names.includes(option.value);
|
|
});
|
|
}
|
|
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
|
|
modal.show();
|
|
} else {
|
|
showErrorMessage('Failed to load user details');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading user:', error);
|
|
showErrorMessage('Failed to load user details');
|
|
}
|
|
}
|
|
|
|
// Manage access keys function
|
|
async function manageAccessKeys(username) {
|
|
try {
|
|
const response = await fetch(`/api/users/${username}`);
|
|
if (response.ok) {
|
|
const user = await response.json();
|
|
document.getElementById('accessKeysUsername').textContent = username;
|
|
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
|
|
const modal = new bootstrap.Modal(document.getElementById('accessKeysModal'));
|
|
modal.show();
|
|
} else {
|
|
showErrorMessage('Failed to load access keys');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading access keys:', error);
|
|
showErrorMessage('Failed to load access keys');
|
|
}
|
|
}
|
|
|
|
// Delete user function
|
|
async function deleteUser(username) {
|
|
if (confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) {
|
|
try {
|
|
const response = await fetch(`/api/users/${username}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
showSuccessMessage('User deleted successfully');
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} else {
|
|
const error = await response.json();
|
|
showErrorMessage('Failed to delete user: ' + (error.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting user:', error);
|
|
showErrorMessage('Failed to delete user: ' + error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle create user form submission
|
|
async function handleCreateUser() {
|
|
const form = document.getElementById('createUserForm');
|
|
const formData = new FormData(form);
|
|
|
|
const userData = {
|
|
username: formData.get('username'),
|
|
email: formData.get('email'),
|
|
actions: Array.from(document.getElementById('actions').selectedOptions).map(option => option.value),
|
|
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',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(userData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
showSuccessMessage('User created successfully');
|
|
|
|
// Show the created access key if generated
|
|
if (result.user && result.user.access_key) {
|
|
showNewAccessKeyModal(result.user);
|
|
}
|
|
|
|
// Close modal and refresh page
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal'));
|
|
modal.hide();
|
|
form.reset();
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} else {
|
|
const error = await response.json();
|
|
showErrorMessage('Failed to create user: ' + (error.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating user:', error);
|
|
showErrorMessage('Failed to create user: ' + error.message);
|
|
}
|
|
}
|
|
|
|
|
|
// Create user details content
|
|
function createUserDetailsContent(user) {
|
|
var detailsHtml = '<div class="row">';
|
|
detailsHtml += '<div class="col-md-6">';
|
|
detailsHtml += '<h6 class="text-muted">Basic Information</h6>';
|
|
detailsHtml += '<table class="table table-sm">';
|
|
detailsHtml += '<tr><td><strong>Username:</strong></td><td>' + escapeHtml(user.username) + '</td></tr>';
|
|
detailsHtml += '<tr><td><strong>Email:</strong></td><td>' + escapeHtml(user.email || 'Not set') + '</td></tr>';
|
|
detailsHtml += '</table>';
|
|
detailsHtml += '</div>';
|
|
detailsHtml += '<div class="col-md-6">';
|
|
detailsHtml += '<h6 class="text-muted">Permissions</h6>';
|
|
detailsHtml += '<div class="mb-3">';
|
|
if (user.actions && user.actions.length > 0) {
|
|
detailsHtml += user.actions.map(function(action) {
|
|
return '<span class="badge bg-info me-1">' + escapeHtml(action) + '</span>';
|
|
}).join('');
|
|
} else {
|
|
detailsHtml += '<span class="text-muted">No permissions assigned</span>';
|
|
}
|
|
detailsHtml += '</div>';
|
|
detailsHtml += '<h6 class="text-muted">Attached Policy Names</h6>';
|
|
detailsHtml += '<div class="mb-3">';
|
|
if (user.policy_names && user.policy_names.length > 0) {
|
|
detailsHtml += user.policy_names.map(function(policy) {
|
|
return '<span class="badge bg-secondary me-1">' + escapeHtml(policy) + '</span>';
|
|
}).join('');
|
|
} else {
|
|
detailsHtml += '<span class="text-muted">No policies attached</span>';
|
|
}
|
|
detailsHtml += '</div>';
|
|
detailsHtml += '<h6 class="text-muted">Access Keys</h6>';
|
|
if (user.access_keys && user.access_keys.length > 0) {
|
|
detailsHtml += '<div class="mb-2">';
|
|
user.access_keys.forEach(function(key) {
|
|
detailsHtml += '<div><code class="text-muted">' + escapeHtml(key.access_key) + '</code></div>';
|
|
});
|
|
detailsHtml += '</div>';
|
|
} else {
|
|
detailsHtml += '<p class="text-muted">No access keys</p>';
|
|
}
|
|
detailsHtml += '</div>';
|
|
detailsHtml += '</div>';
|
|
return detailsHtml;
|
|
}
|
|
|
|
// Create access keys content
|
|
function createAccessKeysContent(user) {
|
|
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>';
|
|
keysHtml += '<td><span class="badge bg-success">Active</span></td>';
|
|
keysHtml += '<td>';
|
|
keysHtml += '<button class="btn btn-outline-danger btn-sm delete-access-key-btn" data-username="' + escapeHtml(user.username) + '" data-access-key="' + escapeHtml(key.access_key) + '">';
|
|
keysHtml += '<i class="fas fa-trash"></i> Delete';
|
|
keysHtml += '</button>';
|
|
keysHtml += '</td>';
|
|
keysHtml += '</tr>';
|
|
});
|
|
|
|
keysHtml += '</tbody>';
|
|
keysHtml += '</table>';
|
|
keysHtml += '</div>';
|
|
return keysHtml;
|
|
}
|
|
|
|
// Create new access key
|
|
async function createAccessKey() {
|
|
const username = document.getElementById('accessKeysUsername').textContent;
|
|
|
|
try {
|
|
const response = await fetch(`/api/users/${username}/access-keys`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
showSuccessMessage('Access key created successfully');
|
|
|
|
// Refresh access keys display
|
|
const userResponse = await fetch(`/api/users/${username}`);
|
|
if (userResponse.ok) {
|
|
const user = await userResponse.json();
|
|
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
|
|
}
|
|
} else {
|
|
const error = await response.json();
|
|
showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating access key:', error);
|
|
showErrorMessage('Failed to create access key: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Delete access key
|
|
async function deleteAccessKey(username, accessKey) {
|
|
if (confirm('Are you sure you want to delete this access key?')) {
|
|
try {
|
|
const response = await fetch(`/api/users/${username}/access-keys/${accessKey}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
showSuccessMessage('Access key deleted successfully');
|
|
|
|
// Refresh access keys display
|
|
const userResponse = await fetch(`/api/users/${username}`);
|
|
if (userResponse.ok) {
|
|
const user = await userResponse.json();
|
|
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
|
|
}
|
|
} else {
|
|
const error = await response.json();
|
|
showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting access key:', error);
|
|
showErrorMessage('Failed to delete access key: ' + error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show new access key modal (when user is created with generated key)
|
|
function showNewAccessKeyModal(user) {
|
|
// Create a simple alert for now - could be enhanced with a dedicated modal
|
|
var message = 'New user created!\n\n';
|
|
message += 'Username: ' + user.username + '\n';
|
|
message += 'Access Key: ' + user.access_key + '\n';
|
|
message += 'Secret Key: ' + user.secret_key + '\n\n';
|
|
message += 'Please save these credentials securely.';
|
|
alert(message);
|
|
}
|
|
|
|
// Utility functions
|
|
function showSuccessMessage(message) {
|
|
// Simple implementation - could be enhanced with toast notifications
|
|
alert('Success: ' + message);
|
|
}
|
|
|
|
function showErrorMessage(message) {
|
|
// Simple implementation - could be enhanced with toast notifications
|
|
alert('Error: ' + message);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
}
|
|
|
|
// Helper functions for template
|
|
|