Add access key status management to Admin UI (#8050)
* Add access key status management to Admin UI - Add Status field to AccessKeyInfo struct - Implement UpdateAccessKeyStatus API endpoint - Add status dropdown in access keys modal - Fix modal backdrop issue by using refreshAccessKeysList helper - Status can be toggled between Active and Inactive * Replace magic strings with constants for access key status - Define AccessKeyStatusActive and AccessKeyStatusInactive constants in admin_data.go - Define STATUS_ACTIVE and STATUS_INACTIVE constants in JavaScript - Replace all hardcoded 'Active' and 'Inactive' strings with constants - Update error messages to use constants for consistency * Remove duplicate manageAccessKeys function definition * Add security improvements to access key status management - Add status validation in UpdateAccessKeyStatus to prevent invalid values - Fix XSS vulnerability by replacing inline onchange with data attributes - Add delegated event listener for status select changes - Add URL encoding to API request path segments
This commit is contained in:
@@ -12,6 +12,12 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
)
|
||||
|
||||
// Access key status constants
|
||||
const (
|
||||
AccessKeyStatusActive = "Active"
|
||||
AccessKeyStatusInactive = "Inactive"
|
||||
)
|
||||
|
||||
type AdminData struct {
|
||||
Username string `json:"username"`
|
||||
TotalVolumes int `json:"total_volumes"`
|
||||
@@ -69,9 +75,14 @@ type UpdateUserPoliciesRequest struct {
|
||||
type AccessKeyInfo struct {
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type UpdateAccessKeyStatusRequest struct {
|
||||
Status string `json:"status" binding:"required"`
|
||||
}
|
||||
|
||||
type UserDetails struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
|
||||
@@ -192,6 +192,7 @@ func (s *AdminServer) GetObjectStoreUserDetails(username string) (*UserDetails,
|
||||
details.AccessKeys = append(details.AccessKeys, AccessKeyInfo{
|
||||
AccessKey: cred.AccessKey,
|
||||
SecretKey: cred.SecretKey,
|
||||
Status: cred.Status,
|
||||
CreatedAt: time.Now().AddDate(0, -1, 0), // Mock creation date
|
||||
})
|
||||
}
|
||||
@@ -223,6 +224,7 @@ func (s *AdminServer) CreateAccessKey(username string) (*AccessKeyInfo, error) {
|
||||
credential := &iam_pb.Credential{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Status: AccessKeyStatusActive,
|
||||
}
|
||||
|
||||
// Create access key using credential manager
|
||||
@@ -234,6 +236,7 @@ func (s *AdminServer) CreateAccessKey(username string) (*AccessKeyInfo, error) {
|
||||
return &AccessKeyInfo{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Status: AccessKeyStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
@@ -261,6 +264,51 @@ func (s *AdminServer) DeleteAccessKey(username, accessKeyId string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAccessKeyStatus updates the status of an access key for a user
|
||||
func (s *AdminServer) UpdateAccessKeyStatus(username, accessKeyId, status string) error {
|
||||
if s.credentialManager == nil {
|
||||
return fmt.Errorf("credential manager not available")
|
||||
}
|
||||
|
||||
// Validate status against allowed values
|
||||
if status != AccessKeyStatusActive && status != AccessKeyStatusInactive {
|
||||
return fmt.Errorf("invalid status '%s': must be '%s' or '%s'", status, AccessKeyStatusActive, AccessKeyStatusInactive)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get user using credential manager
|
||||
identity, err := s.credentialManager.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
if err == credential.ErrUserNotFound {
|
||||
return fmt.Errorf("user %s not found", username)
|
||||
}
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
// Find and update the access key status
|
||||
found := false
|
||||
for _, cred := range identity.Credentials {
|
||||
if cred.AccessKey == accessKeyId {
|
||||
cred.Status = status
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("access key %s not found for user %s", accessKeyId, username)
|
||||
}
|
||||
|
||||
// Update user using credential manager
|
||||
err = s.credentialManager.UpdateUser(ctx, username, identity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user access key status: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserPolicies returns the policies for a user (actions)
|
||||
func (s *AdminServer) GetUserPolicies(username string) ([]string, error) {
|
||||
if s.credentialManager == nil {
|
||||
|
||||
@@ -148,6 +148,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser,
|
||||
usersApi.DELETE("/:username", dash.RequireWriteAccess(), h.userHandlers.DeleteUser)
|
||||
usersApi.POST("/:username/access-keys", dash.RequireWriteAccess(), h.userHandlers.CreateAccessKey)
|
||||
usersApi.DELETE("/:username/access-keys/:accessKeyId", dash.RequireWriteAccess(), h.userHandlers.DeleteAccessKey)
|
||||
usersApi.PUT("/:username/access-keys/:accessKeyId/status", dash.RequireWriteAccess(), h.userHandlers.UpdateAccessKeyStatus)
|
||||
usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies)
|
||||
usersApi.PUT("/:username/policies", dash.RequireWriteAccess(), h.userHandlers.UpdateUserPolicies)
|
||||
}
|
||||
@@ -288,6 +289,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser,
|
||||
usersApi.DELETE("/:username", h.userHandlers.DeleteUser)
|
||||
usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey)
|
||||
usersApi.DELETE("/:username/access-keys/:accessKeyId", h.userHandlers.DeleteAccessKey)
|
||||
usersApi.PUT("/:username/access-keys/:accessKeyId/status", h.userHandlers.UpdateAccessKeyStatus)
|
||||
usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies)
|
||||
usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies)
|
||||
}
|
||||
|
||||
@@ -189,6 +189,40 @@ func (h *UserHandlers) DeleteAccessKey(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAccessKeyStatus updates the status of an access key for a user
|
||||
func (h *UserHandlers) UpdateAccessKeyStatus(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
accessKeyId := c.Param("accessKeyId")
|
||||
|
||||
if username == "" || accessKeyId == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username and access key ID are required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dash.UpdateAccessKeyStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate status
|
||||
if req.Status != dash.AccessKeyStatusActive && req.Status != dash.AccessKeyStatusInactive {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Status must be '%s' or '%s'", dash.AccessKeyStatusActive, dash.AccessKeyStatusInactive)})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.adminServer.UpdateAccessKeyStatus(username, accessKeyId, req.Status)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to update access key status %s for user %s: %v", accessKeyId, username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update access key status: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Access key updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserPolicies returns the policies for a user
|
||||
func (h *UserHandlers) GetUserPolicies(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
|
||||
@@ -396,6 +396,10 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
|
||||
<!-- JavaScript for user management -->
|
||||
<script>
|
||||
// Access key status constants
|
||||
const STATUS_ACTIVE = 'Active';
|
||||
const STATUS_INACTIVE = 'Inactive';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Event delegation for user action buttons
|
||||
@@ -432,6 +436,17 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
}
|
||||
});
|
||||
|
||||
// Event delegation for access key status changes
|
||||
document.addEventListener('change', function(e) {
|
||||
const statusSelect = e.target.closest('.access-key-status-select');
|
||||
if (statusSelect) {
|
||||
const username = statusSelect.getAttribute('data-username');
|
||||
const accessKey = statusSelect.getAttribute('data-access-key');
|
||||
const newStatus = statusSelect.value;
|
||||
updateAccessKeyStatus(username, accessKey, newStatus);
|
||||
}
|
||||
});
|
||||
|
||||
// Load policies for dropdowns
|
||||
loadPolicies();
|
||||
|
||||
@@ -973,7 +988,12 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
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 += '<select class="form-select form-select-sm access-key-status-select" data-username="' + escapeHtml(user.username) + '" data-access-key="' + escapeHtml(key.access_key) + '" style="width: 110px;">';
|
||||
keysHtml += '<option value="' + STATUS_ACTIVE + '" ' + (key.status === STATUS_ACTIVE || !key.status ? 'selected' : '') + '>' + STATUS_ACTIVE + '</option>';
|
||||
keysHtml += '<option value="' + STATUS_INACTIVE + '" ' + (key.status === STATUS_INACTIVE ? 'selected' : '') + '>' + STATUS_INACTIVE + '</option>';
|
||||
keysHtml += '</select>';
|
||||
keysHtml += '</td>';
|
||||
keysHtml += '<td>';
|
||||
// Add "View Secret" button with data attributes
|
||||
keysHtml += '<button class="btn btn-outline-secondary btn-sm me-2 view-secret-btn" data-access-key="' + escapeHtml(key.access_key) + '" data-secret-key="' + escapeHtml(key.secret_key) + '">';
|
||||
@@ -1005,6 +1025,46 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
return keysHtml;
|
||||
}
|
||||
|
||||
// Refresh access keys list content
|
||||
async function refreshAccessKeysList(username) {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${username}`);
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing access keys:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update access key status
|
||||
async function updateAccessKeyStatus(username, accessKey, status) {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${encodeURIComponent(username)}/access-keys/${encodeURIComponent(accessKey)}/status`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ status: status })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showSuccessMessage('Access key status updated successfully');
|
||||
// Refresh access keys display without toggling modal
|
||||
refreshAccessKeysList(username);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showErrorMessage('Failed to update access key status: ' + (error.error || 'Unknown error'));
|
||||
refreshAccessKeysList(username);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating access key status:', error);
|
||||
showErrorMessage('Failed to update access key status: ' + error.message);
|
||||
refreshAccessKeysList(username);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new access key
|
||||
async function createAccessKey() {
|
||||
const username = document.getElementById('accessKeysUsername').textContent;
|
||||
@@ -1029,11 +1089,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
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);
|
||||
}
|
||||
refreshAccessKeysList(username);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error'));
|
||||
@@ -1056,11 +1112,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
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);
|
||||
}
|
||||
refreshAccessKeysList(username);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error'));
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user