eventmanager: add password notification check action

this action allow to send an email notification to users whose
password is about to expire

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-12-16 18:51:29 +01:00
parent ac91170d65
commit 2da3eabc12
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
15 changed files with 562 additions and 108 deletions

View file

@ -13,6 +13,7 @@ The following actions are supported:
- `Transfer quota reset`. The transfer quota values will be reset to `0`. - `Transfer quota reset`. The transfer quota values will be reset to `0`.
- `Data retention check`. You can define per-folder retention policies. - `Data retention check`. You can define per-folder retention policies.
- `Metadata check`. A metadata check requires a metadata plugin such as [this one](https://github.com/sftpgo/sftpgo-plugin-metadata) and removes the metadata associated to missing items (for example objects deleted outside SFTPGo). A metadata check does nothing is no metadata plugin is installed or external metadata are not supported for a filesystem. - `Metadata check`. A metadata check requires a metadata plugin such as [this one](https://github.com/sftpgo/sftpgo-plugin-metadata) and removes the metadata associated to missing items (for example objects deleted outside SFTPGo). A metadata check does nothing is no metadata plugin is installed or external metadata are not supported for a filesystem.
- `Password expiration check`. You can send an email notification to users whose password is about to expire.
- `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: - `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. - `Rename`. You can rename one or more files or directories.
- `Delete`. You can delete one or more files and directories. - `Delete`. You can delete one or more files and directories.

52
go.mod
View file

@ -8,15 +8,15 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go-v2 v1.17.2 github.com/aws/aws-sdk-go-v2 v1.17.3
github.com/aws/aws-sdk-go-v2/config v1.18.4 github.com/aws/aws-sdk-go-v2/config v1.18.5
github.com/aws/aws-sdk-go-v2/credentials v1.13.4 github.com/aws/aws-sdk-go-v2/credentials v1.13.5
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.20 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.43 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.44
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.25 github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.26
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.5 github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.9 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.10
github.com/aws/aws-sdk-go-v2/service/sts v1.17.6 github.com/aws/aws-sdk-go-v2/service/sts v1.17.7
github.com/cockroachdb/cockroach-go/v2 v2.2.19 github.com/cockroachdb/cockroach-go/v2 v2.2.19
github.com/coreos/go-oidc/v3 v3.4.0 github.com/coreos/go-oidc/v3 v3.4.0
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b
@ -45,7 +45,7 @@ require (
github.com/otiai10/copy v1.9.0 github.com/otiai10/copy v1.9.0
github.com/pires/go-proxyproto v0.6.2 github.com/pires/go-proxyproto v0.6.2
github.com/pkg/sftp v1.13.6-0.20221020054726-e4133ab7e9bd github.com/pkg/sftp v1.13.6-0.20221020054726-e4133ab7e9bd
github.com/pquerna/otp v1.3.0 github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.14.0 github.com/prometheus/client_golang v1.14.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5 github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5
@ -72,28 +72,28 @@ require (
golang.org/x/sys v0.3.0 golang.org/x/sys v0.3.0
golang.org/x/term v0.3.0 golang.org/x/term v0.3.0
golang.org/x/time v0.3.0 golang.org/x/time v0.3.0
google.golang.org/api v0.104.0 google.golang.org/api v0.105.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
) )
require ( require (
cloud.google.com/go v0.107.0 // indirect cloud.google.com/go v0.107.0 // indirect
cloud.google.com/go/compute v1.14.0 // indirect cloud.google.com/go/compute v1.14.0 // indirect
cloud.google.com/go/compute/metadata v0.2.2 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.8.0 // indirect cloud.google.com/go/iam v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.26 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.20 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.27 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.17 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.21 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.20 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.20 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.26 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.11.27 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.9 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.10 // indirect
github.com/aws/smithy-go v1.13.5 // indirect github.com/aws/smithy-go v1.13.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect
@ -126,7 +126,7 @@ require (
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.0 // indirect github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lib/pq v1.10.7 // indirect github.com/lib/pq v1.10.7 // indirect
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
@ -141,9 +141,9 @@ require (
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.38.0 // indirect github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect

103
go.sum
View file

@ -52,16 +52,16 @@ cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLq
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0= cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=
cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 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/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk= cloud.google.com/go/iam v0.9.0 h1:bK6Or6mxhuL8lnj1i9j0yMo2wE/IeTO2cWlfUrf/TZs=
cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= cloud.google.com/go/iam v0.9.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM=
cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=
cloud.google.com/go/kms v1.6.0 h1:OWRZzrPmOZUzurjI2FBGtgY2mB1WaJkqhw6oIwSj0Yg= cloud.google.com/go/kms v1.6.0 h1:OWRZzrPmOZUzurjI2FBGtgY2mB1WaJkqhw6oIwSj0Yg=
cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
@ -108,8 +108,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSa
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 h1:Oj853U9kG+RLTCQXpjvOnrv0WaZHxgmZz1TlLywgOPY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 h1:+5VZ72z0Qan5Bog5C+ZkgSqUbeVUd9wgtHOrIKuc5b8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.0.2/go.mod h1:LH9XQnMr2ZYxQdVdCrzLO9mxeDyrDFa6wbSI3x5zCZk= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.0.2/go.mod h1:LH9XQnMr2ZYxQdVdCrzLO9mxeDyrDFa6wbSI3x5zCZk=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1/go.mod h1:eZ4g6GUvXiGulfIbbhh1Xr4XwUYaYaWMqzGD/284wCA= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1/go.mod h1:eZ4g6GUvXiGulfIbbhh1Xr4XwUYaYaWMqzGD/284wCA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1 h1:YvQv9Mz6T8oR5ypQOL6erY0Z5t71ak1uHV4QFokCOZk= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1 h1:YvQv9Mz6T8oR5ypQOL6erY0Z5t71ak1uHV4QFokCOZk=
@ -227,67 +227,67 @@ github.com/aws/aws-sdk-go v1.44.45/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4
github.com/aws/aws-sdk-go v1.44.68/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.44.68/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aws/aws-sdk-go-v2 v1.16.8/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw= github.com/aws/aws-sdk-go-v2 v1.16.8/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw=
github.com/aws/aws-sdk-go-v2 v1.17.2 h1:r0yRZInwiPBNpQ4aDy/Ssh3ROWsGtKDwar2JS8Lm+N8= github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY=
github.com/aws/aws-sdk-go-v2 v1.17.2/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
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.3/go.mod h1:gNsR5CaXKmQSSzrmGxmwmct/r+ZBfbxorAuXYsj/M5Y=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
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.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E=
github.com/aws/aws-sdk-go-v2/config v1.18.4 h1:VZKhr3uAADXHStS/Gf9xSYVmmaluTUfkc0dcbPiDsKE= github.com/aws/aws-sdk-go-v2/config v1.18.5 h1:teGdDCAT3gX99FIKNt6HsvLaeOVdCFiCQDlH8UV6Xvg=
github.com/aws/aws-sdk-go-v2/config v1.18.4/go.mod h1:EZxMPLSdGAZ3eAmkqXfYbRppZJTzFTkv8VyEzJhKko4= github.com/aws/aws-sdk-go-v2/config v1.18.5/go.mod h1:0g4tGVHeUTxekZIkO5Glw2AemETlmnkQvFqkdv3HBAA=
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.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk=
github.com/aws/aws-sdk-go-v2/credentials v1.13.4 h1:nEbHIyJy7mCvQ/kzGG7VWHSBpRB4H6sJy3bWierWUtg= github.com/aws/aws-sdk-go-v2/credentials v1.13.5 h1:vrPwnKCdQlUyxXDZtPpb6Hc3GbTndqaGtEOwm/lF5tI=
github.com/aws/aws-sdk-go-v2/credentials v1.13.4/go.mod h1:/Cj5w9LRsNTLSwexsohwDME32OzJ6U81Zs33zr2ZWOM= github.com/aws/aws-sdk-go-v2/credentials v1.13.5/go.mod h1:sS/NgdbdkQ6XhVkGY/yEmNwxzpRVxLT3Ns+42W37p6g=
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.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.20 h1:tpNOglTZ8kg9T38NpcGBxudqfUAwUzyUnLQ4XSd0CHE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 h1:j9wi1kQ8b+e0FBVHxCqCGo4kxDU175hoDHcWAi0sauU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.20/go.mod h1:d9xFpWd3qYwdIXM0fvu7deD08vvdRXyc/ueV+0SqaWE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21/go.mod h1:ugwW57Z5Z48bpvUyZuaPy4Kv+vEfJWnIrky7RmkBvJg=
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.21/go.mod h1:iIYPrQ2rYfZiB/iADYlhj9HHZ9TTi6PqKQPAqygohbE=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.43 h1:+bkAMTd5OGyHu2nwNOangjEsP65fR0uhMbZJA52sZ64= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.44 h1:BXBHQeRh4fGFiiMZZd/WI/aAPwjhUCrWDhRDbuOZPIM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.43/go.mod h1:sS2tu0VEspKuY5eM1vQgy7P/hpZX8F62o6qsghZExWc= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.44/go.mod h1:yFjq/63PmWzfzHk66kMsouDqS/ilrjFKWu269GWE6iw=
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.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.26 h1:5WU31cY7m0tG+AiaXuXGoMzo2GBQ1IixtWa8Yywsgco= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.26/go.mod h1:2E0LdbJW6lbeU4uxjum99GZzI0ZjDpAb0CoSCM0oeEY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI=
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.9/go.mod h1:08tUpeSGN33QKSO7fwxXczNfiwCpbj+GxK6XKwqWVv0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.20 h1:WW0qSzDWoiWU2FS5DbKpxGilFVlCEJPwx4YtjdfI0Jw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 h1:5NbbMrIzmUn/TXFqAle6mgrH5m9cOvMLRGL7pnG8tRE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.20/go.mod h1:/+6lSiby8TBFpTVXZgKiN/rCfkYXEGvhlM4zCgPpt7w= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE=
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/ini v1.3.16/go.mod h1:CYmI+7x03jjJih8kBEEFKRQc40UjUokT0k7GbvrhhTc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.27 h1:N2eKFw2S+JWRCtTt0IhIX7uoGGQciD4p6ba+SJv4WEU= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 h1:KeTxcGdNnQudb46oOl4d90f2I33DF/c6q3RnZAmvQdQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.27/go.mod h1:RdwFVc7PBYWY33fa2+8T1mSqQ7ZEK4ILpM0wfioDC3w= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28/go.mod h1:yRZVr/iT0AqyHeep00SZ4YfBAKojXz08w3XMBscdi0c=
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/v4a v1.0.6/go.mod h1:O7Oc4peGZDEKlddivslfYFvAbgzvl/GH3J8j3JIGBXc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.17 h1:5tXbMJ7Jq0iG65oiMg6tCLsHkSaO2xLXa2EmZ29vaTA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.18 h1:H/mF2LNWwX00lD6FlYfKpLLZgUW7oIzCBkig78x4Xok=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.17/go.mod h1:twV0fKMQuqLY4klyFH56aXNq3AFiA5LO0/frTczEOFE= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.18/go.mod h1:T2Ku+STrYQ1zIkL1wMvj8P3wWQaaCMKNdz70MT2FLfE=
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.3/go.mod h1:gkb2qADY+OHaGLKNTYxMaQNacfeyQpZ4csDTQMeFmcw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
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.10/go.mod h1:Qks+dxK3O+Z2deAhNo6cJ8ls1bam3tUGUAcgxQP1c70=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.21 h1:77b1GfaSuIok5yB/3HYbG+ypWvOJDQ2rVdq943D17R4= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22 h1:kv5vRAl00tozRxSnI0IszPWGXsJOyA7hmEUHFYqsyvw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.21/go.mod h1:sPOz31BVdqeeurKEuUpLNSve4tdCNPluE+070HNcEHI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22/go.mod h1:Od+GU5+Yx41gryN/ZGZzAJMZ9R1yn6lgA0fD5Lo5SkQ=
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.9/go.mod h1:yQowTpvdZkFVuHrLBXmczat4W+WJKg/PafBZnGBLga0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.20 h1:jlgyHbkZQAgAc7VIxJDmtouH8eNjOk2REVAQfVhdaiQ= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 h1:5C6XgTViSb0bunmU57b3CT+MhxULqHH2721FVA+/kDM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.20/go.mod h1:Xs52xaLBqDEKRcAfX/hgjmD3YQ7c/W+BEyfamlO/W2E= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21/go.mod h1:lRToEJsn+DRA9lW4O9L9+/3hjTkUzlzyzHqn8MTds5k=
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.9/go.mod h1:Rc5+wn2k8gFSi3V1Ch4mhxOzjMh+bYSXVFfVaqowQOY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.20 h1:4K6dbmR0mlp3o4Bo78PnpvzHtYAqEeVMguvEenpMGsI= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21 h1:vY5siRXvW5TrOKm2qKEf9tliBfdLxdfy0i02LOcmqUo=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.20/go.mod h1:1XpDcReIEOHsjwNToDKhIAO3qwLo1BnfbtSqWJa8j7g= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21/go.mod h1:WZvNXT1XuH8dnJM0HvOlvk+RNn7NbAPvA/ACO0QarSc=
github.com/aws/aws-sdk-go-v2/service/kms v1.18.1/go.mod h1:4PZMUkc9rXHWGVB5J9vKaZy3D7Nai79ORworQ3ASMiM= github.com/aws/aws-sdk-go-v2/service/kms v1.18.1/go.mod h1:4PZMUkc9rXHWGVB5J9vKaZy3D7Nai79ORworQ3ASMiM=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.25 h1:rCfUDMT9KO++W4TkIJr3BO0h749xkRX4RpHdydSFfHw= github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.26 h1:NlA5om7Um+ohI/S0inBd55Vsp84BKhBrAgw2IVqEb30=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.25/go.mod h1:xCXhZdPR0p6uaNhggLVGREnJaUIc2CNOJydWnZ2R7OQ= github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.26/go.mod h1:DSuypbY6jb7WZSxrLuCgd7ouB5uRQ+Hg5wbt0GmgRcc=
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.2/go.mod h1:u+566cosFI+d+motIz3USXEh6sN8Nq4GrNXSg2RXVMo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.5 h1:nRSEQj1JergKTVc8RGkhZvOEGgcvo4fWpDPwGDeg2ok= github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6 h1:W8pLcSn6Uy0eXgDBUUl8M8Kxv7JCoP68ZKTD04OXLEA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.5/go.mod h1:wcaJTmjKFDW0s+Se55HBNIds6ghdAGoDDw+SGUdrfAk= github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6/go.mod h1:L2l2/q76teehcW7YEsgsDjqdsDTERJeX3nOMIFlgGUE=
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.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.9 h1:ogcakjF/mrZOo9oJVWmRbG838C04oWGXI8T8IY4xcfM= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.10 h1:6obimjQAiRlEUZT7a2Q1ikH7ck4cPO3phGz4wqI5f2w=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.9/go.mod h1:S7AsUoaHONHV2iGM5QXQOonnaV05cK9fty2dXRdouws= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.10/go.mod h1:jAeo/PdIJZuDSwsvxJS94G4d6h8tStj7WXVuKwLHWU8=
github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM= github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM=
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU= github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU=
github.com/aws/aws-sdk-go-v2/service/ssm v1.27.6/go.mod h1:fiFzQgj4xNOg4/wqmAiPvzgDMXPD+cUEplX/CYn+0j0= github.com/aws/aws-sdk-go-v2/service/ssm v1.27.6/go.mod h1:fiFzQgj4xNOg4/wqmAiPvzgDMXPD+cUEplX/CYn+0j0=
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.13/go.mod h1:d7ptRksDDgvXaUvxyHZ9SYh+iMDymm94JbVcgvSYSzU=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.26 h1:ActQgdTNQej/RuUJjB9uxYVLDOvRGtUreXF8L3c8wyg= github.com/aws/aws-sdk-go-v2/service/sso v1.11.27 h1:Nmvn0DJKg00TBmoBweK253Kdsuy4V5Rs68yL/H15uBQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.26/go.mod h1:uB9tV79ULEZUXc6Ob18A46KSQ0JDlrplPni9XW6Ot60= github.com/aws/aws-sdk-go-v2/service/sso v1.11.27/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.9 h1:wihKuqYUlA2T/Rx+yu2s6NDAns8B9DgnRooB1PVhY+Q= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.10 h1:tGOUUjINuqI8sD6pn+Ku0/f/4UfRDlK+jJUOaxEbWuQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.9/go.mod h1:2E/3D/mB8/r2J7nK42daoKP/ooCwbf0q1PznNc+DZTU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.10/go.mod h1:TZSH7xLO7+phDtViY/KUp9WGCJMQkLJ/VpgkTFd5gh8=
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.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E=
github.com/aws/aws-sdk-go-v2/service/sts v1.17.6 h1:VQFOLQVL3BrKM/NLO/7FiS4vcp5bqK0mGMyk09xLoAY= github.com/aws/aws-sdk-go-v2/service/sts v1.17.7 h1:9Mtq1KM6nD8/+HStvWcvYnixJ5N85DX+P+OY3kI3W2k=
github.com/aws/aws-sdk-go-v2/service/sts v1.17.6/go.mod h1:Az3OXXYGyfNwQNsK/31L4R75qFYnO641RZGAoV3uH1c= github.com/aws/aws-sdk-go-v2/service/sts v1.17.7/go.mod h1:+lGbb3+1ugwKrNTWcf2RT05Xmp543B06zDFTwiTLp7I=
github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
@ -1099,8 +1099,9 @@ github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzlt
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.8 h1:jCFT8oc0hEDVjgUgsBy1F9cbjsjAVZSXNi7JaU9HR/Q= github.com/lestrrat-go/jwx/v2 v2.0.8 h1:jCFT8oc0hEDVjgUgsBy1F9cbjsjAVZSXNi7JaU9HR/Q=
github.com/lestrrat-go/jwx/v2 v2.0.8/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0= github.com/lestrrat-go/jwx/v2 v2.0.8/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -1344,11 +1345,11 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/alertmanager v0.24.0/go.mod h1:r6fy/D7FRuZh5YbnX6J3MBY0eI4Pb5yPYS7/bPSXXqI= github.com/prometheus/alertmanager v0.24.0/go.mod h1:r6fy/D7FRuZh5YbnX6J3MBY0eI4Pb5yPYS7/bPSXXqI=
@ -1390,8 +1391,8 @@ github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE= github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/common v0.38.0 h1:VTQitp6mXTdUoCmDMugDVOJ1opi6ADftKfp/yeqTR/E= github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
github.com/prometheus/common v0.38.0/go.mod h1:MBXfmBQZrK5XpbCkjofnXs96LD2QQ7fEq4C0xjC/yec= github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
github.com/prometheus/common/assets v0.1.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/common/assets v0.1.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI=
github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI=
github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI=
@ -2189,8 +2190,8 @@ google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6F
google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.104.0 h1:KBfmLRqdZEbwQleFlSLnzpQJwhjpmNOk4cKQIBDZ9mg= google.golang.org/api v0.105.0 h1:t6P9Jj+6XTn4U9I2wycQai6Q/Kz7iOT+QzjJ3G2V4x8=
google.golang.org/api v0.104.0/go.mod h1:JCspTXJbBxa5ySXw4UgUqVer7DfVxbvc/CTUFqAED5U= google.golang.org/api v0.105.0/go.mod h1:qh7eD5FJks5+BcE+cjBIm6Gz8vioK7EHvnlniqXBnqI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

View file

@ -1643,9 +1643,9 @@ func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions
} }
} }
func executeQuotaResetForUser(user dataprovider.User) error { func executeQuotaResetForUser(user *dataprovider.User) error {
if err := user.LoadAndApplyGroupSettings(); err != nil { if err := user.LoadAndApplyGroupSettings(); err != nil {
eventManagerLog(logger.LevelDebug, "skipping scheduled quota reset for user %s, cannot apply group settings: %v", eventManagerLog(logger.LevelError, "skipping scheduled quota reset for user %s, cannot apply group settings: %v",
user.Username, err) user.Username, err)
return err return err
} }
@ -1660,7 +1660,7 @@ func executeQuotaResetForUser(user dataprovider.User) error {
eventManagerLog(logger.LevelError, "error scanning quota for user %q: %v", user.Username, err) eventManagerLog(logger.LevelError, "error scanning quota for user %q: %v", user.Username, err)
return fmt.Errorf("error scanning quota for user %q: %w", user.Username, err) return fmt.Errorf("error scanning quota for user %q: %w", user.Username, err)
} }
err = dataprovider.UpdateUserQuota(&user, numFiles, size, true) err = dataprovider.UpdateUserQuota(user, numFiles, size, true)
if err != nil { if err != nil {
eventManagerLog(logger.LevelError, "error updating quota for user %q: %v", user.Username, err) eventManagerLog(logger.LevelError, "error updating quota for user %q: %v", user.Username, err)
return fmt.Errorf("error updating quota for user %q: %w", user.Username, err) return fmt.Errorf("error updating quota for user %q: %w", user.Username, err)
@ -1685,7 +1685,7 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions,
} }
} }
executed++ executed++
if err = executeQuotaResetForUser(user); err != nil { if err = executeQuotaResetForUser(&user); err != nil {
params.AddError(err) params.AddError(err)
failedResets = append(failedResets, user.Username) failedResets = append(failedResets, user.Username)
continue continue
@ -1789,7 +1789,7 @@ func executeDataRetentionCheckForUser(user dataprovider.User, folders []dataprov
params *EventParams, actionName string, params *EventParams, actionName string,
) error { ) error {
if err := user.LoadAndApplyGroupSettings(); err != nil { if err := user.LoadAndApplyGroupSettings(); err != nil {
eventManagerLog(logger.LevelDebug, "skipping scheduled retention check for user %s, cannot apply group settings: %v", eventManagerLog(logger.LevelError, "skipping scheduled retention check for user %s, cannot apply group settings: %v",
user.Username, err) user.Username, err)
return err return err
} }
@ -1850,9 +1850,9 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete
return nil return nil
} }
func executeMetadataCheckForUser(user dataprovider.User) error { func executeMetadataCheckForUser(user *dataprovider.User) error {
if err := user.LoadAndApplyGroupSettings(); err != nil { if err := user.LoadAndApplyGroupSettings(); err != nil {
eventManagerLog(logger.LevelDebug, "skipping scheduled quota reset for user %s, cannot apply group settings: %v", eventManagerLog(logger.LevelError, "skipping scheduled quota reset for user %s, cannot apply group settings: %v",
user.Username, err) user.Username, err)
return err return err
} }
@ -1886,7 +1886,7 @@ func executeMetadataCheckRuleAction(conditions dataprovider.ConditionOptions, pa
} }
} }
executed++ executed++
if err = executeMetadataCheckForUser(user); err != nil { if err = executeMetadataCheckForUser(&user); err != nil {
params.AddError(err) params.AddError(err)
failures = append(failures, user.Username) failures = append(failures, user.Username)
continue continue
@ -1902,6 +1902,72 @@ func executeMetadataCheckRuleAction(conditions dataprovider.ConditionOptions, pa
return nil return nil
} }
func executePwdExpirationCheckForUser(user *dataprovider.User, config dataprovider.EventActionPasswordExpiration) error {
if err := user.LoadAndApplyGroupSettings(); err != nil {
eventManagerLog(logger.LevelError, "skipping password expiration check for user %s, cannot apply group settings: %v",
user.Username, err)
return err
}
if user.Filters.PasswordExpiration == 0 {
eventManagerLog(logger.LevelDebug, "password expiration not set for user %q skipping check", user.Username)
return nil
}
days := user.PasswordExpiresIn()
if days > config.Threshold {
eventManagerLog(logger.LevelDebug, "password for user %q expires in %d days, threshold %d, no need to notify",
user.Username, days, config.Threshold)
return nil
}
body := new(bytes.Buffer)
data := make(map[string]any)
data["Username"] = user.Username
data["Days"] = days
if err := smtp.RenderPasswordExpirationTemplate(body, data); err != nil {
eventManagerLog(logger.LevelError, "unable to notify password expiration for user %s: %v",
user.Username, err)
return err
}
subject := "SFTPGo password expiration notification"
startTime := time.Now()
if err := smtp.SendEmail([]string{user.Email}, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {
eventManagerLog(logger.LevelError, "unable to notify password expiration for user %s: %v, elapsed: %s",
user.Username, err, time.Since(startTime))
return err
}
eventManagerLog(logger.LevelDebug, "password expiration email sent to user %s, days: %d, elapsed: %s",
user.Username, days, time.Since(startTime))
return nil
}
func executePwdExpirationCheckRuleAction(config dataprovider.EventActionPasswordExpiration, 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
for _, user := range users {
// if sender is set, the conditions have already been evaluated
if params.sender == "" {
if !checkUserConditionOptions(&user, &conditions) {
eventManagerLog(logger.LevelDebug, "skipping password check for user %q, condition options don't match",
user.Username)
continue
}
}
if err = executePwdExpirationCheckForUser(&user, config); err != nil {
params.AddError(err)
failures = append(failures, user.Username)
continue
}
}
if len(failures) > 0 {
return fmt.Errorf("password expiration check failed for users: %+v", failures)
}
return nil
}
func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams, func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
conditions dataprovider.ConditionOptions, conditions dataprovider.ConditionOptions,
) error { ) error {
@ -1932,6 +1998,8 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
err = executeMetadataCheckRuleAction(conditions, params) err = executeMetadataCheckRuleAction(conditions, params)
case dataprovider.ActionTypeFilesystem: case dataprovider.ActionTypeFilesystem:
err = executeFsRuleAction(action.Options.FsConfig, conditions, params) err = executeFsRuleAction(action.Options.FsConfig, conditions, params)
case dataprovider.ActionTypePasswordExpirationCheck:
err = executePwdExpirationCheckRuleAction(action.Options.PwdExpirationConfig, conditions, params)
default: default:
err = fmt.Errorf("unsupported action type: %d", action.Type) err = fmt.Errorf("unsupported action type: %d", action.Type)
} }

