From 78cd5d8ebae71c14250bf5c81b9fa75e579fa232 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 13 Feb 2023 19:32:36 +0100 Subject: [PATCH] groups: add expiration date override Signed-off-by: Nicola Murino --- docs/groups.md | 2 ++ go.mod | 4 ++-- go.sum | 12 ++++-------- internal/common/eventmanager.go | 6 +++++- internal/dataprovider/group.go | 1 + internal/dataprovider/user.go | 3 +++ internal/httpd/auth_utils.go | 1 - internal/httpd/httpd_test.go | 16 ++++++++++++++++ internal/httpd/webadmin.go | 5 +++++ internal/httpdtest/httpdtest.go | 4 +++- openapi/openapi.yaml | 3 +++ templates/webadmin/eventaction.html | 2 +- templates/webadmin/group.html | 11 +++++++++++ 13 files changed, 56 insertions(+), 14 deletions(-) diff --git a/docs/groups.md b/docs/groups.md index e45ea60c..6420f872 100644 --- a/docs/groups.md +++ b/docs/groups.md @@ -6,6 +6,7 @@ SFTPGo supports two types of groups: - primary groups - secondary groups +- membership groups A user can be a member of a primary group and many secondary and membership groups. Depending on the group type, the settings are inherited differently. @@ -16,6 +17,7 @@ The following settings are inherited from the primary group: - home dir, if set for the group will replace the one defined for the user. The `%username%` placeholder is replaced with the username - filesystem config, if the provider set for the group is different from the "local provider" will replace the one defined for the user. The `%username%` placeholder is replaced with the username within the defined "prefix", for any vfs, and the "username" for the SFTP filesystem config - max sessions, quota size/files, upload/download bandwidth, upload/download/total data transfer, max upload size, external auth cache time, ftp_security, default share expiration, password expiration: if they are set to `0` for the user they are replaced with the value set for the group, if different from `0` +- expires_in, if defined and the user does not have an expiration date set, defines the expiration of the account in number of days from the creation date - TLS username, check password hook disabled, pre-login hook disabled, external auth hook disabled, filesystem checks disabled, allow API key authentication, anonymous user: if they are not set for the user they are replaced with the value set for the group - starting directory, if the user does not have a starting directory set, the value set for the group is used, if any. The `%username%` placeholder is replaced with the username diff --git a/go.mod b/go.mod index 92edf5c0..31ff1889 100644 --- a/go.mod +++ b/go.mod @@ -45,14 +45,14 @@ require ( github.com/minio/sio v0.3.0 github.com/otiai10/copy v1.9.0 github.com/pires/go-proxyproto v0.6.2 - github.com/pkg/sftp v1.13.6-0.20230104082718-2489717da0f3 + github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6 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 github.com/rs/xid v1.4.0 github.com/rs/zerolog v1.29.0 - github.com/sftpgo/sdk v0.1.3-0.20230213120720-de3129520736 + github.com/sftpgo/sdk v0.1.3-0.20230213182959-2d89540f8810 github.com/shirou/gopsutil/v3 v3.23.1 github.com/spf13/afero v1.9.3 github.com/spf13/cobra v1.6.1 diff --git a/go.sum b/go.sum index ddc7faa8..dc9bc0ab 100644 --- a/go.sum +++ b/go.sum @@ -1683,8 +1683,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pkg/sftp v1.13.6-0.20230104082718-2489717da0f3 h1:eKBJ919kpjpfHltsNthMO6ZQ/XQy76cHHbuz2bOmMSA= -github.com/pkg/sftp v1.13.6-0.20230104082718-2489717da0f3/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6 h1:5TvW1dv00Y13njmQ1AWkxSWtPkwE7ZEF6yDuv9q+Als= +github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -1800,12 +1800,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo= -github.com/sftpgo/sdk v0.1.3-0.20221217110036-383c1bb50fa0 h1:e1OQroqX8SWV06Z270CxG2/v//Wx1026iXKTDRn5J1E= -github.com/sftpgo/sdk v0.1.3-0.20221217110036-383c1bb50fa0/go.mod h1:3GpW3Qy8IHH6kex0ny+Y6ayeYb9OJxz8Pxh3IZgAs2E= -github.com/sftpgo/sdk v0.1.3-0.20230212154322-556375985d8c h1:SiWQZe99SZ/O4QSIsxzL91NgwFJNoo4IJ31cazUrYh4= -github.com/sftpgo/sdk v0.1.3-0.20230212154322-556375985d8c/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA= -github.com/sftpgo/sdk v0.1.3-0.20230213120720-de3129520736 h1:QFzoqYPIxuqDOe2NJfYI7J71bZrsfC0Aejc0ChblkcA= -github.com/sftpgo/sdk v0.1.3-0.20230213120720-de3129520736/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA= +github.com/sftpgo/sdk v0.1.3-0.20230213182959-2d89540f8810 h1:9K/1RGoZcWiv2ue1JvAnKwerOzJsCAUqCR2/BnibT8s= +github.com/sftpgo/sdk v0.1.3-0.20230213182959-2d89540f8810/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA= github.com/shirou/gopsutil/v3 v3.23.1 h1:a9KKO+kGLKEvcPIs4W62v0nu3sciVDOOOPUD0Hz7z/4= github.com/shirou/gopsutil/v3 v3.23.1/go.mod h1:NN6mnm5/0k8jw4cBfCnJtr5L7ErOTg18tMNpgFkn0hA= github.com/shoenig/test v0.4.3/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0= diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go index fda6cce0..3bcda315 100644 --- a/internal/common/eventmanager.go +++ b/internal/common/eventmanager.go @@ -1281,6 +1281,10 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event replacer := strings.NewReplacer(replacements...) body := replaceWithReplacer(c.Body, replacer) subject := replaceWithReplacer(c.Subject, replacer) + recipients := make([]string, 0, len(c.Recipients)) + for _, recipient := range c.Recipients { + recipients = append(recipients, replaceWithReplacer(recipient, replacer)) + } startTime := time.Now() var files []*mail.File fileAttachments := make([]string, 0, len(c.Attachments)) @@ -1317,7 +1321,7 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event } files = append(files, res...) } - err := smtp.SendEmail(c.Recipients, subject, body, smtp.EmailContentTypeTextPlain, files...) + err := smtp.SendEmail(recipients, subject, body, smtp.EmailContentTypeTextPlain, files...) eventManagerLog(logger.LevelDebug, "executed email notification action, elapsed: %s, error: %v", time.Since(startTime), err) if err != nil { diff --git a/internal/dataprovider/group.go b/internal/dataprovider/group.go index a99cc6d5..9d7a4b3c 100644 --- a/internal/dataprovider/group.go +++ b/internal/dataprovider/group.go @@ -223,6 +223,7 @@ func (g *Group) getACopy() Group { UploadDataTransfer: g.UserSettings.UploadDataTransfer, DownloadDataTransfer: g.UserSettings.DownloadDataTransfer, TotalDataTransfer: g.UserSettings.TotalDataTransfer, + ExpiresIn: g.UserSettings.ExpiresIn, Filters: copyBaseUserFilters(g.UserSettings.Filters), }, FsConfig: g.UserSettings.FsConfig.GetACopy(), diff --git a/internal/dataprovider/user.go b/internal/dataprovider/user.go index 5566069a..c9e989e1 100644 --- a/internal/dataprovider/user.go +++ b/internal/dataprovider/user.go @@ -1737,6 +1737,9 @@ func (u *User) mergeWithPrimaryGroup(group Group, replacer *strings.Replacer) { u.DownloadDataTransfer = group.UserSettings.DownloadDataTransfer u.TotalDataTransfer = group.UserSettings.TotalDataTransfer } + if u.ExpirationDate == 0 && group.UserSettings.ExpiresIn > 0 { + u.ExpirationDate = u.CreatedAt + int64(group.UserSettings.ExpiresIn)*86400000 + } u.mergePrimaryGroupFilters(group.UserSettings.Filters, replacer) u.mergeAdditiveProperties(group, sdk.GroupTypePrimary, replacer) } diff --git a/internal/httpd/auth_utils.go b/internal/httpd/auth_utils.go index 293f07d1..789fc83f 100644 --- a/internal/httpd/auth_utils.go +++ b/internal/httpd/auth_utils.go @@ -409,7 +409,6 @@ func verifyCSRFToken(tokenString, ip string) error { if tokenValidationMode != tokenValidationNoIPMatch { if !util.Contains(token.Audience(), ip) { - fmt.Printf("ip %v audience %+v\n\n", ip, token.Audience()) logger.Debug(logSender, "", "error validating CSRF token IP audience") return errors.New("the form token is not valid") } diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index 13ddce6a..386f4909 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -1184,6 +1184,8 @@ func TestGroupSettingsOverride(t *testing.T) { assert.NoError(t, err) assert.Len(t, user1.VirtualFolders, 0) assert.Len(t, user2.VirtualFolders, 3) + assert.Equal(t, int64(0), user1.ExpirationDate) + assert.Equal(t, int64(0), user2.ExpirationDate) group2.UserSettings.FsConfig = vfs.Filesystem{ Provider: sdk.SFTPFilesystemProvider, @@ -1230,6 +1232,7 @@ func TestGroupSettingsOverride(t *testing.T) { group1.UserSettings.UploadBandwidth = 512 group1.UserSettings.DownloadBandwidth = 1024 group1.UserSettings.TotalDataTransfer = 2048 + group1.UserSettings.ExpiresIn = 15 group1.UserSettings.Filters.MaxUploadFileSize = 1024 * 1024 group1.UserSettings.Filters.StartDirectory = "/startdir/%username%" group1.UserSettings.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled} @@ -1250,6 +1253,7 @@ func TestGroupSettingsOverride(t *testing.T) { user, err = dataprovider.CheckUserAndPass(defaultUsername, defaultPassword, "", common.ProtocolHTTP) assert.NoError(t, err) assert.Len(t, user.VirtualFolders, 3) + assert.Equal(t, user.CreatedAt+int64(group1.UserSettings.ExpiresIn)*86400000, user.ExpirationDate) assert.Equal(t, sdk.SFTPFilesystemProvider, user.FsConfig.Provider) assert.Equal(t, altAdminUsername, user.FsConfig.SFTPConfig.Username) assert.Equal(t, "/dirs/"+defaultUsername, user.FsConfig.SFTPConfig.Prefix) @@ -21660,6 +21664,7 @@ func TestAddWebGroup(t *testing.T) { QuotaFiles: 10, UploadBandwidth: 128, DownloadBandwidth: 256, + ExpiresIn: 10, }, } form := make(url.Values) @@ -21727,6 +21732,16 @@ func TestAddWebGroup(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) + assert.Contains(t, rr.Body.String(), "invalid expires in") + form.Set("expires_in", strconv.Itoa(group.UserSettings.ExpiresIn)) + b, contentType, err = getMultipartFormData(form, "", "") + assert.NoError(t, err) + req, err = http.NewRequest(http.MethodPost, webGroupPath, &b) + assert.NoError(t, err) + req.Header.Set("Content-Type", contentType) + setJWTCookieForReq(req, webToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) assert.Contains(t, rr.Body.String(), "invalid max upload file size") form.Set("max_upload_file_size", "0") form.Set("default_shares_expiration", "0") @@ -22163,6 +22178,7 @@ func TestUpdateWebGroupMock(t *testing.T) { form.Set("total_data_transfer", "0") form.Set("max_upload_file_size", "0") form.Set("default_shares_expiration", "0") + form.Set("expires_in", "0") form.Set("password_expiration", "0") form.Set("external_auth_cache_time", "0") form.Set("fs_provider", strconv.FormatInt(int64(group.UserSettings.FsConfig.Provider), 10)) diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index d9a1833c..988656f4 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -2074,6 +2074,10 @@ func getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) { if err != nil { return group, err } + expiresIn, err := strconv.ParseInt(r.Form.Get("expires_in"), 10, 64) + if err != nil { + return group, fmt.Errorf("invalid expires in: %w", err) + } fsConfig, err := getFsConfigFromPostFields(r) if err != nil { return group, err @@ -2099,6 +2103,7 @@ func getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) { UploadDataTransfer: dataTransferUL, DownloadDataTransfer: dataTransferDL, TotalDataTransfer: dataTransferTotal, + ExpiresIn: int(expiresIn), Filters: filters, }, FsConfig: fsConfig, diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go index d79ad09b..56e9287b 100644 --- a/internal/httpdtest/httpdtest.go +++ b/internal/httpdtest/httpdtest.go @@ -2212,7 +2212,6 @@ func compareGCSConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error { return errors.New("GCS upload part size mismatch") } if expected.GCSConfig.UploadPartMaxTime != actual.GCSConfig.UploadPartMaxTime { - fmt.Printf("aaaaaaaaaa %v, %v", expected.GCSConfig.UploadPartMaxTime, actual.GCSConfig.UploadPartMaxTime) return errors.New("GCS upload part max time mismatch") } return nil @@ -2782,6 +2781,9 @@ func compareEqualGroupSettingsFields(expected sdk.BaseGroupUserSettings, actual if expected.TotalDataTransfer != actual.TotalDataTransfer { return errors.New("total_data_transfer mismatch") } + if expected.ExpiresIn != actual.ExpiresIn { + return errors.New("expires_in mismatch") + } return compareUserPermissions(expected.Permissions, actual.Permissions) } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 4efd598c..72a9759d 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -6443,6 +6443,9 @@ components: total_data_transfer: type: integer description: 'Maximum total data transfer as MB' + expires_in: + type: integer + description: 'Account expiration in number of days from creation. 0 means no expiration' filters: $ref: '#/components/schemas/BaseUserFilters' filesystem: diff --git a/templates/webadmin/eventaction.html b/templates/webadmin/eventaction.html index d3d15034..6bc4ddc6 100644 --- a/templates/webadmin/eventaction.html +++ b/templates/webadmin/eventaction.html @@ -426,7 +426,7 @@ along with this program. If not, see . - Comma separated email recipients + Comma separated email recipients. Placeholders are supported diff --git a/templates/webadmin/group.html b/templates/webadmin/group.html index 65527073..3faddc00 100644 --- a/templates/webadmin/group.html +++ b/templates/webadmin/group.html @@ -706,6 +706,17 @@ along with this program. If not, see . +
+ +
+ + + Account expiration as number of days from the creation. 0 means no expiration + +
+
+