From ca32cd5e0e626e977472f31dd2f48e22ed683fa6 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sun, 27 Mar 2022 16:32:21 +0200 Subject: [PATCH] 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 --- cmd/portable.go | 6 +- docs/portable-mode.md | 4 +- go.mod | 4 +- go.sum | 8 +-- httpd/httpd_test.go | 118 +++++++++++++++++++++++++++++-- httpd/webadmin.go | 81 +++++++++++++-------- httpdtest/httpdtest.go | 3 - openapi/openapi.yaml | 4 +- telemetry/telemetry.go | 2 +- templates/webadmin/folder.html | 2 +- templates/webadmin/fsconfig.html | 55 ++++++++------ templates/webadmin/user.html | 4 +- vfs/filesystem.go | 1 - vfs/s3fs.go | 3 +- vfs/vfs.go | 3 - 15 files changed, 216 insertions(+), 82 deletions(-) diff --git a/cmd/portable.go b/cmd/portable.go index 3f6ecd4c..6f78834d 100644 --- a/cmd/portable.go +++ b/cmd/portable.go @@ -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", "", "") diff --git a/docs/portable-mode.md b/docs/portable-mode.md index 91ac8123..ab29c3ca 100644 --- a/docs/portable-mode.md +++ b/docs/portable-mode.md @@ -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 "/") diff --git a/go.mod b/go.mod index 46891a7c..9eebfad9 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 3a06d5ec..88aaaaff 100644 --- a/go.sum +++ b/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= diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 4b098a0d..cf758d8d 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -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) diff --git a/httpd/webadmin.go b/httpd/webadmin.go index 3756d1f7..45783ca1 100644 --- a/httpd/webadmin.go +++ b/httpd/webadmin.go @@ -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 diff --git a/httpdtest/httpdtest.go b/httpdtest/httpdtest.go index bb75269a..eb109565 100644 --- a/httpdtest/httpdtest.go +++ b/httpdtest/httpdtest.go @@ -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") } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 20ee8fbf..677a08a9 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -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 diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 3d1a209c..002ce817 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -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"` diff --git a/templates/webadmin/folder.html b/templates/webadmin/folder.html index d4d8d1ea..6af267e7 100644 --- a/templates/webadmin/folder.html +++ b/templates/webadmin/folder.html @@ -23,7 +23,7 @@ - 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. {{end}} diff --git a/templates/webadmin/fsconfig.html b/templates/webadmin/fsconfig.html index 522f1ab2..96a45fb8 100644 --- a/templates/webadmin/fsconfig.html +++ b/templates/webadmin/fsconfig.html @@ -70,13 +70,19 @@
+ value="{{.S3Config.StorageClass}}" maxlength="255" aria-describedby="S3StorageClassHelpBlock"> + + Leave blank for default +
+ value="{{.S3Config.Endpoint}}" maxlength="512" aria-describedby="S3EndpointHelpBlock"> + + For AWS S3, leave blank to use the default endpoint for the specified region +
@@ -150,7 +156,7 @@ - IAM Role ARN to assume + Optional IAM Role ARN to assume @@ -161,7 +167,7 @@ - ACL for uploaded objects. For more info take a look here + ACL for uploaded objects. Leave blank for default. For more info take a look here @@ -177,20 +183,14 @@ -
- -
- -
-
- -
- + + + It is required for some compatible S3 backends +
@@ -215,15 +215,21 @@
+ value="{{.GCSConfig.StorageClass}}" maxlength="255" aria-describedby="GCSStorageClassHelpBlock"> + + Leave blank for default +
+ aria-describedby="GCSAutoCredentialsHelpBlock" {{if gt .GCSConfig.AutomaticCredentials 0}}checked{{end}}> + + Use default application credentials or credentials from environment +
@@ -242,7 +248,7 @@ - ACL for uploaded objects. For more info refer to the JSON API here + ACL for uploaded objects. Leave blank for default. For more info refer to the JSON API here @@ -272,15 +278,21 @@
- + + Shared Access Signature URL can be used instead of account name/key +
+ aria-describedby="AzEndpointHelpBlock" value="{{.AzBlobConfig.Endpoint}}" maxlength="512"> + + Optional endpoint +
@@ -362,8 +374,11 @@
+ + Passphrase to derive the per-object encryption key +
diff --git a/templates/webadmin/user.html b/templates/webadmin/user.html index 0ad00869..73c0548e 100644 --- a/templates/webadmin/user.html +++ b/templates/webadmin/user.html @@ -30,7 +30,9 @@
  • %username% will be replaced with the specified username
  • %password% will be replaced with the specified password
  • - 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. +
    + 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}}
    Please note that no credentials were copied from user "{{.User.Username}}", you have to set them explicitly. diff --git a/vfs/filesystem.go b/vfs/filesystem.go index 0950879d..950bda80 100644 --- a/vfs/filesystem.go +++ b/vfs/filesystem.go @@ -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, diff --git a/vfs/s3fs.go b/vfs/s3fs.go index 4f52d18e..a36b6fe9 100644 --- a/vfs/s3fs.go +++ b/vfs/s3fs.go @@ -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) { diff --git a/vfs/vfs.go b/vfs/vfs.go index 0e01edbc..a05d0b64 100644 --- a/vfs/vfs.go +++ b/vfs/vfs.go @@ -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 }