View file

@ -408,9 +408,12 @@ func TestEventManagerErrors(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
err = executeCompressFsRuleAction(dataprovider.EventActionFsCompress{}, nil, dataprovider.ConditionOptions{}, &EventParams{}) err = executeCompressFsRuleAction(dataprovider.EventActionFsCompress{}, nil, dataprovider.ConditionOptions{}, &EventParams{})
assert.Error(t, err) assert.Error(t, err)
err = executePwdExpirationCheckRuleAction(dataprovider.EventActionPasswordExpiration{},
dataprovider.ConditionOptions{}, &EventParams{})
assert.Error(t, err)
groupName := "agroup" groupName := "agroup"
err = executeQuotaResetForUser(dataprovider.User{ err = executeQuotaResetForUser(&dataprovider.User{
Groups: []sdk.GroupMapping{ Groups: []sdk.GroupMapping{
{ {
Name: groupName, Name: groupName,
@ -419,7 +422,7 @@ func TestEventManagerErrors(t *testing.T) {
}, },
}) })
assert.Error(t, err) assert.Error(t, err)
err = executeMetadataCheckForUser(dataprovider.User{ err = executeMetadataCheckForUser(&dataprovider.User{
Groups: []sdk.GroupMapping{ Groups: []sdk.GroupMapping{
{ {
Name: groupName, Name: groupName,
@ -490,6 +493,14 @@ func TestEventManagerErrors(t *testing.T) {
}, },
}}, []string{"/a", "/b"}, nil) }}, []string{"/a", "/b"}, nil)
assert.Error(t, err) assert.Error(t, err)
err = executePwdExpirationCheckForUser(&dataprovider.User{
Groups: []sdk.GroupMapping{
{
Name: groupName,
Type: sdk.GroupTypePrimary,
},
}}, dataprovider.EventActionPasswordExpiration{})
assert.Error(t, err)
_, _, err = getHTTPRuleActionBody(dataprovider.EventActionHTTPConfig{ _, _, err = getHTTPRuleActionBody(dataprovider.EventActionHTTPConfig{
Method: http.MethodPost, Method: http.MethodPost,
@ -687,11 +698,24 @@ func TestEventRuleActions(t *testing.T) {
}, },
}, },
} }
user2.Filters.PasswordExpiration = 10
err = dataprovider.AddUser(&user1, "", "", "") err = dataprovider.AddUser(&user1, "", "", "")
assert.NoError(t, err) assert.NoError(t, err)
err = dataprovider.AddUser(&user2, "", "", "") err = dataprovider.AddUser(&user2, "", "", "")
assert.NoError(t, err) assert.NoError(t, err)
err = executePwdExpirationCheckRuleAction(dataprovider.EventActionPasswordExpiration{
Threshold: 20,
}, dataprovider.ConditionOptions{
Names: []dataprovider.ConditionPattern{
{
Pattern: user2.Username,
},
},
}, &EventParams{})
// smtp not configured
assert.Error(t, err)
action = dataprovider.BaseEventAction{ action = dataprovider.BaseEventAction{
Type: dataprovider.ActionTypeUserQuotaReset, Type: dataprovider.ActionTypeUserQuotaReset,
} }

View file

@ -5690,6 +5690,209 @@ func TestEventRuleIPBlocked(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestEventRulePasswordExpiration(t *testing.T) {
smtpCfg := smtp.Config{
Host: "127.0.0.1",
Port: 2525,
From: "notification@example.com",
TemplatesPath: "templates",
}
err := smtpCfg.Initialize(configDir)
require.NoError(t, err)
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
a1 := dataprovider.BaseEventAction{
Name: "a1",
Type: dataprovider.ActionTypeEmail,
Options: dataprovider.BaseEventActionOptions{
EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: []string{"failure@example.net"},
Subject: `Failure`,
Body: "Failure action",
},
},
}
action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
assert.NoError(t, err)
a2 := dataprovider.BaseEventAction{
Name: "a2",
Type: dataprovider.ActionTypePasswordExpirationCheck,
Options: dataprovider.BaseEventActionOptions{
PwdExpirationConfig: dataprovider.EventActionPasswordExpiration{
Threshold: 10,
},
},
}
action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
assert.NoError(t, err)
a3 := dataprovider.BaseEventAction{
Name: "a3",
Type: dataprovider.ActionTypeEmail,
Options: dataprovider.BaseEventActionOptions{
EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: []string{"success@example.net"},
Subject: `OK`,
Body: "OK action",
},
},
}
action3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)
assert.NoError(t, err)
r1 := dataprovider.EventRule{
Name: "rule1",
Trigger: dataprovider.EventTriggerFsEvent,
Conditions: dataprovider.EventConditions{
FsEvents: []string{"mkdir"},
},
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
},
Order: 1,
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action3.Name,
},
Order: 2,
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action1.Name,
},
Options: dataprovider.EventActionOptions{
IsFailureAction: true,
},
},
},
}
rule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated)
assert.NoError(t, err, string(resp))
dirName := "aTestDir"
conn, client, err := getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
lastReceivedEmail.reset()
err := client.Mkdir(dirName)
assert.NoError(t, err)
// the user has no password expiration, the check will be skipped and the ok action executed
assert.Eventually(t, func() bool {
return lastReceivedEmail.get().From != ""
}, 1500*time.Millisecond, 100*time.Millisecond)
email := lastReceivedEmail.get()
assert.Len(t, email.To, 1)
assert.Contains(t, email.To, "success@example.net")
err = client.RemoveDirectory(dirName)
assert.NoError(t, err)
}
user.Filters.PasswordExpiration = 20
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
conn, client, err = getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
lastReceivedEmail.reset()
err := client.Mkdir(dirName)
assert.NoError(t, err)
// the passowrd is not about to expire, the check will be skipped and the ok action executed
assert.Eventually(t, func() bool {
return lastReceivedEmail.get().From != ""
}, 1500*time.Millisecond, 100*time.Millisecond)
email := lastReceivedEmail.get()
assert.Len(t, email.To, 1)
assert.Contains(t, email.To, "success@example.net")
err = client.RemoveDirectory(dirName)
assert.NoError(t, err)
}
user.Filters.PasswordExpiration = 5
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
conn, client, err = getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
lastReceivedEmail.reset()
err := client.Mkdir(dirName)
assert.NoError(t, err)
// the passowrd is about to expire, the user has no email, the failure action will be executed
assert.Eventually(t, func() bool {
return lastReceivedEmail.get().From != ""
}, 1500*time.Millisecond, 100*time.Millisecond)
email := lastReceivedEmail.get()
assert.Len(t, email.To, 1)
assert.Contains(t, email.To, "failure@example.net")
err = client.RemoveDirectory(dirName)
assert.NoError(t, err)
}
// remove the success action
rule1.Actions = []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
},
Order: 1,
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action1.Name,
},
Options: dataprovider.EventActionOptions{
IsFailureAction: true,
},
},
}
_, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK)
assert.NoError(t, err)
user.Email = "user@example.net"
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
conn, client, err = getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
lastReceivedEmail.reset()
err := client.Mkdir(dirName)
assert.NoError(t, err)
// the passowrd expiration will be notified
assert.Eventually(t, func() bool {
return lastReceivedEmail.get().From != ""
}, 1500*time.Millisecond, 100*time.Millisecond)
email := lastReceivedEmail.get()
assert.Len(t, email.To, 1)
assert.Contains(t, email.To, user.Email)
assert.Contains(t, email.Data, "Your SFTPGo password expires in 5 days")
err = client.RemoveDirectory(dirName)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveEventRule(rule1, 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.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
smtpCfg = smtp.Config{}
err = smtpCfg.Initialize(configDir)
require.NoError(t, err)
}
func TestSyncUploadAction(t *testing.T) { func TestSyncUploadAction(t *testing.T) {
if runtime.GOOS == osWindows { if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows") t.Skip("this test is not available on Windows")

View file

@ -45,12 +45,13 @@ const (
ActionTypeDataRetentionCheck ActionTypeDataRetentionCheck
ActionTypeFilesystem ActionTypeFilesystem
ActionTypeMetadataCheck ActionTypeMetadataCheck
ActionTypePasswordExpirationCheck
) )
var ( var (
supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem, supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset, ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
ActionTypeDataRetentionCheck, ActionTypeMetadataCheck} ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypePasswordExpirationCheck}
) )
func isActionTypeValid(action int) bool { func isActionTypeValid(action int) bool {
@ -77,6 +78,8 @@ func getActionTypeAsString(action int) string {
return "Metadata check" return "Metadata check"
case ActionTypeFilesystem: case ActionTypeFilesystem:
return "Filesystem" return "Filesystem"
case ActionTypePasswordExpirationCheck:
return "Password expiration check"
default: default:
return "Command" return "Command"
} }
@ -755,6 +758,20 @@ func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {
} }
} }
// EventActionPasswordExpiration defines the configuration for password expiration actions
type EventActionPasswordExpiration struct {
// An email notification will be generated for users whose password expires in a number
// of days less than or equal to this threshold
Threshold int `json:"threshold,omitempty"`
}
func (c *EventActionPasswordExpiration) validate() error {
if c.Threshold <= 0 {
return util.NewValidationError("threshold must be greater than 0")
}
return nil
}
// BaseEventActionOptions defines the supported configuration options for a base event actions // BaseEventActionOptions defines the supported configuration options for a base event actions
type BaseEventActionOptions struct { type BaseEventActionOptions struct {
HTTPConfig EventActionHTTPConfig `json:"http_config"` HTTPConfig EventActionHTTPConfig `json:"http_config"`
@ -762,6 +779,7 @@ type BaseEventActionOptions struct {
EmailConfig EventActionEmailConfig `json:"email_config"` EmailConfig EventActionEmailConfig `json:"email_config"`
RetentionConfig EventActionDataRetentionConfig `json:"retention_config"` RetentionConfig EventActionDataRetentionConfig `json:"retention_config"`
FsConfig EventActionFilesystemConfig `json:"fs_config"` FsConfig EventActionFilesystemConfig `json:"fs_config"`
PwdExpirationConfig EventActionPasswordExpiration `json:"pwd_expiration_config"`
} }
func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions { func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
@ -819,6 +837,9 @@ func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
RetentionConfig: EventActionDataRetentionConfig{ RetentionConfig: EventActionDataRetentionConfig{
Folders: folders, Folders: folders,
}, },
PwdExpirationConfig: EventActionPasswordExpiration{
Threshold: o.PwdExpirationConfig.Threshold,
},
FsConfig: o.FsConfig.getACopy(), FsConfig: o.FsConfig.getACopy(),
} }
} }
@ -850,37 +871,50 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
o.EmailConfig = EventActionEmailConfig{} o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{} o.RetentionConfig = EventActionDataRetentionConfig{}
o.FsConfig = EventActionFilesystemConfig{} o.FsConfig = EventActionFilesystemConfig{}
o.PwdExpirationConfig = EventActionPasswordExpiration{}
return o.HTTPConfig.validate(name) return o.HTTPConfig.validate(name)
case ActionTypeCommand: case ActionTypeCommand:
o.HTTPConfig = EventActionHTTPConfig{} o.HTTPConfig = EventActionHTTPConfig{}
o.EmailConfig = EventActionEmailConfig{} o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{} o.RetentionConfig = EventActionDataRetentionConfig{}
o.FsConfig = EventActionFilesystemConfig{} o.FsConfig = EventActionFilesystemConfig{}
o.PwdExpirationConfig = EventActionPasswordExpiration{}
return o.CmdConfig.validate() return o.CmdConfig.validate()
case ActionTypeEmail: case ActionTypeEmail:
o.HTTPConfig = EventActionHTTPConfig{} o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{} o.CmdConfig = EventActionCommandConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{} o.RetentionConfig = EventActionDataRetentionConfig{}
o.FsConfig = EventActionFilesystemConfig{} o.FsConfig = EventActionFilesystemConfig{}
o.PwdExpirationConfig = EventActionPasswordExpiration{}
return o.EmailConfig.validate() return o.EmailConfig.validate()
case ActionTypeDataRetentionCheck: case ActionTypeDataRetentionCheck:
o.HTTPConfig = EventActionHTTPConfig{} o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{} o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{} o.EmailConfig = EventActionEmailConfig{}
o.FsConfig = EventActionFilesystemConfig{} o.FsConfig = EventActionFilesystemConfig{}
o.PwdExpirationConfig = EventActionPasswordExpiration{}
return o.RetentionConfig.validate() return o.RetentionConfig.validate()
case ActionTypeFilesystem: case ActionTypeFilesystem:
o.HTTPConfig = EventActionHTTPConfig{} o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{} o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{} o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{} o.RetentionConfig = EventActionDataRetentionConfig{}
o.PwdExpirationConfig = EventActionPasswordExpiration{}
return o.FsConfig.validate() return o.FsConfig.validate()
case ActionTypePasswordExpirationCheck:
o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{}
o.FsConfig = EventActionFilesystemConfig{}
return o.PwdExpirationConfig.validate()
default: default:
o.HTTPConfig = EventActionHTTPConfig{} o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{} o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{} o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{} o.RetentionConfig = EventActionDataRetentionConfig{}
o.FsConfig = EventActionFilesystemConfig{} o.FsConfig = EventActionFilesystemConfig{}
o.PwdExpirationConfig = EventActionPasswordExpiration{}
} }
return nil return nil
} }
@ -1322,7 +1356,7 @@ func (r *EventRule) validate() error {
func (r *EventRule) checkIPBlockedAndCertificateActions() error { func (r *EventRule) checkIPBlockedAndCertificateActions() error {
unavailableActions := []int{ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset, unavailableActions := []int{ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem} ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck}
for _, action := range r.Actions { for _, action := range r.Actions {
if util.Contains(unavailableActions, action.Type) { if util.Contains(unavailableActions, action.Type) {
return fmt.Errorf("action %q, type %q is not supported for event trigger %q", return fmt.Errorf("action %q, type %q is not supported for event trigger %q",
@ -1337,7 +1371,7 @@ func (r *EventRule) checkProviderEventActions(providerObjectType string) error {
// can be executed only if we modify a user. They will be executed for the // 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. // affected user. Folder quota reset can be executed only for folders.
userSpecificActions := []int{ActionTypeUserQuotaReset, ActionTypeTransferQuotaReset, userSpecificActions := []int{ActionTypeUserQuotaReset, ActionTypeTransferQuotaReset,
ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem} ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck}
for _, action := range r.Actions { for _, action := range r.Actions {
if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser { if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
return fmt.Errorf("action %q, type %q is only supported for provider user events", return fmt.Errorf("action %q, type %q is only supported for provider user events",

View file

@ -1110,6 +1110,19 @@ func (u *User) CanDeleteFromWeb(target string) bool {
return u.HasAnyPerm(permsDeleteAny, target) return u.HasAnyPerm(permsDeleteAny, target)
} }
// PasswordExpiresIn returns the number of days before the password expires.
// The returned value is negative if the password is expired.
// The caller must ensure that a PasswordExpiration is set
func (u *User) PasswordExpiresIn() int {
lastPwdChange := util.GetTimeFromMsecSinceEpoch(u.LastPasswordChange)
pwdExpiration := lastPwdChange.Add(time.Duration(u.Filters.PasswordExpiration) * 24 * time.Hour)
res := int(math.Round(float64(time.Until(pwdExpiration)) / float64(24*time.Hour)))
if res == 0 && pwdExpiration.After(time.Now()) {
res = 1
}
return res
}
// MustChangePassword returns true if the user must change the password // MustChangePassword returns true if the user must change the password
func (u *User) MustChangePassword() bool { func (u *User) MustChangePassword() bool {
if u.Filters.RequirePasswordChange { if u.Filters.RequirePasswordChange {

View file

@ -1903,6 +1903,11 @@ func TestEventActionValidation(t *testing.T) {
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest) _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, string(resp), "invalid path to compress") assert.Contains(t, string(resp), "invalid path to compress")
action.Type = dataprovider.ActionTypePasswordExpirationCheck
action.Options.PwdExpirationConfig.Threshold = 0
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "threshold must be greater than 0")
} }
func TestEventRuleValidation(t *testing.T) { func TestEventRuleValidation(t *testing.T) {
@ -20011,6 +20016,7 @@ func TestWebEventAction(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "invalid http timeout") assert.Contains(t, rr.Body.String(), "invalid http timeout")
form.Set("cmd_timeout", "20") form.Set("cmd_timeout", "20")
form.Set("pwd_expiration_threshold", "10")
form.Set("http_timeout", fmt.Sprintf("%d", action.Options.HTTPConfig.Timeout)) form.Set("http_timeout", fmt.Sprintf("%d", action.Options.HTTPConfig.Timeout))
form.Set("http_header_key0", action.Options.HTTPConfig.Headers[0].Key) form.Set("http_header_key0", action.Options.HTTPConfig.Headers[0].Key)
form.Set("http_header_val0", action.Options.HTTPConfig.Headers[0].Value) form.Set("http_header_val0", action.Options.HTTPConfig.Headers[0].Value)
@ -20205,6 +20211,7 @@ func TestWebEventAction(t *testing.T) {
assert.Equal(t, action.Options.CmdConfig.Timeout, actionGet.Options.CmdConfig.Timeout) assert.Equal(t, action.Options.CmdConfig.Timeout, actionGet.Options.CmdConfig.Timeout)
assert.Equal(t, action.Options.CmdConfig.EnvVars, actionGet.Options.CmdConfig.EnvVars) assert.Equal(t, action.Options.CmdConfig.EnvVars, actionGet.Options.CmdConfig.EnvVars)
assert.Equal(t, dataprovider.EventActionHTTPConfig{}, actionGet.Options.HTTPConfig) assert.Equal(t, dataprovider.EventActionHTTPConfig{}, actionGet.Options.HTTPConfig)
assert.Equal(t, dataprovider.EventActionPasswordExpiration{}, actionGet.Options.PwdExpirationConfig)
// change action type again // change action type again
action.Type = dataprovider.ActionTypeEmail action.Type = dataprovider.ActionTypeEmail
action.Options.EmailConfig = dataprovider.EventActionEmailConfig{ action.Options.EmailConfig = dataprovider.EventActionEmailConfig{
@ -20352,6 +20359,33 @@ func TestWebEventAction(t *testing.T) {
} }
} }
action.Type = dataprovider.ActionTypePasswordExpirationCheck
action.Options.PwdExpirationConfig.Threshold = 15
form.Set("type", fmt.Sprintf("%d", action.Type))
form.Set("pwd_expiration_threshold", "a")
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 password expiration threshold")
form.Set("pwd_expiration_threshold", strconv.Itoa(action.Options.PwdExpirationConfig.Threshold))
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)
actionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, action.Type, actionGet.Type)
assert.Equal(t, action.Options.PwdExpirationConfig.Threshold, actionGet.Options.PwdExpirationConfig.Threshold)
assert.Equal(t, 0, actionGet.Options.CmdConfig.Timeout)
assert.Len(t, actionGet.Options.CmdConfig.EnvVars, 0)
req, err = http.NewRequest(http.MethodDelete, path.Join(webAdminEventActionPath, action.Name), nil) req, err = http.NewRequest(http.MethodDelete, path.Join(webAdminEventActionPath, action.Name), nil)
assert.NoError(t, err) assert.NoError(t, err)
setBearerForReq(req, apiToken) setBearerForReq(req, apiToken)
@ -22212,6 +22246,23 @@ func TestPasswordChangeRequired(t *testing.T) {
assert.True(t, user.MustChangePassword()) assert.True(t, user.MustChangePassword())
} }
func TestPasswordExpiresIn(t *testing.T) {
user := getTestUser()
user.Filters.PasswordExpiration = 30
user.LastPasswordChange = util.GetTimeAsMsSinceEpoch(time.Now().Add(-15*24*time.Hour + 1*time.Hour))
res := user.PasswordExpiresIn()
assert.Equal(t, 15, res)
user.Filters.PasswordExpiration = 15
res = user.PasswordExpiresIn()
assert.Equal(t, 1, res)
user.LastPasswordChange = util.GetTimeAsMsSinceEpoch(time.Now().Add(-15*24*time.Hour - 1*time.Hour))
res = user.PasswordExpiresIn()
assert.Equal(t, 0, res)
user.Filters.PasswordExpiration = 5
res = user.PasswordExpiresIn()
assert.Equal(t, -10, res)
}
func TestSecondFactorRequirements(t *testing.T) { func TestSecondFactorRequirements(t *testing.T) {
user := getTestUser() user := getTestUser()
user.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP, common.ProtocolSSH} user.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP, common.ProtocolSSH}

View file

@ -1012,7 +1012,7 @@ func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Reque
currentURL = webAdminEventActionPath currentURL = webAdminEventActionPath
case genericPageModeUpdate: case genericPageModeUpdate:
title = "Update event action" title = "Update event action"
currentURL = fmt.Sprintf("%v/%v", webAdminEventActionPath, url.PathEscape(action.Name)) currentURL = fmt.Sprintf("%s/%s", webAdminEventActionPath, url.PathEscape(action.Name))
} }
if action.Options.HTTPConfig.Timeout == 0 { if action.Options.HTTPConfig.Timeout == 0 {
action.Options.HTTPConfig.Timeout = 20 action.Options.HTTPConfig.Timeout = 20
@ -1020,6 +1020,9 @@ func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Reque
if action.Options.CmdConfig.Timeout == 0 { if action.Options.CmdConfig.Timeout == 0 {
action.Options.CmdConfig.Timeout = 20 action.Options.CmdConfig.Timeout = 20
} }
if action.Options.PwdExpirationConfig.Threshold == 0 {
action.Options.PwdExpirationConfig.Threshold = 10
}
data := eventActionPage{ data := eventActionPage{
basePage: s.getBasePageData(title, currentURL, r), basePage: s.getBasePageData(title, currentURL, r),
@ -2122,6 +2125,10 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
if err != nil { if err != nil {
return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid fs action type: %w", err) return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid fs action type: %w", err)
} }
pwdExpirationThreshold, err := strconv.Atoi(r.Form.Get("pwd_expiration_threshold"))
if err != nil {
return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid password expiration threshold: %w", err)
}
var emailAttachments []string var emailAttachments []string
if r.Form.Get("email_attachments") != "" { if r.Form.Get("email_attachments") != "" {
emailAttachments = strings.Split(strings.ReplaceAll(r.Form.Get("email_attachments"), " ", ""), ",") emailAttachments = strings.Split(strings.ReplaceAll(r.Form.Get("email_attachments"), " ", ""), ",")
@ -2169,6 +2176,9 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
Paths: strings.Split(strings.ReplaceAll(r.Form.Get("fs_compress_paths"), " ", ""), ","), Paths: strings.Split(strings.ReplaceAll(r.Form.Get("fs_compress_paths"), " ", ""), ","),
}, },
}, },
PwdExpirationConfig: dataprovider.EventActionPasswordExpiration{
Threshold: pwdExpirationThreshold,
},
} }
return options, nil return options, nil
} }

View file

@ -1450,6 +1450,9 @@ func checkEventAction(expected, actual dataprovider.BaseEventAction) error {
if expected.Type != actual.Type { if expected.Type != actual.Type {
return errors.New("type mismatch") return errors.New("type mismatch")
} }
if expected.Options.PwdExpirationConfig.Threshold != actual.Options.PwdExpirationConfig.Threshold {
return errors.New("password expiration threshold mismatch")
}
if err := compareEventActionCmdConfigFields(expected.Options.CmdConfig, actual.Options.CmdConfig); err != nil { if err := compareEventActionCmdConfigFields(expected.Options.CmdConfig, actual.Options.CmdConfig); err != nil {
return err return err
} }

View file

@ -45,6 +45,7 @@ const (
const ( const (
templateEmailDir = "email" templateEmailDir = "email"
templatePasswordReset = "reset-password.html" templatePasswordReset = "reset-password.html"
templatePasswordExpiration = "password-expiration.html"
) )
var ( var (
@ -158,8 +159,11 @@ func loadTemplates(templatesPath string) {
passwordResetPath := filepath.Join(templatesPath, templatePasswordReset) passwordResetPath := filepath.Join(templatesPath, templatePasswordReset)
pwdResetTmpl := util.LoadTemplate(nil, passwordResetPath) pwdResetTmpl := util.LoadTemplate(nil, passwordResetPath)
passwordExpirationPath := filepath.Join(templatesPath, templatePasswordExpiration)
pwdExpirationTmpl := util.LoadTemplate(nil, passwordExpirationPath)
emailTemplates[templatePasswordReset] = pwdResetTmpl emailTemplates[templatePasswordReset] = pwdResetTmpl
emailTemplates[templatePasswordExpiration] = pwdExpirationTmpl
} }
// RenderPasswordResetTemplate executes the password reset template // RenderPasswordResetTemplate executes the password reset template
@ -170,11 +174,22 @@ func RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error {
return emailTemplates[templatePasswordReset].Execute(buf, data) return emailTemplates[templatePasswordReset].Execute(buf, data)
} }
// RenderPasswordExpirationTemplate executes the password expiration template
func RenderPasswordExpirationTemplate(buf *bytes.Buffer, data any) error {
if smtpServer == nil {
return errors.New("smtp: not configured")
}
return emailTemplates[templatePasswordExpiration].Execute(buf, data)
}
// SendEmail tries to send an email using the specified parameters. // SendEmail tries to send an email using the specified parameters.
func SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...mail.File) error { func SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...mail.File) error {
if smtpServer == nil { if smtpServer == nil {
return errors.New("smtp: not configured") return errors.New("smtp: not configured")
} }
if len(to) == 0 {
return errors.New("smtp: cannot send an email without recipients")
}
smtpClient, err := smtpServer.Connect() smtpClient, err := smtpServer.Connect()
if err != nil { if err != nil {
return fmt.Errorf("smtp: unable to connect: %w", err) return fmt.Errorf("smtp: unable to connect: %w", err)

View file

@ -6463,6 +6463,12 @@ components:
type: string type: string
compress: compress:
$ref: '#/components/schemas/EventActionFsCompress' $ref: '#/components/schemas/EventActionFsCompress'
EventActionPasswordExpiration:
type: object
properties:
threshold:
type: integer
description: 'An email notification will be generated for users whose password expires in a number of days less than or equal to this threshold'
BaseEventActionOptions: BaseEventActionOptions:
type: object type: object
properties: properties:
@ -6476,6 +6482,8 @@ components:
$ref: '#/components/schemas/EventActionDataRetentionConfig' $ref: '#/components/schemas/EventActionDataRetentionConfig'
fs_config: fs_config:
$ref: '#/components/schemas/EventActionFilesystemConfig' $ref: '#/components/schemas/EventActionFilesystemConfig'
pwd_expiration_config:
$ref: '#/components/schemas/EventActionPasswordExpiration'
BaseEventAction: BaseEventAction:
type: object type: object
properties: properties:

View file

@ -0,0 +1,19 @@
<!--
Copyright (C) 2019-2022 Nicola Murino
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
Hi {{.Username}},
<br>
<p>Your SFTPGo password {{if le .Days 0}}has expired{{else}}expires in {{.Days}} {{if eq .Days 1}}day{{else}}days{{end}}{{end}}.</p>
<p>Please login to the WebClient and set a new password.</p>

View file

@ -74,6 +74,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
</div> </div>
<div class="form-group row action-type action-pwd-expiration">
<label for="idPwdExpirationThreshold" class="col-sm-2 col-form-label">Threshold</label>
<div class="col-sm-10">
<input type="number" min="1" class="form-control" id="idPwdExpirationThreshold" name="pwd_expiration_threshold" placeholder=""
aria-describedby="PwdExpirationThresholdHelpBlock" value="{{.Action.Options.PwdExpirationConfig.Threshold}}">
<small id="PwdExpirationThresholdHelpBlock" class="form-text text-muted">
An email notification will be generated for users whose password expires in a number of days less than or equal to this threshold.
</small>
</div>
</div>
<div class="form-group row action-type action-http"> <div class="form-group row action-type action-http">
<label for="idHTTPEndpoint" class="col-sm-2 col-form-label">Endpoint</label> <label for="idHTTPEndpoint" class="col-sm-2 col-form-label">Endpoint</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -916,26 +927,24 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
$('.action-type').hide(); $('.action-type').hide();
switch (val) { switch (val) {
case '1': case '1':
case 1:
$('.action-http').show(); $('.action-http').show();
break; break;
case '2': case '2':
case 2:
$('.action-cmd').show(); $('.action-cmd').show();
break; break;
case '3': case '3':
case 3:
$('.action-smtp').show(); $('.action-smtp').show();
break; break;
case '8': case '8':
case 8:
$('.action-dataretention').show(); $('.action-dataretention').show();
break; break;
case '9': case '9':
case 9:
$('.action-fs').show(); $('.action-fs').show();
onFsActionChanged($("#idFsActionType").val()); onFsActionChanged($("#idFsActionType").val());
break; break;
case '11':
$('.action-pwd-expiration').show();
break;
} }
} }
@ -943,23 +952,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
$('.action-fs-type').hide(); $('.action-fs-type').hide();
switch (val) { switch (val) {
case '1': case '1':
case 1:
$('.action-fs-rename').show(); $('.action-fs-rename').show();
break; break;
case '2': case '2':
case 2:
$('.action-fs-delete').show(); $('.action-fs-delete').show();
break; break;
case '3': case '3':
case 3:
$('.action-fs-mkdir').show(); $('.action-fs-mkdir').show();
break; break;
case '4': case '4':
case 4:
$('.action-fs-exist').show(); $('.action-fs-exist').show();
break; break;
case '5': case '5':
case 5:
$('.action-fs-compress').show(); $('.action-fs-compress').show();
break; break;
} }