event manager: add IP blocked trigger
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
d65c00728a
commit
194c3c13ac
12 changed files with 247 additions and 31 deletions
|
@ -198,7 +198,7 @@ We only provide the slim variant and so the optional `git` dependency is not ava
|
||||||
|
|
||||||
### `sftpgo:<suite>-slim`
|
### `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`
|
### `sftpgo:<suite>-plugins`
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
ARCH=`uname -m`
|
ARCH=`uname -m`
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ The following trigger events are supported:
|
||||||
- `Filesystem events`, for example `upload`, `download` etc.
|
- `Filesystem events`, for example `upload`, `download` etc.
|
||||||
- `Provider events`, for example `add`, `update`, `delete` user or other resources.
|
- `Provider events`, for example `add`, `update`, `delete` user or other resources.
|
||||||
- `Schedules`.
|
- `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.
|
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.
|
- `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.
|
- `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.
|
- `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.
|
||||||
|
|
8
go.mod
8
go.mod
|
@ -3,7 +3,7 @@ module github.com/drakkan/sftpgo/v2
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
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/azcore v1.1.2
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||||
|
@ -66,9 +66,9 @@ require (
|
||||||
go.uber.org/automaxprocs v1.5.1
|
go.uber.org/automaxprocs v1.5.1
|
||||||
gocloud.dev v0.26.0
|
gocloud.dev v0.26.0
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
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/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
|
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
|
||||||
google.golang.org/api v0.92.0
|
google.golang.org/api v0.92.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.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/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||||
github.com/pkg/sftp => github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d
|
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/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
|
||||||
)
|
)
|
||||||
|
|
12
go.sum
12
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.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.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.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
|
||||||
cloud.google.com/go/storage v1.24.0 h1:a4N0gIkx83uoVFGz8B2eAV3OhN90QoWF5OZWLKl39ig=
|
cloud.google.com/go/storage v1.25.0 h1:D2Dn0PslpK7Z3B2AvuUHyIC762bDbGJdlmQlCBR71os=
|
||||||
cloud.google.com/go/storage v1.24.0/go.mod h1:3xrJEFMXBsQLgxwThyjuD3aYlroL0TMRec1ypGUQ0KE=
|
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.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A=
|
||||||
cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM=
|
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=
|
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/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 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
|
||||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
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-20220811173512-bde04f9047cc h1:nWhdNJ31a4S7oBCwIRRPY/QfpOdHl3i3irjrJXrfM7w=
|
||||||
github.com/drakkan/net v0.0.0-20220805164234-d1ea7e8d1b71/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
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 h1:kNk/KRhszPJASp7WvjagNW254aKK643Lu8/fr4/ukiM=
|
||||||
github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
|
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=
|
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-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-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-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-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||||
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-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 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
|
|
@ -46,7 +46,7 @@ renewed by the SFTPGo service
|
||||||
configDir = util.CleanDirInput(configDir)
|
configDir = util.CleanDirInput(configDir)
|
||||||
err := config.LoadConfig(configDir, configFile)
|
err := config.LoadConfig(configDir, configFile)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
acmeConfig := config.GetACMEConfig()
|
acmeConfig := config.GetACMEConfig()
|
||||||
|
|
|
@ -107,6 +107,13 @@ func (d *dbDefender) AddEvent(ip string, event HostEvent) {
|
||||||
if host.Score > d.config.Threshold {
|
if host.Score > d.config.Threshold {
|
||||||
banTime := time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
|
banTime := time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
|
||||||
err = dataprovider.SetDefenderBanTime(ip, util.GetTimeAsMsSinceEpoch(banTime))
|
err = dataprovider.SetDefenderBanTime(ip, util.GetTimeAsMsSinceEpoch(banTime))
|
||||||
|
if err == nil {
|
||||||
|
eventManager.handleIPBlockedEvent(EventParams{
|
||||||
|
Event: ipBlockedEventName,
|
||||||
|
IP: ip,
|
||||||
|
Timestamp: time.Now().UnixNano(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -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)
|
d.banned[ip] = time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
|
||||||
delete(d.hosts, ip)
|
delete(d.hosts, ip)
|
||||||
d.cleanupBanned()
|
d.cleanupBanned()
|
||||||
|
eventManager.handleIPBlockedEvent(EventParams{
|
||||||
|
Event: ipBlockedEventName,
|
||||||
|
IP: ip,
|
||||||
|
Timestamp: time.Now().UnixNano(),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
d.hosts[ip] = hs
|
d.hosts[ip] = hs
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,10 @@ import (
|
||||||
"github.com/drakkan/sftpgo/v2/internal/vfs"
|
"github.com/drakkan/sftpgo/v2/internal/vfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ipBlockedEventName = "IP Blocked"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// eventManager handle the supported event rules actions
|
// eventManager handle the supported event rules actions
|
||||||
eventManager eventRulesContainer
|
eventManager eventRulesContainer
|
||||||
|
@ -71,11 +75,12 @@ func init() {
|
||||||
// eventRulesContainer stores event rules by trigger
|
// eventRulesContainer stores event rules by trigger
|
||||||
type eventRulesContainer struct {
|
type eventRulesContainer struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
lastLoad int64
|
||||||
FsEvents []dataprovider.EventRule
|
FsEvents []dataprovider.EventRule
|
||||||
ProviderEvents []dataprovider.EventRule
|
ProviderEvents []dataprovider.EventRule
|
||||||
Schedules []dataprovider.EventRule
|
Schedules []dataprovider.EventRule
|
||||||
|
IPBlockedEvents []dataprovider.EventRule
|
||||||
schedulesMapping map[string][]cron.EntryID
|
schedulesMapping map[string][]cron.EntryID
|
||||||
lastLoad int64
|
|
||||||
concurrencyGuard chan struct{}
|
concurrencyGuard chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +129,15 @@ func (r *eventRulesContainer) removeRuleInternal(name string) {
|
||||||
return
|
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 {
|
for idx := range r.Schedules {
|
||||||
if r.Schedules[idx].Name == name {
|
if r.Schedules[idx].Name == name {
|
||||||
if schedules, ok := r.schedulesMapping[name]; ok {
|
if schedules, ok := r.schedulesMapping[name]; ok {
|
||||||
|
@ -160,6 +174,9 @@ func (r *eventRulesContainer) addUpdateRuleInternal(rule dataprovider.EventRule)
|
||||||
case dataprovider.EventTriggerProviderEvent:
|
case dataprovider.EventTriggerProviderEvent:
|
||||||
r.ProviderEvents = append(r.ProviderEvents, rule)
|
r.ProviderEvents = append(r.ProviderEvents, rule)
|
||||||
eventManagerLog(logger.LevelDebug, "added rule %q to provider events", rule.Name)
|
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:
|
case dataprovider.EventTriggerSchedule:
|
||||||
for _, schedule := range rule.Conditions.Schedules {
|
for _, schedule := range rule.Conditions.Schedules {
|
||||||
cronSpec := schedule.GetCronSpec()
|
cronSpec := schedule.GetCronSpec()
|
||||||
|
@ -200,8 +217,8 @@ func (r *eventRulesContainer) loadRules() {
|
||||||
r.addUpdateRuleInternal(rule)
|
r.addUpdateRuleInternal(rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
eventManagerLog(logger.LevelDebug, "event rules updated, fs events: %d, provider events: %d, schedules: %d",
|
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.FsEvents), len(r.ProviderEvents), len(r.Schedules), len(r.IPBlockedEvents))
|
||||||
|
|
||||||
r.setLastLoadTime(modTime)
|
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
|
// EventParams defines the supported event parameters
|
||||||
type EventParams struct {
|
type EventParams struct {
|
||||||
Name string
|
Name string
|
||||||
|
|
|
@ -3530,6 +3530,129 @@ func TestEventRuleFsActions(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
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) {
|
func TestSyncUploadAction(t *testing.T) {
|
||||||
if runtime.GOOS == osWindows {
|
if runtime.GOOS == osWindows {
|
||||||
t.Skip("this test is not available on Windows")
|
t.Skip("this test is not available on Windows")
|
||||||
|
|
|
@ -84,10 +84,12 @@ const (
|
||||||
// Provider events such as add, update, delete
|
// Provider events such as add, update, delete
|
||||||
EventTriggerProviderEvent
|
EventTriggerProviderEvent
|
||||||
EventTriggerSchedule
|
EventTriggerSchedule
|
||||||
|
EventTriggerIPBlocked
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
supportedEventTriggers = []int{EventTriggerFsEvent, EventTriggerProviderEvent, EventTriggerSchedule}
|
supportedEventTriggers = []int{EventTriggerFsEvent, EventTriggerProviderEvent, EventTriggerSchedule,
|
||||||
|
EventTriggerIPBlocked}
|
||||||
)
|
)
|
||||||
|
|
||||||
func isEventTriggerValid(trigger int) bool {
|
func isEventTriggerValid(trigger int) bool {
|
||||||
|
@ -100,6 +102,8 @@ func getTriggerTypeAsString(trigger int) string {
|
||||||
return "Filesystem event"
|
return "Filesystem event"
|
||||||
case EventTriggerProviderEvent:
|
case EventTriggerProviderEvent:
|
||||||
return "Provider event"
|
return "Provider event"
|
||||||
|
case EventTriggerIPBlocked:
|
||||||
|
return "IP blocked"
|
||||||
default:
|
default:
|
||||||
return "Schedule"
|
return "Schedule"
|
||||||
}
|
}
|
||||||
|
@ -881,6 +885,15 @@ func (c *EventConditions) validate(trigger int) error {
|
||||||
return err
|
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:
|
default:
|
||||||
c.FsEvents = nil
|
c.FsEvents = nil
|
||||||
c.ProviderEvents = nil
|
c.ProviderEvents = nil
|
||||||
|
@ -1000,24 +1013,43 @@ func (r *EventRule) validate() error {
|
||||||
return nil
|
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
|
// CheckActionsConsistency returns an error if the actions cannot be executed
|
||||||
func (r *EventRule) CheckActionsConsistency(providerObjectType string) error {
|
func (r *EventRule) CheckActionsConsistency(providerObjectType string) error {
|
||||||
switch r.Trigger {
|
switch r.Trigger {
|
||||||
case EventTriggerProviderEvent:
|
case EventTriggerProviderEvent:
|
||||||
// user quota reset, transfer quota reset, data retention check and filesystem actions
|
if err := r.checkProviderEventActions(providerObjectType); err != nil {
|
||||||
// can be executed only if we modify a user. They will be executed for the
|
return err
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case EventTriggerFsEvent:
|
case EventTriggerFsEvent:
|
||||||
// folder quota reset cannot be executed
|
// folder quota reset cannot be executed
|
||||||
|
@ -1035,6 +1067,10 @@ func (r *EventRule) CheckActionsConsistency(providerObjectType string) error {
|
||||||
action.Name, getActionTypeAsString(action.Type))
|
action.Name, getActionTypeAsString(action.Type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case EventTriggerIPBlocked:
|
||||||
|
if err := r.checkIPBlockedActions(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -183,7 +183,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="card-header">
|
||||||
<b>Name filters</b>
|
<b>Name filters</b>
|
||||||
</div>
|
</div>
|
||||||
|
@ -543,6 +543,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
case 3:
|
case 3:
|
||||||
$('.trigger-schedule').show();
|
$('.trigger-schedule').show();
|
||||||
break;
|
break;
|
||||||
|
case '4':
|
||||||
|
case 4:
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.log(`unsupported event trigger type: ${val}`);
|
console.log(`unsupported event trigger type: ${val}`);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue