mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-24 16:40:26 +00:00
allow placeholders for add/update users and folders
remove session token for S3, a temporary token is useless for our usage Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
e0defafa26
commit
ca32cd5e0e
15 changed files with 216 additions and 82 deletions
|
@ -43,7 +43,7 @@ var (
|
|||
portableS3Region string
|
||||
portableS3AccessKey string
|
||||
portableS3AccessSecret string
|
||||
portableS3SessionToken string
|
||||
portableS3RoleARN string
|
||||
portableS3Endpoint string
|
||||
portableS3StorageClass string
|
||||
portableS3ACL string
|
||||
|
@ -175,7 +175,7 @@ Please take a look at the usage below to customize the serving parameters`,
|
|||
Bucket: portableS3Bucket,
|
||||
Region: portableS3Region,
|
||||
AccessKey: portableS3AccessKey,
|
||||
SessionToken: portableS3SessionToken,
|
||||
RoleARN: portableS3RoleARN,
|
||||
Endpoint: portableS3Endpoint,
|
||||
StorageClass: portableS3StorageClass,
|
||||
ACL: portableS3ACL,
|
||||
|
@ -301,7 +301,7 @@ sftpfs => SFTP (legacy: 5)`)
|
|||
portableCmd.Flags().StringVar(&portableS3Region, "s3-region", "", "")
|
||||
portableCmd.Flags().StringVar(&portableS3AccessKey, "s3-access-key", "", "")
|
||||
portableCmd.Flags().StringVar(&portableS3AccessSecret, "s3-access-secret", "", "")
|
||||
portableCmd.Flags().StringVar(&portableS3SessionToken, "s3-session-token", "", "")
|
||||
portableCmd.Flags().StringVar(&portableS3RoleARN, "s3-role-arn", "", "")
|
||||
portableCmd.Flags().StringVar(&portableS3Endpoint, "s3-endpoint", "", "")
|
||||
portableCmd.Flags().StringVar(&portableS3StorageClass, "s3-storage-class", "", "")
|
||||
portableCmd.Flags().StringVar(&portableS3ACL, "s3-acl", "", "")
|
||||
|
|
|
@ -93,7 +93,7 @@ Flags:
|
|||
virtual folder identified by this
|
||||
prefix and its contents
|
||||
--s3-region string
|
||||
--s3-session-token string
|
||||
--s3-role-arn string
|
||||
--s3-storage-class string
|
||||
--s3-upload-concurrency int How many parts are uploaded in
|
||||
parallel (default 2)
|
||||
|
@ -125,7 +125,7 @@ Flags:
|
|||
-c, --ssh-commands strings SSH commands to enable.
|
||||
"*" means any supported SSH command
|
||||
including scp
|
||||
(default [md5sum,sha1sum,cd,pwd,scp])
|
||||
(default [md5sum,sha1sum,sha256sum,cd,pwd,scp])
|
||||
--start-directory string Alternate start directory.
|
||||
This is a virtual path not a filesystem
|
||||
path (default "/")
|
||||
|
|
4
go.mod
4
go.mod
|
@ -46,8 +46,8 @@ require (
|
|||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/cors v1.8.2
|
||||
github.com/rs/xid v1.4.0
|
||||
github.com/rs/zerolog v1.26.2-0.20220227173336-263b0bde3672
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220323191209-5d4ff81576b4
|
||||
github.com/rs/zerolog v1.26.2-0.20220312163309-e9344a8c507b
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220327080604-3c0f878c8c37
|
||||
github.com/shirou/gopsutil/v3 v3.22.2
|
||||
github.com/spf13/afero v1.8.2
|
||||
github.com/spf13/cobra v1.4.0
|
||||
|
|
8
go.sum
8
go.sum
|
@ -656,14 +656,14 @@ github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
|||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/rs/zerolog v1.26.2-0.20220227173336-263b0bde3672 h1:8tqGbO3HWm9kqGZxc8YLAND7QGJNppiwq+kMTpn8oOk=
|
||||
github.com/rs/zerolog v1.26.2-0.20220227173336-263b0bde3672/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/rs/zerolog v1.26.2-0.20220312163309-e9344a8c507b h1:72Plc168SB6g5i9cOEPaCuMK01bKNyniHnCpqPnX0Cg=
|
||||
github.com/rs/zerolog v1.26.2-0.20220312163309-e9344a8c507b/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220323191209-5d4ff81576b4 h1:zpu89DMnl3d5Bu3YlvQuu3/KsjkhERgvqgqz+Lnn4CY=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220323191209-5d4ff81576b4/go.mod h1:m5J7DH8unhD5RUsREFRiidP8zgBjup0+iQaxQnYHJOM=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220327080604-3c0f878c8c37 h1:ESruo35Pb9cCgaGslAmw6leGhzeL0pLzD6o+z9gsZeQ=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220327080604-3c0f878c8c37/go.mod h1:m5J7DH8unhD5RUsREFRiidP8zgBjup0+iQaxQnYHJOM=
|
||||
github.com/shirou/gopsutil/v3 v3.22.2 h1:wCrArWFkHYIdDxx/FSfF5RB4dpJYW6t7rcp3+zL8uks=
|
||||
github.com/shirou/gopsutil/v3 v3.22.2/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
|
|
|
@ -1906,7 +1906,6 @@ func TestUserRedactedPassword(t *testing.T) {
|
|||
u.FsConfig.S3Config.Bucket = "b"
|
||||
u.FsConfig.S3Config.Region = "eu-west-1"
|
||||
u.FsConfig.S3Config.AccessKey = "access-key"
|
||||
u.FsConfig.S3Config.SessionToken = "session token"
|
||||
u.FsConfig.S3Config.RoleARN = "myRoleARN"
|
||||
u.FsConfig.S3Config.AccessSecret = kms.NewSecret(sdkkms.SecretStatusRedacted, "access-secret", "", "")
|
||||
u.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?k=m"
|
||||
|
@ -2685,7 +2684,6 @@ func TestUserS3Config(t *testing.T) {
|
|||
user.FsConfig.S3Config.Region = "us-east-1" //nolint:goconst
|
||||
user.FsConfig.S3Config.AccessKey = "Server-Access-Key"
|
||||
user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("Server-Access-Secret")
|
||||
user.FsConfig.S3Config.SessionToken = "Session token"
|
||||
user.FsConfig.S3Config.RoleARN = "myRoleARN"
|
||||
user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000"
|
||||
user.FsConfig.S3Config.UploadPartSize = 8
|
||||
|
@ -15470,6 +15468,117 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
require.True(t, user2.Filters.DisableFsChecks)
|
||||
}
|
||||
|
||||
func TestUserPlaceholders(t *testing.T) {
|
||||
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
|
||||
assert.NoError(t, err)
|
||||
u := getTestUser()
|
||||
u.HomeDir = filepath.Join(os.TempDir(), "%username%_%password%")
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", u.Username)
|
||||
form.Set("home_dir", u.HomeDir)
|
||||
form.Set("password", u.Password)
|
||||
form.Set("status", strconv.Itoa(u.Status))
|
||||
form.Set("expiration_date", "")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("public_keys", testPubKey)
|
||||
form.Add("public_keys", testPubKey1)
|
||||
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("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("total_data_transfer", "0")
|
||||
form.Set("upload_data_transfer", "0")
|
||||
form.Set("download_data_transfer", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ := http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
|
||||
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(os.TempDir(), fmt.Sprintf("%v_%v", defaultUsername, defaultPassword)), user.HomeDir)
|
||||
|
||||
dbUser, err := dataprovider.UserExists(defaultUsername)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, dbUser.IsPasswordHashed())
|
||||
hashedPwd := dbUser.Password
|
||||
|
||||
form.Set("password", redactedSecret)
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webUserPath, defaultUsername), &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
|
||||
user, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(os.TempDir(), defaultUsername+"_%password%"), user.HomeDir)
|
||||
// check that the password was unchanged
|
||||
dbUser, err = dataprovider.UserExists(defaultUsername)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, dbUser.IsPasswordHashed())
|
||||
assert.Equal(t, hashedPwd, dbUser.Password)
|
||||
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFolderPlaceholders(t *testing.T) {
|
||||
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
|
||||
assert.NoError(t, err)
|
||||
folderName := "folderName"
|
||||
form := make(url.Values)
|
||||
form.Set("name", folderName)
|
||||
form.Set("mapped_path", filepath.Join(os.TempDir(), "%name%"))
|
||||
form.Set("description", "desc folder %name%")
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, err := http.NewRequest(http.MethodPost, webFolderPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
|
||||
folderGet, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(os.TempDir(), folderName), folderGet.MappedPath)
|
||||
assert.Equal(t, fmt.Sprintf("desc folder %v", folderName), folderGet.Description)
|
||||
|
||||
form.Set("mapped_path", filepath.Join(os.TempDir(), "%name%_%name%"))
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
|
||||
folderGet, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(os.TempDir(), fmt.Sprintf("%v_%v", folderName, folderName)), folderGet.MappedPath)
|
||||
assert.Equal(t, fmt.Sprintf("desc folder %v", folderName), folderGet.Description)
|
||||
|
||||
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFolderSaveFromTemplateMock(t *testing.T) {
|
||||
folder1 := "f1"
|
||||
folder2 := "f2"
|
||||
|
@ -15677,7 +15786,6 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
user.FsConfig.S3Config.Region = "eu-west-1"
|
||||
user.FsConfig.S3Config.AccessKey = "access-key"
|
||||
user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("access-secret")
|
||||
user.FsConfig.S3Config.SessionToken = "new session token"
|
||||
user.FsConfig.S3Config.RoleARN = "arn:aws:iam::123456789012:user/Development/product_1234/*"
|
||||
user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?a=b"
|
||||
user.FsConfig.S3Config.StorageClass = "Standard"
|
||||
|
@ -15717,7 +15825,6 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
form.Set("s3_region", user.FsConfig.S3Config.Region)
|
||||
form.Set("s3_access_key", user.FsConfig.S3Config.AccessKey)
|
||||
form.Set("s3_access_secret", user.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
form.Set("s3_session_token", user.FsConfig.S3Config.SessionToken)
|
||||
form.Set("s3_role_arn", user.FsConfig.S3Config.RoleARN)
|
||||
form.Set("s3_storage_class", user.FsConfig.S3Config.StorageClass)
|
||||
form.Set("s3_acl", user.FsConfig.S3Config.ACL)
|
||||
|
@ -15808,7 +15915,6 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
assert.Equal(t, updateUser.FsConfig.S3Config.Bucket, user.FsConfig.S3Config.Bucket)
|
||||
assert.Equal(t, updateUser.FsConfig.S3Config.Region, user.FsConfig.S3Config.Region)
|
||||
assert.Equal(t, updateUser.FsConfig.S3Config.AccessKey, user.FsConfig.S3Config.AccessKey)
|
||||
assert.Equal(t, updateUser.FsConfig.S3Config.SessionToken, user.FsConfig.S3Config.SessionToken)
|
||||
assert.Equal(t, updateUser.FsConfig.S3Config.RoleARN, user.FsConfig.S3Config.RoleARN)
|
||||
assert.Equal(t, updateUser.FsConfig.S3Config.StorageClass, user.FsConfig.S3Config.StorageClass)
|
||||
assert.Equal(t, updateUser.FsConfig.S3Config.ACL, user.FsConfig.S3Config.ACL)
|
||||
|
@ -16577,7 +16683,6 @@ func TestS3WebFolderMock(t *testing.T) {
|
|||
assert.Equal(t, S3Bucket, folder.FsConfig.S3Config.Bucket)
|
||||
assert.Equal(t, S3Region, folder.FsConfig.S3Config.Region)
|
||||
assert.Equal(t, S3AccessKey, folder.FsConfig.S3Config.AccessKey)
|
||||
assert.Equal(t, S3SessionToken, folder.FsConfig.S3Config.SessionToken)
|
||||
assert.NotEmpty(t, folder.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
assert.Equal(t, S3Endpoint, folder.FsConfig.S3Config.Endpoint)
|
||||
assert.Equal(t, S3StorageClass, folder.FsConfig.S3Config.StorageClass)
|
||||
|
@ -16626,7 +16731,6 @@ func TestS3WebFolderMock(t *testing.T) {
|
|||
assert.Equal(t, S3Bucket, folder.FsConfig.S3Config.Bucket)
|
||||
assert.Equal(t, S3Region, folder.FsConfig.S3Config.Region)
|
||||
assert.Equal(t, S3AccessKey, folder.FsConfig.S3Config.AccessKey)
|
||||
assert.Equal(t, S3SessionToken, folder.FsConfig.S3Config.SessionToken)
|
||||
assert.Equal(t, S3RoleARN, folder.FsConfig.S3Config.RoleARN)
|
||||
assert.NotEmpty(t, folder.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
assert.Equal(t, S3Endpoint, folder.FsConfig.S3Config.Endpoint)
|
||||
|
|
|
@ -237,7 +237,7 @@ type messagePage struct {
|
|||
type userTemplateFields struct {
|
||||
Username string
|
||||
Password string
|
||||
PublicKey string
|
||||
PublicKeys []string
|
||||
}
|
||||
|
||||
func loadAdminTemplates(templatesPath string) {
|
||||
|
@ -716,7 +716,7 @@ func getUsersForTemplate(r *http.Request) []userTemplateFields {
|
|||
res = append(res, userTemplateFields{
|
||||
Username: username,
|
||||
Password: password,
|
||||
PublicKey: publicKey,
|
||||
PublicKeys: []string{publicKey},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -982,7 +982,6 @@ func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
|
|||
config.Bucket = r.Form.Get("s3_bucket")
|
||||
config.Region = r.Form.Get("s3_region")
|
||||
config.AccessKey = r.Form.Get("s3_access_key")
|
||||
config.SessionToken = strings.TrimSpace(r.Form.Get("s3_session_token"))
|
||||
config.RoleARN = r.Form.Get("s3_role_arn")
|
||||
config.AccessSecret = getSecretFromFormField(r, "s3_access_secret")
|
||||
config.Endpoint = r.Form.Get("s3_endpoint")
|
||||
|
@ -1224,14 +1223,13 @@ func getSFTPFsFromTemplate(fsConfig vfs.SFTPFsConfig, replacements map[string]st
|
|||
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)
|
||||
}
|
||||
user.PublicKeys = template.PublicKeys
|
||||
replacements := make(map[string]string)
|
||||
replacements["%username%"] = user.Username
|
||||
if user.Password != "" && !user.IsPasswordHashed() {
|
||||
user.Password = replacePlaceholders(user.Password, replacements)
|
||||
replacements["%password%"] = user.Password
|
||||
}
|
||||
|
||||
user.HomeDir = replacePlaceholders(user.HomeDir, replacements)
|
||||
var vfolders []vfs.VirtualFolder
|
||||
|
@ -1263,19 +1261,31 @@ func getUserFromTemplate(user dataprovider.User, template userTemplateFields) da
|
|||
func getTransferLimits(r *http.Request) (int64, int64, int64, error) {
|
||||
dataTransferUL, err := strconv.ParseInt(r.Form.Get("upload_data_transfer"), 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
return 0, 0, 0, fmt.Errorf("invalid upload data transfer: %w", err)
|
||||
}
|
||||
dataTransferDL, err := strconv.ParseInt(r.Form.Get("download_data_transfer"), 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
return 0, 0, 0, fmt.Errorf("invalid download data transfer: %w", err)
|
||||
}
|
||||
dataTransferTotal, err := strconv.ParseInt(r.Form.Get("total_data_transfer"), 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
return 0, 0, 0, fmt.Errorf("invalid total data transfer: %w", err)
|
||||
}
|
||||
return dataTransferUL, dataTransferDL, dataTransferTotal, nil
|
||||
}
|
||||
|
||||
func getQuotaLimits(r *http.Request) (int64, int, error) {
|
||||
quotaSize, err := strconv.ParseInt(r.Form.Get("quota_size"), 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid quota size: %w", err)
|
||||
}
|
||||
quotaFiles, err := strconv.Atoi(r.Form.Get("quota_files"))
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid quota files: %w", err)
|
||||
}
|
||||
return quotaSize, quotaFiles, nil
|
||||
}
|
||||
|
||||
func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
||||
var user dataprovider.User
|
||||
err := r.ParseMultipartForm(maxRequestSize)
|
||||
|
@ -1285,31 +1295,27 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
|||
defer r.MultipartForm.RemoveAll() //nolint:errcheck
|
||||
uid, err := strconv.Atoi(r.Form.Get("uid"))
|
||||
if err != nil {
|
||||
return user, err
|
||||
return user, fmt.Errorf("invalid uid: %w", err)
|
||||
}
|
||||
gid, err := strconv.Atoi(r.Form.Get("gid"))
|
||||
if err != nil {
|
||||
return user, err
|
||||
return user, fmt.Errorf("invalid uid: %w", err)
|
||||
}
|
||||
maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
|
||||
if err != nil {
|
||||
return user, err
|
||||
return user, fmt.Errorf("invalid max sessions: %w", err)
|
||||
}
|
||||
quotaSize, err := strconv.ParseInt(r.Form.Get("quota_size"), 10, 64)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
quotaFiles, err := strconv.Atoi(r.Form.Get("quota_files"))
|
||||
quotaSize, quotaFiles, err := getQuotaLimits(r)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64)
|
||||
if err != nil {
|
||||
return user, err
|
||||
return user, fmt.Errorf("invalid upload bandwidth: %w", err)
|
||||
}
|
||||
bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64)
|
||||
if err != nil {
|
||||
return user, err
|
||||
return user, fmt.Errorf("invalid download bandwidth: %w", err)
|
||||
}
|
||||
dataTransferUL, dataTransferDL, dataTransferTotal, err := getTransferLimits(r)
|
||||
if err != nil {
|
||||
|
@ -1317,7 +1323,7 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
|||
}
|
||||
status, err := strconv.Atoi(r.Form.Get("status"))
|
||||
if err != nil {
|
||||
return user, err
|
||||
return user, fmt.Errorf("invalid status: %w", err)
|
||||
}
|
||||
expirationDateMillis := int64(0)
|
||||
expirationDateString := r.Form.Get("expiration_date")
|
||||
|
@ -1366,6 +1372,9 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
|||
FsConfig: fsConfig,
|
||||
}
|
||||
maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64)
|
||||
if err != nil {
|
||||
return user, fmt.Errorf("invalid max upload file size: %w", err)
|
||||
}
|
||||
user.Filters.MaxUploadFileSize = maxFileSize
|
||||
return user, err
|
||||
}
|
||||
|
@ -1912,6 +1921,11 @@ func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Reques
|
|||
s.renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
user = getUserFromTemplate(user, userTemplateFields{
|
||||
Username: user.Username,
|
||||
Password: user.Password,
|
||||
PublicKeys: user.PublicKeys,
|
||||
})
|
||||
err = dataprovider.AddUser(&user, claims.Username, ipAddr)
|
||||
if err == nil {
|
||||
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||
|
@ -1958,6 +1972,12 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
|
|||
user.FsConfig.AzBlobConfig.SASURL, user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase,
|
||||
user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey)
|
||||
|
||||
updatedUser = getUserFromTemplate(updatedUser, userTemplateFields{
|
||||
Username: updatedUser.Username,
|
||||
Password: updatedUser.Password,
|
||||
PublicKeys: updatedUser.PublicKeys,
|
||||
})
|
||||
|
||||
err = dataprovider.UpdateUser(&updatedUser, claims.Username, ipAddr)
|
||||
if err == nil {
|
||||
if len(r.Form.Get("disconnect")) > 0 {
|
||||
|
@ -2017,6 +2037,7 @@ func (s *httpdServer) handleWebAddFolderPost(w http.ResponseWriter, r *http.Requ
|
|||
return
|
||||
}
|
||||
folder.FsConfig = fsConfig
|
||||
folder = getFolderFromTemplate(folder, folder.Name)
|
||||
|
||||
err = dataprovider.AddFolder(&folder)
|
||||
if err == nil {
|
||||
|
@ -2073,7 +2094,7 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R
|
|||
s.renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
|
||||
return
|
||||
}
|
||||
updatedFolder := &vfs.BaseVirtualFolder{
|
||||
updatedFolder := vfs.BaseVirtualFolder{
|
||||
MappedPath: r.Form.Get("mapped_path"),
|
||||
Description: r.Form.Get("description"),
|
||||
}
|
||||
|
@ -2085,7 +2106,9 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R
|
|||
folder.FsConfig.AzBlobConfig.SASURL, folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase,
|
||||
folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey)
|
||||
|
||||
err = dataprovider.UpdateFolder(updatedFolder, folder.Users, claims.Username, ipAddr)
|
||||
updatedFolder = getFolderFromTemplate(updatedFolder, updatedFolder.Name)
|
||||
|
||||
err = dataprovider.UpdateFolder(&updatedFolder, folder.Users, claims.Username, ipAddr)
|
||||
if err != nil {
|
||||
s.renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
|
||||
return
|
||||
|
|
|
@ -1275,9 +1275,6 @@ func compareS3Config(expected *vfs.Filesystem, actual *vfs.Filesystem) error { /
|
|||
if expected.S3Config.AccessKey != actual.S3Config.AccessKey {
|
||||
return errors.New("fs S3 access key mismatch")
|
||||
}
|
||||
if expected.S3Config.SessionToken != actual.S3Config.SessionToken {
|
||||
return errors.New("fs S3 session token mismatch")
|
||||
}
|
||||
if expected.S3Config.RoleARN != actual.S3Config.RoleARN {
|
||||
return errors.New("fs S3 role ARN mismatch")
|
||||
}
|
||||
|
|
|
@ -4729,11 +4729,9 @@ components:
|
|||
type: string
|
||||
access_secret:
|
||||
$ref: '#/components/schemas/Secret'
|
||||
session_token:
|
||||
type: string
|
||||
role_arn:
|
||||
type: string
|
||||
description: 'IAM Role ARN to assume'
|
||||
description: 'Optional IAM Role ARN to assume'
|
||||
endpoint:
|
||||
type: string
|
||||
description: optional endpoint
|
||||
|
|
|
@ -33,7 +33,7 @@ var (
|
|||
|
||||
// Conf telemetry server configuration.
|
||||
type Conf struct {
|
||||
// The port used for serving HTTP requests. 0 disable the HTTP server. Default: 10000
|
||||
// The port used for serving HTTP requests. 0 disable the HTTP server. Default: 0
|
||||
BindPort int `json:"bind_port" mapstructure:"bind_port"`
|
||||
// The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1"
|
||||
BindAddress string `json:"bind_address" mapstructure:"bind_address"`
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<ul>
|
||||
<li><span class="text-success">%name%</span> will be replaced with the specified folder name</li>
|
||||
</ul>
|
||||
The generated folders file can be imported from the "Maintenance" section.
|
||||
The generated folders can be saved or exported. Exported folders can be imported from the "Maintenance" section of this SFTPGo instance or another.
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -70,13 +70,19 @@
|
|||
<label for="idS3StorageClass" class="col-sm-2 col-form-label">Storage Class</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idS3StorageClass" name="s3_storage_class" placeholder=""
|
||||
value="{{.S3Config.StorageClass}}" maxlength="255">
|
||||
value="{{.S3Config.StorageClass}}" maxlength="255" aria-describedby="S3StorageClassHelpBlock">
|
||||
<small id="S3StorageClassHelpBlock" class="form-text text-muted">
|
||||
Leave blank for default
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idS3Endpoint" class="col-sm-2 col-form-label">Endpoint</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idS3Endpoint" name="s3_endpoint" placeholder=""
|
||||
value="{{.S3Config.Endpoint}}" maxlength="255">
|
||||
value="{{.S3Config.Endpoint}}" maxlength="512" aria-describedby="S3EndpointHelpBlock">
|
||||
<small id="S3EndpointHelpBlock" class="form-text text-muted">
|
||||
For AWS S3, leave blank to use the default endpoint for the specified region
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -150,7 +156,7 @@
|
|||
<input type="text" class="form-control" id="idS3RoleARN" name="s3_role_arn" placeholder=""
|
||||
value="{{.S3Config.RoleARN}}" aria-describedby="S3RoleARNHelpBlock">
|
||||
<small id="S3RoleARNHelpBlock" class="form-text text-muted">
|
||||
IAM Role ARN to assume
|
||||
Optional IAM Role ARN to assume
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -161,7 +167,7 @@
|
|||
<input type="text" class="form-control" id="idS3ACL" name="s3_acl" placeholder=""
|
||||
value="{{.S3Config.ACL}}" maxlength="255" aria-describedby="S3ACLHelpBlock">
|
||||
<small id="S3ACLHelpBlock" class="form-text text-muted">
|
||||
ACL for uploaded objects. For more info take a look <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl" target="_blank">here</a>
|
||||
ACL for uploaded objects. Leave blank for default. For more info take a look <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl" target="_blank">here</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -177,20 +183,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row fsconfig fsconfig-s3fs">
|
||||
<label for="idS3SessionToken" class="col-sm-2 col-form-label">Session token</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idS3SessionToken" name="s3_session_token"
|
||||
rows="3">{{.S3Config.SessionToken}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group fsconfig fsconfig-s3fs">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idS3ForcePathStyle" name="s3_force_path_style"
|
||||
{{if .S3Config.ForcePathStyle}}checked{{end}}>
|
||||
<label for="idS3ForcePathStyle" class="form-check-label">Use path-style addressing, i.e., "`endpoint`/BUCKET/KEY"</label>
|
||||
<label for="idS3ForcePathStyle" class="form-check-label" aria-describedby="S3PathStyleHelpBlock">Use path-style addressing, i.e., "`endpoint`/BUCKET/KEY"</label>
|
||||
<small id="S3PathStyleHelpBlock" class="form-text text-muted">
|
||||
It is required for some compatible S3 backends
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -215,15 +215,21 @@
|
|||
<label for="idGCSStorageClass" class="col-sm-2 col-form-label">Storage Class</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idGCSStorageClass" name="gcs_storage_class" placeholder=""
|
||||
value="{{.GCSConfig.StorageClass}}" maxlength="255">
|
||||
value="{{.GCSConfig.StorageClass}}" maxlength="255" aria-describedby="GCSStorageClassHelpBlock">
|
||||
<small id="GCSStorageClassHelpBlock" class="form-text text-muted">
|
||||
Leave blank for default
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group fsconfig fsconfig-gcsfs">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idGCSAutoCredentials" name="gcs_auto_credentials"
|
||||
{{if gt .GCSConfig.AutomaticCredentials 0}}checked{{end}}>
|
||||
aria-describedby="GCSAutoCredentialsHelpBlock" {{if gt .GCSConfig.AutomaticCredentials 0}}checked{{end}}>
|
||||
<label for="idGCSAutoCredentials" class="form-check-label">Automatic credentials</label>
|
||||
<small id="GCSAutoCredentialsHelpBlock" class="form-text text-muted">
|
||||
Use default application credentials or credentials from environment
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -242,7 +248,7 @@
|
|||
<input type="text" class="form-control" id="idGCSACL" name="gcs_acl" placeholder=""
|
||||
value="{{.GCSConfig.ACL}}" maxlength="255" aria-describedby="GCSACLHelpBlock">
|
||||
<small id="GCSACLHelpBlock" class="form-text text-muted">
|
||||
ACL for uploaded objects. For more info refer to the JSON API <a href="https://cloud.google.com/storage/docs/access-control/lists#predefined-acl" target="_blank">here</a>
|
||||
ACL for uploaded objects. Leave blank for default. For more info refer to the JSON API <a href="https://cloud.google.com/storage/docs/access-control/lists#predefined-acl" target="_blank">here</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -272,15 +278,21 @@
|
|||
<div class="form-group row fsconfig fsconfig-azblobfs">
|
||||
<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=""
|
||||
<input type="password" class="form-control" id="idAzSASURL" name="az_sas_url" placeholder="" aria-describedby="AzSASURLHelpBlock"
|
||||
value="{{if .AzBlobConfig.SASURL.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.SASURL.GetPayload}}{{end}}">
|
||||
<small id="AzSASURLHelpBlock" class="form-text text-muted">
|
||||
Shared Access Signature URL can be used instead of account name/key
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row fsconfig fsconfig-azblobfs">
|
||||
<label for="idAzEndpoint" class="col-sm-2 col-form-label">Endpoint</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idAzEndpoint" name="az_endpoint" placeholder=""
|
||||
value="{{.AzBlobConfig.Endpoint}}" maxlength="255">
|
||||
aria-describedby="AzEndpointHelpBlock" value="{{.AzBlobConfig.Endpoint}}" maxlength="512">
|
||||
<small id="AzEndpointHelpBlock" class="form-text text-muted">
|
||||
Optional endpoint
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -362,8 +374,11 @@
|
|||
<label for="idCryptPassphrase" class="col-sm-2 col-form-label">Passphrase</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idCryptPassphrase" name="crypt_passphrase"
|
||||
placeholder=""
|
||||
placeholder="" aria-describedby="CryptPassphraseHelpBlock"
|
||||
value="{{if .CryptConfig.Passphrase.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.CryptConfig.Passphrase.GetPayload}}{{end}}">
|
||||
<small id="CryptPassphraseHelpBlock" class="form-text text-muted">
|
||||
Passphrase to derive the per-object encryption key
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -30,7 +30,9 @@
|
|||
<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.
|
||||
They will be replaced, with the specified username and password, in the paths and credentials of the configured storage backend.
|
||||
<br>
|
||||
The generated users can be saved or exported. Exported users can be imported from the "Maintenance" section of this SFTPGo instance or another.
|
||||
{{if .User.Username}}
|
||||
<br>
|
||||
Please note that no credentials were copied from user "{{.User.Username}}", you have to set them explicitly.
|
||||
|
|
|
@ -244,7 +244,6 @@ func (f *Filesystem) GetACopy() Filesystem {
|
|||
Bucket: f.S3Config.Bucket,
|
||||
Region: f.S3Config.Region,
|
||||
AccessKey: f.S3Config.AccessKey,
|
||||
SessionToken: f.S3Config.SessionToken,
|
||||
RoleARN: f.S3Config.RoleARN,
|
||||
Endpoint: f.S3Config.Endpoint,
|
||||
StorageClass: f.S3Config.StorageClass,
|
||||
|
|
|
@ -92,8 +92,7 @@ func NewS3Fs(connectionID, localTempDir, mountPath string, s3Config S3FsConfig)
|
|||
return fs, err
|
||||
}
|
||||
awsConfig.Credentials = aws.NewCredentialsCache(
|
||||
credentials.NewStaticCredentialsProvider(fs.config.AccessKey, fs.config.AccessSecret.GetPayload(),
|
||||
fs.config.SessionToken))
|
||||
credentials.NewStaticCredentialsProvider(fs.config.AccessKey, fs.config.AccessSecret.GetPayload(), ""))
|
||||
}
|
||||
if fs.config.Endpoint != "" {
|
||||
endpointResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
||||
|
|
|
@ -173,9 +173,6 @@ func (c *S3FsConfig) isEqual(other *S3FsConfig) bool {
|
|||
if c.AccessKey != other.AccessKey {
|
||||
return false
|
||||
}
|
||||
if c.SessionToken != other.SessionToken {
|
||||
return false
|
||||
}
|
||||
if c.RoleARN != other.RoleARN {
|
||||
return false
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue