mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 23:00:55 +03:00
web ui: allow to create multiple users from a template
This commit is contained in:
284
httpd/web.go
284
httpd/web.go
@@ -13,6 +13,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
@@ -21,6 +23,14 @@ import (
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
)
|
||||
|
||||
type userPageMode int
|
||||
|
||||
const (
|
||||
userPageModeAdd userPageMode = iota + 1
|
||||
userPageModeUpdate
|
||||
userPageModeTemplate
|
||||
)
|
||||
|
||||
const (
|
||||
templateBase = "base.html"
|
||||
templateUsers = "users.html"
|
||||
@@ -62,6 +72,7 @@ type basePage struct {
|
||||
CurrentURL string
|
||||
UsersURL string
|
||||
UserURL string
|
||||
UserTemplateURL string
|
||||
AdminsURL string
|
||||
AdminURL string
|
||||
QuotaScanURL string
|
||||
@@ -110,7 +121,7 @@ type statusPage struct {
|
||||
|
||||
type userPage struct {
|
||||
basePage
|
||||
User dataprovider.User
|
||||
User *dataprovider.User
|
||||
RootPerms []string
|
||||
Error string
|
||||
ValidPerms []string
|
||||
@@ -118,7 +129,7 @@ type userPage struct {
|
||||
ValidProtocols []string
|
||||
RootDirPerms []string
|
||||
RedactedSecret string
|
||||
IsAdd bool
|
||||
Mode userPageMode
|
||||
}
|
||||
|
||||
type adminPage struct {
|
||||
@@ -158,6 +169,12 @@ type loginPage struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
type userTemplateFields struct {
|
||||
Username string
|
||||
Password string
|
||||
PublicKey string
|
||||
}
|
||||
|
||||
func loadTemplates(templatesPath string) {
|
||||
usersPaths := []string{
|
||||
filepath.Join(templatesPath, templateBase),
|
||||
@@ -239,6 +256,7 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage {
|
||||
CurrentURL: currentURL,
|
||||
UsersURL: webUsersPath,
|
||||
UserURL: webUserPath,
|
||||
UserTemplateURL: webTemplateUser,
|
||||
AdminsURL: webAdminsPath,
|
||||
AdminURL: webAdminPath,
|
||||
FoldersURL: webFoldersPath,
|
||||
@@ -337,27 +355,23 @@ func renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dat
|
||||
renderTemplate(w, templateAdmin, data)
|
||||
}
|
||||
|
||||
func renderAddUserPage(w http.ResponseWriter, r *http.Request, user dataprovider.User, error string) {
|
||||
func renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.User, mode userPageMode, error string) {
|
||||
user.SetEmptySecretsIfNil()
|
||||
data := userPage{
|
||||
basePage: getBasePageData("Add a new user", webUserPath, r),
|
||||
IsAdd: true,
|
||||
Error: error,
|
||||
User: user,
|
||||
ValidPerms: dataprovider.ValidPerms,
|
||||
ValidSSHLoginMethods: dataprovider.ValidSSHLoginMethods,
|
||||
ValidProtocols: dataprovider.ValidProtocols,
|
||||
RootDirPerms: user.GetPermissionsForPath("/"),
|
||||
RedactedSecret: redactedSecret,
|
||||
var title, currentURL string
|
||||
switch mode {
|
||||
case userPageModeAdd:
|
||||
title = "Add a new user"
|
||||
currentURL = webUserPath
|
||||
case userPageModeUpdate:
|
||||
title = "Update user"
|
||||
currentURL = fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(user.Username))
|
||||
case userPageModeTemplate:
|
||||
title = "User template"
|
||||
currentURL = webTemplateUser
|
||||
}
|
||||
renderTemplate(w, templateUser, data)
|
||||
}
|
||||
|
||||
func renderUpdateUserPage(w http.ResponseWriter, r *http.Request, user dataprovider.User, error string) {
|
||||
user.SetEmptySecretsIfNil()
|
||||
data := userPage{
|
||||
basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(user.Username)), r),
|
||||
IsAdd: false,
|
||||
basePage: getBasePageData(title, currentURL, r),
|
||||
Mode: mode,
|
||||
Error: error,
|
||||
User: user,
|
||||
ValidPerms: dataprovider.ValidPerms,
|
||||
@@ -378,6 +392,33 @@ func renderAddFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.Base
|
||||
renderTemplate(w, templateFolder, data)
|
||||
}
|
||||
|
||||
func getUsersForTemplate(r *http.Request) []userTemplateFields {
|
||||
var res []userTemplateFields
|
||||
formValue := r.Form.Get("users")
|
||||
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])
|
||||
}
|
||||
if username == "" || (password == "" && publicKey == "") {
|
||||
continue
|
||||
}
|
||||
res = append(res, userTemplateFields{
|
||||
Username: username,
|
||||
Password: password,
|
||||
PublicKey: publicKey,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {
|
||||
var virtualFolders []vfs.VirtualFolder
|
||||
formValue := r.Form.Get("virtual_folders")
|
||||
@@ -429,7 +470,7 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
|
||||
perms = append(perms, cleanedPerm)
|
||||
}
|
||||
}
|
||||
if len(dir) > 0 {
|
||||
if dir != "" {
|
||||
permissions[dir] = perms
|
||||
}
|
||||
}
|
||||
@@ -442,7 +483,7 @@ func getSliceFromDelimitedValues(values, delimiter string) []string {
|
||||
result := []string{}
|
||||
for _, v := range strings.Split(values, delimiter) {
|
||||
cleaned := strings.TrimSpace(v)
|
||||
if len(cleaned) > 0 {
|
||||
if cleaned != "" {
|
||||
result = append(result, cleaned)
|
||||
}
|
||||
}
|
||||
@@ -708,6 +749,128 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
|
||||
return admin, nil
|
||||
}
|
||||
|
||||
func replacePlaceholders(field string, replacements map[string]string) string {
|
||||
for k, v := range replacements {
|
||||
field = strings.ReplaceAll(field, k, v)
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
func getCryptFsFromTemplate(fsConfig vfs.CryptFsConfig, replacements map[string]string) vfs.CryptFsConfig {
|
||||
if fsConfig.Passphrase != nil {
|
||||
if fsConfig.Passphrase.IsPlain() {
|
||||
payload := replacePlaceholders(fsConfig.Passphrase.GetPayload(), replacements)
|
||||
fsConfig.Passphrase = kms.NewPlainSecret(payload)
|
||||
}
|
||||
}
|
||||
return fsConfig
|
||||
}
|
||||
|
||||
func getS3FsFromTemplate(fsConfig vfs.S3FsConfig, replacements map[string]string) vfs.S3FsConfig {
|
||||
fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)
|
||||
fsConfig.AccessKey = replacePlaceholders(fsConfig.AccessKey, replacements)
|
||||
if fsConfig.AccessSecret != nil && fsConfig.AccessSecret.IsPlain() {
|
||||
payload := replacePlaceholders(fsConfig.AccessSecret.GetPayload(), replacements)
|
||||
fsConfig.AccessSecret = kms.NewPlainSecret(payload)
|
||||
}
|
||||
return fsConfig
|
||||
}
|
||||
|
||||
func getGCSFsFromTemplate(fsConfig vfs.GCSFsConfig, replacements map[string]string) vfs.GCSFsConfig {
|
||||
fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)
|
||||
return fsConfig
|
||||
}
|
||||
|
||||
func getAzBlobFsFromTemplate(fsConfig vfs.AzBlobFsConfig, replacements map[string]string) vfs.AzBlobFsConfig {
|
||||
fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)
|
||||
fsConfig.AccountName = replacePlaceholders(fsConfig.AccountName, replacements)
|
||||
if fsConfig.AccountKey != nil && fsConfig.AccountKey.IsPlain() {
|
||||
payload := replacePlaceholders(fsConfig.AccountKey.GetPayload(), replacements)
|
||||
fsConfig.AccountKey = kms.NewPlainSecret(payload)
|
||||
}
|
||||
return fsConfig
|
||||
}
|
||||
|
||||
func getSFTPFsFromTemplate(fsConfig vfs.SFTPFsConfig, replacements map[string]string) vfs.SFTPFsConfig {
|
||||
fsConfig.Prefix = replacePlaceholders(fsConfig.Prefix, replacements)
|
||||
fsConfig.Username = replacePlaceholders(fsConfig.Username, replacements)
|
||||
if fsConfig.Password != nil && fsConfig.Password.IsPlain() {
|
||||
payload := replacePlaceholders(fsConfig.Password.GetPayload(), replacements)
|
||||
fsConfig.Password = kms.NewPlainSecret(payload)
|
||||
}
|
||||
return fsConfig
|
||||
}
|
||||
|
||||
func getUserFromTemplate(user dataprovider.User, template userTemplateFields) dataprovider.User {
|
||||
user.Username = template.Username
|
||||
user.Password = template.Password
|
||||
user.PublicKeys = nil
|
||||
if template.PublicKey != "" {
|
||||
user.PublicKeys = append(user.PublicKeys, template.PublicKey)
|
||||
}
|
||||
replacements := make(map[string]string)
|
||||
replacements["%username%"] = user.Username
|
||||
user.Password = replacePlaceholders(user.Password, replacements)
|
||||
replacements["%password%"] = user.Password
|
||||
|
||||
user.HomeDir = replacePlaceholders(user.HomeDir, replacements)
|
||||
var vfolders []vfs.VirtualFolder
|
||||
for _, vfolder := range user.VirtualFolders {
|
||||
vfolder.MappedPath = replacePlaceholders(vfolder.MappedPath, replacements)
|
||||
vfolders = append(vfolders, vfolder)
|
||||
}
|
||||
user.VirtualFolders = vfolders
|
||||
user.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements)
|
||||
|
||||
switch user.FsConfig.Provider {
|
||||
case dataprovider.CryptedFilesystemProvider:
|
||||
user.FsConfig.CryptConfig = getCryptFsFromTemplate(user.FsConfig.CryptConfig, replacements)
|
||||
case dataprovider.S3FilesystemProvider:
|
||||
user.FsConfig.S3Config = getS3FsFromTemplate(user.FsConfig.S3Config, replacements)
|
||||
case dataprovider.GCSFilesystemProvider:
|
||||
user.FsConfig.GCSConfig = getGCSFsFromTemplate(user.FsConfig.GCSConfig, replacements)
|
||||
case dataprovider.AzureBlobFilesystemProvider:
|
||||
user.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(user.FsConfig.AzBlobConfig, replacements)
|
||||
case dataprovider.SFTPFilesystemProvider:
|
||||
user.FsConfig.SFTPConfig = getSFTPFsFromTemplate(user.FsConfig.SFTPConfig, replacements)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
/*func decryptSecretsForTemplateUser(user *dataprovider.User) error {
|
||||
user.SetEmptySecretsIfNil()
|
||||
switch user.FsConfig.Provider {
|
||||
case dataprovider.CryptedFilesystemProvider:
|
||||
if user.FsConfig.CryptConfig.Passphrase.IsEncrypted() {
|
||||
return user.FsConfig.CryptConfig.Passphrase.Decrypt()
|
||||
}
|
||||
case dataprovider.S3FilesystemProvider:
|
||||
if user.FsConfig.S3Config.AccessSecret.IsEncrypted() {
|
||||
return user.FsConfig.S3Config.AccessSecret.Decrypt()
|
||||
}
|
||||
case dataprovider.GCSFilesystemProvider:
|
||||
if user.FsConfig.GCSConfig.Credentials.IsEncrypted() {
|
||||
return user.FsConfig.GCSConfig.Credentials.Decrypt()
|
||||
}
|
||||
case dataprovider.AzureBlobFilesystemProvider:
|
||||
if user.FsConfig.AzBlobConfig.AccountKey.IsEncrypted() {
|
||||
return user.FsConfig.AzBlobConfig.AccountKey.Decrypt()
|
||||
}
|
||||
case dataprovider.SFTPFilesystemProvider:
|
||||
if user.FsConfig.SFTPConfig.Password.IsEncrypted() {
|
||||
if err := user.FsConfig.SFTPConfig.Password.Decrypt(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if user.FsConfig.SFTPConfig.PrivateKey.IsEncrypted() {
|
||||
return user.FsConfig.SFTPConfig.PrivateKey.Decrypt()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}*/
|
||||
|
||||
func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
||||
var user dataprovider.User
|
||||
err := r.ParseMultipartForm(maxRequestSize)
|
||||
@@ -1004,18 +1167,13 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
||||
renderTemplate(w, templateUsers, data)
|
||||
}
|
||||
|
||||
func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("cloneFrom") != "" {
|
||||
username := r.URL.Query().Get("cloneFrom")
|
||||
func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("from") != "" {
|
||||
username := r.URL.Query().Get("from")
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if err == nil {
|
||||
user.ID = 0
|
||||
user.Username = ""
|
||||
if err := user.DecryptSecrets(); err != nil {
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
renderAddUserPage(w, r, user, "")
|
||||
user.SetEmptySecrets()
|
||||
renderUserPage(w, r, &user, userPageModeTemplate, "")
|
||||
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
@@ -1023,7 +1181,57 @@ func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
} else {
|
||||
user := dataprovider.User{Status: 1}
|
||||
renderAddUserPage(w, r, user, "")
|
||||
renderUserPage(w, r, &user, userPageModeTemplate, "")
|
||||
}
|
||||
}
|
||||
|
||||
func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
templateUser, err := getUserFromPostFields(r)
|
||||
if err != nil {
|
||||
renderMessagePage(w, r, "Error parsing user fields", "", http.StatusBadRequest, err, "")
|
||||
return
|
||||
}
|
||||
|
||||
var dump dataprovider.BackupData
|
||||
dump.Version = dataprovider.DumpVersion
|
||||
|
||||
userTmplFields := getUsersForTemplate(r)
|
||||
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, "")
|
||||
return
|
||||
}
|
||||
dump.Users = append(dump.Users, u)
|
||||
}
|
||||
|
||||
if len(dump.Users) == 0 {
|
||||
renderMessagePage(w, r, "No users to export", "No valid users found, export is not possible", 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)
|
||||
}
|
||||
|
||||
func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
|
||||
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.SetEmptySecrets()
|
||||
renderUserPage(w, r, &user, userPageModeAdd, "")
|
||||
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
}
|
||||
} else {
|
||||
user := dataprovider.User{Status: 1}
|
||||
renderUserPage(w, r, &user, userPageModeAdd, "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1031,7 +1239,7 @@ func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
|
||||
username := getURLParam(r, "username")
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if err == nil {
|
||||
renderUpdateUserPage(w, r, user, "")
|
||||
renderUserPage(w, r, &user, userPageModeUpdate, "")
|
||||
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
@@ -1043,14 +1251,14 @@ func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
user, err := getUserFromPostFields(r)
|
||||
if err != nil {
|
||||
renderAddUserPage(w, r, user, err.Error())
|
||||
renderUserPage(w, r, &user, userPageModeAdd, err.Error())
|
||||
return
|
||||
}
|
||||
err = dataprovider.AddUser(&user)
|
||||
if err == nil {
|
||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||
} else {
|
||||
renderAddUserPage(w, r, user, err.Error())
|
||||
renderUserPage(w, r, &user, userPageModeAdd, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1067,7 +1275,7 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
updatedUser, err := getUserFromPostFields(r)
|
||||
if err != nil {
|
||||
renderUpdateUserPage(w, r, user, err.Error())
|
||||
renderUserPage(w, r, &user, userPageModeUpdate, err.Error())
|
||||
return
|
||||
}
|
||||
updatedUser.ID = user.ID
|
||||
@@ -1087,7 +1295,7 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||
} else {
|
||||
renderUpdateUserPage(w, r, user, err.Error())
|
||||
renderUserPage(w, r, &user, userPageModeUpdate, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user