groups: add expiration date override
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
2df2803a37
commit
78cd5d8eba
13 changed files with 56 additions and 14 deletions
|
@ -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
|
||||
|
||||
|
|
4
go.mod
4
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
|
||||
|
|
12
go.sum
12
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=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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=""
|
||||
aria-describedby="smtpRecipientsHelpBlock">{{.Action.Options.EmailConfig.GetRecipientsAsString}}</textarea>
|
||||
<small id="smtpRecipientsHelpBlock" class="form-text text-muted">
|
||||
Comma separated email recipients
|
||||
Comma separated email recipients. Placeholders are supported
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -706,6 +706,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</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">
|
||||
<label for="idPasswordExpiration" class="col-sm-2 col-form-label">Password expiration</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
Loading…
Reference in a new issue