groups: add expiration date override

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-02-13 19:32:36 +01:00
parent 2df2803a37
commit 78cd5d8eba
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
13 changed files with 56 additions and 14 deletions

View file

@ -6,6 +6,7 @@ SFTPGo supports two types of groups:
- primary groups - primary groups
- secondary 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. 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 - 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 - 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` - 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 - 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 - 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

4
go.mod
View file

@ -45,14 +45,14 @@ require (
github.com/minio/sio v0.3.0 github.com/minio/sio v0.3.0
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.20230104082718-2489717da0f3 github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6
github.com/pquerna/otp v1.4.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 github.com/rs/cors v1.8.3
github.com/rs/xid v1.4.0 github.com/rs/xid v1.4.0
github.com/rs/zerolog v1.29.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/shirou/gopsutil/v3 v3.23.1
github.com/spf13/afero v1.9.3 github.com/spf13/afero v1.9.3
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1

12
go.sum
View file

@ -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/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.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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.20230213180117-971c283182b6 h1:5TvW1dv00Y13njmQ1AWkxSWtPkwE7ZEF6yDuv9q+Als=
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/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 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.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/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/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.20230213182959-2d89540f8810 h1:9K/1RGoZcWiv2ue1JvAnKwerOzJsCAUqCR2/BnibT8s=
github.com/sftpgo/sdk v0.1.3-0.20221217110036-383c1bb50fa0/go.mod h1:3GpW3Qy8IHH6kex0ny+Y6ayeYb9OJxz8Pxh3IZgAs2E= github.com/sftpgo/sdk v0.1.3-0.20230213182959-2d89540f8810/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA=
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/shirou/gopsutil/v3 v3.23.1 h1:a9KKO+kGLKEvcPIs4W62v0nu3sciVDOOOPUD0Hz7z/4= 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/shirou/gopsutil/v3 v3.23.1/go.mod h1:NN6mnm5/0k8jw4cBfCnJtr5L7ErOTg18tMNpgFkn0hA=
github.com/shoenig/test v0.4.3/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0= github.com/shoenig/test v0.4.3/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0=

View file

@ -1281,6 +1281,10 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
replacer := strings.NewReplacer(replacements...) replacer := strings.NewReplacer(replacements...)
body := replaceWithReplacer(c.Body, replacer) body := replaceWithReplacer(c.Body, replacer)
subject := replaceWithReplacer(c.Subject, 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() startTime := time.Now()
var files []*mail.File var files []*mail.File
fileAttachments := make([]string, 0, len(c.Attachments)) fileAttachments := make([]string, 0, len(c.Attachments))
@ -1317,7 +1321,7 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
} }
files = append(files, res...) 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", eventManagerLog(logger.LevelDebug, "executed email notification action, elapsed: %s, error: %v",
time.Since(startTime), err) time.Since(startTime), err)
if err != nil { if err != nil {

View file

@ -223,6 +223,7 @@ func (g *Group) getACopy() Group {
UploadDataTransfer: g.UserSettings.UploadDataTransfer, UploadDataTransfer: g.UserSettings.UploadDataTransfer,
DownloadDataTransfer: g.UserSettings.DownloadDataTransfer, DownloadDataTransfer: g.UserSettings.DownloadDataTransfer,
TotalDataTransfer: g.UserSettings.TotalDataTransfer, TotalDataTransfer: g.UserSettings.TotalDataTransfer,
ExpiresIn: g.UserSettings.ExpiresIn,
Filters: copyBaseUserFilters(g.UserSettings.Filters), Filters: copyBaseUserFilters(g.UserSettings.Filters),
}, },
FsConfig: g.UserSettings.FsConfig.GetACopy(), FsConfig: g.UserSettings.FsConfig.GetACopy(),

View file

@ -1737,6 +1737,9 @@ func (u *User) mergeWithPrimaryGroup(group Group, replacer *strings.Replacer) {
u.DownloadDataTransfer = group.UserSettings.DownloadDataTransfer u.DownloadDataTransfer = group.UserSettings.DownloadDataTransfer
u.TotalDataTransfer = group.UserSettings.TotalDataTransfer 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.mergePrimaryGroupFilters(group.UserSettings.Filters, replacer)
u.mergeAdditiveProperties(group, sdk.GroupTypePrimary, replacer) u.mergeAdditiveProperties(group, sdk.GroupTypePrimary, replacer)
} }

View file

@ -409,7 +409,6 @@ func verifyCSRFToken(tokenString, ip string) error {
if tokenValidationMode != tokenValidationNoIPMatch { if tokenValidationMode != tokenValidationNoIPMatch {
if !util.Contains(token.Audience(), ip) { 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") logger.Debug(logSender, "", "error validating CSRF token IP audience")
return errors.New("the form token is not valid") return errors.New("the form token is not valid")
} }

View file

@ -1184,6 +1184,8 @@ func TestGroupSettingsOverride(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, user1.VirtualFolders, 0) assert.Len(t, user1.VirtualFolders, 0)
assert.Len(t, user2.VirtualFolders, 3) 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{ group2.UserSettings.FsConfig = vfs.Filesystem{
Provider: sdk.SFTPFilesystemProvider, Provider: sdk.SFTPFilesystemProvider,
@ -1230,6 +1232,7 @@ func TestGroupSettingsOverride(t *testing.T) {
group1.UserSettings.UploadBandwidth = 512 group1.UserSettings.UploadBandwidth = 512
group1.UserSettings.DownloadBandwidth = 1024 group1.UserSettings.DownloadBandwidth = 1024
group1.UserSettings.TotalDataTransfer = 2048 group1.UserSettings.TotalDataTransfer = 2048
group1.UserSettings.ExpiresIn = 15
group1.UserSettings.Filters.MaxUploadFileSize = 1024 * 1024 group1.UserSettings.Filters.MaxUploadFileSize = 1024 * 1024
group1.UserSettings.Filters.StartDirectory = "/startdir/%username%" group1.UserSettings.Filters.StartDirectory = "/startdir/%username%"
group1.UserSettings.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled} group1.UserSettings.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled}
@ -1250,6 +1253,7 @@ func TestGroupSettingsOverride(t *testing.T) {
user, err = dataprovider.CheckUserAndPass(defaultUsername, defaultPassword, "", common.ProtocolHTTP) user, err = dataprovider.CheckUserAndPass(defaultUsername, defaultPassword, "", common.ProtocolHTTP)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, user.VirtualFolders, 3) 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, sdk.SFTPFilesystemProvider, user.FsConfig.Provider)
assert.Equal(t, altAdminUsername, user.FsConfig.SFTPConfig.Username) assert.Equal(t, altAdminUsername, user.FsConfig.SFTPConfig.Username)
assert.Equal(t, "/dirs/"+defaultUsername, user.FsConfig.SFTPConfig.Prefix) assert.Equal(t, "/dirs/"+defaultUsername, user.FsConfig.SFTPConfig.Prefix)
@ -21660,6 +21664,7 @@ func TestAddWebGroup(t *testing.T) {
QuotaFiles: 10, QuotaFiles: 10,
UploadBandwidth: 128, UploadBandwidth: 128,
DownloadBandwidth: 256, DownloadBandwidth: 256,
ExpiresIn: 10,
}, },
} }
form := make(url.Values) form := make(url.Values)
@ -21727,6 +21732,16 @@ func TestAddWebGroup(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) 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") assert.Contains(t, rr.Body.String(), "invalid max upload file size")
form.Set("max_upload_file_size", "0") form.Set("max_upload_file_size", "0")
form.Set("default_shares_expiration", "0") form.Set("default_shares_expiration", "0")
@ -22163,6 +22178,7 @@ func TestUpdateWebGroupMock(t *testing.T) {
form.Set("total_data_transfer", "0") form.Set("total_data_transfer", "0")
form.Set("max_upload_file_size", "0") form.Set("max_upload_file_size", "0")
form.Set("default_shares_expiration", "0") form.Set("default_shares_expiration", "0")
form.Set("expires_in", "0")
form.Set("password_expiration", "0") form.Set("password_expiration", "0")
form.Set("external_auth_cache_time", "0") form.Set("external_auth_cache_time", "0")
form.Set("fs_provider", strconv.FormatInt(int64(group.UserSettings.FsConfig.Provider), 10)) form.Set("fs_provider", strconv.FormatInt(int64(group.UserSettings.FsConfig.Provider), 10))

View file

@ -2074,6 +2074,10 @@ func getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) {
if err != nil { if err != nil {
return group, err 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) fsConfig, err := getFsConfigFromPostFields(r)
if err != nil { if err != nil {
return group, err return group, err
@ -2099,6 +2103,7 @@ func getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) {
UploadDataTransfer: dataTransferUL, UploadDataTransfer: dataTransferUL,
DownloadDataTransfer: dataTransferDL, DownloadDataTransfer: dataTransferDL,
TotalDataTransfer: dataTransferTotal, TotalDataTransfer: dataTransferTotal,
ExpiresIn: int(expiresIn),
Filters: filters, Filters: filters,
}, },
FsConfig: fsConfig, FsConfig: fsConfig,

View file

@ -2212,7 +2212,6 @@ func compareGCSConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
return errors.New("GCS upload part size mismatch") return errors.New("GCS upload part size mismatch")
} }
if expected.GCSConfig.UploadPartMaxTime != actual.GCSConfig.UploadPartMaxTime { 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 errors.New("GCS upload part max time mismatch")
} }
return nil return nil
@ -2782,6 +2781,9 @@ func compareEqualGroupSettingsFields(expected sdk.BaseGroupUserSettings, actual
if expected.TotalDataTransfer != actual.TotalDataTransfer { if expected.TotalDataTransfer != actual.TotalDataTransfer {
return errors.New("total_data_transfer mismatch") return errors.New("total_data_transfer mismatch")
} }
if expected.ExpiresIn != actual.ExpiresIn {
return errors.New("expires_in mismatch")
}
return compareUserPermissions(expected.Permissions, actual.Permissions) return compareUserPermissions(expected.Permissions, actual.Permissions)
} }

View file

@ -6443,6 +6443,9 @@ components:
total_data_transfer: total_data_transfer:
type: integer type: integer
description: 'Maximum total data transfer as MB' 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: filters:
$ref: '#/components/schemas/BaseUserFilters' $ref: '#/components/schemas/BaseUserFilters'
filesystem: filesystem:

View file

@ -426,7 +426,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<textarea class="form-control" id="idEmailRecipients" name="email_recipients" rows="2" placeholder="" <textarea class="form-control" id="idEmailRecipients" name="email_recipients" rows="2" placeholder=""
aria-describedby="smtpRecipientsHelpBlock">{{.Action.Options.EmailConfig.GetRecipientsAsString}}</textarea> aria-describedby="smtpRecipientsHelpBlock">{{.Action.Options.EmailConfig.GetRecipientsAsString}}</textarea>
<small id="smtpRecipientsHelpBlock" class="form-text text-muted"> <small id="smtpRecipientsHelpBlock" class="form-text text-muted">
Comma separated email recipients Comma separated email recipients. Placeholders are supported
</small> </small>
</div> </div>
</div> </div>

View file

@ -706,6 +706,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
</div> </div>
<div class="form-group row">
<label for="idExpiresIn" class="col-sm-2 col-form-label">Expires in</label>
<div class="col-sm-10">
<input type="number" class="form-control" id="idExpiresIn" name="expires_in"
value="{{.Group.UserSettings.ExpiresIn}}" min="0" aria-describedby="expiresInHelpBlock">
<small id="expiresInHelpBlock" class="form-text text-muted">
Account expiration as number of days from the creation. 0 means no expiration
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="idPasswordExpiration" class="col-sm-2 col-form-label">Password expiration</label> <label for="idPasswordExpiration" class="col-sm-2 col-form-label">Password expiration</label>
<div class="col-sm-10"> <div class="col-sm-10">