diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index 95bff6deb..9ae5c6ebd 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -94,6 +94,7 @@ func NewAdminServer(masterAddress string, templateFS http.FileSystem, dataDir st glog.V(1).Infof("Set filer client for credential manager: %s", filerAddr) break } + glog.V(1).Infof("Waiting for filer discovery for credential manager...") time.Sleep(5 * time.Second) // Retry every 5 seconds } }() diff --git a/weed/admin/dash/file_browser_data.go b/weed/admin/dash/file_browser_data.go index 3cb878718..6bb30c469 100644 --- a/weed/admin/dash/file_browser_data.go +++ b/weed/admin/dash/file_browser_data.go @@ -99,7 +99,7 @@ func (s *AdminServer) GetFileBrowser(path string) (*FileBrowserData, error) { var ttlSec int32 if entry.Attributes != nil { - mode = formatFileMode(entry.Attributes.FileMode) + mode = FormatFileMode(entry.Attributes.FileMode) uid = entry.Attributes.Uid gid = entry.Attributes.Gid size = int64(entry.Attributes.FileSize) @@ -270,81 +270,3 @@ func (s *AdminServer) generateBreadcrumbs(path string) []BreadcrumbItem { return breadcrumbs } - -// formatFileMode converts file mode to Unix-style string representation (e.g., "drwxr-xr-x") -func formatFileMode(mode uint32) string { - var result []byte = make([]byte, 10) - - // File type - switch mode & 0170000 { // S_IFMT mask - case 0040000: // S_IFDIR - result[0] = 'd' - case 0100000: // S_IFREG - result[0] = '-' - case 0120000: // S_IFLNK - result[0] = 'l' - case 0020000: // S_IFCHR - result[0] = 'c' - case 0060000: // S_IFBLK - result[0] = 'b' - case 0010000: // S_IFIFO - result[0] = 'p' - case 0140000: // S_IFSOCK - result[0] = 's' - default: - result[0] = '-' // S_IFREG is default - } - - // Owner permissions - if mode&0400 != 0 { // S_IRUSR - result[1] = 'r' - } else { - result[1] = '-' - } - if mode&0200 != 0 { // S_IWUSR - result[2] = 'w' - } else { - result[2] = '-' - } - if mode&0100 != 0 { // S_IXUSR - result[3] = 'x' - } else { - result[3] = '-' - } - - // Group permissions - if mode&0040 != 0 { // S_IRGRP - result[4] = 'r' - } else { - result[4] = '-' - } - if mode&0020 != 0 { // S_IWGRP - result[5] = 'w' - } else { - result[5] = '-' - } - if mode&0010 != 0 { // S_IXGRP - result[6] = 'x' - } else { - result[6] = '-' - } - - // Other permissions - if mode&0004 != 0 { // S_IROTH - result[7] = 'r' - } else { - result[7] = '-' - } - if mode&0002 != 0 { // S_IWOTH - result[8] = 'w' - } else { - result[8] = '-' - } - if mode&0001 != 0 { // S_IXOTH - result[9] = 'x' - } else { - result[9] = '-' - } - - return string(result) -} diff --git a/weed/admin/dash/file_mode_utils.go b/weed/admin/dash/file_mode_utils.go new file mode 100644 index 000000000..19c5b2f49 --- /dev/null +++ b/weed/admin/dash/file_mode_utils.go @@ -0,0 +1,85 @@ +package dash + +// FormatFileMode converts file mode to Unix-style string representation (e.g., "drwxr-xr-x") +// Handles both Go's os.ModeDir format and standard Unix file type bits +func FormatFileMode(mode uint32) string { + var result []byte = make([]byte, 10) + + // File type - handle Go's os.ModeDir first, then standard Unix file type bits + if mode&0x80000000 != 0 { // Go's os.ModeDir (0x80000000 = 2147483648) + result[0] = 'd' + } else { + switch mode & 0170000 { // S_IFMT mask + case 0040000: // S_IFDIR + result[0] = 'd' + case 0100000: // S_IFREG + result[0] = '-' + case 0120000: // S_IFLNK + result[0] = 'l' + case 0020000: // S_IFCHR + result[0] = 'c' + case 0060000: // S_IFBLK + result[0] = 'b' + case 0010000: // S_IFIFO + result[0] = 'p' + case 0140000: // S_IFSOCK + result[0] = 's' + default: + result[0] = '-' // S_IFREG is default + } + } + + // Permission bits (always use the lower 12 bits regardless of file type format) + // Owner permissions + if mode&0400 != 0 { // S_IRUSR + result[1] = 'r' + } else { + result[1] = '-' + } + if mode&0200 != 0 { // S_IWUSR + result[2] = 'w' + } else { + result[2] = '-' + } + if mode&0100 != 0 { // S_IXUSR + result[3] = 'x' + } else { + result[3] = '-' + } + + // Group permissions + if mode&0040 != 0 { // S_IRGRP + result[4] = 'r' + } else { + result[4] = '-' + } + if mode&0020 != 0 { // S_IWGRP + result[5] = 'w' + } else { + result[5] = '-' + } + if mode&0010 != 0 { // S_IXGRP + result[6] = 'x' + } else { + result[6] = '-' + } + + // Other permissions + if mode&0004 != 0 { // S_IROTH + result[7] = 'r' + } else { + result[7] = '-' + } + if mode&0002 != 0 { // S_IWOTH + result[8] = 'w' + } else { + result[8] = '-' + } + if mode&0001 != 0 { // S_IXOTH + result[9] = 'x' + } else { + result[9] = '-' + } + + return string(result) +} diff --git a/weed/admin/dash/policies_management.go b/weed/admin/dash/policies_management.go new file mode 100644 index 000000000..8853bbb54 --- /dev/null +++ b/weed/admin/dash/policies_management.go @@ -0,0 +1,225 @@ +package dash + +import ( + "context" + "fmt" + "time" + + "github.com/seaweedfs/seaweedfs/weed/credential" + "github.com/seaweedfs/seaweedfs/weed/glog" +) + +type IAMPolicy struct { + Name string `json:"name"` + Document credential.PolicyDocument `json:"document"` + DocumentJSON string `json:"document_json"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type PoliciesCollection struct { + Policies map[string]credential.PolicyDocument `json:"policies"` +} + +type PoliciesData struct { + Username string `json:"username"` + Policies []IAMPolicy `json:"policies"` + TotalPolicies int `json:"total_policies"` + LastUpdated time.Time `json:"last_updated"` +} + +// Policy management request structures +type CreatePolicyRequest struct { + Name string `json:"name" binding:"required"` + Document credential.PolicyDocument `json:"document" binding:"required"` + DocumentJSON string `json:"document_json"` +} + +type UpdatePolicyRequest struct { + Document credential.PolicyDocument `json:"document" binding:"required"` + DocumentJSON string `json:"document_json"` +} + +// PolicyManager interface is now in the credential package + +// CredentialStorePolicyManager implements credential.PolicyManager by delegating to the credential store +type CredentialStorePolicyManager struct { + credentialManager *credential.CredentialManager +} + +// NewCredentialStorePolicyManager creates a new CredentialStorePolicyManager +func NewCredentialStorePolicyManager(credentialManager *credential.CredentialManager) *CredentialStorePolicyManager { + return &CredentialStorePolicyManager{ + credentialManager: credentialManager, + } +} + +// GetPolicies retrieves all IAM policies via credential store +func (cspm *CredentialStorePolicyManager) GetPolicies(ctx context.Context) (map[string]credential.PolicyDocument, error) { + // Get policies from credential store + // We'll use the credential store to access the filer indirectly + // Since policies are stored separately, we need to access the underlying store + store := cspm.credentialManager.GetStore() + glog.V(1).Infof("Getting policies from credential store: %T", store) + + // Check if the store supports policy management + if policyStore, ok := store.(credential.PolicyManager); ok { + glog.V(1).Infof("Store supports policy management, calling GetPolicies") + policies, err := policyStore.GetPolicies(ctx) + if err != nil { + glog.Errorf("Error getting policies from store: %v", err) + return nil, err + } + glog.V(1).Infof("Got %d policies from store", len(policies)) + return policies, nil + } else { + // Fallback: use empty policies for stores that don't support policies + glog.V(1).Infof("Credential store doesn't support policy management, returning empty policies") + return make(map[string]credential.PolicyDocument), nil + } +} + +// CreatePolicy creates a new IAM policy via credential store +func (cspm *CredentialStorePolicyManager) CreatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { + store := cspm.credentialManager.GetStore() + + if policyStore, ok := store.(credential.PolicyManager); ok { + return policyStore.CreatePolicy(ctx, name, document) + } + + return fmt.Errorf("credential store doesn't support policy creation") +} + +// UpdatePolicy updates an existing IAM policy via credential store +func (cspm *CredentialStorePolicyManager) UpdatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { + store := cspm.credentialManager.GetStore() + + if policyStore, ok := store.(credential.PolicyManager); ok { + return policyStore.UpdatePolicy(ctx, name, document) + } + + return fmt.Errorf("credential store doesn't support policy updates") +} + +// DeletePolicy deletes an IAM policy via credential store +func (cspm *CredentialStorePolicyManager) DeletePolicy(ctx context.Context, name string) error { + store := cspm.credentialManager.GetStore() + + if policyStore, ok := store.(credential.PolicyManager); ok { + return policyStore.DeletePolicy(ctx, name) + } + + return fmt.Errorf("credential store doesn't support policy deletion") +} + +// GetPolicy retrieves a specific IAM policy via credential store +func (cspm *CredentialStorePolicyManager) GetPolicy(ctx context.Context, name string) (*credential.PolicyDocument, error) { + store := cspm.credentialManager.GetStore() + + if policyStore, ok := store.(credential.PolicyManager); ok { + return policyStore.GetPolicy(ctx, name) + } + + return nil, fmt.Errorf("credential store doesn't support policy retrieval") +} + +// AdminServer policy management methods using credential.PolicyManager +func (s *AdminServer) GetPolicyManager() credential.PolicyManager { + if s.credentialManager == nil { + glog.V(1).Infof("Credential manager is nil, policy management not available") + return nil + } + glog.V(1).Infof("Credential manager available, creating CredentialStorePolicyManager") + return NewCredentialStorePolicyManager(s.credentialManager) +} + +// GetPolicies retrieves all IAM policies +func (s *AdminServer) GetPolicies() ([]IAMPolicy, error) { + policyManager := s.GetPolicyManager() + if policyManager == nil { + return nil, fmt.Errorf("policy manager not available") + } + + ctx := context.Background() + policyMap, err := policyManager.GetPolicies(ctx) + if err != nil { + return nil, err + } + + // Convert map[string]PolicyDocument to []IAMPolicy + var policies []IAMPolicy + for name, doc := range policyMap { + policy := IAMPolicy{ + Name: name, + Document: doc, + DocumentJSON: "", // Will be populated if needed + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + policies = append(policies, policy) + } + + return policies, nil +} + +// CreatePolicy creates a new IAM policy +func (s *AdminServer) CreatePolicy(name string, document credential.PolicyDocument) error { + policyManager := s.GetPolicyManager() + if policyManager == nil { + return fmt.Errorf("policy manager not available") + } + + ctx := context.Background() + return policyManager.CreatePolicy(ctx, name, document) +} + +// UpdatePolicy updates an existing IAM policy +func (s *AdminServer) UpdatePolicy(name string, document credential.PolicyDocument) error { + policyManager := s.GetPolicyManager() + if policyManager == nil { + return fmt.Errorf("policy manager not available") + } + + ctx := context.Background() + return policyManager.UpdatePolicy(ctx, name, document) +} + +// DeletePolicy deletes an IAM policy +func (s *AdminServer) DeletePolicy(name string) error { + policyManager := s.GetPolicyManager() + if policyManager == nil { + return fmt.Errorf("policy manager not available") + } + + ctx := context.Background() + return policyManager.DeletePolicy(ctx, name) +} + +// GetPolicy retrieves a specific IAM policy +func (s *AdminServer) GetPolicy(name string) (*IAMPolicy, error) { + policyManager := s.GetPolicyManager() + if policyManager == nil { + return nil, fmt.Errorf("policy manager not available") + } + + ctx := context.Background() + policyDoc, err := policyManager.GetPolicy(ctx, name) + if err != nil { + return nil, err + } + + if policyDoc == nil { + return nil, nil + } + + // Convert PolicyDocument to IAMPolicy + policy := &IAMPolicy{ + Name: name, + Document: *policyDoc, + DocumentJSON: "", // Will be populated if needed + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return policy, nil +} diff --git a/weed/admin/handlers/admin_handlers.go b/weed/admin/handlers/admin_handlers.go index dc7905bc1..76a123a4f 100644 --- a/weed/admin/handlers/admin_handlers.go +++ b/weed/admin/handlers/admin_handlers.go @@ -17,6 +17,7 @@ type AdminHandlers struct { clusterHandlers *ClusterHandlers fileBrowserHandlers *FileBrowserHandlers userHandlers *UserHandlers + policyHandlers *PolicyHandlers maintenanceHandlers *MaintenanceHandlers mqHandlers *MessageQueueHandlers } @@ -27,6 +28,7 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers { clusterHandlers := NewClusterHandlers(adminServer) fileBrowserHandlers := NewFileBrowserHandlers(adminServer) userHandlers := NewUserHandlers(adminServer) + policyHandlers := NewPolicyHandlers(adminServer) maintenanceHandlers := NewMaintenanceHandlers(adminServer) mqHandlers := NewMessageQueueHandlers(adminServer) return &AdminHandlers{ @@ -35,6 +37,7 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers { clusterHandlers: clusterHandlers, fileBrowserHandlers: fileBrowserHandlers, userHandlers: userHandlers, + policyHandlers: policyHandlers, maintenanceHandlers: maintenanceHandlers, mqHandlers: mqHandlers, } @@ -63,6 +66,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, protected.GET("/object-store/buckets", h.ShowS3Buckets) protected.GET("/object-store/buckets/:bucket", h.ShowBucketDetails) protected.GET("/object-store/users", h.userHandlers.ShowObjectStoreUsers) + protected.GET("/object-store/policies", h.policyHandlers.ShowPolicies) // File browser routes protected.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) @@ -121,6 +125,17 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies) } + // Object Store Policy management API routes + objectStorePoliciesApi := api.Group("/object-store/policies") + { + objectStorePoliciesApi.GET("", h.policyHandlers.GetPolicies) + objectStorePoliciesApi.POST("", h.policyHandlers.CreatePolicy) + objectStorePoliciesApi.GET("/:name", h.policyHandlers.GetPolicy) + objectStorePoliciesApi.PUT("/:name", h.policyHandlers.UpdatePolicy) + objectStorePoliciesApi.DELETE("/:name", h.policyHandlers.DeletePolicy) + objectStorePoliciesApi.POST("/validate", h.policyHandlers.ValidatePolicy) + } + // File management API routes filesApi := api.Group("/files") { @@ -171,6 +186,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, r.GET("/object-store/buckets", h.ShowS3Buckets) r.GET("/object-store/buckets/:bucket", h.ShowBucketDetails) r.GET("/object-store/users", h.userHandlers.ShowObjectStoreUsers) + r.GET("/object-store/policies", h.policyHandlers.ShowPolicies) // File browser routes r.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) @@ -229,6 +245,17 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies) } + // Object Store Policy management API routes + objectStorePoliciesApi := api.Group("/object-store/policies") + { + objectStorePoliciesApi.GET("", h.policyHandlers.GetPolicies) + objectStorePoliciesApi.POST("", h.policyHandlers.CreatePolicy) + objectStorePoliciesApi.GET("/:name", h.policyHandlers.GetPolicy) + objectStorePoliciesApi.PUT("/:name", h.policyHandlers.UpdatePolicy) + objectStorePoliciesApi.DELETE("/:name", h.policyHandlers.DeletePolicy) + objectStorePoliciesApi.POST("/validate", h.policyHandlers.ValidatePolicy) + } + // File management API routes filesApi := api.Group("/files") { diff --git a/weed/admin/handlers/file_browser_handlers.go b/weed/admin/handlers/file_browser_handlers.go index 97621192e..c8e117041 100644 --- a/weed/admin/handlers/file_browser_handlers.go +++ b/weed/admin/handlers/file_browser_handlers.go @@ -8,6 +8,7 @@ import ( "mime/multipart" "net" "net/http" + "os" "path/filepath" "strconv" "strings" @@ -190,7 +191,7 @@ func (h *FileBrowserHandlers) CreateFolder(c *gin.Context) { Name: filepath.Base(fullPath), IsDirectory: true, Attributes: &filer_pb.FuseAttributes{ - FileMode: uint32(0755 | (1 << 31)), // Directory mode + FileMode: uint32(0755 | os.ModeDir), // Directory mode Uid: filer_pb.OS_UID, Gid: filer_pb.OS_GID, Crtime: time.Now().Unix(), @@ -656,8 +657,9 @@ func (h *FileBrowserHandlers) GetFileProperties(c *gin.Context) { properties["created_timestamp"] = entry.Attributes.Crtime } - properties["file_mode"] = fmt.Sprintf("%o", entry.Attributes.FileMode) - properties["file_mode_formatted"] = h.formatFileMode(entry.Attributes.FileMode) + properties["file_mode"] = dash.FormatFileMode(entry.Attributes.FileMode) + properties["file_mode_formatted"] = dash.FormatFileMode(entry.Attributes.FileMode) + properties["file_mode_octal"] = fmt.Sprintf("%o", entry.Attributes.FileMode) properties["uid"] = entry.Attributes.Uid properties["gid"] = entry.Attributes.Gid properties["ttl_seconds"] = entry.Attributes.TtlSec @@ -725,13 +727,6 @@ func (h *FileBrowserHandlers) formatBytes(bytes int64) string { return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) } -// Helper function to format file mode -func (h *FileBrowserHandlers) formatFileMode(mode uint32) string { - // Convert to octal and format as rwx permissions - perm := mode & 0777 - return fmt.Sprintf("%03o", perm) -} - // Helper function to determine MIME type from filename func (h *FileBrowserHandlers) determineMimeType(filename string) string { ext := strings.ToLower(filepath.Ext(filename)) diff --git a/weed/admin/handlers/maintenance_handlers.go b/weed/admin/handlers/maintenance_handlers.go index 954874c14..4b1f91387 100644 --- a/weed/admin/handlers/maintenance_handlers.go +++ b/weed/admin/handlers/maintenance_handlers.go @@ -11,9 +11,6 @@ import ( "github.com/seaweedfs/seaweedfs/weed/admin/view/components" "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" "github.com/seaweedfs/seaweedfs/weed/worker/tasks" - "github.com/seaweedfs/seaweedfs/weed/worker/tasks/balance" - "github.com/seaweedfs/seaweedfs/weed/worker/tasks/erasure_coding" - "github.com/seaweedfs/seaweedfs/weed/worker/tasks/vacuum" "github.com/seaweedfs/seaweedfs/weed/worker/types" ) @@ -114,59 +111,60 @@ func (h *MaintenanceHandlers) ShowTaskConfig(c *gin.Context) { return } - // Try to get templ UI provider first - templUIProvider := getTemplUIProvider(taskType) + // Try to get templ UI provider first - temporarily disabled + // templUIProvider := getTemplUIProvider(taskType) var configSections []components.ConfigSectionData - if templUIProvider != nil { - // Use the new templ-based UI provider - currentConfig := templUIProvider.GetCurrentConfig() - sections, err := templUIProvider.RenderConfigSections(currentConfig) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render configuration sections: " + err.Error()}) - return - } - configSections = sections - } else { - // Fallback to basic configuration for providers that haven't been migrated yet - configSections = []components.ConfigSectionData{ - { - Title: "Configuration Settings", - Icon: "fas fa-cogs", - Description: "Configure task detection and scheduling parameters", - Fields: []interface{}{ - components.CheckboxFieldData{ - FormFieldData: components.FormFieldData{ - Name: "enabled", - Label: "Enable Task", - Description: "Whether this task type should be enabled", - }, - Checked: true, + // Temporarily disabled templ UI provider + // if templUIProvider != nil { + // // Use the new templ-based UI provider + // currentConfig := templUIProvider.GetCurrentConfig() + // sections, err := templUIProvider.RenderConfigSections(currentConfig) + // if err != nil { + // c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render configuration sections: " + err.Error()}) + // return + // } + // configSections = sections + // } else { + // Fallback to basic configuration for providers that haven't been migrated yet + configSections = []components.ConfigSectionData{ + { + Title: "Configuration Settings", + Icon: "fas fa-cogs", + Description: "Configure task detection and scheduling parameters", + Fields: []interface{}{ + components.CheckboxFieldData{ + FormFieldData: components.FormFieldData{ + Name: "enabled", + Label: "Enable Task", + Description: "Whether this task type should be enabled", }, - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "max_concurrent", - Label: "Max Concurrent Tasks", - Description: "Maximum number of concurrent tasks", - Required: true, - }, - Value: 2, - Step: "1", - Min: floatPtr(1), + Checked: true, + }, + components.NumberFieldData{ + FormFieldData: components.FormFieldData{ + Name: "max_concurrent", + Label: "Max Concurrent Tasks", + Description: "Maximum number of concurrent tasks", + Required: true, }, - components.DurationFieldData{ - FormFieldData: components.FormFieldData{ - Name: "scan_interval", - Label: "Scan Interval", - Description: "How often to scan for tasks", - Required: true, - }, - Value: "30m", + Value: 2, + Step: "1", + Min: floatPtr(1), + }, + components.DurationFieldData{ + FormFieldData: components.FormFieldData{ + Name: "scan_interval", + Label: "Scan Interval", + Description: "How often to scan for tasks", + Required: true, }, + Value: "30m", }, }, - } + }, } + // } // End of disabled templ UI provider else block // Create task configuration data using templ components configData := &app.TaskConfigTemplData{ @@ -199,8 +197,8 @@ func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) { return } - // Try to get templ UI provider first - templUIProvider := getTemplUIProvider(taskType) + // Try to get templ UI provider first - temporarily disabled + // templUIProvider := getTemplUIProvider(taskType) // Parse form data err := c.Request.ParseForm() @@ -217,53 +215,54 @@ func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) { var config interface{} - if templUIProvider != nil { - // Use the new templ-based UI provider - config, err = templUIProvider.ParseConfigForm(formData) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse configuration: " + err.Error()}) - return - } + // Temporarily disabled templ UI provider + // if templUIProvider != nil { + // // Use the new templ-based UI provider + // config, err = templUIProvider.ParseConfigForm(formData) + // if err != nil { + // c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse configuration: " + err.Error()}) + // return + // } + // // Apply configuration using templ provider + // err = templUIProvider.ApplyConfig(config) + // if err != nil { + // c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()}) + // return + // } + // } else { + // Fallback to old UI provider for tasks that haven't been migrated yet + // Fallback to old UI provider for tasks that haven't been migrated yet + uiRegistry := tasks.GetGlobalUIRegistry() + typesRegistry := tasks.GetGlobalTypesRegistry() - // Apply configuration using templ provider - err = templUIProvider.ApplyConfig(config) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()}) - return - } - } else { - // Fallback to old UI provider for tasks that haven't been migrated yet - uiRegistry := tasks.GetGlobalUIRegistry() - typesRegistry := tasks.GetGlobalTypesRegistry() - - var provider types.TaskUIProvider - for workerTaskType := range typesRegistry.GetAllDetectors() { - if string(workerTaskType) == string(taskType) { - provider = uiRegistry.GetProvider(workerTaskType) - break - } - } - - if provider == nil { - c.JSON(http.StatusNotFound, gin.H{"error": "UI provider not found for task type"}) - return - } - - // Parse configuration from form using old provider - config, err = provider.ParseConfigForm(formData) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse configuration: " + err.Error()}) - return - } - - // Apply configuration using old provider - err = provider.ApplyConfig(config) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()}) - return + var provider types.TaskUIProvider + for workerTaskType := range typesRegistry.GetAllDetectors() { + if string(workerTaskType) == string(taskType) { + provider = uiRegistry.GetProvider(workerTaskType) + break } } + if provider == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "UI provider not found for task type"}) + return + } + + // Parse configuration from form using old provider + config, err = provider.ParseConfigForm(formData) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse configuration: " + err.Error()}) + return + } + + // Apply configuration using old provider + err = provider.ApplyConfig(config) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()}) + return + } + // } // End of disabled templ UI provider else block + // Redirect back to task configuration page c.Redirect(http.StatusSeeOther, "/maintenance/config/"+taskTypeName) } @@ -350,39 +349,35 @@ func floatPtr(f float64) *float64 { return &f } -// Global templ UI registry -var globalTemplUIRegistry *types.UITemplRegistry +// Global templ UI registry - temporarily disabled +// var globalTemplUIRegistry *types.UITemplRegistry -// initTemplUIRegistry initializes the global templ UI registry +// initTemplUIRegistry initializes the global templ UI registry - temporarily disabled func initTemplUIRegistry() { - if globalTemplUIRegistry == nil { - globalTemplUIRegistry = types.NewUITemplRegistry() - - // Register vacuum templ UI provider using shared instances - vacuumDetector, vacuumScheduler := vacuum.GetSharedInstances() - vacuum.RegisterUITempl(globalTemplUIRegistry, vacuumDetector, vacuumScheduler) - - // Register erasure coding templ UI provider using shared instances - erasureCodingDetector, erasureCodingScheduler := erasure_coding.GetSharedInstances() - erasure_coding.RegisterUITempl(globalTemplUIRegistry, erasureCodingDetector, erasureCodingScheduler) - - // Register balance templ UI provider using shared instances - balanceDetector, balanceScheduler := balance.GetSharedInstances() - balance.RegisterUITempl(globalTemplUIRegistry, balanceDetector, balanceScheduler) - } + // Temporarily disabled due to missing types + // if globalTemplUIRegistry == nil { + // globalTemplUIRegistry = types.NewUITemplRegistry() + // // Register vacuum templ UI provider using shared instances + // vacuumDetector, vacuumScheduler := vacuum.GetSharedInstances() + // vacuum.RegisterUITempl(globalTemplUIRegistry, vacuumDetector, vacuumScheduler) + // // Register erasure coding templ UI provider using shared instances + // erasureCodingDetector, erasureCodingScheduler := erasure_coding.GetSharedInstances() + // erasure_coding.RegisterUITempl(globalTemplUIRegistry, erasureCodingDetector, erasureCodingScheduler) + // // Register balance templ UI provider using shared instances + // balanceDetector, balanceScheduler := balance.GetSharedInstances() + // balance.RegisterUITempl(globalTemplUIRegistry, balanceDetector, balanceScheduler) + // } } -// getTemplUIProvider gets the templ UI provider for a task type -func getTemplUIProvider(taskType maintenance.MaintenanceTaskType) types.TaskUITemplProvider { - initTemplUIRegistry() - +// getTemplUIProvider gets the templ UI provider for a task type - temporarily disabled +func getTemplUIProvider(taskType maintenance.MaintenanceTaskType) interface{} { + // initTemplUIRegistry() // Convert maintenance task type to worker task type - typesRegistry := tasks.GetGlobalTypesRegistry() - for workerTaskType := range typesRegistry.GetAllDetectors() { - if string(workerTaskType) == string(taskType) { - return globalTemplUIRegistry.GetProvider(workerTaskType) - } - } - + // typesRegistry := tasks.GetGlobalTypesRegistry() + // for workerTaskType := range typesRegistry.GetAllDetectors() { + // if string(workerTaskType) == string(taskType) { + // return globalTemplUIRegistry.GetProvider(workerTaskType) + // } + // } return nil } diff --git a/weed/admin/handlers/policy_handlers.go b/weed/admin/handlers/policy_handlers.go new file mode 100644 index 000000000..8f5cc91b1 --- /dev/null +++ b/weed/admin/handlers/policy_handlers.go @@ -0,0 +1,273 @@ +package handlers + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "github.com/seaweedfs/seaweedfs/weed/admin/view/app" + "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" + "github.com/seaweedfs/seaweedfs/weed/credential" + "github.com/seaweedfs/seaweedfs/weed/glog" +) + +// PolicyHandlers contains all the HTTP handlers for policy management +type PolicyHandlers struct { + adminServer *dash.AdminServer +} + +// NewPolicyHandlers creates a new instance of PolicyHandlers +func NewPolicyHandlers(adminServer *dash.AdminServer) *PolicyHandlers { + return &PolicyHandlers{ + adminServer: adminServer, + } +} + +// ShowPolicies renders the policies management page +func (h *PolicyHandlers) ShowPolicies(c *gin.Context) { + // Get policies data from the server + policiesData := h.getPoliciesData(c) + + // Render HTML template + c.Header("Content-Type", "text/html") + policiesComponent := app.Policies(policiesData) + layoutComponent := layout.Layout(c, policiesComponent) + err := layoutComponent.Render(c.Request.Context(), c.Writer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) + return + } +} + +// GetPolicies returns the list of policies as JSON +func (h *PolicyHandlers) GetPolicies(c *gin.Context) { + policies, err := h.adminServer.GetPolicies() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get policies: " + err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"policies": policies}) +} + +// CreatePolicy handles policy creation +func (h *PolicyHandlers) CreatePolicy(c *gin.Context) { + var req dash.CreatePolicyRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Validate policy name + if req.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"}) + return + } + + // Check if policy already exists + existingPolicy, err := h.adminServer.GetPolicy(req.Name) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing policy: " + err.Error()}) + return + } + if existingPolicy != nil { + c.JSON(http.StatusConflict, gin.H{"error": "Policy with this name already exists"}) + return + } + + // Create the policy + err = h.adminServer.CreatePolicy(req.Name, req.Document) + if err != nil { + glog.Errorf("Failed to create policy %s: %v", req.Name, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create policy: " + err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "success": true, + "message": "Policy created successfully", + "policy": req.Name, + }) +} + +// GetPolicy returns a specific policy +func (h *PolicyHandlers) GetPolicy(c *gin.Context) { + policyName := c.Param("name") + if policyName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"}) + return + } + + policy, err := h.adminServer.GetPolicy(policyName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get policy: " + err.Error()}) + return + } + + if policy == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"}) + return + } + + c.JSON(http.StatusOK, policy) +} + +// UpdatePolicy handles policy updates +func (h *PolicyHandlers) UpdatePolicy(c *gin.Context) { + policyName := c.Param("name") + if policyName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"}) + return + } + + var req dash.UpdatePolicyRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Check if policy exists + existingPolicy, err := h.adminServer.GetPolicy(policyName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing policy: " + err.Error()}) + return + } + if existingPolicy == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"}) + return + } + + // Update the policy + err = h.adminServer.UpdatePolicy(policyName, req.Document) + if err != nil { + glog.Errorf("Failed to update policy %s: %v", policyName, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update policy: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Policy updated successfully", + "policy": policyName, + }) +} + +// DeletePolicy handles policy deletion +func (h *PolicyHandlers) DeletePolicy(c *gin.Context) { + policyName := c.Param("name") + if policyName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"}) + return + } + + // Check if policy exists + existingPolicy, err := h.adminServer.GetPolicy(policyName) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing policy: " + err.Error()}) + return + } + if existingPolicy == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"}) + return + } + + // Delete the policy + err = h.adminServer.DeletePolicy(policyName) + if err != nil { + glog.Errorf("Failed to delete policy %s: %v", policyName, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete policy: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Policy deleted successfully", + "policy": policyName, + }) +} + +// ValidatePolicy validates a policy document without saving it +func (h *PolicyHandlers) ValidatePolicy(c *gin.Context) { + var req struct { + Document credential.PolicyDocument `json:"document" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) + return + } + + // Basic validation + if req.Document.Version == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Policy version is required"}) + return + } + + if len(req.Document.Statement) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Policy must have at least one statement"}) + return + } + + // Validate each statement + for i, statement := range req.Document.Statement { + if statement.Effect != "Allow" && statement.Effect != "Deny" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Statement %d: Effect must be 'Allow' or 'Deny'", i+1), + }) + return + } + + if len(statement.Action) == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Statement %d: Action is required", i+1), + }) + return + } + + if len(statement.Resource) == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Statement %d: Resource is required", i+1), + }) + return + } + } + + c.JSON(http.StatusOK, gin.H{ + "valid": true, + "message": "Policy document is valid", + }) +} + +// getPoliciesData retrieves policies data from the server +func (h *PolicyHandlers) getPoliciesData(c *gin.Context) dash.PoliciesData { + username := c.GetString("username") + if username == "" { + username = "admin" + } + + // Get policies + policies, err := h.adminServer.GetPolicies() + if err != nil { + glog.Errorf("Failed to get policies: %v", err) + // Return empty data on error + return dash.PoliciesData{ + Username: username, + Policies: []dash.IAMPolicy{}, + TotalPolicies: 0, + LastUpdated: time.Now(), + } + } + + // Ensure policies is never nil + if policies == nil { + policies = []dash.IAMPolicy{} + } + + return dash.PoliciesData{ + Username: username, + Policies: policies, + TotalPolicies: len(policies), + LastUpdated: time.Now(), + } +} diff --git a/weed/admin/view/app/cluster_collections.templ b/weed/admin/view/app/cluster_collections.templ index 2bd21a3ca..9099fe112 100644 --- a/weed/admin/view/app/cluster_collections.templ +++ b/weed/admin/view/app/cluster_collections.templ @@ -164,22 +164,18 @@ templ ClusterCollections(data dash.ClusterCollectionsData) { } -
- - - -
+ } @@ -209,30 +205,169 @@ templ ClusterCollections(data dash.ClusterCollectionsData) { - - + + + + } func getDiskTypeColor(diskType string) string { diff --git a/weed/admin/view/app/cluster_collections_templ.go b/weed/admin/view/app/cluster_collections_templ.go index 8c675695a..58384c462 100644 --- a/weed/admin/view/app/cluster_collections_templ.go +++ b/weed/admin/view/app/cluster_collections_templ.go @@ -231,48 +231,113 @@ func ClusterCollections(data dash.ClusterCollectionsData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" data-datacenter=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(collection.DataCenter) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 172, Col: 90} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" data-volume-count=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", collection.VolumeCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 173, Col: 112} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" data-file-count=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", collection.FileCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 174, Col: 108} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" data-total-size=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", collection.TotalSize)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 175, Col: 108} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" data-disk-types=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(formatDiskTypes(collection.DiskTypes)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 176, Col: 106} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
No Collections Found

No collections are currently configured in the cluster.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
No Collections Found

No collections are currently configured in the cluster.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
Last updated: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
Last updated: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 204, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 200, Col: 81} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
Delete Collection

Are you sure you want to delete the collection ?

This action cannot be undone. All volumes in this collection will be affected.
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/cluster_filers.templ b/weed/admin/view/app/cluster_filers.templ index 6ed14ac6e..023fd4478 100644 --- a/weed/admin/view/app/cluster_filers.templ +++ b/weed/admin/view/app/cluster_filers.templ @@ -121,6 +121,62 @@ templ ClusterFilers(data dash.ClusterFilersData) { + + + } \ No newline at end of file diff --git a/weed/admin/view/app/cluster_filers_templ.go b/weed/admin/view/app/cluster_filers_templ.go index ecc2d873e..69c489ce4 100644 --- a/weed/admin/view/app/cluster_filers_templ.go +++ b/weed/admin/view/app/cluster_filers_templ.go @@ -183,7 +183,7 @@ func ClusterFilers(data dash.ClusterFilersData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/cluster_masters.templ b/weed/admin/view/app/cluster_masters.templ index 9f6e2d0a9..6a53c5493 100644 --- a/weed/admin/view/app/cluster_masters.templ +++ b/weed/admin/view/app/cluster_masters.templ @@ -136,14 +136,15 @@ templ ClusterMasters(data dash.ClusterMastersData) { } -
- - -
+ } @@ -170,6 +171,112 @@ templ ClusterMasters(data dash.ClusterMastersData) { + + + } \ No newline at end of file diff --git a/weed/admin/view/app/cluster_masters_templ.go b/weed/admin/view/app/cluster_masters_templ.go index 951db551e..e0be75cc4 100644 --- a/weed/admin/view/app/cluster_masters_templ.go +++ b/weed/admin/view/app/cluster_masters_templ.go @@ -154,35 +154,74 @@ func ClusterMasters(data dash.ClusterMastersData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
No Masters Found

No master servers are currently available in the cluster.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
No Masters Found

No master servers are currently available in the cluster.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
Last updated: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
Last updated: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 168, Col: 67} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 169, Col: 67} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/cluster_volume_servers.templ b/weed/admin/view/app/cluster_volume_servers.templ index 20c661d40..f6b737a57 100644 --- a/weed/admin/view/app/cluster_volume_servers.templ +++ b/weed/admin/view/app/cluster_volume_servers.templ @@ -148,16 +148,22 @@ templ ClusterVolumeServers(data dash.ClusterVolumeServersData) { -
- - -
+ } @@ -184,6 +190,161 @@ templ ClusterVolumeServers(data dash.ClusterVolumeServersData) { + + + } \ No newline at end of file diff --git a/weed/admin/view/app/cluster_volume_servers_templ.go b/weed/admin/view/app/cluster_volume_servers_templ.go index 1bd439974..094774c7a 100644 --- a/weed/admin/view/app/cluster_volume_servers_templ.go +++ b/weed/admin/view/app/cluster_volume_servers_templ.go @@ -213,35 +213,165 @@ func ClusterVolumeServers(data dash.ClusterVolumeServersData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
No Volume Servers Found

No volume servers are currently available in the cluster.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
No Volume Servers Found

No volume servers are currently available in the cluster.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
Last updated: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
Last updated: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 182, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 188, Col: 81} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/file_browser.templ b/weed/admin/view/app/file_browser.templ index a1e00555f..83db7df0f 100644 --- a/weed/admin/view/app/file_browser.templ +++ b/weed/admin/view/app/file_browser.templ @@ -228,7 +228,7 @@ templ FileBrowser(data dash.FileBrowserData) { } - { entry.Mode } + { entry.Mode }
@@ -356,6 +356,380 @@ templ FileBrowser(data dash.FileBrowserData) {
+ + + } func countDirectories(entries []dash.FileEntry) int { diff --git a/weed/admin/view/app/file_browser_templ.go b/weed/admin/view/app/file_browser_templ.go index c4367e82d..ca1db51b2 100644 --- a/weed/admin/view/app/file_browser_templ.go +++ b/weed/admin/view/app/file_browser_templ.go @@ -392,136 +392,162 @@ func FileBrowser(data dash.FileBrowserData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" data-is-directory=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%t", entry.IsDirectory)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 231, Col: 131} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Mode) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 231, Col: 146} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if !entry.IsDirectory { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
Empty Directory

This directory contains no files or subdirectories.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
Empty Directory

This directory contains no files or subdirectories.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
Last updated: ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 271, Col: 66} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
Create New Folder
Folder names cannot contain / or \\ characters.
Upload Files
Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking.
Last updated: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentPath) + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 328, Col: 79} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 271, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">
0%
Preparing upload...
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
Create New Folder
Folder names cannot contain / or \\ characters.
Upload Files
Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking.
0%
Preparing upload...
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/object_store_users.templ b/weed/admin/view/app/object_store_users.templ index 7c457a3d8..dedd258e2 100644 --- a/weed/admin/view/app/object_store_users.templ +++ b/weed/admin/view/app/object_store_users.templ @@ -317,7 +317,355 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { } diff --git a/weed/admin/view/app/object_store_users_templ.go b/weed/admin/view/app/object_store_users_templ.go index a4a194d59..8d08d5161 100644 --- a/weed/admin/view/app/object_store_users_templ.go +++ b/weed/admin/view/app/object_store_users_templ.go @@ -193,7 +193,7 @@ func ObjectStoreUsers(data dash.ObjectStoreUsersData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Create New User
Hold Ctrl/Cmd to select multiple permissions
Edit User
User Details
Manage Access Keys
Access Keys for
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Create New User
Hold Ctrl/Cmd to select multiple permissions
Edit User
User Details
Manage Access Keys
Access Keys for
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/policies.templ b/weed/admin/view/app/policies.templ new file mode 100644 index 000000000..e613d535e --- /dev/null +++ b/weed/admin/view/app/policies.templ @@ -0,0 +1,658 @@ +package app + +import ( + "fmt" + "github.com/seaweedfs/seaweedfs/weed/admin/dash" +) + +templ Policies(data dash.PoliciesData) { +
+

+ IAM Policies +

+
+
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+ Total Policies +
+
+ {fmt.Sprintf("%d", data.TotalPolicies)} +
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ Active Policies +
+
+ {fmt.Sprintf("%d", data.TotalPolicies)} +
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ Last Updated +
+
+ {data.LastUpdated.Format("15:04")} +
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
+ IAM Policies +
+ +
+
+
+ + + + + + + + + + + + + for _, policy := range data.Policies { + + + + + + + + + } + if len(data.Policies) == 0 { + + + + } + +
Policy NameVersionStatementsCreatedUpdatedActions
+ {policy.Name} + + {policy.Document.Version} + + {fmt.Sprintf("%d statements", len(policy.Document.Statement))} + + {policy.CreatedAt.Format("2006-01-02 15:04")} + + {policy.UpdatedAt.Format("2006-01-02 15:04")} + +
+ + + +
+
+ +
+
No IAM policies found
+

Create your first policy to manage access permissions.

+ +
+
+
+
+
+
+
+
+ + + + + + + + + + + + +} \ No newline at end of file diff --git a/weed/admin/view/app/policies_templ.go b/weed/admin/view/app/policies_templ.go new file mode 100644 index 000000000..2e005fb58 --- /dev/null +++ b/weed/admin/view/app/policies_templ.go @@ -0,0 +1,204 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.906 +package app + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + "github.com/seaweedfs/seaweedfs/weed/admin/dash" +) + +func Policies(data dash.PoliciesData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

IAM Policies

Total Policies
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalPolicies)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/policies.templ`, Line: 34, Col: 74} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
Active Policies
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalPolicies)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/policies.templ`, Line: 54, Col: 74} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
Last Updated
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/policies.templ`, Line: 74, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
IAM Policies
Actions:
Export List
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, policy := range data.Policies { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Policies) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Policy NameVersionStatementsCreatedUpdatedActions
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(policy.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/policies.templ`, Line: 123, Col: 68} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(policy.Document.Version) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/policies.templ`, Line: 126, Col: 100} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d statements", len(policy.Document.Statement))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/policies.templ`, Line: 129, Col: 142} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(policy.CreatedAt.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/policies.templ`, Line: 132, Col: 118} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(policy.UpdatedAt.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/policies.templ`, Line: 135, Col: 118} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
No IAM policies found

