diff --git a/go.mod b/go.mod index 3f65c20d..9eef0364 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ require ( github.com/rs/xid v1.5.0 github.com/rs/zerolog v1.31.0 github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c - github.com/shirou/gopsutil/v3 v3.23.12 + github.com/shirou/gopsutil/v3 v3.24.1 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 diff --git a/go.sum b/go.sum index ec8c3e9b..ab872297 100644 --- a/go.sum +++ b/go.sum @@ -352,8 +352,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c h1:07TYPvNbOnmKsBxjNsUr+gsILIUWflw1UYwjn1jognM= github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c/go.mod h1:AWoY2YYe/P1ymfTlRER/meERQjCcZZTbgVPGcPQgaqc= -github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= -github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI= +github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= diff --git a/internal/dataprovider/eventrule.go b/internal/dataprovider/eventrule.go index f013969a..e3852da5 100644 --- a/internal/dataprovider/eventrule.go +++ b/internal/dataprovider/eventrule.go @@ -117,19 +117,19 @@ func isEventTriggerValid(trigger int) bool { func getTriggerTypeAsString(trigger int) string { switch trigger { case EventTriggerFsEvent: - return "Filesystem event" + return util.I18nTriggerFsEvent case EventTriggerProviderEvent: - return "Provider event" + return util.I18nTriggerProviderEvent case EventTriggerIPBlocked: - return "IP blocked" + return util.I18nTriggerIPBlockedEvent case EventTriggerCertificate: - return "Certificate renewal" + return util.I18nTriggerCertificateRenewEvent case EventTriggerOnDemand: - return "On demand" + return util.I18nTriggerOnDemandEvent case EventTriggerIDPLogin: - return "Identity Provider login" + return util.I18nTriggerIDPLoginEvent default: - return "Schedule" + return util.I18nTriggerScheduleEvent } } @@ -1212,17 +1212,26 @@ func (a *EventAction) getACopy() EventAction { func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error { if a.Options.IsFailureAction { if a.Options.ExecuteSync { - return util.NewValidationError("sync execution is not supported for failure actions") + return util.NewI18nError( + util.NewValidationError("sync execution is not supported for failure actions"), + util.I18nErrorEvSyncFailureActions, + ) } } if a.Options.ExecuteSync { if trigger != EventTriggerFsEvent && trigger != EventTriggerIDPLogin { - return util.NewValidationError("sync execution is only supported for some filesystem events and Identity Provider logins") + return util.NewI18nError( + util.NewValidationError("sync execution is only supported for some filesystem events and Identity Provider logins"), + util.I18nErrorEvSyncUnsupported, + ) } if trigger == EventTriggerFsEvent { for _, ev := range fsEvents { if !util.Contains(allowedSyncFsEvents, ev) { - return util.NewValidationError("sync execution is only supported for upload and pre-* events") + return util.NewI18nError( + util.NewValidationError("sync execution is only supported for upload and pre-* events"), + util.I18nErrorEvSyncUnsupportedFs, + ) } } } @@ -1379,11 +1388,14 @@ func (c *EventConditions) getACopy() EventConditions { func (c *EventConditions) validateSchedules() error { if len(c.Schedules) == 0 { - return util.NewValidationError("at least one schedule is required") + return util.NewI18nError( + util.NewValidationError("at least one schedule is required"), + util.I18nErrorRuleScheduleRequired, + ) } for _, schedule := range c.Schedules { if err := schedule.validate(); err != nil { - return err + return util.NewI18nError(err, util.I18nErrorRuleScheduleInvalid) } } return nil @@ -1397,7 +1409,10 @@ func (c *EventConditions) validate(trigger int) error { c.Options.ProviderObjects = nil c.IDPLoginEvent = 0 if len(c.FsEvents) == 0 { - return util.NewValidationError("at least one filesystem event is required") + return util.NewI18nError( + util.NewValidationError("at least one filesystem event is required"), + util.I18nErrorRuleFsEventRequired, + ) } for _, ev := range c.FsEvents { if !util.Contains(SupportedFsEvents, ev) { @@ -1414,7 +1429,10 @@ func (c *EventConditions) validate(trigger int) error { c.Options.MaxFileSize = 0 c.IDPLoginEvent = 0 if len(c.ProviderEvents) == 0 { - return util.NewValidationError("at least one provider event is required") + return util.NewI18nError( + util.NewValidationError("at least one provider event is required"), + util.I18nErrorRuleProviderEventRequired, + ) } for _, ev := range c.ProviderEvents { if !util.Contains(SupportedProviderEvents, ev) { @@ -1558,7 +1576,7 @@ func (r *EventRule) isStatusValid() bool { func (r *EventRule) validate() error { if r.Name == "" { - return util.NewValidationError("name is mandatory") + return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired) } if !r.isStatusValid() { return util.NewValidationError(fmt.Sprintf("invalid event rule status: %d", r.Status)) @@ -1570,7 +1588,7 @@ func (r *EventRule) validate() error { return err } if len(r.Actions) == 0 { - return util.NewValidationError("at least one action is required") + return util.NewI18nError(util.NewValidationError("at least one action is required"), util.I18nErrorRuleActionRequired) } actionNames := make(map[string]bool) actionOrders := make(map[int]bool) @@ -1581,7 +1599,10 @@ func (r *EventRule) validate() error { return util.NewValidationError(fmt.Sprintf("invalid action at position %d, name not specified", idx)) } if actionNames[r.Actions[idx].Name] { - return util.NewValidationError(fmt.Sprintf("duplicated action %q", r.Actions[idx].Name)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("duplicated action %q", r.Actions[idx].Name)), + util.I18nErrorRuleDuplicateActions, + ) } if actionOrders[r.Actions[idx].Order] { return util.NewValidationError(fmt.Sprintf("duplicated order %d for action %q", @@ -1600,7 +1621,10 @@ func (r *EventRule) validate() error { actionOrders[r.Actions[idx].Order] = true } if len(r.Actions) == failureActions { - return util.NewValidationError("at least a non-failure action is required") + return util.NewI18nError( + util.NewValidationError("at least a non-failure action is required"), + util.I18nErrorRuleFailureActionsOnly, + ) } if !hasSyncAction { return r.validateMandatorySyncActions() @@ -1614,7 +1638,13 @@ func (r *EventRule) validateMandatorySyncActions() error { } for _, ev := range r.Conditions.FsEvents { if util.Contains(mandatorySyncFsEvents, ev) { - return util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev)), + util.I18nErrorRuleSyncActionRequired, + util.I18nErrorArgs(map[string]any{ + "val": ev, + }), + ) } } return nil diff --git a/internal/httpd/server.go b/internal/httpd/server.go index 470ab1bb..637c8cc0 100644 --- a/internal/httpd/server.go +++ b/internal/httpd/server.go @@ -1775,6 +1775,8 @@ func (s *httpdServer) setupWebAdminRoutes() { s.handleWebUpdateEventActionPost) router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader). Delete(webAdminEventActionPath+"/{name}", deleteEventAction) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie). + Get(webAdminEventRulesPath+jsonAPISuffix, getAllRules) router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). Get(webAdminEventRulesPath, s.handleWebGetEventRules) router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index 3fb39183..e4844a0e 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -98,7 +98,6 @@ const ( templateMaintenance = "maintenance.html" templateMFA = "mfa.html" templateSetup = "adminsetup.html" - pageEventRulesTitle = "Event rules" defaultQueryLimit = 1000 inversePatternType = "inverse" ) @@ -153,11 +152,6 @@ type basePage struct { Branding UIBranding } -type eventRulesPage struct { - basePage - Rules []dataprovider.EventRule -} - type statusPage struct { basePage Status *ServicesStatus @@ -312,7 +306,7 @@ type eventRulePage struct { Protocols []string ProviderEvents []string ProviderObjects []string - Error string + Error *util.I18nError Mode genericPageMode IsShared bool } @@ -1105,19 +1099,19 @@ func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Reque } func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request, rule dataprovider.EventRule, - mode genericPageMode, error string, + mode genericPageMode, err error, ) { - actions, err := s.getWebEventActions(w, r, defaultQueryLimit, true) - if err != nil { + actions, errActions := s.getWebEventActions(w, r, defaultQueryLimit, true) + if errActions != nil { return } var title, currentURL string switch mode { case genericPageModeAdd: - title = "Add new event rules" + title = util.I18nAddRuleTitle currentURL = webAdminEventRulePath case genericPageModeUpdate: - title = "Update event rules" + title = util.I18nUpdateRuleTitle currentURL = fmt.Sprintf("%v/%v", webAdminEventRulePath, url.PathEscape(rule.Name)) } @@ -1130,7 +1124,7 @@ func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request Protocols: dataprovider.SupportedRuleConditionProtocols, ProviderEvents: dataprovider.SupportedProviderEvents, ProviderObjects: dataprovider.SupporteRuleConditionProviderObjects, - Error: error, + Error: getI18nError(err), Mode: mode, IsShared: s.isShared > 0, } @@ -2420,74 +2414,66 @@ func getIDPLoginEventFromPostField(r *http.Request) int { func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) { var schedules []dataprovider.Schedule var names, groupNames, roleNames, fsPaths []dataprovider.ConditionPattern - for k := range r.Form { - if strings.HasPrefix(k, "schedule_hour") { - hour := strings.TrimSpace(r.Form.Get(k)) - if hour != "" { - idx := strings.TrimPrefix(k, "schedule_hour") - dayOfWeek := strings.TrimSpace(r.Form.Get(fmt.Sprintf("schedule_day_of_week%s", idx))) - dayOfMonth := strings.TrimSpace(r.Form.Get(fmt.Sprintf("schedule_day_of_month%s", idx))) - month := strings.TrimSpace(r.Form.Get(fmt.Sprintf("schedule_month%s", idx))) - schedules = append(schedules, dataprovider.Schedule{ - Hours: hour, - DayOfWeek: dayOfWeek, - DayOfMonth: dayOfMonth, - Month: month, - }) - } - } - if strings.HasPrefix(k, "name_pattern") { - pattern := strings.TrimSpace(r.Form.Get(k)) - if pattern != "" { - idx := strings.TrimPrefix(k, "name_pattern") - patternType := r.Form.Get(fmt.Sprintf("type_name_pattern%s", idx)) - names = append(names, dataprovider.ConditionPattern{ - Pattern: pattern, - InverseMatch: patternType == inversePatternType, - }) - } - } - if strings.HasPrefix(k, "group_name_pattern") { - pattern := strings.TrimSpace(r.Form.Get(k)) - if pattern != "" { - idx := strings.TrimPrefix(k, "group_name_pattern") - patternType := r.Form.Get(fmt.Sprintf("type_group_name_pattern%s", idx)) - groupNames = append(groupNames, dataprovider.ConditionPattern{ - Pattern: pattern, - InverseMatch: patternType == inversePatternType, - }) - } - } - if strings.HasPrefix(k, "role_name_pattern") { - pattern := strings.TrimSpace(r.Form.Get(k)) - if pattern != "" { - idx := strings.TrimPrefix(k, "role_name_pattern") - patternType := r.Form.Get(fmt.Sprintf("type_role_name_pattern%s", idx)) - roleNames = append(roleNames, dataprovider.ConditionPattern{ - Pattern: pattern, - InverseMatch: patternType == inversePatternType, - }) - } - } - if strings.HasPrefix(k, "fs_path_pattern") { - pattern := strings.TrimSpace(r.Form.Get(k)) - if pattern != "" { - idx := strings.TrimPrefix(k, "fs_path_pattern") - patternType := r.Form.Get(fmt.Sprintf("type_fs_path_pattern%s", idx)) - fsPaths = append(fsPaths, dataprovider.ConditionPattern{ - Pattern: pattern, - InverseMatch: patternType == inversePatternType, - }) - } + + scheduleHours := r.Form["schedule_hour"] + scheduleDayOfWeeks := r.Form["schedule_day_of_week"] + scheduleDayOfMonths := r.Form["schedule_day_of_month"] + scheduleMonths := r.Form["schedule_month"] + + for idx, hour := range scheduleHours { + if hour != "" { + schedules = append(schedules, dataprovider.Schedule{ + Hours: hour, + DayOfWeek: scheduleDayOfWeeks[idx], + DayOfMonth: scheduleDayOfMonths[idx], + Month: scheduleMonths[idx], + }) } } + + for idx, name := range r.Form["name_pattern"] { + if name != "" { + names = append(names, dataprovider.ConditionPattern{ + Pattern: name, + InverseMatch: r.Form["type_name_pattern"][idx] == inversePatternType, + }) + } + } + + for idx, name := range r.Form["group_name_pattern"] { + if name != "" { + groupNames = append(groupNames, dataprovider.ConditionPattern{ + Pattern: name, + InverseMatch: r.Form["type_group_name_pattern"][idx] == inversePatternType, + }) + } + } + + for idx, name := range r.Form["role_name_pattern"] { + if name != "" { + roleNames = append(roleNames, dataprovider.ConditionPattern{ + Pattern: name, + InverseMatch: r.Form["type_role_name_pattern"][idx] == inversePatternType, + }) + } + } + + for idx, name := range r.Form["fs_path_pattern"] { + if name != "" { + fsPaths = append(fsPaths, dataprovider.ConditionPattern{ + Pattern: name, + InverseMatch: r.Form["type_fs_path_pattern"][idx] == inversePatternType, + }) + } + } + minFileSize, err := util.ParseBytes(r.Form.Get("fs_min_size")) if err != nil { - return dataprovider.EventConditions{}, fmt.Errorf("invalid min file size: %w", err) + return dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf("invalid min file size: %w", err), util.I18nErrorInvalidMinSize) } maxFileSize, err := util.ParseBytes(r.Form.Get("fs_max_size")) if err != nil { - return dataprovider.EventConditions{}, fmt.Errorf("invalid max file size: %w", err) + return dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf("invalid max file size: %w", err), util.I18nErrorInvalidMaxSize) } conditions := dataprovider.EventConditions{ FsEvents: r.Form["fs_events"], @@ -2511,38 +2497,86 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo func getEventRuleActionsFromPostFields(r *http.Request) ([]dataprovider.EventAction, error) { var actions []dataprovider.EventAction - for k := range r.Form { - if strings.HasPrefix(k, "action_name") { - name := strings.TrimSpace(r.Form.Get(k)) - if name != "" { - idx := strings.TrimPrefix(k, "action_name") - order, err := strconv.Atoi(r.Form.Get(fmt.Sprintf("action_order%s", idx))) - if err != nil { - return actions, fmt.Errorf("invalid order: %w", err) - } - options := r.Form[fmt.Sprintf("action_options%s", idx)] - actions = append(actions, dataprovider.EventAction{ - BaseEventAction: dataprovider.BaseEventAction{ - Name: name, - }, - Order: order + 1, - Options: dataprovider.EventActionOptions{ - IsFailureAction: util.Contains(options, "1"), - StopOnFailure: util.Contains(options, "2"), - ExecuteSync: util.Contains(options, "3"), - }, - }) + + names := r.Form["action_name"] + orders := r.Form["action_order"] + + for idx, name := range names { + if name != "" { + order, err := strconv.Atoi(orders[idx]) + if err != nil { + return actions, fmt.Errorf("invalid order: %w", err) } + options := r.Form["action_options"+strconv.Itoa(idx)] + actions = append(actions, dataprovider.EventAction{ + BaseEventAction: dataprovider.BaseEventAction{ + Name: name, + }, + Order: order + 1, + Options: dataprovider.EventActionOptions{ + IsFailureAction: util.Contains(options, "1"), + StopOnFailure: util.Contains(options, "2"), + ExecuteSync: util.Contains(options, "3"), + }, + }) } } + return actions, nil } +func updateRepeaterFormRuleFields(r *http.Request) { + for k := range r.Form { + if hasPrefixAndSuffix(k, "schedules[", "][schedule_hour]") { + base, _ := strings.CutSuffix(k, "[schedule_hour]") + r.Form.Add("schedule_hour", strings.TrimSpace(r.Form.Get(k))) + r.Form.Add("schedule_day_of_week", strings.TrimSpace(r.Form.Get(base+"[schedule_day_of_week]"))) + r.Form.Add("schedule_day_of_month", strings.TrimSpace(r.Form.Get(base+"[schedule_day_of_month]"))) + r.Form.Add("schedule_month", strings.TrimSpace(r.Form.Get(base+"[schedule_month]"))) + continue + } + if hasPrefixAndSuffix(k, "name_filters[", "][name_pattern]") { + base, _ := strings.CutSuffix(k, "[name_pattern]") + r.Form.Add("name_pattern", strings.TrimSpace(r.Form.Get(k))) + r.Form.Add("type_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_name_pattern]"))) + continue + } + if hasPrefixAndSuffix(k, "group_name_filters[", "][group_name_pattern]") { + base, _ := strings.CutSuffix(k, "[group_name_pattern]") + r.Form.Add("group_name_pattern", strings.TrimSpace(r.Form.Get(k))) + r.Form.Add("type_group_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_group_name_pattern]"))) + continue + } + if hasPrefixAndSuffix(k, "role_name_filters[", "][role_name_pattern]") { + base, _ := strings.CutSuffix(k, "[role_name_pattern]") + r.Form.Add("role_name_pattern", strings.TrimSpace(r.Form.Get(k))) + r.Form.Add("type_role_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_role_name_pattern]"))) + continue + } + if hasPrefixAndSuffix(k, "path_filters[", "][fs_path_pattern]") { + base, _ := strings.CutSuffix(k, "[fs_path_pattern]") + r.Form.Add("fs_path_pattern", strings.TrimSpace(r.Form.Get(k))) + r.Form.Add("type_fs_path_pattern", strings.TrimSpace(r.Form.Get(base+"[type_fs_path_pattern]"))) + continue + } + if hasPrefixAndSuffix(k, "actions[", "][action_name]") { + base, _ := strings.CutSuffix(k, "[action_name]") + order, _ := strings.CutPrefix(k, "actions[") + order, _ = strings.CutSuffix(order, "][action_name]") + r.Form.Add("action_name", strings.TrimSpace(r.Form.Get(k))) + r.Form["action_options"+strconv.Itoa(len(r.Form["action_name"])-1)] = r.Form[base+"[action_options][]"] + r.Form.Add("action_order", order) + continue + } + } +} + func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error) { err := r.ParseForm() if err != nil { return dataprovider.EventRule{}, util.NewI18nError(err, util.I18nErrorInvalidForm) } + updateRepeaterFormRuleFields(r) status, err := strconv.Atoi(r.Form.Get("status")) if err != nil { return dataprovider.EventRule{}, fmt.Errorf("invalid status: %w", err) @@ -3789,31 +3823,27 @@ func (s *httpdServer) handleWebUpdateEventActionPost(w http.ResponseWriter, r *h http.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther) } -func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) { +func getAllRules(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - limit := defaultQueryLimit - if _, ok := r.URL.Query()["qlimit"]; ok { - if lim, err := strconv.Atoi(r.URL.Query().Get("qlimit")); err == nil { - limit = lim - } - } - rules := make([]dataprovider.EventRule, 0, limit) + rules := make([]dataprovider.EventRule, 0, 10) for { - res, err := dataprovider.GetEventRules(limit, len(rules), dataprovider.OrderASC) + res, err := dataprovider.GetEventRules(defaultQueryLimit, len(rules), dataprovider.OrderASC) if err != nil { - s.renderInternalServerErrorPage(w, r, err) + sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), http.StatusInternalServerError) return } rules = append(rules, res...) - if len(res) < limit { + if len(res) < defaultQueryLimit { break } } + render.JSON(w, r, rules) +} - data := eventRulesPage{ - basePage: s.getBasePageData(pageEventRulesTitle, webAdminEventRulesPath, r), - Rules: rules, - } +func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + + data := s.getBasePageData(util.I18nRulesTitle, webAdminEventRulesPath, r) renderAdminTemplate(w, templateEventRules, data) } @@ -3823,7 +3853,7 @@ func (s *httpdServer) handleWebAddEventRuleGet(w http.ResponseWriter, r *http.Re Status: 1, Trigger: dataprovider.EventTriggerFsEvent, } - s.renderEventRulePage(w, r, rule, genericPageModeAdd, "") + s.renderEventRulePage(w, r, rule, genericPageModeAdd, nil) } func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.Request) { @@ -3835,7 +3865,7 @@ func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.R } rule, err := getEventRuleFromPostFields(r) if err != nil { - s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error()) + s.renderEventRulePage(w, r, rule, genericPageModeAdd, err) return } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) @@ -3845,7 +3875,7 @@ func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.R return } if err = dataprovider.AddEventRule(&rule, claims.Username, ipAddr, claims.Role); err != nil { - s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error()) + s.renderEventRulePage(w, r, rule, genericPageModeAdd, err) return } http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther) @@ -3856,7 +3886,7 @@ func (s *httpdServer) handleWebUpdateEventRuleGet(w http.ResponseWriter, r *http name := getURLParam(r, "name") rule, err := dataprovider.EventRuleExists(name) if err == nil { - s.renderEventRulePage(w, r, rule, genericPageModeUpdate, "") + s.renderEventRulePage(w, r, rule, genericPageModeUpdate, nil) } else if errors.Is(err, util.ErrNotFound) { s.renderNotFoundPage(w, r, err) } else { @@ -3882,7 +3912,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt } updatedRule, err := getEventRuleFromPostFields(r) if err != nil { - s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error()) + s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err) return } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) @@ -3894,7 +3924,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt updatedRule.Name = rule.Name err = dataprovider.UpdateEventRule(&updatedRule, claims.Username, ipAddr, claims.Role) if err != nil { - s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error()) + s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err) return } http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther) diff --git a/internal/util/i18n.go b/internal/util/i18n.go index fb33dc4d..f7c37828 100644 --- a/internal/util/i18n.go +++ b/internal/util/i18n.go @@ -69,8 +69,11 @@ const ( I18nDefenderTitle = "title.defender" I18nEventsTitle = "title.logs" I18nActionsTitle = "title.event_actions" + I18nRulesTitle = "title.event_rules" I18nAddActionTitle = "title.add_action" I18nUpdateActionTitle = "title.update_action" + I18nAddRuleTitle = "title.add_rule" + I18nUpdateRuleTitle = "title.update_rule" I18nStatusTitle = "status.desc" I18nErrorSetupInstallCode = "setup.install_code_mismatch" I18nInvalidAuth = "general.invalid_auth_request" @@ -278,6 +281,26 @@ const ( I18nActionFsTypeCompress = "actions.fs_types.compress" I18nActionFsTypeCopy = "actions.fs_types.copy" I18nActionFsTypeCreateDirs = "actions.fs_types.create_dirs" + I18nTriggerFsEvent = "rules.triggers.fs_event" + I18nTriggerProviderEvent = "rules.triggers.provider_event" + I18nTriggerIPBlockedEvent = "rules.triggers.ip_blocked" + I18nTriggerCertificateRenewEvent = "rules.triggers.certificate_renewal" + I18nTriggerOnDemandEvent = "rules.triggers.on_demand" + I18nTriggerIDPLoginEvent = "rules.triggers.idp_login" + I18nTriggerScheduleEvent = "rules.triggers.schedule" + I18nErrorInvalidMinSize = "rules.invalid_fs_min_size" + I18nErrorInvalidMaxSize = "rules.invalid_fs_max_size" + I18nErrorRuleActionRequired = "rules.action_required" + I18nErrorRuleFsEventRequired = "rules.fs_event_required" + I18nErrorRuleProviderEventRequired = "rules.provider_event_required" + I18nErrorRuleScheduleRequired = "rules.schedule_required" + I18nErrorRuleScheduleInvalid = "rules.schedule_invalid" + I18nErrorRuleDuplicateActions = "rules.duplicate_actions" + I18nErrorEvSyncFailureActions = "rules.sync_failure_actions" + I18nErrorEvSyncUnsupported = "rules.sync_unsupported" + I18nErrorEvSyncUnsupportedFs = "rules.sync_unsupported_fs_event" + I18nErrorRuleFailureActionsOnly = "rules.only_failure_actions" + I18nErrorRuleSyncActionRequired = "rules.sync_action_required" ) // NewI18nError returns a I18nError wrappring the provided error diff --git a/static/locales/en/translation.json b/static/locales/en/translation.json index df84ab10..dfa9db5f 100644 --- a/static/locales/en/translation.json +++ b/static/locales/en/translation.json @@ -63,7 +63,9 @@ "add_ip_list": "Add IP list entry", "update_ip_list": "Update IP list entry", "add_action": "Add action", - "update_action": "Update action" + "update_action": "Update action", + "add_rule": "Add rule", + "update_rule": "Update rule" }, "setup": { "desc": "To start using SFTPGo you need to create an administrator user", @@ -252,7 +254,12 @@ "timeout": "Timeout", "env_vars": "Environment variables", "hours": "Hours", - "paths": "Paths" + "paths": "Paths", + "hour": "Hour", + "day_of_week": "Day of week", + "day_of_month": "Day of month", + "month": "Month", + "options": "Options" }, "fs": { "view_file": "View file \"{{- path}}\"", @@ -1003,5 +1010,63 @@ "metadata_string": "Cloud storage metadata for the downloaded file as JSON escaped string", "uid": "Unique ID" } + }, + "rules": { + "view_manage": "View and manage rules for events", + "trigger": "Trigger", + "run_confirm": "Do you want to execute the selected rule?", + "run_confirm_btn": "Yes, run", + "run_error_generic": "Unable to run the selected rule", + "run_ok": "Rule actions started", + "run": "Run", + "invalid_fs_min_size": "Invalid min size", + "invalid_fs_max_size": "Invalid max size", + "action_required": "At least one action is required", + "fs_event_required": "At least one filesystem event is required", + "provider_event_required": "At least one provider event is required", + "schedule_required": "At least one schedule is required", + "schedule_invalid": "Invalid schedule", + "duplicate_actions": "Duplicate actions detected", + "sync_failure_actions": "Synchronous execution is not supported for failure actions", + "sync_unsupported": "Synchronous execution is only supported for some filesystem events and Identity Provider logins", + "sync_unsupported_fs_event": "Synchronous execution is only supported for upload and pre-* filesystem events", + "only_failure_actions": "At least a non-failure action is required", + "sync_action_required": "Event \"{{val}}\" requires at least a synchronous action", + "scheduler_help": "The scheduler uses UTC time. Hours: 0-23. Day of week: 0-6 (Sun-Sat). Day of month: 1-31. Month: 1-12. Asterisk (*) indicates a match for all the values of the field. e.g. every day of week, every day of month and so on", + "concurrent_run": "Allow concurrent execution from multiple instances", + "protocol_filters": "Protocol filters", + "object_filters": "Object filters", + "name_filters": "Name filters", + "name_filters_help": "Shell-like pattern filters for usernames, folder names. For example \"user*\"\" will match names starting with \"user\". For provider events, this filter is applied to the username of the admin executing the event", + "inverse_match": "Inverse match", + "group_name_filters": "Group name filters", + "group_name_filters_help": "Shell-like pattern filters for group names. For example \"group*\"\" will match group names starting with \"group\"", + "role_name_filters": "Role name filters", + "role_name_filters_help": "Shell-like pattern filters for role names. For example \"role*\"\" will match role names starting with \"role\"", + "path_filters": "Path filters", + "path_filters_help": "Shell-like pattern filters on filesystem event paths. For example \"/adir/*.txt\"\" will match paths in the \"/adir\" directory ending with \".txt\". Double asterisk is supported, for example \"/**/*.txt\" will match any file ending with \".txt\". \"/mydir/**\" will match any entry in \"/mydir\"", + "file_size_limits": "File size limits", + "file_size_limits_help": "0 means no limit. You can use MB/GB suffix", + "min_size": "Minimum size", + "max_size": "Maximum size", + "actions_help": "One or more actions to execute. The \"Execute sync\" option is supported for \"upload\" events and required for \"pre-*\" events and Identity provider login events if the action checks the account", + "option_failure_action": "Failure action", + "option_stop_on_failure": "Stop on failure", + "option_execute_sync": "Synchronous execution", + "no_filter": "No filter means always triggering events", + "action_placeholder": "Select an action", + "triggers": { + "fs_event": "Filesystem events", + "provider_event": "Provider events", + "ip_blocked": "IP blocked", + "certificate_renewal": "Certificate renewal", + "on_demand": "On demand", + "idp_login": "Identity Provider logins", + "schedule": "Schedules" + }, + "idp_logins": { + "user": "User login", + "admin": "Admin login" + } } } \ No newline at end of file diff --git a/static/locales/it/translation.json b/static/locales/it/translation.json index ddd002f3..e51399d4 100644 --- a/static/locales/it/translation.json +++ b/static/locales/it/translation.json @@ -63,7 +63,9 @@ "add_ip_list": "Aggiungi elemento a lista IP", "update_ip_list": "Aggiorna elemento lista IP", "add_action": "Aggiungi azione", - "update_action": "Aggiorna azione" + "update_action": "Aggiorna azione", + "add_rule": "Aggiungi regola", + "update_rule": "Aggiorna regola" }, "setup": { "desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore", @@ -252,7 +254,12 @@ "timeout": "Timeout", "env_vars": "Variabili d'ambiente", "hours": "Ore", - "paths": "Percorsi" + "paths": "Percorsi", + "hour": "Ora", + "day_of_week": "Giorno settimana", + "day_of_month": "Giorno mese", + "month": "Mese", + "options": "Opzioni" }, "fs": { "view_file": "Visualizza file \"{{- path}}\"", @@ -1003,5 +1010,63 @@ "metadata_string": "Metadati del Cloud Storage Provider serializzati come stringa JSON escaped per i file scaricati", "uid": "ID univoco" } + }, + "rules": { + "view_manage": "Visualizza e gestisci le regole per gli eventi", + "trigger": "Attivazione", + "run_confirm": "Vuoi eseguire la regola selezionata?", + "run_confirm_btn": "Si, esegui", + "run_error_generic": "Impossibile eseguire la regola selezionata", + "run_ok": "Azioni delle regola avviate", + "run": "Esegui", + "invalid_fs_min_size": "Dimensione minima non valida", + "invalid_fs_max_size": "Dimensione massima non valida", + "action_required": "Almeno un'azione è obbligatoria", + "fs_event_required": "Almeno un evento file system è obbligatorio", + "provider_event_required": "Almeno un evento provider è obbligatorio", + "schedule_required": "Almeno una schedulazione è obbligatoria", + "schedule_invalid": "Schedulazione non valida", + "duplicate_actions": "Rilevata azioni duplicate", + "sync_failure_actions": "L'esecuzione sincrona non è supportata per le azioni su errore", + "sync_unsupported": "L'esecuzione sincrona è supportata solo per alcuni eventi del file system e per gli accessi tramite Identity Provider", + "sync_unsupported_fs_event": "L'esecuzione sincrona è supporta solo per gli eventi \"upload\" e \"pre-*\"", + "only_failure_actions": "E' richiesta almeno un'azione che non venga eseguita su errore", + "sync_action_required": "L'evento \"{{val}}\" richiede almeno un'azione da eseguire sincronamente", + "scheduler_help": "Lo scheduler utilizza l'ora UTC. Orari: 0-23. Giorno della settimana: 0-6 (dom-sab). Giorno del mese: 1-31. Mese: 1-12. L'asterisco (*) indica una corrispondenza per tutti i valori del campo. per esempio. ogni giorno della settimana, ogni giorno del mese e così via", + "concurrent_run": "Consentire l'esecuzione simultanea da più istanze", + "protocol_filters": "Filtro su protocolli", + "object_filters": "Filtro su oggetti", + "name_filters": "Filtro su nomi", + "name_filters_help": "Filtri per nomi utente e nomi di cartelle. Ad esempio, \"user*\"\" corrisponderà per i nomi che iniziano con \"user\". Per gli eventi del provider, questo filtro viene applicato al nome utente dell'amministratore che esegue l'evento", + "inverse_match": "Corrispondenza inversa", + "group_name_filters": "Filtro su nome gruppi", + "group_name_filters_help": "Filtri per nomi dei gruppi. Ad esempio \"group*\"\" corrisponderà ai nomi dei gruppi che iniziano con \"group\"", + "role_name_filters": "Filtri su nome ruoli", + "role_name_filters_help": "Filtri per nomi dei ruoli. Ad esempio \"role*\"\" corrisponderà ai nomi dei gruppi che iniziano con \"role\"", + "path_filters": "Filtri sui percorsi", + "path_filters_help": "Filtri sui percorsi degli eventi del file system. Ad esempio \"/adir/*.txt\"\" corrisponderà ai percorsi nella directory \"/adir\" che terminano con \".txt\". È supportato il doppio asterisco, ad esempio \"/**/*. txt\" corrisponderà a qualsiasi file che termina con \".txt\". \"/mydir/**\" corrisponderà a qualsiasi voce in \"/mydir\"", + "file_size_limits": "Filtri sulla dimensione file", + "file_size_limits_help": "0 significa nessun limite. È possibile utilizzare il suffisso MB/GB", + "min_size": "Dimensione min", + "max_size": "Dimensione max", + "actions_help": "Una o più azioni da eseguire. L'opzione \"Esecuzione sincrona\" è supportata per gli eventi di \"upload\" ed è richiesta per gli eventi \"pre-*\" e gli eventi di accesso tramite Identity provider se l'azione controlla l'account", + "option_failure_action": "Azione su errore", + "option_stop_on_failure": "Termina su errore", + "option_execute_sync": "Esecuzione sincrona", + "no_filter": "Nessun filtro significa attivare sempre gli eventi", + "action_placeholder": "Seleziona un'azione", + "triggers": { + "fs_event": "Eventi file system", + "provider_event": "Eventi provider", + "ip_blocked": "IP bloccato", + "certificate_renewal": "Rinnovo certificato", + "on_demand": "Su richiesta", + "idp_login": "Accessi tramite Identity Provider", + "schedule": "Schedulazioni" + }, + "idp_logins": { + "user": "Accesso utente", + "admin": "Accesso amministratore" + } } } \ No newline at end of file diff --git a/templates/webadmin/eventaction.html b/templates/webadmin/eventaction.html index 21df870d..01297753 100644 --- a/templates/webadmin/eventaction.html +++ b/templates/webadmin/eventaction.html @@ -1020,13 +1020,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). $('#idType').on("change", function(){ onTypeChanged(this.value); - }) + }); $('#idFsActionType').on("change", function(){ onFsActionChanged(this.value); }); - $('#role_form').submit(function (event) { + $('#eventaction_form').submit(function (event) { let submitButton = document.querySelector('#form_submit'); submitButton.setAttribute('data-kt-indicator', 'on'); submitButton.disabled = true; diff --git a/templates/webadmin/eventrule.html b/templates/webadmin/eventrule.html index e2d3fbc2..abaad99a 100644 --- a/templates/webadmin/eventrule.html +++ b/templates/webadmin/eventrule.html @@ -1,85 +1,69 @@ {{template "base" .}} -{{define "title"}}{{.Title}}{{end}} - -{{define "extra_css"}} - -{{end}} - -{{define "page_body"}} -
-
-
{{.Title}}
+{{- define "page_body"}} +
+
+

- {{if .Error}} - - {{end}} + {{- template "errmsg" .Error}}
+
- -
- + +
+
-
- -
- + +
-
- -
- - - Optional description - +
+ +
+
-
- -
- {{- range .TriggerTypes}} - + {{- end}}
-
- -
- {{- range $event := .FsEvents}} {{- end}} @@ -87,10 +71,10 @@ along with this program. If not, see .
-
- -
- {{- range $event := .ProviderEvents}} {{- end}} @@ -98,628 +82,579 @@ along with this program. If not, see .
-
- -
- + + +
-
-
- Schedules +
+
+

Schedules

-
The scheduler uses UTC time. Hours: 0-23. Day of week: 0-6 (Sun-Sat). Day of month: 1-31. Month: 1-12. Asterisk (*) indicates a match for all the values of the field. e.g. every day of week, every day of month and so on. More info.
-
-
- {{range $idx, $val := .Rule.Conditions.Schedules}} -
-
- +
+ {{template "infomsg" "rules.scheduler_help"}} +
+
+ {{- range $idx, $val := .Rule.Conditions.Schedules}} +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
-
- -
-
- -
-
- -
-
- + {{- else}} +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ {{- end}}
- {{else}} -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- {{end}} +
+ +
+
+
-
- + {{- if .IsShared}} +
+
+
+ +
+ {{- end}} - {{if .IsShared}} -
-
- - -
-
- {{end}} - -
- -
- {{- range $p := .Protocols}} {{- end}} - - No selection means any protocol will trigger events - +
-
- -
- {{- range $p := .ProviderObjects}} {{- end}} - - No selection means any provider object will trigger events - +
-
-
- Name filters +
+
+

Name filters

-
Shell-like pattern filters for usernames, folder names. For example "user*"" will match names starting with "user". For provider events, this filter is applied to the username of the admin executing the event.
-
-
- {{range $idx, $val := .Rule.Conditions.Options.Names}} -
-
- +
+ {{template "infomsg" "rules.name_filters_help"}} +
+
+ {{- range $idx, $val := .Rule.Conditions.Options.Names}} +
+
+
+
+ +
+
+ +
+ +
+
-
- -
-
- + {{- else}} +
+
+
+ +
+
+ +
+ +
+ {{- end}}
- {{else}} -
-
- -
-
- -
-
- -
-
- {{end}}
-
-
- +
-
-
- Group name filters +
+
+

Group name filters

-
Shell-like pattern filters for group names. For example "group*"" will match group names starting with "group".
-
-
- {{range $idx, $val := .Rule.Conditions.Options.GroupNames}} -
-
- +
+ {{template "infomsg" "rules.group_name_filters_help"}} +
+
+ {{- range $idx, $val := .Rule.Conditions.Options.GroupNames}} +
+
+
+
+ +
+
+ +
+ +
+
-
- -
-
- + {{- else}} +
+
+
+ +
+
+ +
+ +
+ {{- end}}
- {{else}} -
-
- -
-
- -
-
- -
-
- {{end}}
-
-
- +
-
-
- Role name filters +
+
+

Role name filters

-
Shell-like pattern filters for role names. For example "role*"" will match role names starting with "role".
-
-
- {{range $idx, $val := .Rule.Conditions.Options.RoleNames}} -
-
- +
+ {{template "infomsg" "rules.role_name_filters_help"}} +
+
+ {{- range $idx, $val := .Rule.Conditions.Options.RoleNames}} +
+
+
+
+ +
+
+ +
+ +
+
-
- -
-
- + {{- else}} +
+
+
+ +
+
+ +
+ +
+ {{- end}}
- {{else}} -
-
- -
-
- -
-
- -
-
- {{end}}
-
-
- +
-
-
- Path filters +
+
+

Path filters

-
Shell-like pattern filters for filesystem events. For example "/adir/*.txt"" will match paths in the "/adir" directory ending with ".txt". Double asterisk is supported, for example "/**/*.txt" will match any file ending with ".txt". "/mydir/**" will match any entry in "/mydir"
-
-
- {{range $idx, $val := .Rule.Conditions.Options.FsPaths}} -
-
- +
+ {{template "infomsg" "rules.path_filters_help"}} +
+
+ {{- range $idx, $val := .Rule.Conditions.Options.FsPaths}} +
+
+
+
+ +
+
+ +
+ +
+
-
- -
-
- + {{- else}} +
+
+
+ +
+
+ +
+ +
+ {{- end}}
- {{else}} -
-
- -
-
- -
-
- -
-
- {{end}}
-
-
- +
-
-
- File size limits. 0 means no limit. You can use MB/GB suffix +
+
+

+ File size limits +

-
- -
- + {{template "infomsg" "rules.file_size_limits_help"}} +
+ +
+
-
- -
- +
+ +
+
-
-
- Actions +
+
+

Actions

-
One or more actions to execute. The "Execute sync" options is supported for upload events and required for pre-* events and Identity provider login events if the action checks the account
-
-
- {{range $idx, $val := .Rule.Actions}} -
-
- +
+ {{template "infomsg" "rules.actions_help"}} +
+
+ {{- range $idx, $val := .Rule.Actions}} +
+
+
+
+ +
+
+ +
+ +
+
-
- -
-
- -
-
- + {{- else}} +
+
+
+ +
+
+ +
+ +
+ {{- end}}
- {{else}} -
-
- -
-
- -
-
- -
-
- -
-
- {{end}}
-
-
- +
- -
- +
+ +
+
-{{end}} - -{{define "extra_js"}} - - + -{{end}} \ No newline at end of file +{{- end}} \ No newline at end of file diff --git a/templates/webadmin/eventrules.html b/templates/webadmin/eventrules.html index 7cbf482d..f90e1ec0 100644 --- a/templates/webadmin/eventrules.html +++ b/templates/webadmin/eventrules.html @@ -1,326 +1,406 @@ {{template "base" .}} -{{define "title"}}{{.Title}}{{end}} +{{- define "extra_css"}} + +{{- end}} -{{define "extra_css"}} - - - - - -{{end}} - -{{define "page_body"}} - - - -
-
-
View and manage event rules
+{{- define "page_body"}} +{{- template "errmsg" ""}} +
+
+

View and manage event rules

-
-
- +
+
+ + Loading... +
+
+
+
+ + +
+
+ {{- if .LoggedUser.HasPermission "manage_event_rules"}} + + + Add + + {{- end}} +
+
+ +
- - - - - - - + + + + + + - - {{range .Rules}} - - - - - - - - - {{end}} - +
NameStatusDescriptionTriggerActions
NameStatusTriggerActions
{{.Trigger}}{{.Name}}{{if eq .Status 1 }}Active{{else}}Inactive{{end}}{{.Description}}{{.GetTriggerAsString}}{{.GetActionsAsString}}
+
-{{end}} +{{- end}} -{{define "dialog"}} - +{{- define "extra_js"}} + + - - - - - - - - - - -{{end}} \ No newline at end of file +{{- end}} \ No newline at end of file