virtual folders: allow overlapped mapped paths if quota is disabled
See #95
This commit is contained in:
parent
7807fa7cc2
commit
8e22dd1b13
8 changed files with 192 additions and 13 deletions
|
@ -32,8 +32,8 @@ Fully featured and highly configurable SFTP server, written in Go
|
|||
- Support for serving local filesystem, S3 Compatible Object Storage and Google Cloud Storage over SFTP/SCP.
|
||||
- [Prometheus metrics](./docs/metrics.md) are exposed.
|
||||
- Support for HAProxy PROXY protocol: you can proxy and/or load balance the SFTP/SCP service without losing the information about the client's address.
|
||||
- [REST API](./docs/rest-api.md) for users management, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection.
|
||||
- [Web based administration interface](./docs/web-admin.md) to easily manage users and connections.
|
||||
- [REST API](./docs/rest-api.md) for users and folders management, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection.
|
||||
- [Web based administration interface](./docs/web-admin.md) to easily manage users, folders and connections.
|
||||
- Easy [migration](./examples/rest-api-cli#convert-users-from-other-stores) from Linux system user accounts.
|
||||
- [Portable mode](./docs/portable-mode.md): a convenient way to share a single directory on demand.
|
||||
- Performance analysis using built-in [profiler](./docs/profiling.md).
|
||||
|
|
|
@ -784,9 +784,15 @@ func validateUserVirtualFolders(user *User) error {
|
|||
QuotaFiles: v.QuotaFiles,
|
||||
})
|
||||
for k, virtual := range mappedPaths {
|
||||
if isMappedDirOverlapped(k, cleanedMPath) {
|
||||
return &ValidationError{err: fmt.Sprintf("invalid mapped folder %#v overlaps with mapped folder %#v",
|
||||
v.MappedPath, k)}
|
||||
if GetQuotaTracking() > 0 {
|
||||
if isMappedDirOverlapped(k, cleanedMPath) {
|
||||
return &ValidationError{err: fmt.Sprintf("invalid mapped folder %#v overlaps with mapped folder %#v",
|
||||
v.MappedPath, k)}
|
||||
}
|
||||
} else {
|
||||
if k == cleanedMPath {
|
||||
return &ValidationError{err: fmt.Sprintf("duplicated mapped folder %#v", v.MappedPath)}
|
||||
}
|
||||
}
|
||||
if isVirtualDirOverlapped(virtual, cleanedVPath) {
|
||||
return &ValidationError{err: fmt.Sprintf("invalid virtual folder %#v overlaps with virtual folder %#v",
|
||||
|
|
|
@ -239,6 +239,17 @@ func (u *User) AddVirtualDirs(list []os.FileInfo, sftpPath string) []os.FileInfo
|
|||
return list
|
||||
}
|
||||
|
||||
// IsMappedPath returns true if the specified filesystem path has a virtual folder mapping.
|
||||
// The filesystem path must be cleaned before calling this method
|
||||
func (u *User) IsMappedPath(fsPath string) bool {
|
||||
for _, v := range u.VirtualFolders {
|
||||
if fsPath == v.MappedPath {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsVirtualFolder returns true if the specified sftp path is a virtual folder
|
||||
func (u *User) IsVirtualFolder(sftpPath string) bool {
|
||||
for _, v := range u.VirtualFolders {
|
||||
|
@ -262,6 +273,24 @@ func (u *User) HasVirtualFoldersInside(sftpPath string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// HasOverlappedMappedPaths returns true if this user has virtual folders with overlapped mapped paths
|
||||
func (u *User) HasOverlappedMappedPaths() bool {
|
||||
if len(u.VirtualFolders) <= 1 {
|
||||
return false
|
||||
}
|
||||
for _, v1 := range u.VirtualFolders {
|
||||
for _, v2 := range u.VirtualFolders {
|
||||
if v1.VirtualPath == v2.VirtualPath {
|
||||
continue
|
||||
}
|
||||
if isMappedDirOverlapped(v1.MappedPath, v2.MappedPath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasPerm returns true if the user has the given permission or any permission
|
||||
func (u *User) HasPerm(permission, path string) bool {
|
||||
perms := u.GetPermissionsForPath(path)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# REST API
|
||||
|
||||
SFTPGo exposes REST API to manage, backup, and restore users, and to get real time reports of the active connections with the ability to forcibly close a connection.
|
||||
SFTPGo exposes REST API to manage, backup, and restore users and folders, and to get real time reports of the active connections with the ability to forcibly close a connection.
|
||||
|
||||
If quota tracking is enabled in the configuration file, then the used size and number of files are updated each time a file is added/removed. If files are added/removed not using SFTP/SCP, or if you change `track_quota` from `2` to `1`, you can rescan the users home dir and update the used quota using the REST API.
|
||||
|
||||
|
|
|
@ -16,16 +16,16 @@ For example if you configure `/tmp/mapped` or `C:\mapped` as mapped path and `/v
|
|||
The same virtual folder, identified by the `mapped_path`, can be shared among users and different folder quota limits for each user are supported.
|
||||
Folder quota limits can also be included inside the user quota but in this case the folder is considered "private" and sharing it with other users will break user quota calculation.
|
||||
|
||||
You don't need to create virtual folders, inside the data provider, to associate them to the users: any missing virtual folder will be automatically created when you add/update an user. You have to create the folder on the filesystem yourself.
|
||||
You don't need to create virtual folders, inside the data provider, to associate them to the users: any missing virtual folder will be automatically created when you add/update an user. You only have to create the folder on the filesystem.
|
||||
|
||||
Using the REST API you can:
|
||||
|
||||
- monitor folder quota usage
|
||||
- scan quota for a virtual folder
|
||||
- inspect the users associated with a virtual folder
|
||||
- delete a virtual folder. SFTPGo remove folders from the data provider, no deletion will occur on the filesystem
|
||||
- monitor folders quota usage
|
||||
- scan quota for folders
|
||||
- inspect the relationships among users and folders
|
||||
- delete a virtual folder. SFTPGo removes folders from the data provider, no files deletion will occur
|
||||
|
||||
If you remove a folder, from the data provider, any users relationship will be cleaned up. If the deleted folder is included inside the user quota you need to do a user quota scan to update its quota. An orphan virtual folder will not be automatically deleted since if you add it again later then a quota scan is needed and it could be quite expensive, anyway you can easily list the orphan virtual folders via REST API and delete them if they are not needed anymore.
|
||||
If you remove a folder, from the data provider, any users relationships will be cleared up. If the deleted folder is included inside the user quota you need to do a user quota scan to update its quota. An orphan virtual folder will not be automatically deleted since if you add it again later then a quota scan is needed and it could be quite expensive, anyway you can easily list the orphan folders using the REST API and delete them if they are not needed anymore.
|
||||
|
||||
Overlapping virtual or mapped paths are not allowed for the same user.
|
||||
Overlapping virtual paths are not allowed for the same user, overlapping mapped paths are allowed only if quota tracking is globally disabled inside the configuration file (`track_quota` must be set to `0`).
|
||||
Virtual folders are supported for local filesystem only.
|
|
@ -304,6 +304,14 @@ func (c Connection) handleSFTPRename(sourcePath, targetPath string, request *sft
|
|||
if !c.isRenamePermitted(sourcePath, request) {
|
||||
return sftp.ErrSSHFxPermissionDenied
|
||||
}
|
||||
if c.User.IsMappedPath(sourcePath) {
|
||||
c.Log(logger.LevelWarn, logSender, "renaming a directory mapped as virtual folder is not allowed: %#v", sourcePath)
|
||||
return sftp.ErrSSHFxPermissionDenied
|
||||
}
|
||||
if c.User.IsMappedPath(targetPath) {
|
||||
c.Log(logger.LevelWarn, logSender, "renaming to a directory mapped as virtual folder is not allowed: %#v", targetPath)
|
||||
return sftp.ErrSSHFxPermissionDenied
|
||||
}
|
||||
if c.User.HasVirtualFoldersInside(request.Filepath) {
|
||||
if fi, err := c.fs.Stat(sourcePath); err == nil {
|
||||
if fi.IsDir() {
|
||||
|
@ -359,6 +367,10 @@ func (c Connection) handleSFTPRmdir(dirPath string, request *sftp.Request) error
|
|||
c.Log(logger.LevelWarn, logSender, "removing a directory with a virtual folder inside is not allowed: %#v", request.Filepath)
|
||||
return sftp.ErrSSHFxOpUnsupported
|
||||
}
|
||||
if c.User.IsMappedPath(dirPath) {
|
||||
c.Log(logger.LevelWarn, logSender, "removing a directory mapped as virtual folder is not allowed: %#v", dirPath)
|
||||
return sftp.ErrSSHFxPermissionDenied
|
||||
}
|
||||
if !c.User.HasPerm(dataprovider.PermDelete, path.Dir(request.Filepath)) {
|
||||
return sftp.ErrSSHFxPermissionDenied
|
||||
}
|
||||
|
@ -399,6 +411,14 @@ func (c Connection) handleSFTPSymlink(sourcePath string, targetPath string, requ
|
|||
c.Log(logger.LevelWarn, logSender, "cross folder symlink is not supported, src: %v dst: %v", request.Filepath, request.Target)
|
||||
return sftp.ErrSSHFxFailure
|
||||
}
|
||||
if c.User.IsMappedPath(sourcePath) {
|
||||
c.Log(logger.LevelWarn, logSender, "symlinking a directory mapped as virtual folder is not allowed: %#v", sourcePath)
|
||||
return sftp.ErrSSHFxPermissionDenied
|
||||
}
|
||||
if c.User.IsMappedPath(targetPath) {
|
||||
c.Log(logger.LevelWarn, logSender, "symlinking to a directory mapped as virtual folder is not allowed: %#v", targetPath)
|
||||
return sftp.ErrSSHFxPermissionDenied
|
||||
}
|
||||
if err := c.fs.Symlink(sourcePath, targetPath); err != nil {
|
||||
c.Log(logger.LevelWarn, logSender, "failed to create symlink %#v -> %#v: %+v", sourcePath, targetPath, err)
|
||||
return vfs.GetSFTPError(c.fs, err)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -475,6 +476,11 @@ func loginUser(user dataprovider.User, loginMethod, publicKey string, conn ssh.C
|
|||
logger.Debug(logSender, connectionID, "cannot login user %#v, login method %#v is not allowed", user.Username, loginMethod)
|
||||
return nil, fmt.Errorf("Login method %#v is not allowed for user %#v", loginMethod, user.Username)
|
||||
}
|
||||
if dataprovider.GetQuotaTracking() > 0 && user.HasOverlappedMappedPaths() {
|
||||
logger.Debug(logSender, connectionID, "cannot login user %#v, overlapping mapped folders are allowed only with quota tracking disabled",
|
||||
user.Username)
|
||||
return nil, errors.New("overlapping mapped folders are allowed only with quota tracking disabled")
|
||||
}
|
||||
remoteAddr := conn.RemoteAddr().String()
|
||||
if !user.IsLoginFromAddrAllowed(remoteAddr) {
|
||||
logger.Debug(logSender, connectionID, "cannot login user %#v, remote address is not allowed: %v", user.Username, remoteAddr)
|
||||
|
|
|
@ -3547,6 +3547,124 @@ func TestVirtualFoldersLink(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestOverlappedMappedFolders(t *testing.T) {
|
||||
dataProvider := dataprovider.GetProvider()
|
||||
err := dataprovider.Close(dataProvider)
|
||||
assert.NoError(t, err)
|
||||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf := config.GetProviderConf()
|
||||
providerConf.TrackQuota = 0
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
assert.NoError(t, err)
|
||||
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||
|
||||
usePubKey := false
|
||||
u := getTestUser(usePubKey)
|
||||
subDir := "subdir"
|
||||
mappedPath1 := filepath.Join(os.TempDir(), "vdir1")
|
||||
vdirPath1 := "/vdir1"
|
||||
mappedPath2 := filepath.Join(os.TempDir(), "vdir1", subDir)
|
||||
vdirPath2 := "/vdir2"
|
||||
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: mappedPath1,
|
||||
},
|
||||
VirtualPath: vdirPath1,
|
||||
})
|
||||
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
MappedPath: mappedPath2,
|
||||
},
|
||||
VirtualPath: vdirPath2,
|
||||
})
|
||||
err = os.MkdirAll(mappedPath1, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.MkdirAll(mappedPath2, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer client.Close()
|
||||
err = checkBasicSFTP(client)
|
||||
assert.NoError(t, err)
|
||||
testFileName := "test_file.dat"
|
||||
testFileSize := int64(131072)
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
fi, err := client.Stat(path.Join(vdirPath1, subDir, testFileName))
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, testFileSize, fi.Size())
|
||||
}
|
||||
err = client.Rename(path.Join(vdirPath1, subDir, testFileName), path.Join(vdirPath2, testFileName+"1"))
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(path.Join(vdirPath2, testFileName+"1"), path.Join(vdirPath1, subDir, testFileName))
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(path.Join(vdirPath1, subDir), path.Join(vdirPath2, subDir))
|
||||
assert.Error(t, err)
|
||||
err = client.Mkdir(subDir)
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(subDir, path.Join(vdirPath1, subDir))
|
||||
assert.Error(t, err)
|
||||
err = client.RemoveDirectory(path.Join(vdirPath1, subDir))
|
||||
assert.Error(t, err)
|
||||
err = client.Symlink(path.Join(vdirPath1, subDir), path.Join(vdirPath1, "adir"))
|
||||
assert.Error(t, err)
|
||||
err = client.Mkdir(path.Join(vdirPath1, subDir+"1"))
|
||||
assert.NoError(t, err)
|
||||
err = client.Symlink(path.Join(vdirPath1, subDir+"1"), path.Join(vdirPath1, subDir))
|
||||
assert.Error(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
dataProvider = dataprovider.GetProvider()
|
||||
err = dataprovider.Close(dataProvider)
|
||||
assert.NoError(t, err)
|
||||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf = config.GetProviderConf()
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
assert.NoError(t, err)
|
||||
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||
|
||||
if providerConf.Driver != dataprovider.MemoryDataProviderName {
|
||||
client, err = getSftpClient(user, usePubKey)
|
||||
if !assert.Error(t, err) {
|
||||
client.Close()
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath1}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath2}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, _, err = httpd.AddUser(u, http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath1)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(mappedPath2)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestVirtualFolderQuotaScan(t *testing.T) {
|
||||
mappedPath := filepath.Join(os.TempDir(), "mapped_dir")
|
||||
err := os.MkdirAll(mappedPath, os.ModePerm)
|
||||
|
|
Loading…
Reference in a new issue