diff --git a/dataprovider/user.go b/dataprovider/user.go index 22ba6ac0..5636aca3 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -344,16 +344,10 @@ func (u *User) IsTLSUsernameVerificationEnabled() bool { // SetEmptySecrets sets to empty any user secret func (u *User) SetEmptySecrets() { - u.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() - u.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret() - u.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret() - u.FsConfig.AzBlobConfig.SASURL = kms.NewEmptySecret() - u.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret() - u.FsConfig.SFTPConfig.Password = kms.NewEmptySecret() - u.FsConfig.SFTPConfig.PrivateKey = kms.NewEmptySecret() + u.FsConfig.SetEmptySecrets() for idx := range u.VirtualFolders { folder := &u.VirtualFolders[idx] - folder.FsConfig.SetEmptySecretsIfNil() + folder.FsConfig.SetEmptySecrets() } u.Filters.TOTPConfig.Secret = kms.NewEmptySecret() } @@ -572,6 +566,9 @@ func (u *User) AddVirtualDirs(list []os.FileInfo, virtualPath string) []os.FileI for index := range list { for dir := range vdirs { if list[index].Name() == dir { + if !list[index].IsDir() { + list[index] = vfs.NewFileInfo(dir, true, 0, time.Now(), false) + } delete(vdirs, dir) } } diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 26c79c9e..8344a270 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -4104,11 +4104,6 @@ func TestProviderErrors(t *testing.T) { setJWTCookieForReq(req, testServerToken) rr = executeRequest(req) checkResponseCode(t, http.StatusInternalServerError, rr) - req, err = http.NewRequest(http.MethodGet, webUserPath+"?clone-from=user", nil) - assert.NoError(t, err) - setJWTCookieForReq(req, testServerToken) - rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr) req, err = http.NewRequest(http.MethodGet, webTemplateUser+"?from=auser", nil) assert.NoError(t, err) setJWTCookieForReq(req, testServerToken) @@ -13596,28 +13591,6 @@ func TestRenderUserTemplateMock(t *testing.T) { assert.NoError(t, err) } -func TestRenderWebCloneUserMock(t *testing.T) { - token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) - assert.NoError(t, err) - user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) - assert.NoError(t, err) - - req, err := http.NewRequest(http.MethodGet, webUserPath+fmt.Sprintf("?clone-from=%v", user.Username), nil) - assert.NoError(t, err) - setJWTCookieForReq(req, token) - rr := executeRequest(req) - checkResponseCode(t, http.StatusOK, rr) - - req, err = http.NewRequest(http.MethodGet, webUserPath+fmt.Sprintf("?clone-from=%v", altAdminPassword), nil) - assert.NoError(t, err) - setJWTCookieForReq(req, token) - rr = executeRequest(req) - checkResponseCode(t, http.StatusNotFound, rr) - - _, err = httpdtest.RemoveUser(user, http.StatusOK) - assert.NoError(t, err) -} - func TestUserTemplateWithFoldersMock(t *testing.T) { folder := vfs.BaseVirtualFolder{ Name: "vfolder", @@ -13659,6 +13632,7 @@ func TestUserTemplateWithFoldersMock(t *testing.T) { form.Add("tpl_username", "auser1") form.Add("tpl_password", "password") form.Add("tpl_public_keys", "") + form.Set("form_action", "export_from_template") b, contentType, _ := getMultipartFormData(form, "", "") req, _ := http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b) setJWTCookieForReq(req, token) @@ -13714,6 +13688,73 @@ func TestUserTemplateWithFoldersMock(t *testing.T) { assert.NoError(t, err) } +func TestUserSaveFromTemplateMock(t *testing.T) { + token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath) + assert.NoError(t, err) + user1 := "u1" + user2 := "u2" + form := make(url.Values) + form.Set("username", "") + form.Set("home_dir", filepath.Join(os.TempDir(), "%username%")) + form.Set("upload_bandwidth", "0") + form.Set("download_bandwidth", "0") + form.Set("uid", "0") + form.Set("gid", "0") + form.Set("max_sessions", "0") + form.Set("quota_size", "0") + form.Set("quota_files", "0") + form.Set("permissions", "*") + form.Set("status", "1") + form.Set("expiration_date", "") + form.Set("fs_provider", "0") + form.Set("max_upload_file_size", "0") + form.Add("tpl_username", user1) + form.Add("tpl_password", "password1") + form.Add("tpl_public_keys", " ") + form.Add("tpl_username", user2) + form.Add("tpl_public_keys", testPubKey) + form.Set(csrfFormToken, csrfToken) + b, contentType, _ := getMultipartFormData(form, "", "") + req, _ := http.NewRequest(http.MethodPost, webTemplateUser, &b) + setJWTCookieForReq(req, token) + req.Header.Set("Content-Type", contentType) + rr := executeRequest(req) + checkResponseCode(t, http.StatusSeeOther, rr) + + u1, _, err := httpdtest.GetUserByUsername(user1, http.StatusOK) + assert.NoError(t, err) + u2, _, err := httpdtest.GetUserByUsername(user2, http.StatusOK) + assert.NoError(t, err) + + _, err = httpdtest.RemoveUser(u1, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveUser(u2, http.StatusOK) + assert.NoError(t, err) + + err = dataprovider.Close() + assert.NoError(t, err) + + b, contentType, _ = getMultipartFormData(form, "", "") + req, err = http.NewRequest(http.MethodPost, webTemplateUser, &b) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + req.Header.Set("Content-Type", contentType) + rr = executeRequest(req) + checkResponseCode(t, http.StatusInternalServerError, rr) + assert.Contains(t, rr.Body.String(), "Cannot save the defined users") + + err = config.LoadConfig(configDir, "") + assert.NoError(t, err) + providerConf := config.GetProviderConf() + providerConf.CredentialsPath = credentialsPath + err = os.RemoveAll(credentialsPath) + assert.NoError(t, err) + err = dataprovider.Initialize(providerConf, configDir, true) + assert.NoError(t, err) +} + func TestUserTemplateMock(t *testing.T) { token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) assert.NoError(t, err) @@ -13760,6 +13801,7 @@ func TestUserTemplateMock(t *testing.T) { form.Set("s3_download_part_max_time", "0") // test invalid s3_upload_part_size form.Set("s3_upload_part_size", "a") + form.Set("form_action", "export_from_template") b, contentType, _ := getMultipartFormData(form, "", "") req, _ := http.NewRequest(http.MethodPost, webTemplateUser, &b) setJWTCookieForReq(req, token) @@ -13798,7 +13840,7 @@ func TestUserTemplateMock(t *testing.T) { req.Header.Set("Content-Type", contentType) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - require.Contains(t, rr.Body.String(), "No valid users found, export is not possible") + require.Contains(t, rr.Body.String(), "No valid users defined, unable to complete the requested action") form.Set("tpl_username", "user1") form.Set("tpl_password", "password1") @@ -13853,6 +13895,59 @@ func TestUserTemplateMock(t *testing.T) { require.True(t, user2.Filters.DisableFsChecks) } +func TestFolderSaveFromTemplateMock(t *testing.T) { + folder1 := "f1" + folder2 := "f2" + token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath) + assert.NoError(t, err) + form := make(url.Values) + form.Set("name", "name") + form.Set("mapped_path", filepath.Join(os.TempDir(), "%name%")) + form.Set("description", "desc folder %name%") + form.Add("tpl_foldername", folder1) + form.Add("tpl_foldername", folder2) + form.Set(csrfFormToken, csrfToken) + b, contentType, _ := getMultipartFormData(form, "", "") + req, err := http.NewRequest(http.MethodPost, webTemplateFolder, &b) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + req.Header.Set("Content-Type", contentType) + rr := executeRequest(req) + checkResponseCode(t, http.StatusSeeOther, rr) + + _, _, err = httpdtest.GetFolderByName(folder1, http.StatusOK) + assert.NoError(t, err) + _, _, err = httpdtest.GetFolderByName(folder2, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folder1}, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folder2}, http.StatusOK) + assert.NoError(t, err) + + err = dataprovider.Close() + assert.NoError(t, err) + + b, contentType, _ = getMultipartFormData(form, "", "") + req, err = http.NewRequest(http.MethodPost, webTemplateFolder, &b) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + req.Header.Set("Content-Type", contentType) + rr = executeRequest(req) + checkResponseCode(t, http.StatusInternalServerError, rr) + assert.Contains(t, rr.Body.String(), "Cannot save the defined folders") + + err = config.LoadConfig(configDir, "") + assert.NoError(t, err) + providerConf := config.GetProviderConf() + providerConf.CredentialsPath = credentialsPath + err = os.RemoveAll(credentialsPath) + assert.NoError(t, err) + err = dataprovider.Initialize(providerConf, configDir, true) + assert.NoError(t, err) +} + func TestFolderTemplateMock(t *testing.T) { folderName := "vfolder-template" mappedPath := filepath.Join(os.TempDir(), "%name%mapped%name%path") @@ -13869,6 +13964,7 @@ func TestFolderTemplateMock(t *testing.T) { form.Add("tpl_foldername", "folder3") form.Add("tpl_foldername", "folder1 ") form.Add("tpl_foldername", " ") + form.Set("form_action", "export_from_template") b, contentType, _ := getMultipartFormData(form, "", "") req, _ := http.NewRequest(http.MethodPost, webTemplateFolder, &b) setJWTCookieForReq(req, token) @@ -13971,7 +14067,7 @@ func TestFolderTemplateMock(t *testing.T) { req.Header.Set("Content-Type", contentType) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "No folders to export") + assert.Contains(t, rr.Body.String(), "No valid folders defined") form.Set("tpl_foldername", "name") form.Set("mapped_path", "relative-path") diff --git a/httpd/internal_test.go b/httpd/internal_test.go index 1b8e3d81..5e003a92 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -508,6 +508,16 @@ func TestInvalidToken(t *testing.T) { assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Contains(t, rr.Body.String(), "invalid token claims") + rr = httptest.NewRecorder() + handleWebTemplateFolderPost(rr, req) + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Contains(t, rr.Body.String(), "invalid token claims") + + rr = httptest.NewRecorder() + handleWebTemplateUserPost(rr, req) + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Contains(t, rr.Body.String(), "invalid token claims") + rr = httptest.NewRecorder() updateFolder(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) diff --git a/httpd/webadmin.go b/httpd/webadmin.go index 6bb1b581..c49550d4 100644 --- a/httpd/webadmin.go +++ b/httpd/webadmin.go @@ -1544,7 +1544,7 @@ func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) { updatedAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - renderAddUpdateAdminPage(w, r, &updatedAdmin, fmt.Sprintf("Invalid token claims: %v", err), false) + renderAddUpdateAdminPage(w, r, &updatedAdmin, "Invalid token claims", false) return } if username == claims.Username { @@ -1610,6 +1610,7 @@ func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("from") folder, err := dataprovider.GetFolderByName(name) if err == nil { + folder.FsConfig.SetEmptySecrets() renderFolderPage(w, r, folder, folderPageModeTemplate, "") } else if _, ok := err.(*util.RecordNotFoundError); ok { renderNotFoundPage(w, r, err) @@ -1624,8 +1625,13 @@ func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) { func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + claims, err := getTokenClaims(r) + if err != nil || claims.Username == "" { + renderBadRequestPage(w, r, errors.New("invalid token claims")) + return + } templateFolder := vfs.BaseVirtualFolder{} - err := r.ParseMultipartForm(maxRequestSize) + err = r.ParseMultipartForm(maxRequestSize) if err != nil { renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "") return @@ -1653,19 +1659,30 @@ func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) { for _, tmpl := range foldersFields { f := getFolderFromTemplate(templateFolder, tmpl) if err := dataprovider.ValidateFolder(&f); err != nil { - renderMessagePage(w, r, fmt.Sprintf("Error validating folder %#v", f.Name), "", http.StatusBadRequest, err, "") + renderMessagePage(w, r, "Folder validation error", fmt.Sprintf("Error validating folder %#v", f.Name), + http.StatusBadRequest, err, "") return } dump.Folders = append(dump.Folders, f) } if len(dump.Folders) == 0 { - renderMessagePage(w, r, "No folders to export", "No valid folders found, export is not possible", http.StatusBadRequest, nil, "") + renderMessagePage(w, r, "No folders defined", "No valid folders defined, unable to complete the requested action", + http.StatusBadRequest, nil, "") return } - - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-folders-from-template.json\"", len(dump.Folders))) - render.JSON(w, r, dump) + if r.Form.Get("form_action") == "export_from_template" { + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-folders-from-template.json\"", + len(dump.Folders))) + render.JSON(w, r, dump) + return + } + if err = RestoreFolders(dump.Folders, "", 1, 0, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil { + renderMessagePage(w, r, "Unable to save folders", "Cannot save the defined folders:", + getRespStatus(err), err, "") + return + } + http.Redirect(w, r, webFoldersPath, http.StatusSeeOther) } func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) { @@ -1675,6 +1692,7 @@ func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) { user, err := dataprovider.UserExists(username) if err == nil { user.SetEmptySecrets() + user.PublicKeys = nil user.Email = "" user.Description = "" renderUserPage(w, r, &user, userPageModeTemplate, "") @@ -1684,13 +1702,23 @@ func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) { renderInternalServerErrorPage(w, r, err) } } else { - user := dataprovider.User{BaseUser: sdk.BaseUser{Status: 1}} + user := dataprovider.User{BaseUser: sdk.BaseUser{ + Status: 1, + Permissions: map[string][]string{ + "/": {dataprovider.PermAny}, + }, + }} renderUserPage(w, r, &user, userPageModeTemplate, "") } } func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + claims, err := getTokenClaims(r) + if err != nil || claims.Username == "" { + renderBadRequestPage(w, r, errors.New("invalid token claims")) + return + } templateUser, err := getUserFromPostFields(r) if err != nil { renderMessagePage(w, r, "Error parsing user fields", "", http.StatusBadRequest, err, "") @@ -1708,7 +1736,8 @@ func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) { for _, tmpl := range userTmplFields { u := getUserFromTemplate(templateUser, tmpl) if err := dataprovider.ValidateUser(&u); err != nil { - renderMessagePage(w, r, fmt.Sprintf("Error validating user %#v", u.Username), "", http.StatusBadRequest, err, "") + renderMessagePage(w, r, "User validation error", fmt.Sprintf("Error validating user %#v", u.Username), + http.StatusBadRequest, err, "") return } dump.Users = append(dump.Users, u) @@ -1720,40 +1749,33 @@ func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) { } if len(dump.Users) == 0 { - renderMessagePage(w, r, "No users to export", "No valid users found, export is not possible", http.StatusBadRequest, nil, "") + renderMessagePage(w, r, "No users defined", "No valid users defined, unable to complete the requested action", + http.StatusBadRequest, nil, "") return } - - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-users-from-template.json\"", len(dump.Users))) - render.JSON(w, r, dump) + if r.Form.Get("form_action") == "export_from_template" { + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-users-from-template.json\"", + len(dump.Users))) + render.JSON(w, r, dump) + return + } + if err = RestoreUsers(dump.Users, "", 1, 0, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil { + renderMessagePage(w, r, "Unable to save users", "Cannot save the defined users:", + getRespStatus(err), err, "") + return + } + http.Redirect(w, r, webUsersPath, http.StatusSeeOther) } func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - if r.URL.Query().Get("clone-from") != "" { - username := r.URL.Query().Get("clone-from") - user, err := dataprovider.UserExists(username) - if err == nil { - user.ID = 0 - user.Username = "" - user.Password = "" - user.PublicKeys = nil - user.SetEmptySecrets() - renderUserPage(w, r, &user, userPageModeAdd, "") - } else if _, ok := err.(*util.RecordNotFoundError); ok { - renderNotFoundPage(w, r, err) - } else { - renderInternalServerErrorPage(w, r, err) - } - } else { - user := dataprovider.User{BaseUser: sdk.BaseUser{ - Status: 1, - Permissions: map[string][]string{ - "/": {dataprovider.PermAny}, - }, - }} - renderUserPage(w, r, &user, userPageModeAdd, "") - } + user := dataprovider.User{BaseUser: sdk.BaseUser{ + Status: 1, + Permissions: map[string][]string{ + "/": {dataprovider.PermAny}, + }, + }} + renderUserPage(w, r, &user, userPageModeAdd, "") } func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) { diff --git a/templates/webadmin/admin.html b/templates/webadmin/admin.html index 6ce1b409..ab2b6ae7 100644 --- a/templates/webadmin/admin.html +++ b/templates/webadmin/admin.html @@ -111,7 +111,7 @@ - + diff --git a/templates/webadmin/changepassword.html b/templates/webadmin/changepassword.html index 72deeee1..fe87f53a 100644 --- a/templates/webadmin/changepassword.html +++ b/templates/webadmin/changepassword.html @@ -37,7 +37,7 @@ - + diff --git a/templates/webadmin/folder.html b/templates/webadmin/folder.html index 6c092991..d4d8d1ea 100644 --- a/templates/webadmin/folder.html +++ b/templates/webadmin/folder.html @@ -16,7 +16,7 @@ {{if eq .Mode 3}}