mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 07:30:25 +00:00
web ui: allow to create multiple users from a template
This commit is contained in:
parent
5fcbf2528f
commit
54321c5240
14 changed files with 532 additions and 86 deletions
|
@ -397,7 +397,7 @@ func (p *BoltProvider) userExists(username string) (User, error) {
|
|||
}
|
||||
|
||||
func (p *BoltProvider) addUser(user *User) error {
|
||||
err := validateUser(user)
|
||||
err := ValidateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -437,7 +437,7 @@ func (p *BoltProvider) addUser(user *User) error {
|
|||
}
|
||||
|
||||
func (p *BoltProvider) updateUser(user *User) error {
|
||||
err := validateUser(user)
|
||||
err := ValidateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1003,7 +1003,7 @@ func updateV4BoltCompatUser(dbHandle *bolt.DB, user compatUserV4) error {
|
|||
}
|
||||
|
||||
func updateV4BoltUser(dbHandle *bolt.DB, user User) error {
|
||||
err := validateUser(&user)
|
||||
err := ValidateUser(&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1323,7 +1323,9 @@ func validateFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateUser(user *User) error {
|
||||
// ValidateUser returns an error if the user is not valid
|
||||
// FIXME: this should be defined as User struct method
|
||||
func ValidateUser(user *User) error {
|
||||
user.SetEmptySecretsIfNil()
|
||||
buildUserHomeDir(user)
|
||||
if err := validateBaseParams(user); err != nil {
|
||||
|
|
|
@ -185,7 +185,7 @@ func (p *MemoryProvider) addUser(user *User) error {
|
|||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
err := validateUser(user)
|
||||
err := ValidateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ func (p *MemoryProvider) updateUser(user *User) error {
|
|||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
err := validateUser(user)
|
||||
err := ValidateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -306,7 +306,7 @@ func sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error {
|
|||
}
|
||||
|
||||
func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
|
||||
err := validateUser(user)
|
||||
err := ValidateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -360,7 +360,7 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
|
|||
}
|
||||
|
||||
func sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error {
|
||||
err := validateUser(user)
|
||||
err := ValidateUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -261,6 +261,16 @@ func (u *User) HideConfidentialData() {
|
|||
}
|
||||
}
|
||||
|
||||
// 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.CryptConfig.Passphrase = kms.NewEmptySecret()
|
||||
u.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()
|
||||
u.FsConfig.SFTPConfig.PrivateKey = kms.NewEmptySecret()
|
||||
}
|
||||
|
||||
// DecryptSecrets tries to decrypts kms secrets
|
||||
func (u *User) DecryptSecrets() error {
|
||||
switch u.FsConfig.Provider {
|
||||
|
@ -801,12 +811,12 @@ func (u *User) GetExpirationDateAsString() string {
|
|||
}
|
||||
|
||||
// GetAllowedIPAsString returns the allowed IP as comma separated string
|
||||
func (u User) GetAllowedIPAsString() string {
|
||||
func (u *User) GetAllowedIPAsString() string {
|
||||
return strings.Join(u.Filters.AllowedIP, ",")
|
||||
}
|
||||
|
||||
// GetDeniedIPAsString returns the denied IP as comma separated string
|
||||
func (u User) GetDeniedIPAsString() string {
|
||||
func (u *User) GetDeniedIPAsString() string {
|
||||
return strings.Join(u.Filters.DeniedIP, ",")
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ const (
|
|||
webScanVFolderPath = "/web/folder-quota-scans"
|
||||
webQuotaScanPath = "/web/quota-scans"
|
||||
webChangeAdminPwdPath = "/web/changepwd/admin"
|
||||
webTemplateUser = "/web/template/user"
|
||||
webStaticFilesPath = "/static"
|
||||
// MaxRestoreSize defines the max size for the loaddata input file
|
||||
MaxRestoreSize = 10485760 // 10 MB
|
||||
|
|
|
@ -76,6 +76,7 @@ const (
|
|||
webMaintenancePath = "/web/maintenance"
|
||||
webRestorePath = "/web/restore"
|
||||
webChangeAdminPwdPath = "/web/changepwd/admin"
|
||||
webTemplateUser = "/web/template/user"
|
||||
httpBaseURL = "http://127.0.0.1:8081"
|
||||
configDir = ".."
|
||||
httpsCert = `-----BEGIN CERTIFICATE-----
|
||||
|
@ -2214,11 +2215,16 @@ func TestProviderErrors(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.Remove(backupFilePath)
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodGet, webUserPath+"?cloneFrom=user", nil)
|
||||
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)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf := config.GetProviderConf()
|
||||
|
@ -4439,49 +4445,163 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusOK, rr)
|
||||
}
|
||||
|
||||
func TestRenderUserTemplateMock(t *testing.T) {
|
||||
token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodGet, webTemplateUser, nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webTemplateUser+fmt.Sprintf("?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, webTemplateUser+"?from=unknown", 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 TestRenderWebCloneUserMock(t *testing.T) {
|
||||
token, err := getJWTTokenFromTestServer(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("?cloneFrom=%v", user.Username), nil)
|
||||
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("?cloneFrom=%v", altAdminPassword), nil)
|
||||
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)
|
||||
|
||||
if config.GetProviderConf().Driver != "memory" {
|
||||
user.FsConfig = dataprovider.Filesystem{
|
||||
Provider: dataprovider.CryptedFilesystemProvider,
|
||||
CryptConfig: vfs.CryptFsConfig{
|
||||
Passphrase: kms.NewPlainSecret("secret"),
|
||||
},
|
||||
}
|
||||
err = user.FsConfig.CryptConfig.Passphrase.Encrypt()
|
||||
assert.NoError(t, err)
|
||||
user.FsConfig.CryptConfig.Passphrase.SetStatus(kms.SecretStatusAWS)
|
||||
user.Password = defaultPassword
|
||||
err = dataprovider.UpdateUser(&user)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webUserPath+fmt.Sprintf("?cloneFrom=%v", user.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserTemplateMock(t *testing.T) {
|
||||
token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
user.FsConfig.Provider = dataprovider.S3FilesystemProvider
|
||||
user.FsConfig.S3Config.Bucket = "test"
|
||||
user.FsConfig.S3Config.Region = "eu-central-1"
|
||||
user.FsConfig.S3Config.AccessKey = "%username%"
|
||||
user.FsConfig.S3Config.KeyPrefix = "somedir/subdir/"
|
||||
user.FsConfig.S3Config.UploadPartSize = 5
|
||||
user.FsConfig.S3Config.UploadConcurrency = 4
|
||||
form := make(url.Values)
|
||||
form.Set("username", user.Username)
|
||||
form.Set("home_dir", filepath.Join(os.TempDir(), "%username%"))
|
||||
form.Set("uid", "0")
|
||||
form.Set("gid", strconv.FormatInt(int64(user.GID), 10))
|
||||
form.Set("max_sessions", strconv.FormatInt(int64(user.MaxSessions), 10))
|
||||
form.Set("quota_size", strconv.FormatInt(user.QuotaSize, 10))
|
||||
form.Set("quota_files", strconv.FormatInt(int64(user.QuotaFiles), 10))
|
||||
form.Set("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("sub_dirs_permissions", "")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("allowed_ip", "")
|
||||
form.Set("denied_ip", "")
|
||||
form.Set("fs_provider", "1")
|
||||
form.Set("s3_bucket", user.FsConfig.S3Config.Bucket)
|
||||
form.Set("s3_region", user.FsConfig.S3Config.Region)
|
||||
form.Set("s3_access_key", "%username%")
|
||||
form.Set("s3_access_secret", "%password%")
|
||||
form.Set("s3_key_prefix", "base/%username%")
|
||||
form.Set("allowed_extensions", "/dir1::.jpg,.png")
|
||||
form.Set("denied_extensions", "/dir2::.zip")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
// test invalid s3_upload_part_size
|
||||
form.Set("s3_upload_part_size", "a")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ := http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
form.Set("s3_upload_part_size", strconv.FormatInt(user.FsConfig.S3Config.UploadPartSize, 10))
|
||||
form.Set("s3_upload_concurrency", strconv.Itoa(user.FsConfig.S3Config.UploadConcurrency))
|
||||
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
|
||||
form.Set("users", "user1::password1::invalid-pkey")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
require.Contains(t, rr.Body.String(), "Error validating user")
|
||||
|
||||
form.Set("users", "user1:password1")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
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")
|
||||
|
||||
form.Set("users", "user1::password1\nuser2::password2::"+testPubKey+"\nuser3::::")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
var dump dataprovider.BackupData
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &dump)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, dump.Users, 2)
|
||||
user1 := dump.Users[0]
|
||||
user2 := dump.Users[1]
|
||||
require.Equal(t, "user1", user1.Username)
|
||||
require.Equal(t, dataprovider.S3FilesystemProvider, user1.FsConfig.Provider)
|
||||
require.Equal(t, "user2", user2.Username)
|
||||
require.Equal(t, dataprovider.S3FilesystemProvider, user2.FsConfig.Provider)
|
||||
require.Len(t, user2.PublicKeys, 1)
|
||||
require.Equal(t, filepath.Join(os.TempDir(), user1.Username), user1.HomeDir)
|
||||
require.Equal(t, filepath.Join(os.TempDir(), user2.Username), user2.HomeDir)
|
||||
require.Equal(t, user1.Username, user1.FsConfig.S3Config.AccessKey)
|
||||
require.Equal(t, user2.Username, user2.FsConfig.S3Config.AccessKey)
|
||||
require.Equal(t, path.Join("base", user1.Username)+"/", user1.FsConfig.S3Config.KeyPrefix)
|
||||
require.Equal(t, path.Join("base", user2.Username)+"/", user2.FsConfig.S3Config.KeyPrefix)
|
||||
require.True(t, user1.FsConfig.S3Config.AccessSecret.IsEncrypted())
|
||||
err = user1.FsConfig.S3Config.AccessSecret.Decrypt()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "password1", user1.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
require.True(t, user2.FsConfig.S3Config.AccessSecret.IsEncrypted())
|
||||
err = user2.FsConfig.S3Config.AccessSecret.Decrypt()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "password2", user2.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
}
|
||||
|
||||
func TestWebUserS3Mock(t *testing.T) {
|
||||
token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -29,7 +29,9 @@ import (
|
|||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -705,3 +707,51 @@ func TestVerifyTLSConnection(t *testing.T) {
|
|||
|
||||
certMgr = oldCertMgr
|
||||
}
|
||||
|
||||
func TestGetUserFromTemplate(t *testing.T) {
|
||||
user := dataprovider.User{
|
||||
Status: 1,
|
||||
}
|
||||
user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: "dir%username%",
|
||||
},
|
||||
})
|
||||
|
||||
username := "userTemplate"
|
||||
password := "pwdTemplate"
|
||||
templateFields := userTemplateFields{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
userTemplate := getUserFromTemplate(user, templateFields)
|
||||
require.Len(t, userTemplate.VirtualFolders, 1)
|
||||
require.Equal(t, "dir"+username, userTemplate.VirtualFolders[0].MappedPath)
|
||||
|
||||
user.FsConfig.Provider = dataprovider.CryptedFilesystemProvider
|
||||
user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("%password%")
|
||||
userTemplate = getUserFromTemplate(user, templateFields)
|
||||
require.Equal(t, password, userTemplate.FsConfig.CryptConfig.Passphrase.GetPayload())
|
||||
|
||||
user.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
user.FsConfig.GCSConfig.KeyPrefix = "%username%%password%"
|
||||
userTemplate = getUserFromTemplate(user, templateFields)
|
||||
require.Equal(t, username+password, userTemplate.FsConfig.GCSConfig.KeyPrefix)
|
||||
|
||||
user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider
|
||||
user.FsConfig.AzBlobConfig.KeyPrefix = "a%username%"
|
||||
user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("pwd%password%%username%")
|
||||
userTemplate = getUserFromTemplate(user, templateFields)
|
||||
require.Equal(t, "a"+username, userTemplate.FsConfig.AzBlobConfig.KeyPrefix)
|
||||
require.Equal(t, "pwd"+password+username, userTemplate.FsConfig.AzBlobConfig.AccountKey.GetPayload())
|
||||
|
||||
user.FsConfig.Provider = dataprovider.SFTPFilesystemProvider
|
||||
user.FsConfig.SFTPConfig.Prefix = "%username%"
|
||||
user.FsConfig.SFTPConfig.Username = "sftp_%username%"
|
||||
user.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftp%password%")
|
||||
userTemplate = getUserFromTemplate(user, templateFields)
|
||||
require.Equal(t, username, userTemplate.FsConfig.SFTPConfig.Prefix)
|
||||
require.Equal(t, "sftp_"+username, userTemplate.FsConfig.SFTPConfig.Username)
|
||||
require.Equal(t, "sftp"+password, userTemplate.FsConfig.SFTPConfig.Password.GetPayload())
|
||||
}
|
||||
|
|
|
@ -369,6 +369,9 @@ func (s *httpdServer) initializeRouter() {
|
|||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, handleWebMaintenance)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webRestorePath, handleWebRestore)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
|
||||
Get(webTemplateUser, handleWebTemplateUserGet)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateUser, handleWebTemplateUserPost)
|
||||
})
|
||||
|
||||
router.Group(func(router chi.Router) {
|
||||
|
|
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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ function deleteAction() {
|
|||
var table = $('#dataTable').DataTable();
|
||||
table.button('delete:name').enable(false);
|
||||
var folderPath = table.row({ selected: true }).data()[0];
|
||||
var path = '{{.FolderURL}}' + "?folder_path=" + encodeURIComponent(folderPath);
|
||||
var path = '{{.FolderURL}}' + "?folder-path=" + encodeURIComponent(folderPath);
|
||||
$('#deleteModal').modal('hide');
|
||||
$.ajax({
|
||||
url: path,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{{define "page_body"}}
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Restore</h6>
|
||||
<h6 class="m-0 font-weight-bold text-primary">Import</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .Error}}
|
||||
|
@ -20,7 +20,7 @@
|
|||
<input type="file" class="form-control-file" id="idBackupFile" name="backup_file"
|
||||
aria-describedby="BackupFileHelpBlock">
|
||||
<small id="BackupFileHelpBlock" class="form-text text-muted">
|
||||
Restore data from a JSON backup file
|
||||
Import data from a JSON backup file
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,7 +44,7 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Restore</button>
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Import</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<!-- Page Heading -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">{{if .IsAdd}}Add a new user{{else}}Edit user{{end}}</h6>
|
||||
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .Error}}
|
||||
|
@ -19,14 +19,47 @@
|
|||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="user_form" enctype="multipart/form-data" action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
||||
{{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.
|
||||
<br>
|
||||
The following placeholders are supported:
|
||||
<br><br>
|
||||
<ul>
|
||||
<li><span class="text-success">%username%</span> will be replaced with the specified username</li>
|
||||
<li><span class="text-success">%password%</span> will be replaced with the specified password</li>
|
||||
</ul>
|
||||
The generated users file can be imported from the "Maintenance" section.
|
||||
{{if .User.Username}}
|
||||
<br>
|
||||
Please note that no credentials were copied from user "{{.User.Username}}", you have to set them explicitly.
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="user_form" enctype="multipart/form-data" action="{{.CurrentURL}}" method="POST" autocomplete="off" {{if eq .Mode 3}}target="_blank"{{end}}>
|
||||
{{if eq .Mode 3}}
|
||||
<div class="form-group row">
|
||||
<label for="idUsers" class="col-sm-2 col-form-label">Users</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idUsers" name="users" rows="5" required
|
||||
aria-describedby="usersHelpBlock"></textarea>
|
||||
<small id="usersHelpBlock" class="form-text text-muted">
|
||||
Specify the username and at least one of the password and public key. Each line must be username::password::public-key
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="username" id="idUsername" value="{{.User.Username}}">
|
||||
{{else}}
|
||||
<div class="form-group row">
|
||||
<label for="idUsername" class="col-sm-2 col-form-label">Username</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idUsername" name="username" placeholder=""
|
||||
value="{{.User.Username}}" maxlength="255" autocomplete="nope" required {{if not .IsAdd}}readonly{{end}}>
|
||||
value="{{.User.Username}}" maxlength="255" autocomplete="nope" required {{if ge .Mode 2}}readonly{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idStatus" class="col-sm-2 col-form-label">Status</label>
|
||||
|
@ -48,12 +81,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if ne .Mode 3}}
|
||||
<div class="form-group row">
|
||||
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idPassword" name="password" placeholder="" {{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}>
|
||||
{{if not .IsAdd}}
|
||||
<input type="password" class="form-control" id="idPassword" name="password" placeholder="" {{if eq .Mode 2}}aria-describedby="pwdHelpBlock" {{end}}>
|
||||
{{if eq .Mode 2}}
|
||||
<small id="pwdHelpBlock" class="form-text text-muted">
|
||||
If empty the current password will not be changed
|
||||
</small>
|
||||
|
@ -71,7 +104,7 @@
|
|||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{end}}
|
||||
<div class="form-group row">
|
||||
<label for="idProtocols" class="col-sm-2 col-form-label">Denied protocols</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -612,7 +645,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{if not .IsAdd}}
|
||||
{{if eq .Mode 2}}
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idDisconnect" name="disconnect"
|
||||
|
@ -626,7 +659,7 @@
|
|||
{{end}}
|
||||
|
||||
<input type="hidden" name="expiration_date" id="hidden_start_datetime" value="">
|
||||
<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 px-3">{{if eq .Mode 3}}Generate and export users{{else}}Submit{{end}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -153,12 +153,27 @@
|
|||
name: 'clone',
|
||||
action: function (e, dt, node, config) {
|
||||
var username = dt.row({ selected: true }).data()[1];
|
||||
var path = '{{.UserURL}}' + "?cloneFrom=" + encodeURIComponent(username);
|
||||
var path = '{{.UserURL}}' + "?clone-from=" + encodeURIComponent(username);
|
||||
window.location.href = path;
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.template = {
|
||||
text: 'Template',
|
||||
name: 'template',
|
||||
action: function (e, dt, node, config) {
|
||||
var selectedRows = table.rows({ selected: true }).count();
|
||||
if (selectedRows == 1){
|
||||
var username = dt.row({ selected: true }).data()[1];
|
||||
var path = '{{.UserTemplateURL}}' + "?from=" + encodeURIComponent(username);
|
||||
window.location.href = path;
|
||||
} else {
|
||||
window.location.href = '{{.UserTemplateURL}}';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.delete = {
|
||||
text: 'Delete',
|
||||
name: 'delete',
|
||||
|
@ -240,6 +255,10 @@
|
|||
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}}
|
||||
|
|
Loading…
Reference in a new issue