Browse Source

add setstat_mode 2

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

Fixes #223
Nicola Murino 4 years ago
parent
commit
5720d40fee

+ 2 - 0
common/common.go

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

+ 22 - 4
common/connection.go

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

+ 29 - 0
common/connection_test.go

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

+ 2 - 2
docker/scripts/entrypoint-alpine.sh

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

+ 4 - 4
docker/scripts/entrypoint.sh

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

+ 1 - 1
docs/full-configuration.md

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

+ 2 - 3
docs/s3.md

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

+ 0 - 11
sftpd/internal_test.go

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

+ 15 - 10
vfs/azblobfs.go

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

+ 15 - 10
vfs/gcsfs.go

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

+ 8 - 0
vfs/osfs.go

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

+ 15 - 10
vfs/s3fs.go

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

+ 2 - 1
vfs/vfs.go

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