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
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)
}
}

View file

@ -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")

View file

@ -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)

View file

@ -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)))
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,32 +1749,26 @@ 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)))
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{
@ -1753,7 +1776,6 @@ func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
},
}}
renderUserPage(w, r, &user, userPageModeAdd, "")
}
}
func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {

View file

@ -111,7 +111,7 @@
</div>
<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>
</div>
</div>

View file

@ -37,7 +37,7 @@
</div>
<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>
</div>
</div>

View file

@ -16,7 +16,7 @@
{{if eq .Mode 3}}
<div class="card mb-4 border-left-info">
<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>
The following placeholder is supported:
<br><br>
@ -31,7 +31,7 @@
{{if eq .Mode 3}}
<div class="card bg-light mb-3">
<div class="card-header">
Folders
<b>Folders</b>
</div>
<div class="card-body">
<div class="form-group row">
@ -80,7 +80,12 @@
{{template "fshtml" .FsWrapper}}
<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>
</div>
</div>

View file

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

View file

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

View file

@ -45,7 +45,7 @@
</div>
</div>
<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>
</div>
</div>

View file

@ -43,7 +43,7 @@
</div>
<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>
</div>
</div>

View file

@ -22,7 +22,7 @@
{{if eq .Mode 3}}
<div class="card mb-4 border-left-info">
<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>
The following placeholders are supported:
<br><br>
@ -42,7 +42,7 @@
{{if eq .Mode 3}}
<div class="card bg-light mb-3">
<div class="card-header">
Users
<b>Users</b>
</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>
@ -753,7 +753,12 @@
<input type="hidden" name="expiration_date" id="hidden_start_datetime" value="">
<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>
</div>
</div>

View file

@ -153,21 +153,10 @@
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 = {
text: 'Template',
text: '<i class="fas fa-clone"></i>',
name: 'template',
titleAttr: "Template",
action: function (e, dt, node, config) {
var selectedRows = table.rows({ selected: true }).count();
if (selectedRows == 1){
@ -277,16 +266,12 @@
table.button().add(0,'quota_scan');
{{end}}
{{if .LoggedAdmin.HasPermission "manage_system"}}
table.button().add(0,'template');
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}}
table.button().add(0,'delete');
{{end}}
{{if .LoggedAdmin.HasPermission "add_users"}}
table.button().add(0,'clone');
table.button().add(0,'template');
{{end}}
{{if .LoggedAdmin.HasPermission "edit_users"}}
@ -304,9 +289,6 @@
{{if .LoggedAdmin.HasPermission "edit_users"}}
table.button('edit:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "add_users"}}
table.button('clone:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}}
table.button('delete:name').enable(selectedRows == 1);
{{end}}

View file

@ -37,7 +37,7 @@
</div>
<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>
</div>
</div>

View file

@ -88,7 +88,7 @@
{{end}}
{{if .CanSubmit}}
<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}}
</form>
</div>

View file

@ -135,7 +135,7 @@
<input type="hidden" name="expiration_date" id="hidden_start_datetime" value="">
<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>
</div>
</div>

View file

@ -27,6 +27,17 @@ type Filesystem struct {
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
func (f *Filesystem) SetEmptySecretsIfNil() {
if f.S3Config.AccessSecret == nil {