Răsfoiți Sursa

file patterns: fix denied except rules

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 2 ani în urmă
părinte
comite
c457538280

+ 238 - 7
internal/common/connection_test.go

@@ -28,6 +28,7 @@ import (
 	"github.com/rs/xid"
 	"github.com/sftpgo/sdk"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/drakkan/sftpgo/v2/internal/dataprovider"
 	"github.com/drakkan/sftpgo/v2/internal/kms"
@@ -647,7 +648,7 @@ func TestFsFileCopier(t *testing.T) {
 	assert.True(t, ok)
 }
 
-func TestFilterListDirs(t *testing.T) {
+func TestFilePatterns(t *testing.T) {
 	filters := dataprovider.UserFilters{
 		BaseUserFilters: sdk.BaseUserFilters{
 			FilePatterns: []sdk.PatternsFilter{
@@ -661,6 +662,16 @@ func TestFilterListDirs(t *testing.T) {
 					DenyPolicy:      sdk.DenyPolicyHide,
 					AllowedPatterns: []string{"*.jpg"},
 				},
+				{
+					Path:           "/dir3",
+					DenyPolicy:     sdk.DenyPolicyDefault,
+					DeniedPatterns: []string{"*.jpg"},
+				},
+				{
+					Path:           "/dir4",
+					DenyPolicy:     sdk.DenyPolicyHide,
+					DeniedPatterns: []string{"*"},
+				},
 			},
 		},
 	}
@@ -693,26 +704,98 @@ func TestFilterListDirs(t *testing.T) {
 		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
 		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
 	}
-
+	// dirContents are modified in place, we need to redefine them each time
 	filtered := user.FilterListDir(dirContents, "/dir1")
 	assert.Len(t, filtered, 5)
 
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir1/vdir1")
+	assert.Len(t, filtered, 2)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir2/vdir2")
+	require.Len(t, filtered, 1)
+	assert.Equal(t, "file1.jpg", filtered[0].Name())
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir2/vdir2/sub")
+	require.Len(t, filtered, 1)
+	assert.Equal(t, "file1.jpg", filtered[0].Name())
+
+	res, _ := user.IsFileAllowed("/dir1/vdir1/file.txt")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir1/vdir1/sub/file.txt")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir1/vdir1/file.jpg")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir1/vdir1/sub/file.jpg")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/file.jpg")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir3/dir1/file.jpg")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir3/dir1/sub/file.jpg")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir4/file.jpg")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir4/dir1/sub/file.jpg")
+	assert.False(t, res)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir4")
+	require.Len(t, filtered, 0)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir4/vdir2/sub")
+	require.Len(t, filtered, 0)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+	}
+
 	filtered = user.FilterListDir(dirContents, "/dir2")
 	assert.Len(t, filtered, 2)
 
 	dirContents = []os.FileInfo{
 		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
-		vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
 	}
 
-	filtered = user.FilterListDir(dirContents, "/dir1")
-	assert.Len(t, filtered, 5)
+	filtered = user.FilterListDir(dirContents, "/dir4")
+	assert.Len(t, filtered, 0)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+	}
+
+	filtered = user.FilterListDir(dirContents, "/dir4/sub")
+	assert.Len(t, filtered, 0)
 
 	dirContents = []os.FileInfo{
 		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
 		vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
 	}
 
