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:
Nicola Murino 2022-03-27 16:32:21 +02:00
parent e0defafa26
commit ca32cd5e0e
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
15 changed files with 216 additions and 82 deletions

View file

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

View file

@ -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
View file

@ -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
View file

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

View file

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

View file

@ -235,9 +235,9 @@ type messagePage struct {
}
type userTemplateFields struct {
Username string
Password string
PublicKey string
Username string
Password string
PublicKeys []string
}
func loadAdminTemplates(templatesPath string) {
@ -714,9 +714,9 @@ func getUsersForTemplate(r *http.Request) []userTemplateFields {
users[username] = true
res = append(res, userTemplateFields{
Username: username,
Password: password,
PublicKey: publicKey,
Username: username,
Password: password,
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
user.Password = replacePlaceholders(user.Password, replacements)
replacements["%password%"] = user.Password
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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