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"
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Access key status constants
|
||||||
|
const (
|
||||||
|
AccessKeyStatusActive = "Active"
|
||||||
|
AccessKeyStatusInactive = "Inactive"
|
||||||
|
)
|
||||||
|
|
||||||
type AdminData struct {
|
type AdminData struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
TotalVolumes int `json:"total_volumes"`
|
TotalVolumes int `json:"total_volumes"`
|
||||||
@@ -69,9 +75,14 @@ type UpdateUserPoliciesRequest struct {
|
|||||||
type AccessKeyInfo struct {
|
type AccessKeyInfo struct {
|
||||||
AccessKey string `json:"access_key"`
|
AccessKey string `json:"access_key"`
|
||||||
SecretKey string `json:"secret_key"`
|
SecretKey string `json:"secret_key"`
|
||||||
|
Status string `json:"status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateAccessKeyStatusRequest struct {
|
||||||
|
Status string `json:"status" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserDetails struct {
|
type UserDetails struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ func (s *AdminServer) GetObjectStoreUserDetails(username string) (*UserDetails,
|
|||||||
details.AccessKeys = append(details.AccessKeys, AccessKeyInfo{
|
details.AccessKeys = append(details.AccessKeys, AccessKeyInfo{
|
||||||
AccessKey: cred.AccessKey,
|
AccessKey: cred.AccessKey,
|
||||||
SecretKey: cred.SecretKey,
|
SecretKey: cred.SecretKey,
|
||||||
|
Status: cred.Status,
|
||||||
CreatedAt: time.Now().AddDate(0, -1, 0), // Mock creation date
|
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{
|
credential := &iam_pb.Credential{
|
||||||
AccessKey: accessKey,
|
AccessKey: accessKey,
|
||||||
SecretKey: secretKey,
|
SecretKey: secretKey,
|
||||||
|
Status: AccessKeyStatusActive,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create access key using credential manager
|
// Create access key using credential manager
|
||||||
@@ -234,6 +236,7 @@ func (s *AdminServer) CreateAccessKey(username string) (*AccessKeyInfo, error) {
|
|||||||
return &AccessKeyInfo{
|
return &AccessKeyInfo{
|
||||||
AccessKey: accessKey,
|
AccessKey: accessKey,
|
||||||
SecretKey: secretKey,
|
SecretKey: secretKey,
|
||||||
|
Status: AccessKeyStatusActive,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -261,6 +264,51 @@ func (s *AdminServer) DeleteAccessKey(username, accessKeyId string) error {
|
|||||||
return nil
|
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)
|
// GetUserPolicies returns the policies for a user (actions)
|
||||||
func (s *AdminServer) GetUserPolicies(username string) ([]string, error) {
|
func (s *AdminServer) GetUserPolicies(username string) ([]string, error) {
|
||||||
if s.credentialManager == nil {
|
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.DELETE("/:username", dash.RequireWriteAccess(), h.userHandlers.DeleteUser)
|
||||||
usersApi.POST("/:username/access-keys", dash.RequireWriteAccess(), h.userHandlers.CreateAccessKey)
|
usersApi.POST("/:username/access-keys", dash.RequireWriteAccess(), h.userHandlers.CreateAccessKey)
|
||||||
usersApi.DELETE("/:username/access-keys/:accessKeyId", dash.RequireWriteAccess(), h.userHandlers.DeleteAccessKey)
|
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.GET("/:username/policies", h.userHandlers.GetUserPolicies)
|
||||||
usersApi.PUT("/:username/policies", dash.RequireWriteAccess(), h.userHandlers.UpdateUserPolicies)
|
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.DELETE("/:username", h.userHandlers.DeleteUser)
|
||||||
usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey)
|
usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey)
|
||||||
usersApi.DELETE("/:username/access-keys/:accessKeyId", h.userHandlers.DeleteAccessKey)
|
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.GET("/:username/policies", h.userHandlers.GetUserPolicies)
|
||||||
usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies)
|
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
|
// GetUserPolicies returns the policies for a user
|
||||||
func (h *UserHandlers) GetUserPolicies(c *gin.Context) {
|
func (h *UserHandlers) GetUserPolicies(c *gin.Context) {
|
||||||
username := c.Param("username")
|
username := c.Param("username")
|
||||||
|
|||||||
@@ -396,6 +396,10 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
|||||||
|
|
||||||
<!-- JavaScript for user management -->
|
<!-- JavaScript for user management -->
|
||||||
<script>
|
<script>
|
||||||
|
// Access key status constants
|
||||||
|
const STATUS_ACTIVE = 'Active';
|
||||||
|
const STATUS_INACTIVE = 'Inactive';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
// Event delegation for user action buttons
|
// 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
|
// Load policies for dropdowns
|
||||||
loadPolicies();
|
loadPolicies();
|
||||||
|
|
||||||
@@ -973,7 +988,12 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
|||||||
user.access_keys.forEach(function(key) {
|
user.access_keys.forEach(function(key) {
|
||||||
keysHtml += '<tr>';
|
keysHtml += '<tr>';
|
||||||
keysHtml += '<td><code>' + escapeHtml(key.access_key) + '</code></td>';
|
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>';
|
keysHtml += '<td>';
|
||||||
// Add "View Secret" button with data attributes
|
// 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) + '">';
|
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;
|
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
|
// Create new access key
|
||||||
async function createAccessKey() {
|
async function createAccessKey() {
|
||||||
const username = document.getElementById('accessKeysUsername').textContent;
|
const username = document.getElementById('accessKeysUsername').textContent;
|
||||||
@@ -1029,11 +1089,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
|||||||
showSuccessMessage('Access key created successfully');
|
showSuccessMessage('Access key created successfully');
|
||||||
|
|
||||||
// Refresh access keys display
|
// Refresh access keys display
|
||||||
const userResponse = await fetch(`/api/users/${username}`);
|
refreshAccessKeysList(username);
|
||||||
if (userResponse.ok) {
|
|
||||||
const user = await userResponse.json();
|
|
||||||
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error'));
|
showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error'));
|
||||||
@@ -1056,11 +1112,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
|||||||
showSuccessMessage('Access key deleted successfully');
|
showSuccessMessage('Access key deleted successfully');
|
||||||
|
|
||||||
// Refresh access keys display
|
// Refresh access keys display
|
||||||
const userResponse = await fetch(`/api/users/${username}`);
|
refreshAccessKeysList(username);
|
||||||
if (userResponse.ok) {
|
|
||||||
const user = await userResponse.json();
|
|
||||||
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error'));
|
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