+	filtered = user.FilterListDir(dirContents, "/dir1")
+	assert.Len(t, filtered, 5)
+
 	filtered = user.FilterListDir(dirContents, "/dir2")
 	if assert.Len(t, filtered, 1) {
 		assert.True(t, filtered[0].IsDir())
@@ -799,7 +882,155 @@ func TestFilterListDirs(t *testing.T) {
 	dirContents = append(dirContents, vfs.NewFileInfo("file.jpg", false, 123, time.Now(), false))
 
 	filtered = user.FilterListDir(dirContents, "/dir3")
-	if assert.Len(t, filtered, 1) {
-		assert.Equal(t, "ic35", filtered[0].Name())
+	require.Len(t, filtered, 1)
+	assert.Equal(t, "ic35", filtered[0].Name())
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir3/ic36")
+	require.Len(t, filtered, 0)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir3/ic35")
+	require.Len(t, filtered, 3)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub")
+	require.Len(t, filtered, 3)
+
+	res, _ = user.IsFileAllowed("/dir3/file.txt")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35a")
+	assert.False(t, res)
+	res, policy := user.IsFileAllowed("/dir3/ic35a/file")
+	assert.False(t, res)
+	assert.Equal(t, sdk.DenyPolicyHide, policy)
+	res, _ = user.IsFileAllowed("/dir3/ic35")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub/file.txt")
+	assert.True(t, res)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub")
+	require.Len(t, filtered, 3)
+
+	user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
+		Path:            "/dir3/ic35/sub1",
+		AllowedPatterns: []string{"*.jpg"},
+		DenyPolicy:      sdk.DenyPolicyDefault,
+	})
+	user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
+		Path:           "/dir3/ic35/sub2",
+		DeniedPatterns: []string{"*.jpg"},
+		DenyPolicy:     sdk.DenyPolicyHide,
+	})
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub1")
+	require.Len(t, filtered, 3)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub2")
+	require.Len(t, filtered, 2)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub2/sub1")
+	require.Len(t, filtered, 2)
+
+	res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub/dir/file.txt")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub/dir/file.jpg")
+	assert.True(t, res)
+
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub1/file.jpg")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub1/file.txt")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub1/sub/file.jpg")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub1/sub2/file.txt")
+	assert.False(t, res)
+
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.jpg")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.txt")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub/file.jpg")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub1/file.txt")
+	assert.True(t, res)
+
+	user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
+		Path:           "/dir3/ic35",
+		DeniedPatterns: []string{"*.txt"},
+		DenyPolicy:     sdk.DenyPolicyHide,
+	})
+	res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/adir/sub/file.jpg")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/adir/file.txt")
+	assert.False(t, res)
+
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.jpg")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.txt")
+	assert.True(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub/file.jpg")
+	assert.False(t, res)
+	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub1/file.txt")
+	assert.True(t, res)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
+	}
+	filtered = user.FilterListDir(dirContents, "/dir3/ic35")
+	require.Len(t, filtered, 1)
+
+	dirContents = []os.FileInfo{
+		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
+		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
 	}
+	filtered = user.FilterListDir(dirContents, "/dir3/ic35/abc")
+	require.Len(t, filtered, 1)
 }

+ 7 - 2
internal/dataprovider/user.go

@@ -981,9 +981,14 @@ func (u *User) getPatternsFilterForPath(virtualPath string) sdk.PatternsFilter {
 		return filter
 	}
 	dirsForPath := util.GetDirsForVirtualPath(virtualPath)
-	for _, dir := range dirsForPath {
+	for idx, dir := range dirsForPath {
 		for _, f := range u.Filters.FilePatterns {
 			if f.Path == dir {
+				if idx > 0 && len(f.AllowedPatterns) > 0 && len(f.DeniedPatterns) > 0 && f.DeniedPatterns[0] == "*" {
+					if f.CheckAllowed(path.Base(dirsForPath[idx-1])) {
+						return filter
+					}
+				}
 				filter = f
 				break
 			}
@@ -1004,7 +1009,7 @@ func (u *User) isDirHidden(virtualPath string) bool {
 			return false
 		}
 		filter := u.getPatternsFilterForPath(dirPath)
-		if filter.DenyPolicy == sdk.DenyPolicyHide {
+		if filter.DenyPolicy == sdk.DenyPolicyHide && filter.Path != dirPath {
 			if !filter.CheckAllowed(path.Base(dirPath)) {
 				return true
 			}

+ 1 - 1
templates/webadmin/group.html

@@ -222,7 +222,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
                                 </div>
                                 <div class="card-body">
                                     <h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
-                                    <p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories</p>
+                                    <p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories. Setting a denied pattern as "*" and allowed pattern/s for the same directory you can create denied except rules, but note that if you allow a directory, everything in it will be allowed unless more specific patterns/permissions are defined.</p>
                                     <div class="form-group row">
                                         <div class="col-md-12 form_field_patterns_outer">
                                             {{range $idx, $pattern := .Group.UserSettings.Filters.GetFlatFilePatterns -}}

+ 1 - 1
templates/webadmin/user.html

@@ -502,7 +502,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
                                 </div>
                                 <div class="card-body">
                                     <h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
-                                    <p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories</p>
+                                    <p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories. Setting a denied pattern as "*" and allowed pattern/s for the same directory you can create denied except rules, but note that if you allow a directory, everything in it will be allowed unless more specific patterns/permissions are defined.</p>
                                     <div class="form-group row">
                                         <div class="col-md-12 form_field_patterns_outer">
                                             {{range $idx, $pattern := .User.Filters.GetFlatFilePatterns -}}