Procházet zdrojové kódy

event manager: add IP blocked trigger

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino před 3 roky
rodič
revize
194c3c13ac

+ 1 - 1
docker/README.md

@@ -198,7 +198,7 @@ We only provide the slim variant and so the optional `git` dependency is not ava
 
 ### `sftpgo:<suite>-slim`
 
-These tags provide a slimmer image that does not include the optional `git`, `rsync` and `jq` dependencies.
+These tags provide a slimmer image that does not include `jq` and the optional `git` and `rsync` dependencies.
 
 ### `sftpgo:<suite>-plugins`
 

+ 1 - 0
docker/scripts/download-plugins.sh

@@ -1,4 +1,5 @@
 #!/usr/bin/env bash
+set -e
 
 ARCH=`uname -m`
 

+ 2 - 0
docs/eventmanager.md

@@ -40,6 +40,7 @@ The following trigger events are supported:
 - `Filesystem events`, for example `upload`, `download` etc.
 - `Provider events`, for example `add`, `update`, `delete` user or other resources.
 - `Schedules`.
+- `IP Blocked`, this event can be generated if you enable the [defender](./defender.md).
 
 You can further restrict a rule by specifying additional conditions that must be met before the rule’s actions are taken. For example you can react to uploads only if they are performed by a particular user or using a specified protocol.
 
@@ -58,3 +59,4 @@ Some actions are not supported for some triggers, rules containing incompatible
 - `Filesystem events`, folder quota reset cannot be executed, we don't have a direct way to get the affected folder.
 - `Provider events`, user quota reset, transfer quota reset, data retention check and filesystem actions can be executed only if we modify a user. They will be executed for the affected user. Folder quota reset can be executed only for folders. Filesystem actions are not executed for `delete` user events because the actions is executed after the user deletion.
 - `Schedules`, filesystem actions cannot be executed, they require a user.
+- `IP Blocked`, user quota reset, folder quota reset, transfer quota reset, data retention check and filesystem actions cannot be executed, we only have an IP.

+ 4 - 4
go.mod

