Explorar el Código

eventmanager: allow to execute fs actions based on schedules

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino hace 3 años
padre
commit
57935f585c
Se han modificado 7 ficheros con 298 adiciones y 85 borrados
  1. 1 2
      docs/eventmanager.md
  2. 9 9
      go.mod
  3. 18 17
      go.sum
  4. 113 17
      internal/common/eventmanager.go
  5. 156 31
      internal/common/eventmanager_test.go
  6. 0 8
      internal/dataprovider/eventrule.go
  7. 1 1
      pkgs/build.sh

+ 1 - 2
docs/eventmanager.md

@@ -45,7 +45,7 @@ The following trigger events are supported:
 
 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.
 
-Actions such as user quota reset, transfer quota reset, data retention check and folder quota reset are executed for all matching users if the trigger is a schedule or for the affected user if the trigger is a provider event or a filesystem action.
+Actions such as user quota reset, transfer quota reset, data retention check, folder quota reset and filesystem events are executed for all matching users if the trigger is a schedule or for the affected user if the trigger is a provider event or a filesystem action.
 
 Actions are executed in a sequential order except for sync actions that are executed before the others. For each action associated to a rule you can define the following settings:
 
@@ -59,6 +59,5 @@ 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.
 - `Certificate`, user quota reset, folder quota reset, transfer quota reset, data retention check and filesystem actions cannot be executed.

+ 9 - 9
go.mod

