WIP new WebAdmin: event rules

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-02-01 20:32:43 +01:00
parent c85601146d
commit ad80d4e475
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
11 changed files with 1264 additions and 1014 deletions

2
go.mod
View file

@ -54,7 +54,7 @@ require (
github.com/rs/xid v1.5.0 github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c 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/afero v1.11.0
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2 github.com/spf13/viper v1.18.2

4
go.sum
View file

@ -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/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 h1:07TYPvNbOnmKsBxjNsUr+gsILIUWflw1UYwjn1jognM=
github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c/go.mod h1:AWoY2YYe/P1ymfTlRER/meERQjCcZZTbgVPGcPQgaqc= 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.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= 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 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=

View file

@ -117,19 +117,19 @@ func isEventTriggerValid(trigger int) bool {
func getTriggerTypeAsString(trigger int) string { func getTriggerTypeAsString(trigger int) string {
switch trigger { switch trigger {
case EventTriggerFsEvent: case EventTriggerFsEvent:
return "Filesystem event" return util.I18nTriggerFsEvent
case EventTriggerProviderEvent: case EventTriggerProviderEvent:
return "Provider event" return util.I18nTriggerProviderEvent
case EventTriggerIPBlocked: case EventTriggerIPBlocked:
return "IP blocked" return util.I18nTriggerIPBlockedEvent
case EventTriggerCertificate: case EventTriggerCertificate:
return "Certificate renewal" return util.I18nTriggerCertificateRenewEvent
case EventTriggerOnDemand: case EventTriggerOnDemand:
return "On demand" return util.I18nTriggerOnDemandEvent
case EventTriggerIDPLogin: case EventTriggerIDPLogin:
return "Identity Provider login" return util.I18nTriggerIDPLoginEvent
default: default:
return "Schedule" return util.I18nTriggerScheduleEvent
} }
} }
@ -1212,17 +1212,26 @@ func (a *EventAction) getACopy() EventAction {
func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error { func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error {
if a.Options.IsFailureAction { if a.Options.IsFailureAction {
if a.Options.ExecuteSync { 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 a.Options.ExecuteSync {
if trigger != EventTriggerFsEvent && trigger != EventTriggerIDPLogin { 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 { if trigger == EventTriggerFsEvent {
for _, ev := range fsEvents { for _, ev := range fsEvents {
if !util.Contains(allowedSyncFsEvents, ev) { 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 { func (c *EventConditions) validateSchedules() error {
if len(c.Schedules) == 0 { 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 { for _, schedule := range c.Schedules {
if err := schedule.validate(); err != nil { if err := schedule.validate(); err != nil {
return err return util.NewI18nError(err, util.I18nErrorRuleScheduleInvalid)
} }
} }
return nil return nil
@ -1397,7 +1409,10 @@ func (c *EventConditions) validate(trigger int) error {
c.Options.ProviderObjects = nil c.Options.ProviderObjects = nil
c.IDPLoginEvent = 0 c.IDPLoginEvent = 0
if len(c.FsEvents) == 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 { for _, ev := range c.FsEvents {
if !util.Contains(SupportedFsEvents, ev) { if !util.Contains(SupportedFsEvents, ev) {
@ -1414,7 +1429,10 @@ func (c *EventConditions) validate(trigger int) error {
c.Options.MaxFileSize = 0 c.Options.MaxFileSize = 0
c.IDPLoginEvent = 0 c.IDPLoginEvent = 0
if len(c.ProviderEvents) == 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 { for _, ev := range c.ProviderEvents {
if !util.Contains(SupportedProviderEvents, ev) { if !util.Contains(SupportedProviderEvents, ev) {
@ -1558,7 +1576,7 @@ func (r *EventRule) isStatusValid() bool {
func (r *EventRule) validate() error { func (r *EventRule) validate() error {
if r.Name == "" { if r.Name == "" {
return util.NewValidationError("name is mandatory") return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
} }
if !r.isStatusValid() { if !r.isStatusValid() {
return util.NewValidationError(fmt.Sprintf("invalid event rule status: %d", r.Status)) return util.NewValidationError(fmt.Sprintf("invalid event rule status: %d", r.Status))
@ -1570,7 +1588,7 @@ func (r *EventRule) validate() error {
return err return err
} }
if len(r.Actions) == 0 { 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) actionNames := make(map[string]bool)
actionOrders := make(map[int]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)) return util.NewValidationError(fmt.Sprintf("invalid action at position %d, name not specified", idx))
} }
if actionNames[r.Actions[idx].Name] { 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] { if actionOrders[r.Actions[idx].Order] {
return util.NewValidationError(fmt.Sprintf("duplicated order %d for action %q", 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 actionOrders[r.Actions[idx].Order] = true
} }
if len(r.Actions) == failureActions { 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 { if !hasSyncAction {
return r.validateMandatorySyncActions() return r.validateMandatorySyncActions()
@ -1614,7 +1638,13 @@ func (r *EventRule) validateMandatorySyncActions() error {
} }
for _, ev := range r.Conditions.FsEvents { for _, ev := range r.Conditions.FsEvents {
if util.Contains(mandatorySyncFsEvents, ev) { 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 return nil

View file

@ -1775,6 +1775,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
s.handleWebUpdateEventActionPost) s.handleWebUpdateEventActionPost)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader). router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
Delete(webAdminEventActionPath+"/{name}", deleteEventAction) 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). router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
Get(webAdminEventRulesPath, s.handleWebGetEventRules) Get(webAdminEventRulesPath, s.handleWebGetEventRules)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).

View file

@ -98,7 +98,6 @@ const (
templateMaintenance = "maintenance.html" templateMaintenance = "maintenance.html"
templateMFA = "mfa.html" templateMFA = "mfa.html"
templateSetup = "adminsetup.html" templateSetup = "adminsetup.html"
pageEventRulesTitle = "Event rules"
defaultQueryLimit = 1000 defaultQueryLimit = 1000
inversePatternType = "inverse" inversePatternType = "inverse"
) )
@ -153,11 +152,6 @@ type basePage struct {
Branding UIBranding Branding UIBranding
} }
type eventRulesPage struct {
basePage
Rules []dataprovider.EventRule
}
type statusPage struct { type statusPage struct {
basePage basePage
Status *ServicesStatus Status *ServicesStatus
@ -312,7 +306,7 @@ type eventRulePage struct {
Protocols []string Protocols []string
ProviderEvents []string ProviderEvents []string
ProviderObjects []string ProviderObjects []string
Error string Error *util.I18nError
Mode genericPageMode Mode genericPageMode
IsShared bool 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, 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) actions, errActions := s.getWebEventActions(w, r, defaultQueryLimit, true)
if err != nil { if errActions != nil {
return return
} }
var title, currentURL string var title, currentURL string
switch mode { switch mode {
case genericPageModeAdd: case genericPageModeAdd:
title = "Add new event rules" title = util.I18nAddRuleTitle
currentURL = webAdminEventRulePath currentURL = webAdminEventRulePath
case genericPageModeUpdate: case genericPageModeUpdate:
title = "Update event rules" title = util.I18nUpdateRuleTitle
currentURL = fmt.Sprintf("%v/%v", webAdminEventRulePath, url.PathEscape(rule.Name)) 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, Protocols: dataprovider.SupportedRuleConditionProtocols,
ProviderEvents: dataprovider.SupportedProviderEvents, ProviderEvents: dataprovider.SupportedProviderEvents,
ProviderObjects: dataprovider.SupporteRuleConditionProviderObjects, ProviderObjects: dataprovider.SupporteRuleConditionProviderObjects,
Error: error, Error: getI18nError(err),
Mode: mode, Mode: mode,
IsShared: s.isShared > 0, IsShared: s.isShared > 0,
} }
@ -2420,74 +2414,66 @@ func getIDPLoginEventFromPostField(r *http.Request) int {
func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) { func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) {
var schedules []dataprovider.Schedule var schedules []dataprovider.Schedule
var names, groupNames, roleNames, fsPaths []dataprovider.ConditionPattern var names, groupNames, roleNames, fsPaths []dataprovider.ConditionPattern
for k := range r.Form {
if strings.HasPrefix(k, "schedule_hour") { scheduleHours := r.Form["schedule_hour"]
hour := strings.TrimSpace(r.Form.Get(k)) scheduleDayOfWeeks := r.Form["schedule_day_of_week"]
if hour != "" { scheduleDayOfMonths := r.Form["schedule_day_of_month"]
idx := strings.TrimPrefix(k, "schedule_hour") scheduleMonths := r.Form["schedule_month"]
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))) for idx, hour := range scheduleHours {
month := strings.TrimSpace(r.Form.Get(fmt.Sprintf("schedule_month%s", idx))) if hour != "" {
schedules = append(schedules, dataprovider.Schedule{ schedules = append(schedules, dataprovider.Schedule{
Hours: hour, Hours: hour,
DayOfWeek: dayOfWeek, DayOfWeek: scheduleDayOfWeeks[idx],
DayOfMonth: dayOfMonth, DayOfMonth: scheduleDayOfMonths[idx],
Month: month, Month: scheduleMonths[idx],
}) })
}
}
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,
})
}
} }
} }
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")) minFileSize, err := util.ParseBytes(r.Form.Get("fs_min_size"))
if err != nil { 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")) maxFileSize, err := util.ParseBytes(r.Form.Get("fs_max_size"))
if err != nil { 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{ conditions := dataprovider.EventConditions{
FsEvents: r.Form["fs_events"], FsEvents: r.Form["fs_events"],
@ -2511,38 +2497,86 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo
func getEventRuleActionsFromPostFields(r *http.Request) ([]dataprovider.EventAction, error) { func getEventRuleActionsFromPostFields(r *http.Request) ([]dataprovider.EventAction, error) {
var actions []dataprovider.EventAction var actions []dataprovider.EventAction
for k := range r.Form {
if strings.HasPrefix(k, "action_name") { names := r.Form["action_name"]
name := strings.TrimSpace(r.Form.Get(k)) orders := r.Form["action_order"]
if name != "" {
idx := strings.TrimPrefix(k, "action_name") for idx, name := range names {
order, err := strconv.Atoi(r.Form.Get(fmt.Sprintf("action_order%s", idx))) if name != "" {
if err != nil { order, err := strconv.Atoi(orders[idx])
return actions, fmt.Errorf("invalid order: %w", err) 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"),
},
})
} }
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 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) { func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return dataprovider.EventRule{}, util.NewI18nError(err, util.I18nErrorInvalidForm) return dataprovider.EventRule{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
} }
updateRepeaterFormRuleFields(r)
status, err := strconv.Atoi(r.Form.Get("status")) status, err := strconv.Atoi(r.Form.Get("status"))
if err != nil { if err != nil {
return dataprovider.EventRule{}, fmt.Errorf("invalid status: %w", err) 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) 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) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
limit := defaultQueryLimit rules := make([]dataprovider.EventRule, 0, 10)
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)
for { for {
res, err := dataprovider.GetEventRules(limit, len(rules), dataprovider.OrderASC) res, err := dataprovider.GetEventRules(defaultQueryLimit, len(rules), dataprovider.OrderASC)
if err != nil { if err != nil {
s.renderInternalServerErrorPage(w, r, err) sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), http.StatusInternalServerError)
return return
} }
rules = append(rules, res...) rules = append(rules, res...)
if len(res) < limit { if len(res) < defaultQueryLimit {
break break
} }
} }
render.JSON(w, r, rules)
}
data := eventRulesPage{ func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) {
basePage: s.getBasePageData(pageEventRulesTitle, webAdminEventRulesPath, r), r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
Rules: rules,
} data := s.getBasePageData(util.I18nRulesTitle, webAdminEventRulesPath, r)
renderAdminTemplate(w, templateEventRules, data) renderAdminTemplate(w, templateEventRules, data)
} }
@ -3823,7 +3853,7 @@ func (s *httpdServer) handleWebAddEventRuleGet(w http.ResponseWriter, r *http.Re
Status: 1, Status: 1,
Trigger: dataprovider.EventTriggerFsEvent, 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) { 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) rule, err := getEventRuleFromPostFields(r)
if err != nil { if err != nil {
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error()) s.renderEventRulePage(w, r, rule, genericPageModeAdd, err)
return return
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -3845,7 +3875,7 @@ func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.R
return return
} }
if err = dataprovider.AddEventRule(&rule, claims.Username, ipAddr, claims.Role); err != nil { 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 return
} }
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther) http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
@ -3856,7 +3886,7 @@ func (s *httpdServer) handleWebUpdateEventRuleGet(w http.ResponseWriter, r *http
name := getURLParam(r, "name") name := getURLParam(r, "name")
rule, err := dataprovider.EventRuleExists(name) rule, err := dataprovider.EventRuleExists(name)
if err == nil { if err == nil {
s.renderEventRulePage(w, r, rule, genericPageModeUpdate, "") s.renderEventRulePage(w, r, rule, genericPageModeUpdate, nil)
} else if errors.Is(err, util.ErrNotFound) { } else if errors.Is(err, util.ErrNotFound) {
s.renderNotFoundPage(w, r, err) s.renderNotFoundPage(w, r, err)
} else { } else {
@ -3882,7 +3912,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt
} }
updatedRule, err := getEventRuleFromPostFields(r) updatedRule, err := getEventRuleFromPostFields(r)
if err != nil { if err != nil {
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error()) s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err)
return return
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -3894,7 +3924,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt
updatedRule.Name = rule.Name updatedRule.Name = rule.Name
err = dataprovider.UpdateEventRule(&updatedRule, claims.Username, ipAddr, claims.Role) err = dataprovider.UpdateEventRule(&updatedRule, claims.Username, ipAddr, claims.Role)
if err != nil { if err != nil {
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error()) s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err)
return return
} }
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther) http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)

View file

@ -69,8 +69,11 @@ const (
I18nDefenderTitle = "title.defender" I18nDefenderTitle = "title.defender"
I18nEventsTitle = "title.logs" I18nEventsTitle = "title.logs"
I18nActionsTitle = "title.event_actions" I18nActionsTitle = "title.event_actions"
I18nRulesTitle = "title.event_rules"
I18nAddActionTitle = "title.add_action" I18nAddActionTitle = "title.add_action"
I18nUpdateActionTitle = "title.update_action" I18nUpdateActionTitle = "title.update_action"
I18nAddRuleTitle = "title.add_rule"
I18nUpdateRuleTitle = "title.update_rule"
I18nStatusTitle = "status.desc" I18nStatusTitle = "status.desc"
I18nErrorSetupInstallCode = "setup.install_code_mismatch" I18nErrorSetupInstallCode = "setup.install_code_mismatch"
I18nInvalidAuth = "general.invalid_auth_request" I18nInvalidAuth = "general.invalid_auth_request"
@ -278,6 +281,26 @@ const (
I18nActionFsTypeCompress = "actions.fs_types.compress" I18nActionFsTypeCompress = "actions.fs_types.compress"
I18nActionFsTypeCopy = "actions.fs_types.copy" I18nActionFsTypeCopy = "actions.fs_types.copy"
I18nActionFsTypeCreateDirs = "actions.fs_types.create_dirs" 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 // NewI18nError returns a I18nError wrappring the provided error

View file

@ -63,7 +63,9 @@
"add_ip_list": "Add IP list entry", "add_ip_list": "Add IP list entry",
"update_ip_list": "Update IP list entry", "update_ip_list": "Update IP list entry",
"add_action": "Add action", "add_action": "Add action",
"update_action": "Update action" "update_action": "Update action",
"add_rule": "Add rule",
"update_rule": "Update rule"
}, },
"setup": { "setup": {
"desc": "To start using SFTPGo you need to create an administrator user", "desc": "To start using SFTPGo you need to create an administrator user",
@ -252,7 +254,12 @@
"timeout": "Timeout", "timeout": "Timeout",
"env_vars": "Environment variables", "env_vars": "Environment variables",
"hours": "Hours", "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": { "fs": {
"view_file": "View file \"{{- path}}\"", "view_file": "View file \"{{- path}}\"",
@ -1003,5 +1010,63 @@
"metadata_string": "Cloud storage metadata for the downloaded file as JSON escaped string", "metadata_string": "Cloud storage metadata for the downloaded file as JSON escaped string",
"uid": "Unique ID" "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"
}
} }
} }

View file

@ -63,7 +63,9 @@
"add_ip_list": "Aggiungi elemento a lista IP", "add_ip_list": "Aggiungi elemento a lista IP",
"update_ip_list": "Aggiorna elemento lista IP", "update_ip_list": "Aggiorna elemento lista IP",
"add_action": "Aggiungi azione", "add_action": "Aggiungi azione",
"update_action": "Aggiorna azione" "update_action": "Aggiorna azione",
"add_rule": "Aggiungi regola",
"update_rule": "Aggiorna regola"
}, },
"setup": { "setup": {
"desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore", "desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore",
@ -252,7 +254,12 @@
"timeout": "Timeout", "timeout": "Timeout",
"env_vars": "Variabili d'ambiente", "env_vars": "Variabili d'ambiente",
"hours": "Ore", "hours": "Ore",
"paths": "Percorsi" "paths": "Percorsi",
"hour": "Ora",
"day_of_week": "Giorno settimana",
"day_of_month": "Giorno mese",
"month": "Mese",
"options": "Opzioni"
}, },
"fs": { "fs": {
"view_file": "Visualizza file \"{{- path}}\"", "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", "metadata_string": "Metadati del Cloud Storage Provider serializzati come stringa JSON escaped per i file scaricati",
"uid": "ID univoco" "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"
}
} }
} }

View file

@ -1020,13 +1020,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
$('#idType').on("change", function(){ $('#idType').on("change", function(){
onTypeChanged(this.value); onTypeChanged(this.value);
}) });
$('#idFsActionType').on("change", function(){ $('#idFsActionType').on("change", function(){
onFsActionChanged(this.value); onFsActionChanged(this.value);
}); });
$('#role_form').submit(function (event) { $('#eventaction_form').submit(function (event) {
let submitButton = document.querySelector('#form_submit'); let submitButton = document.querySelector('#form_submit');
submitButton.setAttribute('data-kt-indicator', 'on'); submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true; submitButton.disabled = true;

File diff suppressed because it is too large Load diff

View file

@ -1,326 +1,406 @@
<!-- <!--
Copyright (C) 2019 Nicola Murino Copyright (C) 2024 Nicola Murino
This program is free software: you can redistribute it and/or modify This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, https://keenthemes.com/products/templates-mega-bundle
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License KeenThemes HTML/CSS/JS components are allowed for use only within the
along with this program. If not, see <https://www.gnu.org/licenses/>. SFTPGo product and restricted to be used in a resealable HTML template
that can compete with KeenThemes products anyhow.
This WebUI is allowed for use only within the SFTPGo product and
therefore cannot be used in derivative works/products without an
explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{define "title"}}{{.Title}}{{end}} {{- define "extra_css"}}
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- end}}
{{define "extra_css"}} {{- define "page_body"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet"> {{- template "errmsg" ""}}
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet"> <div class="card shadow-sm">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet"> <div class="card-header bg-light">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet"> <h3 data-i18n="rules.view_manage" class="card-title section-title">View and manage event rules</h3>
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body"></div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage event rules</h6>
</div> </div>
<div class="card-body"> <div id="card_body" class="card-body">
<div class="table-responsive"> <div id="loader" class="align-items-center text-center my-10">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0"> <span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
<span data-i18n="general.loading" class="text-gray-700">Loading...</span>
</div>
<div id="card_content" class="d-none">
<div class="d-flex flex-stack flex-wrap mb-5">
<div class="d-flex align-items-center position-relative my-2">
<i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
<input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
</div>
<div class="d-flex justify-content-end my-2" data-table-toolbar="base">
{{- if .LoggedUser.HasPermission "manage_event_rules"}}
<a href="{{.EventRuleURL}}" class="btn btn-primary ms-5">
<i class="ki-duotone ki-plus fs-2"></i>
<span data-i18n="general.add">Add</span>
</a>
{{- end}}
</div>
</div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead> <thead>
<tr> <tr class="text-start text-muted fw-bold fs-6 gs-0">
<th></th> <th data-i18n="general.name">Name</th>
<th>Name</th> <th data-i18n="general.status">Status</th>
<th>Status</th> <th data-i18n="rules.trigger">Trigger</th>
<th>Description</th> <th data-i18n="title.event_actions">Actions</th>
<th>Trigger</th> <th class="min-w-100px"></th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
{{range .Rules}}
<tr>
<td>{{.Trigger}}</td>
<td>{{.Name}}</td>
<td>{{if eq .Status 1 }}Active{{else}}Inactive{{end}}</td>
<td>{{.Description}}</td>
<td>{{.GetTriggerAsString}}</td>
<td>{{.GetActionsAsString}}</td>
</tr>
{{end}}
</tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
{{end}} {{- end}}
{{define "dialog"}} {{- define "extra_js"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
aria-hidden="true"> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
<div class="modal-dialog" role="document"> function deleteAction(name) {
<div class="modal-content"> ModalAlert.fire({
<div class="modal-header"> text: $.t('general.delete_confirm_generic'),
<h5 class="modal-title" id="deleteModalLabel"> icon: "warning",
Confirmation required confirmButtonText: $.t('general.delete_confirm_btn'),
</h5> cancelButtonText: $.t('general.cancel'),
<button class="close" type="button" data-dismiss="modal" aria-label="Close"> customClass: {
<span aria-hidden="true">&times;</span> confirmButton: "btn btn-danger",
</button> cancelButton: 'btn btn-secondary'
</div> }
<div class="modal-body">Do you want to delete the selected rule?</div> }).then((result) => {
<div class="modal-footer"> if (result.isConfirmed){
<button class="btn btn-secondary" type="button" data-dismiss="modal"> $('#loading_message').text("");
Cancel KTApp.showPageLoading();
</button> let path = '{{.EventRuleURL}}' + "/" + encodeURIComponent(name);
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
<div class="modal fade" id="runModal" tabindex="-1" role="dialog" aria-labelledby="runModalLabel" axios.delete(path, {
aria-hidden="true"> timeout: 15000,
<div class="modal-dialog" role="document"> headers: {
<div class="modal-content"> 'X-CSRF-TOKEN': '{{.CSRFToken}}'
<div class="modal-header"> },
<h5 class="modal-title" id="runModalLabel"> validateStatus: function (status) {
Confirmation required return status == 200;
</h5> }
<button class="close" type="button" data-dismiss="modal" aria-label="Close"> }).then(function(response){
<span aria-hidden="true">&times;</span> location.reload();
</button> }).catch(function(error){
</div> KTApp.hidePageLoading();
<div class="modal-body">Do you want to execute the selected rule?</div> let errorMessage;
<div class="modal-footer"> if (error && error.response) {
<button class="btn btn-secondary" type="button" data-dismiss="modal"> switch (error.response.status) {
Cancel case 403:
</button> errorMessage = "general.delete_error_403";
<a class="btn btn-warning" href="#" onclick="runAction()"> break;
Run case 404:
</a> errorMessage = "general.delete_error_404";
</div> break;
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
<script type="text/javascript">
function runAction(){
let table = $('#dataTable').DataTable();
table.button('run:name').enable(false);
let name = table.row({ selected: true }).data()[1];
let path = '{{.EventRuleURL}}' + "/run/" + fixedEncodeURIComponent(name);
$('#runModal').modal('hide');
$('#successMsg').hide();
$('#errorMsg').hide();
$.ajax({
url: path,
type: 'POST',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
$('#successTxt').text("Rule actions started");
$('#successMsg').show();
setTimeout(function () {
$('#successMsg').hide();
}, 8000);
},
error: function ($xhr, textStatus, errorThrown) {
var txt = "Unable to run the selected rule";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
} }
} }
} if (!errorMessage){
$('#errorTxt').text(txt); errorMessage = "general.delete_error_generic";
$('#errorMsg').show(); }
ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
} }
}); });
} }
function deleteAction() { function runAction(name) {
let table = $('#dataTable').DataTable(); ModalAlert.fire({
table.button('delete:name').enable(false); text: $.t('rules.run_confirm'),
let name = table.row({ selected: true }).data()[1]; icon: "warning",
let path = '{{.EventRuleURL}}' + "/" + fixedEncodeURIComponent(name); confirmButtonText: $.t('rules.run_confirm_btn'),
$('#deleteModal').modal('hide'); cancelButtonText: $.t('general.cancel'),
$('#errorMsg').hide(); customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.EventRuleURL}}' + "/run/" + encodeURIComponent(name);
$.ajax({ axios.post(path, null, {
url: path, timeout: 15000,
type: 'DELETE', headers: {
dataType: 'json', 'X-CSRF-TOKEN': '{{.CSRFToken}}'
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, },
timeout: 15000, validateStatus: function (status) {
success: function (result) { return status == 202;
window.location.href = '{{.EventRulesURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
var txt = "Unable to delete the selected rule";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
} }
} }).then(function(response){
$('#errorTxt').text(txt); KTApp.hidePageLoading();
$('#errorMsg').show(); ModalAlert.fire({
text: $.t("rules.run_ok"),
icon: "success",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
}).catch(function(error){
KTApp.hidePageLoading();
ModalAlert.fire({
text: $.t("rules.run_error_generic"),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
} }
}); });
} }
$(document).ready(function () { var datatable = function(){
$.fn.dataTable.ext.buttons.add = { var dt;
text: '<i class="fas fa-plus"></i>',
name: 'add',
titleAttr: "Add",
action: function (e, dt, node, config) {
window.location.href = '{{.EventRuleURL}}';
}
};
$.fn.dataTable.ext.buttons.edit = { var initDatatable = function () {
text: '<i class="fas fa-pen"></i>', $('#errorMsg').addClass("d-none");
name: 'edit', dt = $('#dataTable').DataTable({
titleAttr: "Edit", ajax: {
action: function (e, dt, node, config) { url: "{{.EventRulesURL}}/json",
let name = table.row({ selected: true }).data()[1]; dataSrc: "",
let path = '{{.EventRuleURL}}' + "/" + fixedEncodeURIComponent(name); error: function ($xhr, textStatus, errorThrown) {
window.location.href = path; $(".dataTables_processing").hide();
}, let txt = "";
enabled: false if ($xhr) {
}; let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt = json.message;
}
}
}
if (!txt){
txt = "general.error500";
}
setI18NData($('#errorTxt'), txt);
$('#errorMsg').removeClass("d-none");
}
},
columns: [
{
data: "name",
render: function(data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data;
}
},
{
data: "status",
render: function(data, type, row) {
if (type === 'display') {
switch (data){
case 1:
return $.t('general.active');
default:
return $.t('general.inactive');
}
}
return data;
}
},
{
data: "trigger",
render: function(data, type, row) {
if (type === 'display') {
switch (data){
case 1:
return $.t('rules.triggers.fs_event');
case 2:
return $.t('rules.triggers.provider_event');
case 3:
return $.t('rules.triggers.schedule');
case 4:
return $.t('rules.triggers.ip_blocked');
case 5:
return $.t('rules.triggers.certificate_renewal');
case 6:
return $.t('rules.triggers.on_demand');
case 7:
return $.t('rules.triggers.idp_login');
default:
return "";
}
}
return data;
}
},
{
data: "actions",
defaultContent: [],
searchable: false,
orderable: false,
render: function(data, type, row) {
if (type === 'display') {
if (data){
let actions = [];
for (i = 0; i < data.length; i++){
actions.push(data[i].name);
}
return escapeHTML(actions.join(', '));
}
return ""
}
return "";
}
},
{
data: "id",
searchable: false,
orderable: false,
className: 'text-end',
render: function (data, type, row) {
if (type === 'display') {
let numActions = 0;
let actions = `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
<span data-i18n="general.actions" class="fs-6">Actions</span>
<i class="ki-duotone ki-down fs-5 ms-1 rotate-180"></i>
</button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">`;
$.fn.dataTable.ext.buttons.delete = { //{{- if .LoggedUser.HasPermission "manage_event_rules"}}
text: '<i class="fas fa-trash"></i>', numActions++;
name: 'delete', actions+=`<div class="menu-item px-3">
titleAttr: "Delete", <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
action: function (e, dt, node, config) { </div>`
$('#deleteModal').modal('show'); numActions++;
}, if (row.trigger === 6){
enabled: false actions+=`<div class="menu-item px-3">
}; <a data-i18n="rules.run" href="#" class="menu-link px-3" data-table-action="run_row">Run</a>
</div>`
$.fn.dataTable.ext.buttons.run = { numActions++;
text: '<i class="fas fa-play"></i>', }
name: 'run', actions+=`<div class="menu-item px-3">
titleAttr: "Run", <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
action: function (e, dt, node, config) { </div>`
$('#runModal').modal('show'); //{{- end}}
}, if (numActions > 0){
enabled: false actions+=`</div>`;
}; return actions;
}
var table = $('#dataTable').DataTable({ }
"select": { return "";
"style": "single", }
"blurable": true },
}, ],
"stateSave": true, deferRender: true,
"stateDuration": 0, stateSave: true,
"buttons": [ stateDuration: 0,
{ stateLoadParams: function (settings, data) {
"text": "Column visibility", if (data.search.search){
"extend": "colvis", const filterSearch = document.querySelector('[data-table-filter="search"]');
"columns": ":not(.noVis)" filterSearch.value = data.search.search;
}
},
language: {
info: $.t('datatable.info'),
infoEmpty: $.t('datatable.info_empty'),
infoFiltered: $.t('datatable.info_filtered'),
loadingRecords: "",
processing: $.t('datatable.processing'),
zeroRecords: "",
emptyTable: $.t('datatable.no_records')
},
order: [[0, 'asc']],
initComplete: function(settings, json) {
$('#loader').addClass("d-none");
$('#card_content').removeClass("d-none");
let api = $.fn.dataTable.Api(settings);
api.columns.adjust().draw("page");
drawAction();
} }
], });
"columnDefs": [
{
"targets": [0],
"visible": false,
"searchable": false,
"className": "noVis"
},
{
"targets": [1,2],
"className": "noVis"
},
{
"targets": [3],
"visible": false
},
{
"targets": [5],
"render": $.fn.dataTable.render.ellipsis(100, true)
},
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"emptyTable": "No event rules defined"
},
"order": [[1, 'asc']]
});
new $.fn.dataTable.FixedHeader( table ); dt.on('draw', drawAction);
dt.on('column-reorder', function(e, settings, details){
drawAction();
});
}
table.button().add(0,'run'); function drawAction() {
table.button().add(0,'delete'); KTMenu.createInstances();
table.button().add(0,'edit'); handleRowActions();
table.button().add(0,'add'); $('#table_body').localize();
}
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container()); var handleDatatableActions = function () {
const filterSearch = $(document.querySelector('[data-table-filter="search"]'));
filterSearch.off("keyup");
filterSearch.on('keyup', function (e) {
dt.rows().deselect();
dt.search(e.target.value, true, false).draw();
});
}
table.on('select deselect', function () { function handleRowActions() {
var selectedRows = table.rows({ selected: true }).count(); const editButtons = document.querySelectorAll('[data-table-action="edit_row"]');
table.button('delete:name').enable(selectedRows == 1); editButtons.forEach(d => {
table.button('edit:name').enable(selectedRows == 1); let el = $(d);
if (selectedRows == 1){ el.off("click");
table.button('run:name').enable(table.row({ selected: true }).data()[0] == 6); el.on("click", function(e){
} else { e.preventDefault();
table.button('run:name').enable(false); let rowData = dt.row(e.target.closest('tr')).data();
window.location.replace('{{.EventRuleURL}}' + "/" + encodeURIComponent(rowData['name']));
});
});
const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
const parent = e.target.closest('tr');
deleteAction(dt.row(parent).data()['name']);
});
});
const runButtons = document.querySelectorAll('[data-table-action="run_row"]');
runButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
const parent = e.target.closest('tr');
runAction(dt.row(parent).data()['name']);
});
});
}
return {
init: function () {
initDatatable();
handleDatatableActions();
} }
}); }
}();
$(document).on("i18nshow", function(){
datatable.init();
}); });
</script> </script>
{{end}} {{- end}}