diff --git a/docs/eventmanager.md b/docs/eventmanager.md
index 1ac971db..f6c3ebae 100644
--- a/docs/eventmanager.md
+++ b/docs/eventmanager.md
@@ -13,6 +13,7 @@ The following actions are supported:
- `Transfer quota reset`. The transfer quota values will be reset to `0`.
- `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.
+- `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:
- `Rename`. You can rename one or more files or directories.
- `Delete`. You can delete one or more files and directories.
diff --git a/go.mod b/go.mod
index 4dc4424e..4f3a94da 100644
--- a/go.mod
+++ b/go.mod
@@ -8,15 +8,15 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.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.17.2
- github.com/aws/aws-sdk-go-v2/config v1.18.4
- github.com/aws/aws-sdk-go-v2/credentials v1.13.4
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.20
- github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.43
- github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.25
- github.com/aws/aws-sdk-go-v2/service/s3 v1.29.5
- github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.9
- github.com/aws/aws-sdk-go-v2/service/sts v1.17.6
+ github.com/aws/aws-sdk-go-v2 v1.17.3
+ github.com/aws/aws-sdk-go-v2/config v1.18.5
+ github.com/aws/aws-sdk-go-v2/credentials v1.13.5
+ 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.44
+ github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.26
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6
+ github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.10
+ github.com/aws/aws-sdk-go-v2/service/sts v1.17.7
github.com/cockroachdb/cockroach-go/v2 v2.2.19
github.com/coreos/go-oidc/v3 v3.4.0
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b
@@ -45,7 +45,7 @@ require (
github.com/otiai10/copy v1.9.0
github.com/pires/go-proxyproto v0.6.2
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/robfig/cron/v3 v3.0.1
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/term 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
)
require (
cloud.google.com/go v0.107.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/iam v0.8.0 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 // indirect
+ cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ cloud.google.com/go/iam v0.9.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 // 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/internal/configsources v1.1.26 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.20 // indirect
- github.com/aws/aws-sdk-go-v2/internal/ini v1.3.27 // indirect
- github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.17 // 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.21 // 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.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/checksum v1.1.21 // 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/s3shared v1.13.20 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.11.26 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.9 // 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.21 // 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.27 // 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/beorn7/perks 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/httprc v1.0.4 // 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/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // 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/v2 v2.0.6 // 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/common v0.38.0 // indirect
+ github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
diff --git a/go.sum b/go.sum
index 6d19df52..8a9cfdf9 100644
--- a/go.sum
+++ b/go.sum
@@ -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.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
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.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+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.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.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.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.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE=
+cloud.google.com/go/iam v0.9.0 h1:bK6Or6mxhuL8lnj1i9j0yMo2wE/IeTO2cWlfUrf/TZs=
+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.6.0 h1:OWRZzrPmOZUzurjI2FBGtgY2mB1WaJkqhw6oIwSj0Yg=
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/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.1.1 h1:Oj853U9kG+RLTCQXpjvOnrv0WaZHxgmZz1TlLywgOPY=
-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 h1:+5VZ72z0Qan5Bog5C+ZkgSqUbeVUd9wgtHOrIKuc5b8=
+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/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=
@@ -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-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.17.2 h1:r0yRZInwiPBNpQ4aDy/Ssh3ROWsGtKDwar2JS8Lm+N8=
-github.com/aws/aws-sdk-go-v2 v1.17.2/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
+github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY=
+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.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/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.4/go.mod h1:EZxMPLSdGAZ3eAmkqXfYbRppZJTzFTkv8VyEzJhKko4=
+github.com/aws/aws-sdk-go-v2/config v1.18.5 h1:teGdDCAT3gX99FIKNt6HsvLaeOVdCFiCQDlH8UV6Xvg=
+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.13.4 h1:nEbHIyJy7mCvQ/kzGG7VWHSBpRB4H6sJy3bWierWUtg=
-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 h1:vrPwnKCdQlUyxXDZtPpb6Hc3GbTndqaGtEOwm/lF5tI=
+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.20 h1:tpNOglTZ8kg9T38NpcGBxudqfUAwUzyUnLQ4XSd0CHE=
-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 h1:j9wi1kQ8b+e0FBVHxCqCGo4kxDU175hoDHcWAi0sauU=
+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.43 h1:+bkAMTd5OGyHu2nwNOangjEsP65fR0uhMbZJA52sZ64=
-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 h1:BXBHQeRh4fGFiiMZZd/WI/aAPwjhUCrWDhRDbuOZPIM=
+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.26 h1:5WU31cY7m0tG+AiaXuXGoMzo2GBQ1IixtWa8Yywsgco=
-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 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU=
+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.20 h1:WW0qSzDWoiWU2FS5DbKpxGilFVlCEJPwx4YtjdfI0Jw=
-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 h1:5NbbMrIzmUn/TXFqAle6mgrH5m9cOvMLRGL7pnG8tRE=
+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.27 h1:N2eKFw2S+JWRCtTt0IhIX7uoGGQciD4p6ba+SJv4WEU=
-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 h1:KeTxcGdNnQudb46oOl4d90f2I33DF/c6q3RnZAmvQdQ=
+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.17 h1:5tXbMJ7Jq0iG65oiMg6tCLsHkSaO2xLXa2EmZ29vaTA=
-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 h1:H/mF2LNWwX00lD6FlYfKpLLZgUW7oIzCBkig78x4Xok=
+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.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/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.21/go.mod h1:sPOz31BVdqeeurKEuUpLNSve4tdCNPluE+070HNcEHI=
+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.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.20 h1:jlgyHbkZQAgAc7VIxJDmtouH8eNjOk2REVAQfVhdaiQ=
-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 h1:5C6XgTViSb0bunmU57b3CT+MhxULqHH2721FVA+/kDM=
+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.20 h1:4K6dbmR0mlp3o4Bo78PnpvzHtYAqEeVMguvEenpMGsI=
-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 h1:vY5siRXvW5TrOKm2qKEf9tliBfdLxdfy0i02LOcmqUo=
+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/marketplacemetering v1.13.25 h1:rCfUDMT9KO++W4TkIJr3BO0h749xkRX4RpHdydSFfHw=
-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 h1:NlA5om7Um+ohI/S0inBd55Vsp84BKhBrAgw2IVqEb30=
+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.29.5 h1:nRSEQj1JergKTVc8RGkhZvOEGgcvo4fWpDPwGDeg2ok=
-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 h1:W8pLcSn6Uy0eXgDBUUl8M8Kxv7JCoP68ZKTD04OXLEA=
+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.16.9 h1:ogcakjF/mrZOo9oJVWmRbG838C04oWGXI8T8IY4xcfM=
-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 h1:6obimjQAiRlEUZT7a2Q1ikH7ck4cPO3phGz4wqI5f2w=
+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/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/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.26/go.mod h1:uB9tV79ULEZUXc6Ob18A46KSQ0JDlrplPni9XW6Ot60=
-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.9/go.mod h1:2E/3D/mB8/r2J7nK42daoKP/ooCwbf0q1PznNc+DZTU=
+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.27/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A=
+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.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.17.6 h1:VQFOLQVL3BrKM/NLO/7FiS4vcp5bqK0mGMyk09xLoAY=
-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 h1:9Mtq1KM6nD8/+HStvWcvYnixJ5N85DX+P+OY3kI3W2k=
+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.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
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/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/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.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.1.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.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-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
-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 h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
+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/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
-github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
+github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
+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/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
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.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE=
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.38.0/go.mod h1:MBXfmBQZrK5XpbCkjofnXs96LD2QQ7fEq4C0xjC/yec=
+github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
+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.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI=
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.90.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.104.0/go.mod h1:JCspTXJbBxa5ySXw4UgUqVer7DfVxbvc/CTUFqAED5U=
+google.golang.org/api v0.105.0 h1:t6P9Jj+6XTn4U9I2wycQai6Q/Kz7iOT+QzjJ3G2V4x8=
+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.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go
index d35d48f1..5d0bef0f 100644
--- a/internal/common/eventmanager.go
+++ b/internal/common/eventmanager.go
@@ -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 {
- 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)
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)
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 {
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)
@@ -1685,7 +1685,7 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions,
}
}
executed++
- if err = executeQuotaResetForUser(user); err != nil {
+ if err = executeQuotaResetForUser(&user); err != nil {
params.AddError(err)
failedResets = append(failedResets, user.Username)
continue
@@ -1789,7 +1789,7 @@ func executeDataRetentionCheckForUser(user dataprovider.User, folders []dataprov
params *EventParams, actionName string,
) error {
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)
return err
}
@@ -1850,9 +1850,9 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete
return nil
}
-func executeMetadataCheckForUser(user dataprovider.User) error {
+func executeMetadataCheckForUser(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",
+ eventManagerLog(logger.LevelError, "skipping scheduled quota reset for user %s, cannot apply group settings: %v",
user.Username, err)
return err
}
@@ -1886,7 +1886,7 @@ func executeMetadataCheckRuleAction(conditions dataprovider.ConditionOptions, pa
}
}
executed++
- if err = executeMetadataCheckForUser(user); err != nil {
+ if err = executeMetadataCheckForUser(&user); err != nil {
params.AddError(err)
failures = append(failures, user.Username)
continue
@@ -1902,6 +1902,72 @@ func executeMetadataCheckRuleAction(conditions dataprovider.ConditionOptions, pa
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,
conditions dataprovider.ConditionOptions,
) error {
@@ -1932,6 +1998,8 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
err = executeMetadataCheckRuleAction(conditions, params)
case dataprovider.ActionTypeFilesystem:
err = executeFsRuleAction(action.Options.FsConfig, conditions, params)
+ case dataprovider.ActionTypePasswordExpirationCheck:
+ err = executePwdExpirationCheckRuleAction(action.Options.PwdExpirationConfig, conditions, params)
default:
err = fmt.Errorf("unsupported action type: %d", action.Type)
}
diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go
index 4c314a24..78c38aa6 100644
--- a/internal/common/eventmanager_test.go
+++ b/internal/common/eventmanager_test.go
@@ -408,9 +408,12 @@ func TestEventManagerErrors(t *testing.T) {
assert.Error(t, err)
err = executeCompressFsRuleAction(dataprovider.EventActionFsCompress{}, nil, dataprovider.ConditionOptions{}, &EventParams{})
assert.Error(t, err)
+ err = executePwdExpirationCheckRuleAction(dataprovider.EventActionPasswordExpiration{},
+ dataprovider.ConditionOptions{}, &EventParams{})
+ assert.Error(t, err)
groupName := "agroup"
- err = executeQuotaResetForUser(dataprovider.User{
+ err = executeQuotaResetForUser(&dataprovider.User{
Groups: []sdk.GroupMapping{
{
Name: groupName,
@@ -419,7 +422,7 @@ func TestEventManagerErrors(t *testing.T) {
},
})
assert.Error(t, err)
- err = executeMetadataCheckForUser(dataprovider.User{
+ err = executeMetadataCheckForUser(&dataprovider.User{
Groups: []sdk.GroupMapping{
{
Name: groupName,
@@ -490,6 +493,14 @@ func TestEventManagerErrors(t *testing.T) {
},
}}, []string{"/a", "/b"}, nil)
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{
Method: http.MethodPost,
@@ -687,11 +698,24 @@ func TestEventRuleActions(t *testing.T) {
},
},
}
+ user2.Filters.PasswordExpiration = 10
err = dataprovider.AddUser(&user1, "", "", "")
assert.NoError(t, err)
err = dataprovider.AddUser(&user2, "", "", "")
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{
Type: dataprovider.ActionTypeUserQuotaReset,
}
diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go
index d98b0988..d015a729 100644
--- a/internal/common/protocol_test.go
+++ b/internal/common/protocol_test.go
@@ -5690,6 +5690,209 @@ func TestEventRuleIPBlocked(t *testing.T) {
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) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
diff --git a/internal/dataprovider/eventrule.go b/internal/dataprovider/eventrule.go
index 0b8370bd..04f19f5c 100644
--- a/internal/dataprovider/eventrule.go
+++ b/internal/dataprovider/eventrule.go
@@ -45,12 +45,13 @@ const (
ActionTypeDataRetentionCheck
ActionTypeFilesystem
ActionTypeMetadataCheck
+ ActionTypePasswordExpirationCheck
)
var (
supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
- ActionTypeDataRetentionCheck, ActionTypeMetadataCheck}
+ ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypePasswordExpirationCheck}
)
func isActionTypeValid(action int) bool {
@@ -77,6 +78,8 @@ func getActionTypeAsString(action int) string {
return "Metadata check"
case ActionTypeFilesystem:
return "Filesystem"
+ case ActionTypePasswordExpirationCheck:
+ return "Password expiration check"
default:
return "Command"
}
@@ -755,13 +758,28 @@ 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
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"`
+ 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"`
+ PwdExpirationConfig EventActionPasswordExpiration `json:"pwd_expiration_config"`
}
func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
@@ -819,6 +837,9 @@ func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
RetentionConfig: EventActionDataRetentionConfig{
Folders: folders,
},
+ PwdExpirationConfig: EventActionPasswordExpiration{
+ Threshold: o.PwdExpirationConfig.Threshold,
+ },
FsConfig: o.FsConfig.getACopy(),
}
}
@@ -850,37 +871,50 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{}
o.FsConfig = EventActionFilesystemConfig{}
+ o.PwdExpirationConfig = EventActionPasswordExpiration{}
return o.HTTPConfig.validate(name)
case ActionTypeCommand:
o.HTTPConfig = EventActionHTTPConfig{}
o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{}
o.FsConfig = EventActionFilesystemConfig{}
+ o.PwdExpirationConfig = EventActionPasswordExpiration{}
return o.CmdConfig.validate()
case ActionTypeEmail:
o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{}
o.FsConfig = EventActionFilesystemConfig{}
+ o.PwdExpirationConfig = EventActionPasswordExpiration{}
return o.EmailConfig.validate()
case ActionTypeDataRetentionCheck:
o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{}
o.FsConfig = EventActionFilesystemConfig{}
+ o.PwdExpirationConfig = EventActionPasswordExpiration{}
return o.RetentionConfig.validate()
case ActionTypeFilesystem:
o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{}
+ o.PwdExpirationConfig = EventActionPasswordExpiration{}
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:
o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{}
o.RetentionConfig = EventActionDataRetentionConfig{}
o.FsConfig = EventActionFilesystemConfig{}
+ o.PwdExpirationConfig = EventActionPasswordExpiration{}
}
return nil
}
@@ -1322,7 +1356,7 @@ func (r *EventRule) validate() error {
func (r *EventRule) checkIPBlockedAndCertificateActions() error {
unavailableActions := []int{ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
- ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem}
+ ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck}
for _, action := range r.Actions {
if util.Contains(unavailableActions, action.Type) {
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
// affected user. Folder quota reset can be executed only for folders.
userSpecificActions := []int{ActionTypeUserQuotaReset, ActionTypeTransferQuotaReset,
- ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem}
+ ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck}
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",
diff --git a/internal/dataprovider/user.go b/internal/dataprovider/user.go
index ca87e187..afa5627f 100644
--- a/internal/dataprovider/user.go
+++ b/internal/dataprovider/user.go
@@ -1110,6 +1110,19 @@ func (u *User) CanDeleteFromWeb(target string) bool {
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
func (u *User) MustChangePassword() bool {
if u.Filters.RequirePasswordChange {
diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go
index 173ef1f8..94b3516e 100644
--- a/internal/httpd/httpd_test.go
+++ b/internal/httpd/httpd_test.go
@@ -1903,6 +1903,11 @@ func TestEventActionValidation(t *testing.T) {
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
assert.NoError(t, err)
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) {
@@ -20011,6 +20016,7 @@ func TestWebEventAction(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "invalid http timeout")
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_header_key0", action.Options.HTTPConfig.Headers[0].Key)
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.EnvVars, actionGet.Options.CmdConfig.EnvVars)
assert.Equal(t, dataprovider.EventActionHTTPConfig{}, actionGet.Options.HTTPConfig)
+ assert.Equal(t, dataprovider.EventActionPasswordExpiration{}, actionGet.Options.PwdExpirationConfig)
// change action type again
action.Type = dataprovider.ActionTypeEmail
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)
assert.NoError(t, err)
setBearerForReq(req, apiToken)
@@ -22212,6 +22246,23 @@ func TestPasswordChangeRequired(t *testing.T) {
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) {
user := getTestUser()
user.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP, common.ProtocolSSH}
diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go
index da25104f..933096db 100644
--- a/internal/httpd/webadmin.go
+++ b/internal/httpd/webadmin.go
@@ -1012,7 +1012,7 @@ func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Reque
currentURL = webAdminEventActionPath
case genericPageModeUpdate:
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 {
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 {
action.Options.CmdConfig.Timeout = 20
}
+ if action.Options.PwdExpirationConfig.Threshold == 0 {
+ action.Options.PwdExpirationConfig.Threshold = 10
+ }
data := eventActionPage{
basePage: s.getBasePageData(title, currentURL, r),
@@ -2122,6 +2125,10 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
if err != nil {
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
if 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"), " ", ""), ","),
},
},
+ PwdExpirationConfig: dataprovider.EventActionPasswordExpiration{
+ Threshold: pwdExpirationThreshold,
+ },
}
return options, nil
}
diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go
index 5a811c0d..a14196d1 100644
--- a/internal/httpdtest/httpdtest.go
+++ b/internal/httpdtest/httpdtest.go
@@ -1450,6 +1450,9 @@ func checkEventAction(expected, actual dataprovider.BaseEventAction) error {
if expected.Type != actual.Type {
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 {
return err
}
diff --git a/internal/smtp/smtp.go b/internal/smtp/smtp.go
index 61bef9f5..37926753 100644
--- a/internal/smtp/smtp.go
+++ b/internal/smtp/smtp.go
@@ -43,8 +43,9 @@ const (
)
const (
- templateEmailDir = "email"
- templatePasswordReset = "reset-password.html"
+ templateEmailDir = "email"
+ templatePasswordReset = "reset-password.html"
+ templatePasswordExpiration = "password-expiration.html"
)
var (
@@ -158,8 +159,11 @@ func loadTemplates(templatesPath string) {
passwordResetPath := filepath.Join(templatesPath, templatePasswordReset)
pwdResetTmpl := util.LoadTemplate(nil, passwordResetPath)
+ passwordExpirationPath := filepath.Join(templatesPath, templatePasswordExpiration)
+ pwdExpirationTmpl := util.LoadTemplate(nil, passwordExpirationPath)
emailTemplates[templatePasswordReset] = pwdResetTmpl
+ emailTemplates[templatePasswordExpiration] = pwdExpirationTmpl
}
// RenderPasswordResetTemplate executes the password reset template
@@ -170,11 +174,22 @@ func RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error {
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.
func SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...mail.File) error {
if smtpServer == nil {
return errors.New("smtp: not configured")
}
+ if len(to) == 0 {
+ return errors.New("smtp: cannot send an email without recipients")
+ }
smtpClient, err := smtpServer.Connect()
if err != nil {
return fmt.Errorf("smtp: unable to connect: %w", err)
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index fb116223..0984c186 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -6463,6 +6463,12 @@ components:
type: string
compress:
$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:
type: object
properties:
@@ -6476,6 +6482,8 @@ components:
$ref: '#/components/schemas/EventActionDataRetentionConfig'
fs_config:
$ref: '#/components/schemas/EventActionFilesystemConfig'
+ pwd_expiration_config:
+ $ref: '#/components/schemas/EventActionPasswordExpiration'
BaseEventAction:
type: object
properties:
diff --git a/templates/email/password-expiration.html b/templates/email/password-expiration.html
new file mode 100644
index 00000000..08a1d4f9
--- /dev/null
+++ b/templates/email/password-expiration.html
@@ -0,0 +1,19 @@
+
+Hi {{.Username}},
+
+
Your SFTPGo password {{if le .Days 0}}has expired{{else}}expires in {{.Days}} {{if eq .Days 1}}day{{else}}days{{end}}{{end}}.
+Please login to the WebClient and set a new password.
\ No newline at end of file diff --git a/templates/webadmin/eventaction.html b/templates/webadmin/eventaction.html index 7aa36453..1ca9bf0b 100644 --- a/templates/webadmin/eventaction.html +++ b/templates/webadmin/eventaction.html @@ -74,6 +74,17 @@ along with this program. If not, see