add setstat_mode 2

in this mode chmod/chtimes/chown can be silently ignored only for cloud
based file systems

Fixes #223
This commit is contained in:
Nicola Murino 2020-11-12 10:39:46 +01:00
parent 38e0cba675
commit 5720d40fee
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
13 changed files with 117 additions and 56 deletions

View file

@ -223,6 +223,8 @@ type Configuration struct {
Actions ProtocolActions `json:"actions" mapstructure:"actions"`
// SetstatMode 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.
// 2 means "ignore mode for cloud fs": requests for changing permissions and owner/group/time are
// silently ignored for cloud based filesystem such as S3, GCS, Azure Blob
SetstatMode int `json:"setstat_mode" mapstructure:"setstat_mode"`
// Support for HAProxy PROXY protocol.
// If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable

View file

@ -448,17 +448,28 @@ func (c *BaseConnection) DoStat(fsPath string, mode int) (os.FileInfo, error) {
return c.Fs.Stat(c.getRealFsPath(fsPath))
}
// SetStat set StatAttributes for the specified fsPath
func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAttributes) error {
func (c *BaseConnection) ignoreSetStat() bool {
if Config.SetstatMode == 1 {
return nil
return true
}
if Config.SetstatMode == 2 && !vfs.IsLocalOsFs(c.Fs) {
return true
}
return false
}
// SetStat set StatAttributes for the specified fsPath
// nolint:gocyclo
func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAttributes) error {
pathForPerms := c.getPathForSetStatPerms(fsPath, virtualPath)
if attributes.Flags&StatAttrPerms != 0 {
if !c.User.HasPerm(dataprovider.PermChmod, pathForPerms) {
return c.GetPermissionDeniedError()
}
if c.ignoreSetStat() {
return nil
}
if err := c.Fs.Chmod(c.getRealFsPath(fsPath), attributes.Mode); err != nil {
c.Log(logger.LevelWarn, "failed to chmod path %#v, mode: %v, err: %+v", fsPath, attributes.Mode.String(), err)
return c.GetFsError(err)
@ -471,6 +482,9 @@ func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAtt
if !c.User.HasPerm(dataprovider.PermChown, pathForPerms) {
return c.GetPermissionDeniedError()
}
if c.ignoreSetStat() {
return nil
}
if err := c.Fs.Chown(c.getRealFsPath(fsPath), attributes.UID, attributes.GID); err != nil {
c.Log(logger.LevelWarn, "failed to chown path %#v, uid: %v, gid: %v, err: %+v", fsPath, attributes.UID,
attributes.GID, err)
@ -484,7 +498,9 @@ func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAtt
if !c.User.HasPerm(dataprovider.PermChtimes, pathForPerms) {
return c.GetPermissionDeniedError()
}
if c.ignoreSetStat() {
return nil
}
if err := c.Fs.Chtimes(c.getRealFsPath(fsPath), attributes.Atime, attributes.Mtime); err != nil {
c.Log(logger.LevelWarn, "failed to chtimes for path %#v, access time: %v, modification time: %v, err: %+v",
fsPath, attributes.Atime, attributes.Mtime, err)
@ -950,6 +966,8 @@ func (c *BaseConnection) GetFsError(err error) error {
return c.GetNotExistError()
} else if c.Fs.IsPermission(err) {
return c.GetPermissionDeniedError()
} else if c.Fs.IsNotSupported(err) {
return c.GetOpUnsupportedError()
} else if err != nil {
return c.GetGenericError(err)
}

View file

@ -496,6 +496,29 @@ func TestSetStat(t *testing.T) {
err = c.SetStat(user.GetHomeDir(), "/", &StatAttributes{})
assert.NoError(t, err)
err = c.SetStat(dir2, "/dir1/file", &StatAttributes{
Mode: os.ModePerm,
Flags: StatAttrPerms,
})
assert.NoError(t, err)
err = c.SetStat(dir1, "/dir2/file", &StatAttributes{
UID: os.Getuid(),
GID: os.Getgid(),
Flags: StatAttrUIDGID,
})
assert.NoError(t, err)
err = c.SetStat(dir1, "/dir3/file", &StatAttributes{
Atime: time.Now(),
Mtime: time.Now(),
Flags: StatAttrTimes,
})
assert.NoError(t, err)
Config.SetstatMode = 2
assert.False(t, c.ignoreSetStat())
c1 := NewBaseConnection("", ProtocolSFTP, user, newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir()))
assert.True(t, c1.ignoreSetStat())
Config.SetstatMode = oldSetStatMode
// chmod
err = c.SetStat(dir1, "/dir1/file", &StatAttributes{
@ -1146,6 +1169,12 @@ func TestErrorsMapping(t *testing.T) {
} else {
assert.EqualError(t, err, ErrPermissionDenied.Error())
}
err = conn.GetFsError(vfs.ErrVfsUnsupported)
if protocol == ProtocolSFTP {
assert.EqualError(t, err, sftp.ErrSSHFxOpUnsupported.Error())
} else {
assert.EqualError(t, err, ErrOpUnsupported.Error())
}
err = conn.GetFsError(nil)
assert.NoError(t, err)
err = conn.GetOpUnsupportedError()

View file

@ -10,7 +10,7 @@ if [ "$1" = 'sftpgo' ]; then
DIR_UID=$(stat -c %u ${DIR})
DIR_GID=$(stat -c %g ${DIR})
if [ ${DIR_UID} != ${SFTPGO_PUID} ] || [ ${DIR_GID} != ${SFTPGO_PGID} ]; then
echo `date +%Y-%m-%dT%H:%M:%S` - "entrypoint, change owner for ${DIR} uid: ${SFTPGO_PUID} gid: ${SFTPGO_PGID}"
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.000`'","sender":"entrypoint","message":"change owner for \"'${DIR}'\" UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
if [ ${DIR} = "/etc/sftpgo" ]; then
chown -R ${SFTPGO_PUID}:${SFTPGO_PGID} ${DIR}
else
@ -18,7 +18,7 @@ if [ "$1" = 'sftpgo' ]; then
fi
fi
done
echo `date +%Y-%m-%dT%H:%M:%S` - "entrypoint, run as uid: ${SFTPGO_PUID} gid: ${SFTPGO_PGID}"
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.000`'","sender":"entrypoint","message":"run as UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
exec su-exec ${SFTPGO_PUID}:${SFTPGO_PGID} "$@"
fi

View file

@ -10,19 +10,19 @@ if [ "$1" = 'sftpgo' ]; then
getent group ${SFTPGO_PGID} > /dev/null
HAS_PGID=$?
if [ ${HAS_PUID} -ne 0 ] || [ ${HAS_PGID} -ne 0 ]; then
echo `date +%Y-%m-%dT%H:%M:%S.%3N` - "entrypoint, prepare to run as uid: ${SFTPGO_PUID} gid: ${SFTPGO_PGID}"
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"prepare to run as UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
if [ ${HAS_PGID} -ne 0 ]; then
echo `date +%Y-%m-%dT%H:%M:%S.%3N` - "entrypoint, set GID to: ${SFTPGO_PGID}"
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"set GID to: '${SFTPGO_PGID}'"}'
groupmod -g ${SFTPGO_PGID} sftpgo
fi
if [ ${HAS_PUID} -ne 0 ]; then
echo `date +%Y-%m-%dT%H:%M:%S.%3N` - "entrypoint, set UID to: ${SFTPGO_PUID}"
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"set UID to: '${SFTPGO_PUID}'"}'
usermod -u ${SFTPGO_PUID} sftpgo
fi
chown -R ${SFTPGO_PUID}:${SFTPGO_PGID} /etc/sftpgo
chown ${SFTPGO_PUID}:${SFTPGO_PGID} /var/lib/sftpgo /srv/sftpgo
fi
echo `date +%Y-%m-%dT%H:%M:%S.%3N` - "entrypoint, run as uid: ${SFTPGO_PUID} gid: ${SFTPGO_PGID}"
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"run as UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
exec gosu ${SFTPGO_PUID}:${SFTPGO_PGID} "$@"
fi

View file

@ -54,7 +54,7 @@ The configuration file contains the following sections:
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See [Custom Actions](./custom-actions.md) for more details
- `execute_on`, list of strings. Valid values are `download`, `upload`, `pre-delete`, `delete`, `rename`, `ssh_cmd`. Leave empty to disable actions.
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
- `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.
- `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. 2 means "ignore mode for cloud based filesystems": requests for changing permissions, owner/group and access/modification times are silently ignored for cloud filesystems and executed for local filesystem.
- `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported. If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too. For example, for HAProxy, add `send-proxy` or `send-proxy-v2` to each server configuration line. The following modes are supported:
- 0, disabled
- 1, enabled. Proxy header will be used and requests without proxy header will be accepted

View file

@ -20,9 +20,8 @@ The configured bucket must exist.
Some SFTP commands don't work over S3:
- `symlink` and `chtimes` will fail
- `chown` and `chmod` are silently ignored
- `truncate` is not supported
- `chtimes`, `chown` and `chmod` will fail. If you want to silently ignore these method set `setstat_mode` to `1` or `2` in your configuration file
- `truncate`, `symlink`, `readlink` are not supported
- opening a file for both reading and writing at the same time is not supported
- upload resume is not supported
- upload mode `atomic` is ignored since S3 uploads are already atomic

View file

@ -397,17 +397,6 @@ func TestSFTPCmdTargetPath(t *testing.T) {
assert.True(t, os.IsNotExist(err))
}
func TestSetstatModeIgnore(t *testing.T) {
originalMode := common.Config.SetstatMode
common.Config.SetstatMode = 1
connection := Connection{}
request := sftp.NewRequest("Setstat", "invalid")
request.Flags = 0
err := connection.handleSFTPSetstat("invalid", request)
assert.NoError(t, err)
common.Config.SetstatMode = originalMode
}
func TestSFTPGetUsedQuota(t *testing.T) {
u := dataprovider.User{}
u.HomeDir = "home_rel_path"

View file

@ -361,37 +361,34 @@ func (fs *AzureBlobFs) Mkdir(name string) error {
// Symlink creates source as a symbolic link to target.
func (*AzureBlobFs) Symlink(source, target string) error {
return errors.New("403 symlinks are not supported")
return ErrVfsUnsupported
}
// Readlink returns the destination of the named symbolic link
func (*AzureBlobFs) Readlink(name string) (string, error) {
return "", errors.New("403 readlink is not supported")
return "", ErrVfsUnsupported
}
// Chown changes the numeric uid and gid of the named file.
// Silently ignored.
func (*AzureBlobFs) Chown(name string, uid int, gid int) error {
return nil
return ErrVfsUnsupported
}
// Chmod changes the mode of the named file to mode.
// Silently ignored.
func (*AzureBlobFs) Chmod(name string, mode os.FileMode) error {
return nil
return ErrVfsUnsupported
}
// Chtimes changes the access and modification times of the named file.
// Silently ignored.
func (*AzureBlobFs) Chtimes(name string, atime, mtime time.Time) error {
return errors.New("403 chtimes is not supported")
return ErrVfsUnsupported
}
// Truncate changes the size of the named file.
// Truncate by path is not supported, while truncating an opened
// file is handled inside base transfer
func (*AzureBlobFs) Truncate(name string, size int64) error {
return errors.New("403 truncate is not supported")
return ErrVfsUnsupported
}
// ReadDir reads the directory named by dirname and returns
@ -519,6 +516,14 @@ func (*AzureBlobFs) IsPermission(err error) bool {
return strings.Contains(err.Error(), "403")
}
// IsNotSupported returns true if the error indicate an unsupported operation
func (*AzureBlobFs) IsNotSupported(err error) bool {
if err == nil {
return false
}
return err == ErrVfsUnsupported
}
// CheckRootPath creates the specified local root directory if it does not exists
func (fs *AzureBlobFs) CheckRootPath(username string, uid int, gid int) bool {
// we need a local directory for temporary files
@ -575,7 +580,7 @@ func (fs *AzureBlobFs) ScanRootDirContents() (int, int64, error) {
// GetDirSize returns the number of files and the size for a folder
// including any subfolders
func (*AzureBlobFs) GetDirSize(dirname string) (int, int64, error) {
return 0, 0, errUnsupported
return 0, 0, ErrVfsUnsupported
}
// GetAtomicUploadPath returns the path to use for an atomic upload.

View file

@ -314,37 +314,34 @@ func (fs *GCSFs) Mkdir(name string) error {
// Symlink creates source as a symbolic link to target.
func (*GCSFs) Symlink(source, target string) error {
return errors.New("403 symlinks are not supported")
return ErrVfsUnsupported
}
// Readlink returns the destination of the named symbolic link
func (*GCSFs) Readlink(name string) (string, error) {
return "", errors.New("403 readlink is not supported")
return "", ErrVfsUnsupported
}
// Chown changes the numeric uid and gid of the named file.
// Silently ignored.
func (*GCSFs) Chown(name string, uid int, gid int) error {
return nil
return ErrVfsUnsupported
}
// Chmod changes the mode of the named file to mode.
// Silently ignored.
func (*GCSFs) Chmod(name string, mode os.FileMode) error {
return nil
return ErrVfsUnsupported
}
// Chtimes changes the access and modification times of the named file.
// Silently ignored.
func (*GCSFs) Chtimes(name string, atime, mtime time.Time) error {
return errors.New("403 chtimes is not supported")
return ErrVfsUnsupported
}
// Truncate changes the size of the named file.
// Truncate by path is not supported, while truncating an opened
// file is handled inside base transfer
func (*GCSFs) Truncate(name string, size int64) error {
return errors.New("403 truncate is not supported")
return ErrVfsUnsupported
}
// ReadDir reads the directory named by dirname and returns
@ -455,6 +452,14 @@ func (*GCSFs) IsPermission(err error) bool {
return strings.Contains(err.Error(), "403")
}
// IsNotSupported returns true if the error indicate an unsupported operation
func (*GCSFs) IsNotSupported(err error) bool {
if err == nil {
return false
}
return err == ErrVfsUnsupported
}
// CheckRootPath creates the specified local root directory if it does not exists
func (fs *GCSFs) CheckRootPath(username string, uid int, gid int) bool {
// we need a local directory for temporary files
@ -502,7 +507,7 @@ func (fs *GCSFs) ScanRootDirContents() (int, int64, error) {
// GetDirSize returns the number of files and the size for a folder
// including any subfolders
func (*GCSFs) GetDirSize(dirname string) (int, int64, error) {
return 0, 0, errUnsupported
return 0, 0, ErrVfsUnsupported
}
// GetAtomicUploadPath returns the path to use for an atomic upload.

View file

@ -185,6 +185,14 @@ func (*OsFs) IsPermission(err error) bool {
return os.IsPermission(err)
}
// IsNotSupported returns true if the error indicate an unsupported operation
func (*OsFs) IsNotSupported(err error) bool {
if err == nil {
return false
}
return err == ErrVfsUnsupported
}
// CheckRootPath creates the root directory if it does not exists
func (fs *OsFs) CheckRootPath(username string, uid int, gid int) bool {
var err error

View file

@ -350,37 +350,34 @@ func (fs *S3Fs) Mkdir(name string) error {
// Symlink creates source as a symbolic link to target.
func (*S3Fs) Symlink(source, target string) error {
return errors.New("403 symlinks are not supported")
return ErrVfsUnsupported
}
// Readlink returns the destination of the named symbolic link
func (*S3Fs) Readlink(name string) (string, error) {
return "", errors.New("403 readlink is not supported")
return "", ErrVfsUnsupported
}
// Chown changes the numeric uid and gid of the named file.
// Silently ignored.
func (*S3Fs) Chown(name string, uid int, gid int) error {
return nil
return ErrVfsUnsupported
}
// Chmod changes the mode of the named file to mode.
// Silently ignored.
func (*S3Fs) Chmod(name string, mode os.FileMode) error {
return nil
return ErrVfsUnsupported
}
// Chtimes changes the access and modification times of the named file.
// Silently ignored.
func (*S3Fs) Chtimes(name string, atime, mtime time.Time) error {
return errors.New("403 chtimes is not supported")
return ErrVfsUnsupported
}
// Truncate changes the size of the named file.
// Truncate by path is not supported, while truncating an opened
// file is handled inside base transfer
func (*S3Fs) Truncate(name string, size int64) error {
return errors.New("403 truncate is not supported")
return ErrVfsUnsupported
}
// ReadDir reads the directory named by dirname and returns
@ -485,6 +482,14 @@ func (*S3Fs) IsPermission(err error) bool {
return strings.Contains(err.Error(), "403")
}
// IsNotSupported returns true if the error indicate an unsupported operation
func (*S3Fs) IsNotSupported(err error) bool {
if err == nil {
return false
}
return err == ErrVfsUnsupported
}
// CheckRootPath creates the specified local root directory if it does not exists
func (fs *S3Fs) CheckRootPath(username string, uid int, gid int) bool {
// we need a local directory for temporary files
@ -520,7 +525,7 @@ func (fs *S3Fs) ScanRootDirContents() (int, int64, error) {
// GetDirSize returns the number of files and the size for a folder
// including any subfolders
func (*S3Fs) GetDirSize(dirname string) (int, int64, error) {
return 0, 0, errUnsupported
return 0, 0, ErrVfsUnsupported
}
// GetAtomicUploadPath returns the path to use for an atomic upload.

View file

@ -46,6 +46,7 @@ type Fs interface {
ResolvePath(sftpPath string) (string, error)
IsNotExist(err error) bool
IsPermission(err error) bool
IsNotSupported(err error) bool
ScanRootDirContents() (int, int64, error)
GetDirSize(dirname string) (int, int64, error)
GetAtomicUploadPath(name string) string
@ -56,7 +57,7 @@ type Fs interface {
GetMimeType(name string) (string, error)
}
var errUnsupported = errors.New("Not supported")
var ErrVfsUnsupported = errors.New("Not supported")
// QuotaCheckResult defines the result for a quota check
type QuotaCheckResult struct {