Create your first policy to manage access permissions.

Create IAM Policy
Enter a unique name for this policy (alphanumeric and underscores only)
Enter the policy document in AWS IAM JSON format
View IAM Policy
Loading...

Loading policy...

Edit IAM Policy
Policy name cannot be changed
Edit the policy document in AWS IAM JSON format
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/weed/admin/view/app/s3_buckets.templ b/weed/admin/view/app/s3_buckets.templ index d6625f5e8..1afafb294 100644 --- a/weed/admin/view/app/s3_buckets.templ +++ b/weed/admin/view/app/s3_buckets.templ @@ -187,11 +187,12 @@ templ S3Buckets(data dash.S3BucketsData) { title="Browse Files"> - + + + + + + + + ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
Create New S3 Bucket
Bucket names must be between 3 and 63 characters, contain only lowercase letters, numbers, dots, and hyphens.
Set the maximum storage size for this bucket.
Keep multiple versions of objects in this bucket.
Prevent objects from being deleted or overwritten for a specified period. Automatically enables versioning.
Governance allows override with special permissions, Compliance is immutable.
Default retention period for new objects (1-36500 days).
Delete Bucket

Are you sure you want to delete the bucket ?

Warning: This action cannot be undone. All objects in the bucket will be permanently deleted.
Manage Bucket Quota
Set the maximum storage size for this bucket. Set to 0 to remove quota.
Bucket Details
Loading...
Loading bucket details...
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/layout/layout.templ b/weed/admin/view/layout/layout.templ index 2261f1e41..b5e2cefbf 100644 --- a/weed/admin/view/layout/layout.templ +++ b/weed/admin/view/layout/layout.templ @@ -147,6 +147,11 @@ templ Layout(c *gin.Context, content templ.Component) { Users + diff --git a/weed/admin/view/layout/layout_templ.go b/weed/admin/view/layout/layout_templ.go index c321c7a6b..562faa677 100644 --- a/weed/admin/view/layout/layout_templ.go +++ b/weed/admin/view/layout/layout_templ.go @@ -62,7 +62,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
MAIN
MANAGEMENT
MAIN
MANAGEMENT
  • File Browser
  • Object Store
  • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -153,7 +153,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component { var templ_7745c5c3_Var3 templ.SafeURL templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(menuItem.URL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 248, Col: 117} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 253, Col: 117} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -188,7 +188,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(menuItem.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 249, Col: 109} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 254, Col: 109} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -206,7 +206,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component { var templ_7745c5c3_Var7 templ.SafeURL templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(menuItem.URL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 252, Col: 110} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 257, Col: 110} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -241,7 +241,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(menuItem.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 253, Col: 109} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 258, Col: 109} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -274,7 +274,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component { var templ_7745c5c3_Var11 templ.SafeURL templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(menuItem.URL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 265, Col: 106} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 270, Col: 106} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -309,7 +309,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(menuItem.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 266, Col: 105} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 271, Col: 105} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -370,7 +370,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component { var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", time.Now().Year())) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 313, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 318, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -383,7 +383,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component { var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(version.VERSION_NUMBER) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 313, Col: 102} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 318, Col: 102} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -435,7 +435,7 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 337, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 342, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -448,7 +448,7 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen var templ_7745c5c3_Var19 string templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 351, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 356, Col: 57} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { @@ -466,7 +466,7 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen var templ_7745c5c3_Var20 string templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 358, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 363, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { diff --git a/weed/credential/credential_store.go b/weed/credential/credential_store.go index cd36263dc..6fe5a5da1 100644 --- a/weed/credential/credential_store.go +++ b/weed/credential/credential_store.go @@ -86,5 +86,27 @@ type UserCredentials struct { UpdatedAt time.Time `json:"updatedAt"` } +// PolicyStatement represents a single policy statement in an IAM policy +type PolicyStatement struct { + Effect string `json:"Effect"` + Action []string `json:"Action"` + Resource []string `json:"Resource"` +} + +// PolicyDocument represents an IAM policy document +type PolicyDocument struct { + Version string `json:"Version"` + Statement []*PolicyStatement `json:"Statement"` +} + +// PolicyManager interface for managing IAM policies +type PolicyManager interface { + GetPolicies(ctx context.Context) (map[string]PolicyDocument, error) + CreatePolicy(ctx context.Context, name string, document PolicyDocument) error + UpdatePolicy(ctx context.Context, name string, document PolicyDocument) error + DeletePolicy(ctx context.Context, name string) error + GetPolicy(ctx context.Context, name string) (*PolicyDocument, error) +} + // Stores holds all available credential store implementations var Stores []CredentialStore diff --git a/weed/credential/filer_etc/filer_etc_identity.go b/weed/credential/filer_etc/filer_etc_identity.go new file mode 100644 index 000000000..103c988ff --- /dev/null +++ b/weed/credential/filer_etc/filer_etc_identity.go @@ -0,0 +1,188 @@ +package filer_etc + +import ( + "bytes" + "context" + "fmt" + + "github.com/seaweedfs/seaweedfs/weed/credential" + "github.com/seaweedfs/seaweedfs/weed/filer" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" +) + +func (store *FilerEtcStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { + s3cfg := &iam_pb.S3ApiConfiguration{} + + err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { + if err != filer_pb.ErrNotFound { + return err + } + } + if buf.Len() > 0 { + return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) + } + return nil + }) + + return s3cfg, err +} + +func (store *FilerEtcStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { + return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ProtoToText(&buf, config); err != nil { + return fmt.Errorf("failed to marshal configuration: %v", err) + } + return filer.SaveInsideFiler(client, filer.IamConfigDirectory, filer.IamIdentityFile, buf.Bytes()) + }) +} + +func (store *FilerEtcStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { + // Load existing configuration + config, err := store.LoadConfiguration(ctx) + if err != nil { + return fmt.Errorf("failed to load configuration: %v", err) + } + + // Check if user already exists + for _, existingIdentity := range config.Identities { + if existingIdentity.Name == identity.Name { + return credential.ErrUserAlreadyExists + } + } + + // Add new identity + config.Identities = append(config.Identities, identity) + + // Save configuration + return store.SaveConfiguration(ctx, config) +} + +func (store *FilerEtcStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { + config, err := store.LoadConfiguration(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load configuration: %v", err) + } + + for _, identity := range config.Identities { + if identity.Name == username { + return identity, nil + } + } + + return nil, credential.ErrUserNotFound +} + +func (store *FilerEtcStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { + config, err := store.LoadConfiguration(ctx) + if err != nil { + return fmt.Errorf("failed to load configuration: %v", err) + } + + // Find and update the user + for i, existingIdentity := range config.Identities { + if existingIdentity.Name == username { + config.Identities[i] = identity + return store.SaveConfiguration(ctx, config) + } + } + + return credential.ErrUserNotFound +} + +func (store *FilerEtcStore) DeleteUser(ctx context.Context, username string) error { + config, err := store.LoadConfiguration(ctx) + if err != nil { + return fmt.Errorf("failed to load configuration: %v", err) + } + + // Find and remove the user + for i, identity := range config.Identities { + if identity.Name == username { + config.Identities = append(config.Identities[:i], config.Identities[i+1:]...) + return store.SaveConfiguration(ctx, config) + } + } + + return credential.ErrUserNotFound +} + +func (store *FilerEtcStore) ListUsers(ctx context.Context) ([]string, error) { + config, err := store.LoadConfiguration(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load configuration: %v", err) + } + + var usernames []string + for _, identity := range config.Identities { + usernames = append(usernames, identity.Name) + } + + return usernames, nil +} + +func (store *FilerEtcStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { + config, err := store.LoadConfiguration(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load configuration: %v", err) + } + + for _, identity := range config.Identities { + for _, credential := range identity.Credentials { + if credential.AccessKey == accessKey { + return identity, nil + } + } + } + + return nil, credential.ErrAccessKeyNotFound +} + +func (store *FilerEtcStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { + config, err := store.LoadConfiguration(ctx) + if err != nil { + return fmt.Errorf("failed to load configuration: %v", err) + } + + // Find the user and add the credential + for _, identity := range config.Identities { + if identity.Name == username { + // Check if access key already exists + for _, existingCred := range identity.Credentials { + if existingCred.AccessKey == cred.AccessKey { + return fmt.Errorf("access key %s already exists", cred.AccessKey) + } + } + + identity.Credentials = append(identity.Credentials, cred) + return store.SaveConfiguration(ctx, config) + } + } + + return credential.ErrUserNotFound +} + +func (store *FilerEtcStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { + config, err := store.LoadConfiguration(ctx) + if err != nil { + return fmt.Errorf("failed to load configuration: %v", err) + } + + // Find the user and remove the credential + for _, identity := range config.Identities { + if identity.Name == username { + for i, cred := range identity.Credentials { + if cred.AccessKey == accessKey { + identity.Credentials = append(identity.Credentials[:i], identity.Credentials[i+1:]...) + return store.SaveConfiguration(ctx, config) + } + } + return credential.ErrAccessKeyNotFound + } + } + + return credential.ErrUserNotFound +} diff --git a/weed/credential/filer_etc/filer_etc_policy.go b/weed/credential/filer_etc/filer_etc_policy.go new file mode 100644 index 000000000..fdd3156ff --- /dev/null +++ b/weed/credential/filer_etc/filer_etc_policy.go @@ -0,0 +1,114 @@ +package filer_etc + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/seaweedfs/seaweedfs/weed/credential" + "github.com/seaweedfs/seaweedfs/weed/filer" + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" +) + +type PoliciesCollection struct { + Policies map[string]credential.PolicyDocument `json:"policies"` +} + +// GetPolicies retrieves all IAM policies from the filer +func (store *FilerEtcStore) GetPolicies(ctx context.Context) (map[string]credential.PolicyDocument, error) { + policiesCollection := &PoliciesCollection{ + Policies: make(map[string]credential.PolicyDocument), + } + + // Check if filer client is configured + if store.filerGrpcAddress == "" { + glog.V(1).Infof("Filer client not configured for policy retrieval, returning empty policies") + // Return empty policies if filer client is not configured + return policiesCollection.Policies, nil + } + + err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { + var buf bytes.Buffer + if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamPoliciesFile, &buf); err != nil { + if err == filer_pb.ErrNotFound { + glog.V(1).Infof("Policies file not found at %s/%s, returning empty policies", filer.IamConfigDirectory, filer.IamPoliciesFile) + // If file doesn't exist, return empty collection + return nil + } + return err + } + + if buf.Len() > 0 { + return json.Unmarshal(buf.Bytes(), policiesCollection) + } + return nil + }) + + if err != nil { + return nil, err + } + + return policiesCollection.Policies, nil +} + +// CreatePolicy creates a new IAM policy in the filer +func (store *FilerEtcStore) CreatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { + return store.updatePolicies(ctx, func(policies map[string]credential.PolicyDocument) { + policies[name] = document + }) +} + +// UpdatePolicy updates an existing IAM policy in the filer +func (store *FilerEtcStore) UpdatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { + return store.updatePolicies(ctx, func(policies map[string]credential.PolicyDocument) { + policies[name] = document + }) +} + +// DeletePolicy deletes an IAM policy from the filer +func (store *FilerEtcStore) DeletePolicy(ctx context.Context, name string) error { + return store.updatePolicies(ctx, func(policies map[string]credential.PolicyDocument) { + delete(policies, name) + }) +} + +// updatePolicies is a helper method to update policies atomically +func (store *FilerEtcStore) updatePolicies(ctx context.Context, updateFunc func(map[string]credential.PolicyDocument)) error { + // Load existing policies + policies, err := store.GetPolicies(ctx) + if err != nil { + return err + } + + // Apply update + updateFunc(policies) + + // Save back to filer + policiesCollection := &PoliciesCollection{ + Policies: policies, + } + + data, err := json.Marshal(policiesCollection) + if err != nil { + return err + } + + return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { + return filer.SaveInsideFiler(client, filer.IamConfigDirectory, filer.IamPoliciesFile, data) + }) +} + +// GetPolicy retrieves a specific IAM policy by name from the filer +func (store *FilerEtcStore) GetPolicy(ctx context.Context, name string) (*credential.PolicyDocument, error) { + policies, err := store.GetPolicies(ctx) + if err != nil { + return nil, err + } + + if policy, exists := policies[name]; exists { + return &policy, nil + } + + return nil, nil // Policy not found +} diff --git a/weed/credential/filer_etc/filer_etc_store.go b/weed/credential/filer_etc/filer_etc_store.go index 6951cc103..f8750cb25 100644 --- a/weed/credential/filer_etc/filer_etc_store.go +++ b/weed/credential/filer_etc/filer_etc_store.go @@ -1,15 +1,11 @@ package filer_etc import ( - "bytes" - "context" "fmt" "github.com/seaweedfs/seaweedfs/weed/credential" - "github.com/seaweedfs/seaweedfs/weed/filer" "github.com/seaweedfs/seaweedfs/weed/pb" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" - "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" "github.com/seaweedfs/seaweedfs/weed/util" "google.golang.org/grpc" ) @@ -54,182 +50,6 @@ func (store *FilerEtcStore) withFilerClient(fn func(client filer_pb.SeaweedFiler return pb.WithGrpcFilerClient(false, 0, pb.ServerAddress(store.filerGrpcAddress), store.grpcDialOption, fn) } -func (store *FilerEtcStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { - s3cfg := &iam_pb.S3ApiConfiguration{} - - err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { - var buf bytes.Buffer - if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { - if err != filer_pb.ErrNotFound { - return err - } - } - if buf.Len() > 0 { - return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) - } - return nil - }) - - return s3cfg, err -} - -func (store *FilerEtcStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { - return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { - var buf bytes.Buffer - if err := filer.ProtoToText(&buf, config); err != nil { - return fmt.Errorf("failed to marshal configuration: %v", err) - } - return filer.SaveInsideFiler(client, filer.IamConfigDirectory, filer.IamIdentityFile, buf.Bytes()) - }) -} - -func (store *FilerEtcStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { - // Load existing configuration - config, err := store.LoadConfiguration(ctx) - if err != nil { - return fmt.Errorf("failed to load configuration: %v", err) - } - - // Check if user already exists - for _, existingIdentity := range config.Identities { - if existingIdentity.Name == identity.Name { - return credential.ErrUserAlreadyExists - } - } - - // Add new identity - config.Identities = append(config.Identities, identity) - - // Save configuration - return store.SaveConfiguration(ctx, config) -} - -func (store *FilerEtcStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { - config, err := store.LoadConfiguration(ctx) - if err != nil { - return nil, fmt.Errorf("failed to load configuration: %v", err) - } - - for _, identity := range config.Identities { - if identity.Name == username { - return identity, nil - } - } - - return nil, credential.ErrUserNotFound -} - -func (store *FilerEtcStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { - config, err := store.LoadConfiguration(ctx) - if err != nil { - return fmt.Errorf("failed to load configuration: %v", err) - } - - // Find and update the user - for i, existingIdentity := range config.Identities { - if existingIdentity.Name == username { - config.Identities[i] = identity - return store.SaveConfiguration(ctx, config) - } - } - - return credential.ErrUserNotFound -} - -func (store *FilerEtcStore) DeleteUser(ctx context.Context, username string) error { - config, err := store.LoadConfiguration(ctx) - if err != nil { - return fmt.Errorf("failed to load configuration: %v", err) - } - - // Find and remove the user - for i, identity := range config.Identities { - if identity.Name == username { - config.Identities = append(config.Identities[:i], config.Identities[i+1:]...) - return store.SaveConfiguration(ctx, config) - } - } - - return credential.ErrUserNotFound -} - -func (store *FilerEtcStore) ListUsers(ctx context.Context) ([]string, error) { - config, err := store.LoadConfiguration(ctx) - if err != nil { - return nil, fmt.Errorf("failed to load configuration: %v", err) - } - - var usernames []string - for _, identity := range config.Identities { - usernames = append(usernames, identity.Name) - } - - return usernames, nil -} - -func (store *FilerEtcStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { - config, err := store.LoadConfiguration(ctx) - if err != nil { - return nil, fmt.Errorf("failed to load configuration: %v", err) - } - - for _, identity := range config.Identities { - for _, credential := range identity.Credentials { - if credential.AccessKey == accessKey { - return identity, nil - } - } - } - - return nil, credential.ErrAccessKeyNotFound -} - -func (store *FilerEtcStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { - config, err := store.LoadConfiguration(ctx) - if err != nil { - return fmt.Errorf("failed to load configuration: %v", err) - } - - // Find the user and add the credential - for _, identity := range config.Identities { - if identity.Name == username { - // Check if access key already exists - for _, existingCred := range identity.Credentials { - if existingCred.AccessKey == cred.AccessKey { - return fmt.Errorf("access key %s already exists", cred.AccessKey) - } - } - - identity.Credentials = append(identity.Credentials, cred) - return store.SaveConfiguration(ctx, config) - } - } - - return credential.ErrUserNotFound -} - -func (store *FilerEtcStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { - config, err := store.LoadConfiguration(ctx) - if err != nil { - return fmt.Errorf("failed to load configuration: %v", err) - } - - // Find the user and remove the credential - for _, identity := range config.Identities { - if identity.Name == username { - for i, cred := range identity.Credentials { - if cred.AccessKey == accessKey { - identity.Credentials = append(identity.Credentials[:i], identity.Credentials[i+1:]...) - return store.SaveConfiguration(ctx, config) - } - } - return credential.ErrAccessKeyNotFound - } - } - - return credential.ErrUserNotFound -} - func (store *FilerEtcStore) Shutdown() { // No cleanup needed for file store } diff --git a/weed/credential/memory/memory_identity.go b/weed/credential/memory/memory_identity.go new file mode 100644 index 000000000..191aa5d16 --- /dev/null +++ b/weed/credential/memory/memory_identity.go @@ -0,0 +1,302 @@ +package memory + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/seaweedfs/seaweedfs/weed/credential" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" +) + +func (store *MemoryStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { + store.mu.RLock() + defer store.mu.RUnlock() + + if !store.initialized { + return nil, fmt.Errorf("store not initialized") + } + + config := &iam_pb.S3ApiConfiguration{} + + // Convert all users to identities + for _, user := range store.users { + // Deep copy the identity to avoid mutation issues + identityCopy := store.deepCopyIdentity(user) + config.Identities = append(config.Identities, identityCopy) + } + + return config, nil +} + +func (store *MemoryStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { + store.mu.Lock() + defer store.mu.Unlock() + + if !store.initialized { + return fmt.Errorf("store not initialized") + } + + // Clear existing data + store.users = make(map[string]*iam_pb.Identity) + store.accessKeys = make(map[string]string) + + // Add all identities + for _, identity := range config.Identities { + // Deep copy to avoid mutation issues + identityCopy := store.deepCopyIdentity(identity) + store.users[identity.Name] = identityCopy + + // Index access keys + for _, credential := range identity.Credentials { + store.accessKeys[credential.AccessKey] = identity.Name + } + } + + return nil +} + +func (store *MemoryStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { + store.mu.Lock() + defer store.mu.Unlock() + + if !store.initialized { + return fmt.Errorf("store not initialized") + } + + if _, exists := store.users[identity.Name]; exists { + return credential.ErrUserAlreadyExists + } + + // Check for duplicate access keys + for _, cred := range identity.Credentials { + if _, exists := store.accessKeys[cred.AccessKey]; exists { + return fmt.Errorf("access key %s already exists", cred.AccessKey) + } + } + + // Deep copy to avoid mutation issues + identityCopy := store.deepCopyIdentity(identity) + store.users[identity.Name] = identityCopy + + // Index access keys + for _, cred := range identity.Credentials { + store.accessKeys[cred.AccessKey] = identity.Name + } + + return nil +} + +func (store *MemoryStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { + store.mu.RLock() + defer store.mu.RUnlock() + + if !store.initialized { + return nil, fmt.Errorf("store not initialized") + } + + user, exists := store.users[username] + if !exists { + return nil, credential.ErrUserNotFound + } + + // Return a deep copy to avoid mutation issues + return store.deepCopyIdentity(user), nil +} + +func (store *MemoryStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { + store.mu.Lock() + defer store.mu.Unlock() + + if !store.initialized { + return fmt.Errorf("store not initialized") + } + + existingUser, exists := store.users[username] + if !exists { + return credential.ErrUserNotFound + } + + // Remove old access keys from index + for _, cred := range existingUser.Credentials { + delete(store.accessKeys, cred.AccessKey) + } + + // Check for duplicate access keys (excluding current user) + for _, cred := range identity.Credentials { + if existingUsername, exists := store.accessKeys[cred.AccessKey]; exists && existingUsername != username { + return fmt.Errorf("access key %s already exists", cred.AccessKey) + } + } + + // Deep copy to avoid mutation issues + identityCopy := store.deepCopyIdentity(identity) + store.users[username] = identityCopy + + // Re-index access keys + for _, cred := range identity.Credentials { + store.accessKeys[cred.AccessKey] = username + } + + return nil +} + +func (store *MemoryStore) DeleteUser(ctx context.Context, username string) error { + store.mu.Lock() + defer store.mu.Unlock() + + if !store.initialized { + return fmt.Errorf("store not initialized") + } + + user, exists := store.users[username] + if !exists { + return credential.ErrUserNotFound + } + + // Remove access keys from index + for _, cred := range user.Credentials { + delete(store.accessKeys, cred.AccessKey) + } + + // Remove user + delete(store.users, username) + + return nil +} + +func (store *MemoryStore) ListUsers(ctx context.Context) ([]string, error) { + store.mu.RLock() + defer store.mu.RUnlock() + + if !store.initialized { + return nil, fmt.Errorf("store not initialized") + } + + var usernames []string + for username := range store.users { + usernames = append(usernames, username) + } + + return usernames, nil +} + +func (store *MemoryStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { + store.mu.RLock() + defer store.mu.RUnlock() + + if !store.initialized { + return nil, fmt.Errorf("store not initialized") + } + + username, exists := store.accessKeys[accessKey] + if !exists { + return nil, credential.ErrAccessKeyNotFound + } + + user, exists := store.users[username] + if !exists { + // This should not happen, but handle it gracefully + return nil, credential.ErrUserNotFound + } + + // Return a deep copy to avoid mutation issues + return store.deepCopyIdentity(user), nil +} + +func (store *MemoryStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { + store.mu.Lock() + defer store.mu.Unlock() + + if !store.initialized { + return fmt.Errorf("store not initialized") + } + + user, exists := store.users[username] + if !exists { + return credential.ErrUserNotFound + } + + // Check if access key already exists + if _, exists := store.accessKeys[cred.AccessKey]; exists { + return fmt.Errorf("access key %s already exists", cred.AccessKey) + } + + // Add credential to user + user.Credentials = append(user.Credentials, &iam_pb.Credential{ + AccessKey: cred.AccessKey, + SecretKey: cred.SecretKey, + }) + + // Index the access key + store.accessKeys[cred.AccessKey] = username + + return nil +} + +func (store *MemoryStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { + store.mu.Lock() + defer store.mu.Unlock() + + if !store.initialized { + return fmt.Errorf("store not initialized") + } + + user, exists := store.users[username] + if !exists { + return credential.ErrUserNotFound + } + + // Find and remove the credential + var newCredentials []*iam_pb.Credential + found := false + for _, cred := range user.Credentials { + if cred.AccessKey == accessKey { + found = true + // Remove from access key index + delete(store.accessKeys, accessKey) + } else { + newCredentials = append(newCredentials, cred) + } + } + + if !found { + return credential.ErrAccessKeyNotFound + } + + user.Credentials = newCredentials + return nil +} + +// deepCopyIdentity creates a deep copy of an identity to avoid mutation issues +func (store *MemoryStore) deepCopyIdentity(identity *iam_pb.Identity) *iam_pb.Identity { + if identity == nil { + return nil + } + + // Use JSON marshaling/unmarshaling for deep copy + // This is simple and safe for protobuf messages + data, err := json.Marshal(identity) + if err != nil { + // Fallback to shallow copy if JSON fails + return &iam_pb.Identity{ + Name: identity.Name, + Account: identity.Account, + Credentials: identity.Credentials, + Actions: identity.Actions, + } + } + + var copy iam_pb.Identity + if err := json.Unmarshal(data, ©); err != nil { + // Fallback to shallow copy if JSON fails + return &iam_pb.Identity{ + Name: identity.Name, + Account: identity.Account, + Credentials: identity.Credentials, + Actions: identity.Actions, + } + } + + return © +} diff --git a/weed/credential/memory/memory_policy.go b/weed/credential/memory/memory_policy.go new file mode 100644 index 000000000..1c9268958 --- /dev/null +++ b/weed/credential/memory/memory_policy.go @@ -0,0 +1,77 @@ +package memory + +import ( + "context" + "fmt" + + "github.com/seaweedfs/seaweedfs/weed/credential" +) + +// GetPolicies retrieves all IAM policies from memory +func (store *MemoryStore) GetPolicies(ctx context.Context) (map[string]credential.PolicyDocument, error) { + store.mu.RLock() + defer store.mu.RUnlock() + + if !store.initialized { + return nil, fmt.Errorf("store not initialized") + } + + // Create a copy of the policies map to avoid mutation issues + policies := make(map[string]credential.PolicyDocument) + for name, doc := range store.policies { + policies[name] = doc + } + + return policies, nil +} + +// GetPolicy retrieves a specific IAM policy by name from memory +func (store *MemoryStore) GetPolicy(ctx context.Context, name string) (*credential.PolicyDocument, error) { + store.mu.RLock() + defer store.mu.RUnlock() + + if policy, exists := store.policies[name]; exists { + return &policy, nil + } + + return nil, nil // Policy not found +} + +// CreatePolicy creates a new IAM policy in memory +func (store *MemoryStore) CreatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { + store.mu.Lock() + defer store.mu.Unlock() + + if !store.initialized { + return fmt.Errorf("store not initialized") + } + + store.policies[name] = document + return nil +} + +// UpdatePolicy updates an existing IAM policy in memory +func (store *MemoryStore) UpdatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { + store.mu.Lock() + defer store.mu.Unlock() + + if !store.initialized { + return fmt.Errorf("store not initialized") + } + + store.policies[name] = document + return nil +} + +// DeletePolicy deletes an IAM policy from memory +func (store *MemoryStore) DeletePolicy(ctx context.Context, name string) error { + store.mu.Lock() + defer store.mu.Unlock() + + if !store.initialized { + return fmt.Errorf("store not initialized") + } + + delete(store.policies, name) + return nil +} diff --git a/weed/credential/memory/memory_store.go b/weed/credential/memory/memory_store.go index e6117bf48..f0f383c04 100644 --- a/weed/credential/memory/memory_store.go +++ b/weed/credential/memory/memory_store.go @@ -1,9 +1,6 @@ package memory import ( - "context" - "encoding/json" - "fmt" "sync" "github.com/seaweedfs/seaweedfs/weed/credential" @@ -19,8 +16,9 @@ func init() { // This is primarily intended for testing purposes type MemoryStore struct { mu sync.RWMutex - users map[string]*iam_pb.Identity // username -> identity - accessKeys map[string]string // access_key -> username + users map[string]*iam_pb.Identity // username -> identity + accessKeys map[string]string // access_key -> username + policies map[string]credential.PolicyDocument // policy_name -> policy_document initialized bool } @@ -38,313 +36,22 @@ func (store *MemoryStore) Initialize(configuration util.Configuration, prefix st store.users = make(map[string]*iam_pb.Identity) store.accessKeys = make(map[string]string) + store.policies = make(map[string]credential.PolicyDocument) store.initialized = true return nil } -func (store *MemoryStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { - store.mu.RLock() - defer store.mu.RUnlock() - - if !store.initialized { - return nil, fmt.Errorf("store not initialized") - } - - config := &iam_pb.S3ApiConfiguration{} - - // Convert all users to identities - for _, user := range store.users { - // Deep copy the identity to avoid mutation issues - identityCopy := store.deepCopyIdentity(user) - config.Identities = append(config.Identities, identityCopy) - } - - return config, nil -} - -func (store *MemoryStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { - store.mu.Lock() - defer store.mu.Unlock() - - if !store.initialized { - return fmt.Errorf("store not initialized") - } - - // Clear existing data - store.users = make(map[string]*iam_pb.Identity) - store.accessKeys = make(map[string]string) - - // Add all identities - for _, identity := range config.Identities { - // Deep copy to avoid mutation issues - identityCopy := store.deepCopyIdentity(identity) - store.users[identity.Name] = identityCopy - - // Index access keys - for _, credential := range identity.Credentials { - store.accessKeys[credential.AccessKey] = identity.Name - } - } - - return nil -} - -func (store *MemoryStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { - store.mu.Lock() - defer store.mu.Unlock() - - if !store.initialized { - return fmt.Errorf("store not initialized") - } - - if _, exists := store.users[identity.Name]; exists { - return credential.ErrUserAlreadyExists - } - - // Check for duplicate access keys - for _, cred := range identity.Credentials { - if _, exists := store.accessKeys[cred.AccessKey]; exists { - return fmt.Errorf("access key %s already exists", cred.AccessKey) - } - } - - // Deep copy to avoid mutation issues - identityCopy := store.deepCopyIdentity(identity) - store.users[identity.Name] = identityCopy - - // Index access keys - for _, cred := range identity.Credentials { - store.accessKeys[cred.AccessKey] = identity.Name - } - - return nil -} - -func (store *MemoryStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { - store.mu.RLock() - defer store.mu.RUnlock() - - if !store.initialized { - return nil, fmt.Errorf("store not initialized") - } - - user, exists := store.users[username] - if !exists { - return nil, credential.ErrUserNotFound - } - - // Return a deep copy to avoid mutation issues - return store.deepCopyIdentity(user), nil -} - -func (store *MemoryStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { - store.mu.Lock() - defer store.mu.Unlock() - - if !store.initialized { - return fmt.Errorf("store not initialized") - } - - existingUser, exists := store.users[username] - if !exists { - return credential.ErrUserNotFound - } - - // Remove old access keys from index - for _, cred := range existingUser.Credentials { - delete(store.accessKeys, cred.AccessKey) - } - - // Check for duplicate access keys (excluding current user) - for _, cred := range identity.Credentials { - if existingUsername, exists := store.accessKeys[cred.AccessKey]; exists && existingUsername != username { - return fmt.Errorf("access key %s already exists", cred.AccessKey) - } - } - - // Deep copy to avoid mutation issues - identityCopy := store.deepCopyIdentity(identity) - store.users[username] = identityCopy - - // Re-index access keys - for _, cred := range identity.Credentials { - store.accessKeys[cred.AccessKey] = username - } - - return nil -} - -func (store *MemoryStore) DeleteUser(ctx context.Context, username string) error { - store.mu.Lock() - defer store.mu.Unlock() - - if !store.initialized { - return fmt.Errorf("store not initialized") - } - - user, exists := store.users[username] - if !exists { - return credential.ErrUserNotFound - } - - // Remove access keys from index - for _, cred := range user.Credentials { - delete(store.accessKeys, cred.AccessKey) - } - - // Remove user - delete(store.users, username) - - return nil -} - -func (store *MemoryStore) ListUsers(ctx context.Context) ([]string, error) { - store.mu.RLock() - defer store.mu.RUnlock() - - if !store.initialized { - return nil, fmt.Errorf("store not initialized") - } - - var usernames []string - for username := range store.users { - usernames = append(usernames, username) - } - - return usernames, nil -} - -func (store *MemoryStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { - store.mu.RLock() - defer store.mu.RUnlock() - - if !store.initialized { - return nil, fmt.Errorf("store not initialized") - } - - username, exists := store.accessKeys[accessKey] - if !exists { - return nil, credential.ErrAccessKeyNotFound - } - - user, exists := store.users[username] - if !exists { - // This should not happen, but handle it gracefully - return nil, credential.ErrUserNotFound - } - - // Return a deep copy to avoid mutation issues - return store.deepCopyIdentity(user), nil -} - -func (store *MemoryStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { - store.mu.Lock() - defer store.mu.Unlock() - - if !store.initialized { - return fmt.Errorf("store not initialized") - } - - user, exists := store.users[username] - if !exists { - return credential.ErrUserNotFound - } - - // Check if access key already exists - if _, exists := store.accessKeys[cred.AccessKey]; exists { - return fmt.Errorf("access key %s already exists", cred.AccessKey) - } - - // Add credential to user - user.Credentials = append(user.Credentials, &iam_pb.Credential{ - AccessKey: cred.AccessKey, - SecretKey: cred.SecretKey, - }) - - // Index the access key - store.accessKeys[cred.AccessKey] = username - - return nil -} - -func (store *MemoryStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { - store.mu.Lock() - defer store.mu.Unlock() - - if !store.initialized { - return fmt.Errorf("store not initialized") - } - - user, exists := store.users[username] - if !exists { - return credential.ErrUserNotFound - } - - // Find and remove the credential - var newCredentials []*iam_pb.Credential - found := false - for _, cred := range user.Credentials { - if cred.AccessKey == accessKey { - found = true - // Remove from access key index - delete(store.accessKeys, accessKey) - } else { - newCredentials = append(newCredentials, cred) - } - } - - if !found { - return credential.ErrAccessKeyNotFound - } - - user.Credentials = newCredentials - return nil -} - func (store *MemoryStore) Shutdown() { store.mu.Lock() defer store.mu.Unlock() - // Clear all data store.users = nil store.accessKeys = nil + store.policies = nil store.initialized = false } -// deepCopyIdentity creates a deep copy of an identity to avoid mutation issues -func (store *MemoryStore) deepCopyIdentity(identity *iam_pb.Identity) *iam_pb.Identity { - if identity == nil { - return nil - } - - // Use JSON marshaling/unmarshaling for deep copy - // This is simple and safe for protobuf messages - data, err := json.Marshal(identity) - if err != nil { - // Fallback to shallow copy if JSON fails - return &iam_pb.Identity{ - Name: identity.Name, - Account: identity.Account, - Credentials: identity.Credentials, - Actions: identity.Actions, - } - } - - var copy iam_pb.Identity - if err := json.Unmarshal(data, ©); err != nil { - // Fallback to shallow copy if JSON fails - return &iam_pb.Identity{ - Name: identity.Name, - Account: identity.Account, - Credentials: identity.Credentials, - Actions: identity.Actions, - } - } - - return © -} - // Reset clears all data in the store (useful for testing) func (store *MemoryStore) Reset() { store.mu.Lock() diff --git a/weed/credential/postgres/postgres_identity.go b/weed/credential/postgres/postgres_identity.go new file mode 100644 index 000000000..ea3627c50 --- /dev/null +++ b/weed/credential/postgres/postgres_identity.go @@ -0,0 +1,446 @@ +package postgres + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + + "github.com/seaweedfs/seaweedfs/weed/credential" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" +) + +func (store *PostgresStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { + if !store.configured { + return nil, fmt.Errorf("store not configured") + } + + config := &iam_pb.S3ApiConfiguration{} + + // Query all users + rows, err := store.db.QueryContext(ctx, "SELECT username, email, account_data, actions FROM users") + if err != nil { + return nil, fmt.Errorf("failed to query users: %v", err) + } + defer rows.Close() + + for rows.Next() { + var username, email string + var accountDataJSON, actionsJSON []byte + + if err := rows.Scan(&username, &email, &accountDataJSON, &actionsJSON); err != nil { + return nil, fmt.Errorf("failed to scan user row: %v", err) + } + + identity := &iam_pb.Identity{ + Name: username, + } + + // Parse account data + if len(accountDataJSON) > 0 { + if err := json.Unmarshal(accountDataJSON, &identity.Account); err != nil { + return nil, fmt.Errorf("failed to unmarshal account data for user %s: %v", username, err) + } + } + + // Parse actions + if len(actionsJSON) > 0 { + if err := json.Unmarshal(actionsJSON, &identity.Actions); err != nil { + return nil, fmt.Errorf("failed to unmarshal actions for user %s: %v", username, err) + } + } + + // Query credentials for this user + credRows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username) + if err != nil { + return nil, fmt.Errorf("failed to query credentials for user %s: %v", username, err) + } + + for credRows.Next() { + var accessKey, secretKey string + if err := credRows.Scan(&accessKey, &secretKey); err != nil { + credRows.Close() + return nil, fmt.Errorf("failed to scan credential row for user %s: %v", username, err) + } + + identity.Credentials = append(identity.Credentials, &iam_pb.Credential{ + AccessKey: accessKey, + SecretKey: secretKey, + }) + } + credRows.Close() + + config.Identities = append(config.Identities, identity) + } + + return config, nil +} + +func (store *PostgresStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { + if !store.configured { + return fmt.Errorf("store not configured") + } + + // Start transaction + tx, err := store.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("failed to begin transaction: %v", err) + } + defer tx.Rollback() + + // Clear existing data + if _, err := tx.ExecContext(ctx, "DELETE FROM credentials"); err != nil { + return fmt.Errorf("failed to clear credentials: %v", err) + } + if _, err := tx.ExecContext(ctx, "DELETE FROM users"); err != nil { + return fmt.Errorf("failed to clear users: %v", err) + } + + // Insert all identities + for _, identity := range config.Identities { + // Marshal account data + var accountDataJSON []byte + if identity.Account != nil { + accountDataJSON, err = json.Marshal(identity.Account) + if err != nil { + return fmt.Errorf("failed to marshal account data for user %s: %v", identity.Name, err) + } + } + + // Marshal actions + var actionsJSON []byte + if identity.Actions != nil { + actionsJSON, err = json.Marshal(identity.Actions) + if err != nil { + return fmt.Errorf("failed to marshal actions for user %s: %v", identity.Name, err) + } + } + + // Insert user + _, err := tx.ExecContext(ctx, + "INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)", + identity.Name, "", accountDataJSON, actionsJSON) + if err != nil { + return fmt.Errorf("failed to insert user %s: %v", identity.Name, err) + } + + // Insert credentials + for _, cred := range identity.Credentials { + _, err := tx.ExecContext(ctx, + "INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", + identity.Name, cred.AccessKey, cred.SecretKey) + if err != nil { + return fmt.Errorf("failed to insert credential for user %s: %v", identity.Name, err) + } + } + } + + return tx.Commit() +} + +func (store *PostgresStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { + if !store.configured { + return fmt.Errorf("store not configured") + } + + // Check if user already exists + var count int + err := store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", identity.Name).Scan(&count) + if err != nil { + return fmt.Errorf("failed to check user existence: %v", err) + } + if count > 0 { + return credential.ErrUserAlreadyExists + } + + // Start transaction + tx, err := store.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("failed to begin transaction: %v", err) + } + defer tx.Rollback() + + // Marshal account data + var accountDataJSON []byte + if identity.Account != nil { + accountDataJSON, err = json.Marshal(identity.Account) + if err != nil { + return fmt.Errorf("failed to marshal account data: %v", err) + } + } + + // Marshal actions + var actionsJSON []byte + if identity.Actions != nil { + actionsJSON, err = json.Marshal(identity.Actions) + if err != nil { + return fmt.Errorf("failed to marshal actions: %v", err) + } + } + + // Insert user + _, err = tx.ExecContext(ctx, + "INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)", + identity.Name, "", accountDataJSON, actionsJSON) + if err != nil { + return fmt.Errorf("failed to insert user: %v", err) + } + + // Insert credentials + for _, cred := range identity.Credentials { + _, err = tx.ExecContext(ctx, + "INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", + identity.Name, cred.AccessKey, cred.SecretKey) + if err != nil { + return fmt.Errorf("failed to insert credential: %v", err) + } + } + + return tx.Commit() +} + +func (store *PostgresStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { + if !store.configured { + return nil, fmt.Errorf("store not configured") + } + + var email string + var accountDataJSON, actionsJSON []byte + + err := store.db.QueryRowContext(ctx, + "SELECT email, account_data, actions FROM users WHERE username = $1", + username).Scan(&email, &accountDataJSON, &actionsJSON) + if err != nil { + if err == sql.ErrNoRows { + return nil, credential.ErrUserNotFound + } + return nil, fmt.Errorf("failed to query user: %v", err) + } + + identity := &iam_pb.Identity{ + Name: username, + } + + // Parse account data + if len(accountDataJSON) > 0 { + if err := json.Unmarshal(accountDataJSON, &identity.Account); err != nil { + return nil, fmt.Errorf("failed to unmarshal account data: %v", err) + } + } + + // Parse actions + if len(actionsJSON) > 0 { + if err := json.Unmarshal(actionsJSON, &identity.Actions); err != nil { + return nil, fmt.Errorf("failed to unmarshal actions: %v", err) + } + } + + // Query credentials + rows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username) + if err != nil { + return nil, fmt.Errorf("failed to query credentials: %v", err) + } + defer rows.Close() + + for rows.Next() { + var accessKey, secretKey string + if err := rows.Scan(&accessKey, &secretKey); err != nil { + return nil, fmt.Errorf("failed to scan credential: %v", err) + } + + identity.Credentials = append(identity.Credentials, &iam_pb.Credential{ + AccessKey: accessKey, + SecretKey: secretKey, + }) + } + + return identity, nil +} + +func (store *PostgresStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { + if !store.configured { + return fmt.Errorf("store not configured") + } + + // Start transaction + tx, err := store.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("failed to begin transaction: %v", err) + } + defer tx.Rollback() + + // Check if user exists + var count int + err = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) + if err != nil { + return fmt.Errorf("failed to check user existence: %v", err) + } + if count == 0 { + return credential.ErrUserNotFound + } + + // Marshal account data + var accountDataJSON []byte + if identity.Account != nil { + accountDataJSON, err = json.Marshal(identity.Account) + if err != nil { + return fmt.Errorf("failed to marshal account data: %v", err) + } + } + + // Marshal actions + var actionsJSON []byte + if identity.Actions != nil { + actionsJSON, err = json.Marshal(identity.Actions) + if err != nil { + return fmt.Errorf("failed to marshal actions: %v", err) + } + } + + // Update user + _, err = tx.ExecContext(ctx, + "UPDATE users SET email = $2, account_data = $3, actions = $4, updated_at = CURRENT_TIMESTAMP WHERE username = $1", + username, "", accountDataJSON, actionsJSON) + if err != nil { + return fmt.Errorf("failed to update user: %v", err) + } + + // Delete existing credentials + _, err = tx.ExecContext(ctx, "DELETE FROM credentials WHERE username = $1", username) + if err != nil { + return fmt.Errorf("failed to delete existing credentials: %v", err) + } + + // Insert new credentials + for _, cred := range identity.Credentials { + _, err = tx.ExecContext(ctx, + "INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", + username, cred.AccessKey, cred.SecretKey) + if err != nil { + return fmt.Errorf("failed to insert credential: %v", err) + } + } + + return tx.Commit() +} + +func (store *PostgresStore) DeleteUser(ctx context.Context, username string) error { + if !store.configured { + return fmt.Errorf("store not configured") + } + + result, err := store.db.ExecContext(ctx, "DELETE FROM users WHERE username = $1", username) + if err != nil { + return fmt.Errorf("failed to delete user: %v", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %v", err) + } + + if rowsAffected == 0 { + return credential.ErrUserNotFound + } + + return nil +} + +func (store *PostgresStore) ListUsers(ctx context.Context) ([]string, error) { + if !store.configured { + return nil, fmt.Errorf("store not configured") + } + + rows, err := store.db.QueryContext(ctx, "SELECT username FROM users ORDER BY username") + if err != nil { + return nil, fmt.Errorf("failed to query users: %v", err) + } + defer rows.Close() + + var usernames []string + for rows.Next() { + var username string + if err := rows.Scan(&username); err != nil { + return nil, fmt.Errorf("failed to scan username: %v", err) + } + usernames = append(usernames, username) + } + + return usernames, nil +} + +func (store *PostgresStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { + if !store.configured { + return nil, fmt.Errorf("store not configured") + } + + var username string + err := store.db.QueryRowContext(ctx, "SELECT username FROM credentials WHERE access_key = $1", accessKey).Scan(&username) + if err != nil { + if err == sql.ErrNoRows { + return nil, credential.ErrAccessKeyNotFound + } + return nil, fmt.Errorf("failed to query access key: %v", err) + } + + return store.GetUser(ctx, username) +} + +func (store *PostgresStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { + if !store.configured { + return fmt.Errorf("store not configured") + } + + // Check if user exists + var count int + err := store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) + if err != nil { + return fmt.Errorf("failed to check user existence: %v", err) + } + if count == 0 { + return credential.ErrUserNotFound + } + + // Insert credential + _, err = store.db.ExecContext(ctx, + "INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", + username, cred.AccessKey, cred.SecretKey) + if err != nil { + return fmt.Errorf("failed to insert credential: %v", err) + } + + return nil +} + +func (store *PostgresStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { + if !store.configured { + return fmt.Errorf("store not configured") + } + + result, err := store.db.ExecContext(ctx, + "DELETE FROM credentials WHERE username = $1 AND access_key = $2", + username, accessKey) + if err != nil { + return fmt.Errorf("failed to delete access key: %v", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %v", err) + } + + if rowsAffected == 0 { + // Check if user exists + var count int + err = store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) + if err != nil { + return fmt.Errorf("failed to check user existence: %v", err) + } + if count == 0 { + return credential.ErrUserNotFound + } + return credential.ErrAccessKeyNotFound + } + + return nil +} diff --git a/weed/credential/postgres/postgres_policy.go b/weed/credential/postgres/postgres_policy.go new file mode 100644 index 000000000..8be2b108c --- /dev/null +++ b/weed/credential/postgres/postgres_policy.go @@ -0,0 +1,130 @@ +package postgres + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/seaweedfs/seaweedfs/weed/credential" +) + +// GetPolicies retrieves all IAM policies from PostgreSQL +func (store *PostgresStore) GetPolicies(ctx context.Context) (map[string]credential.PolicyDocument, error) { + if !store.configured { + return nil, fmt.Errorf("store not configured") + } + + policies := make(map[string]credential.PolicyDocument) + + rows, err := store.db.QueryContext(ctx, "SELECT name, document FROM policies") + if err != nil { + return nil, fmt.Errorf("failed to query policies: %v", err) + } + defer rows.Close() + + for rows.Next() { + var name string + var documentJSON []byte + + if err := rows.Scan(&name, &documentJSON); err != nil { + return nil, fmt.Errorf("failed to scan policy row: %v", err) + } + + var document credential.PolicyDocument + if err := json.Unmarshal(documentJSON, &document); err != nil { + return nil, fmt.Errorf("failed to unmarshal policy document for %s: %v", name, err) + } + + policies[name] = document + } + + return policies, nil +} + +// CreatePolicy creates a new IAM policy in PostgreSQL +func (store *PostgresStore) CreatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { + if !store.configured { + return fmt.Errorf("store not configured") + } + + documentJSON, err := json.Marshal(document) + if err != nil { + return fmt.Errorf("failed to marshal policy document: %v", err) + } + + _, err = store.db.ExecContext(ctx, + "INSERT INTO policies (name, document) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET document = $2, updated_at = CURRENT_TIMESTAMP", + name, documentJSON) + if err != nil { + return fmt.Errorf("failed to insert policy: %v", err) + } + + return nil +} + +// UpdatePolicy updates an existing IAM policy in PostgreSQL +func (store *PostgresStore) UpdatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { + if !store.configured { + return fmt.Errorf("store not configured") + } + + documentJSON, err := json.Marshal(document) + if err != nil { + return fmt.Errorf("failed to marshal policy document: %v", err) + } + + result, err := store.db.ExecContext(ctx, + "UPDATE policies SET document = $2, updated_at = CURRENT_TIMESTAMP WHERE name = $1", + name, documentJSON) + if err != nil { + return fmt.Errorf("failed to update policy: %v", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %v", err) + } + + if rowsAffected == 0 { + return fmt.Errorf("policy %s not found", name) + } + + return nil +} + +// DeletePolicy deletes an IAM policy from PostgreSQL +func (store *PostgresStore) DeletePolicy(ctx context.Context, name string) error { + if !store.configured { + return fmt.Errorf("store not configured") + } + + result, err := store.db.ExecContext(ctx, "DELETE FROM policies WHERE name = $1", name) + if err != nil { + return fmt.Errorf("failed to delete policy: %v", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %v", err) + } + + if rowsAffected == 0 { + return fmt.Errorf("policy %s not found", name) + } + + return nil +} + +// GetPolicy retrieves a specific IAM policy by name from PostgreSQL +func (store *PostgresStore) GetPolicy(ctx context.Context, name string) (*credential.PolicyDocument, error) { + policies, err := store.GetPolicies(ctx) + if err != nil { + return nil, err + } + + if policy, exists := policies[name]; exists { + return &policy, nil + } + + return nil, nil // Policy not found +} diff --git a/weed/credential/postgres/postgres_store.go b/weed/credential/postgres/postgres_store.go index 0d75ad8c0..40d200668 100644 --- a/weed/credential/postgres/postgres_store.go +++ b/weed/credential/postgres/postgres_store.go @@ -1,14 +1,11 @@ package postgres import ( - "context" "database/sql" - "encoding/json" "fmt" "time" "github.com/seaweedfs/seaweedfs/weed/credential" - "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" "github.com/seaweedfs/seaweedfs/weed/util" _ "github.com/lib/pq" @@ -114,6 +111,17 @@ func (store *PostgresStore) createTables() error { CREATE INDEX IF NOT EXISTS idx_credentials_access_key ON credentials(access_key); ` + // Create policies table + policiesTable := ` + CREATE TABLE IF NOT EXISTS policies ( + name VARCHAR(255) PRIMARY KEY, + document JSONB NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + CREATE INDEX IF NOT EXISTS idx_policies_name ON policies(name); + ` + // Execute table creation if _, err := store.db.Exec(usersTable); err != nil { return fmt.Errorf("failed to create users table: %v", err) @@ -123,439 +131,8 @@ func (store *PostgresStore) createTables() error { return fmt.Errorf("failed to create credentials table: %v", err) } - return nil -} - -func (store *PostgresStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { - if !store.configured { - return nil, fmt.Errorf("store not configured") - } - - config := &iam_pb.S3ApiConfiguration{} - - // Query all users - rows, err := store.db.QueryContext(ctx, "SELECT username, email, account_data, actions FROM users") - if err != nil { - return nil, fmt.Errorf("failed to query users: %v", err) - } - defer rows.Close() - - for rows.Next() { - var username, email string - var accountDataJSON, actionsJSON []byte - - if err := rows.Scan(&username, &email, &accountDataJSON, &actionsJSON); err != nil { - return nil, fmt.Errorf("failed to scan user row: %v", err) - } - - identity := &iam_pb.Identity{ - Name: username, - } - - // Parse account data - if len(accountDataJSON) > 0 { - if err := json.Unmarshal(accountDataJSON, &identity.Account); err != nil { - return nil, fmt.Errorf("failed to unmarshal account data for user %s: %v", username, err) - } - } - - // Parse actions - if len(actionsJSON) > 0 { - if err := json.Unmarshal(actionsJSON, &identity.Actions); err != nil { - return nil, fmt.Errorf("failed to unmarshal actions for user %s: %v", username, err) - } - } - - // Query credentials for this user - credRows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username) - if err != nil { - return nil, fmt.Errorf("failed to query credentials for user %s: %v", username, err) - } - - for credRows.Next() { - var accessKey, secretKey string - if err := credRows.Scan(&accessKey, &secretKey); err != nil { - credRows.Close() - return nil, fmt.Errorf("failed to scan credential row for user %s: %v", username, err) - } - - identity.Credentials = append(identity.Credentials, &iam_pb.Credential{ - AccessKey: accessKey, - SecretKey: secretKey, - }) - } - credRows.Close() - - config.Identities = append(config.Identities, identity) - } - - return config, nil -} - -func (store *PostgresStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { - if !store.configured { - return fmt.Errorf("store not configured") - } - - // Start transaction - tx, err := store.db.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("failed to begin transaction: %v", err) - } - defer tx.Rollback() - - // Clear existing data - if _, err := tx.ExecContext(ctx, "DELETE FROM credentials"); err != nil { - return fmt.Errorf("failed to clear credentials: %v", err) - } - if _, err := tx.ExecContext(ctx, "DELETE FROM users"); err != nil { - return fmt.Errorf("failed to clear users: %v", err) - } - - // Insert all identities - for _, identity := range config.Identities { - // Marshal account data - var accountDataJSON []byte - if identity.Account != nil { - accountDataJSON, err = json.Marshal(identity.Account) - if err != nil { - return fmt.Errorf("failed to marshal account data for user %s: %v", identity.Name, err) - } - } - - // Marshal actions - var actionsJSON []byte - if identity.Actions != nil { - actionsJSON, err = json.Marshal(identity.Actions) - if err != nil { - return fmt.Errorf("failed to marshal actions for user %s: %v", identity.Name, err) - } - } - - // Insert user - _, err := tx.ExecContext(ctx, - "INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)", - identity.Name, "", accountDataJSON, actionsJSON) - if err != nil { - return fmt.Errorf("failed to insert user %s: %v", identity.Name, err) - } - - // Insert credentials - for _, cred := range identity.Credentials { - _, err := tx.ExecContext(ctx, - "INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", - identity.Name, cred.AccessKey, cred.SecretKey) - if err != nil { - return fmt.Errorf("failed to insert credential for user %s: %v", identity.Name, err) - } - } - } - - return tx.Commit() -} - -func (store *PostgresStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { - if !store.configured { - return fmt.Errorf("store not configured") - } - - // Check if user already exists - var count int - err := store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", identity.Name).Scan(&count) - if err != nil { - return fmt.Errorf("failed to check user existence: %v", err) - } - if count > 0 { - return credential.ErrUserAlreadyExists - } - - // Start transaction - tx, err := store.db.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("failed to begin transaction: %v", err) - } - defer tx.Rollback() - - // Marshal account data - var accountDataJSON []byte - if identity.Account != nil { - accountDataJSON, err = json.Marshal(identity.Account) - if err != nil { - return fmt.Errorf("failed to marshal account data: %v", err) - } - } - - // Marshal actions - var actionsJSON []byte - if identity.Actions != nil { - actionsJSON, err = json.Marshal(identity.Actions) - if err != nil { - return fmt.Errorf("failed to marshal actions: %v", err) - } - } - - // Insert user - _, err = tx.ExecContext(ctx, - "INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)", - identity.Name, "", accountDataJSON, actionsJSON) - if err != nil { - return fmt.Errorf("failed to insert user: %v", err) - } - - // Insert credentials - for _, cred := range identity.Credentials { - _, err = tx.ExecContext(ctx, - "INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", - identity.Name, cred.AccessKey, cred.SecretKey) - if err != nil { - return fmt.Errorf("failed to insert credential: %v", err) - } - } - - return tx.Commit() -} - -func (store *PostgresStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { - if !store.configured { - return nil, fmt.Errorf("store not configured") - } - - var email string - var accountDataJSON, actionsJSON []byte - - err := store.db.QueryRowContext(ctx, - "SELECT email, account_data, actions FROM users WHERE username = $1", - username).Scan(&email, &accountDataJSON, &actionsJSON) - if err != nil { - if err == sql.ErrNoRows { - return nil, credential.ErrUserNotFound - } - return nil, fmt.Errorf("failed to query user: %v", err) - } - - identity := &iam_pb.Identity{ - Name: username, - } - - // Parse account data - if len(accountDataJSON) > 0 { - if err := json.Unmarshal(accountDataJSON, &identity.Account); err != nil { - return nil, fmt.Errorf("failed to unmarshal account data: %v", err) - } - } - - // Parse actions - if len(actionsJSON) > 0 { - if err := json.Unmarshal(actionsJSON, &identity.Actions); err != nil { - return nil, fmt.Errorf("failed to unmarshal actions: %v", err) - } - } - - // Query credentials - rows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username) - if err != nil { - return nil, fmt.Errorf("failed to query credentials: %v", err) - } - defer rows.Close() - - for rows.Next() { - var accessKey, secretKey string - if err := rows.Scan(&accessKey, &secretKey); err != nil { - return nil, fmt.Errorf("failed to scan credential: %v", err) - } - - identity.Credentials = append(identity.Credentials, &iam_pb.Credential{ - AccessKey: accessKey, - SecretKey: secretKey, - }) - } - - return identity, nil -} - -func (store *PostgresStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { - if !store.configured { - return fmt.Errorf("store not configured") - } - - // Start transaction - tx, err := store.db.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("failed to begin transaction: %v", err) - } - defer tx.Rollback() - - // Check if user exists - var count int - err = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) - if err != nil { - return fmt.Errorf("failed to check user existence: %v", err) - } - if count == 0 { - return credential.ErrUserNotFound - } - - // Marshal account data - var accountDataJSON []byte - if identity.Account != nil { - accountDataJSON, err = json.Marshal(identity.Account) - if err != nil { - return fmt.Errorf("failed to marshal account data: %v", err) - } - } - - // Marshal actions - var actionsJSON []byte - if identity.Actions != nil { - actionsJSON, err = json.Marshal(identity.Actions) - if err != nil { - return fmt.Errorf("failed to marshal actions: %v", err) - } - } - - // Update user - _, err = tx.ExecContext(ctx, - "UPDATE users SET email = $2, account_data = $3, actions = $4, updated_at = CURRENT_TIMESTAMP WHERE username = $1", - username, "", accountDataJSON, actionsJSON) - if err != nil { - return fmt.Errorf("failed to update user: %v", err) - } - - // Delete existing credentials - _, err = tx.ExecContext(ctx, "DELETE FROM credentials WHERE username = $1", username) - if err != nil { - return fmt.Errorf("failed to delete existing credentials: %v", err) - } - - // Insert new credentials - for _, cred := range identity.Credentials { - _, err = tx.ExecContext(ctx, - "INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", - username, cred.AccessKey, cred.SecretKey) - if err != nil { - return fmt.Errorf("failed to insert credential: %v", err) - } - } - - return tx.Commit() -} - -func (store *PostgresStore) DeleteUser(ctx context.Context, username string) error { - if !store.configured { - return fmt.Errorf("store not configured") - } - - result, err := store.db.ExecContext(ctx, "DELETE FROM users WHERE username = $1", username) - if err != nil { - return fmt.Errorf("failed to delete user: %v", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("failed to get rows affected: %v", err) - } - - if rowsAffected == 0 { - return credential.ErrUserNotFound - } - - return nil -} - -func (store *PostgresStore) ListUsers(ctx context.Context) ([]string, error) { - if !store.configured { - return nil, fmt.Errorf("store not configured") - } - - rows, err := store.db.QueryContext(ctx, "SELECT username FROM users ORDER BY username") - if err != nil { - return nil, fmt.Errorf("failed to query users: %v", err) - } - defer rows.Close() - - var usernames []string - for rows.Next() { - var username string - if err := rows.Scan(&username); err != nil { - return nil, fmt.Errorf("failed to scan username: %v", err) - } - usernames = append(usernames, username) - } - - return usernames, nil -} - -func (store *PostgresStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { - if !store.configured { - return nil, fmt.Errorf("store not configured") - } - - var username string - err := store.db.QueryRowContext(ctx, "SELECT username FROM credentials WHERE access_key = $1", accessKey).Scan(&username) - if err != nil { - if err == sql.ErrNoRows { - return nil, credential.ErrAccessKeyNotFound - } - return nil, fmt.Errorf("failed to query access key: %v", err) - } - - return store.GetUser(ctx, username) -} - -func (store *PostgresStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { - if !store.configured { - return fmt.Errorf("store not configured") - } - - // Check if user exists - var count int - err := store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) - if err != nil { - return fmt.Errorf("failed to check user existence: %v", err) - } - if count == 0 { - return credential.ErrUserNotFound - } - - // Insert credential - _, err = store.db.ExecContext(ctx, - "INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", - username, cred.AccessKey, cred.SecretKey) - if err != nil { - return fmt.Errorf("failed to insert credential: %v", err) - } - - return nil -} - -func (store *PostgresStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { - if !store.configured { - return fmt.Errorf("store not configured") - } - - result, err := store.db.ExecContext(ctx, - "DELETE FROM credentials WHERE username = $1 AND access_key = $2", - username, accessKey) - if err != nil { - return fmt.Errorf("failed to delete access key: %v", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("failed to get rows affected: %v", err) - } - - if rowsAffected == 0 { - // Check if user exists - var count int - err = store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) - if err != nil { - return fmt.Errorf("failed to check user existence: %v", err) - } - if count == 0 { - return credential.ErrUserNotFound - } - return credential.ErrAccessKeyNotFound + if _, err := store.db.Exec(policiesTable); err != nil { + return fmt.Errorf("failed to create policies table: %v", err) } return nil diff --git a/weed/credential/test/policy_test.go b/weed/credential/test/policy_test.go new file mode 100644 index 000000000..341a05003 --- /dev/null +++ b/weed/credential/test/policy_test.go @@ -0,0 +1,146 @@ +package test + +import ( + "context" + "testing" + + "github.com/seaweedfs/seaweedfs/weed/credential" + "github.com/seaweedfs/seaweedfs/weed/credential/memory" + + // Import all store implementations to register them + _ "github.com/seaweedfs/seaweedfs/weed/credential/filer_etc" + _ "github.com/seaweedfs/seaweedfs/weed/credential/memory" + _ "github.com/seaweedfs/seaweedfs/weed/credential/postgres" +) + +// TestPolicyManagement tests policy management across all credential stores +func TestPolicyManagement(t *testing.T) { + ctx := context.Background() + + // Test with memory store (easiest to test) + credentialManager, err := credential.NewCredentialManager(credential.StoreTypeMemory, nil, "") + if err != nil { + t.Fatalf("Failed to create credential manager: %v", err) + } + + // Test policy operations + testPolicyOperations(t, ctx, credentialManager) +} + +func testPolicyOperations(t *testing.T, ctx context.Context, credentialManager *credential.CredentialManager) { + store := credentialManager.GetStore() + + // Cast to memory store to access policy methods + memoryStore, ok := store.(*memory.MemoryStore) + if !ok { + t.Skip("Store is not a memory store") + } + + // Test GetPolicies (should be empty initially) + policies, err := memoryStore.GetPolicies(ctx) + if err != nil { + t.Fatalf("Failed to get policies: %v", err) + } + if len(policies) != 0 { + t.Errorf("Expected 0 policies, got %d", len(policies)) + } + + // Test CreatePolicy + testPolicy := credential.PolicyDocument{ + Version: "2012-10-17", + Statement: []*credential.PolicyStatement{ + { + Effect: "Allow", + Action: []string{"s3:GetObject"}, + Resource: []string{"arn:aws:s3:::test-bucket/*"}, + }, + }, + } + + err = memoryStore.CreatePolicy(ctx, "test-policy", testPolicy) + if err != nil { + t.Fatalf("Failed to create policy: %v", err) + } + + // Test GetPolicies (should have 1 policy now) + policies, err = memoryStore.GetPolicies(ctx) + if err != nil { + t.Fatalf("Failed to get policies: %v", err) + } + if len(policies) != 1 { + t.Errorf("Expected 1 policy, got %d", len(policies)) + } + + // Verify policy content + policy, exists := policies["test-policy"] + if !exists { + t.Error("test-policy not found") + } + if policy.Version != "2012-10-17" { + t.Errorf("Expected policy version '2012-10-17', got '%s'", policy.Version) + } + if len(policy.Statement) != 1 { + t.Errorf("Expected 1 statement, got %d", len(policy.Statement)) + } + + // Test UpdatePolicy + updatedPolicy := credential.PolicyDocument{ + Version: "2012-10-17", + Statement: []*credential.PolicyStatement{ + { + Effect: "Allow", + Action: []string{"s3:GetObject", "s3:PutObject"}, + Resource: []string{"arn:aws:s3:::test-bucket/*"}, + }, + }, + } + + err = memoryStore.UpdatePolicy(ctx, "test-policy", updatedPolicy) + if err != nil { + t.Fatalf("Failed to update policy: %v", err) + } + + // Verify the update + policies, err = memoryStore.GetPolicies(ctx) + if err != nil { + t.Fatalf("Failed to get policies after update: %v", err) + } + + updatedPolicyResult, exists := policies["test-policy"] + if !exists { + t.Error("test-policy not found after update") + } + if len(updatedPolicyResult.Statement) != 1 { + t.Errorf("Expected 1 statement after update, got %d", len(updatedPolicyResult.Statement)) + } + if len(updatedPolicyResult.Statement[0].Action) != 2 { + t.Errorf("Expected 2 actions after update, got %d", len(updatedPolicyResult.Statement[0].Action)) + } + + // Test DeletePolicy + err = memoryStore.DeletePolicy(ctx, "test-policy") + if err != nil { + t.Fatalf("Failed to delete policy: %v", err) + } + + // Verify deletion + policies, err = memoryStore.GetPolicies(ctx) + if err != nil { + t.Fatalf("Failed to get policies after deletion: %v", err) + } + if len(policies) != 0 { + t.Errorf("Expected 0 policies after deletion, got %d", len(policies)) + } +} + +// TestPolicyManagementWithFilerEtc tests policy management with filer_etc store +func TestPolicyManagementWithFilerEtc(t *testing.T) { + // Skip this test if we can't connect to a filer + t.Skip("Filer connection required for filer_etc store testing") +} + +// TestPolicyManagementWithPostgres tests policy management with postgres store +func TestPolicyManagementWithPostgres(t *testing.T) { + // Skip this test if we can't connect to PostgreSQL + t.Skip("PostgreSQL connection required for postgres store testing") +} diff --git a/weed/worker/tasks/balance/ui_templ.go b/weed/worker/tasks/balance/ui_templ.go deleted file mode 100644 index 54998af4c..000000000 --- a/weed/worker/tasks/balance/ui_templ.go +++ /dev/null @@ -1,369 +0,0 @@ -package balance - -import ( - "fmt" - "strconv" - "time" - - "github.com/seaweedfs/seaweedfs/weed/admin/view/components" - "github.com/seaweedfs/seaweedfs/weed/glog" - "github.com/seaweedfs/seaweedfs/weed/worker/types" -) - -// Helper function to format seconds as duration string -func formatDurationFromSeconds(seconds int) string { - d := time.Duration(seconds) * time.Second - return d.String() -} - -// Helper functions to convert between seconds and value+unit format -func secondsToValueAndUnit(seconds int) (float64, string) { - if seconds == 0 { - return 0, "minutes" - } - - // Try days first - if seconds%(24*3600) == 0 && seconds >= 24*3600 { - return float64(seconds / (24 * 3600)), "days" - } - - // Try hours - if seconds%3600 == 0 && seconds >= 3600 { - return float64(seconds / 3600), "hours" - } - - // Default to minutes - return float64(seconds / 60), "minutes" -} - -func valueAndUnitToSeconds(value float64, unit string) int { - switch unit { - case "days": - return int(value * 24 * 3600) - case "hours": - return int(value * 3600) - case "minutes": - return int(value * 60) - default: - return int(value * 60) // Default to minutes - } -} - -// UITemplProvider provides the templ-based UI for balance task configuration -type UITemplProvider struct { - detector *BalanceDetector - scheduler *BalanceScheduler -} - -// NewUITemplProvider creates a new balance templ UI provider -func NewUITemplProvider(detector *BalanceDetector, scheduler *BalanceScheduler) *UITemplProvider { - return &UITemplProvider{ - detector: detector, - scheduler: scheduler, - } -} - -// GetTaskType returns the task type -func (ui *UITemplProvider) GetTaskType() types.TaskType { - return types.TaskTypeBalance -} - -// GetDisplayName returns the human-readable name -func (ui *UITemplProvider) GetDisplayName() string { - return "Volume Balance" -} - -// GetDescription returns a description of what this task does -func (ui *UITemplProvider) GetDescription() string { - return "Redistributes volumes across volume servers to optimize storage utilization and performance" -} - -// GetIcon returns the icon CSS class for this task type -func (ui *UITemplProvider) GetIcon() string { - return "fas fa-balance-scale text-secondary" -} - -// RenderConfigSections renders the configuration as templ section data -func (ui *UITemplProvider) RenderConfigSections(currentConfig interface{}) ([]components.ConfigSectionData, error) { - config := ui.getCurrentBalanceConfig() - - // Detection settings section - detectionSection := components.ConfigSectionData{ - Title: "Detection Settings", - Icon: "fas fa-search", - Description: "Configure when balance tasks should be triggered", - Fields: []interface{}{ - components.CheckboxFieldData{ - FormFieldData: components.FormFieldData{ - Name: "enabled", - Label: "Enable Balance Tasks", - Description: "Whether balance tasks should be automatically created", - }, - Checked: config.Enabled, - }, - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "imbalance_threshold", - Label: "Imbalance Threshold", - Description: "Trigger balance when storage imbalance exceeds this percentage (0.0-1.0)", - Required: true, - }, - Value: config.ImbalanceThreshold, - Step: "0.01", - Min: floatPtr(0.0), - Max: floatPtr(1.0), - }, - components.DurationInputFieldData{ - FormFieldData: components.FormFieldData{ - Name: "scan_interval", - Label: "Scan Interval", - Description: "How often to scan for imbalanced volumes", - Required: true, - }, - Seconds: config.ScanIntervalSeconds, - }, - }, - } - - // Scheduling settings section - schedulingSection := components.ConfigSectionData{ - Title: "Scheduling Settings", - Icon: "fas fa-clock", - Description: "Configure task scheduling and concurrency", - Fields: []interface{}{ - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "max_concurrent", - Label: "Max Concurrent Tasks", - Description: "Maximum number of balance tasks that can run simultaneously", - Required: true, - }, - Value: float64(config.MaxConcurrent), - Step: "1", - Min: floatPtr(1), - }, - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "min_server_count", - Label: "Minimum Server Count", - Description: "Only balance when at least this many servers are available", - Required: true, - }, - Value: float64(config.MinServerCount), - Step: "1", - Min: floatPtr(1), - }, - }, - } - - // Timing constraints section - timingSection := components.ConfigSectionData{ - Title: "Timing Constraints", - Icon: "fas fa-calendar-clock", - Description: "Configure when balance operations are allowed", - Fields: []interface{}{ - components.CheckboxFieldData{ - FormFieldData: components.FormFieldData{ - Name: "move_during_off_hours", - Label: "Restrict to Off-Hours", - Description: "Only perform balance operations during off-peak hours", - }, - Checked: config.MoveDuringOffHours, - }, - components.TextFieldData{ - FormFieldData: components.FormFieldData{ - Name: "off_hours_start", - Label: "Off-Hours Start Time", - Description: "Start time for off-hours window (e.g., 23:00)", - }, - Value: config.OffHoursStart, - }, - components.TextFieldData{ - FormFieldData: components.FormFieldData{ - Name: "off_hours_end", - Label: "Off-Hours End Time", - Description: "End time for off-hours window (e.g., 06:00)", - }, - Value: config.OffHoursEnd, - }, - }, - } - - // Performance impact info section - performanceSection := components.ConfigSectionData{ - Title: "Performance Considerations", - Icon: "fas fa-exclamation-triangle", - Description: "Important information about balance operations", - Fields: []interface{}{ - components.TextFieldData{ - FormFieldData: components.FormFieldData{ - Name: "performance_info", - Label: "Performance Impact", - Description: "Volume balancing involves data movement and can impact cluster performance", - }, - Value: "Enable off-hours restriction to minimize impact on production workloads", - }, - components.TextFieldData{ - FormFieldData: components.FormFieldData{ - Name: "safety_info", - Label: "Safety Requirements", - Description: fmt.Sprintf("Requires at least %d servers to ensure data safety during moves", config.MinServerCount), - }, - Value: "Maintains data safety during volume moves between servers", - }, - }, - } - - return []components.ConfigSectionData{detectionSection, schedulingSection, timingSection, performanceSection}, nil -} - -// ParseConfigForm parses form data into configuration -func (ui *UITemplProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) { - config := &BalanceConfig{} - - // Parse enabled checkbox - config.Enabled = len(formData["enabled"]) > 0 && formData["enabled"][0] == "on" - - // Parse imbalance threshold - if thresholdStr := formData["imbalance_threshold"]; len(thresholdStr) > 0 { - if threshold, err := strconv.ParseFloat(thresholdStr[0], 64); err != nil { - return nil, fmt.Errorf("invalid imbalance threshold: %v", err) - } else if threshold < 0 || threshold > 1 { - return nil, fmt.Errorf("imbalance threshold must be between 0.0 and 1.0") - } else { - config.ImbalanceThreshold = threshold - } - } - - // Parse scan interval - if valueStr := formData["scan_interval"]; len(valueStr) > 0 { - if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { - return nil, fmt.Errorf("invalid scan interval value: %v", err) - } else { - unit := "minutes" // default - if unitStr := formData["scan_interval_unit"]; len(unitStr) > 0 { - unit = unitStr[0] - } - config.ScanIntervalSeconds = valueAndUnitToSeconds(value, unit) - } - } - - // Parse max concurrent - if concurrentStr := formData["max_concurrent"]; len(concurrentStr) > 0 { - if concurrent, err := strconv.Atoi(concurrentStr[0]); err != nil { - return nil, fmt.Errorf("invalid max concurrent: %v", err) - } else if concurrent < 1 { - return nil, fmt.Errorf("max concurrent must be at least 1") - } else { - config.MaxConcurrent = concurrent - } - } - - // Parse min server count - if serverCountStr := formData["min_server_count"]; len(serverCountStr) > 0 { - if serverCount, err := strconv.Atoi(serverCountStr[0]); err != nil { - return nil, fmt.Errorf("invalid min server count: %v", err) - } else if serverCount < 1 { - return nil, fmt.Errorf("min server count must be at least 1") - } else { - config.MinServerCount = serverCount - } - } - - // Parse move during off hours - config.MoveDuringOffHours = len(formData["move_during_off_hours"]) > 0 && formData["move_during_off_hours"][0] == "on" - - // Parse off hours start time - if startStr := formData["off_hours_start"]; len(startStr) > 0 { - config.OffHoursStart = startStr[0] - } - - // Parse off hours end time - if endStr := formData["off_hours_end"]; len(endStr) > 0 { - config.OffHoursEnd = endStr[0] - } - - return config, nil -} - -// GetCurrentConfig returns the current configuration -func (ui *UITemplProvider) GetCurrentConfig() interface{} { - return ui.getCurrentBalanceConfig() -} - -// ApplyConfig applies the new configuration -func (ui *UITemplProvider) ApplyConfig(config interface{}) error { - balanceConfig, ok := config.(*BalanceConfig) - if !ok { - return fmt.Errorf("invalid config type, expected *BalanceConfig") - } - - // Apply to detector - if ui.detector != nil { - ui.detector.SetEnabled(balanceConfig.Enabled) - ui.detector.SetThreshold(balanceConfig.ImbalanceThreshold) - ui.detector.SetMinCheckInterval(time.Duration(balanceConfig.ScanIntervalSeconds) * time.Second) - } - - // Apply to scheduler - if ui.scheduler != nil { - ui.scheduler.SetEnabled(balanceConfig.Enabled) - ui.scheduler.SetMaxConcurrent(balanceConfig.MaxConcurrent) - ui.scheduler.SetMinServerCount(balanceConfig.MinServerCount) - ui.scheduler.SetMoveDuringOffHours(balanceConfig.MoveDuringOffHours) - ui.scheduler.SetOffHoursStart(balanceConfig.OffHoursStart) - ui.scheduler.SetOffHoursEnd(balanceConfig.OffHoursEnd) - } - - glog.V(1).Infof("Applied balance configuration: enabled=%v, threshold=%.1f%%, max_concurrent=%d, min_servers=%d, off_hours=%v", - balanceConfig.Enabled, balanceConfig.ImbalanceThreshold*100, balanceConfig.MaxConcurrent, - balanceConfig.MinServerCount, balanceConfig.MoveDuringOffHours) - - return nil -} - -// getCurrentBalanceConfig gets the current configuration from detector and scheduler -func (ui *UITemplProvider) getCurrentBalanceConfig() *BalanceConfig { - config := &BalanceConfig{ - // Default values (fallback if detectors/schedulers are nil) - Enabled: true, - ImbalanceThreshold: 0.1, // 10% imbalance - ScanIntervalSeconds: int((4 * time.Hour).Seconds()), - MaxConcurrent: 1, - MinServerCount: 3, - MoveDuringOffHours: true, - OffHoursStart: "23:00", - OffHoursEnd: "06:00", - } - - // Get current values from detector - if ui.detector != nil { - config.Enabled = ui.detector.IsEnabled() - config.ImbalanceThreshold = ui.detector.GetThreshold() - config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds()) - } - - // Get current values from scheduler - if ui.scheduler != nil { - config.MaxConcurrent = ui.scheduler.GetMaxConcurrent() - config.MinServerCount = ui.scheduler.GetMinServerCount() - config.MoveDuringOffHours = ui.scheduler.GetMoveDuringOffHours() - config.OffHoursStart = ui.scheduler.GetOffHoursStart() - config.OffHoursEnd = ui.scheduler.GetOffHoursEnd() - } - - return config -} - -// floatPtr is a helper function to create float64 pointers -func floatPtr(f float64) *float64 { - return &f -} - -// RegisterUITempl registers the balance templ UI provider with the UI registry -func RegisterUITempl(uiRegistry *types.UITemplRegistry, detector *BalanceDetector, scheduler *BalanceScheduler) { - uiProvider := NewUITemplProvider(detector, scheduler) - uiRegistry.RegisterUI(uiProvider) - - glog.V(1).Infof("✅ Registered balance task templ UI provider") -} diff --git a/weed/worker/tasks/erasure_coding/ui_templ.go b/weed/worker/tasks/erasure_coding/ui_templ.go deleted file mode 100644 index 12c3d199e..000000000 --- a/weed/worker/tasks/erasure_coding/ui_templ.go +++ /dev/null @@ -1,319 +0,0 @@ -package erasure_coding - -import ( - "fmt" - "strconv" - "time" - - "github.com/seaweedfs/seaweedfs/weed/admin/view/components" - "github.com/seaweedfs/seaweedfs/weed/glog" - "github.com/seaweedfs/seaweedfs/weed/worker/types" -) - -// Helper function to format seconds as duration string -func formatDurationFromSeconds(seconds int) string { - d := time.Duration(seconds) * time.Second - return d.String() -} - -// Helper function to convert value and unit to seconds -func valueAndUnitToSeconds(value float64, unit string) int { - switch unit { - case "days": - return int(value * 24 * 60 * 60) - case "hours": - return int(value * 60 * 60) - case "minutes": - return int(value * 60) - default: - return int(value * 60) // Default to minutes - } -} - -// UITemplProvider provides the templ-based UI for erasure coding task configuration -type UITemplProvider struct { - detector *EcDetector - scheduler *Scheduler -} - -// NewUITemplProvider creates a new erasure coding templ UI provider -func NewUITemplProvider(detector *EcDetector, scheduler *Scheduler) *UITemplProvider { - return &UITemplProvider{ - detector: detector, - scheduler: scheduler, - } -} - -// ErasureCodingConfig is defined in ui.go - we reuse it - -// GetTaskType returns the task type -func (ui *UITemplProvider) GetTaskType() types.TaskType { - return types.TaskTypeErasureCoding -} - -// GetDisplayName returns the human-readable name -func (ui *UITemplProvider) GetDisplayName() string { - return "Erasure Coding" -} - -// GetDescription returns a description of what this task does -func (ui *UITemplProvider) GetDescription() string { - return "Converts replicated volumes to erasure-coded format for efficient storage" -} - -// GetIcon returns the icon CSS class for this task type -func (ui *UITemplProvider) GetIcon() string { - return "fas fa-shield-alt text-info" -} - -// RenderConfigSections renders the configuration as templ section data -func (ui *UITemplProvider) RenderConfigSections(currentConfig interface{}) ([]components.ConfigSectionData, error) { - config := ui.getCurrentECConfig() - - // Detection settings section - detectionSection := components.ConfigSectionData{ - Title: "Detection Settings", - Icon: "fas fa-search", - Description: "Configure when erasure coding tasks should be triggered", - Fields: []interface{}{ - components.CheckboxFieldData{ - FormFieldData: components.FormFieldData{ - Name: "enabled", - Label: "Enable Erasure Coding Tasks", - Description: "Whether erasure coding tasks should be automatically created", - }, - Checked: config.Enabled, - }, - components.DurationInputFieldData{ - FormFieldData: components.FormFieldData{ - Name: "scan_interval", - Label: "Scan Interval", - Description: "How often to scan for volumes needing erasure coding", - Required: true, - }, - Seconds: config.ScanIntervalSeconds, - }, - components.DurationInputFieldData{ - FormFieldData: components.FormFieldData{ - Name: "volume_age_threshold", - Label: "Volume Age Threshold", - Description: "Only apply erasure coding to volumes older than this age", - Required: true, - }, - Seconds: config.VolumeAgeHoursSeconds, - }, - }, - } - - // Erasure coding parameters section - paramsSection := components.ConfigSectionData{ - Title: "Erasure Coding Parameters", - Icon: "fas fa-cogs", - Description: "Configure erasure coding scheme and performance", - Fields: []interface{}{ - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "data_shards", - Label: "Data Shards", - Description: "Number of data shards in the erasure coding scheme", - Required: true, - }, - Value: float64(config.ShardCount), - Step: "1", - Min: floatPtr(1), - Max: floatPtr(16), - }, - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "parity_shards", - Label: "Parity Shards", - Description: "Number of parity shards (determines fault tolerance)", - Required: true, - }, - Value: float64(config.ParityCount), - Step: "1", - Min: floatPtr(1), - Max: floatPtr(16), - }, - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "max_concurrent", - Label: "Max Concurrent Tasks", - Description: "Maximum number of erasure coding tasks that can run simultaneously", - Required: true, - }, - Value: float64(config.MaxConcurrent), - Step: "1", - Min: floatPtr(1), - }, - }, - } - - // Performance impact info section - infoSection := components.ConfigSectionData{ - Title: "Performance Impact", - Icon: "fas fa-info-circle", - Description: "Important information about erasure coding operations", - Fields: []interface{}{ - components.TextFieldData{ - FormFieldData: components.FormFieldData{ - Name: "durability_info", - Label: "Durability", - Description: fmt.Sprintf("With %d+%d configuration, can tolerate up to %d shard failures", - config.ShardCount, config.ParityCount, config.ParityCount), - }, - Value: "High durability with space efficiency", - }, - components.TextFieldData{ - FormFieldData: components.FormFieldData{ - Name: "performance_info", - Label: "Performance Note", - Description: "Erasure coding is CPU and I/O intensive. Consider running during off-peak hours", - }, - Value: "Schedule during low-traffic periods", - }, - }, - } - - return []components.ConfigSectionData{detectionSection, paramsSection, infoSection}, nil -} - -// ParseConfigForm parses form data into configuration -func (ui *UITemplProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) { - config := &ErasureCodingConfig{} - - // Parse enabled checkbox - config.Enabled = len(formData["enabled"]) > 0 && formData["enabled"][0] == "on" - - // Parse volume age threshold - if valueStr := formData["volume_age_threshold"]; len(valueStr) > 0 { - if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { - return nil, fmt.Errorf("invalid volume age threshold value: %v", err) - } else { - unit := "hours" // default - if unitStr := formData["volume_age_threshold_unit"]; len(unitStr) > 0 { - unit = unitStr[0] - } - config.VolumeAgeHoursSeconds = valueAndUnitToSeconds(value, unit) - } - } - - // Parse scan interval - if valueStr := formData["scan_interval"]; len(valueStr) > 0 { - if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { - return nil, fmt.Errorf("invalid scan interval value: %v", err) - } else { - unit := "hours" // default - if unitStr := formData["scan_interval_unit"]; len(unitStr) > 0 { - unit = unitStr[0] - } - config.ScanIntervalSeconds = valueAndUnitToSeconds(value, unit) - } - } - - // Parse data shards - if shardsStr := formData["data_shards"]; len(shardsStr) > 0 { - if shards, err := strconv.Atoi(shardsStr[0]); err != nil { - return nil, fmt.Errorf("invalid data shards: %v", err) - } else if shards < 1 || shards > 16 { - return nil, fmt.Errorf("data shards must be between 1 and 16") - } else { - config.ShardCount = shards - } - } - - // Parse parity shards - if shardsStr := formData["parity_shards"]; len(shardsStr) > 0 { - if shards, err := strconv.Atoi(shardsStr[0]); err != nil { - return nil, fmt.Errorf("invalid parity shards: %v", err) - } else if shards < 1 || shards > 16 { - return nil, fmt.Errorf("parity shards must be between 1 and 16") - } else { - config.ParityCount = shards - } - } - - // Parse max concurrent - if concurrentStr := formData["max_concurrent"]; len(concurrentStr) > 0 { - if concurrent, err := strconv.Atoi(concurrentStr[0]); err != nil { - return nil, fmt.Errorf("invalid max concurrent: %v", err) - } else if concurrent < 1 { - return nil, fmt.Errorf("max concurrent must be at least 1") - } else { - config.MaxConcurrent = concurrent - } - } - - return config, nil -} - -// GetCurrentConfig returns the current configuration -func (ui *UITemplProvider) GetCurrentConfig() interface{} { - return ui.getCurrentECConfig() -} - -// ApplyConfig applies the new configuration -func (ui *UITemplProvider) ApplyConfig(config interface{}) error { - ecConfig, ok := config.(*ErasureCodingConfig) - if !ok { - return fmt.Errorf("invalid config type, expected *ErasureCodingConfig") - } - - // Apply to detector - if ui.detector != nil { - ui.detector.SetEnabled(ecConfig.Enabled) - ui.detector.SetVolumeAgeHours(ecConfig.VolumeAgeHoursSeconds) - ui.detector.SetScanInterval(time.Duration(ecConfig.ScanIntervalSeconds) * time.Second) - } - - // Apply to scheduler - if ui.scheduler != nil { - ui.scheduler.SetMaxConcurrent(ecConfig.MaxConcurrent) - ui.scheduler.SetEnabled(ecConfig.Enabled) - } - - glog.V(1).Infof("Applied erasure coding configuration: enabled=%v, age_threshold=%ds, max_concurrent=%d", - ecConfig.Enabled, ecConfig.VolumeAgeHoursSeconds, ecConfig.MaxConcurrent) - - return nil -} - -// getCurrentECConfig gets the current configuration from detector and scheduler -func (ui *UITemplProvider) getCurrentECConfig() *ErasureCodingConfig { - config := &ErasureCodingConfig{ - // Default values (fallback if detectors/schedulers are nil) - Enabled: true, - VolumeAgeHoursSeconds: int((24 * time.Hour).Seconds()), - ScanIntervalSeconds: int((2 * time.Hour).Seconds()), - MaxConcurrent: 1, - ShardCount: 10, - ParityCount: 4, - } - - // Get current values from detector - if ui.detector != nil { - config.Enabled = ui.detector.IsEnabled() - config.VolumeAgeHoursSeconds = ui.detector.GetVolumeAgeHours() - config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds()) - } - - // Get current values from scheduler - if ui.scheduler != nil { - config.MaxConcurrent = ui.scheduler.GetMaxConcurrent() - } - - return config -} - -// floatPtr is a helper function to create float64 pointers -func floatPtr(f float64) *float64 { - return &f -} - -// RegisterUITempl registers the erasure coding templ UI provider with the UI registry -func RegisterUITempl(uiRegistry *types.UITemplRegistry, detector *EcDetector, scheduler *Scheduler) { - uiProvider := NewUITemplProvider(detector, scheduler) - uiRegistry.RegisterUI(uiProvider) - - glog.V(1).Infof("✅ Registered erasure coding task templ UI provider") -} diff --git a/weed/worker/tasks/vacuum/ui_templ.go b/weed/worker/tasks/vacuum/ui_templ.go deleted file mode 100644 index 15558b832..000000000 --- a/weed/worker/tasks/vacuum/ui_templ.go +++ /dev/null @@ -1,330 +0,0 @@ -package vacuum - -import ( - "fmt" - "strconv" - "time" - - "github.com/seaweedfs/seaweedfs/weed/admin/view/components" - "github.com/seaweedfs/seaweedfs/weed/glog" - "github.com/seaweedfs/seaweedfs/weed/worker/types" -) - -// Helper function to format seconds as duration string -func formatDurationFromSeconds(seconds int) string { - d := time.Duration(seconds) * time.Second - return d.String() -} - -// Helper functions to convert between seconds and value+unit format -func secondsToValueAndUnit(seconds int) (float64, string) { - if seconds == 0 { - return 0, "minutes" - } - - // Try days first - if seconds%(24*3600) == 0 && seconds >= 24*3600 { - return float64(seconds / (24 * 3600)), "days" - } - - // Try hours - if seconds%3600 == 0 && seconds >= 3600 { - return float64(seconds / 3600), "hours" - } - - // Default to minutes - return float64(seconds / 60), "minutes" -} - -func valueAndUnitToSeconds(value float64, unit string) int { - switch unit { - case "days": - return int(value * 24 * 3600) - case "hours": - return int(value * 3600) - case "minutes": - return int(value * 60) - default: - return int(value * 60) // Default to minutes - } -} - -// UITemplProvider provides the templ-based UI for vacuum task configuration -type UITemplProvider struct { - detector *VacuumDetector - scheduler *VacuumScheduler -} - -// NewUITemplProvider creates a new vacuum templ UI provider -func NewUITemplProvider(detector *VacuumDetector, scheduler *VacuumScheduler) *UITemplProvider { - return &UITemplProvider{ - detector: detector, - scheduler: scheduler, - } -} - -// GetTaskType returns the task type -func (ui *UITemplProvider) GetTaskType() types.TaskType { - return types.TaskTypeVacuum -} - -// GetDisplayName returns the human-readable name -func (ui *UITemplProvider) GetDisplayName() string { - return "Volume Vacuum" -} - -// GetDescription returns a description of what this task does -func (ui *UITemplProvider) GetDescription() string { - return "Reclaims disk space by removing deleted files from volumes" -} - -// GetIcon returns the icon CSS class for this task type -func (ui *UITemplProvider) GetIcon() string { - return "fas fa-broom text-primary" -} - -// RenderConfigSections renders the configuration as templ section data -func (ui *UITemplProvider) RenderConfigSections(currentConfig interface{}) ([]components.ConfigSectionData, error) { - config := ui.getCurrentVacuumConfig() - - // Detection settings section - detectionSection := components.ConfigSectionData{ - Title: "Detection Settings", - Icon: "fas fa-search", - Description: "Configure when vacuum tasks should be triggered", - Fields: []interface{}{ - components.CheckboxFieldData{ - FormFieldData: components.FormFieldData{ - Name: "enabled", - Label: "Enable Vacuum Tasks", - Description: "Whether vacuum tasks should be automatically created", - }, - Checked: config.Enabled, - }, - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "garbage_threshold", - Label: "Garbage Threshold", - Description: "Trigger vacuum when garbage ratio exceeds this percentage (0.0-1.0)", - Required: true, - }, - Value: config.GarbageThreshold, - Step: "0.01", - Min: floatPtr(0.0), - Max: floatPtr(1.0), - }, - components.DurationInputFieldData{ - FormFieldData: components.FormFieldData{ - Name: "scan_interval", - Label: "Scan Interval", - Description: "How often to scan for volumes needing vacuum", - Required: true, - }, - Seconds: config.ScanIntervalSeconds, - }, - components.DurationInputFieldData{ - FormFieldData: components.FormFieldData{ - Name: "min_volume_age", - Label: "Minimum Volume Age", - Description: "Only vacuum volumes older than this duration", - Required: true, - }, - Seconds: config.MinVolumeAgeSeconds, - }, - }, - } - - // Scheduling settings section - schedulingSection := components.ConfigSectionData{ - Title: "Scheduling Settings", - Icon: "fas fa-clock", - Description: "Configure task scheduling and concurrency", - Fields: []interface{}{ - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "max_concurrent", - Label: "Max Concurrent Tasks", - Description: "Maximum number of vacuum tasks that can run simultaneously", - Required: true, - }, - Value: float64(config.MaxConcurrent), - Step: "1", - Min: floatPtr(1), - }, - components.DurationInputFieldData{ - FormFieldData: components.FormFieldData{ - Name: "min_interval", - Label: "Minimum Interval", - Description: "Minimum time between vacuum operations on the same volume", - Required: true, - }, - Seconds: config.MinIntervalSeconds, - }, - }, - } - - // Performance impact info section - performanceSection := components.ConfigSectionData{ - Title: "Performance Impact", - Icon: "fas fa-exclamation-triangle", - Description: "Important information about vacuum operations", - Fields: []interface{}{ - components.TextFieldData{ - FormFieldData: components.FormFieldData{ - Name: "info_impact", - Label: "Impact", - Description: "Volume vacuum operations are I/O intensive and should be scheduled appropriately", - }, - Value: "Configure thresholds and intervals based on your storage usage patterns", - }, - }, - } - - return []components.ConfigSectionData{detectionSection, schedulingSection, performanceSection}, nil -} - -// ParseConfigForm parses form data into configuration -func (ui *UITemplProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) { - config := &VacuumConfig{} - - // Parse enabled checkbox - config.Enabled = len(formData["enabled"]) > 0 && formData["enabled"][0] == "on" - - // Parse garbage threshold - if thresholdStr := formData["garbage_threshold"]; len(thresholdStr) > 0 { - if threshold, err := strconv.ParseFloat(thresholdStr[0], 64); err != nil { - return nil, fmt.Errorf("invalid garbage threshold: %v", err) - } else if threshold < 0 || threshold > 1 { - return nil, fmt.Errorf("garbage threshold must be between 0.0 and 1.0") - } else { - config.GarbageThreshold = threshold - } - } - - // Parse scan interval - if valueStr := formData["scan_interval"]; len(valueStr) > 0 { - if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { - return nil, fmt.Errorf("invalid scan interval value: %v", err) - } else { - unit := "minutes" // default - if unitStr := formData["scan_interval_unit"]; len(unitStr) > 0 { - unit = unitStr[0] - } - config.ScanIntervalSeconds = valueAndUnitToSeconds(value, unit) - } - } - - // Parse min volume age - if valueStr := formData["min_volume_age"]; len(valueStr) > 0 { - if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { - return nil, fmt.Errorf("invalid min volume age value: %v", err) - } else { - unit := "minutes" // default - if unitStr := formData["min_volume_age_unit"]; len(unitStr) > 0 { - unit = unitStr[0] - } - config.MinVolumeAgeSeconds = valueAndUnitToSeconds(value, unit) - } - } - - // Parse max concurrent - if concurrentStr := formData["max_concurrent"]; len(concurrentStr) > 0 { - if concurrent, err := strconv.Atoi(concurrentStr[0]); err != nil { - return nil, fmt.Errorf("invalid max concurrent: %v", err) - } else if concurrent < 1 { - return nil, fmt.Errorf("max concurrent must be at least 1") - } else { - config.MaxConcurrent = concurrent - } - } - - // Parse min interval - if valueStr := formData["min_interval"]; len(valueStr) > 0 { - if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { - return nil, fmt.Errorf("invalid min interval value: %v", err) - } else { - unit := "minutes" // default - if unitStr := formData["min_interval_unit"]; len(unitStr) > 0 { - unit = unitStr[0] - } - config.MinIntervalSeconds = valueAndUnitToSeconds(value, unit) - } - } - - return config, nil -} - -// GetCurrentConfig returns the current configuration -func (ui *UITemplProvider) GetCurrentConfig() interface{} { - return ui.getCurrentVacuumConfig() -} - -// ApplyConfig applies the new configuration -func (ui *UITemplProvider) ApplyConfig(config interface{}) error { - vacuumConfig, ok := config.(*VacuumConfig) - if !ok { - return fmt.Errorf("invalid config type, expected *VacuumConfig") - } - - // Apply to detector - if ui.detector != nil { - ui.detector.SetEnabled(vacuumConfig.Enabled) - ui.detector.SetGarbageThreshold(vacuumConfig.GarbageThreshold) - ui.detector.SetScanInterval(time.Duration(vacuumConfig.ScanIntervalSeconds) * time.Second) - ui.detector.SetMinVolumeAge(time.Duration(vacuumConfig.MinVolumeAgeSeconds) * time.Second) - } - - // Apply to scheduler - if ui.scheduler != nil { - ui.scheduler.SetEnabled(vacuumConfig.Enabled) - ui.scheduler.SetMaxConcurrent(vacuumConfig.MaxConcurrent) - ui.scheduler.SetMinInterval(time.Duration(vacuumConfig.MinIntervalSeconds) * time.Second) - } - - glog.V(1).Infof("Applied vacuum configuration: enabled=%v, threshold=%.1f%%, scan_interval=%s, max_concurrent=%d", - vacuumConfig.Enabled, vacuumConfig.GarbageThreshold*100, formatDurationFromSeconds(vacuumConfig.ScanIntervalSeconds), vacuumConfig.MaxConcurrent) - - return nil -} - -// getCurrentVacuumConfig gets the current configuration from detector and scheduler -func (ui *UITemplProvider) getCurrentVacuumConfig() *VacuumConfig { - config := &VacuumConfig{ - // Default values (fallback if detectors/schedulers are nil) - Enabled: true, - GarbageThreshold: 0.3, - ScanIntervalSeconds: int((30 * time.Minute).Seconds()), - MinVolumeAgeSeconds: int((1 * time.Hour).Seconds()), - MaxConcurrent: 2, - MinIntervalSeconds: int((6 * time.Hour).Seconds()), - } - - // Get current values from detector - if ui.detector != nil { - config.Enabled = ui.detector.IsEnabled() - config.GarbageThreshold = ui.detector.GetGarbageThreshold() - config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds()) - config.MinVolumeAgeSeconds = int(ui.detector.GetMinVolumeAge().Seconds()) - } - - // Get current values from scheduler - if ui.scheduler != nil { - config.MaxConcurrent = ui.scheduler.GetMaxConcurrent() - config.MinIntervalSeconds = int(ui.scheduler.GetMinInterval().Seconds()) - } - - return config -} - -// floatPtr is a helper function to create float64 pointers -func floatPtr(f float64) *float64 { - return &f -} - -// RegisterUITempl registers the vacuum templ UI provider with the UI registry -func RegisterUITempl(uiRegistry *types.UITemplRegistry, detector *VacuumDetector, scheduler *VacuumScheduler) { - uiProvider := NewUITemplProvider(detector, scheduler) - uiRegistry.RegisterUI(uiProvider) - - glog.V(1).Infof("✅ Registered vacuum task templ UI provider") -} diff --git a/weed/worker/types/task_ui_templ.go b/weed/worker/types/task_ui_templ.go deleted file mode 100644 index 77e80b408..000000000 --- a/weed/worker/types/task_ui_templ.go +++ /dev/null @@ -1,63 +0,0 @@ -package types - -import ( - "github.com/seaweedfs/seaweedfs/weed/admin/view/components" -) - -// TaskUITemplProvider defines how tasks provide their configuration UI using templ components -type TaskUITemplProvider interface { - // GetTaskType returns the task type - GetTaskType() TaskType - - // GetDisplayName returns the human-readable name - GetDisplayName() string - - // GetDescription returns a description of what this task does - GetDescription() string - - // GetIcon returns the icon CSS class or HTML for this task type - GetIcon() string - - // RenderConfigSections renders the configuration as templ section data - RenderConfigSections(currentConfig interface{}) ([]components.ConfigSectionData, error) - - // ParseConfigForm parses form data into configuration - ParseConfigForm(formData map[string][]string) (interface{}, error) - - // GetCurrentConfig returns the current configuration - GetCurrentConfig() interface{} - - // ApplyConfig applies the new configuration - ApplyConfig(config interface{}) error -} - -// UITemplRegistry manages task UI providers that use templ components -type UITemplRegistry struct { - providers map[TaskType]TaskUITemplProvider -} - -// NewUITemplRegistry creates a new templ-based UI registry -func NewUITemplRegistry() *UITemplRegistry { - return &UITemplRegistry{ - providers: make(map[TaskType]TaskUITemplProvider), - } -} - -// RegisterUI registers a task UI provider -func (r *UITemplRegistry) RegisterUI(provider TaskUITemplProvider) { - r.providers[provider.GetTaskType()] = provider -} - -// GetProvider returns the UI provider for a task type -func (r *UITemplRegistry) GetProvider(taskType TaskType) TaskUITemplProvider { - return r.providers[taskType] -} - -// GetAllProviders returns all registered UI providers -func (r *UITemplRegistry) GetAllProviders() map[TaskType]TaskUITemplProvider { - result := make(map[TaskType]TaskUITemplProvider) - for k, v := range r.providers { - result[k] = v - } - return result -}