try to make the web admin more user friendly
removed all the textarea with fields separated using "::". This should, hopefully, improve user experience
|
@ -85,6 +85,17 @@ var (
|
|||
errNoMatchingVirtualFolder = errors.New("no matching virtual folder found")
|
||||
)
|
||||
|
||||
// DirectoryPermissions defines permissions for a directory path
|
||||
type DirectoryPermissions struct {
|
||||
Path string
|
||||
Permissions []string
|
||||
}
|
||||
|
||||
// HasPerm returns true if the directory has the specified permissions
|
||||
func (d *DirectoryPermissions) HasPerm(perm string) bool {
|
||||
return utils.IsStringInSlice(perm, d.Permissions)
|
||||
}
|
||||
|
||||
// PatternsFilter defines filters based on shell like patterns.
|
||||
// These restrictions do not apply to files listing for performance reasons, so
|
||||
// a denied file cannot be downloaded/overwritten/renamed but will still be
|
||||
|
@ -106,6 +117,24 @@ type PatternsFilter struct {
|
|||
DeniedPatterns []string `json:"denied_patterns,omitempty"`
|
||||
}
|
||||
|
||||
// GetCommaSeparatedPatterns returns the first non empty patterns list comma separated
|
||||
func (p *PatternsFilter) GetCommaSeparatedPatterns() string {
|
||||
if len(p.DeniedPatterns) > 0 {
|
||||
return strings.Join(p.DeniedPatterns, ",")
|
||||
}
|
||||
return strings.Join(p.AllowedPatterns, ",")
|
||||
}
|
||||
|
||||
// IsDenied returns true if the patterns has one or more denied patterns
|
||||
func (p *PatternsFilter) IsDenied() bool {
|
||||
return len(p.DeniedPatterns) > 0
|
||||
}
|
||||
|
||||
// IsAllowed returns true if the patterns has one or more allowed patterns
|
||||
func (p *PatternsFilter) IsAllowed() bool {
|
||||
return len(p.AllowedPatterns) > 0
|
||||
}
|
||||
|
||||
// HooksFilter defines user specific overrides for global hooks
|
||||
type HooksFilter struct {
|
||||
ExternalAuthDisabled bool `json:"external_auth_disabled"`
|
||||
|
@ -328,6 +357,22 @@ func (u *User) hideConfidentialData() {
|
|||
}
|
||||
}
|
||||
|
||||
// GetSubDirPermissions returns permissions for sub directories
|
||||
func (u *User) GetSubDirPermissions() []DirectoryPermissions {
|
||||
var result []DirectoryPermissions
|
||||
for k, v := range u.Permissions {
|
||||
if k == "/" {
|
||||
continue
|
||||
}
|
||||
dirPerms := DirectoryPermissions{
|
||||
Path: k,
|
||||
Permissions: v,
|
||||
}
|
||||
result = append(result, dirPerms)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PrepareForRendering prepares a user for rendering.
|
||||
// It hides confidential data and set to nil the empty secrets
|
||||
// so they are not serialized
|
||||
|
@ -754,6 +799,28 @@ func (u *User) GetAllowedLoginMethods() []string {
|
|||
return allowedMethods
|
||||
}
|
||||
|
||||
// GetFlatFilePatterns returns file patterns as flat list
|
||||
// duplicating a path if it has both allowed and denied patterns
|
||||
func (u *User) GetFlatFilePatterns() []PatternsFilter {
|
||||
var result []PatternsFilter
|
||||
|
||||
for _, pattern := range u.Filters.FilePatterns {
|
||||
if len(pattern.AllowedPatterns) > 0 {
|
||||
result = append(result, PatternsFilter{
|
||||
Path: pattern.Path,
|
||||
AllowedPatterns: pattern.AllowedPatterns,
|
||||
})
|
||||
}
|
||||
if len(pattern.DeniedPatterns) > 0 {
|
||||
result = append(result, PatternsFilter{
|
||||
Path: pattern.Path,
|
||||
DeniedPatterns: pattern.DeniedPatterns,
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// IsFileAllowed returns true if the specified file is allowed by the file restrictions filters
|
||||
func (u *User) IsFileAllowed(virtualPath string) bool {
|
||||
return u.isFilePatternAllowed(virtualPath)
|
||||
|
|
|
@ -129,9 +129,10 @@ You can find more details about Data At Rest Encryption [here](../dare.md).
|
|||
|
||||
SFTPGo supports per directory virtual permissions. For each user you have to specify global permissions and then override them on a per-directory basis.
|
||||
|
||||
Take a look at the following screen.
|
||||
Take a look at the following screens.
|
||||
|
||||
![Virtual permissions](./img/virtual-permissions.png)
|
||||
![Per-directory permissions](./img/dir-permissions.png)
|
||||
|
||||
This user has full access as default (`*`), can only list and download from `/read-only` path and has no permissions at all for the `/subdir` path.
|
||||
|
||||
|
@ -176,7 +177,7 @@ From the web admin interface click `Folders` and then the `+` icon.
|
|||
|
||||
To create a local folder you need to specify a `Name` and an `Absolute path`. For other backends you have to specify the backend type and its credentials, this is the same procedure already detailed for creating users with cloud backends.
|
||||
|
||||
Suppose we created two folders name `localfolder` and `minio` as you can see in the following screen.
|
||||
Suppose we created two virtual folders name `localfolder` and `minio` as you can see in the following screen.
|
||||
|
||||
![Folders](./img/folders.png)
|
||||
|
||||
|
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 64 KiB |
BIN
docs/howto/img/dir-permissions.png
Normal file
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 7.6 KiB |
|
@ -245,7 +245,7 @@ func createCSRFToken() string {
|
|||
func verifyCSRFToken(tokenString string) error {
|
||||
token, err := jwtauth.VerifyToken(csrfTokenAuth, tokenString)
|
||||
if err != nil || token == nil {
|
||||
logger.Debug(logSender, "", "error validating CSRF: %v", err)
|
||||
logger.Debug(logSender, "", "error validating CSRF token %#v: %v", tokenString, err)
|
||||
return fmt.Errorf("unable to verify form token: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ const (
|
|||
defaultUsername = "test_user"
|
||||
defaultPassword = "test_password"
|
||||
testPubKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1"
|
||||
testPubKey1 = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCd60+/j+y8f0tLftihWV1YN9RSahMI9btQMDIMqts/jeNbD8jgoogM3nhF7KxfcaMKURuD47KC4Ey6iAJUJ0sWkSNNxOcIYuvA+5MlspfZDsa8Ag76Fe1vyz72WeHMHMeh/hwFo2TeIeIXg480T1VI6mzfDrVp2GzUx0SS0dMsQBjftXkuVR8YOiOwMCAH2a//M1OrvV7d/NBk6kBN0WnuIBb2jKm15PAA7+jQQG7tzwk2HedNH3jeL5GH31xkSRwlBczRK0xsCQXehAlx6cT/e/s44iJcJTHfpPKoSk6UAhPJYe7Z1QnuoawY9P9jQaxpyeImBZxxUEowhjpj2avBxKdRGBVK8R7EL8tSOeLbhdyWe5Mwc1+foEbq9Zz5j5Kd+hn3Wm1UnsGCrXUUUoZp1jnlNl0NakCto+5KmqnT9cHxaY+ix2RLUWAZyVFlRq71OYux1UHJnEJPiEI1/tr4jFBSL46qhQZv/TfpkfVW8FLz0lErfqu0gQEZnNHr3Fc= nicola@p1"
|
||||
defaultTokenAuthUser = "admin"
|
||||
defaultTokenAuthPass = "password"
|
||||
altAdminUsername = "newTestAdmin"
|
||||
|
@ -4842,6 +4843,7 @@ func TestWebClientChangePubKeys(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
form := make(url.Values)
|
||||
form.Set("public_keys", testPubKey)
|
||||
form.Add("public_keys", testPubKey1)
|
||||
// no csrf token
|
||||
req, _ := http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
@ -4860,7 +4862,7 @@ func TestWebClientChangePubKeys(t *testing.T) {
|
|||
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, user.PublicKeys, 1)
|
||||
assert.Len(t, user.PublicKeys, 2)
|
||||
|
||||
form.Set("public_keys", "invalid")
|
||||
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
|
@ -5899,10 +5901,28 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("sub_dirs_permissions", " /subdir::list ,download ")
|
||||
form.Set("virtual_folders", fmt.Sprintf(" /vdir:: %v :: 2 :: 1024", folderName))
|
||||
form.Set("allowed_patterns", "/dir2::*.jpg,*.png\n/dir1::*.png")
|
||||
form.Set("denied_patterns", "/dir1::*.zip\n/dir3::*.rar\n/dir2::*.mkv")
|
||||
form.Set("sub_perm_path0", "/subdir")
|
||||
form.Set("sub_perm_permissions0", "list")
|
||||
form.Add("sub_perm_permissions0", "download")
|
||||
form.Set("vfolder_path", " /vdir")
|
||||
form.Set("vfolder_name", folderName)
|
||||
form.Set("vfolder_quota_size", "1024")
|
||||
form.Set("vfolder_quota_files", "2")
|
||||
form.Set("pattern_path0", "/dir2")
|
||||
form.Set("patterns0", "*.jpg,*.png")
|
||||
form.Set("pattern_type0", "allowed")
|
||||
form.Set("pattern_path1", "/dir1")
|
||||
form.Set("patterns1", "*.png")
|
||||
form.Set("pattern_type1", "allowed")
|
||||
form.Set("pattern_path2", "/dir1")
|
||||
form.Set("patterns2", "*.zip")
|
||||
form.Set("pattern_type2", "denied")
|
||||
form.Set("pattern_path3", "/dir3")
|
||||
form.Set("patterns3", "*.rar")
|
||||
form.Set("pattern_type3", "denied")
|
||||
form.Set("pattern_path4", "/dir2")
|
||||
form.Set("patterns4", "*.mkv")
|
||||
form.Set("pattern_type4", "denied")
|
||||
form.Set("additional_info", user.AdditionalInfo)
|
||||
form.Set("description", user.Description)
|
||||
form.Add("hooks", "external_auth_disabled")
|
||||
|
@ -5915,6 +5935,7 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
form.Set("public_keys", testPubKey)
|
||||
form.Add("public_keys", testPubKey1)
|
||||
form.Set("uid", strconv.FormatInt(int64(user.UID), 10))
|
||||
form.Set("gid", "a")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
|
@ -6077,6 +6098,7 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
} else {
|
||||
assert.Fail(t, "user permissions must contain /somedir", "actual: %v", newUser.Permissions)
|
||||
}
|
||||
assert.Len(t, newUser.PublicKeys, 2)
|
||||
assert.Len(t, newUser.VirtualFolders, 1)
|
||||
for _, v := range newUser.VirtualFolders {
|
||||
assert.Equal(t, v.VirtualPath, "/vdir")
|
||||
|
@ -6154,12 +6176,16 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
form.Set("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("sub_dirs_permissions", "/otherdir :: list ,upload ")
|
||||
form.Set("sub_perm_path0", "/otherdir")
|
||||
form.Set("sub_perm_permissions0", "list")
|
||||
form.Add("sub_perm_permissions0", "upload")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("allowed_ip", " 192.168.1.3/32, 192.168.2.0/24 ")
|
||||
form.Set("denied_ip", " 10.0.0.2/32 ")
|
||||
form.Set("denied_patterns", "/dir1::*.zip")
|
||||
form.Set("pattern_path0", "/dir1")
|
||||
form.Set("patterns0", "*.zip")
|
||||
form.Set("pattern_type0", "denied")
|
||||
form.Set("ssh_login_methods", dataprovider.SSHLoginMethodKeyboardInteractive)
|
||||
form.Set("denied_protocols", common.ProtocolFTP)
|
||||
form.Set("max_upload_file_size", "100")
|
||||
|
@ -6355,14 +6381,24 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
|
|||
form.Set("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("sub_dirs_permissions", "")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("fs_provider", "0")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("description", "desc %username% %password%")
|
||||
form.Set("virtual_folders", "/vdir%username%::"+folder.Name+"::-1::-1")
|
||||
form.Set("users", "auser1::password1\nauser2::password2::"+testPubKey+"\nauser1::password")
|
||||
form.Set("vfolder_path", "/vdir%username%")
|
||||
form.Set("vfolder_name", folder.Name)
|
||||
form.Set("vfolder_quota_size", "-1")
|
||||
form.Set("vfolder_quota_files", "-1")
|
||||
form.Add("tpl_username", "auser1")
|
||||
form.Add("tpl_password", "password1")
|
||||
form.Add("tpl_public_keys", " ")
|
||||
form.Add("tpl_username", "auser2")
|
||||
form.Add("tpl_password", "password2")
|
||||
form.Add("tpl_public_keys", testPubKey)
|
||||
form.Add("tpl_username", "auser1")
|
||||
form.Add("tpl_password", "password")
|
||||
form.Add("tpl_public_keys", "")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ := http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -6443,7 +6479,6 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
form.Set("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("sub_dirs_permissions", "")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("allowed_ip", "")
|
||||
|
@ -6478,7 +6513,9 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
|
||||
form.Set("users", "user1::password1::invalid-pkey")
|
||||
form.Set("tpl_username", "user1")
|
||||
form.Set("tpl_password", "password1")
|
||||
form.Set("tpl_public_keys", "invalid-pkey")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -6487,7 +6524,9 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
require.Contains(t, rr.Body.String(), "Error validating user")
|
||||
|
||||
form.Set("users", "user1:password1")
|
||||
form.Set("tpl_username", "user1")
|
||||
form.Set("tpl_password", " ")
|
||||
form.Set("tpl_public_keys", "")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -6496,7 +6535,15 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
require.Contains(t, rr.Body.String(), "No valid users found, export is not possible")
|
||||
|
||||
form.Set("users", "user1::password1\nuser2::password2::"+testPubKey+"\nuser3::::")
|
||||
form.Set("tpl_username", "user1")
|
||||
form.Set("tpl_password", "password1")
|
||||
form.Set("tpl_public_keys", " ")
|
||||
form.Add("tpl_username", "user2")
|
||||
form.Add("tpl_password", "password2")
|
||||
form.Add("tpl_public_keys", testPubKey)
|
||||
form.Add("tpl_username", "user3")
|
||||
form.Add("tpl_password", "")
|
||||
form.Add("tpl_public_keys", "")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -6552,9 +6599,13 @@ func TestFolderTemplateMock(t *testing.T) {
|
|||
form.Set("name", folderName)
|
||||
form.Set("mapped_path", mappedPath)
|
||||
form.Set("description", "desc folder %name%")
|
||||
form.Set("folders", "folder1\nfolder2\nfolder3\nfolder1\n\n\n")
|
||||
contentType := "application/x-www-form-urlencoded"
|
||||
req, _ := http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
form.Add("tpl_foldername", "folder1")
|
||||
form.Add("tpl_foldername", "folder2")
|
||||
form.Add("tpl_foldername", "folder3")
|
||||
form.Add("tpl_foldername", "folder1 ")
|
||||
form.Add("tpl_foldername", " ")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ := http.NewRequest(http.MethodPost, webTemplateFolder, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr := executeRequest(req)
|
||||
|
@ -6562,7 +6613,8 @@ func TestFolderTemplateMock(t *testing.T) {
|
|||
assert.Contains(t, rr.Body.String(), "unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder+"?param=p%C3%AO%GG", bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder+"?param=p%C3%AO%GG", &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
|
@ -6572,7 +6624,8 @@ func TestFolderTemplateMock(t *testing.T) {
|
|||
folder1 := "folder1"
|
||||
folder2 := "folder2"
|
||||
folder3 := "folder3"
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
|
@ -6601,7 +6654,8 @@ func TestFolderTemplateMock(t *testing.T) {
|
|||
form.Set("s3_access_secret", "pwd%name%")
|
||||
form.Set("s3_key_prefix", "base/%name%")
|
||||
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
|
@ -6610,7 +6664,8 @@ func TestFolderTemplateMock(t *testing.T) {
|
|||
|
||||
form.Set("s3_upload_part_size", "5")
|
||||
form.Set("s3_upload_concurrency", "4")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
|
@ -6641,17 +6696,19 @@ func TestFolderTemplateMock(t *testing.T) {
|
|||
require.Equal(t, "pwd"+folder3, dump.Folders[2].FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
require.Equal(t, "base/"+folder3+"/", dump.Folders[2].FsConfig.S3Config.KeyPrefix)
|
||||
|
||||
form.Set("folders", "\n\n\n")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
form.Set("tpl_foldername", " ")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "No folders to export")
|
||||
|
||||
form.Set("folders", "name")
|
||||
form.Set("tpl_foldername", "name")
|
||||
form.Set("mapped_path", "relative-path")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
|
@ -6698,7 +6755,6 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
form.Set("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("sub_dirs_permissions", "")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("allowed_ip", "")
|
||||
|
@ -6711,8 +6767,12 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
form.Set("s3_storage_class", user.FsConfig.S3Config.StorageClass)
|
||||
form.Set("s3_endpoint", user.FsConfig.S3Config.Endpoint)
|
||||
form.Set("s3_key_prefix", user.FsConfig.S3Config.KeyPrefix)
|
||||
form.Set("allowed_patterns", "/dir1::*.jpg,*.png")
|
||||
form.Set("denied_patterns", "/dir2::*.zip")
|
||||
form.Set("pattern_path0", "/dir1")
|
||||
form.Set("patterns0", "*.jpg,*.png")
|
||||
form.Set("pattern_type0", "allowed")
|
||||
form.Set("pattern_path1", "/dir2")
|
||||
form.Set("patterns1", "*.zip")
|
||||
form.Set("pattern_type1", "denied")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("description", user.Description)
|
||||
form.Add("hooks", "pre_login_disabled")
|
||||
|
@ -6847,7 +6907,6 @@ func TestWebUserGCSMock(t *testing.T) {
|
|||
form.Set("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("sub_dirs_permissions", "")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("allowed_ip", "")
|
||||
|
@ -6856,7 +6915,9 @@ func TestWebUserGCSMock(t *testing.T) {
|
|||
form.Set("gcs_bucket", user.FsConfig.GCSConfig.Bucket)
|
||||
form.Set("gcs_storage_class", user.FsConfig.GCSConfig.StorageClass)
|
||||
form.Set("gcs_key_prefix", user.FsConfig.GCSConfig.KeyPrefix)
|
||||
form.Set("allowed_patterns", "/dir1::*.jpg,*.png")
|
||||
form.Set("pattern_path0", "/dir1")
|
||||
form.Set("patterns0", "*.jpg,*.png")
|
||||
form.Set("pattern_type0", "allowed")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
|
@ -6890,7 +6951,12 @@ func TestWebUserGCSMock(t *testing.T) {
|
|||
assert.Equal(t, user.FsConfig.GCSConfig.Bucket, updateUser.FsConfig.GCSConfig.Bucket)
|
||||
assert.Equal(t, user.FsConfig.GCSConfig.StorageClass, updateUser.FsConfig.GCSConfig.StorageClass)
|
||||
assert.Equal(t, user.FsConfig.GCSConfig.KeyPrefix, updateUser.FsConfig.GCSConfig.KeyPrefix)
|
||||
if assert.Len(t, updateUser.Filters.FilePatterns, 1) {
|
||||
assert.Equal(t, "/dir1", updateUser.Filters.FilePatterns[0].Path)
|
||||
assert.Len(t, updateUser.Filters.FilePatterns[0].AllowedPatterns, 2)
|
||||
assert.Contains(t, updateUser.Filters.FilePatterns[0].AllowedPatterns, "*.png")
|
||||
assert.Contains(t, updateUser.Filters.FilePatterns[0].AllowedPatterns, "*.jpg")
|
||||
}
|
||||
form.Set("gcs_auto_credentials", "on")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
|
@ -6950,7 +7016,6 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
|||
form.Set("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("sub_dirs_permissions", "")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("allowed_ip", "")
|
||||
|
@ -6963,8 +7028,12 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
|||
form.Set("az_endpoint", user.FsConfig.AzBlobConfig.Endpoint)
|
||||
form.Set("az_key_prefix", user.FsConfig.AzBlobConfig.KeyPrefix)
|
||||
form.Set("az_use_emulator", "checked")
|
||||
form.Set("allowed_patterns", "/dir1::*.jpg,*.png")
|
||||
form.Set("denied_patterns", "/dir2::*.zip")
|
||||
form.Set("pattern_path0", "/dir1")
|
||||
form.Set("patterns0", "*.jpg,*.png")
|
||||
form.Set("pattern_type0", "allowed")
|
||||
form.Set("pattern_path1", "/dir2")
|
||||
form.Set("patterns1", "*.zip")
|
||||
form.Set("pattern_type1", "denied")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
// test invalid az_upload_part_size
|
||||
form.Set("az_upload_part_size", "a")
|
||||
|
@ -7066,15 +7135,18 @@ func TestWebUserCryptMock(t *testing.T) {
|
|||
form.Set("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("sub_dirs_permissions", "")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("allowed_ip", "")
|
||||
form.Set("denied_ip", "")
|
||||
form.Set("fs_provider", "4")
|
||||
form.Set("crypt_passphrase", "")
|
||||
form.Set("allowed_patterns", "/dir1::*.jpg,*.png")
|
||||
form.Set("denied_patterns", "/dir2::*.zip")
|
||||
form.Set("pattern_path0", "/dir1")
|
||||
form.Set("patterns0", "*.jpg,*.png")
|
||||
form.Set("pattern_type0", "allowed")
|
||||
form.Set("pattern_path1", "/dir2")
|
||||
form.Set("patterns1", "*.zip")
|
||||
form.Set("pattern_type1", "denied")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
// passphrase cannot be empty
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
|
@ -7165,15 +7237,18 @@ func TestWebUserSFTPFsMock(t *testing.T) {
|
|||
form.Set("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("sub_dirs_permissions", "")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("allowed_ip", "")
|
||||
form.Set("denied_ip", "")
|
||||
form.Set("fs_provider", "5")
|
||||
form.Set("crypt_passphrase", "")
|
||||
form.Set("allowed_patterns", "/dir1::*.jpg,*.png")
|
||||
form.Set("denied_patterns", "/dir2::*.zip")
|
||||
form.Set("pattern_path0", "/dir1")
|
||||
form.Set("patterns0", "*.jpg,*.png")
|
||||
form.Set("pattern_type0", "allowed")
|
||||
form.Set("pattern_path1", "/dir2")
|
||||
form.Set("patterns1", "*.zip")
|
||||
form.Set("pattern_type1", "denied")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
// empty sftpconfig
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
|
|
32
httpd/web.go
|
@ -1,10 +1,7 @@
|
|||
package httpd
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -38,32 +35,3 @@ func getSliceFromDelimitedValues(values, delimiter string) []string {
|
|||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getListFromPostFields(value string) map[string][]string {
|
||||
result := make(map[string][]string)
|
||||
for _, cleaned := range getSliceFromDelimitedValues(value, "\n") {
|
||||
if strings.Contains(cleaned, "::") {
|
||||
dirExts := strings.Split(cleaned, "::")
|
||||
if len(dirExts) > 1 {
|
||||
dir := dirExts[0]
|
||||
dir = path.Clean(strings.TrimSpace(dir))
|
||||
exts := []string{}
|
||||
for _, e := range strings.Split(dirExts[1], ",") {
|
||||
cleanedExt := strings.TrimSpace(e)
|
||||
if cleanedExt != "" {
|
||||
exts = append(exts, cleanedExt)
|
||||
}
|
||||
}
|
||||
if dir != "" {
|
||||
if _, ok := result[dir]; ok {
|
||||
result[dir] = append(result[dir], exts...)
|
||||
} else {
|
||||
result[dir] = exts
|
||||
}
|
||||
result[dir] = utils.RemoveDuplicates(result[dir])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -137,6 +137,7 @@ type userPage struct {
|
|||
RootDirPerms []string
|
||||
RedactedSecret string
|
||||
Mode userPageMode
|
||||
VirtualFolders []vfs.BaseVirtualFolder
|
||||
}
|
||||
|
||||
type adminPage struct {
|
||||
|
@ -388,6 +389,10 @@ func renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dat
|
|||
}
|
||||
|
||||
func renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.User, mode userPageMode, error string) {
|
||||
folders, err := getWebVirtualFolders(w, r, defaultQueryLimit)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
user.SetEmptySecretsIfNil()
|
||||
var title, currentURL string
|
||||
switch mode {
|
||||
|
@ -415,6 +420,7 @@ func renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.U
|
|||
ValidProtocols: dataprovider.ValidProtocols,
|
||||
WebClientOptions: dataprovider.WebClientOptions,
|
||||
RootDirPerms: user.GetPermissionsForPath("/"),
|
||||
VirtualFolders: folders,
|
||||
}
|
||||
renderAdminTemplate(w, templateUser, data)
|
||||
}
|
||||
|
@ -446,9 +452,13 @@ func renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVir
|
|||
|
||||
func getFoldersForTemplate(r *http.Request) []string {
|
||||
var res []string
|
||||
formValue := r.Form.Get("folders")
|
||||
folderNames := r.Form["tpl_foldername"]
|
||||
folders := make(map[string]bool)
|
||||
for _, name := range getSliceFromDelimitedValues(formValue, "\n") {
|
||||
for _, name := range folderNames {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := folders[name]; ok {
|
||||
continue
|
||||
}
|
||||
|
@ -460,17 +470,20 @@ func getFoldersForTemplate(r *http.Request) []string {
|
|||
|
||||
func getUsersForTemplate(r *http.Request) []userTemplateFields {
|
||||
var res []userTemplateFields
|
||||
formValue := r.Form.Get("users")
|
||||
tplUsernames := r.Form["tpl_username"]
|
||||
tplPasswords := r.Form["tpl_password"]
|
||||
tplPublicKeys := r.Form["tpl_public_keys"]
|
||||
|
||||
users := make(map[string]bool)
|
||||
for _, cleaned := range getSliceFromDelimitedValues(formValue, "\n") {
|
||||
if strings.Contains(cleaned, "::") {
|
||||
mapping := strings.Split(cleaned, "::")
|
||||
if len(mapping) > 1 {
|
||||
username := strings.TrimSpace(mapping[0])
|
||||
password := strings.TrimSpace(mapping[1])
|
||||
var publicKey string
|
||||
if len(mapping) > 2 {
|
||||
publicKey = strings.TrimSpace(mapping[2])
|
||||
for idx, username := range tplUsernames {
|
||||
username = strings.TrimSpace(username)
|
||||
password := ""
|
||||
publicKey := ""
|
||||
if len(tplPasswords) > idx {
|
||||
password = strings.TrimSpace(tplPasswords[idx])
|
||||
}
|
||||
if len(tplPublicKeys) > idx {
|
||||
publicKey = strings.TrimSpace(tplPublicKeys[idx])
|
||||
}
|
||||
if username == "" || (password == "" && publicKey == "") {
|
||||
continue
|
||||
|
@ -486,14 +499,48 @@ func getUsersForTemplate(r *http.Request) []userTemplateFields {
|
|||
PublicKey: publicKey,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {
|
||||
var virtualFolders []vfs.VirtualFolder
|
||||
formValue := r.Form.Get("virtual_folders")
|
||||
folderPaths := r.Form["vfolder_path"]
|
||||
folderNames := r.Form["vfolder_name"]
|
||||
folderQuotaSizes := r.Form["vfolder_quota_size"]
|
||||
folderQuotaFiles := r.Form["vfolder_quota_files"]
|
||||
for idx, p := range folderPaths {
|
||||
p = strings.TrimSpace(p)
|
||||
name := ""
|
||||
if len(folderNames) > idx {
|
||||
name = folderNames[idx]
|
||||
}
|
||||
if p != "" && name != "" {
|
||||
vfolder := vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
Name: name,
|
||||
},
|
||||
VirtualPath: p,
|
||||
QuotaFiles: -1,
|
||||
QuotaSize: -1,
|
||||
}
|
||||
if len(folderQuotaSizes) > idx {
|
||||
quotaSize, err := strconv.ParseInt(strings.TrimSpace(folderQuotaSizes[idx]), 10, 64)
|
||||
if err == nil {
|
||||
vfolder.QuotaSize = quotaSize
|
||||
}
|
||||
}
|
||||
if len(folderQuotaFiles) > idx {
|
||||
quotaFiles, err := strconv.Atoi(strings.TrimSpace(folderQuotaFiles[idx]))
|
||||
if err == nil {
|
||||
vfolder.QuotaFiles = quotaFiles
|
||||
}
|
||||
}
|
||||
virtualFolders = append(virtualFolders, vfolder)
|
||||
}
|
||||
}
|
||||
|
||||
/*formValue := r.Form.Get("virtual_folders")
|
||||
for _, cleaned := range getSliceFromDelimitedValues(formValue, "\n") {
|
||||
if strings.Contains(cleaned, "::") {
|
||||
mapping := strings.Split(cleaned, "::")
|
||||
|
@ -521,49 +568,58 @@ func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {
|
|||
virtualFolders = append(virtualFolders, vfolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
return virtualFolders
|
||||
}
|
||||
|
||||
func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
|
||||
permissions := make(map[string][]string)
|
||||
permissions["/"] = r.Form["permissions"]
|
||||
subDirsPermsValue := r.Form.Get("sub_dirs_permissions")
|
||||
for _, cleaned := range getSliceFromDelimitedValues(subDirsPermsValue, "\n") {
|
||||
if strings.Contains(cleaned, "::") {
|
||||
dirPerms := strings.Split(cleaned, "::")
|
||||
if len(dirPerms) > 1 {
|
||||
dir := dirPerms[0]
|
||||
dir = strings.TrimSpace(dir)
|
||||
perms := []string{}
|
||||
for _, p := range strings.Split(dirPerms[1], ",") {
|
||||
cleanedPerm := strings.TrimSpace(p)
|
||||
if cleanedPerm != "" {
|
||||
perms = append(perms, cleanedPerm)
|
||||
}
|
||||
}
|
||||
if dir != "" {
|
||||
permissions[dir] = perms
|
||||
}
|
||||
|
||||
for k := range r.Form {
|
||||
if strings.HasPrefix(k, "sub_perm_path") {
|
||||
p := strings.TrimSpace(r.Form.Get(k))
|
||||
if p != "" {
|
||||
idx := strings.TrimPrefix(k, "sub_perm_path")
|
||||
permissions[p] = r.Form[fmt.Sprintf("sub_perm_permissions%v", idx)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return permissions
|
||||
}
|
||||
|
||||
func getFilePatternsFromPostField(valueAllowed, valuesDenied string) []dataprovider.PatternsFilter {
|
||||
func getFilePatternsFromPostField(r *http.Request) []dataprovider.PatternsFilter {
|
||||
var result []dataprovider.PatternsFilter
|
||||
allowedPatterns := getListFromPostFields(valueAllowed)
|
||||
deniedPatterns := getListFromPostFields(valuesDenied)
|
||||
|
||||
allowedPatterns := make(map[string][]string)
|
||||
deniedPatterns := make(map[string][]string)
|
||||
|
||||
for k := range r.Form {
|
||||
if strings.HasPrefix(k, "pattern_path") {
|
||||
p := strings.TrimSpace(r.Form.Get(k))
|
||||
idx := strings.TrimPrefix(k, "pattern_path")
|
||||
filters := strings.TrimSpace(r.Form.Get(fmt.Sprintf("patterns%v", idx)))
|
||||
filters = strings.ReplaceAll(filters, " ", "")
|
||||
patternType := r.Form.Get(fmt.Sprintf("pattern_type%v", idx))
|
||||
if p != "" && filters != "" {
|
||||
if patternType == "allowed" {
|
||||
allowedPatterns[p] = append(allowedPatterns[p], strings.Split(filters, ",")...)
|
||||
} else {
|
||||
deniedPatterns[p] = append(deniedPatterns[p], strings.Split(filters, ",")...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for dirAllowed, allowPatterns := range allowedPatterns {
|
||||
filter := dataprovider.PatternsFilter{
|
||||
Path: dirAllowed,
|
||||
AllowedPatterns: allowPatterns,
|
||||
AllowedPatterns: utils.RemoveDuplicates(allowPatterns),
|
||||
}
|
||||
for dirDenied, denPatterns := range deniedPatterns {
|
||||
if dirAllowed == dirDenied {
|
||||
filter.DeniedPatterns = denPatterns
|
||||
filter.DeniedPatterns = utils.RemoveDuplicates(denPatterns)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -593,7 +649,7 @@ func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters {
|
|||
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
|
||||
filters.DeniedLoginMethods = r.Form["ssh_login_methods"]
|
||||
filters.DeniedProtocols = r.Form["denied_protocols"]
|
||||
filters.FilePatterns = getFilePatternsFromPostField(r.Form.Get("allowed_patterns"), r.Form.Get("denied_patterns"))
|
||||
filters.FilePatterns = getFilePatternsFromPostField(r)
|
||||
filters.TLSUsername = dataprovider.TLSUsername(r.Form.Get("tls_username"))
|
||||
filters.WebClient = r.Form["web_client_options"]
|
||||
hooks := r.Form["hooks"]
|
||||
|
@ -885,8 +941,6 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
|||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
publicKeysFormValue := r.Form.Get("public_keys")
|
||||
publicKeys := getSliceFromDelimitedValues(publicKeysFormValue, "\n")
|
||||
uid, err := strconv.Atoi(r.Form.Get("uid"))
|
||||
if err != nil {
|
||||
return user, err
|
||||
|
@ -935,7 +989,7 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
|||
user = dataprovider.User{
|
||||
Username: r.Form.Get("username"),
|
||||
Password: r.Form.Get("password"),
|
||||
PublicKeys: publicKeys,
|
||||
PublicKeys: r.Form["public_keys"],
|
||||
HomeDir: r.Form.Get("home_dir"),
|
||||
VirtualFolders: getVirtualFoldersFromPostFields(r),
|
||||
UID: uid,
|
||||
|
@ -1226,7 +1280,7 @@ func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
|
|||
func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
templateFolder := vfs.BaseVirtualFolder{}
|
||||
err := r.ParseForm()
|
||||
err := r.ParseMultipartForm(maxRequestSize)
|
||||
if err != nil {
|
||||
renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "")
|
||||
return
|
||||
|
@ -1527,6 +1581,22 @@ func handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
|
|||
http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func getWebVirtualFolders(w http.ResponseWriter, r *http.Request, limit int) ([]vfs.BaseVirtualFolder, error) {
|
||||
folders := make([]vfs.BaseVirtualFolder, 0, limit)
|
||||
for {
|
||||
f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC)
|
||||
if err != nil {
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
return folders, err
|
||||
}
|
||||
folders = append(folders, f...)
|
||||
if len(f) < limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
|
||||
limit := defaultQueryLimit
|
||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||
|
@ -1536,18 +1606,10 @@ func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
|
|||
limit = defaultQueryLimit
|
||||
}
|
||||
}
|
||||
folders := make([]vfs.BaseVirtualFolder, 0, limit)
|
||||
for {
|
||||
f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC)
|
||||
folders, err := getWebVirtualFolders(w, r, limit)
|
||||
if err != nil {
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
folders = append(folders, f...)
|
||||
if len(f) < limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
data := foldersPage{
|
||||
basePage: getBasePageData(pageFoldersTitle, webFoldersPath, r),
|
||||
|
|
|
@ -418,14 +418,14 @@ func (c *sshCommand) isSystemCommandAllowed() error {
|
|||
for _, f := range c.connection.User.Filters.FilePatterns {
|
||||
if f.Path == sshDestPath {
|
||||
c.connection.Log(logger.LevelDebug,
|
||||
"command %#v is not allowed inside folders with files patterns filters %#v user %#v",
|
||||
"command %#v is not allowed inside folders with file patterns filters %#v user %#v",
|
||||
c.command, sshDestPath, c.connection.User.Username)
|
||||
return errUnsupportedConfig
|
||||
}
|
||||
if len(sshDestPath) > len(f.Path) {
|
||||
if strings.HasPrefix(sshDestPath, f.Path+"/") || f.Path == "/" {
|
||||
c.connection.Log(logger.LevelDebug,
|
||||
"command %#v is not allowed it includes folders with files patterns filters %#v user %#v",
|
||||
"command %#v is not allowed it includes folders with file patterns filters %#v user %#v",
|
||||
c.command, sshDestPath, c.connection.User.Username)
|
||||
return errUnsupportedConfig
|
||||
}
|
||||
|
@ -433,7 +433,7 @@ func (c *sshCommand) isSystemCommandAllowed() error {
|
|||
if len(sshDestPath) < len(f.Path) {
|
||||
if strings.HasPrefix(sshDestPath+"/", f.Path) || sshDestPath == "/" {
|
||||
c.connection.Log(logger.LevelDebug,
|
||||
"command %#v is not allowed inside folder with files extensions filters %#v user %#v",
|
||||
"command %#v is not allowed inside folder with file patterns filters %#v user %#v",
|
||||
c.command, sshDestPath, c.connection.User.Username)
|
||||
return errUnsupportedConfig
|
||||
}
|
||||
|
|
|
@ -29,14 +29,31 @@
|
|||
{{end}}
|
||||
<form id="folder_form" enctype="multipart/form-data" action="{{.CurrentURL}}" method="POST" autocomplete="off" {{if eq .Mode 3}}target="_blank"{{end}}>
|
||||
{{if eq .Mode 3}}
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
Folders
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<label for="idFolders" class="col-sm-2 col-form-label">Folders</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idFolders" name="folders" rows="5" required
|
||||
aria-describedby="foldersHelpBlock"></textarea>
|
||||
<small id="foldersHelpBlock" class="form-text text-muted">
|
||||
Specify the folder names, one for line.
|
||||
</small>
|
||||
<div class="col-md-12 form_field_tpl_folders_outer">
|
||||
<div class="row form_field_tpl_folder_outer_row">
|
||||
<div class="form-group col-md-10">
|
||||
<input type="text" class="form-control" id="idTplFolder0" name="tpl_foldername" placeholder="Folder name" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<button class="btn btn-circle btn-danger remove_tpl_folder_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_tpl_folder_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new folder name
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="name" id="idFolderName" value="{{.Folder.Name}}">
|
||||
|
@ -83,6 +100,30 @@
|
|||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
onFilesystemChanged('{{.Folder.FsConfig.Provider}}');
|
||||
|
||||
$("body").on("click", ".add_new_tpl_folder_field_btn", function () {
|
||||
var index = $(".form_field_tpl_folders_outer").find(".form_field_tpl_folder_outer_row").length;
|
||||
while (document.getElementById("idTplFolder"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_tpl_folders_outer").append(`
|
||||
<div class="row form_field_tpl_folder_outer_row">
|
||||
<div class="form-group col-md-10">
|
||||
<input type="text" class="form-control" id="idTplFolder${index}" name="tpl_foldername" placeholder="Folder name" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<button class="btn btn-circle btn-danger remove_tpl_folder_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_tpl_folder_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_tpl_folder_outer_row").remove();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
{{template "fsjs"}}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{{define "fshtml"}}
|
||||
<div class="form-group row">
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-body pb-1">
|
||||
<div class="form-group row">
|
||||
<label for="idFilesystem" class="col-sm-2 col-form-label">Storage</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idFilesystem" name="fs_provider"
|
||||
|
@ -12,9 +14,9 @@
|
|||
<option value="5" {{if eq .Provider 5 }}selected{{end}}>SFTP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row s3">
|
||||
<div class="form-group row s3">
|
||||
<label for="idS3Bucket" class="col-sm-2 col-form-label">Bucket</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idS3Bucket" name="s3_bucket" placeholder=""
|
||||
|
@ -26,9 +28,9 @@
|
|||
<input type="text" class="form-control" id="idS3Region" name="s3_region" placeholder=""
|
||||
value="{{.S3Config.Region}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row s3">
|
||||
<div class="form-group row s3">
|
||||
<label for="idS3AccessKey" class="col-sm-2 col-form-label">Access Key</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idS3AccessKey" name="s3_access_key" placeholder=""
|
||||
|
@ -37,14 +39,13 @@
|
|||
<div class="col-sm-2"></div>
|
||||
<label for="idS3AccessSecret" class="col-sm-2 col-form-label">Access Secret</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="password" class="form-control" id="idS3AccessSecret" name="s3_access_secret"
|
||||
placeholder=""
|
||||
<input type="password" class="form-control" id="idS3AccessSecret" name="s3_access_secret" placeholder=""
|
||||
value="{{if .S3Config.AccessSecret.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.S3Config.AccessSecret.GetPayload}}{{end}}"
|
||||
maxlength="1000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row s3">
|
||||
<div class="form-group row s3">
|
||||
<label for="idS3StorageClass" class="col-sm-2 col-form-label">Storage Class</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idS3StorageClass" name="s3_storage_class" placeholder=""
|
||||
|
@ -56,14 +57,13 @@
|
|||
<input type="text" class="form-control" id="idS3Endpoint" name="s3_endpoint" placeholder=""
|
||||
value="{{.S3Config.Endpoint}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row s3">
|
||||
<div class="form-group row s3">
|
||||
<label for="idS3PartSize" class="col-sm-2 col-form-label">UL Part Size (MB)</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idS3PartSize" name="s3_upload_part_size"
|
||||
placeholder="" value="{{.S3Config.UploadPartSize}}"
|
||||
aria-describedby="S3PartSizeHelpBlock">
|
||||
<input type="number" class="form-control" id="idS3PartSize" name="s3_upload_part_size" placeholder=""
|
||||
value="{{.S3Config.UploadPartSize}}" aria-describedby="S3PartSizeHelpBlock">
|
||||
<small id="S3PartSizeHelpBlock" class="form-text text-muted">
|
||||
The buffer size for multipart uploads. Zero means the default (5 MB). Minimum is 5
|
||||
</small>
|
||||
|
@ -78,29 +78,28 @@
|
|||
How many parts are uploaded in parallel. Zero means the default (2)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row s3">
|
||||
<div class="form-group row s3">
|
||||
<label for="idS3KeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idS3KeyPrefix" name="s3_key_prefix" placeholder=""
|
||||
value="{{.S3Config.KeyPrefix}}" maxlength="255"
|
||||
aria-describedby="S3KeyPrefixHelpBlock">
|
||||
value="{{.S3Config.KeyPrefix}}" maxlength="255" aria-describedby="S3KeyPrefixHelpBlock">
|
||||
<small id="S3KeyPrefixHelpBlock" class="form-text text-muted">
|
||||
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row gcs">
|
||||
<div class="form-group row gcs">
|
||||
<label for="idGCSBucket" class="col-sm-2 col-form-label">Bucket</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idGCSBucket" name="gcs_bucket" placeholder=""
|
||||
value="{{.GCSConfig.Bucket}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row gcs">
|
||||
<div class="form-group row gcs">
|
||||
<label for="idGCSCredentialFile" class="col-sm-2 col-form-label">Credentials file</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="file" class="form-control-file" id="idGCSCredentialFile" name="gcs_credential_file"
|
||||
|
@ -112,32 +111,31 @@
|
|||
<div class="col-sm-1"></div>
|
||||
<label for="idGCSStorageClass" class="col-sm-2 col-form-label">Storage Class</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idGCSStorageClass" name="gcs_storage_class"
|
||||
placeholder="" value="{{.GCSConfig.StorageClass}}" maxlength="255">
|
||||
<input type="text" class="form-control" id="idGCSStorageClass" name="gcs_storage_class" placeholder=""
|
||||
value="{{.GCSConfig.StorageClass}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group gcs">
|
||||
<div class="form-group gcs">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idGCSAutoCredentials"
|
||||
name="gcs_auto_credentials" {{if gt .GCSConfig.AutomaticCredentials 0}}checked{{end}}>
|
||||
<input type="checkbox" class="form-check-input" id="idGCSAutoCredentials" name="gcs_auto_credentials"
|
||||
{{if gt .GCSConfig.AutomaticCredentials 0}}checked{{end}}>
|
||||
<label for="idGCSAutoCredentials" class="form-check-label">Automatic credentials</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row gcs">
|
||||
<div class="form-group row gcs">
|
||||
<label for="idGCSKeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idGCSKeyPrefix" name="gcs_key_prefix" placeholder=""
|
||||
value="{{.GCSConfig.KeyPrefix}}" maxlength="255"
|
||||
aria-describedby="GCSKeyPrefixHelpBlock">
|
||||
value="{{.GCSConfig.KeyPrefix}}" maxlength="255" aria-describedby="GCSKeyPrefixHelpBlock">
|
||||
<small id="GCSKeyPrefixHelpBlock" class="form-text text-muted">
|
||||
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row azblob">
|
||||
<div class="form-group row azblob">
|
||||
<label for="idAzContainer" class="col-sm-2 col-form-label">Container</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idAzContainer" name="az_container" placeholder=""
|
||||
|
@ -149,50 +147,50 @@
|
|||
<input type="text" class="form-control" id="idAzAccountName" name="az_account_name" placeholder=""
|
||||
value="{{.AzBlobConfig.AccountName}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row azblob">
|
||||
<div class="form-group row azblob">
|
||||
<label for="idAzAccountKey" class="col-sm-2 col-form-label">Account Key</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idAzAccountKey" name="az_account_key" placeholder=""
|
||||
value="{{if .AzBlobConfig.AccountKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.AccountKey.GetPayload}}{{end}}"
|
||||
maxlength="1000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row azblob">
|
||||
<div class="form-group row azblob">
|
||||
<label for="idAzSASURL" class="col-sm-2 col-form-label">SAS URL</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idAzSASURL" name="az_sas_url" placeholder=""
|
||||
value="{{.AzBlobConfig.SASURL}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row azblob">
|
||||
</div>
|
||||
<div class="form-group row azblob">
|
||||
<label for="idAzEndpoint" class="col-sm-2 col-form-label">Endpoint</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idAzEndpoint" name="az_endpoint" placeholder=""
|
||||
value="{{.AzBlobConfig.Endpoint}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row azblob">
|
||||
<div class="form-group row azblob">
|
||||
<label for="idAzAccessTier" class="col-sm-2 col-form-label">Access Tier</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idAzAccessTier" name="az_access_tier">
|
||||
<option value="" {{if eq .AzBlobConfig.AccessTier "" }}selected{{end}}>Default</option>
|
||||
<option value="Hot" {{if eq .AzBlobConfig.AccessTier "Hot" }}selected{{end}}>Hot</option>
|
||||
<option value="Cool" {{if eq .AzBlobConfig.AccessTier "Cool" }}selected{{end}}>Cool</option>
|
||||
<option value="Archive" {{if eq .AzBlobConfig.AccessTier "Archive"}}selected{{end}}>Archive</option>
|
||||
<option value="Archive" {{if eq .AzBlobConfig.AccessTier "Archive" }}selected{{end}}>Archive
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row azblob">
|
||||
<div class="form-group row azblob">
|
||||
<label for="idAzPartSize" class="col-sm-2 col-form-label">UL Part Size (MB)</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idAzPartSize" name="az_upload_part_size"
|
||||
placeholder="" value="{{.AzBlobConfig.UploadPartSize}}"
|
||||
aria-describedby="AzPartSizeHelpBlock">
|
||||
<input type="number" class="form-control" id="idAzPartSize" name="az_upload_part_size" placeholder=""
|
||||
value="{{.AzBlobConfig.UploadPartSize}}" aria-describedby="AzPartSizeHelpBlock">
|
||||
<small id="AzPartSizeHelpBlock" class="form-text text-muted">
|
||||
The buffer size for multipart uploads. Zero means the default (4 MB)
|
||||
</small>
|
||||
|
@ -207,28 +205,28 @@
|
|||
How many parts are uploaded in parallel. Zero means the default (2)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row azblob">
|
||||
<div class="form-group row azblob">
|
||||
<label for="idAzKeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idAzKeyPrefix" name="az_key_prefix" placeholder=""
|
||||
value="{{.AzBlobConfig.KeyPrefix}}" maxlength="255"
|
||||
aria-describedby="AzKeyPrefixHelpBlock">
|
||||
value="{{.AzBlobConfig.KeyPrefix}}" maxlength="255" aria-describedby="AzKeyPrefixHelpBlock">
|
||||
<small id="AzKeyPrefixHelpBlock" class="form-text text-muted">
|
||||
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group azblob">
|
||||
<div class="form-group azblob">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idUseEmulator" name="az_use_emulator" {{if .AzBlobConfig.UseEmulator}}checked{{end}}>
|
||||
<input type="checkbox" class="form-check-input" id="idUseEmulator" name="az_use_emulator" {{if
|
||||
.AzBlobConfig.UseEmulator}}checked{{end}}>
|
||||
<label for="idUseEmulator" class="form-check-label">Use Azure Blob emulator</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row crypt">
|
||||
<div class="form-group row crypt">
|
||||
<label for="idCryptPassphrase" class="col-sm-2 col-form-label">Passphrase</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idCryptPassphrase" name="crypt_passphrase"
|
||||
|
@ -236,9 +234,9 @@
|
|||
value="{{if .CryptConfig.Passphrase.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.CryptConfig.Passphrase.GetPayload}}{{end}}"
|
||||
maxlength="1000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row sftp">
|
||||
<div class="form-group row sftp">
|
||||
<label for="idSFTPEndpoint" class="col-sm-2 col-form-label">Endpoint</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idSFTPEndpoint" name="sftp_endpoint" placeholder=""
|
||||
|
@ -256,9 +254,9 @@
|
|||
A buffer size > 0 enables concurrent transfers
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row sftp">
|
||||
<div class="form-group row sftp">
|
||||
<label for="idSFTPUsername" class="col-sm-2 col-form-label">Username</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idSFTPUsername" name="sftp_username" placeholder=""
|
||||
|
@ -271,17 +269,17 @@
|
|||
value="{{if .SFTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.Password.GetPayload}}{{end}}"
|
||||
maxlength="1000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row sftp">
|
||||
<div class="form-group row sftp">
|
||||
<label for="idSFTPPrivateKey" class="col-sm-2 col-form-label">Private key</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea type="password" class="form-control" id="idSFTPPrivateKey" name="sftp_private_key"
|
||||
rows="3">{{if .SFTPConfig.PrivateKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.PrivateKey.GetPayload}}{{end}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row sftp">
|
||||
<div class="form-group row sftp">
|
||||
<label for="idSFTPFingerprints" class="col-sm-2 col-form-label">Fingerprints</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idSFTPFingerprints" name="sftp_fingerprints" rows="3"
|
||||
|
@ -291,25 +289,27 @@
|
|||
empty any host key will be accepted: this is a security risk!
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row sftp">
|
||||
<div class="form-group row sftp">
|
||||
<label for="idSFTPPrefix" class="col-sm-2 col-form-label">Prefix</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idSFTPPrefix" name="sftp_prefix" placeholder=""
|
||||
value="{{.SFTPConfig.Prefix}}" maxlength="255"
|
||||
aria-describedby="SFTPPrefixHelpBlock">
|
||||
value="{{.SFTPConfig.Prefix}}" maxlength="255" aria-describedby="SFTPPrefixHelpBlock">
|
||||
<small id="SFTPPrefixHelpBlock" class="form-text text-muted">
|
||||
Similar to a chroot for local filesystem. Example: "/somedir/subdir".
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group sftp">
|
||||
<div class="form-group sftp">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idDisableConcurrentReads" name="sftp_disable_concurrent_reads" {{if .SFTPConfig.DisableCouncurrentReads}}checked{{end}}>
|
||||
<input type="checkbox" class="form-check-input" id="idDisableConcurrentReads"
|
||||
name="sftp_disable_concurrent_reads" {{if .SFTPConfig.DisableCouncurrentReads}}checked{{end}}>
|
||||
<label for="idDisableConcurrentReads" class="form-check-label">Disable concurrent reads</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
|
|
@ -40,14 +40,39 @@
|
|||
{{end}}
|
||||
<form id="user_form" enctype="multipart/form-data" action="{{.CurrentURL}}" method="POST" autocomplete="off" {{if eq .Mode 3}}target="_blank"{{end}}>
|
||||
{{if eq .Mode 3}}
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
Users
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-4">For each user set the username and at least one of the password and public key</h6>
|
||||
<div class="form-group row">
|
||||
<label for="idUsers" class="col-sm-2 col-form-label">Users</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idUsers" name="users" rows="5" required
|
||||
aria-describedby="usersHelpBlock"></textarea>
|
||||
<small id="usersHelpBlock" class="form-text text-muted">
|
||||
Specify the username and at least one of the password and public key. Each line must be username::password::public-key
|
||||
</small>
|
||||
<div class="col-md-12 form_field_tpl_users_outer">
|
||||
<div class="row form_field_tpl_user_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idTplUsername0" name="tpl_username" placeholder="Username" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="password" class="form-control" id="idTplPassword0" name="tpl_password" placeholder="Password" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<textarea class="form-control" id="idTplPublicKey0" name="tpl_public_keys" rows="5"
|
||||
placeholder="Paste your public key here"></textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_tpl_user_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_tpl_user_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new user
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="username" id="idUsername" value="{{.User.Username}}">
|
||||
|
@ -100,14 +125,46 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
Public keys
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<label for="idPublicKeys" class="col-sm-2 col-form-label">Public keys</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idPublicKeys" name="public_keys" rows="3"
|
||||
aria-describedby="pkHelpBlock">{{range .User.PublicKeys}}{{.}} {{end}}</textarea>
|
||||
<small id="pkHelpBlock" class="form-text text-muted">
|
||||
One public key or SSH user certificate per line
|
||||
</small>
|
||||
<div class="col-md-12 form_field_pk_outer">
|
||||
{{range $idx, $val := .User.PublicKeys}}
|
||||
<div class="row form_field_pk_outer_row">
|
||||
<div class="form-group col-md-11">
|
||||
<textarea class="form-control" id="idPublicKey{{$idx}}" name="public_keys" rows="3"
|
||||
placeholder="Paste your public key here">{{$val}}</textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_pk_outer_row">
|
||||
<div class="form-group col-md-11">
|
||||
<textarea class="form-control" id="idPublicKey0" name="public_keys" rows="3"
|
||||
placeholder="Paste your public key here"></textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_pk_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new public key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -125,69 +182,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idProtocols" class="col-sm-2 col-form-label">Denied protocols</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idProtocols" name="denied_protocols" multiple>
|
||||
{{range $protocol := .ValidProtocols}}
|
||||
<option value="{{$protocol}}" {{range $p :=$.User.Filters.DeniedProtocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idLoginMethods" class="col-sm-2 col-form-label">Denied login methods</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idLoginMethods" name="ssh_login_methods" multiple>
|
||||
{{range $method := .ValidLoginMethods}}
|
||||
<option value="{{$method}}" {{range $m :=$.User.Filters.DeniedLoginMethods }}{{if eq $m $method}}selected{{end}}{{end}}>{{$method}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idPermissions" class="col-sm-2 col-form-label">Permissions</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idPermissions" name="permissions" required multiple>
|
||||
{{range $validPerm := .ValidPerms}}
|
||||
<option value="{{$validPerm}}" {{range $perm :=$.RootDirPerms }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idSubDirsPermissions" class="col-sm-2 col-form-label">Sub dirs permissions</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idSubDirsPermissions" name="sub_dirs_permissions" rows="3"
|
||||
aria-describedby="subDirsHelpBlock">{{range $dir, $perms := .User.Permissions -}}
|
||||
{{if ne $dir "/" -}}
|
||||
{{$dir}}::{{range $index, $p := $perms}}{{if $index}},{{end}}{{$p}}{{end}}
|
||||
{{- end}}
|
||||
{{- end}}</textarea>
|
||||
<small id="subDirsHelpBlock" class="form-text text-muted">
|
||||
One exposed virtual directory path per line as /dir::perms, for example /somedir::list,download
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idWebClient" class="col-sm-2 col-form-label">Web client</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idWebClient" name="web_client_options" multiple>
|
||||
{{range $option := .WebClientOptions}}
|
||||
<option value="{{$option}}" {{range $p :=$.User.Filters.WebClient }}{{if eq $p $option}}selected{{end}}{{end}}>{{$option}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idHomeDir" class="col-sm-2 col-form-label">Home Dir</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -197,29 +191,172 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idVirtualFolders" class="col-sm-2 col-form-label">Virtual folders</label>
|
||||
<label for="idPermissions" class="col-sm-2 col-form-label">Permissions</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idVirtualFolders" name="virtual_folders" rows="3"
|
||||
aria-describedby="vfHelpBlock">{{range $index, $mapping := .User.VirtualFolders -}}
|
||||
{{$mapping.VirtualPath}}::{{$mapping.Name}}::{{$mapping.QuotaFiles}}::{{$mapping.QuotaSize}}
|
||||
{{- end}}</textarea>
|
||||
<small id="vfHelpBlock" class="form-text text-muted">
|
||||
One mapping per line as vpath::folder-name::[quota_files]::[quota_size(bytes)], for example
|
||||
/vdir::afolder or /vdir::afolder::10::104857600. Quota -1 means included inside user quota.
|
||||
<select class="form-control" id="idPermissions" name="permissions" required multiple>
|
||||
{{range $validPerm := .ValidPerms}}
|
||||
<option value="{{$validPerm}}" {{range $perm :=$.RootDirPerms }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "fshtml" .User.FsConfig}}
|
||||
{{if .VirtualFolders}}
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
Virtual folders
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-4">Quota -1 means included within user quota, 0 unlimited. Don't set -1 for shared folders</h6>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_vfolders_outer">
|
||||
{{range $idx, $val := .User.VirtualFolders}}
|
||||
<div class="row form_field_vfolder_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idVolderPath{{$idx}}" name="vfolder_path" placeholder="mount path, i.e. /vfolder" value="{{$val.VirtualPath}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idVfolderName{{$idx}}" name="vfolder_name">
|
||||
<option value=""></option>
|
||||
{{range $.VirtualFolders}}
|
||||
<option value="{{.Name}}" {{if eq $val.Name .Name}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaSize{{$idx}}" name="vfolder_quota_size"
|
||||
value="{{$val.QuotaSize}}" min="-1" aria-describedby="vqsHelpBlock{{$idx}}">
|
||||
<small id="vqsHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
Quota size (bytes)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaFiles{{$idx}}" name="vfolder_quota_files"
|
||||
value="{{$val.QuotaFiles}}" min="-1" aria-describedby="vqfHelpBlock{{$idx}}">
|
||||
<small id="vqfHelpBlock{{$idx}}" class="form-text text-muted">
|
||||
Quota files
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_vfolder_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_vfolder_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idVolderPath0" name="vfolder_path" placeholder="mount path, i.e. /vfolder" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idVfolderName0" name="vfolder_name">
|
||||
<option value=""></option>
|
||||
{{range .VirtualFolders}}
|
||||
<option value="{{.Name}}">{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaSize0" name="vfolder_quota_size"
|
||||
value="" min="-1" aria-describedby="vqsHelpBlock0">
|
||||
<small id="vqsHelpBlock0" class="form-text text-muted">
|
||||
Quota size (bytes)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaFiles0" name="vfolder_quota_files"
|
||||
value="" min="-1" aria-describedby="vqfHelpBlock0">
|
||||
<small id="vqfHelpBlock0" class="form-text text-muted">
|
||||
Quota files
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_vfolder_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_vfolder_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new virtual folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
Per-directory permissions
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_dirperms_outer">
|
||||
{{range $idx, $dirPerms := .User.GetSubDirPermissions -}}
|
||||
<div class="row form_field_dirperms_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<input type="text" class="form-control" id="idSubDirPermsPath{{$idx}}" name="sub_perm_path{{$idx}}" placeholder="directory path, i.e. /dir" value="{{$dirPerms.Path}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idSubDirPermissions{{$idx}}" name="sub_perm_permissions{{$idx}}" multiple>
|
||||
{{range $validPerm := $.ValidPerms}}
|
||||
<option value="{{$validPerm}}" {{range $perm := $dirPerms.Permissions }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_dirperms_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<input type="text" class="form-control" id="idSubDirPermsPath0" name="sub_perm_path0" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idSubDirPermissions0" name="sub_perm_permissions0" multiple>
|
||||
{{range $validPerm := .ValidPerms}}
|
||||
<option value="{{$validPerm}}">{{$validPerm}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_dirperms_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new directory permissions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idDisableFsChecks" name="disable_fs_checks"
|
||||
{{if .User.Filters.DisableFsChecks}}checked{{end}} aria-describedby="disableFsChecksHelpBlock">
|
||||
<label for="idDisableFsChecks" class="form-check-label">Disable filesystem checks</label>
|
||||
<small id="disableFsChecksHelpBlock" class="form-text text-muted">
|
||||
Disable checks for existence and automatic creation of home directory and virtual folders
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idQuotaFiles" class="col-sm-2 col-form-label">Quota files</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idQuotaFiles" name="quota_files" placeholder=""
|
||||
value="{{.User.QuotaFiles}}" min="0" aria-describedby="qfHelpBlock">
|
||||
<small id="qfHelpBlock" class="form-text text-muted">
|
||||
0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idQuotaSize" class="col-sm-2 col-form-label">Quota size (bytes)</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idQuotaSize" name="quota_size" placeholder=""
|
||||
|
@ -228,6 +365,15 @@
|
|||
0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idQuotaFiles" class="col-sm-2 col-form-label">Quota files</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="number" class="form-control" id="idQuotaFiles" name="quota_files" placeholder=""
|
||||
value="{{.User.QuotaFiles}}" min="0" aria-describedby="qfHelpBlock">
|
||||
<small id="qfHelpBlock" class="form-text text-muted">
|
||||
0 means no limit
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
|
@ -285,6 +431,30 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idProtocols" class="col-sm-2 col-form-label">Denied protocols</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idProtocols" name="denied_protocols" multiple>
|
||||
{{range $protocol := .ValidProtocols}}
|
||||
<option value="{{$protocol}}" {{range $p :=$.User.Filters.DeniedProtocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idLoginMethods" class="col-sm-2 col-form-label">Denied login methods</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idLoginMethods" name="ssh_login_methods" multiple>
|
||||
{{range $method := .ValidLoginMethods}}
|
||||
<option value="{{$method}}" {{range $m :=$.User.Filters.DeniedLoginMethods }}{{if eq $m $method}}selected{{end}}{{end}}>{{$method}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idDeniedIP" class="col-sm-2 col-form-label">Denied IP/Mask</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -307,35 +477,75 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
Per-directory file patterns
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-4">Comma separated denied or allowed files, based on shell patterns</h6>
|
||||
<div class="form-group row">
|
||||
<label for="idFilePatternsDenied" class="col-sm-2 col-form-label">Denied file patterns</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idFilePatternsDenied" name="denied_patterns" rows="3"
|
||||
aria-describedby="deniedPatternsHelpBlock">{{range $index, $filter := .User.Filters.FilePatterns -}}
|
||||
{{if $filter.DeniedPatterns -}}
|
||||
{{$filter.Path}}::{{range $idx, $p := $filter.DeniedPatterns}}{{if $idx}},{{end}}{{$p}}{{end}}
|
||||
{{- end}}
|
||||
{{- end}}</textarea>
|
||||
<small id="deniedPatternsHelpBlock" class="form-text text-muted">
|
||||
One exposed virtual directory per line as /dir::pattern1,pattern2, for example
|
||||
/subdir::*.zip,*.rar
|
||||
</small>
|
||||
<div class="col-md-12 form_field_patterns_outer">
|
||||
{{range $idx, $pattern := .User.GetFlatFilePatterns -}}
|
||||
<div class="row form_field_patterns_outer_row">
|
||||
<div class="form-group col-md-4">
|
||||
<input type="text" class="form-control" id="idPatternPath{{$idx}}" name="pattern_path{{$idx}}" placeholder="directory path, i.e. /dir" value="{{$pattern.Path}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<input type="text" class="form-control" id="idPatterns{{$idx}}" name="patterns{{$idx}}" placeholder="*.zip,?.txt" value="{{$pattern.GetCommaSeparatedPatterns}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternType{{$idx}}" name="pattern_type{{$idx}}">
|
||||
<option value="denied" {{if $pattern.IsDenied}}selected{{end}}>Denied</option>
|
||||
<option value="allowed" {{if $pattern.IsAllowed}}selected{{end}}>Allowed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_patterns_outer_row">
|
||||
<div class="form-group col-md-4">
|
||||
<input type="text" class="form-control" id="idPatternPath0" name="pattern_path0" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<input type="text" class="form-control" id="idPatterns0" name="patterns0" placeholder="*.zip,?.txt" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternType0" name="pattern_type0">
|
||||
<option value="denied">Denied</option>
|
||||
<option value="allowed">Allowed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-1">
|
||||
<button type="button" class="btn btn-secondary add_new_pattern_field_btn">
|
||||
<i class="fas fa-plus"></i> Add new file pattern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idFilePatternsAllowed" class="col-sm-2 col-form-label">Allowed file patterns</label>
|
||||
<label for="idWebClient" class="col-sm-2 col-form-label">Web client</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idFilePatternsAllowed" name="allowed_patterns" rows="3"
|
||||
aria-describedby="allowedPatternsHelpBlock">{{range $index, $filter := .User.Filters.FilePatterns -}}
|
||||
{{if $filter.AllowedPatterns -}}
|
||||
{{$filter.Path}}::{{range $idx, $p := $filter.AllowedPatterns}}{{if $idx}},{{end}}{{$p}}{{end}}
|
||||
{{- end}}
|
||||
{{- end}}</textarea>
|
||||
<small id="allowedPatternsHelpBlock" class="form-text text-muted">
|
||||
One exposed virtual directory per line as /dir::pattern1,pattern2, for example
|
||||
/somedir::*.jpg,*.png
|
||||
</small>
|
||||
<select class="form-control" id="idWebClient" name="web_client_options" multiple>
|
||||
{{range $option := .WebClientOptions}}
|
||||
<option value="{{$option}}" {{range $p :=$.User.Filters.WebClient }}{{if eq $p $option}}selected{{end}}{{end}}>{{$option}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -356,19 +566,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idDisableFsChecks" name="disable_fs_checks"
|
||||
{{if .User.Filters.DisableFsChecks}}checked{{end}} aria-describedby="disableFsChecksHelpBlock">
|
||||
<label for="idDisableFsChecks" class="form-check-label">Disable filesystem checks</label>
|
||||
<small id="disableFsChecksHelpBlock" class="form-text text-muted">
|
||||
Disable checks for existence and automatic creation of home directory and virtual folders
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "fshtml" .User.FsConfig}}
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idAdditionalInfo" class="col-sm-2 col-form-label">Additional info</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -440,6 +637,169 @@
|
|||
|
||||
onFilesystemChanged('{{.User.FsConfig.Provider}}');
|
||||
|
||||
$("body").on("click", ".add_new_pk_field_btn", function () {
|
||||
var index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
|
||||
while (document.getElementById("idPublicKey"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_pk_outer").append(`
|
||||
<div class="row form_field_pk_outer_row">
|
||||
<div class="form-group col-md-11">
|
||||
<textarea class="form-control" id="idPublicKey${index}" name="public_keys" rows="3"
|
||||
placeholder="Paste your public key here"></textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_pk_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_pk_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_dirperms_field_btn", function () {
|
||||
var index = $(".form_field_dirperms_outer").find(".form_field_dirperms_outer_row").length;
|
||||
while (document.getElementById("idSubDirPermsPath"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_dirperms_outer").append(`
|
||||
<div class="row form_field_dirperms_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<input type="text" class="form-control" id="idSubDirPermsPath${index}" name="sub_perm_path${index}" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idSubDirPermissions${index}" name="sub_perm_permissions${index}" multiple>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
{{range .ValidPerms}}
|
||||
$("#idSubDirPermissions"+index).append($('<option>').val('{{.}}').text('{{.}}'));
|
||||
{{end}}
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_dirperms_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_dirperms_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_vfolder_field_btn", function () {
|
||||
var index = $(".form_field_vfolders_outer").find(".form_field_vfolder_outer_row").length;
|
||||
while (document.getElementById("idVolderPath"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_vfolders_outer").append(`
|
||||
<div class="row form_field_vfolder_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idVolderPath${index}" name="vfolder_path" placeholder="mount path, i.e. /vfolder" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idVfolderName${index}" name="vfolder_name">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaSize${index}" name="vfolder_quota_size"
|
||||
value="" min="-1" aria-describedby="vqsHelpBlock${index}">
|
||||
<small id="vqsHelpBlock${index}" class="form-text text-muted">
|
||||
Quota size (bytes)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<input type="number" class="form-control" id="idVfolderQuotaFiles${index}" name="vfolder_quota_files"
|
||||
value="" min="-1" aria-describedby="vqfHelpBlock${index}">
|
||||
<small id="vqfHelpBlock${index}" class="form-text text-muted">
|
||||
Quota files
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_vfolder_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
{{range .VirtualFolders}}
|
||||
$("#idVfolderName"+index).append($('<option>').val('{{.Name}}').text('{{.Name}}'));
|
||||
{{end}}
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_vfolder_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_vfolder_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_pattern_field_btn", function () {
|
||||
var index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
|
||||
while (document.getElementById("idPatternPath"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_patterns_outer").append(`
|
||||
<div class="row form_field_patterns_outer_row">
|
||||
<div class="form-group col-md-4">
|
||||
<input type="text" class="form-control" id="idPatternPath${index}" name="pattern_path${index}" placeholder="directory path, i.e. /dir" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<input type="text" class="form-control" id="idPatterns${index}" name="patterns${index}" placeholder="*.zip,?.txt" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<select class="form-control" id="idPatternType${index}" name="pattern_type${index}">
|
||||
<option value="denied">Denied</option>
|
||||
<option value="allowed">Allowed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_pattern_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_patterns_outer_row").remove();
|
||||
});
|
||||
|
||||
$("body").on("click", ".add_new_tpl_user_field_btn", function () {
|
||||
var index = $(".form_field_tpl_users_outer").find(".form_field_tpl_user_outer_row").length;
|
||||
while (document.getElementById("idTplUsername"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_tpl_users_outer").append(`
|
||||
<div class="row form_field_tpl_user_outer_row">
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" class="form-control" id="idTplUsername${index}" name="tpl_username" placeholder="Username" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="password" class="form-control" id="idTplPassword${index}" name="tpl_password" placeholder="Password" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<textarea class="form-control" id="idTplPublicKey${index}" name="tpl_public_keys" rows="5"
|
||||
placeholder="Paste your public key here"></textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_tpl_user_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_tpl_user_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_tpl_user_outer_row").remove();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
{{template "fsjs"}}
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
placeholder="Paste your public key here">{{$val}}</textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field" {{if eq $idx 0}}disabled{{end}}>
|
||||
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -102,6 +102,9 @@
|
|||
$(document).ready(function () {
|
||||
$("body").on("click", ".add_new_pk_field_btn", function () {
|
||||
var index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
|
||||
while (document.getElementById("idPublicKey"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_pk_outer").append(`
|
||||
<div class="row form_field_pk_outer_row">
|
||||
<div class="form-group col-md-11">
|
||||
|
@ -109,15 +112,12 @@
|
|||
placeholder="Paste your public key here"></textarea>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field" disabled>
|
||||
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$(".form_field_pk_outer").find(".remove_pk_btn_frm_field:not(:first)").prop("disabled", false);
|
||||
$(".form_field_pk_outer").find(".remove_pk_btn_frm_field").first().prop("disabled", true);
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_pk_btn_frm_field", function () {
|
||||
|
|