diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index c6cdfa08..b32308be 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -280,7 +280,7 @@ jobs: - arch: armv7 distro: ubuntu18.04 go: latest - go-arch: arm + go-arch: arm7 steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd4fa699..cf7463b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -213,9 +213,9 @@ jobs: tar-arch: ppc64le - arch: armv7 distro: ubuntu18.04 - go-arch: arm + go-arch: arm7 deb-arch: armhf - rpm-arch: arm + rpm-arch: armv7hl tar-arch: armv7 steps: diff --git a/common/common_test.go b/common/common_test.go index 8def764f..81aff116 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -822,6 +822,23 @@ func TestHideConfidentialData(t *testing.T) { a.HideConfidentialData() } +func TestUserPerms(t *testing.T) { + u := dataprovider.User{} + u.Permissions = make(map[string][]string) + u.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermDelete} + assert.True(t, u.HasAnyPerm([]string{dataprovider.PermRename, dataprovider.PermDelete}, "/")) + assert.False(t, u.HasAnyPerm([]string{dataprovider.PermRename, dataprovider.PermCreateDirs}, "/")) + u.Permissions["/"] = []string{dataprovider.PermDelete, dataprovider.PermCreateDirs} + assert.True(t, u.HasPermsDeleteAll("/")) + assert.False(t, u.HasPermsRenameAll("/")) + u.Permissions["/"] = []string{dataprovider.PermDeleteDirs, dataprovider.PermDeleteFiles, dataprovider.PermRenameDirs} + assert.True(t, u.HasPermsDeleteAll("/")) + assert.False(t, u.HasPermsRenameAll("/")) + u.Permissions["/"] = []string{dataprovider.PermDeleteDirs, dataprovider.PermRenameFiles, dataprovider.PermRenameDirs} + assert.False(t, u.HasPermsDeleteAll("/")) + assert.True(t, u.HasPermsRenameAll("/")) +} + func BenchmarkBcryptHashing(b *testing.B) { bcryptPassword := "bcryptpassword" for i := 0; i < b.N; i++ { diff --git a/common/connection.go b/common/connection.go index ef4290d0..2e6bbc0d 100644 --- a/common/connection.go +++ b/common/connection.go @@ -259,7 +259,7 @@ func (c *BaseConnection) CreateDir(virtualPath string) error { // IsRemoveFileAllowed returns an error if removing this file is not allowed func (c *BaseConnection) IsRemoveFileAllowed(virtualPath string) error { - if !c.User.HasPerm(dataprovider.PermDelete, path.Dir(virtualPath)) { + if !c.User.HasAnyPerm([]string{dataprovider.PermDeleteFiles, dataprovider.PermDelete}, path.Dir(virtualPath)) { return c.GetPermissionDeniedError() } if !c.User.IsFileAllowed(virtualPath) { @@ -323,7 +323,7 @@ func (c *BaseConnection) IsRemoveDirAllowed(fs vfs.Fs, fsPath, virtualPath strin c.Log(logger.LevelWarn, "removing a directory mapped as virtual folder is not allowed: %#v", fsPath) return c.GetPermissionDeniedError() } - if !c.User.HasPerm(dataprovider.PermDelete, path.Dir(virtualPath)) { + if !c.User.HasAnyPerm([]string{dataprovider.PermDeleteDirs, dataprovider.PermDelete}, path.Dir(virtualPath)) { return c.GetPermissionDeniedError() } return nil @@ -650,11 +650,11 @@ func (c *BaseConnection) checkRecursiveRenameDirPermissions(fsSrc, fsDst vfs.Fs, // inside the parent path was checked. If the current dir has no subdirs with defined permissions inside it // and it has all the possible permissions we can stop scanning if !c.User.HasPermissionsInside(path.Dir(virtualSrcPath)) && !c.User.HasPermissionsInside(path.Dir(virtualDstPath)) { - if c.User.HasPerm(dataprovider.PermRename, path.Dir(virtualSrcPath)) && - c.User.HasPerm(dataprovider.PermRename, path.Dir(virtualDstPath)) { + if c.User.HasPermsRenameAll(path.Dir(virtualSrcPath)) && + c.User.HasPermsRenameAll(path.Dir(virtualDstPath)) { return ErrSkipPermissionsCheck } - if c.User.HasPerm(dataprovider.PermDelete, path.Dir(virtualSrcPath)) && + if c.User.HasPermsDeleteAll(path.Dir(virtualSrcPath)) && c.User.HasPerms(dstPerms, path.Dir(virtualDstPath)) { return ErrSkipPermissionsCheck } @@ -673,19 +673,42 @@ func (c *BaseConnection) checkRecursiveRenameDirPermissions(fsSrc, fsDst vfs.Fs, } func (c *BaseConnection) hasRenamePerms(virtualSourcePath, virtualTargetPath string, fi os.FileInfo) bool { - if c.User.HasPerm(dataprovider.PermRename, path.Dir(virtualSourcePath)) && - c.User.HasPerm(dataprovider.PermRename, path.Dir(virtualTargetPath)) { + if c.User.HasPermsRenameAll(path.Dir(virtualSourcePath)) && + c.User.HasPermsRenameAll(path.Dir(virtualTargetPath)) { return true } - if !c.User.HasPerm(dataprovider.PermDelete, path.Dir(virtualSourcePath)) { + if fi == nil { + // we don't know if this is a file or a directory we have to check all the required permissions + if !c.User.HasPermsDeleteAll(path.Dir(virtualSourcePath)) { + return false + } + dstPerms := []string{ + dataprovider.PermCreateDirs, + dataprovider.PermUpload, + dataprovider.PermCreateSymlinks, + } + return c.User.HasPerms(dstPerms, path.Dir(virtualTargetPath)) + } + if fi.IsDir() { + if c.User.HasPerm(dataprovider.PermRenameDirs, path.Dir(virtualSourcePath)) && + c.User.HasPerm(dataprovider.PermRenameDirs, path.Dir(virtualTargetPath)) { + return true + } + if !c.User.HasAnyPerm([]string{dataprovider.PermDeleteDirs, dataprovider.PermDelete}, path.Dir(virtualSourcePath)) { + return false + } + return c.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(virtualTargetPath)) + } + // file or symlink + if c.User.HasPerm(dataprovider.PermRenameFiles, path.Dir(virtualSourcePath)) && + c.User.HasPerm(dataprovider.PermRenameFiles, path.Dir(virtualTargetPath)) { + return true + } + if !c.User.HasAnyPerm([]string{dataprovider.PermDeleteFiles, dataprovider.PermDelete}, path.Dir(virtualSourcePath)) { return false } - if fi != nil { - if fi.IsDir() { - return c.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(virtualTargetPath)) - } else if fi.Mode()&os.ModeSymlink != 0 { - return c.User.HasPerm(dataprovider.PermCreateSymlinks, path.Dir(virtualTargetPath)) - } + if fi.Mode()&os.ModeSymlink != 0 { + return c.User.HasPerm(dataprovider.PermCreateSymlinks, path.Dir(virtualTargetPath)) } return c.User.HasPerm(dataprovider.PermUpload, path.Dir(virtualTargetPath)) } diff --git a/common/connection_test.go b/common/connection_test.go index f2df9046..80add81c 100644 --- a/common/connection_test.go +++ b/common/connection_test.go @@ -148,6 +148,31 @@ func TestRenameVirtualFolders(t *testing.T) { assert.False(t, res) } +func TestRenamePerms(t *testing.T) { + src := "source" + target := "target" + u := dataprovider.User{} + u.Permissions = map[string][]string{} + u.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermCreateSymlinks, + dataprovider.PermDeleteFiles} + conn := NewBaseConnection("", ProtocolSFTP, "", "", u) + assert.False(t, conn.hasRenamePerms(src, target, nil)) + u.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermCreateSymlinks, + dataprovider.PermDeleteFiles, dataprovider.PermDeleteDirs} + assert.True(t, conn.hasRenamePerms(src, target, nil)) + u.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermDeleteFiles, + dataprovider.PermDeleteDirs} + assert.False(t, conn.hasRenamePerms(src, target, nil)) + + info := vfs.NewFileInfo(src, true, 0, time.Now(), false) + u.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermDeleteFiles} + assert.False(t, conn.hasRenamePerms(src, target, info)) + u.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermDeleteDirs} + assert.True(t, conn.hasRenamePerms(src, target, info)) + u.Permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDeleteDirs} + assert.False(t, conn.hasRenamePerms(src, target, info)) +} + func TestUpdateQuotaAfterRename(t *testing.T) { user := dataprovider.User{ BaseUser: sdk.BaseUser{ diff --git a/common/dataretention.go b/common/dataretention.go index 62a92aff..3341c2d6 100644 --- a/common/dataretention.go +++ b/common/dataretention.go @@ -236,13 +236,13 @@ func (c *RetentionCheck) removeFile(virtualPath string, info os.FileInfo) error } func (c *RetentionCheck) cleanupFolder(folderPath string) error { - cleanupPerms := []string{dataprovider.PermListItems, dataprovider.PermDelete} + deleteFilesPerms := []string{dataprovider.PermDelete, dataprovider.PermDeleteFiles} startTime := time.Now() result := &folderRetentionCheckResult{ Path: folderPath, } c.results = append(c.results, result) - if !c.conn.User.HasPerms(cleanupPerms, folderPath) { + if !c.conn.User.HasPerm(dataprovider.PermListItems, folderPath) || !c.conn.User.HasAnyPerm(deleteFilesPerms, folderPath) { result.Elapsed = time.Since(startTime) result.Info = "data retention check skipped: no permissions" c.conn.Log(logger.LevelInfo, "user %#v does not have permissions to check retention on %#v, retention check skipped", @@ -316,7 +316,11 @@ func (c *RetentionCheck) cleanupFolder(folderPath string) error { } func (c *RetentionCheck) checkEmptyDirRemoval(folderPath string) { - if folderPath != "/" && c.conn.User.HasPerm(dataprovider.PermDelete, path.Dir(folderPath)) { + if folderPath != "/" && c.conn.User.HasAnyPerm([]string{ + dataprovider.PermDelete, + dataprovider.PermDeleteDirs, + }, path.Dir(folderPath), + ) { files, err := c.conn.ListDir(folderPath) if err == nil && len(files) == 0 { err = c.conn.RemoveDir(folderPath) diff --git a/common/protocol_test.go b/common/protocol_test.go index 6ce07c26..068f4139 100644 --- a/common/protocol_test.go +++ b/common/protocol_test.go @@ -2668,6 +2668,92 @@ func TestRenameSymlink(t *testing.T) { assert.NoError(t, err) } +func TestSplittedDeletePerms(t *testing.T) { + u := getTestUser() + u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDeleteDirs, + dataprovider.PermCreateDirs} + user, _, err := httpdtest.AddUser(u, http.StatusCreated) + assert.NoError(t, err) + conn, client, err := getSftpClient(user) + if assert.NoError(t, err) { + defer conn.Close() + defer client.Close() + err = writeSFTPFile(testFileName, 4096, client) + assert.NoError(t, err) + err = client.Remove(testFileName) + assert.Error(t, err) + err = client.Mkdir(testDir) + assert.NoError(t, err) + err = client.RemoveDirectory(testDir) + assert.NoError(t, err) + } + u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDeleteFiles, + dataprovider.PermCreateDirs, dataprovider.PermOverwrite} + _, _, err = httpdtest.UpdateUser(u, http.StatusOK, "") + assert.NoError(t, err) + + conn, client, err = getSftpClient(user) + if assert.NoError(t, err) { + defer conn.Close() + defer client.Close() + err = writeSFTPFile(testFileName, 4096, client) + assert.NoError(t, err) + err = client.Remove(testFileName) + assert.NoError(t, err) + err = client.Mkdir(testDir) + assert.NoError(t, err) + err = client.RemoveDirectory(testDir) + assert.Error(t, err) + } + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) +} + +func TestSplittedRenamePerms(t *testing.T) { + u := getTestUser() + u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermRenameDirs, + dataprovider.PermCreateDirs} + user, _, err := httpdtest.AddUser(u, http.StatusCreated) + assert.NoError(t, err) + conn, client, err := getSftpClient(user) + if assert.NoError(t, err) { + defer conn.Close() + defer client.Close() + err = writeSFTPFile(testFileName, 4096, client) + assert.NoError(t, err) + err = client.Mkdir(testDir) + assert.NoError(t, err) + err = client.Rename(testFileName, testFileName+"_renamed") + assert.Error(t, err) + err = client.Rename(testDir, testDir+"_renamed") + assert.NoError(t, err) + } + u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermRenameFiles, + dataprovider.PermCreateDirs, dataprovider.PermOverwrite} + _, _, err = httpdtest.UpdateUser(u, http.StatusOK, "") + assert.NoError(t, err) + + conn, client, err = getSftpClient(user) + if assert.NoError(t, err) { + defer conn.Close() + defer client.Close() + err = writeSFTPFile(testFileName, 4096, client) + assert.NoError(t, err) + err = client.Mkdir(testDir) + assert.NoError(t, err) + err = client.Rename(testFileName, testFileName+"_renamed") + assert.NoError(t, err) + err = client.Rename(testDir, testDir+"_renamed") + assert.Error(t, err) + } + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) +} + func TestSFTPLoopError(t *testing.T) { user1 := getTestUser() user2 := getTestUser() diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 46e899d0..dac58aaa 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -114,8 +114,9 @@ var ( SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName, BoltDataProviderName, MemoryDataProviderName, CockroachDataProviderName} // ValidPerms defines all the valid permissions for a user - ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete, - PermCreateDirs, PermCreateSymlinks, PermChmod, PermChown, PermChtimes} + ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermCreateDirs, PermRename, + PermRenameFiles, PermRenameDirs, PermDelete, PermDeleteFiles, PermDeleteDirs, PermCreateSymlinks, PermChmod, + PermChown, PermChtimes} // ValidLoginMethods defines all the valid login methods ValidLoginMethods = []string{SSHLoginMethodPublicKey, LoginMethodPassword, SSHLoginMethodKeyboardInteractive, SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt, LoginMethodTLSCertificate, diff --git a/dataprovider/user.go b/dataprovider/user.go index 771cda82..5b2b1a29 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -38,8 +38,16 @@ const ( PermOverwrite = "overwrite" // delete files or directories is allowed PermDelete = "delete" + // delete files is allowed + PermDeleteFiles = "delete_files" + // delete directories is allowed + PermDeleteDirs = "delete_dirs" // rename files or directories is allowed PermRename = "rename" + // rename files is allowed + PermRenameFiles = "rename_files" + // rename directories is allowed + PermRenameDirs = "rename_dirs" // create directories is allowed PermCreateDirs = "create_dirs" // create symbolic links is allowed @@ -66,6 +74,9 @@ const ( var ( errNoMatchingVirtualFolder = errors.New("no matching virtual folder found") + permsRenameAny = []string{PermRename, PermRenameDirs, PermRenameFiles} + permsDeleteAny = []string{PermDelete, PermDeleteDirs, PermDeleteFiles} + permsCreateAny = []string{PermUpload, PermCreateDirs} ) // User defines a SFTPGo user @@ -564,7 +575,21 @@ func (u *User) HasPerm(permission, path string) bool { return util.IsStringInSlice(permission, perms) } -// HasPerms return true if the user has all the given permissions +// HasAnyPerm returns true if the user has at least one of the given permissions +func (u *User) HasAnyPerm(permissions []string, path string) bool { + perms := u.GetPermissionsForPath(path) + if util.IsStringInSlice(PermAny, perms) { + return true + } + for _, permission := range permissions { + if util.IsStringInSlice(permission, perms) { + return true + } + } + return false +} + +// HasPerms returns true if the user has all the given permissions func (u *User) HasPerms(permissions []string, path string) bool { perms := u.GetPermissionsForPath(path) if util.IsStringInSlice(PermAny, perms) { @@ -578,6 +603,46 @@ func (u *User) HasPerms(permissions []string, path string) bool { return true } +// HasPermsDeleteAll returns true if the user can delete both files and directories +// for the given path +func (u *User) HasPermsDeleteAll(path string) bool { + perms := u.GetPermissionsForPath(path) + canDeleteFiles := false + canDeleteDirs := false + for _, permission := range perms { + if permission == PermAny || permission == PermDelete { + return true + } + if permission == PermDeleteFiles { + canDeleteFiles = true + } + if permission == PermDeleteDirs { + canDeleteDirs = true + } + } + return canDeleteFiles && canDeleteDirs +} + +// HasPermsRenameAll returns true if the user can rename both files and directories +// for the given path +func (u *User) HasPermsRenameAll(path string) bool { + perms := u.GetPermissionsForPath(path) + canRenameFiles := false + canRenameDirs := false + for _, permission := range perms { + if permission == PermAny || permission == PermRename { + return true + } + if permission == PermRenameFiles { + canRenameFiles = true + } + if permission == PermRenameDirs { + canRenameDirs = true + } + } + return canRenameFiles && canRenameDirs +} + // HasNoQuotaRestrictions returns true if no quota restrictions need to be applyed func (u *User) HasNoQuotaRestrictions(checkFiles bool) bool { if u.QuotaSize == 0 && (!checkFiles || u.QuotaFiles == 0) { @@ -782,13 +847,13 @@ func (u *User) CanRenameFromWeb(src, dest string) bool { if util.IsStringInSlice(sdk.WebClientWriteDisabled, u.Filters.WebClient) { return false } - if u.HasPerm(PermRename, src) && u.HasPerm(PermRename, dest) { + if u.HasAnyPerm(permsRenameAny, src) && u.HasAnyPerm(permsRenameAny, dest) { return true } - if !u.HasPerm(PermDelete, src) { + if !u.HasAnyPerm(permsDeleteAny, src) { return false } - return u.HasPerm(PermUpload, dest) || u.HasPerm(PermCreateDirs, dest) + return u.HasAnyPerm(permsCreateAny, dest) } // CanDeleteFromWeb returns true if the client can delete objects from the web UI. @@ -797,7 +862,7 @@ func (u *User) CanDeleteFromWeb(target string) bool { if util.IsStringInSlice(sdk.WebClientWriteDisabled, u.Filters.WebClient) { return false } - return u.HasPerm(PermDelete, target) + return u.HasAnyPerm(permsDeleteAny, target) } // GetSignature returns a signature for this admin. diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index 9a321cab..4b74ee17 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -3424,7 +3424,7 @@ paths: tags: - user APIs summary: Rename a directory - description: Rename a directory for the logged in user. The rename is allowed for empty directory or for non empty, local directories, with no virtual folders inside + description: Rename a directory for the logged in user. The rename is allowed for empty directory or for non empty local directories, with no virtual folders inside operationId: rename_user_dir parameters: - in: query @@ -3772,7 +3772,11 @@ components: - upload - overwrite - delete + - delete_files + - delete_dirs - rename + - rename_files + - rename_dirs - create_dirs - create_symlinks - chmod @@ -3786,7 +3790,11 @@ components: * `upload` - upload files is allowed * `overwrite` - overwrite an existing file, while uploading, is allowed. upload permission is required to allow file overwrite * `delete` - delete files or directories is allowed + * `delete_files` - delete files is allowed + * `delete_dirs` - delete directories is allowed * `rename` - rename files or directories is allowed + * `rename_files` - rename files is allowed + * `rename_dirs` - rename directories is allowed * `create_dirs` - create directories is allowed * `create_symlinks` - create links is allowed * `chmod` changing file or directory permissions is allowed diff --git a/pkgs/build.sh b/pkgs/build.sh index c889a420..41424f89 100755 --- a/pkgs/build.sh +++ b/pkgs/build.sh @@ -1,12 +1,11 @@ #!/bin/bash -NFPM_VERSION=2.8.0 +NFPM_VERSION=2.9.2 NFPM_ARCH=${NFPM_ARCH:-amd64} if [ -z ${SFTPGO_VERSION} ] then LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) NUM_COMMITS_FROM_TAG=$(git rev-list ${LATEST_TAG}.. --count) - #COMMIT_HASH=$(git rev-parse --short HEAD) VERSION=$(echo "${LATEST_TAG}" | awk -F. -v OFS=. '{$NF++;print}')-dev.${NUM_COMMITS_FROM_TAG} else VERSION=${SFTPGO_VERSION} @@ -87,10 +86,11 @@ contents: dst: "/etc/sftpgo/sftpgo.json" type: "config|noreplace" + - dst: "/srv/sftpgo" + type: dir -empty_folders: - - /var/lib/sftpgo - - /srv/sftpgo + - dst: "/var/lib/sftpgo" + type: dir overrides: deb: