diff --git a/README.md b/README.md index da85912d..43b8166d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Full featured and highly configurable SFTP server - Quota support: accounts can have individual quota expressed as max total size and/or max number of files. - Bandwidth throttling is supported, with distinct settings for upload and download. - Per user maximum concurrent sessions. -- Per user permissions: list directories content, upload, overwrite, download, delete, rename, create directories, create symlinks, changing owner/group and mode can be enabled or disabled. +- Per user permissions: list directories content, upload, overwrite, download, delete, rename, create directories, create symlinks, changing owner/group and mode, changing access and modification times can be enabled or disabled. - Per user files/folders ownership: you can map all the users to the system account that runs SFTPGo (all platforms are supported) or you can run SFTPGo as root user and map each user or group of users to a different system account (*NIX only). - Configurable custom commands and/or HTTP notifications on files upload, download, delete, rename and on users add, update and delete. - Automatically terminating idle connections. @@ -150,7 +150,7 @@ The `sftpgo` configuration file contains the following sections: - `ciphers`, list of strings. Allowed ciphers. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L28 "Supported ciphers") - `macs`, list of strings. available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L84 "Supported MACs") - `login_banner_file`, path to the login banner file. The contents of the specified file, if any, are sent to the remote user before authentication is allowed. It can be a path relative to the config dir or an absolute one. Leave empty to send no login banner - - `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions and owner/group are executed. 1 means "ignore mode": requests for changing permissions and owner/group are silently ignored. + - `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored. - **"data_provider"**, the configuration for the data provider - `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `bolt`, `memory` - `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database. @@ -367,6 +367,7 @@ For each account the following properties can be configured: - `create_symlinks` create symbolic links is allowed - `chmod` changing file or directory permissions is allowed. On Windows, only the 0200 bit (owner writable) of mode is used; it controls whether the file's read-only attribute is set or cleared. The other bits are currently unused. Use mode 0400 for a read-only file and 0600 for a readable+writable file. - `chown` changing file or directory owner and group is allowed. Changing owner and group is not supported on Windows. + - `chtimes` changing file or directory access and modification time is allowed - `upload_bandwidth` maximum upload bandwidth as KB/s, 0 means unlimited. - `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited. @@ -456,7 +457,7 @@ The logs can be divided into the following categories: - `connection_id` string. Unique connection identifier - `protocol` string. `SFTP` or `SCP` - **"command logs"**, SFTP/SCP command logs: - - `sender` string. `Rename`, `Rmdir`, `Mkdir`, `Symlink`, `Remove`, `Chmod`, `Chown` + - `sender` string. `Rename`, `Rmdir`, `Mkdir`, `Symlink`, `Remove`, `Chmod`, `Chown`, `Chtimes` - `level` string - `username`, string - `file_path` string @@ -464,6 +465,8 @@ The logs can be divided into the following categories: - `filemode` string. Valid for sender `Chmod` otherwise empty - `uid` integer. Valid for sender `Chown` otherwise -1 - `gid` integer. Valid for sender `Chown` otherwise -1 + - `access_time` datetime as YYYY-MM-DDTHH:MM:SS. Valid for sender `Chtimes` otherwise empty + - `modification_time` datetime as YYYY-MM-DDTHH:MM:SS. Valid for sender `Chtimes` otherwise empty - `connection_id` string. Unique connection identifier - `protocol` string. `SFTP` or `SCP` - **"http logs"**, REST API logs: diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index c41b49e1..4948b7c4 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -65,7 +65,7 @@ var ( BoltDataProviderName, MemoryDataProviderName} // ValidPerms list that contains all the valid permissions for an user ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete, - PermCreateDirs, PermCreateSymlinks, PermChmod, PermChown} + PermCreateDirs, PermCreateSymlinks, PermChmod, PermChown, PermChtimes} config Config provider Provider sqlPlaceholders []string diff --git a/dataprovider/user.go b/dataprovider/user.go index c1825799..5b847e21 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -34,6 +34,8 @@ const ( PermChmod = "chmod" // changing file or directory owner and group is allowed PermChown = "chown" + // changing file or directory access and modification time is allowed + PermChtimes = "chtimes" ) // User defines an SFTP user diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index 7a34f6f6..de44ed08 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -545,6 +545,7 @@ components: - create_symlinks - chmod - chown + - chtimes description: > Permissions: * `*` - all permissions are granted @@ -558,6 +559,7 @@ components: * `create_symlinks` - create links is allowed * `chmod` changing file or directory permissions is allowed * `chown` changing file or directory owner and group is allowed + * `chtimes` changing file or directory access and modification time is allowed User: type: object properties: diff --git a/logger/logger.go b/logger/logger.go index 0eebebf7..add5858d 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -150,7 +150,7 @@ func TransferLog(operation string, path string, elapsed int64, size int64, user } // CommandLog logs an SFTP/SCP command -func CommandLog(command, path, target, user, fileMode, connectionID, protocol string, uid, gid int) { +func CommandLog(command, path, target, user, fileMode, connectionID, protocol string, uid, gid int, atime, mtime string) { logger.Info(). Timestamp(). Str("sender", command). @@ -160,6 +160,8 @@ func CommandLog(command, path, target, user, fileMode, connectionID, protocol st Str("filemode", fileMode). Int("uid", uid). Int("gid", gid). + Str("access_time", atime). + Str("modification_time", atime). Str("connection_id", connectionID). Str("protocol", protocol). Msg("") diff --git a/scripts/sftpgo_api_cli.py b/scripts/sftpgo_api_cli.py index 9043003e..068d6f00 100755 --- a/scripts/sftpgo_api_cli.py +++ b/scripts/sftpgo_api_cli.py @@ -160,7 +160,7 @@ def addCommonUserArguments(parser): parser.add_argument('-F', '--quota-files', type=int, default=0, help="default: %(default)s") parser.add_argument('-G', '--permissions', type=str, nargs='+', default=[], choices=['*', 'list', 'download', 'upload', 'overwrite', 'delete', 'rename', 'create_dirs', - 'create_symlinks', 'chmod', 'chown'], help='Default: %(default)s') + 'create_symlinks', 'chmod', 'chown', 'chtimes'], help='Default: %(default)s') parser.add_argument('-U', '--upload-bandwidth', type=int, default=0, help='Maximum upload bandwidth as KB/s, 0 means unlimited. Default: %(default)s') parser.add_argument('-D', '--download-bandwidth', type=int, default=0, diff --git a/sftpd/handler.go b/sftpd/handler.go index 2c57ed16..d92de322 100644 --- a/sftpd/handler.go +++ b/sftpd/handler.go @@ -229,10 +229,10 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) { return nil, sftp.ErrSSHFxPermissionDenied } - c.Log(logger.LevelDebug, logSender, "requested stat for file: %#v", p) + c.Log(logger.LevelDebug, logSender, "requested stat for path: %#v", p) s, err := os.Stat(p) if err != nil { - c.Log(logger.LevelWarn, logSender, "error running Stat on file: %#v", err) + c.Log(logger.LevelWarn, logSender, "error running stat on path: %#v", err) return nil, getSFTPErrorFromOSError(err) } @@ -270,7 +270,7 @@ func (c Connection) handleSFTPSetstat(path string, request *sftp.Request) error c.Log(logger.LevelWarn, logSender, "failed to chmod path %#v, mode: %v, err: %v", path, fileMode.String(), err) return getSFTPErrorFromOSError(err) } - logger.CommandLog(chmodLogSender, path, "", c.User.Username, fileMode.String(), c.ID, c.protocol, -1, -1) + logger.CommandLog(chmodLogSender, path, "", c.User.Username, fileMode.String(), c.ID, c.protocol, -1, -1, "", "") return nil } else if attrFlags.UidGid { if !c.User.HasPerm(dataprovider.PermChown) { @@ -282,7 +282,24 @@ func (c Connection) handleSFTPSetstat(path string, request *sftp.Request) error c.Log(logger.LevelWarn, logSender, "failed to chown path %#v, uid: %v, gid: %v, err: %v", path, uid, gid, err) return getSFTPErrorFromOSError(err) } - logger.CommandLog(chownLogSender, path, "", c.User.Username, "", c.ID, c.protocol, uid, gid) + logger.CommandLog(chownLogSender, path, "", c.User.Username, "", c.ID, c.protocol, uid, gid, "", "") + return nil + } else if attrFlags.Acmodtime { + if !c.User.HasPerm(dataprovider.PermChtimes) { + return sftp.ErrSSHFxPermissionDenied + } + dateFormat := "2006-01-02T15:04:05" // YYYY-MM-DDTHH:MM:SS + accessTime := time.Unix(int64(request.Attributes().Atime), 0) + modificationTime := time.Unix(int64(request.Attributes().Mtime), 0) + accessTimeString := accessTime.Format(dateFormat) + modificationTimeString := modificationTime.Format(dateFormat) + if err := os.Chtimes(path, accessTime, modificationTime); err != nil { + c.Log(logger.LevelWarn, logSender, "failed to chtimes for path %#v, access time: %v, modification time: %v, err: %v", + path, accessTime, modificationTime, err) + return getSFTPErrorFromOSError(err) + } + logger.CommandLog(chtimesLogSender, path, "", c.User.Username, "", c.ID, c.protocol, -1, -1, accessTimeString, + modificationTimeString) return nil } return nil @@ -296,7 +313,7 @@ func (c Connection) handleSFTPRename(sourcePath string, targetPath string) error c.Log(logger.LevelWarn, logSender, "failed to rename file, source: %#v target: %#v: %v", sourcePath, targetPath, err) return getSFTPErrorFromOSError(err) } - logger.CommandLog(renameLogSender, sourcePath, targetPath, c.User.Username, "", c.ID, c.protocol, -1, -1) + logger.CommandLog(renameLogSender, sourcePath, targetPath, c.User.Username, "", c.ID, c.protocol, -1, -1, "", "") go executeAction(operationRename, c.User.Username, sourcePath, targetPath) return nil } @@ -322,7 +339,7 @@ func (c Connection) handleSFTPRmdir(path string) error { return getSFTPErrorFromOSError(err) } - logger.CommandLog(rmdirLogSender, path, "", c.User.Username, "", c.ID, c.protocol, -1, -1) + logger.CommandLog(rmdirLogSender, path, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "") return sftp.ErrSSHFxOk } @@ -335,7 +352,7 @@ func (c Connection) handleSFTPSymlink(sourcePath string, targetPath string) erro return getSFTPErrorFromOSError(err) } - logger.CommandLog(symlinkLogSender, sourcePath, targetPath, c.User.Username, "", c.ID, c.protocol, -1, -1) + logger.CommandLog(symlinkLogSender, sourcePath, targetPath, c.User.Username, "", c.ID, c.protocol, -1, -1, "", "") return nil } @@ -349,7 +366,7 @@ func (c Connection) handleSFTPMkdir(path string) error { } utils.SetPathPermissions(path, c.User.GetUID(), c.User.GetGID()) - logger.CommandLog(mkdirLogSender, path, "", c.User.Username, "", c.ID, c.protocol, -1, -1) + logger.CommandLog(mkdirLogSender, path, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "") return nil } @@ -375,7 +392,7 @@ func (c Connection) handleSFTPRemove(path string) error { return getSFTPErrorFromOSError(err) } - logger.CommandLog(removeLogSender, path, "", c.User.Username, "", c.ID, c.protocol, -1, -1) + logger.CommandLog(removeLogSender, path, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "") if fi.Mode()&os.ModeSymlink != os.ModeSymlink { dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false) } diff --git a/sftpd/sftpd.go b/sftpd/sftpd.go index 91e57da1..859386ec 100644 --- a/sftpd/sftpd.go +++ b/sftpd/sftpd.go @@ -31,6 +31,7 @@ const ( removeLogSender = "Remove" chownLogSender = "Chown" chmodLogSender = "Chmod" + chtimesLogSender = "Chtimes" operationDownload = "download" operationUpload = "upload" operationDelete = "delete" diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index fda02625..f06e078e 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/ioutil" + "math" "net" "net/http" "os" @@ -548,9 +549,9 @@ func TestStat(t *testing.T) { if err == nil { t.Errorf("readlink is not supported and must fail") } - err = client.Chtimes(testFileName, time.Now(), time.Now()) + err = client.Truncate(testFileName, 0) if err != nil { - t.Errorf("chtime must be silently ignored: %v", err) + t.Errorf("truncate must be silently ignored: %v", err) } } _, err = httpd.RemoveUser(user, http.StatusOK) @@ -619,6 +620,67 @@ func TestStatChownChmod(t *testing.T) { os.RemoveAll(user.GetHomeDir()) } +func TestChtimes(t *testing.T) { + usePubKey := false + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) + if err != nil { + t.Errorf("unable to add user: %v", err) + } + client, err := getSftpClient(user, usePubKey) + if err != nil { + t.Errorf("unable to create sftp client: %v", err) + } else { + defer client.Close() + testFileName := "test_file.dat" + testFilePath := filepath.Join(homeBasePath, testFileName) + testFileSize := int64(65535) + testDir := "test" + createTestFile(testFilePath, testFileSize) + err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) + if err != nil { + t.Errorf("file upload error: %v", err) + } + acmodTime := time.Now() + err = client.Chtimes(testFileName, acmodTime, acmodTime) + if err != nil { + t.Errorf("error changing file times") + } + newFi, err := client.Lstat(testFileName) + if err != nil { + t.Errorf("file stat error: %v", err) + } + diff := math.Abs(newFi.ModTime().Sub(acmodTime).Seconds()) + if diff > 1 { + t.Errorf("diff between wanted and real modification time too big: %v", diff) + } + err = client.Chtimes("invalidFile", acmodTime, acmodTime) + if !os.IsNotExist(err) { + t.Errorf("unexpected error: %v", err) + } + err = client.Mkdir(testDir) + if err != nil { + t.Errorf("unable to create dir: %v", err) + } + err = client.Chtimes(testDir, acmodTime, acmodTime) + if err != nil { + t.Errorf("error changing dir times") + } + newFi, err = client.Lstat(testDir) + if err != nil { + t.Errorf("dir stat error: %v", err) + } + diff = math.Abs(newFi.ModTime().Sub(acmodTime).Seconds()) + if diff > 1 { + t.Errorf("diff between wanted and real modification time too big: %v", diff) + } + } + _, err = httpd.RemoveUser(user, http.StatusOK) + if err != nil { + t.Errorf("unable to remove user: %v", err) + } + os.RemoveAll(user.GetHomeDir()) +} + // basic tests to verify virtual chroot, should be improved to cover more cases ... func TestEscapeHomeDir(t *testing.T) { usePubKey := true @@ -1586,7 +1648,8 @@ func TestPermList(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, - dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown} + dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, + dataprovider.PermChown, dataprovider.PermChtimes} user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -1616,7 +1679,8 @@ func TestPermDownload(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, - dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown} + dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, + dataprovider.PermChown, dataprovider.PermChtimes} user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -1658,7 +1722,8 @@ func TestPermUpload(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermDelete, dataprovider.PermRename, - dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown} + dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, + dataprovider.PermChown, dataprovider.PermChtimes} user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -1691,7 +1756,8 @@ func TestPermOverwrite(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, - dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermChmod, dataprovider.PermChown} + dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermChmod, + dataprovider.PermChown, dataprovider.PermChtimes} user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -1728,7 +1794,8 @@ func TestPermDelete(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermRename, - dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown} + dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, + dataprovider.PermChown, dataprovider.PermChtimes} user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -1765,7 +1832,8 @@ func TestPermRename(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, - dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown} + dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, + dataprovider.PermChown, dataprovider.PermChtimes} user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -1806,7 +1874,8 @@ func TestPermCreateDirs(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, - dataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown} + dataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, + dataprovider.PermChown, dataprovider.PermChtimes} user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -1832,7 +1901,8 @@ func TestPermSymlink(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, - dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown} + dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown, + dataprovider.PermChtimes} user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -1874,7 +1944,7 @@ func TestPermChmod(t *testing.T) { u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, - dataprovider.PermChown} + dataprovider.PermChown, dataprovider.PermChtimes} user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -1916,7 +1986,7 @@ func TestPermChown(t *testing.T) { u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, - dataprovider.PermChmod} + dataprovider.PermChmod, dataprovider.PermChtimes} user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) @@ -1952,6 +2022,49 @@ func TestPermChown(t *testing.T) { } os.RemoveAll(user.GetHomeDir()) } + +func TestPermChtimes(t *testing.T) { + usePubKey := false + u := getTestUser(usePubKey) + u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, + dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, + dataprovider.PermChmod, dataprovider.PermChown} + user, _, err := httpd.AddUser(u, http.StatusOK) + if err != nil { + t.Errorf("unable to add user: %v", err) + } + client, err := getSftpClient(user, usePubKey) + if err != nil { + t.Errorf("unable to create sftp client: %v", err) + } else { + defer client.Close() + testFileName := "test_file.dat" + testFilePath := filepath.Join(homeBasePath, testFileName) + testFileSize := int64(65535) + err = createTestFile(testFilePath, testFileSize) + if err != nil { + t.Errorf("unable to create test file: %v", err) + } + err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) + if err != nil { + t.Errorf("file upload error: %v", err) + } + err = client.Chtimes(testFileName, time.Now(), time.Now()) + if err == nil { + t.Errorf("chtimes without permission should not succeed") + } + err = client.Remove(testFileName) + if err != nil { + t.Errorf("error removing uploaded file: %v", err) + } + } + _, err = httpd.RemoveUser(user, http.StatusOK) + if err != nil { + t.Errorf("unable to remove user: %v", err) + } + os.RemoveAll(user.GetHomeDir()) +} + func TestSSHConnection(t *testing.T) { usePubKey := false user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)