@@ -15,13 +15,13 @@ require (
 	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27
 	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.12
 	github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5
-	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.17
+	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.18
 	github.com/aws/aws-sdk-go-v2/service/sts v1.16.13
 	github.com/cockroachdb/cockroach-go/v2 v2.2.15
 	github.com/coreos/go-oidc/v3 v3.2.0
 	github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
 	github.com/fclairamb/ftpserverlib v0.19.0
-	github.com/fclairamb/go-log v0.4.0
+	github.com/fclairamb/go-log v0.4.1
 	github.com/go-acme/lego/v4 v4.8.0
 	github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6
 	github.com/go-chi/jwtauth/v5 v5.0.2
@@ -32,7 +32,7 @@ require (
 	github.com/google/uuid v1.3.0
 	github.com/grandcat/zeroconf v1.0.0
 	github.com/hashicorp/go-hclog v1.2.2
-	github.com/hashicorp/go-plugin v1.4.4
+	github.com/hashicorp/go-plugin v1.4.5
 	github.com/hashicorp/go-retryablehttp v0.7.1
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
 	github.com/klauspost/compress v1.15.9
@@ -68,15 +68,15 @@ require (
 	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
 	golang.org/x/net v0.0.0-20220811182439-13a9a731de15
 	golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7
-	golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
+	golang.org/x/sys v0.0.0-20220818161305-2296e01440c6
 	golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
-	google.golang.org/api v0.92.0
+	google.golang.org/api v0.93.0
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 )
 
 require (
 	cloud.google.com/go v0.103.0 // indirect
-	cloud.google.com/go/compute v1.8.0 // indirect
+	cloud.google.com/go/compute v1.9.0 // indirect
 	cloud.google.com/go/iam v0.3.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect
 	github.com/ajg/form v1.5.1 // indirect
@@ -99,12 +99,12 @@ require (
 	github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
+	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
 	github.com/fatih/color v1.13.0 // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
 	github.com/go-test/deep v1.0.8 // indirect
-	github.com/goccy/go-json v0.9.10 // indirect
+	github.com/goccy/go-json v0.9.11 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-cmp v0.5.8 // indirect
@@ -155,7 +155,7 @@ require (
 	golang.org/x/tools v0.1.12 // indirect
 	golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959 // indirect
+	google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1 // indirect
 	google.golang.org/grpc v1.48.0 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect

+ 18 - 17
go.sum

@@ -48,8 +48,8 @@ cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6m
 cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
 cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
 cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
-cloud.google.com/go/compute v1.8.0 h1:NLtR56/eKx9K1s2Tw/4hec2vsU1S3WeKRMj8HXbBo6E=
-cloud.google.com/go/compute v1.8.0/go.mod h1:boQ44qJsMqZjKzzsEkoJWQGj4h8ygmyk17UArClWzmg=
+cloud.google.com/go/compute v1.9.0 h1:ED/FP4xv8GJw63v556/ASNc1CeeLUO2Bs8nzaHchkHg=
+cloud.google.com/go/compute v1.9.0/go.mod h1:lWv1h/zUWTm/LozzfTJhBSkd6ShQq8la8VeeuOEGxfY=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
@@ -191,8 +191,8 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z
 github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5 h1:h9qqTedYnA9JcWjKyLV6UYIMSdp91ExLCUbjbpDLH7A=
 github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5/go.mod h1:J8SS5Tp/zeLxaubB0xGfKnVrvssNBNLwTipreTKLhjQ=
 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.17 h1:x4JtJ0TaVVCoNc3bUtv0W5VvMLFiQ1++ReiRfSxRYf8=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.17/go.mod h1:HvF8QZUW+evBsd/SJn4VA0WWW5qVMKxPpWiRRK4w3eM=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.18 h1:OEPeoMWuUp1SvUvrLMh8B7SJPRz6M1hP/AV4pmXybx4=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.18/go.mod h1:HvF8QZUW+evBsd/SJn4VA0WWW5qVMKxPpWiRRK4w3eM=
 github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw=
 github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM=
 github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0=
@@ -254,8 +254,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
 github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU=
 github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
@@ -286,8 +286,8 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/fclairamb/ftpserverlib v0.19.0 h1:5QcSQ0OIJBlezIqmGehiL/AVsRb6dIkMxbkuhyPkESM=
 github.com/fclairamb/ftpserverlib v0.19.0/go.mod h1:pmukdVOFKKUY9zjWRoxFW8JAljyulC/uK5FfusJzK2E=
-github.com/fclairamb/go-log v0.4.0 h1:HLm0yU9IzNCqayuTqtLyWUy/Bjud7+DZWTSg0lAC5pQ=
-github.com/fclairamb/go-log v0.4.0/go.mod h1:sw1KvnkZ4wKCYkvy4SL3qVZcJSWFP8Ure4pM3z+KNn4=
+github.com/fclairamb/go-log v0.4.1 h1:rLtdSG9x2pK41AIAnE8WYpl05xBJfw1ZyYxZaXFcBsM=
+github.com/fclairamb/go-log v0.4.1/go.mod h1:sw1KvnkZ4wKCYkvy4SL3qVZcJSWFP8Ure4pM3z+KNn4=
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
@@ -338,8 +338,8 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
 github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
-github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
+github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
 github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
@@ -465,8 +465,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-hclog v1.2.2 h1:ihRI7YFwcZdiSD7SIenIhHfQH3OuDvWerAUBZbeQS3M=
 github.com/hashicorp/go-hclog v1.2.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
-github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ=
-github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
+github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo=
+github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
 github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
 github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -976,8 +976,9 @@ 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-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 h1:Sx/u41w+OwrInGdEckYmEuU5gHoGSL4QbDz3S9s6j4U=
+golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/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=
@@ -1120,8 +1121,8 @@ google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3
 google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
 google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
 google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
-google.golang.org/api v0.92.0 h1:8JHk7q/+rJla+iRsWj9FQ9/wjv2M1SKtpKSdmLhxPT0=
-google.golang.org/api v0.92.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
+google.golang.org/api v0.93.0 h1:T2xt9gi0gHdxdnRkVQhT8mIvPaXKNsDNWz+L696M66M=
+google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1228,8 +1229,8 @@ google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljW
 google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
-google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959 h1:hw4Y42zL1VyVKxPgRHHh191fpVBGV8sNVmcow5Z8VXY=
-google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
+google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1 h1:C2UVWqrgLYKrT5nh5oU6hLRm1AeEklCK5eloQA1NtFY=
+google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=

+ 113 - 17
internal/common/eventmanager.go

@@ -628,8 +628,8 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params EventP
 	return err
 }
 
-func getUserForEventAction(username string) (dataprovider.User, error) {
-	user, err := dataprovider.GetUserWithGroupSettings(username)
+func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
+	err := user.LoadAndApplyGroupSettings()
 	if err != nil {
 		return dataprovider.User{}, err
 	}
@@ -649,8 +649,8 @@ func executeDeleteFileFsAction(conn *BaseConnection, item string, info os.FileIn
 	return conn.RemoveFile(fs, fsPath, item, info)
 }
 
-func executeDeleteFsAction(deletes []string, replacer *strings.Replacer, username string) error {
-	user, err := getUserForEventAction(username)
+func executeDeleteFsActionForUser(deletes []string, replacer *strings.Replacer, user dataprovider.User) error {
+	user, err := getUserForEventAction(user)
 	if err != nil {
 		return err
 	}
@@ -684,8 +684,40 @@ func executeDeleteFsAction(deletes []string, replacer *strings.Replacer, usernam
 	return nil
 }
 
-func executeMkDirsFsAction(dirs []string, replacer *strings.Replacer, username string) error {
-	user, err := getUserForEventAction(username)
+func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer,
+	conditions dataprovider.ConditionOptions, params EventParams,
+) error {
+	users, err := params.getUsers()
+	if err != nil {
+		return fmt.Errorf("unable to get users: %w", err)
+	}
+	var failures []string
+	executed := 0
+	for _, user := range users {
+		// if sender is set, the conditions have already been evaluated
+		if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
+			eventManagerLog(logger.LevelDebug, "skipping fs delete for user %s, name conditions don't match",
+				user.Username)
+			continue
+		}
+		executed++
+		if err = executeDeleteFsActionForUser(deletes, replacer, user); err != nil {
+			failures = append(failures, user.Username)
+			continue
+		}
+	}
+	if len(failures) > 0 {
+		return fmt.Errorf("fs delete failed for users: %+v", failures)
+	}
+	if executed == 0 {
+		eventManagerLog(logger.LevelError, "no delete executed")
+		return errors.New("no delete executed")
+	}
+	return nil
+}
+
+func executeMkDirsFsActionForUser(dirs []string, replacer *strings.Replacer, user dataprovider.User) error {
+	user, err := getUserForEventAction(user)
 	if err != nil {
 		return err
 	}
@@ -709,8 +741,42 @@ func executeMkDirsFsAction(dirs []string, replacer *strings.Replacer, username s
 	return nil
 }
 
-func executeRenameFsAction(renames []dataprovider.KeyValue, replacer *strings.Replacer, username string) error {
-	user, err := getUserForEventAction(username)
+func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,
+	conditions dataprovider.ConditionOptions, params EventParams,
+) error {
+	users, err := params.getUsers()
+	if err != nil {
+		return fmt.Errorf("unable to get users: %w", err)
+	}
+	var failures []string
+	executed := 0
+	for _, user := range users {
+		// if sender is set, the conditions have already been evaluated
+		if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
+			eventManagerLog(logger.LevelDebug, "skipping fs mkdir for user %s, name conditions don't match",
+				user.Username)
+			continue
+		}
+		executed++
+		if err = executeMkDirsFsActionForUser(dirs, replacer, user); err != nil {
+			failures = append(failures, user.Username)
+			continue
+		}
+	}
+	if len(failures) > 0 {
+		return fmt.Errorf("fs mkdir failed for users: %+v", failures)
+	}
+	if executed == 0 {
+		eventManagerLog(logger.LevelError, "no mkdir executed")
+		return errors.New("no mkdir executed")
+	}
+	return nil
+}
+
+func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *strings.Replacer,
+	user dataprovider.User,
+) error {
+	user, err := getUserForEventAction(user)
 	if err != nil {
 		return err
 	}
@@ -732,17 +798,51 @@ func executeRenameFsAction(renames []dataprovider.KeyValue, replacer *strings.Re
 	return nil
 }
 
-func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, params EventParams) error {
+func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *strings.Replacer,
+	conditions dataprovider.ConditionOptions, params EventParams,
+) error {
+	users, err := params.getUsers()
+	if err != nil {
+		return fmt.Errorf("unable to get users: %w", err)
+	}
+	var failures []string
+	executed := 0
+	for _, user := range users {
+		// if sender is set, the conditions have already been evaluated
+		if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
+			eventManagerLog(logger.LevelDebug, "skipping fs rename for user %s, name conditions don't match",
+				user.Username)
+			continue
+		}
+		executed++
+		if err = executeRenameFsActionForUser(renames, replacer, user); err != nil {
+			failures = append(failures, user.Username)
+			continue
+		}
+	}
+	if len(failures) > 0 {
+		return fmt.Errorf("fs rename failed for users: %+v", failures)
+	}
+	if executed == 0 {
+		eventManagerLog(logger.LevelError, "no rename executed")
+		return errors.New("no rename executed")
+	}
+	return nil
+}
+
+func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions dataprovider.ConditionOptions,
+	params EventParams,
+) error {
 	addObjectData := false
 	replacements := params.getStringReplacements(addObjectData)
 	replacer := strings.NewReplacer(replacements...)
 	switch c.Type {
 	case dataprovider.FilesystemActionRename:
-		return executeRenameFsAction(c.Renames, replacer, params.sender)
+		return executeRenameFsRuleAction(c.Renames, replacer, conditions, params)
 	case dataprovider.FilesystemActionDelete:
-		return executeDeleteFsAction(c.Deletes, replacer, params.sender)
+		return executeDeleteFsRuleAction(c.Deletes, replacer, conditions, params)
 	case dataprovider.FilesystemActionMkdirs:
-		return executeMkDirsFsAction(c.MkDirs, replacer, params.sender)
+		return executeMkdirFsRuleAction(c.MkDirs, replacer, conditions, params)
 	default:
 		return fmt.Errorf("unsupported filesystem action %d", c.Type)
 	}
@@ -953,7 +1053,7 @@ func executeRuleAction(action dataprovider.BaseEventAction, params EventParams,
 	case dataprovider.ActionTypeDataRetentionCheck:
 		return executeDataRetentionCheckRuleAction(action.Options.RetentionConfig, conditions, params)
 	case dataprovider.ActionTypeFilesystem:
-		return executeFsRuleAction(action.Options.FsConfig, params)
+		return executeFsRuleAction(action.Options.FsConfig, conditions, params)
 	default:
 		return fmt.Errorf("unsupported action type: %d", action.Type)
 	}
@@ -1070,10 +1170,6 @@ func (j *eventCronJob) Run() {
 		eventManagerLog(logger.LevelError, "unable to load rule with name %q", j.ruleName)
 		return
 	}
-	if err = rule.CheckActionsConsistency(""); err != nil {
-		eventManagerLog(logger.LevelWarn, "scheduled rule %q skipped: %v", rule.Name, err)
-		return
-	}
 	task, err := j.getTask(rule)
 	if err != nil {
 		return

+ 156 - 31
internal/common/eventmanager_test.go

@@ -275,10 +275,18 @@ func TestEventManagerErrors(t *testing.T) {
 	assert.Error(t, err)
 	err = executeTransferQuotaResetRuleAction(dataprovider.ConditionOptions{}, EventParams{})
 	assert.Error(t, err)
+	err = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{})
+	assert.Error(t, err)
+	err = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{})
+	assert.Error(t, err)
+	err = executeRenameFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{})
+	assert.Error(t, err)
+
+	groupName := "agroup"
 	err = executeQuotaResetForUser(dataprovider.User{
 		Groups: []sdk.GroupMapping{
 			{
-				Name: "agroup",
+				Name: groupName,
 				Type: sdk.GroupTypePrimary,
 			},
 		},
@@ -287,12 +295,39 @@ func TestEventManagerErrors(t *testing.T) {
 	err = executeDataRetentionCheckForUser(dataprovider.User{
 		Groups: []sdk.GroupMapping{
 			{
-				Name: "agroup",
+				Name: groupName,
 				Type: sdk.GroupTypePrimary,
 			},
 		},
 	}, nil)
 	assert.Error(t, err)
+	err = executeDeleteFsActionForUser(nil, nil, dataprovider.User{
+		Groups: []sdk.GroupMapping{
+			{
+				Name: groupName,
+				Type: sdk.GroupTypePrimary,
+			},
+		},
+	})
+	assert.Error(t, err)
+	err = executeMkDirsFsActionForUser(nil, nil, dataprovider.User{
+		Groups: []sdk.GroupMapping{
+			{
+				Name: groupName,
+				Type: sdk.GroupTypePrimary,
+			},
+		},
+	})
+	assert.Error(t, err)
+	err = executeRenameFsActionForUser(nil, nil, dataprovider.User{
+		Groups: []sdk.GroupMapping{
+			{
+				Name: groupName,
+				Type: sdk.GroupTypePrimary,
+			},
+		},
+	})
+	assert.Error(t, err)
 
 	dataRetentionAction := dataprovider.BaseEventAction{
 		Type: dataprovider.ActionTypeDataRetentionCheck,
@@ -633,6 +668,60 @@ func TestEventRuleActions(t *testing.T) {
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "no transfer quota reset executed")
 	}
+	action.Type = dataprovider.ActionTypeFilesystem
+	action.Options = dataprovider.BaseEventActionOptions{
+		FsConfig: dataprovider.EventActionFilesystemConfig{
+			Type: dataprovider.FilesystemActionRename,
+			Renames: []dataprovider.KeyValue{
+				{
+					Key:   "/source",
+					Value: "/target",
+				},
+			},
+		},
+	}
+	err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{
+		Names: []dataprovider.ConditionPattern{
+			{
+				Pattern: "no match",
+			},
+		},
+	})
+	if assert.Error(t, err) {
+		assert.Contains(t, err.Error(), "no rename executed")
+	}
+	action.Options = dataprovider.BaseEventActionOptions{
+		FsConfig: dataprovider.EventActionFilesystemConfig{
+			Type:    dataprovider.FilesystemActionDelete,
+			Deletes: []string{"/dir1"},
+		},
+	}
+	err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{
+		Names: []dataprovider.ConditionPattern{
+			{
+				Pattern: "no match",
+			},
+		},
+	})
+	if assert.Error(t, err) {
+		assert.Contains(t, err.Error(), "no delete executed")
+	}
+	action.Options = dataprovider.BaseEventActionOptions{
+		FsConfig: dataprovider.EventActionFilesystemConfig{
+			Type:    dataprovider.FilesystemActionMkdirs,
+			Deletes: []string{"/dir1"},
+		},
+	}
+	err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{
+		Names: []dataprovider.ConditionPattern{
+			{
+				Pattern: "no match",
+			},
+		},
+	})
+	if assert.Error(t, err) {
+		assert.Contains(t, err.Error(), "no mkdir executed")
+	}
 
 	err = dataprovider.DeleteUser(username1, "", "")
 	assert.NoError(t, err)
@@ -712,19 +801,12 @@ func TestEventRuleActions(t *testing.T) {
 }
 
 func TestFilesystemActionErrors(t *testing.T) {
-	err := executeFsRuleAction(dataprovider.EventActionFilesystemConfig{}, EventParams{})
+	err := executeFsRuleAction(dataprovider.EventActionFilesystemConfig{}, dataprovider.ConditionOptions{}, EventParams{})
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "unsupported filesystem action")
 	}
 	username := "test_user_for_actions"
 	testReplacer := strings.NewReplacer("old", "new")
-	err = executeDeleteFsAction(nil, testReplacer, username)
-	assert.Error(t, err)
-	err = executeMkDirsFsAction(nil, testReplacer, username)
-	assert.Error(t, err)
-	err = executeRenameFsAction(nil, testReplacer, username)
-	assert.Error(t, err)
-
 	user := dataprovider.User{
 		BaseUser: sdk.BaseUser{
 			Username: username,
@@ -750,11 +832,11 @@ func TestFilesystemActionErrors(t *testing.T) {
 	err = dataprovider.AddUser(&user, "", "")
 	assert.NoError(t, err)
 	// check root fs fails
-	err = executeDeleteFsAction(nil, testReplacer, username)
+	err = executeDeleteFsActionForUser(nil, testReplacer, user)
 	assert.Error(t, err)
-	err = executeMkDirsFsAction(nil, testReplacer, username)
+	err = executeMkDirsFsActionForUser(nil, testReplacer, user)
 	assert.Error(t, err)
-	err = executeRenameFsAction(nil, testReplacer, username)
+	err = executeRenameFsActionForUser(nil, testReplacer, user)
 	assert.Error(t, err)
 
 	user.FsConfig.Provider = sdk.LocalFilesystemProvider
@@ -763,15 +845,36 @@ func TestFilesystemActionErrors(t *testing.T) {
 	assert.NoError(t, err)
 	err = dataprovider.AddUser(&user, "", "")
 	assert.NoError(t, err)
-	err = executeRenameFsAction([]dataprovider.KeyValue{
+	err = executeRenameFsActionForUser([]dataprovider.KeyValue{
 		{
 			Key:   "/p1",
 			Value: "/p1",
 		},
-	}, testReplacer, username)
+	}, testReplacer, user)
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "the rename source and target cannot be the same")
 	}
+	err = executeRuleAction(dataprovider.BaseEventAction{
+		Type: dataprovider.ActionTypeFilesystem,
+		Options: dataprovider.BaseEventActionOptions{
+			FsConfig: dataprovider.EventActionFilesystemConfig{
+				Type: dataprovider.FilesystemActionRename,
+				Renames: []dataprovider.KeyValue{
+					{
+						Key:   "/p2",
+						Value: "/p2",
+					},
+				},
+			},
+		},
+	}, EventParams{}, dataprovider.ConditionOptions{
+		Names: []dataprovider.ConditionPattern{
+			{
+				Pattern: username,
+			},
+		},
+	})
+	assert.Error(t, err)
 
 	if runtime.GOOS != osWindows {
 		dirPath := filepath.Join(user.HomeDir, "adir", "sub")
@@ -783,26 +886,59 @@ func TestFilesystemActionErrors(t *testing.T) {
 		err = os.Chmod(dirPath, 0001)
 		assert.NoError(t, err)
 
-		err = executeDeleteFsAction([]string{"/adir/sub"}, testReplacer, username)
+		err = executeDeleteFsActionForUser([]string{"/adir/sub"}, testReplacer, user)
 		assert.Error(t, err)
-		err = executeDeleteFsAction([]string{"/adir/sub/f.dat"}, testReplacer, username)
+		err = executeDeleteFsActionForUser([]string{"/adir/sub/f.dat"}, testReplacer, user)
 		assert.Error(t, err)
 		err = os.Chmod(dirPath, 0555)
 		assert.NoError(t, err)
-		err = executeDeleteFsAction([]string{"/adir/sub/f.dat"}, testReplacer, username)
+		err = executeDeleteFsActionForUser([]string{"/adir/sub/f.dat"}, testReplacer, user)
 		if assert.Error(t, err) {
 			assert.Contains(t, err.Error(), "unable to remove file")
 		}
+		err = executeRuleAction(dataprovider.BaseEventAction{
+			Type: dataprovider.ActionTypeFilesystem,
+			Options: dataprovider.BaseEventActionOptions{
+				FsConfig: dataprovider.EventActionFilesystemConfig{
+					Type:    dataprovider.FilesystemActionDelete,
+					Deletes: []string{"/adir/sub/f.dat"},
+				},
+			},
+		}, EventParams{}, dataprovider.ConditionOptions{
+			Names: []dataprovider.ConditionPattern{
+				{
+					Pattern: username,
+				},
+			},
+		})
+		assert.Error(t, err)
 
-		err = executeMkDirsFsAction([]string{"/adir/sub/sub"}, testReplacer, username)
+		err = executeMkDirsFsActionForUser([]string{"/adir/sub/sub"}, testReplacer, user)
 		if assert.Error(t, err) {
 			assert.Contains(t, err.Error(), "unable to create dir")
 		}
-		err = executeMkDirsFsAction([]string{"/adir/sub/sub/sub"}, testReplacer, username)
+		err = executeMkDirsFsActionForUser([]string{"/adir/sub/sub/sub"}, testReplacer, user)
 		if assert.Error(t, err) {
 			assert.Contains(t, err.Error(), "unable to check parent dirs")
 		}
 
+		err = executeRuleAction(dataprovider.BaseEventAction{
+			Type: dataprovider.ActionTypeFilesystem,
+			Options: dataprovider.BaseEventActionOptions{
+				FsConfig: dataprovider.EventActionFilesystemConfig{
+					Type:   dataprovider.FilesystemActionMkdirs,
+					MkDirs: []string{"/adir/sub/sub1"},
+				},
+			},
+		}, EventParams{}, dataprovider.ConditionOptions{
+			Names: []dataprovider.ConditionPattern{
+				{
+					Pattern: username,
+				},
+			},
+		})
+		assert.Error(t, err)
+
 		err = os.Chmod(dirPath, os.ModePerm)
 		assert.NoError(t, err)
 	}
@@ -944,17 +1080,6 @@ func TestScheduledActions(t *testing.T) {
 	job.Run()
 	assert.DirExists(t, backupsPath)
 
-	action.Type = dataprovider.ActionTypeFilesystem
-	action.Options = dataprovider.BaseEventActionOptions{
-		FsConfig: dataprovider.EventActionFilesystemConfig{
-			Type:   dataprovider.FilesystemActionMkdirs,
-			MkDirs: []string{"/dir"},
-		},
-	}
-	err = dataprovider.UpdateEventAction(action, "", "")
-	assert.NoError(t, err)
-	job.Run() // action is not compatible with a scheduled rule
-
 	err = dataprovider.DeleteEventRule(rule.Name, "", "")
 	assert.NoError(t, err)
 	err = dataprovider.DeleteEventAction(action.Name, "", "")

+ 0 - 8
internal/dataprovider/eventrule.go

@@ -1062,14 +1062,6 @@ func (r *EventRule) CheckActionsConsistency(providerObjectType string) error {
 					action.Name, getActionTypeAsString(action.Type))
 			}
 		}
-	case EventTriggerSchedule:
-		// to execute a filesystem action we need a user
-		for _, action := range r.Actions {
-			if action.Type == ActionTypeFilesystem {
-				return fmt.Errorf("action %q, type %q is not supported for scheduled events",
-					action.Name, getActionTypeAsString(action.Type))
-			}
-		}
 	case EventTriggerIPBlocked, EventTriggerCertificate:
 		if err := r.checkIPBlockedAndCertificateActions(); err != nil {
 			return err

+ 1 - 1
pkgs/build.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 
-NFPM_VERSION=2.17.0
+NFPM_VERSION=2.18.1
 NFPM_ARCH=${NFPM_ARCH:-amd64}
 if [ -z ${SFTPGO_VERSION} ]
 then