Admin UI: allow to create multiple users/folders from templates

the clone button is not needed anymore, you can select a user and
click on template to generate one or more similar users or you can
create users/folders from an empty template

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-01-12 19:01:19 +01:00
parent ef626befb1
commit 467708dc1c
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
17 changed files with 247 additions and 122 deletions

View file

@ -344,16 +344,10 @@ func (u *User) IsTLSUsernameVerificationEnabled() bool {
// SetEmptySecrets sets to empty any user secret // SetEmptySecrets sets to empty any user secret
func (u *User) SetEmptySecrets() { func (u *User) SetEmptySecrets() {
u.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() u.FsConfig.SetEmptySecrets()
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()
for idx := range u.VirtualFolders { for idx := range u.VirtualFolders {
folder := &u.VirtualFolders[idx] folder := &u.VirtualFolders[idx]
folder.FsConfig.SetEmptySecretsIfNil() folder.FsConfig.SetEmptySecrets()
} }
u.Filters.TOTPConfig.Secret = kms.NewEmptySecret() 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 index := range list {
for dir := range vdirs { for dir := range vdirs {
if list[index].Name() == dir { if list[index].Name() == dir {
if !list[index].IsDir() {
list[index] = vfs.NewFileInfo(dir, true, 0, time.Now(), false)
}
delete(vdirs, dir) delete(vdirs, dir)
} }
} }

View file

@ -4104,11 +4104,6 @@ func TestProviderErrors(t *testing.T) {
setJWTCookieForReq(req, testServerToken) setJWTCookieForReq(req, testServerToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr) 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) req, err = http.NewRequest(http.MethodGet, webTemplateUser+"?from=auser", nil)
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, testServerToken) setJWTCookieForReq(req, testServerToken)
@ -13596,28 +13591,6 @@ func TestRenderUserTemplateMock(t *testing.T) {
assert.NoError(t, err) 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) { func TestUserTemplateWithFoldersMock(t *testing.T) {
folder := vfs.BaseVirtualFolder{ folder := vfs.BaseVirtualFolder{
Name: "vfolder", Name: "vfolder",
@ -13659,6 +13632,7 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
form.Add("tpl_username", "auser1") form.Add("tpl_username", "auser1")
form.Add("tpl_password", "password") form.Add("tpl_password", "password")
form.Add("tpl_public_keys", "") form.Add("tpl_public_keys", "")
form.Set("form_action", "export_from_template")
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
req, _ := http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b) req, _ := http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
@ -13714,6 +13688,73 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
assert.NoError(t, err) 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) { func TestUserTemplateMock(t *testing.T) {
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err) assert.NoError(t, err)
@ -13760,6 +13801,7 @@ func TestUserTemplateMock(t *testing.T) {
form.Set("s3_download_part_max_time", "0") form.Set("s3_download_part_max_time", "0")
// test invalid s3_upload_part_size // test invalid s3_upload_part_size
form.Set("s3_upload_part_size", "a") form.Set("s3_upload_part_size", "a")
form.Set("form_action", "export_from_template")
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
req, _ := http.NewRequest(http.MethodPost, webTemplateUser, &b) req, _ := http.NewRequest(http.MethodPost, webTemplateUser, &b)
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
@ -13798,7 +13840,7 @@ func TestUserTemplateMock(t *testing.T) {
req.Header.Set("Content-Type", contentType) req.Header.Set("Content-Type", contentType)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) 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_username", "user1")
form.Set("tpl_password", "password1") form.Set("tpl_password", "password1")
@ -13853,6 +13895,59 @@ func TestUserTemplateMock(t *testing.T) {
require.True(t, user2.Filters.DisableFsChecks) 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) { func TestFolderTemplateMock(t *testing.T) {
folderName := "vfolder-template" folderName := "vfolder-template"
mappedPath := filepath.Join(os.TempDir(), "%name%mapped%name%path") 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", "folder3")
form.Add("tpl_foldername", "folder1 ") form.Add("tpl_foldername", "folder1 ")
form.Add("tpl_foldername", " ") form.Add("tpl_foldername", " ")
form.Set("form_action", "export_from_template")
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
req, _ := http.NewRequest(http.MethodPost, webTemplateFolder, &b) req, _ := http.NewRequest(http.MethodPost, webTemplateFolder, &b)
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
@ -13971,7 +14067,7 @@ func TestFolderTemplateMock(t *testing.T) {
req.Header.Set("Content-Type", contentType) req.Header.Set("Content-Type", contentType)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) 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("tpl_foldername", "name")
form.Set("mapped_path", "relative-path") form.Set("mapped_path", "relative-path")

View file

@ -508,6 +508,16 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "invalid token claims") 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() rr = httptest.NewRecorder()
updateFolder(rr, req) updateFolder(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, http.StatusBadRequest, rr.Code)

View file

@ -1544,7 +1544,7 @@ func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) {
updatedAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes updatedAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes
claims, err := getTokenClaims(r) claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" { 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 return
} }
if username == claims.Username { if username == claims.Username {
@ -1610,6 +1610,7 @@ func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("from") name := r.URL.Query().Get("from")
folder, err := dataprovider.GetFolderByName(name) folder, err := dataprovider.GetFolderByName(name)
if err == nil { if err == nil {
folder.FsConfig.SetEmptySecrets()
renderFolderPage(w, r, folder, folderPageModeTemplate, "") renderFolderPage(w, r, folder, folderPageModeTemplate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok { } else if _, ok := err.(*util.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err) renderNotFoundPage(w, r, err)
@ -1624,8 +1625,13 @@ func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) { func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 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{} templateFolder := vfs.BaseVirtualFolder{}
err := r.ParseMultipartForm(maxRequestSize) err = r.ParseMultipartForm(maxRequestSize)
if err != nil { if err != nil {
renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "") renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "")
return return
@ -1653,19 +1659,30 @@ func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
for _, tmpl := range foldersFields { for _, tmpl := range foldersFields {
f := getFolderFromTemplate(templateFolder, tmpl) f := getFolderFromTemplate(templateFolder, tmpl)
if err := dataprovider.ValidateFolder(&f); err != nil { 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 return
} }
dump.Folders = append(dump.Folders, f) dump.Folders = append(dump.Folders, f)
} }
if len(dump.Folders) == 0 { 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 return
} }
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))) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-folders-from-template.json\"",
render.JSON(w, r, dump) 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) { 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) user, err := dataprovider.UserExists(username)
if err == nil { if err == nil {
user.SetEmptySecrets() user.SetEmptySecrets()
user.PublicKeys = nil
user.Email = "" user.Email = ""
user.Description = "" user.Description = ""
renderUserPage(w, r, &user, userPageModeTemplate, "") renderUserPage(w, r, &user, userPageModeTemplate, "")
@ -1684,13 +1702,23 @@ func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {
renderInternalServerErrorPage(w, r, err) renderInternalServerErrorPage(w, r, err)
} }
} else { } 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, "") renderUserPage(w, r, &user, userPageModeTemplate, "")
} }
} }
func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) { func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 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) templateUser, err := getUserFromPostFields(r)
if err != nil { if err != nil {
renderMessagePage(w, r, "Error parsing user fields", "", http.StatusBadRequest, err, "") 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 { for _, tmpl := range userTmplFields {
u := getUserFromTemplate(templateUser, tmpl) u := getUserFromTemplate(templateUser, tmpl)
if err := dataprovider.ValidateUser(&u); err != nil { 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 return
} }
dump.Users = append(dump.Users, u) dump.Users = append(dump.Users, u)
@ -1720,40 +1749,33 @@ func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) {
} }
if len(dump.Users) == 0 { 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 return
} }
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))) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-users-from-template.json\"",
render.JSON(w, r, dump) 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) { func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if r.URL.Query().Get("clone-from") != "" { user := dataprovider.User{BaseUser: sdk.BaseUser{
username := r.URL.Query().Get("clone-from") Status: 1,
user, err := dataprovider.UserExists(username) Permissions: map[string][]string{
if err == nil { "/": {dataprovider.PermAny},
user.ID = 0 },
user.Username = "" }}
user.Password = "" renderUserPage(w, r, &user, userPageModeAdd, "")
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, "")
}
} }
func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) { func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {

View file

@ -111,7 +111,7 @@
</div> </div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button> <button type="submit" class="btn btn-primary float-right mt-3 px-5">Submit</button>
</form> </form>
</div> </div>
</div> </div>

View file

@ -37,7 +37,7 @@
</div> </div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Change my password</button> <button type="submit" class="btn btn-primary float-right mt-3 px-5">Change my password</button>
</form> </form>
</div> </div>
</div> </div>

View file

@ -16,7 +16,7 @@
{{if eq .Mode 3}} {{if eq .Mode 3}}
<div class="card mb-4 border-left-info"> <div class="card mb-4 border-left-info">
<div class="card-body"> <div class="card-body">
Generate a data provider independent JSON file to create new folders or update existing ones. Create and save one or more new folders or generate a data provider independent JSON file to import.
<br> <br>
The following placeholder is supported: The following placeholder is supported:
<br><br> <br><br>
@ -31,7 +31,7 @@
{{if eq .Mode 3}} {{if eq .Mode 3}}
<div class="card bg-light mb-3"> <div class="card bg-light mb-3">
<div class="card-header"> <div class="card-header">
Folders <b>Folders</b>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="form-group row"> <div class="form-group row">
@ -80,7 +80,12 @@
{{template "fshtml" .FsWrapper}} {{template "fshtml" .FsWrapper}}
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">{{if eq .Mode 3}}Generate and export folders{{else}}Submit{{end}}</button> <div class="col-sm-12 text-right px-0">
{{if eq .Mode 3}}
<button type="submit" class="btn btn-secondary mt-3 px-5" name="form_action" value="export_from_template">Generate and export folders</button>
{{end}}
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">{{if eq .Mode 3}}Generate and save new folders{{else}}Submit{{end}}</button>
</div>
</form> </form>
</div> </div>
</div> </div>

View file

@ -149,8 +149,9 @@ function deleteAction() {
}; };
$.fn.dataTable.ext.buttons.template = { $.fn.dataTable.ext.buttons.template = {
text: 'Template', text: '<i class="fas fa-clone"></i>',
name: 'template', name: 'template',
titleAttr: "Template",
action: function (e, dt, node, config) { action: function (e, dt, node, config) {
var selectedRows = table.rows({ selected: true }).count(); var selectedRows = table.rows({ selected: true }).count();
if (selectedRows == 1){ if (selectedRows == 1){
@ -254,14 +255,14 @@ function deleteAction() {
table.button().add(0,'quota_scan'); table.button().add(0,'quota_scan');
{{end}} {{end}}
{{if .LoggedAdmin.HasPermission "manage_system"}}
table.button().add(0,'template');
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}} {{if .LoggedAdmin.HasPermission "del_users"}}
table.button().add(0,'delete'); table.button().add(0,'delete');
{{end}} {{end}}
{{if .LoggedAdmin.HasPermission "add_users"}}
table.button().add(0,'template');
{{end}}
{{if .LoggedAdmin.HasPermission "edit_users"}} {{if .LoggedAdmin.HasPermission "edit_users"}}
table.button().add(0,'edit'); table.button().add(0,'edit');
{{end}} {{end}}

View file

@ -62,8 +62,7 @@
<label for="idS3AccessSecret" class="col-sm-2 col-form-label">Access Secret</label> <label for="idS3AccessSecret" class="col-sm-2 col-form-label">Access Secret</label>
<div class="col-sm-3"> <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}}" value="{{if .S3Config.AccessSecret.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.S3Config.AccessSecret.GetPayload}}{{end}}">
maxlength="1000">
</div> </div>
</div> </div>
@ -235,8 +234,7 @@
<label for="idAzAccountKey" class="col-sm-2 col-form-label">Account Key</label> <label for="idAzAccountKey" class="col-sm-2 col-form-label">Account Key</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" class="form-control" id="idAzAccountKey" name="az_account_key" placeholder="" <input type="password" class="form-control" id="idAzAccountKey" name="az_account_key" placeholder=""
value="{{if .AzBlobConfig.AccountKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.AccountKey.GetPayload}}{{end}}" value="{{if .AzBlobConfig.AccountKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.AccountKey.GetPayload}}{{end}}">
maxlength="1000">
</div> </div>
</div> </div>
@ -244,7 +242,7 @@
<label for="idAzSASURL" class="col-sm-2 col-form-label">SAS URL</label> <label for="idAzSASURL" class="col-sm-2 col-form-label">SAS URL</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" class="form-control" id="idAzSASURL" name="az_sas_url" placeholder="" <input type="password" class="form-control" id="idAzSASURL" name="az_sas_url" placeholder=""
value="{{if .AzBlobConfig.SASURL.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.SASURL.GetPayload}}{{end}}" maxlength="1000"> value="{{if .AzBlobConfig.SASURL.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.SASURL.GetPayload}}{{end}}">
</div> </div>
</div> </div>
<div class="form-group row fsconfig fsconfig-azblobfs"> <div class="form-group row fsconfig fsconfig-azblobfs">
@ -313,8 +311,7 @@
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" class="form-control" id="idCryptPassphrase" name="crypt_passphrase" <input type="password" class="form-control" id="idCryptPassphrase" name="crypt_passphrase"
placeholder="" placeholder=""
value="{{if .CryptConfig.Passphrase.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.CryptConfig.Passphrase.GetPayload}}{{end}}" value="{{if .CryptConfig.Passphrase.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.CryptConfig.Passphrase.GetPayload}}{{end}}">
maxlength="1000">
</div> </div>
</div> </div>
@ -348,8 +345,7 @@
<label for="idSFTPPassword" class="col-sm-2 col-form-label">Password</label> <label for="idSFTPPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-3"> <div class="col-sm-3">
<input type="password" class="form-control" id="idSFTPPassword" name="sftp_password" placeholder="" <input type="password" class="form-control" id="idSFTPPassword" name="sftp_password" placeholder=""
value="{{if .SFTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.Password.GetPayload}}{{end}}" value="{{if .SFTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.Password.GetPayload}}{{end}}">
maxlength="1000">
</div> </div>
</div> </div>

View file

@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Import</button> <button type="submit" class="btn btn-primary float-right mt-3 px-5">Import</button>
</form> </form>
</div> </div>
</div> </div>

View file

@ -43,7 +43,7 @@
</div> </div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button> <button type="submit" class="btn btn-primary float-right mt-3 px-5">Submit</button>
</form> </form>
</div> </div>
</div> </div>

View file

@ -22,7 +22,7 @@
{{if eq .Mode 3}} {{if eq .Mode 3}}
<div class="card mb-4 border-left-info"> <div class="card mb-4 border-left-info">
<div class="card-body"> <div class="card-body">
Generate a data provider independent JSON file to create new users or update existing ones. Create and save one or more new users or generate a data provider independent JSON file to import.
<br> <br>
The following placeholders are supported: The following placeholders are supported:
<br><br> <br><br>
@ -42,7 +42,7 @@
{{if eq .Mode 3}} {{if eq .Mode 3}}
<div class="card bg-light mb-3"> <div class="card bg-light mb-3">
<div class="card-header"> <div class="card-header">
Users <b>Users</b>
</div> </div>
<div class="card-body"> <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> <h6 class="card-title mb-4">For each user set the username and at least one of the password and public key</h6>
@ -753,7 +753,12 @@
<input type="hidden" name="expiration_date" id="hidden_start_datetime" value=""> <input type="hidden" name="expiration_date" id="hidden_start_datetime" value="">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">{{if eq .Mode 3}}Generate and export users{{else}}Submit{{end}}</button> <div class="col-sm-12 text-right px-0">
{{if eq .Mode 3}}
<button type="submit" class="btn btn-secondary mt-3 px-5" name="form_action" value="export_from_template">Generate and export users</button>
{{end}}
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">{{if eq .Mode 3}}Generate and save new users{{else}}Submit{{end}}</button>
</div>
</form> </form>
</div> </div>
</div> </div>

View file

@ -153,21 +153,10 @@
enabled: false enabled: false
}; };
$.fn.dataTable.ext.buttons.clone = {
text: '<i class="fas fa-clone"></i>',
name: 'clone',
titleAttr: "Clone",
action: function (e, dt, node, config) {
var username = dt.row({ selected: true }).data()[1];
var path = '{{.UserURL}}' + "?clone-from=" + encodeURIComponent(username);
window.location.href = path;
},
enabled: false
};
$.fn.dataTable.ext.buttons.template = { $.fn.dataTable.ext.buttons.template = {
text: 'Template', text: '<i class="fas fa-clone"></i>',
name: 'template', name: 'template',
titleAttr: "Template",
action: function (e, dt, node, config) { action: function (e, dt, node, config) {
var selectedRows = table.rows({ selected: true }).count(); var selectedRows = table.rows({ selected: true }).count();
if (selectedRows == 1){ if (selectedRows == 1){
@ -277,16 +266,12 @@
table.button().add(0,'quota_scan'); table.button().add(0,'quota_scan');
{{end}} {{end}}
{{if .LoggedAdmin.HasPermission "manage_system"}}
table.button().add(0,'template');
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}} {{if .LoggedAdmin.HasPermission "del_users"}}
table.button().add(0,'delete'); table.button().add(0,'delete');
{{end}} {{end}}
{{if .LoggedAdmin.HasPermission "add_users"}} {{if .LoggedAdmin.HasPermission "add_users"}}
table.button().add(0,'clone'); table.button().add(0,'template');
{{end}} {{end}}
{{if .LoggedAdmin.HasPermission "edit_users"}} {{if .LoggedAdmin.HasPermission "edit_users"}}
@ -304,9 +289,6 @@
{{if .LoggedAdmin.HasPermission "edit_users"}} {{if .LoggedAdmin.HasPermission "edit_users"}}
table.button('edit:name').enable(selectedRows == 1); table.button('edit:name').enable(selectedRows == 1);
{{end}} {{end}}
{{if .LoggedAdmin.HasPermission "add_users"}}
table.button('clone:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}} {{if .LoggedAdmin.HasPermission "del_users"}}
table.button('delete:name').enable(selectedRows == 1); table.button('delete:name').enable(selectedRows == 1);
{{end}} {{end}}

View file

@ -37,7 +37,7 @@
</div> </div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Change my password</button> <button type="submit" class="btn btn-primary float-right mt-3 px-5">Change my password</button>
</form> </form>
</div> </div>
</div> </div>

View file

@ -88,7 +88,7 @@
{{end}} {{end}}
{{if .CanSubmit}} {{if .CanSubmit}}
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button> <button type="submit" class="btn btn-primary float-right mt-3 px-5">Submit</button>
{{end}} {{end}}
</form> </form>
</div> </div>

View file

@ -135,7 +135,7 @@
<input type="hidden" name="expiration_date" id="hidden_start_datetime" value=""> <input type="hidden" name="expiration_date" id="hidden_start_datetime" value="">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button> <button type="submit" class="btn btn-primary float-right mt-3 px-5">Submit</button>
</form> </form>
</div> </div>
</div> </div>

View file

@ -27,6 +27,17 @@ type Filesystem struct {
SFTPConfig SFTPFsConfig `json:"sftpconfig,omitempty"` SFTPConfig SFTPFsConfig `json:"sftpconfig,omitempty"`
} }
// SetEmptySecrets sets the secrets to empty
func (f *Filesystem) SetEmptySecrets() {
f.S3Config.AccessSecret = kms.NewEmptySecret()
f.GCSConfig.Credentials = kms.NewEmptySecret()
f.AzBlobConfig.AccountKey = kms.NewEmptySecret()
f.AzBlobConfig.SASURL = kms.NewEmptySecret()
f.CryptConfig.Passphrase = kms.NewEmptySecret()
f.SFTPConfig.Password = kms.NewEmptySecret()
f.SFTPConfig.PrivateKey = kms.NewEmptySecret()
}
// SetEmptySecretsIfNil sets the secrets to empty if nil // SetEmptySecretsIfNil sets the secrets to empty if nil
func (f *Filesystem) SetEmptySecretsIfNil() { func (f *Filesystem) SetEmptySecretsIfNil() {
if f.S3Config.AccessSecret == nil { if f.S3Config.AccessSecret == nil {