shares: allow to force an expiration date
this is a soft requirement, users can reactivate expired shares by updating the expiration date Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
ea96fe9a26
commit
830116bcf2
13 changed files with 233 additions and 9 deletions
|
@ -16,7 +16,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, password strength: if they are set to `0` for the user they are replaced with the value set for the group, if different from `0`. The password strength defined at group level is only enforce when users change their password
|
- max sessions, quota size/files, upload/download bandwidth, upload/download/total data transfer, max upload size, external auth cache time, ftp_security, default shares expiration, max shares expiration, password expiration, password strength: if they are set to `0` for the user they are replaced with the value set for the group, if different from `0`. The password strength defined at group level is only enforce when users change their password
|
||||||
- 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
|
- 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
4
go.mod
|
@ -53,7 +53,7 @@ require (
|
||||||
github.com/rs/cors v1.9.0
|
github.com/rs/cors v1.9.0
|
||||||
github.com/rs/xid v1.5.0
|
github.com/rs/xid v1.5.0
|
||||||
github.com/rs/zerolog v1.30.0
|
github.com/rs/zerolog v1.30.0
|
||||||
github.com/sftpgo/sdk v0.1.5
|
github.com/sftpgo/sdk v0.1.6-0.20230807170339-3178878ce745
|
||||||
github.com/shirou/gopsutil/v3 v3.23.7
|
github.com/shirou/gopsutil/v3 v3.23.7
|
||||||
github.com/spf13/afero v1.9.5
|
github.com/spf13/afero v1.9.5
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
|
@ -159,7 +159,7 @@ require (
|
||||||
golang.org/x/mod v0.12.0 // indirect
|
golang.org/x/mod v0.12.0 // indirect
|
||||||
golang.org/x/sync v0.3.0 // indirect
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
golang.org/x/text v0.12.0 // indirect
|
golang.org/x/text v0.12.0 // indirect
|
||||||
golang.org/x/tools v0.11.1 // indirect
|
golang.org/x/tools v0.12.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
|
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -421,8 +421,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
||||||
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/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
github.com/sftpgo/sdk v0.1.5 h1:3vpE5wohtJvJKyPKB7smAMZiLjLyoJzbtIkYNyNh5iw=
|
github.com/sftpgo/sdk v0.1.6-0.20230807170339-3178878ce745 h1:IRKfXQ0/P0ON9UzltTmgLKU0HWYSkuafARw3Pv3hDRU=
|
||||||
github.com/sftpgo/sdk v0.1.5/go.mod h1:TjeoMWS0JEXt9RukJveTnaiHj4+MVLtUiDC+mY++Odk=
|
github.com/sftpgo/sdk v0.1.6-0.20230807170339-3178878ce745/go.mod h1:TjeoMWS0JEXt9RukJveTnaiHj4+MVLtUiDC+mY++Odk=
|
||||||
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
|
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
|
||||||
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
|
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
|
@ -725,8 +725,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.11.1 h1:ojD5zOW8+7dOGzdnNgersm8aPfcDjhMp12UfG93NIMc=
|
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||||
golang.org/x/tools v0.11.1/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
|
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|
|
@ -2570,6 +2570,7 @@ func copyBaseUserFilters(in sdk.BaseUserFilters) sdk.BaseUserFilters {
|
||||||
filters.AllowAPIKeyAuth = in.AllowAPIKeyAuth
|
filters.AllowAPIKeyAuth = in.AllowAPIKeyAuth
|
||||||
filters.ExternalAuthCacheTime = in.ExternalAuthCacheTime
|
filters.ExternalAuthCacheTime = in.ExternalAuthCacheTime
|
||||||
filters.DefaultSharesExpiration = in.DefaultSharesExpiration
|
filters.DefaultSharesExpiration = in.DefaultSharesExpiration
|
||||||
|
filters.MaxSharesExpiration = in.MaxSharesExpiration
|
||||||
filters.PasswordExpiration = in.PasswordExpiration
|
filters.PasswordExpiration = in.PasswordExpiration
|
||||||
filters.PasswordStrength = in.PasswordStrength
|
filters.PasswordStrength = in.PasswordStrength
|
||||||
filters.WebClient = make([]string, len(in.WebClient))
|
filters.WebClient = make([]string, len(in.WebClient))
|
||||||
|
@ -2967,6 +2968,10 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
|
||||||
return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts))
|
return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if filters.MaxSharesExpiration > 0 && filters.MaxSharesExpiration < filters.DefaultSharesExpiration {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("default shares expiration: %d must be less than or equal to max shares expiration: %d",
|
||||||
|
filters.DefaultSharesExpiration, filters.MaxSharesExpiration))
|
||||||
|
}
|
||||||
updateFiltersValues(filters)
|
updateFiltersValues(filters)
|
||||||
|
|
||||||
return validateFiltersPatternExtensions(filters)
|
return validateFiltersPatternExtensions(filters)
|
||||||
|
|
|
@ -359,6 +359,21 @@ func (u *User) hideConfidentialData() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckMaxShareExpiration returns an error if the share expiration exceed the
|
||||||
|
// maximum allowed date.
|
||||||
|
func (u *User) CheckMaxShareExpiration(expiresAt time.Time) error {
|
||||||
|
if u.Filters.MaxSharesExpiration == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
maxAllowedExpiration := time.Now().Add(24 * time.Hour * time.Duration(u.Filters.MaxSharesExpiration+1))
|
||||||
|
maxAllowedExpiration = time.Date(maxAllowedExpiration.Year(), maxAllowedExpiration.Month(),
|
||||||
|
maxAllowedExpiration.Day(), 0, 0, 0, 0, maxAllowedExpiration.Location())
|
||||||
|
if util.GetTimeAsMsSinceEpoch(expiresAt) == 0 || expiresAt.After(maxAllowedExpiration) {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("the share must expire before %s", maxAllowedExpiration.Format(time.DateOnly)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetSubDirPermissions returns permissions for sub directories
|
// GetSubDirPermissions returns permissions for sub directories
|
||||||
func (u *User) GetSubDirPermissions() []sdk.DirectoryPermissions {
|
func (u *User) GetSubDirPermissions() []sdk.DirectoryPermissions {
|
||||||
var result []sdk.DirectoryPermissions
|
var result []sdk.DirectoryPermissions
|
||||||
|
@ -1738,7 +1753,7 @@ func (u *User) mergeWithPrimaryGroup(group *Group, replacer *strings.Replacer) {
|
||||||
u.mergeAdditiveProperties(group, sdk.GroupTypePrimary, replacer)
|
u.mergeAdditiveProperties(group, sdk.GroupTypePrimary, replacer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) mergePrimaryGroupFilters(filters *sdk.BaseUserFilters, replacer *strings.Replacer) {
|
func (u *User) mergePrimaryGroupFilters(filters *sdk.BaseUserFilters, replacer *strings.Replacer) { //nolint:gocyclo
|
||||||
if u.Filters.MaxUploadFileSize == 0 {
|
if u.Filters.MaxUploadFileSize == 0 {
|
||||||
u.Filters.MaxUploadFileSize = filters.MaxUploadFileSize
|
u.Filters.MaxUploadFileSize = filters.MaxUploadFileSize
|
||||||
}
|
}
|
||||||
|
@ -1775,6 +1790,9 @@ func (u *User) mergePrimaryGroupFilters(filters *sdk.BaseUserFilters, replacer *
|
||||||
if u.Filters.DefaultSharesExpiration == 0 {
|
if u.Filters.DefaultSharesExpiration == 0 {
|
||||||
u.Filters.DefaultSharesExpiration = filters.DefaultSharesExpiration
|
u.Filters.DefaultSharesExpiration = filters.DefaultSharesExpiration
|
||||||
}
|
}
|
||||||
|
if u.Filters.MaxSharesExpiration == 0 {
|
||||||
|
u.Filters.MaxSharesExpiration = filters.MaxSharesExpiration
|
||||||
|
}
|
||||||
if u.Filters.PasswordExpiration == 0 {
|
if u.Filters.PasswordExpiration == 0 {
|
||||||
u.Filters.PasswordExpiration = filters.PasswordExpiration
|
u.Filters.PasswordExpiration = filters.PasswordExpiration
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,10 @@ func addShare(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(share.ExpiresAt)); err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
share.ID = 0
|
share.ID = 0
|
||||||
share.ShareID = util.GenerateUniqueID()
|
share.ShareID = util.GenerateUniqueID()
|
||||||
share.LastUseAt = 0
|
share.LastUseAt = 0
|
||||||
|
@ -126,6 +130,11 @@ func updateShare(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "Unable to retrieve your user", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
shareID := getURLParam(r, "id")
|
shareID := getURLParam(r, "id")
|
||||||
share, err := dataprovider.ShareExists(shareID, claims.Username)
|
share, err := dataprovider.ShareExists(shareID, claims.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -152,6 +161,10 @@ func updateShare(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(updatedShare.ExpiresAt)); err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
err = dataprovider.UpdateShare(&updatedShare, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
|
err = dataprovider.UpdateShare(&updatedShare, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
|
|
@ -13502,6 +13502,117 @@ func TestShareUsage(t *testing.T) {
|
||||||
executeRequest(req)
|
executeRequest(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShareMaxExpiration(t *testing.T) {
|
||||||
|
u := getTestUser()
|
||||||
|
u.Filters.MaxSharesExpiration = 5
|
||||||
|
u.Filters.DefaultSharesExpiration = 10
|
||||||
|
_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, string(resp), "must be less than or equal to max shares expiration")
|
||||||
|
|
||||||
|
u.Filters.DefaultSharesExpiration = 0
|
||||||
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
webClientToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
s := dataprovider.Share{
|
||||||
|
Name: "test share",
|
||||||
|
Scope: dataprovider.ShareScopeRead,
|
||||||
|
Password: defaultPassword,
|
||||||
|
Paths: []string{"/"},
|
||||||
|
ExpiresAt: util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(u.Filters.MaxSharesExpiration+2))),
|
||||||
|
}
|
||||||
|
asJSON, err := json.Marshal(s)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setBearerForReq(req, token)
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||||
|
assert.Contains(t, rr.Body.String(), "share must expire before")
|
||||||
|
|
||||||
|
req, err = http.NewRequest(http.MethodPut, path.Join(userSharesPath, "shareID"), bytes.NewBuffer(asJSON))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setBearerForReq(req, token)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusNotFound, rr)
|
||||||
|
// expiresAt is mandatory
|
||||||
|
s.ExpiresAt = 0
|
||||||
|
asJSON, err = json.Marshal(s)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setBearerForReq(req, token)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||||
|
assert.Contains(t, rr.Body.String(), "share must expire before")
|
||||||
|
|
||||||
|
s.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(2 * time.Hour))
|
||||||
|
asJSON, err = json.Marshal(s)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setBearerForReq(req, token)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusCreated, rr)
|
||||||
|
shareID := rr.Header().Get("X-Object-ID")
|
||||||
|
assert.NotEmpty(t, shareID)
|
||||||
|
|
||||||
|
s.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(u.Filters.MaxSharesExpiration+2)))
|
||||||
|
asJSON, err = json.Marshal(s)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req, err = http.NewRequest(http.MethodPut, path.Join(userSharesPath, shareID), bytes.NewBuffer(asJSON))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setBearerForReq(req, token)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||||
|
assert.Contains(t, rr.Body.String(), "share must expire before")
|
||||||
|
form := make(url.Values)
|
||||||
|
form.Set("name", s.Name)
|
||||||
|
form.Set("scope", strconv.Itoa(int(s.Scope)))
|
||||||
|
form.Set("max_tokens", "0")
|
||||||
|
form.Set("paths", "/")
|
||||||
|
form.Set("expiration_date", time.Now().Add(24*time.Hour*time.Duration(u.Filters.MaxSharesExpiration+2)).Format("2006-01-02 15:04:05"))
|
||||||
|
form.Set(csrfFormToken, csrfToken)
|
||||||
|
req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.RemoteAddr = defaultRemoteAddr
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
setJWTCookieForReq(req, webClientToken)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
|
assert.Contains(t, rr.Body.String(), "share must expire before")
|
||||||
|
|
||||||
|
req, err = http.NewRequest(http.MethodPost, path.Join(webClientSharePath, shareID), bytes.NewBuffer([]byte(form.Encode())))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.RemoteAddr = defaultRemoteAddr
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
setJWTCookieForReq(req, webClientToken)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
|
assert.Contains(t, rr.Body.String(), "share must expire before")
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.RemoteAddr = defaultRemoteAddr
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
setJWTCookieForReq(req, webClientToken)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Unable to retrieve your user")
|
||||||
|
}
|
||||||
|
|
||||||
func TestWebClientShareCredentials(t *testing.T) {
|
func TestWebClientShareCredentials(t *testing.T) {
|
||||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -19486,6 +19597,16 @@ func TestWebUserAddMock(t *testing.T) {
|
||||||
checkResponseCode(t, http.StatusOK, rr)
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
assert.Contains(t, rr.Body.String(), "invalid default shares expiration")
|
assert.Contains(t, rr.Body.String(), "invalid default shares expiration")
|
||||||
form.Set("default_shares_expiration", "10")
|
form.Set("default_shares_expiration", "10")
|
||||||
|
// test invalid max shares expiration
|
||||||
|
form.Set("max_shares_expiration", "a")
|
||||||
|
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||||
|
setJWTCookieForReq(req, webToken)
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid max shares expiration")
|
||||||
|
form.Set("max_shares_expiration", "30")
|
||||||
// test invalid password expiration
|
// test invalid password expiration
|
||||||
form.Set("password_expiration", "a")
|
form.Set("password_expiration", "a")
|
||||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||||
|
@ -19613,6 +19734,7 @@ func TestWebUserAddMock(t *testing.T) {
|
||||||
assert.Equal(t, "/start/dir", newUser.Filters.StartDirectory)
|
assert.Equal(t, "/start/dir", newUser.Filters.StartDirectory)
|
||||||
assert.Equal(t, 0, newUser.Filters.FTPSecurity)
|
assert.Equal(t, 0, newUser.Filters.FTPSecurity)
|
||||||
assert.Equal(t, 10, newUser.Filters.DefaultSharesExpiration)
|
assert.Equal(t, 10, newUser.Filters.DefaultSharesExpiration)
|
||||||
|
assert.Equal(t, 30, newUser.Filters.MaxSharesExpiration)
|
||||||
assert.Equal(t, 90, newUser.Filters.PasswordExpiration)
|
assert.Equal(t, 90, newUser.Filters.PasswordExpiration)
|
||||||
assert.Equal(t, 60, newUser.Filters.PasswordStrength)
|
assert.Equal(t, 60, newUser.Filters.PasswordStrength)
|
||||||
assert.Greater(t, newUser.LastPasswordChange, int64(0))
|
assert.Greater(t, newUser.LastPasswordChange, int64(0))
|
||||||
|
@ -19798,6 +19920,7 @@ func TestWebUserUpdateMock(t *testing.T) {
|
||||||
form.Set("denied_protocols", common.ProtocolFTP)
|
form.Set("denied_protocols", common.ProtocolFTP)
|
||||||
form.Set("max_upload_file_size", "100")
|
form.Set("max_upload_file_size", "100")
|
||||||
form.Set("default_shares_expiration", "30")
|
form.Set("default_shares_expiration", "30")
|
||||||
|
form.Set("max_shares_expiration", "60")
|
||||||
form.Set("password_expiration", "60")
|
form.Set("password_expiration", "60")
|
||||||
form.Set("password_strength", "40")
|
form.Set("password_strength", "40")
|
||||||
form.Set("disconnect", "1")
|
form.Set("disconnect", "1")
|
||||||
|
@ -19879,6 +20002,7 @@ func TestWebUserUpdateMock(t *testing.T) {
|
||||||
assert.Equal(t, int64(0), updateUser.UploadDataTransfer)
|
assert.Equal(t, int64(0), updateUser.UploadDataTransfer)
|
||||||
assert.Equal(t, int64(0), updateUser.Filters.ExternalAuthCacheTime)
|
assert.Equal(t, int64(0), updateUser.Filters.ExternalAuthCacheTime)
|
||||||
assert.Equal(t, 30, updateUser.Filters.DefaultSharesExpiration)
|
assert.Equal(t, 30, updateUser.Filters.DefaultSharesExpiration)
|
||||||
|
assert.Equal(t, 60, updateUser.Filters.MaxSharesExpiration)
|
||||||
assert.Equal(t, 60, updateUser.Filters.PasswordExpiration)
|
assert.Equal(t, 60, updateUser.Filters.PasswordExpiration)
|
||||||
assert.Equal(t, 40, updateUser.Filters.PasswordStrength)
|
assert.Equal(t, 40, updateUser.Filters.PasswordStrength)
|
||||||
assert.True(t, updateUser.Filters.RequirePasswordChange)
|
assert.True(t, updateUser.Filters.RequirePasswordChange)
|
||||||
|
@ -19993,6 +20117,7 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
|
||||||
form.Set("fs_provider", "0")
|
form.Set("fs_provider", "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("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
form.Set("ftp_security", "1")
|
form.Set("ftp_security", "1")
|
||||||
|
@ -20089,6 +20214,7 @@ func TestUserSaveFromTemplateMock(t *testing.T) {
|
||||||
form.Set("fs_provider", "0")
|
form.Set("fs_provider", "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("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
form.Set("external_auth_cache_time", "0")
|
form.Set("external_auth_cache_time", "0")
|
||||||
|
@ -20180,6 +20306,7 @@ func TestUserTemplateMock(t *testing.T) {
|
||||||
form.Set("denied_extensions", "/dir2::.zip")
|
form.Set("denied_extensions", "/dir2::.zip")
|
||||||
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("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
form.Add("hooks", "external_auth_disabled")
|
form.Add("hooks", "external_auth_disabled")
|
||||||
|
@ -20313,6 +20440,7 @@ func TestUserPlaceholders(t *testing.T) {
|
||||||
form.Set("external_auth_cache_time", "0")
|
form.Set("external_auth_cache_time", "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("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||||
|
@ -20659,6 +20787,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
||||||
form.Set("pattern_policy1", "1")
|
form.Set("pattern_policy1", "1")
|
||||||
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("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
form.Set("ftp_security", "1")
|
form.Set("ftp_security", "1")
|
||||||
|
@ -20876,6 +21005,7 @@ func TestWebUserGCSMock(t *testing.T) {
|
||||||
form.Set("pattern_type0", "allowed")
|
form.Set("pattern_type0", "allowed")
|
||||||
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("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
form.Set("ftp_security", "1")
|
form.Set("ftp_security", "1")
|
||||||
|
@ -21005,6 +21135,7 @@ func TestWebUserHTTPFsMock(t *testing.T) {
|
||||||
form.Set("pattern_type1", "denied")
|
form.Set("pattern_type1", "denied")
|
||||||
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("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
form.Set("http_equality_check_mode", "true")
|
form.Set("http_equality_check_mode", "true")
|
||||||
|
@ -21132,6 +21263,7 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
||||||
form.Set("pattern_type1", "denied")
|
form.Set("pattern_type1", "denied")
|
||||||
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("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
// test invalid az_upload_part_size
|
// test invalid az_upload_part_size
|
||||||
|
@ -21316,6 +21448,7 @@ func TestWebUserCryptMock(t *testing.T) {
|
||||||
form.Set("pattern_type1", "denied")
|
form.Set("pattern_type1", "denied")
|
||||||
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("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
// passphrase cannot be empty
|
// passphrase cannot be empty
|
||||||
|
@ -21428,6 +21561,7 @@ func TestWebUserSFTPFsMock(t *testing.T) {
|
||||||
form.Set("pattern_type1", "denied")
|
form.Set("pattern_type1", "denied")
|
||||||
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("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
// empty sftpconfig
|
// empty sftpconfig
|
||||||
|
@ -21555,6 +21689,7 @@ func TestWebUserRole(t *testing.T) {
|
||||||
form.Set("total_data_transfer", strconv.FormatInt(user.TotalDataTransfer, 10))
|
form.Set("total_data_transfer", strconv.FormatInt(user.TotalDataTransfer, 10))
|
||||||
form.Set("max_upload_file_size", "0")
|
form.Set("max_upload_file_size", "0")
|
||||||
form.Set("default_shares_expiration", "10")
|
form.Set("default_shares_expiration", "10")
|
||||||
|
form.Set("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||||
|
@ -22770,6 +22905,7 @@ func TestAddWebGroup(t *testing.T) {
|
||||||
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")
|
||||||
|
form.Set("max_shares_expiration", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
b, contentType, err = getMultipartFormData(form, "", "")
|
b, contentType, err = getMultipartFormData(form, "", "")
|
||||||
|
@ -23208,6 +23344,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("max_shares_expiration", "0")
|
||||||
form.Set("expires_in", "0")
|
form.Set("expires_in", "0")
|
||||||
form.Set("password_expiration", "0")
|
form.Set("password_expiration", "0")
|
||||||
form.Set("password_strength", "0")
|
form.Set("password_strength", "0")
|
||||||
|
|
|
@ -1498,6 +1498,10 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filters, fmt.Errorf("invalid default shares expiration: %w", err)
|
return filters, fmt.Errorf("invalid default shares expiration: %w", err)
|
||||||
}
|
}
|
||||||
|
maxSharesExpiration, err := strconv.Atoi(r.Form.Get("max_shares_expiration"))
|
||||||
|
if err != nil {
|
||||||
|
return filters, fmt.Errorf("invalid max shares expiration: %w", err)
|
||||||
|
}
|
||||||
passwordExpiration, err := strconv.Atoi(r.Form.Get("password_expiration"))
|
passwordExpiration, err := strconv.Atoi(r.Form.Get("password_expiration"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filters, fmt.Errorf("invalid password expiration: %w", err)
|
return filters, fmt.Errorf("invalid password expiration: %w", err)
|
||||||
|
@ -1519,6 +1523,7 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
||||||
filters.TLSUsername = sdk.TLSUsername(strings.TrimSpace(r.Form.Get("tls_username")))
|
filters.TLSUsername = sdk.TLSUsername(strings.TrimSpace(r.Form.Get("tls_username")))
|
||||||
filters.WebClient = r.Form["web_client_options"]
|
filters.WebClient = r.Form["web_client_options"]
|
||||||
filters.DefaultSharesExpiration = defaultSharesExpiration
|
filters.DefaultSharesExpiration = defaultSharesExpiration
|
||||||
|
filters.MaxSharesExpiration = maxSharesExpiration
|
||||||
filters.PasswordExpiration = passwordExpiration
|
filters.PasswordExpiration = passwordExpiration
|
||||||
filters.PasswordStrength = passwordStrength
|
filters.PasswordStrength = passwordStrength
|
||||||
hooks := r.Form["hooks"]
|
hooks := r.Form["hooks"]
|
||||||
|
|
|
@ -1319,6 +1319,15 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
|
if err != nil {
|
||||||
|
s.renderAddUpdateSharePage(w, r, share, "Unable to retrieve your user", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(share.ExpiresAt)); err != nil {
|
||||||
|
s.renderAddUpdateSharePage(w, r, share, err.Error(), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
err = dataprovider.AddShare(share, claims.Username, ipAddr, claims.Role)
|
err = dataprovider.AddShare(share, claims.Username, ipAddr, claims.Role)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
|
http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
|
||||||
|
@ -1364,6 +1373,15 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
|
if err != nil {
|
||||||
|
s.renderAddUpdateSharePage(w, r, updatedShare, "Unable to retrieve your user", false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(updatedShare.ExpiresAt)); err != nil {
|
||||||
|
s.renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false)
|
||||||
|
return
|
||||||
|
}
|
||||||
err = dataprovider.UpdateShare(updatedShare, claims.Username, ipAddr, claims.Role)
|
err = dataprovider.UpdateShare(updatedShare, claims.Username, ipAddr, claims.Role)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
|
http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
|
||||||
|
|
|
@ -2431,7 +2431,7 @@ func compareUserFiltersEqualFields(expected sdk.BaseUserFilters, actual sdk.Base
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareBaseUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
func compareBaseUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error { //nolint:gocyclo
|
||||||
if len(expected.AllowedIP) != len(actual.AllowedIP) {
|
if len(expected.AllowedIP) != len(actual.AllowedIP) {
|
||||||
return errors.New("allowed IP mismatch")
|
return errors.New("allowed IP mismatch")
|
||||||
}
|
}
|
||||||
|
@ -2468,6 +2468,9 @@ func compareBaseUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFil
|
||||||
if expected.DefaultSharesExpiration != actual.DefaultSharesExpiration {
|
if expected.DefaultSharesExpiration != actual.DefaultSharesExpiration {
|
||||||
return errors.New("default_shares_expiration mismatch")
|
return errors.New("default_shares_expiration mismatch")
|
||||||
}
|
}
|
||||||
|
if expected.MaxSharesExpiration != actual.MaxSharesExpiration {
|
||||||
|
return errors.New("max_shares_expiration mismatch")
|
||||||
|
}
|
||||||
if expected.PasswordExpiration != actual.PasswordExpiration {
|
if expected.PasswordExpiration != actual.PasswordExpiration {
|
||||||
return errors.New("password_expiration mismatch")
|
return errors.New("password_expiration mismatch")
|
||||||
}
|
}
|
||||||
|
|
|
@ -5540,6 +5540,9 @@ components:
|
||||||
default_shares_expiration:
|
default_shares_expiration:
|
||||||
type: integer
|
type: integer
|
||||||
description: 'Defines the default expiration for newly created shares as number of days. 0 means no expiration'
|
description: 'Defines the default expiration for newly created shares as number of days. 0 means no expiration'
|
||||||
|
max_shares_expiration:
|
||||||
|
type: integer
|
||||||
|
description: 'Defines the maximum allowed expiration, as a number of days, when a user creates or updates a share. 0 means no expiration'
|
||||||
password_expiration:
|
password_expiration:
|
||||||
type: integer
|
type: integer
|
||||||
description: 'The password expires after the defined number of days. 0 means no expiration'
|
description: 'The password expires after the defined number of days. 0 means no expiration'
|
||||||
|
|
|
@ -651,6 +651,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idMaxSharesExpiration" class="col-sm-2 col-form-label">Max shares expiration</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="number" class="form-control" id="idMaxSharesExpiration" name="max_shares_expiration"
|
||||||
|
value="{{.Group.UserSettings.Filters.MaxSharesExpiration}}" min="0" aria-describedby="maxSharesExpirationHelpBlock">
|
||||||
|
<small id="maxSharesExpirationHelpBlock" class="form-text text-muted">
|
||||||
|
Maximum allowed expiration, as number of days, when a user creates or updates a share
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
|
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
|
|
@ -393,6 +393,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idMaxSharesExpiration" class="col-sm-2 col-form-label">Max shares expiration</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="number" class="form-control" id="idMaxSharesExpiration" name="max_shares_expiration"
|
||||||
|
value="{{.User.Filters.MaxSharesExpiration}}" min="0" aria-describedby="maxSharesExpirationHelpBlock">
|
||||||
|
<small id="maxSharesExpirationHelpBlock" class="form-text text-muted">
|
||||||
|
Maximum allowed expiration, as number of days, when a user creates or updates a share
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
|
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
|
Loading…
Reference in a new issue