diff --git a/README.md b/README.md
index eac931e5..25664756 100644
--- a/README.md
+++ b/README.md
@@ -96,8 +96,10 @@ SFTPGo is developed and tested on Linux. After each commit, the code is automati
## Requirements
- Go as build only dependency. We support the Go version(s) used in [continuous integration workflows](./.github/workflows).
-- A suitable SQL server to use as data provider: PostgreSQL 9.4+, MySQL 5.6+, SQLite 3.x, CockroachDB stable.
-- The SQL server is optional: you can choose to use an embedded bolt database as key/value store or an in memory data provider.
+- A suitable SQL server to use as data provider:
+ - upstream supported versions of PostgreSQL, MySQL and MariaDB.
+ - CockroachDB stable.
+- The SQL server is optional: you can choose to use an embedded SQLite, bolt or in memory data provider.
## Installation
@@ -238,16 +240,18 @@ The `revertprovider` command is not supported for the memory provider.
Please note that we only support the current release branch and the current main branch, if you find a bug it is better to report it rather than downgrading to an older unsupported version.
-## Users and folders management
+## Users, groups and folders management
-After starting SFTPGo you can manage users and folders using:
+After starting SFTPGo you can manage users, groups, folders and other resources using:
- the [web based administration interface](./docs/web-admin.md)
- the [REST API](./docs/rest-api.md)
-To support embedded data providers like `bolt` and `SQLite` we can't have a CLI that directly write users and folders to the data provider, we always have to use the REST API.
+To support embedded data providers like `bolt` and `SQLite`, which do not support concurrent connections, we can't have a CLI that directly write users and other resources to the data provider, we always have to use the REST API.
-Full details for users, folders, admins and other resources are documented in the [OpenAPI](./openapi/openapi.yaml) schema. If you want to render the schema without importing it manually, you can explore it on [Stoplight](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml).
+Full details for users, groups, folders, admins and other resources are documented in the [OpenAPI](./openapi/openapi.yaml) schema. If you want to render the schema without importing it manually, you can explore it on [Stoplight](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml).
+
+:warning: SFTPGo users, groups and folders are virtual and therefore unrelated to the system ones. There is no need to create system-wide users and groups.
## Tutorials
diff --git a/docs/custom-actions.md b/docs/custom-actions.md
index 6abb00c6..d7005ff8 100644
--- a/docs/custom-actions.md
+++ b/docs/custom-actions.md
@@ -43,7 +43,7 @@ If the `hook` defines a path to an external program, then this program can read
- `SFTPGO_ACTION_BUCKET`, non-empty for S3, GCS and Azure backends
- `SFTPGO_ACTION_ENDPOINT`, non-empty for S3, SFTP and Azure backend if configured
- `SFTPGO_ACTION_STATUS`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error
-- `SFTPGO_ACTION_PROTOCOL`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`, `HTTPShare`, `OIDC`, `DataRetention`
+- `SFTPGO_ACTION_PROTOCOL`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`, `HTTPShare`, `OIDC`, `DataRetention`, `EventAction`
- `SFTPGO_ACTION_IP`, the action was executed from this IP address
- `SFTPGO_ACTION_SESSION_ID`, string. Unique protocol session identifier. For stateless protocols such as HTTP the session id will change for each request
- `SFTPGO_ACTION_OPEN_FLAGS`, integer. File open flags, can be non-zero for `pre-upload` action. If `SFTPGO_ACTION_FILE_SIZE` is greater than zero and `SFTPGO_ACTION_OPEN_FLAGS&512 == 0` the target file will not be truncated
@@ -66,7 +66,7 @@ If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. Th
- `bucket`, string, included for S3, GCS and Azure backends
- `endpoint`, string, included for S3, SFTP and Azure backend if configured
- `status`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error
-- `protocol`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`, `HTTPShare`, `OIDC`, `DataRetention`
+- `protocol`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`, `HTTPShare`, `OIDC`, `DataRetention`, `EventAction`
- `ip`, string. The action was executed from this IP address
- `session_id`, string. Unique protocol session identifier. For stateless protocols such as HTTP the session id will change for each request
- `open_flags`, integer. File open flags, can be non-zero for `pre-upload` action. If `file_size` is greater than zero and `file_size&512 == 0` the target file will not be truncated
diff --git a/docs/eventmanager.md b/docs/eventmanager.md
index ac5e4a35..d892d146 100644
--- a/docs/eventmanager.md
+++ b/docs/eventmanager.md
@@ -12,6 +12,10 @@ The following actions are supported:
- `Folder quota reset`. The quota used by virtual folders will be updated based on current usage.
- `Transfer quota reset`. The transfer quota values will be reset to `0`.
- `Data retention check`. You can define per-folder retention policies.
+- `Filesystem`. For these actions, the required permissions are automatically granted. This is the same as executing the actions from an SFTP client and the same restrictions applies. Supported actions:
+ - `Rename`. You can rename one or more files or directories.
+ - `Delete`. You can delete one or more files and directories.
+ - `Create directories`. You can create one or more directories including sub-directories.
The following placeholders are supported:
@@ -34,15 +38,23 @@ Event rules are based on the premise that an event occours. To each rule you can
The following trigger events are supported:
- `Filesystem events`, for example `upload`, `download` etc.
-- `Provider events`, for example add/update/delete user.
+- `Provider events`, for example `add`, `update`, `delete` user or other resources.
- `Schedules`.
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 are executed in a sequential order. For each action associated to a rule you can define the following settings:
+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 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:
- `Stop on failure`, the next action will not be executed if the current one fails.
- `Failure action`, this action will be executed only if at least another one fails.
- `Execute sync`, for upload events, you can execute the action synchronously. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your action have completed its execution. If your acion takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection.
If you are running multiple SFTPGo instances connected to the same data provider, you can choose whether to allow simultaneous execution for scheduled actions.
+
+Some actions are not supported for some triggers, rules containing incompatible actions are skipped at runtime:
+
+- `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.
diff --git a/go.mod b/go.mod
index 1c46e2f2..ff7b17d0 100644
--- a/go.mod
+++ b/go.mod
@@ -4,19 +4,19 @@ go 1.19
require (
cloud.google.com/go/storage v1.24.0
- github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1
+ 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
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
- github.com/aws/aws-sdk-go-v2 v1.16.8
- github.com/aws/aws-sdk-go-v2/config v1.15.15
- github.com/aws/aws-sdk-go-v2/credentials v1.12.10
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9
- github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.21
- github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.9
- github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2
- github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14
- github.com/aws/aws-sdk-go-v2/service/sts v1.16.10
+ github.com/aws/aws-sdk-go-v2 v1.16.10
+ github.com/aws/aws-sdk-go-v2/config v1.15.17
+ github.com/aws/aws-sdk-go-v2/credentials v1.12.12
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.11
+ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.23
+ github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.11
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.27.4
+ github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.16
+ github.com/aws/aws-sdk-go-v2/service/sts v1.16.12
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
@@ -46,7 +46,7 @@ require (
github.com/pires/go-proxyproto v0.6.2
github.com/pkg/sftp v1.13.5
github.com/pquerna/otp v1.3.0
- github.com/prometheus/client_golang v1.12.2
+ github.com/prometheus/client_golang v1.13.0
github.com/robfig/cron/v3 v3.0.1
github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5
github.com/rs/xid v1.4.0
@@ -67,8 +67,8 @@ require (
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/oauth2 v0.0.0-20220722155238-128564f6959c
- golang.org/x/sys v0.0.0-20220804214406-8e32c043e418
+ golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7
+ golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
google.golang.org/api v0.91.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
@@ -76,21 +76,21 @@ require (
require (
cloud.google.com/go v0.103.0 // indirect
- cloud.google.com/go/compute v1.7.0 // indirect
+ cloud.google.com/go/compute v1.8.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
- github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.9 // indirect
- github.com/aws/aws-sdk-go-v2/internal/ini v1.3.16 // indirect
- github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.6 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.10 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.9 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.9 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.11.13 // indirect
- github.com/aws/smithy-go v1.12.0 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.17 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.11 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.3.18 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.8 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.12 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.11 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.11 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.11.15 // indirect
+ github.com/aws/smithy-go v1.12.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
@@ -113,7 +113,7 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
- github.com/inconshreveable/mousetrap v1.0.0 // indirect
+ github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
github.com/kr/fs v0.1.0 // indirect
@@ -155,10 +155,10 @@ 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-20220805133916-01dd62135a58 // indirect
+ google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c // indirect
google.golang.org/grpc v1.48.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
- gopkg.in/ini.v1 v1.66.6 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index f462aed3..7303ca41 100644
--- a/go.sum
+++ b/go.sum
@@ -47,8 +47,9 @@ cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJW
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
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 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk=
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/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=
@@ -91,8 +92,8 @@ github.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/azure-sdk-for-go v59.3.0+incompatible h1:dPIm0BO4jsMXFcCI/sLTPkBtE7mk8WMuRHA0JeWhlcQ=
github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1 h1:tz19qLF65vuu2ibfTqGVJxG/zZAI27NEIIbvAOQwYbw=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2 h1:lneMk5qtUMulXa/eVxjVd+/bDYMEDIqYpLzLa2/EsNI=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 h1:Yoicul8bnVdQrhDMTHxdEckRGX01XvwXDHUT9zYZ3k0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
@@ -143,67 +144,67 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
-github.com/aws/aws-sdk-go-v2 v1.16.8 h1:gOe9UPR98XSf7oEJCcojYg+N2/jCRm4DdeIsP85pIyQ=
-github.com/aws/aws-sdk-go-v2 v1.16.8/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw=
+github.com/aws/aws-sdk-go-v2 v1.16.10 h1:+yDD0tcuHRQZgqONkpDwzepqmElQaSlFPymHRHR9mrc=
+github.com/aws/aws-sdk-go-v2 v1.16.10/go.mod h1:WTACcleLz6VZTp7fak4EO5b9Q4foxbn+8PIz3PmyKlo=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 h1:S/ZBwevQkr7gv5YxONYpGQxlMFFYSRfz3RMcjsC9Qhk=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3/go.mod h1:gNsR5CaXKmQSSzrmGxmwmct/r+ZBfbxorAuXYsj/M5Y=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 h1:zfT11pa7ifu/VlLDpmc5OY2W4nYmnKkFDGeMVnmqAI0=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4/go.mod h1:ES0I1GBs+YYgcDS1ek47Erbn4TOL811JKqBXtgzqyZ8=
github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg=
-github.com/aws/aws-sdk-go-v2/config v1.15.15 h1:yBV+J7Au5KZwOIrIYhYkTGJbifZPCkAnCFSvGsF3ui8=
-github.com/aws/aws-sdk-go-v2/config v1.15.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E=
+github.com/aws/aws-sdk-go-v2/config v1.15.17 h1:cM/4dqEPc5SjBOeYVdUI7iL/B6jDupCesXzg3AuUzRE=
+github.com/aws/aws-sdk-go-v2/config v1.15.17/go.mod h1:eatrtwIm5WdvASoYCy5oPkinfiwiYFg2jLG9tJoKzkE=
github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g=
-github.com/aws/aws-sdk-go-v2/credentials v1.12.10 h1:7gGcMQePejwiKoDWjB9cWnpfVdnz/e5JwJFuT6OrroI=
-github.com/aws/aws-sdk-go-v2/credentials v1.12.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk=
+github.com/aws/aws-sdk-go-v2/credentials v1.12.12 h1:iShu6VaWZZZfUZvlGtRjl+g1lWk44g1QmiCTD4KS0jI=
+github.com/aws/aws-sdk-go-v2/credentials v1.12.12/go.mod h1:vFHC2HifIWHebmoVsfpqliKuqbAY2LaVlvy03JzF4c4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9 h1:hz8tc+OW17YqxyFFPSkvfSikbqWcyyHRyPVSTzC0+aI=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.11 h1:zZHPdM2x09/0F8D7XyVvQnP2/jaW7bEMmtcSCPYq/iI=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.11/go.mod h1:38Asv/UyQbDNpSXCurZRlDMjzIl6J+wUe8vY3TtUuzA=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.21 h1:bpiKFJ9aC0xTVpygSRRRL/YHC1JZ+pHQHENATHuoiwo=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.21/go.mod h1:iIYPrQ2rYfZiB/iADYlhj9HHZ9TTi6PqKQPAqygohbE=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.23 h1:lzS1GSHBzvBMlCA030/ecL5tF2ip8RLr/LBq5fBpv/4=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.23/go.mod h1:yGuKwoNVv2eGUHlp7ciCQLHmFNeESebnHucZfRL9EkA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15 h1:bx5F2mr6H6FC7zNIQoDoUr8wEKnvmwRncujT3FYRtic=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.17 h1:U8DZvyFFesBmK62dYC6BRXm4Cd/wPP3aPcecu3xv/F4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.17/go.mod h1:6qtGip7sJEyvgsLjphRZWF9qPe3xJf1mL/MM01E35Wc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.9 h1:5sbyznZC2TeFpa4fvtpvpcGbzeXEEs1l1Jo51ynUNsQ=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.9/go.mod h1:08tUpeSGN33QKSO7fwxXczNfiwCpbj+GxK6XKwqWVv0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.11 h1:GMp98usVW5tzQhxd26KWhoNQPlR2noIlfbzqjVGBhLU=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.11/go.mod h1:cYAfnB+9ZkmZWpQWmPDsuIGm4EA+6k2ZVtxKjw/XJBY=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.3.16 h1:f0ySVcmQhwmzn7zQozd8wBM3yuGBfzdpsOaKQ0/Epzw=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.3.16/go.mod h1:CYmI+7x03jjJih8kBEEFKRQc40UjUokT0k7GbvrhhTc=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.6 h1:3L8pcjvgaSOs0zzZcMKzxDSkYKEpwJ2dNVDdxm68jAY=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.6/go.mod h1:O7Oc4peGZDEKlddivslfYFvAbgzvl/GH3J8j3JIGBXc=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.18 h1:/spg6h3tG4pefphbvhpgdMtFMegSajPPSEJd1t8lnpc=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.18/go.mod h1:hTHq8hL4bAxJyng364s9d4IUGXZOs7Y5LSqAhIiIQ2A=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.8 h1:9PY5a+kHQzC6d9eR+KLNSJP3DHDLYmPFA5/+eSDBo9o=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.8/go.mod h1:pcQfUOFVK4lMnSzgX3dCA81UsA9YCilRUSYgkjSU2i8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 h1:4n4KCtv5SUoT5Er5XV41huuzrCqepxlW3SDI9qHQebc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3/go.mod h1:gkb2qADY+OHaGLKNTYxMaQNacfeyQpZ4csDTQMeFmcw=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.4 h1:akfcyqM9SvrBKWZOkBcXAGDrHfKaEP4Aca8H/bCiLW8=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.4/go.mod h1:oehQLbMQkppKLXvpx/1Eo0X47Fe+0971DXC9UjGnKcI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.10 h1:7LJcuRalaLw+GYQTMGmVUl4opg2HrDZkvn/L3KvIQfw=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.10/go.mod h1:Qks+dxK3O+Z2deAhNo6cJ8ls1bam3tUGUAcgxQP1c70=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.12 h1:eNQYkKjDSLDjIbBQ85rIkjpBGgnavrl/U3YKDdxAz14=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.12/go.mod h1:k2HaF2yfT082M+kKo3Xdf4rd5HGKvDmrPC5Kwzc2KUw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.9 h1:sHfDuhbOuuWSIAEDd3pma6p0JgUcR2iePxtCE8gfCxQ=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.9/go.mod h1:yQowTpvdZkFVuHrLBXmczat4W+WJKg/PafBZnGBLga0=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.11 h1:GkYtp4gi4wdWUV+pPetjk5y2aDxbr0t8n5OjVBwZdII=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.11/go.mod h1:OEofCUKF7Hri4ShOCokF6k6hGq9PCB2sywt/9rLSXjY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.9 h1:sJdKvydGYDML9LTFcp6qq6Z5fIjN0Rdq2Gvw1hUg8tc=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.9/go.mod h1:Rc5+wn2k8gFSi3V1Ch4mhxOzjMh+bYSXVFfVaqowQOY=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.11 h1:ZBLEKweAzBBtJa8H+MTFfVyvo+eHdM8xec5oTm9IlqI=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.11/go.mod h1:mNS1VHxYXPNqxIdCTxf87j9ROfTMa4fNpIkA+iAfz0g=
github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g=
-github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.9 h1:mp3uhL8qDbpWj6s2O2c7Dva7L+HXNDEPHqd8ByCMuzI=
-github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.9/go.mod h1:NYicI4GfDyWaGlosi6PONumh2b//1azVE0rEeCbPUUk=
+github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.11 h1:GFxBWTb0DLD+PkhVPvNWtPsGBFusifSwHb2uDrIV0E0=
+github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.11/go.mod h1:jETcaK7szguipGK6ibOHjRemfxelIygcSUZe+xv9Vp8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2 h1:NvzGue25jKnuAsh6yQ+TZ4ResMcnp49AWgWGm2L4b5o=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2/go.mod h1:u+566cosFI+d+motIz3USXEh6sN8Nq4GrNXSg2RXVMo=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.27.4 h1:0RPAahwT63znFepvhfS+/WYtT+gEuAwaeNcCrzTQMH0=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.27.4/go.mod h1:wcpDmROpK5W7oWI6JcJIYGrVpHbF/Pu+FHxyBXyoa1E=
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.14 h1:dvvIB9OYsOH10RUNAY7yiCq5fQwGebXx1auBOkBTUlg=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.16 h1:+8J3OA/fUAAKpSyI6lAPyPhZVleLxDmuT2dv4lVHK20=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.16/go.mod h1:vveF0vVbSg0WNZNsi27F0Tbyx9JB8NyExl5Iv0RKLcY=
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=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU=
-github.com/aws/aws-sdk-go-v2/service/sso v1.11.13 h1:DQpf+al+aWozOEmVEdml67qkVZ6vdtGUi71BZZWw40k=
-github.com/aws/aws-sdk-go-v2/service/sso v1.11.13/go.mod h1:d7ptRksDDgvXaUvxyHZ9SYh+iMDymm94JbVcgvSYSzU=
+github.com/aws/aws-sdk-go-v2/service/sso v1.11.15 h1:HaIE5/TtKr66qZTJpvMifDxH4lRt2JZawbkLYOo1F+Y=
+github.com/aws/aws-sdk-go-v2/service/sso v1.11.15/go.mod h1:dDVD4ElJRTQXx7dOQ59EkqGyNU9tnwy1RKln+oLIOTU=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8=
-github.com/aws/aws-sdk-go-v2/service/sts v1.16.10 h1:7tquJrhjYz2EsCBvA9VTl+sBAAh1bv7h/sGASdZOGGo=
-github.com/aws/aws-sdk-go-v2/service/sts v1.16.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E=
+github.com/aws/aws-sdk-go-v2/service/sts v1.16.12 h1:YU9UHPukkCCnETHEExOptF/BxPvGJKXO/NBx+RMQ/2A=
+github.com/aws/aws-sdk-go-v2/service/sts v1.16.12/go.mod h1:b53qpmhHk7mTL2J/tfG6f38neZiyBQSiNXGCuNKq4+4=
github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
-github.com/aws/smithy-go v1.12.0 h1:gXpeZel/jPoWQ7OEmLIgCUnhkFftqNfwWUwAHSlp1v0=
-github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
+github.com/aws/smithy-go v1.12.1 h1:yQRC55aXN/y1W10HgwHle01DRuV9Dpf31iGkotjt3Ag=
+github.com/aws/smithy-go v1.12.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -476,8 +477,9 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
@@ -671,8 +673,8 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
-github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
-github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
+github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
+github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -871,8 +873,8 @@ golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
-golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c h1:q3gFqPqH7NVofKo3c3yETAP//pPI+G5mvB7qqj1Y5kY=
-golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7 h1:dtndE8FcEta75/4kHF3AbpuWzV6f1LjnLrM4pe2SZrw=
+golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -972,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-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME=
-golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/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/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=
@@ -1224,8 +1226,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-20220805133916-01dd62135a58 h1:sRT5xdTkj1Kbk30qbYC7VyMj73N5pZYsw6v+Nrzdhno=
-google.golang.org/genproto v0.0.0-20220805133916-01dd62135a58/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
+google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c h1:IooGDWedfLC6KLczH/uduUsKQP42ZZYhKx+zd50L1Sk=
+google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c/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=
@@ -1284,8 +1286,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
-gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
-gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
diff --git a/internal/common/actions.go b/internal/common/actions.go
index 00f01f0e..e7b0e811 100644
--- a/internal/common/actions.go
+++ b/internal/common/actions.go
@@ -43,8 +43,17 @@ var (
errUnconfiguredAction = errors.New("no hook is configured for this action")
errNoHook = errors.New("unable to execute action, no hook defined")
errUnexpectedHTTResponse = errors.New("unexpected HTTP hook response code")
+ hooksConcurrencyGuard = make(chan struct{}, 150)
)
+func startNewHook() {
+ hooksConcurrencyGuard <- struct{}{}
+}
+
+func hookEnded() {
+ <-hooksConcurrencyGuard
+}
+
// ProtocolActions defines the action to execute on file operations and SSH commands
type ProtocolActions struct {
// Valid values are download, upload, pre-delete, delete, rename, ssh_cmd. Empty slice to disable
@@ -135,7 +144,12 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
errRes = errHook
}
} else {
- go actionHandler.Handle(notification) //nolint:errcheck
+ go func() {
+ startNewHook()
+ defer hookEnded()
+
+ actionHandler.Handle(notification) //nolint:errcheck
+ }()
}
}
return errRes
diff --git a/internal/common/common.go b/internal/common/common.go
index 2c1f641e..3603af77 100644
--- a/internal/common/common.go
+++ b/internal/common/common.go
@@ -100,6 +100,7 @@ const (
ProtocolHTTPShare = "HTTPShare"
ProtocolDataRetention = "DataRetention"
ProtocolOIDC = "OIDC"
+ protocolEventAction = "EventAction"
)
// Upload modes
@@ -587,6 +588,9 @@ func (c *Configuration) ExecuteStartupHook() error {
}
func (c *Configuration) executePostDisconnectHook(remoteAddr, protocol, username, connID string, connectionTime time.Time) {
+ startNewHook()
+ defer hookEnded()
+
ipAddr := util.GetIPFromRemoteAddress(remoteAddr)
connDuration := int64(time.Since(connectionTime) / time.Millisecond)
diff --git a/internal/common/connection.go b/internal/common/connection.go
index a70e5a5f..1a823ab3 100644
--- a/internal/common/connection.go
+++ b/internal/common/connection.go
@@ -253,7 +253,7 @@ func (c *BaseConnection) getRealFsPath(fsPath string) string {
defer c.RUnlock()
for _, t := range c.activeTransfers {
- if p := t.GetRealFsPath(fsPath); len(p) > 0 {
+ if p := t.GetRealFsPath(fsPath); p != "" {
return p
}
}
@@ -858,7 +858,7 @@ func (c *BaseConnection) isRenamePermitted(fsSrc, fsDst vfs.Fs, fsSourcePath, fs
c.Log(logger.LevelWarn, "renaming to a directory mapped as virtual folder is not allowed: %#v", fsTargetPath)
return false
}
- if fsSrc.GetRelativePath(fsSourcePath) == "/" {
+ if virtualSourcePath == "/" || virtualTargetPath == "/" || fsSrc.GetRelativePath(fsSourcePath) == "/" {
c.Log(logger.LevelWarn, "renaming root dir is not allowed")
return false
}
diff --git a/internal/common/dataretention.go b/internal/common/dataretention.go
index 214832c2..0f806fde 100644
--- a/internal/common/dataretention.go
+++ b/internal/common/dataretention.go
@@ -384,6 +384,9 @@ func (c *RetentionCheck) sendEmailNotification(elapsed time.Duration, errCheck e
}
func (c *RetentionCheck) sendHookNotification(elapsed time.Duration, errCheck error) error {
+ startNewHook()
+ defer hookEnded()
+
data := make(map[string]any)
totalDeletedFiles := 0
totalDeletedSize := int64(0)
diff --git a/internal/common/defender_test.go b/internal/common/defender_test.go
index 5c31c0ea..20946ede 100644
--- a/internal/common/defender_test.go
+++ b/internal/common/defender_test.go
@@ -366,7 +366,7 @@ func TestLoadHostListFromFile(t *testing.T) {
assert.Len(t, hostList.IPAddresses, 0)
assert.Equal(t, 0, hostList.Ranges.Len())
- if runtime.GOOS != "windows" {
+ if runtime.GOOS != osWindows {
err = os.Chmod(hostsFilePath, 0111)
assert.NoError(t, err)
diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go
index 90a1dd13..0a7a6f6d 100644
--- a/internal/common/eventmanager.go
+++ b/internal/common/eventmanager.go
@@ -17,6 +17,7 @@ package common
import (
"bytes"
"context"
+ "errors"
"fmt"
"io"
"net/http"
@@ -30,6 +31,7 @@ import (
"time"
"github.com/robfig/cron/v3"
+ "github.com/rs/xid"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
@@ -47,6 +49,9 @@ var (
func init() {
eventManager = eventRulesContainer{
schedulesMapping: make(map[string][]cron.EntryID),
+ // arbitrary maximum number of concurrent asynchronous tasks,
+ // each task could execute multiple actions
+ concurrencyGuard: make(chan struct{}, 200),
}
dataprovider.SetEventRulesCallbacks(eventManager.loadRules, eventManager.RemoveRule,
func(operation, executor, ip, objectType, objectName string, object plugin.Renderer) {
@@ -71,6 +76,15 @@ type eventRulesContainer struct {
Schedules []dataprovider.EventRule
schedulesMapping map[string][]cron.EntryID
lastLoad int64
+ concurrencyGuard chan struct{}
+}
+
+func (r *eventRulesContainer) addAsyncTask() {
+ r.concurrencyGuard <- struct{}{}
+}
+
+func (r *eventRulesContainer) removeAsyncTask() {
+ <-r.concurrencyGuard
}
func (r *eventRulesContainer) getLastLoadTime() int64 {
@@ -245,11 +259,19 @@ func (r *eventRulesContainer) hasFsRules() bool {
// handleFsEvent executes the rules actions defined for the specified event
func (r *eventRulesContainer) handleFsEvent(params EventParams) error {
+ if params.Protocol == protocolEventAction {
+ return nil
+ }
r.RLock()
var rulesWithSyncActions, rulesAsync []dataprovider.EventRule
for _, rule := range r.FsEvents {
if r.checkFsEventMatch(rule.Conditions, params) {
+ if err := rule.CheckActionsConsistency(""); err != nil {
+ eventManagerLog(logger.LevelWarn, "rule %q skipped: %v, event %q",
+ rule.Name, err, params.Event)
+ continue
+ }
hasSyncActions := false
for _, action := range rule.Actions {
if action.Options.ExecuteSync {
@@ -267,6 +289,7 @@ func (r *eventRulesContainer) handleFsEvent(params EventParams) error {
r.RUnlock()
+ params.sender = params.Name
if len(rulesAsync) > 0 {
go executeAsyncRulesActions(rulesAsync, params)
}
@@ -277,6 +300,7 @@ func (r *eventRulesContainer) handleFsEvent(params EventParams) error {
return nil
}
+// username is populated for user objects
func (r *eventRulesContainer) handleProviderEvent(params EventParams) {
r.RLock()
defer r.RUnlock()
@@ -284,11 +308,17 @@ func (r *eventRulesContainer) handleProviderEvent(params EventParams) {
var rules []dataprovider.EventRule
for _, rule := range r.ProviderEvents {
if r.checkProviderEventMatch(rule.Conditions, params) {
- rules = append(rules, rule)
+ if err := rule.CheckActionsConsistency(params.ObjectType); err == nil {
+ rules = append(rules, rule)
+ } else {
+ eventManagerLog(logger.LevelWarn, "rule %q skipped: %v, event %q object type %q",
+ rule.Name, err, params.Event, params.ObjectType)
+ }
}
}
if len(rules) > 0 {
+ params.sender = params.ObjectName
go executeAsyncRulesActions(rules, params)
}
}
@@ -309,6 +339,30 @@ type EventParams struct {
IP string
Timestamp int64
Object plugin.Renderer
+ sender string
+}
+
+// getUsers returns users with group settings not applied
+func (p *EventParams) getUsers() ([]dataprovider.User, error) {
+ if p.sender == "" {
+ return dataprovider.DumpUsers()
+ }
+ user, err := dataprovider.UserExists(p.sender)
+ if err != nil {
+ return nil, fmt.Errorf("error getting user %q: %w", p.sender, err)
+ }
+ return []dataprovider.User{user}, nil
+}
+
+func (p *EventParams) getFolders() ([]vfs.BaseVirtualFolder, error) {
+ if p.sender == "" {
+ return dataprovider.DumpFolders()
+ }
+ folder, err := dataprovider.GetFolderByName(p.sender)
+ if err != nil {
+ return nil, fmt.Errorf("error getting folder %q: %w", p.sender, err)
+ }
+ return []vfs.BaseVirtualFolder{folder}, nil
}
func (p *EventParams) getStringReplacements(addObjectData bool) []string {
@@ -495,6 +549,123 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params EventP
return err
}
+func getUserForEventAction(username string) (dataprovider.User, error) {
+ user, err := dataprovider.GetUserWithGroupSettings(username)
+ if err != nil {
+ return dataprovider.User{}, err
+ }
+ user.Filters.DisableFsChecks = false
+ user.Filters.FilePatterns = nil
+ for k := range user.Permissions {
+ user.Permissions[k] = []string{dataprovider.PermAny}
+ }
+ return user, err
+}
+
+func executeDeleteFileFsAction(conn *BaseConnection, item string, info os.FileInfo) error {
+ fs, fsPath, err := conn.GetFsAndResolvedPath(item)
+ if err != nil {
+ return err
+ }
+ return conn.RemoveFile(fs, fsPath, item, info)
+}
+
+func executeDeleteFsAction(deletes []string, replacer *strings.Replacer, username string) error {
+ user, err := getUserForEventAction(username)
+ if err != nil {
+ return err
+ }
+ connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
+ err = user.CheckFsRoot(connectionID)
+ defer user.CloseFs() //nolint:errcheck
+ if err != nil {
+ return err
+ }
+ conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
+ for _, item := range deletes {
+ item = replaceWithReplacer(item, replacer)
+ info, err := conn.DoStat(item, 0, false)
+ if err != nil {
+ if conn.IsNotExistError(err) {
+ continue
+ }
+ return err
+ }
+ if info.IsDir() {
+ if err = conn.RemoveDir(item); err != nil {
+ return err
+ }
+ } else {
+ if err = executeDeleteFileFsAction(conn, item, info); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func executeMkDirsFsAction(dirs []string, replacer *strings.Replacer, username string) error {
+ user, err := getUserForEventAction(username)
+ if err != nil {
+ return err
+ }
+ connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
+ err = user.CheckFsRoot(connectionID)
+ defer user.CloseFs() //nolint:errcheck
+ if err != nil {
+ return err
+ }
+ conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
+ for _, item := range dirs {
+ item = replaceWithReplacer(item, replacer)
+ if err = conn.CheckParentDirs(path.Dir(item)); err != nil {
+ return err
+ }
+ if err = conn.CreateDir(item, false); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func executeRenameFsAction(renames []dataprovider.KeyValue, replacer *strings.Replacer, username string) error {
+ user, err := getUserForEventAction(username)
+ if err != nil {
+ return err
+ }
+ connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
+ err = user.CheckFsRoot(connectionID)
+ defer user.CloseFs() //nolint:errcheck
+ if err != nil {
+ return err
+ }
+ conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
+ for _, item := range renames {
+ source := replaceWithReplacer(item.Key, replacer)
+ target := replaceWithReplacer(item.Value, replacer)
+ if err = conn.Rename(source, target); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, 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)
+ case dataprovider.FilesystemActionDelete:
+ return executeDeleteFsAction(c.Deletes, replacer, params.sender)
+ case dataprovider.FilesystemActionMkdirs:
+ return executeMkDirsFsAction(c.MkDirs, replacer, params.sender)
+ default:
+ return fmt.Errorf("unsupported filesystem action %d", c.Type)
+ }
+}
+
func executeQuotaResetForUser(user dataprovider.User) error {
if err := user.LoadAndApplyGroupSettings(); err != nil {
eventManagerLog(logger.LevelDebug, "skipping scheduled quota reset for user %s, cannot apply group settings: %v",
@@ -520,18 +691,21 @@ func executeQuotaResetForUser(user dataprovider.User) error {
return nil
}
-func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions) error {
- users, err := dataprovider.DumpUsers()
+func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params EventParams) error {
+ users, err := params.getUsers()
if err != nil {
return fmt.Errorf("unable to get users: %w", err)
}
var failedResets []string
+ executed := 0
for _, user := range users {
- if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ // if sender is set, the conditions have already been evaluated
+ if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
eventManagerLog(logger.LevelDebug, "skipping scheduled quota reset for user %s, name conditions don't match",
user.Username)
continue
}
+ executed++
if err = executeQuotaResetForUser(user); err != nil {
failedResets = append(failedResets, user.Username)
continue
@@ -540,17 +714,23 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions)
if len(failedResets) > 0 {
return fmt.Errorf("quota reset failed for users: %+v", failedResets)
}
+ if executed == 0 {
+ eventManagerLog(logger.LevelError, "no user quota reset executed")
+ return errors.New("no user quota reset executed")
+ }
return nil
}
-func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions) error {
- folders, err := dataprovider.DumpFolders()
+func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params EventParams) error {
+ folders, err := params.getFolders()
if err != nil {
return fmt.Errorf("unable to get folders: %w", err)
}
var failedResets []string
+ executed := 0
for _, folder := range folders {
- if !checkEventConditionPatterns(folder.Name, conditions.Names) {
+ // if sender is set, the conditions have already been evaluated
+ if params.sender == "" && !checkEventConditionPatterns(folder.Name, conditions.Names) {
eventManagerLog(logger.LevelDebug, "skipping scheduled quota reset for folder %s, name conditions don't match",
folder.Name)
continue
@@ -560,6 +740,7 @@ func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions
failedResets = append(failedResets, folder.Name)
continue
}
+ executed++
f := vfs.VirtualFolder{
BaseVirtualFolder: folder,
VirtualPath: "/",
@@ -580,21 +761,28 @@ func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions
if len(failedResets) > 0 {
return fmt.Errorf("quota reset failed for folders: %+v", failedResets)
}
+ if executed == 0 {
+ eventManagerLog(logger.LevelError, "no folder quota reset executed")
+ return errors.New("no folder quota reset executed")
+ }
return nil
}
-func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOptions) error {
- users, err := dataprovider.DumpUsers()
+func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params EventParams) error {
+ users, err := params.getUsers()
if err != nil {
return fmt.Errorf("unable to get users: %w", err)
}
var failedResets []string
+ executed := 0
for _, user := range users {
- if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ // if sender is set, the conditions have already been evaluated
+ if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
eventManagerLog(logger.LevelDebug, "skipping scheduled transfer quota reset for user %s, name conditions don't match",
user.Username)
continue
}
+ executed++
err = dataprovider.UpdateUserTransferQuota(&user, 0, 0, true)
if err != nil {
eventManagerLog(logger.LevelError, "error updating transfer quota for user %s: %v", user.Username, err)
@@ -604,6 +792,10 @@ func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOption
if len(failedResets) > 0 {
return fmt.Errorf("transfer quota reset failed for users: %+v", failedResets)
}
+ if executed == 0 {
+ eventManagerLog(logger.LevelError, "no transfer quota reset executed")
+ return errors.New("no transfer quota reset executed")
+ }
return nil
}
@@ -629,19 +821,22 @@ func executeDataRetentionCheckForUser(user dataprovider.User, folders []dataprov
}
func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRetentionConfig,
- conditions dataprovider.ConditionOptions,
+ conditions dataprovider.ConditionOptions, params EventParams,
) error {
- users, err := dataprovider.DumpUsers()
+ users, err := params.getUsers()
if err != nil {
return fmt.Errorf("unable to get users: %w", err)
}
var failedChecks []string
+ executed := 0
for _, user := range users {
- if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ // if sender is set, the conditions have already been evaluated
+ if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
eventManagerLog(logger.LevelDebug, "skipping scheduled retention check for user %s, name conditions don't match",
user.Username)
continue
}
+ executed++
if err = executeDataRetentionCheckForUser(user, config.Folders); err != nil {
failedChecks = append(failedChecks, user.Username)
continue
@@ -650,6 +845,10 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete
if len(failedChecks) > 0 {
return fmt.Errorf("retention check failed for users: %+v", failedChecks)
}
+ if executed == 0 {
+ eventManagerLog(logger.LevelError, "no retention check executed")
+ return errors.New("no retention check executed")
+ }
return nil
}
@@ -664,13 +863,15 @@ func executeRuleAction(action dataprovider.BaseEventAction, params EventParams,
case dataprovider.ActionTypeBackup:
return dataprovider.ExecuteBackup()
case dataprovider.ActionTypeUserQuotaReset:
- return executeUsersQuotaResetRuleAction(conditions)
+ return executeUsersQuotaResetRuleAction(conditions, params)
case dataprovider.ActionTypeFolderQuotaReset:
- return executeFoldersQuotaResetRuleAction(conditions)
+ return executeFoldersQuotaResetRuleAction(conditions, params)
case dataprovider.ActionTypeTransferQuotaReset:
- return executeTransferQuotaResetRuleAction(conditions)
+ return executeTransferQuotaResetRuleAction(conditions, params)
case dataprovider.ActionTypeDataRetentionCheck:
- return executeDataRetentionCheckRuleAction(action.Options.RetentionConfig, conditions)
+ return executeDataRetentionCheckRuleAction(action.Options.RetentionConfig, conditions, params)
+ case dataprovider.ActionTypeFilesystem:
+ return executeFsRuleAction(action.Options.FsConfig, params)
default:
return fmt.Errorf("unsupported action type: %d", action.Type)
}
@@ -707,6 +908,9 @@ func executeSyncRulesActions(rules []dataprovider.EventRule, params EventParams)
}
func executeAsyncRulesActions(rules []dataprovider.EventRule, params EventParams) {
+ eventManager.addAsyncTask()
+ defer eventManager.removeAsyncTask()
+
for _, rule := range rules {
executeRuleAsyncActions(rule, params, nil)
}
@@ -784,6 +988,10 @@ 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
@@ -823,9 +1031,9 @@ func (j *eventCronJob) Run() {
}
}(task.Name)
- executeRuleAsyncActions(rule, EventParams{}, nil)
+ executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{})
} else {
- executeRuleAsyncActions(rule, EventParams{}, nil)
+ executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{})
}
eventManagerLog(logger.LevelDebug, "execution for scheduled rule %q finished", j.ruleName)
}
diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go
index cbdb8de9..699b0374 100644
--- a/internal/common/eventmanager_test.go
+++ b/internal/common/eventmanager_test.go
@@ -20,6 +20,8 @@ import (
"os"
"path"
"path/filepath"
+ "runtime"
+ "strings"
"testing"
"time"
@@ -259,11 +261,19 @@ func TestEventManagerErrors(t *testing.T) {
err := dataprovider.Close()
assert.NoError(t, err)
- err = executeUsersQuotaResetRuleAction(dataprovider.ConditionOptions{})
+ params := EventParams{
+ sender: "sender",
+ }
+ _, err = params.getUsers()
assert.Error(t, err)
- err = executeFoldersQuotaResetRuleAction(dataprovider.ConditionOptions{})
+ _, err = params.getFolders()
assert.Error(t, err)
- err = executeTransferQuotaResetRuleAction(dataprovider.ConditionOptions{})
+
+ err = executeUsersQuotaResetRuleAction(dataprovider.ConditionOptions{}, EventParams{})
+ assert.Error(t, err)
+ err = executeFoldersQuotaResetRuleAction(dataprovider.ConditionOptions{}, EventParams{})
+ assert.Error(t, err)
+ err = executeTransferQuotaResetRuleAction(dataprovider.ConditionOptions{}, EventParams{})
assert.Error(t, err)
err = executeQuotaResetForUser(dataprovider.User{
Groups: []sdk.GroupMapping{
@@ -488,6 +498,17 @@ func TestEventRuleActions(t *testing.T) {
})
assert.Error(t, err)
assert.True(t, QuotaScans.RemoveUserQuotaScan(username1))
+ // non matching pattern
+ err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{
+ Names: []dataprovider.ConditionPattern{
+ {
+ Pattern: "don't match",
+ },
+ },
+ })
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no user quota reset executed")
+ }
dataRetentionAction := dataprovider.BaseEventAction{
Type: dataprovider.ActionTypeDataRetentionCheck,
@@ -571,6 +592,17 @@ func TestEventRuleActions(t *testing.T) {
assert.Error(t, err)
RetentionChecks.remove(user1.Username)
+ err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{
+ Names: []dataprovider.ConditionPattern{
+ {
+ Pattern: "no match",
+ },
+ },
+ })
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no retention check executed")
+ }
+
err = os.RemoveAll(user1.GetHomeDir())
assert.NoError(t, err)
@@ -591,6 +623,17 @@ func TestEventRuleActions(t *testing.T) {
assert.Equal(t, int64(0), userGet.UsedDownloadDataTransfer)
assert.Equal(t, int64(0), userGet.UsedUploadDataTransfer)
+ err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{
+ Names: []dataprovider.ConditionPattern{
+ {
+ Pattern: "no match",
+ },
+ },
+ })
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no transfer quota reset executed")
+ }
+
err = dataprovider.DeleteUser(username1, "", "")
assert.NoError(t, err)
err = dataprovider.DeleteUser(username2, "", "")
@@ -649,6 +692,17 @@ func TestEventRuleActions(t *testing.T) {
assert.Error(t, err)
assert.True(t, QuotaScans.RemoveVFolderQuotaScan(foldername1))
+ err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{
+ Names: []dataprovider.ConditionPattern{
+ {
+ Pattern: "no folder match",
+ },
+ },
+ })
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no folder quota reset executed")
+ }
+
err = os.RemoveAll(folder1.MappedPath)
assert.NoError(t, err)
err = dataprovider.DeleteFolder(foldername1, "", "")
@@ -657,6 +711,102 @@ func TestEventRuleActions(t *testing.T) {
assert.NoError(t, err)
}
+func TestFilesystemActionErrors(t *testing.T) {
+ err := executeFsRuleAction(dataprovider.EventActionFilesystemConfig{}, 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,
+ Permissions: map[string][]string{
+ "/": {dataprovider.PermAny},
+ },
+ HomeDir: filepath.Join(os.TempDir(), username),
+ },
+ FsConfig: vfs.Filesystem{
+ Provider: sdk.SFTPFilesystemProvider,
+ SFTPConfig: vfs.SFTPFsConfig{
+ BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+ Endpoint: "127.0.0.1:4022",
+ Username: username,
+ },
+ Password: kms.NewPlainSecret("pwd"),
+ },
+ },
+ }
+ conn := NewBaseConnection("", protocolEventAction, "", "", user)
+ err = executeDeleteFileFsAction(conn, "", nil)
+ assert.Error(t, err)
+ err = dataprovider.AddUser(&user, "", "")
+ assert.NoError(t, err)
+ // check root fs fails
+ 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.FsConfig.Provider = sdk.LocalFilesystemProvider
+ user.Permissions["/"] = []string{dataprovider.PermUpload}
+ err = dataprovider.DeleteUser(username, "", "")
+ assert.NoError(t, err)
+ err = dataprovider.AddUser(&user, "", "")
+ assert.NoError(t, err)
+ err = executeRenameFsAction([]dataprovider.KeyValue{
+ {
+ Key: "/p1",
+ Value: "/p1",
+ },
+ }, testReplacer, username)
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "the rename source and target cannot be the same")
+ }
+
+ if runtime.GOOS != osWindows {
+ dirPath := filepath.Join(user.HomeDir, "adir", "sub")
+ err := os.MkdirAll(dirPath, os.ModePerm)
+ assert.NoError(t, err)
+ filePath := filepath.Join(dirPath, "f.dat")
+ err = os.WriteFile(filePath, nil, 0666)
+ assert.NoError(t, err)
+ err = os.Chmod(dirPath, 0001)
+ assert.NoError(t, err)
+
+ err = executeDeleteFsAction([]string{"/adir/sub"}, testReplacer, username)
+ assert.Error(t, err)
+ err = executeDeleteFsAction([]string{"/adir/sub/f.dat"}, testReplacer, username)
+ assert.Error(t, err)
+ err = os.Chmod(dirPath, 0555)
+ assert.NoError(t, err)
+ err = executeDeleteFsAction([]string{"/adir/sub/f.dat"}, testReplacer, username)
+ assert.Error(t, err)
+
+ err = executeMkDirsFsAction([]string{"/adir/sub"}, testReplacer, username)
+ assert.Error(t, err)
+ err = executeMkDirsFsAction([]string{"/adir/sub/sub/sub"}, testReplacer, username)
+ assert.Error(t, err)
+
+ err = os.Chmod(dirPath, os.ModePerm)
+ assert.NoError(t, err)
+ }
+
+ err = dataprovider.DeleteUser(username, "", "")
+ assert.NoError(t, err)
+ err = os.RemoveAll(user.GetHomeDir())
+ assert.NoError(t, err)
+}
+
func TestQuotaActionsWithQuotaTrackDisabled(t *testing.T) {
oldProviderConf := dataprovider.GetProviderConfig()
providerConf := dataprovider.GetProviderConfig()
@@ -788,6 +938,17 @@ 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, "", "")
diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go
index 99080e8e..ef472e3b 100644
--- a/internal/common/protocol_test.go
+++ b/internal/common/protocol_test.go
@@ -3272,6 +3272,264 @@ func TestEventRuleProviderEvents(t *testing.T) {
assert.NoError(t, err)
}
+func TestEventRuleFsActions(t *testing.T) {
+ dirsToCreate := []string{
+ "/basedir/1",
+ "/basedir/sub/2",
+ "/basedir/3",
+ }
+ a1 := dataprovider.BaseEventAction{
+ Name: "a1",
+ Type: dataprovider.ActionTypeFilesystem,
+ Options: dataprovider.BaseEventActionOptions{
+ FsConfig: dataprovider.EventActionFilesystemConfig{
+ Type: dataprovider.FilesystemActionMkdirs,
+ MkDirs: dirsToCreate,
+ },
+ },
+ }
+ a2 := dataprovider.BaseEventAction{
+ Name: "a2",
+ Type: dataprovider.ActionTypeFilesystem,
+ Options: dataprovider.BaseEventActionOptions{
+ FsConfig: dataprovider.EventActionFilesystemConfig{
+ Type: dataprovider.FilesystemActionRename,
+ Renames: []dataprovider.KeyValue{
+ {
+ Key: "/{{VirtualPath}}",
+ Value: "/{{ObjectName}}_renamed",
+ },
+ },
+ },
+ },
+ }
+ a3 := dataprovider.BaseEventAction{
+ Name: "a3",
+ Type: dataprovider.ActionTypeFilesystem,
+ Options: dataprovider.BaseEventActionOptions{
+ FsConfig: dataprovider.EventActionFilesystemConfig{
+ Type: dataprovider.FilesystemActionDelete,
+ Deletes: []string{"/{{ObjectName}}_renamed"},
+ },
+ },
+ }
+ a4 := dataprovider.BaseEventAction{
+ Name: "a4",
+ Type: dataprovider.ActionTypeFolderQuotaReset,
+ }
+ a5 := dataprovider.BaseEventAction{
+ Name: "a5",
+ Type: dataprovider.ActionTypeUserQuotaReset,
+ }
+ action1, resp, err := httpdtest.AddEventAction(a1, http.StatusCreated)
+ assert.NoError(t, err, string(resp))
+ action2, resp, err := httpdtest.AddEventAction(a2, http.StatusCreated)
+ assert.NoError(t, err, string(resp))
+ action3, resp, err := httpdtest.AddEventAction(a3, http.StatusCreated)
+ assert.NoError(t, err, string(resp))
+ action4, resp, err := httpdtest.AddEventAction(a4, http.StatusCreated)
+ assert.NoError(t, err, string(resp))
+ action5, resp, err := httpdtest.AddEventAction(a5, http.StatusCreated)
+ assert.NoError(t, err, string(resp))
+
+ r1 := dataprovider.EventRule{
+ Name: "r1",
+ Trigger: dataprovider.EventTriggerProviderEvent,
+ Conditions: dataprovider.EventConditions{
+ ProviderEvents: []string{"add"},
+ },
+ Actions: []dataprovider.EventAction{
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action1.Name,
+ },
+ Order: 1,
+ },
+ },
+ }
+ r2 := dataprovider.EventRule{
+ Name: "r2",
+ Trigger: dataprovider.EventTriggerFsEvent,
+ Conditions: dataprovider.EventConditions{
+ FsEvents: []string{"upload"},
+ },
+ Actions: []dataprovider.EventAction{
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action2.Name,
+ },
+ Order: 1,
+ Options: dataprovider.EventActionOptions{
+ ExecuteSync: true,
+ },
+ },
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action5.Name,
+ },
+ Order: 2,
+ },
+ },
+ }
+ r3 := dataprovider.EventRule{
+ Name: "r3",
+ Trigger: dataprovider.EventTriggerFsEvent,
+ Conditions: dataprovider.EventConditions{
+ FsEvents: []string{"mkdir"},
+ },
+ Actions: []dataprovider.EventAction{
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action3.Name,
+ },
+ Order: 1,
+ },
+ },
+ }
+ r4 := dataprovider.EventRule{
+ Name: "r4",
+ Trigger: dataprovider.EventTriggerFsEvent,
+ Conditions: dataprovider.EventConditions{
+ FsEvents: []string{"rmdir"},
+ },
+ Actions: []dataprovider.EventAction{
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action4.Name,
+ },
+ Order: 1,
+ },
+ },
+ }
+ r5 := dataprovider.EventRule{
+ Name: "r5",
+ Trigger: dataprovider.EventTriggerProviderEvent,
+ Conditions: dataprovider.EventConditions{
+ ProviderEvents: []string{"add"},
+ },
+ Actions: []dataprovider.EventAction{
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action4.Name,
+ },
+ Order: 1,
+ },
+ },
+ }
+ rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
+ assert.NoError(t, err)
+ rule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)
+ assert.NoError(t, err)
+ rule3, _, err := httpdtest.AddEventRule(r3, http.StatusCreated)
+ assert.NoError(t, err)
+ rule4, _, err := httpdtest.AddEventRule(r4, http.StatusCreated)
+ assert.NoError(t, err)
+ rule5, _, err := httpdtest.AddEventRule(r5, http.StatusCreated)
+ assert.NoError(t, err)
+
+ folderMappedPath := filepath.Join(os.TempDir(), "folder")
+ err = os.MkdirAll(folderMappedPath, os.ModePerm)
+ assert.NoError(t, err)
+ err = os.WriteFile(filepath.Join(folderMappedPath, "file.txt"), []byte("1"), 0666)
+ assert.NoError(t, err)
+
+ folder, _, err := httpdtest.AddFolder(vfs.BaseVirtualFolder{
+ Name: "test folder",
+ MappedPath: folderMappedPath,
+ }, http.StatusCreated)
+ assert.NoError(t, err)
+ assert.Eventually(t, func() bool {
+ folderGet, _, err := httpdtest.GetFolderByName(folder.Name, http.StatusOK)
+ if err != nil {
+ return false
+ }
+ return folderGet.UsedQuotaFiles == 1 && folderGet.UsedQuotaSize == 1
+ }, 2*time.Second, 100*time.Millisecond)
+
+ u := getTestUser()
+ u.Filters.DisableFsChecks = true
+ user, _, err := httpdtest.AddUser(u, http.StatusCreated)
+ assert.NoError(t, err)
+ conn, client, err := getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+ // check initial directories creation
+ for _, dir := range dirsToCreate {
+ assert.Eventually(t, func() bool {
+ _, err := client.Stat(dir)
+ return err == nil
+ }, 2*time.Second, 100*time.Millisecond)
+ }
+ // upload a file and check the sync rename
+ size := int64(32768)
+ err = writeSFTPFileNoCheck(path.Join("basedir", testFileName), size, client)
+ assert.NoError(t, err)
+ _, err = client.Stat(path.Join("basedir", testFileName))
+ assert.Error(t, err)
+ info, err := client.Stat(testFileName + "_renamed")
+ if assert.NoError(t, err) {
+ assert.Equal(t, size, info.Size())
+ }
+ assert.NoError(t, err)
+ assert.Eventually(t, func() bool {
+ userGet, _, err := httpdtest.GetUserByUsername(user.Username, http.StatusOK)
+ if err != nil {
+ return false
+ }
+ return userGet.UsedQuotaFiles == 1 && userGet.UsedQuotaSize == size
+ }, 2*time.Second, 100*time.Millisecond)
+
+ for i := 0; i < 2; i++ {
+ err = client.Mkdir(testFileName)
+ assert.NoError(t, err)
+ assert.Eventually(t, func() bool {
+ _, err = client.Stat(testFileName + "_renamed")
+ return err != nil
+ }, 2*time.Second, 100*time.Millisecond)
+ err = client.RemoveDirectory(testFileName)
+ assert.NoError(t, err)
+ }
+ err = client.Mkdir(testFileName + "_renamed")
+ assert.NoError(t, err)
+ err = client.Mkdir(testFileName)
+ assert.NoError(t, err)
+ assert.Eventually(t, func() bool {
+ _, err = client.Stat(testFileName + "_renamed")
+ return err != nil
+ }, 2*time.Second, 100*time.Millisecond)
+ }
+
+ _, err = httpdtest.RemoveUser(user, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(user.GetHomeDir())
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveFolder(folder, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(folderMappedPath)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventRule(rule3, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventRule(rule4, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventRule(rule5, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventAction(action3, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventAction(action4, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventAction(action5, http.StatusOK)
+ assert.NoError(t, err)
+}
+
func TestSyncUploadAction(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
diff --git a/internal/dataprovider/actions.go b/internal/dataprovider/actions.go
index 2bef46d5..de71592b 100644
--- a/internal/dataprovider/actions.go
+++ b/internal/dataprovider/actions.go
@@ -52,6 +52,10 @@ const (
actionObjectEventRule = "event_rule"
)
+var (
+ actionsConcurrencyGuard = make(chan struct{}, 100)
+)
+
func executeAction(operation, executor, ip, objectType, objectName string, object plugin.Renderer) {
if plugin.Handler.HasNotifiers() {
plugin.Handler.NotifyProviderEvent(¬ifier.ProviderEvent{
@@ -75,6 +79,11 @@ func executeAction(operation, executor, ip, objectType, objectName string, objec
}
go func() {
+ actionsConcurrencyGuard <- struct{}{}
+ defer func() {
+ <-actionsConcurrencyGuard
+ }()
+
dataAsJSON, err := object.RenderAsJSON(operation != operationDelete)
if err != nil {
providerLog(logger.LevelError, "unable to serialize user as JSON for operation %#v: %v", operation, err)
diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go
index 5fbf9c1a..d417930f 100644
--- a/internal/dataprovider/dataprovider.go
+++ b/internal/dataprovider/dataprovider.go
@@ -3591,6 +3591,11 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
}
go func() {
+ actionsConcurrencyGuard <- struct{}{}
+ defer func() {
+ <-actionsConcurrencyGuard
+ }()
+
status := "0"
if err == nil {
status = "1"
diff --git a/internal/dataprovider/eventrule.go b/internal/dataprovider/eventrule.go
index 11a36a01..71e911db 100644
--- a/internal/dataprovider/eventrule.go
+++ b/internal/dataprovider/eventrule.go
@@ -41,12 +41,13 @@ const (
ActionTypeFolderQuotaReset
ActionTypeTransferQuotaReset
ActionTypeDataRetentionCheck
+ ActionTypeFilesystem
)
var (
supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeBackup,
ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
- ActionTypeDataRetentionCheck}
+ ActionTypeDataRetentionCheck, ActionTypeFilesystem}
)
func isActionTypeValid(action int) bool {
@@ -69,6 +70,8 @@ func getActionTypeAsString(action int) string {
return "Transfer quota reset"
case ActionTypeDataRetentionCheck:
return "Data retention check"
+ case ActionTypeFilesystem:
+ return "Filesystem"
default:
return "Command"
}
@@ -102,6 +105,32 @@ func getTriggerTypeAsString(trigger int) string {
}
}
+// Supported filesystem actions
+const (
+ FilesystemActionRename = iota + 1
+ FilesystemActionDelete
+ FilesystemActionMkdirs
+)
+
+var (
+ supportedFsActions = []int{FilesystemActionRename, FilesystemActionDelete, FilesystemActionMkdirs}
+)
+
+func isFilesystemActionValid(value int) bool {
+ return util.Contains(supportedFsActions, value)
+}
+
+func getFsActionTypeAsString(value int) string {
+ switch value {
+ case FilesystemActionRename:
+ return "Rename"
+ case FilesystemActionDelete:
+ return "Delete"
+ default:
+ return "Create directories"
+ }
+}
+
// TODO: replace the copied strings with shared constants
var (
// SupportedFsEvents defines the supported filesystem events
@@ -112,8 +141,8 @@ var (
SupportedRuleConditionProtocols = []string{"SFTP", "SCP", "SSH", "FTP", "DAV", "HTTP", "HTTPShare",
"OIDC"}
// SupporteRuleConditionProviderObjects defines the supported provider objects for rule conditions
- SupporteRuleConditionProviderObjects = []string{actionObjectUser, actionObjectGroup, actionObjectAdmin,
- actionObjectAPIKey, actionObjectShare, actionObjectEventRule, actionObjectEventAction}
+ SupporteRuleConditionProviderObjects = []string{actionObjectUser, actionObjectFolder, actionObjectGroup,
+ actionObjectAdmin, actionObjectAPIKey, actionObjectShare, actionObjectEventRule, actionObjectEventAction}
// SupportedHTTPActionMethods defines the supported methods for HTTP actions
SupportedHTTPActionMethods = []string{http.MethodPost, http.MethodGet, http.MethodPut}
)
@@ -122,6 +151,7 @@ var (
var (
EventActionTypes []EnumMapping
EventTriggerTypes []EnumMapping
+ FsActionTypes []EnumMapping
)
func init() {
@@ -137,6 +167,12 @@ func init() {
Name: getTriggerTypeAsString(t),
})
}
+ for _, t := range supportedFsActions {
+ FsActionTypes = append(FsActionTypes, EnumMapping{
+ Value: t,
+ Name: getFsActionTypeAsString(t),
+ })
+ }
}
// EnumMapping defines a mapping between enum values and names
@@ -331,12 +367,120 @@ func (c *EventActionDataRetentionConfig) validate() error {
return nil
}
+// EventActionFilesystemConfig defines the configuration for filesystem actions
+type EventActionFilesystemConfig struct {
+ // Filesystem actions, see the above enum
+ Type int `json:"type,omitempty"`
+ // files/dirs to rename, key is the source and target the value
+ Renames []KeyValue `json:"renames,omitempty"`
+ // directories to create
+ MkDirs []string `json:"mkdirs,omitempty"`
+ // files/dirs to delete
+ Deletes []string `json:"deletes,omitempty"`
+}
+
+// GetDeletesAsString returns the list of items to delete as comma separated string.
+// Using a pointer receiver will not work in web templates
+func (c EventActionFilesystemConfig) GetDeletesAsString() string {
+ return strings.Join(c.Deletes, ",")
+}
+
+// GetMkDirsAsString returns the list of directories to create as comma separated string.
+// Using a pointer receiver will not work in web templates
+func (c EventActionFilesystemConfig) GetMkDirsAsString() string {
+ return strings.Join(c.MkDirs, ",")
+}
+
+func (c *EventActionFilesystemConfig) validateRenames() error {
+ if len(c.Renames) == 0 {
+ return util.NewValidationError("no items to rename specified")
+ }
+ for idx, kv := range c.Renames {
+ key := strings.TrimSpace(kv.Key)
+ value := strings.TrimSpace(kv.Value)
+ if key == "" || value == "" {
+ return util.NewValidationError("invalid items to rename")
+ }
+ key = util.CleanPath(key)
+ value = util.CleanPath(value)
+ if key == value {
+ return util.NewValidationError("rename source and target cannot be equal")
+ }
+ if key == "/" || value == "/" {
+ return util.NewValidationError("renaming the root directory is not allowed")
+ }
+ c.Renames[idx] = KeyValue{
+ Key: key,
+ Value: value,
+ }
+ }
+ return nil
+}
+
+func (c *EventActionFilesystemConfig) validate() error {
+ if !isFilesystemActionValid(c.Type) {
+ return util.NewValidationError(fmt.Sprintf("invalid filesystem action type: %d", c.Type))
+ }
+ switch c.Type {
+ case FilesystemActionRename:
+ c.MkDirs = nil
+ c.Deletes = nil
+ if err := c.validateRenames(); err != nil {
+ return err
+ }
+ case FilesystemActionDelete:
+ c.Renames = nil
+ c.MkDirs = nil
+ if len(c.Deletes) == 0 {
+ return util.NewValidationError("no item to delete specified")
+ }
+ for idx, val := range c.Deletes {
+ val = strings.TrimSpace(val)
+ if val == "" {
+ return util.NewValidationError("invalid item to delete")
+ }
+ c.Deletes[idx] = util.CleanPath(val)
+ }
+ c.Deletes = util.RemoveDuplicates(c.Deletes, false)
+ case FilesystemActionMkdirs:
+ c.Renames = nil
+ c.Deletes = nil
+ if len(c.MkDirs) == 0 {
+ return util.NewValidationError("no directory to create specified")
+ }
+ for idx, val := range c.MkDirs {
+ val = strings.TrimSpace(val)
+ if val == "" {
+ return util.NewValidationError("invalid directory to create")
+ }
+ c.MkDirs[idx] = util.CleanPath(val)
+ }
+ c.MkDirs = util.RemoveDuplicates(c.MkDirs, false)
+ }
+ return nil
+}
+
+func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {
+ mkdirs := make([]string, len(c.MkDirs))
+ copy(mkdirs, c.MkDirs)
+ deletes := make([]string, len(c.Deletes))
+ copy(deletes, c.Deletes)
+
+ return EventActionFilesystemConfig{
+ Type: c.Type,
+ Renames: cloneKeyValues(c.Renames),
+ MkDirs: mkdirs,
+ Deletes: deletes,
+ }
+}
+
// BaseEventActionOptions defines the supported configuration options for a base event actions
type BaseEventActionOptions struct {
HTTPConfig EventActionHTTPConfig `json:"http_config"`
CmdConfig EventActionCommandConfig `json:"cmd_config"`
EmailConfig EventActionEmailConfig `json:"email_config"`
RetentionConfig EventActionDataRetentionConfig `json:"retention_config"`
+ FsConfig EventActionFilesystemConfig `json:"fs_config"`
}
func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
@@ -378,6 +522,7 @@ func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
RetentionConfig: EventActionDataRetentionConfig{
Folders: folders,
},
+ FsConfig: o.FsConfig.getACopy(),
}
}
@@ -407,27 +552,38 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{}
+ o.FsConfig = EventActionFilesystemConfig{}
return o.HTTPConfig.validate(name)
case ActionTypeCommand:
o.HTTPConfig = EventActionHTTPConfig{}
o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{}
+ o.FsConfig = EventActionFilesystemConfig{}
return o.CmdConfig.validate()
case ActionTypeEmail:
o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{}
+ o.FsConfig = EventActionFilesystemConfig{}
return o.EmailConfig.validate()
case ActionTypeDataRetentionCheck:
o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{}
+ o.FsConfig = EventActionFilesystemConfig{}
return o.RetentionConfig.validate()
+ case ActionTypeFilesystem:
+ o.HTTPConfig = EventActionHTTPConfig{}
+ o.CmdConfig = EventActionCommandConfig{}
+ o.EmailConfig = EventActionEmailConfig{}
+ o.RetentionConfig = EventActionDataRetentionConfig{}
+ return o.FsConfig.validate()
default:
o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{}
+ o.FsConfig = EventActionFilesystemConfig{}
}
return nil
}
@@ -844,6 +1000,45 @@ func (r *EventRule) validate() error {
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))
+ }
+ }
+ case EventTriggerFsEvent:
+ // folder quota reset cannot be executed
+ for _, action := range r.Actions {
+ if action.Type == ActionTypeFolderQuotaReset {
+ return fmt.Errorf("action %q, type %q is not supported for filesystem events",
+ 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))
+ }
+ }
+ }
+ return nil
+}
+
// PrepareForRendering prepares an EventRule for rendering.
// It hides confidential data and set to nil the empty secrets
// so they are not serialized
diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go
index 11c63f36..60ad7c7c 100644
--- a/internal/httpd/httpd_test.go
+++ b/internal/httpd/httpd_test.go
@@ -1592,6 +1592,56 @@ func TestEventActionValidation(t *testing.T) {
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "invalid folder retention")
+ action.Type = dataprovider.ActionTypeFilesystem
+ action.Options.FsConfig = dataprovider.EventActionFilesystemConfig{
+ Type: dataprovider.FilesystemActionRename,
+ }
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "no items to rename specified")
+ action.Options.FsConfig.Renames = []dataprovider.KeyValue{
+ {
+ Key: "",
+ Value: "/adir",
+ },
+ }
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "invalid items to rename")
+ action.Options.FsConfig.Renames = []dataprovider.KeyValue{
+ {
+ Key: "adir",
+ Value: "/adir",
+ },
+ }
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "rename source and target cannot be equal")
+ action.Options.FsConfig.Renames = []dataprovider.KeyValue{
+ {
+ Key: "/",
+ Value: "/dir",
+ },
+ }
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "renaming the root directory is not allowed")
+ action.Options.FsConfig.Type = dataprovider.FilesystemActionMkdirs
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "no directory to create specified")
+ action.Options.FsConfig.MkDirs = []string{"dir1", ""}
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "invalid directory to create")
+ action.Options.FsConfig.Type = dataprovider.FilesystemActionDelete
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "no item to delete specified")
+ action.Options.FsConfig.Deletes = []string{"item1", ""}
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "invalid item to delete")
}
func TestEventRuleValidation(t *testing.T) {
@@ -18550,6 +18600,7 @@ func TestWebEventAction(t *testing.T) {
form := make(url.Values)
form.Set("name", action.Name)
form.Set("description", action.Description)
+ form.Set("fs_action_type", "0")
form.Set("type", "a")
req, err := http.NewRequest(http.MethodPost, webAdminEventActionPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
@@ -18806,6 +18857,45 @@ func TestWebEventAction(t *testing.T) {
}
}
}
+ action.Type = dataprovider.ActionTypeFilesystem
+ action.Options.FsConfig = dataprovider.EventActionFilesystemConfig{
+ Type: dataprovider.FilesystemActionMkdirs,
+ MkDirs: []string{"a ", " a/b"},
+ }
+ form.Set("type", fmt.Sprintf("%d", action.Type))
+ form.Set("fs_mkdir_paths", strings.Join(action.Options.FsConfig.MkDirs, ","))
+ form.Set("fs_action_type", "invalid")
+ req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),
+ bytes.NewBuffer([]byte(form.Encode())))
+ assert.NoError(t, err)
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusOK, rr)
+ assert.Contains(t, rr.Body.String(), "invalid fs action type")
+
+ form.Set("fs_action_type", fmt.Sprintf("%d", action.Options.FsConfig.Type))
+ req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),
+ bytes.NewBuffer([]byte(form.Encode())))
+ assert.NoError(t, err)
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusSeeOther, rr)
+ // check the update
+ actionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)
+ assert.NoError(t, err)
+ assert.Equal(t, action.Type, actionGet.Type)
+ if assert.Len(t, actionGet.Options.FsConfig.MkDirs, 2) {
+ for _, dir := range actionGet.Options.FsConfig.MkDirs {
+ switch dir {
+ case "/a":
+ case "/a/b":
+ default:
+ t.Errorf("unexpected dir path %v", dir)
+ }
+ }
+ }
req, err = http.NewRequest(http.MethodDelete, path.Join(webAdminEventActionPath, action.Name), nil)
assert.NoError(t, err)
diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go
index 8b07f398..52af6a0c 100644
--- a/internal/httpd/webadmin.go
+++ b/internal/httpd/webadmin.go
@@ -304,6 +304,7 @@ type eventActionPage struct {
basePage
Action dataprovider.BaseEventAction
ActionTypes []dataprovider.EnumMapping
+ FsActions []dataprovider.EnumMapping
HTTPMethods []string
RedactedSecret string
Error string
@@ -905,6 +906,7 @@ func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Reque
basePage: s.getBasePageData(title, currentURL, r),
Action: action,
ActionTypes: dataprovider.EventActionTypes,
+ FsActions: dataprovider.FsActionTypes,
HTTPMethods: dataprovider.SupportedHTTPActionMethods,
RedactedSecret: redactedSecret,
Error: error,
@@ -1873,6 +1875,10 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
if err != nil {
return dataprovider.BaseEventActionOptions{}, err
}
+ fsActionType, err := strconv.Atoi(r.Form.Get("fs_action_type"))
+ if err != nil {
+ return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid fs action type: %w", err)
+ }
options := dataprovider.BaseEventActionOptions{
HTTPConfig: dataprovider.EventActionHTTPConfig{
Endpoint: r.Form.Get("http_endpoint"),
@@ -1898,6 +1904,12 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
RetentionConfig: dataprovider.EventActionDataRetentionConfig{
Folders: foldersRetention,
},
+ FsConfig: dataprovider.EventActionFilesystemConfig{
+ Type: fsActionType,
+ Renames: getKeyValsFromPostFields(r, "fs_rename_source", "fs_rename_target"),
+ Deletes: strings.Split(strings.ReplaceAll(r.Form.Get("fs_delete_paths"), " ", ""), ","),
+ MkDirs: strings.Split(strings.ReplaceAll(r.Form.Get("fs_mkdir_paths"), " ", ""), ","),
+ },
}
return options, nil
}
diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go
index fa5182ba..66364d61 100644
--- a/internal/httpdtest/httpdtest.go
+++ b/internal/httpdtest/httpdtest.go
@@ -1349,6 +1349,9 @@ func checkEventAction(expected, actual dataprovider.BaseEventAction) error {
if err := compareEventActionDataRetentionFields(expected.Options.RetentionConfig, actual.Options.RetentionConfig); err != nil {
return err
}
+ if err := compareEventActionFsConfigFields(expected.Options.FsConfig, actual.Options.FsConfig); err != nil {
+ return err
+ }
return compareEventActionHTTPConfigFields(expected.Options.HTTPConfig, actual.Options.HTTPConfig)
}
@@ -2238,6 +2241,32 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
return nil
}
+func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionFilesystemConfig) error {
+ if expected.Type != actual.Type {
+ return errors.New("fs type mismatch")
+ }
+ if err := compareKeyValues(expected.Renames, actual.Renames); err != nil {
+ return errors.New("fs renames mismatch")
+ }
+ if len(expected.Deletes) != len(actual.Deletes) {
+ return errors.New("fs deletes mismatch")
+ }
+ for _, v := range expected.Deletes {
+ if !util.Contains(actual.Deletes, v) {
+ return errors.New("fs deletes content mismatch")
+ }
+ }
+ if len(expected.MkDirs) != len(actual.MkDirs) {
+ return errors.New("fs mkdirs mismatch")
+ }
+ for _, v := range expected.MkDirs {
+ if !util.Contains(actual.MkDirs, v) {
+ return errors.New("fs mkdir content mismatch")
+ }
+ }
+ return nil
+}
+
func compareEventActionCmdConfigFields(expected, actual dataprovider.EventActionCommandConfig) error {
if expected.Cmd != actual.Cmd {
return errors.New("command mismatch")
diff --git a/internal/plugin/notifier.go b/internal/plugin/notifier.go
index 9a1d1745..5a4862af 100644
--- a/internal/plugin/notifier.go
+++ b/internal/plugin/notifier.go
@@ -200,6 +200,9 @@ func (p *notifierPlugin) notifyFsAction(event *notifier.FsEvent) {
}
go func() {
+ Handler.addTask()
+ defer Handler.removeTask()
+
p.sendFsEvent(event)
}()
}
@@ -211,6 +214,9 @@ func (p *notifierPlugin) notifyProviderAction(event *notifier.ProviderEvent, obj
}
go func() {
+ Handler.addTask()
+ defer Handler.removeTask()
+
objectAsJSON, err := object.RenderAsJSON(event.Action != "delete")
if err != nil {
logger.Warn(logSender, "", "unable to render user as json for action %v: %v", event.Action, err)
diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go
index 57101b9d..323979fb 100644
--- a/internal/plugin/plugin.go
+++ b/internal/plugin/plugin.go
@@ -96,35 +96,37 @@ type Manager struct {
closed int32
done chan bool
// List of configured plugins
- Configs []Config `json:"plugins" mapstructure:"plugins"`
- notifLock sync.RWMutex
- notifiers []*notifierPlugin
- kmsLock sync.RWMutex
- kms []*kmsPlugin
- authLock sync.RWMutex
- auths []*authPlugin
- searcherLock sync.RWMutex
- searcher *searcherPlugin
- metadaterLock sync.RWMutex
- metadater *metadataPlugin
- ipFilterLock sync.RWMutex
- filter *ipFilterPlugin
- authScopes int
- hasSearcher bool
- hasMetadater bool
- hasNotifiers bool
- hasAuths bool
- hasIPFilter bool
+ Configs []Config `json:"plugins" mapstructure:"plugins"`
+ notifLock sync.RWMutex
+ notifiers []*notifierPlugin
+ kmsLock sync.RWMutex
+ kms []*kmsPlugin
+ authLock sync.RWMutex
+ auths []*authPlugin
+ searcherLock sync.RWMutex
+ searcher *searcherPlugin
+ metadaterLock sync.RWMutex
+ metadater *metadataPlugin
+ ipFilterLock sync.RWMutex
+ filter *ipFilterPlugin
+ authScopes int
+ hasSearcher bool
+ hasMetadater bool
+ hasNotifiers bool
+ hasAuths bool
+ hasIPFilter bool
+ concurrencyGuard chan struct{}
}
// Initialize initializes the configured plugins
func Initialize(configs []Config, logLevel string) error {
logger.Debug(logSender, "", "initialize")
Handler = Manager{
- Configs: configs,
- done: make(chan bool),
- closed: 0,
- authScopes: -1,
+ Configs: configs,
+ done: make(chan bool),
+ closed: 0,
+ authScopes: -1,
+ concurrencyGuard: make(chan struct{}, 250),
}
setLogLevel(logLevel)
if len(configs) == 0 {
@@ -699,6 +701,14 @@ func (m *Manager) restartIPFilterPlugin(config Config) {
m.ipFilterLock.Unlock()
}
+func (m *Manager) addTask() {
+ m.concurrencyGuard <- struct{}{}
+}
+
+func (m *Manager) removeTask() {
+ <-m.concurrencyGuard
+}
+
// Cleanup releases all the active plugins
func (m *Manager) Cleanup() {
logger.Debug(logSender, "", "cleanup")
diff --git a/templates/webadmin/eventaction.html b/templates/webadmin/eventaction.html
index 899552cb..0ced8df9 100644
--- a/templates/webadmin/eventaction.html
+++ b/templates/webadmin/eventaction.html
@@ -404,6 +404,89 @@ along with this program. If not, see