@@ -3,7 +3,7 @@ module github.com/drakkan/sftpgo/v2
 go 1.19
 
 require (
-	cloud.google.com/go/storage v1.24.0
+	cloud.google.com/go/storage v1.25.0
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
@@ -66,9 +66,9 @@ require (
 	go.uber.org/automaxprocs v1.5.1
 	gocloud.dev v0.26.0
 	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
-	golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48
+	golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced
 	golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7
-	golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664
+	golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
 	golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
 	google.golang.org/api v0.92.0
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
@@ -168,5 +168,5 @@ replace (
 	github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
 	github.com/pkg/sftp => github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d
 	golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220723143649-81550382d55e
-	golang.org/x/net => github.com/drakkan/net v0.0.0-20220805164234-d1ea7e8d1b71
+	golang.org/x/net => github.com/drakkan/net v0.0.0-20220811173512-bde04f9047cc
 )

+ 6 - 6
go.sum

@@ -77,8 +77,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
 cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA=
 cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
 cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
-cloud.google.com/go/storage v1.24.0 h1:a4N0gIkx83uoVFGz8B2eAV3OhN90QoWF5OZWLKl39ig=
-cloud.google.com/go/storage v1.24.0/go.mod h1:3xrJEFMXBsQLgxwThyjuD3aYlroL0TMRec1ypGUQ0KE=
+cloud.google.com/go/storage v1.25.0 h1:D2Dn0PslpK7Z3B2AvuUHyIC762bDbGJdlmQlCBR71os=
+cloud.google.com/go/storage v1.25.0/go.mod h1:Qys4JU+jeup3QnuKKAosWuxrD95C4MSqxfVDnSirDsI=
 cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A=
 cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM=
 contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA=
@@ -266,8 +266,8 @@ github.com/drakkan/crypto v0.0.0-20220723143649-81550382d55e h1:ZvOJ5DqEUZig5lGl
 github.com/drakkan/crypto v0.0.0-20220723143649-81550382d55e/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro=
 github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
 github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
-github.com/drakkan/net v0.0.0-20220805164234-d1ea7e8d1b71 h1:zC5D08STgdsK74Nh0457cZp7hKmbW+upbr8lfPo3CJw=
-github.com/drakkan/net v0.0.0-20220805164234-d1ea7e8d1b71/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+github.com/drakkan/net v0.0.0-20220811173512-bde04f9047cc h1:nWhdNJ31a4S7oBCwIRRPY/QfpOdHl3i3irjrJXrfM7w=
+github.com/drakkan/net v0.0.0-20220811173512-bde04f9047cc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d h1:kNk/KRhszPJASp7WvjagNW254aKK643Lu8/fr4/ukiM=
 github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
@@ -974,8 +974,8 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs=
-golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

+ 1 - 1
internal/cmd/acme.go

@@ -46,7 +46,7 @@ renewed by the SFTPGo service
 			configDir = util.CleanDirInput(configDir)
 			err := config.LoadConfig(configDir, configFile)
 			if err != nil {
-				logger.ErrorToConsole("Unable to initialize data provider, config load error: %v", err)
+				logger.ErrorToConsole("Unable to initialize ACME, config load error: %v", err)
 				return
 			}
 			acmeConfig := config.GetACMEConfig()

+ 7 - 0
internal/common/defenderdb.go

@@ -107,6 +107,13 @@ func (d *dbDefender) AddEvent(ip string, event HostEvent) {
 	if host.Score > d.config.Threshold {
 		banTime := time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
 		err = dataprovider.SetDefenderBanTime(ip, util.GetTimeAsMsSinceEpoch(banTime))
+		if err == nil {
+			eventManager.handleIPBlockedEvent(EventParams{
+				Event:     ipBlockedEventName,
+				IP:        ip,
+				Timestamp: time.Now().UnixNano(),
+			})
+		}
 	}
 
 	if err == nil {

+ 5 - 0
internal/common/defendermem.go

@@ -209,6 +209,11 @@ func (d *memoryDefender) AddEvent(ip string, event HostEvent) {
 			d.banned[ip] = time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
 			delete(d.hosts, ip)
 			d.cleanupBanned()
+			eventManager.handleIPBlockedEvent(EventParams{
+				Event:     ipBlockedEventName,
+				IP:        ip,
+				Timestamp: time.Now().UnixNano(),
+			})
 		} else {
 			d.hosts[ip] = hs
 		}

+ 42 - 3
internal/common/eventmanager.go

@@ -41,6 +41,10 @@ import (
 	"github.com/drakkan/sftpgo/v2/internal/vfs"
 )
 
+const (
+	ipBlockedEventName = "IP Blocked"
+)
+
 var (
 	// eventManager handle the supported event rules actions
 	eventManager eventRulesContainer
@@ -71,11 +75,12 @@ func init() {
 // eventRulesContainer stores event rules by trigger
 type eventRulesContainer struct {
 	sync.RWMutex
+	lastLoad         int64
 	FsEvents         []dataprovider.EventRule
 	ProviderEvents   []dataprovider.EventRule
 	Schedules        []dataprovider.EventRule
+	IPBlockedEvents  []dataprovider.EventRule
 	schedulesMapping map[string][]cron.EntryID
-	lastLoad         int64
 	concurrencyGuard chan struct{}
 }
 
@@ -124,6 +129,15 @@ func (r *eventRulesContainer) removeRuleInternal(name string) {
 			return
 		}
 	}
+	for idx := range r.IPBlockedEvents {
+		if r.IPBlockedEvents[idx].Name == name {
+			lastIdx := len(r.IPBlockedEvents) - 1
+			r.IPBlockedEvents[idx] = r.IPBlockedEvents[lastIdx]
+			r.IPBlockedEvents = r.IPBlockedEvents[:lastIdx]
+			eventManagerLog(logger.LevelDebug, "removed rule %q from IP blocked events", name)
+			return
+		}
+	}
 	for idx := range r.Schedules {
 		if r.Schedules[idx].Name == name {
 			if schedules, ok := r.schedulesMapping[name]; ok {
@@ -160,6 +174,9 @@ func (r *eventRulesContainer) addUpdateRuleInternal(rule dataprovider.EventRule)
 	case dataprovider.EventTriggerProviderEvent:
 		r.ProviderEvents = append(r.ProviderEvents, rule)
 		eventManagerLog(logger.LevelDebug, "added rule %q to provider events", rule.Name)
+	case dataprovider.EventTriggerIPBlocked:
+		r.IPBlockedEvents = append(r.IPBlockedEvents, rule)
+		eventManagerLog(logger.LevelDebug, "added rule %q to IP blocked events", rule.Name)
 	case dataprovider.EventTriggerSchedule:
 		for _, schedule := range rule.Conditions.Schedules {
 			cronSpec := schedule.GetCronSpec()
@@ -200,8 +217,8 @@ func (r *eventRulesContainer) loadRules() {
 			r.addUpdateRuleInternal(rule)
 		}
 	}
-	eventManagerLog(logger.LevelDebug, "event rules updated, fs events: %d, provider events: %d, schedules: %d",
-		len(r.FsEvents), len(r.ProviderEvents), len(r.Schedules))
+	eventManagerLog(logger.LevelDebug, "event rules updated, fs events: %d, provider events: %d, schedules: %d, ip blocked events: %d",
+		len(r.FsEvents), len(r.ProviderEvents), len(r.Schedules), len(r.IPBlockedEvents))
 
 	r.setLastLoadTime(modTime)
 }
@@ -323,6 +340,28 @@ func (r *eventRulesContainer) handleProviderEvent(params EventParams) {
 	}
 }
 
+func (r *eventRulesContainer) handleIPBlockedEvent(params EventParams) {
+	r.RLock()
+	defer r.RUnlock()
+
+	if len(r.IPBlockedEvents) == 0 {
+		return
+	}
+	var rules []dataprovider.EventRule
+	for _, rule := range r.IPBlockedEvents {
+		if err := rule.CheckActionsConsistency(""); err == nil {
+			rules = append(rules, rule)
+		} else {
+			eventManagerLog(logger.LevelWarn, "rule %q skipped: %v, event %q",
+				rule.Name, err, params.Event)
+		}
+	}
+
+	if len(rules) > 0 {
+		go executeAsyncRulesActions(rules, params)
+	}
+}
+
 // EventParams defines the supported event parameters
 type EventParams struct {
 	Name              string

+ 123 - 0
internal/common/protocol_test.go

@@ -3530,6 +3530,129 @@ func TestEventRuleFsActions(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func TestEventRuleIPBlocked(t *testing.T) {
+	oldConfig := config.GetCommonConfig()
+
+	cfg := config.GetCommonConfig()
+	cfg.DefenderConfig.Enabled = true
+	cfg.DefenderConfig.Threshold = 3
+	cfg.DefenderConfig.ScoreLimitExceeded = 2
+
+	err := common.Initialize(cfg, 0)
+	assert.NoError(t, err)
+
+	smtpCfg := smtp.Config{
+		Host:          "127.0.0.1",
+		Port:          2525,
+		From:          "notification@example.com",
+		TemplatesPath: "templates",
+	}
+	err = smtpCfg.Initialize(configDir)
+	require.NoError(t, err)
+
+	a1 := dataprovider.BaseEventAction{
+		Name: "action1",
+		Type: dataprovider.ActionTypeEmail,
+		Options: dataprovider.BaseEventActionOptions{
+			EmailConfig: dataprovider.EventActionEmailConfig{
+				Recipients: []string{"test3@example.com", "test4@example.com"},
+				Subject:    `New "{{Event}}"`,
+				Body:       "IP: {{IP}} Timestamp: {{Timestamp}}",
+			},
+		},
+	}
+	action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
+	assert.NoError(t, err)
+
+	a2 := dataprovider.BaseEventAction{
+		Name: "action2",
+		Type: dataprovider.ActionTypeFolderQuotaReset,
+	}
+	action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
+	assert.NoError(t, err)
+
+	r1 := dataprovider.EventRule{
+		Name:    "test rule ip blocked",
+		Trigger: dataprovider.EventTriggerIPBlocked,
+		Actions: []dataprovider.EventAction{
+			{
+				BaseEventAction: dataprovider.BaseEventAction{
+					Name: action1.Name,
+				},
+				Order: 1,
+			},
+		},
+	}
+	rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
+	assert.NoError(t, err)
+	r2 := dataprovider.EventRule{
+		Name:    "test rule 2",
+		Trigger: dataprovider.EventTriggerIPBlocked,
+		Actions: []dataprovider.EventAction{
+			{
+				BaseEventAction: dataprovider.BaseEventAction{
+					Name: action1.Name,
+				},
+				Order: 1,
+			},
+			{
+				BaseEventAction: dataprovider.BaseEventAction{
+					Name: action2.Name,
+				},
+				Order: 2,
+			},
+		},
+	}
+	rule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)
+	assert.NoError(t, err)
+
+	u := getTestUser()
+	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
+	assert.NoError(t, err)
+	lastReceivedEmail.reset()
+	time.Sleep(300 * time.Millisecond)
+	assert.Empty(t, lastReceivedEmail.get().From, string(lastReceivedEmail.get().Data))
+
+	for i := 0; i < 3; i++ {
+		user.Password = "wrong_pwd"
+		_, _, err = getSftpClient(user)
+		assert.Error(t, err)
+	}
+	// the client is now banned
+	user.Password = defaultPassword
+	_, _, err = getSftpClient(user)
+	assert.Error(t, err)
+	// check the email notification
+	assert.Eventually(t, func() bool {
+		return lastReceivedEmail.get().From != ""
+	}, 3000*time.Millisecond, 100*time.Millisecond)
+	email := lastReceivedEmail.get()
+	assert.Len(t, email.To, 2)
+	assert.True(t, util.Contains(email.To, "test3@example.com"))
+	assert.True(t, util.Contains(email.To, "test4@example.com"))
+	assert.Contains(t, string(email.Data), `Subject: New "IP Blocked"`)
+
+	err = dataprovider.DeleteEventRule(rule1.Name, "", "")
+	assert.NoError(t, err)
+	err = dataprovider.DeleteEventRule(rule2.Name, "", "")
+	assert.NoError(t, err)
+	err = dataprovider.DeleteEventAction(action1.Name, "", "")
+	assert.NoError(t, err)
+	err = dataprovider.DeleteEventAction(action2.Name, "", "")
+	assert.NoError(t, err)
+	err = dataprovider.DeleteUser(user.Username, "", "")
+	assert.NoError(t, err)
+	err = os.RemoveAll(user.GetHomeDir())
+	assert.NoError(t, err)
+
+	smtpCfg = smtp.Config{}
+	err = smtpCfg.Initialize(configDir)
+	require.NoError(t, err)
+
+	err = common.Initialize(oldConfig, 0)
+	assert.NoError(t, err)
+}
+
 func TestSyncUploadAction(t *testing.T) {
 	if runtime.GOOS == osWindows {
 		t.Skip("this test is not available on Windows")

+ 51 - 15
internal/dataprovider/eventrule.go

@@ -84,10 +84,12 @@ const (
 	// Provider events such as add, update, delete
 	EventTriggerProviderEvent
 	EventTriggerSchedule
+	EventTriggerIPBlocked
 )
 
 var (
-	supportedEventTriggers = []int{EventTriggerFsEvent, EventTriggerProviderEvent, EventTriggerSchedule}
+	supportedEventTriggers = []int{EventTriggerFsEvent, EventTriggerProviderEvent, EventTriggerSchedule,
+		EventTriggerIPBlocked}
 )
 
 func isEventTriggerValid(trigger int) bool {
@@ -100,6 +102,8 @@ func getTriggerTypeAsString(trigger int) string {
 		return "Filesystem event"
 	case EventTriggerProviderEvent:
 		return "Provider event"
+	case EventTriggerIPBlocked:
+		return "IP blocked"
 	default:
 		return "Schedule"
 	}
@@ -881,6 +885,15 @@ func (c *EventConditions) validate(trigger int) error {
 				return err
 			}
 		}
+	case EventTriggerIPBlocked:
+		c.FsEvents = nil
+		c.ProviderEvents = nil
+		c.Options.Names = nil
+		c.Options.FsPaths = nil
+		c.Options.Protocols = nil
+		c.Options.MinFileSize = 0
+		c.Options.MaxFileSize = 0
+		c.Schedules = nil
 	default:
 		c.FsEvents = nil
 		c.ProviderEvents = nil
@@ -1000,24 +1013,43 @@ func (r *EventRule) validate() error {
 	return nil
 }
 
+func (r *EventRule) checkIPBlockedActions() error {
+	unavailableActions := []int{ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
+		ActionTypeDataRetentionCheck, ActionTypeFilesystem}
+	for _, action := range r.Actions {
+		if util.Contains(unavailableActions, action.Type) {
+			return fmt.Errorf("action %q, type %q is not supported for IP blocked events",
+				action.Name, getActionTypeAsString(action.Type))
+		}
+	}
+	return nil
+}
+
+func (r *EventRule) checkProviderEventActions(providerObjectType string) error {
+	// user quota reset, transfer quota reset, data retention check and filesystem actions
+	// can be executed only if we modify a user. They will be executed for the
+	// affected user. Folder quota reset can be executed only for folders.
+	userSpecificActions := []int{ActionTypeUserQuotaReset, ActionTypeTransferQuotaReset,
+		ActionTypeDataRetentionCheck, ActionTypeFilesystem}
+	for _, action := range r.Actions {
+		if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
+			return fmt.Errorf("action %q, type %q is only supported for provider user events",
+				action.Name, getActionTypeAsString(action.Type))
+		}
+		if action.Type == ActionTypeFolderQuotaReset && providerObjectType != actionObjectFolder {
+			return fmt.Errorf("action %q, type %q is only supported for provider folder events",
+				action.Name, getActionTypeAsString(action.Type))
+		}
+	}
+	return nil
+}
+
 // CheckActionsConsistency returns an error if the actions cannot be executed
 func (r *EventRule) CheckActionsConsistency(providerObjectType string) error {
 	switch r.Trigger {
 	case EventTriggerProviderEvent:
-		// user quota reset, transfer quota reset, data retention check and filesystem actions
-		// can be executed only if we modify a user. They will be executed for the
-		// affected user. Folder quota reset can be executed only for folders.
-		userSpecificActions := []int{ActionTypeUserQuotaReset, ActionTypeTransferQuotaReset,
-			ActionTypeDataRetentionCheck, ActionTypeFilesystem}
-		for _, action := range r.Actions {
-			if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
-				return fmt.Errorf("action %q, type %q is only supported for provider user events",
-					action.Name, getActionTypeAsString(action.Type))
-			}
-			if action.Type == ActionTypeFolderQuotaReset && providerObjectType != actionObjectFolder {
-				return fmt.Errorf("action %q, type %q is only supported for provider folder events",
-					action.Name, getActionTypeAsString(action.Type))
-			}
+		if err := r.checkProviderEventActions(providerObjectType); err != nil {
+			return err
 		}
 	case EventTriggerFsEvent:
 		// folder quota reset cannot be executed
@@ -1035,6 +1067,10 @@ func (r *EventRule) CheckActionsConsistency(providerObjectType string) error {
 					action.Name, getActionTypeAsString(action.Type))
 			}
 		}
+	case EventTriggerIPBlocked:
+		if err := r.checkIPBlockedActions(); err != nil {
+			return err
+		}
 	}
 	return nil
 }

+ 4 - 1
templates/webadmin/eventrule.html

@@ -183,7 +183,7 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
                 </div>
             </div>
 
-            <div class="card bg-light mb-3">
+            <div class="card bg-light mb-3 trigger trigger-fs trigger-provider trigger-schedule">
                 <div class="card-header">
                     <b>Name filters</b>
                 </div>
@@ -543,6 +543,9 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
             case 3:
                 $('.trigger-schedule').show();
                 break;
+            case '4':
+            case 4:
+                break;
             default:
                 console.log(`unsupported event trigger type: ${val}`);
         }