Repository: AlistGo/alist (language: go)

Issue:
把webdav的删除和写入权限分离可以单独设置，
### Please make sure of the following things

- [X] I have read the [documentation](https://alist.nn.ci).
- [X] I'm sure there are no duplicate issues or discussions.
- [X] I'm sure this feature is not implemented.
- [X] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

webdav的读取，写入，删除权限可以单独设置，

### Suggested solution / 实现思路

_No response_

### Additional context / 附件

_No response_


Discussion / hints:
这个是防止文件被误删除但是又需要一起上传文件收集资源的好办法，我认为这个功能应该有很多人喜欢，谢谢，
值得提醒的是，目前alist用上传一个同名的文件会直接覆盖源文件，没有任何提示。即上传一个同名的空白文件相当于删除了原有内容

Fix patch:
diff --git a/internal/bootstrap/data/user.go b/internal/bootstrap/data/user.go
index 3b71e498206..37cba7a5e74 100644
--- a/internal/bootstrap/data/user.go
+++ b/internal/bootstrap/data/user.go
@@ -26,12 +26,13 @@ func initUser() {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 			salt := random.String(16)
 			admin = &model.User{
-				Username: "admin",
-				Salt:     salt,
-				PwdHash:  model.TwoHashPwd(adminPassword, salt),
-				Role:     model.ADMIN,
-				BasePath: "/",
-				Authn:    "[]",
+				Username:   "admin",
+				Salt:       salt,
+				PwdHash:    model.TwoHashPwd(adminPassword, salt),
+				Role:       model.ADMIN,
+				BasePath:   "/",
+				Authn:      "[]",
+				Permission: 0xFF, // 0(can see hidden) - 7(can remove)
 			}
 			if err := op.CreateUser(admin); err != nil {
 				panic(err)
diff --git a/internal/model/user.go b/internal/model/user.go
index b4e876a47ab..f75fc6875c5 100644
--- a/internal/model/user.go
+++ b/internal/model/user.go
@@ -32,16 +32,18 @@ type User struct {
 	Role     int    `json:"role"`                                      // user's role
 	Disabled bool   `json:"disabled"`
 	// Determine permissions by bit
-	//   0: can see hidden files
-	//   1: can access without password
-	//   2: can add offline download tasks
-	//   3: can mkdir and upload
-	//   4: can rename
-	//   5: can move
-	//   6: can copy
-	//   7: can remove
-	//   8: webdav read
-	//   9: webdav write
+	//   0:  can see hidden files
+	//   1:  can access without password
+	//   2:  can add offline download tasks
+	//   3:  can mkdir and upload
+	//   4:  can rename
+	//   5:  can move
+	//   6:  can copy
+	//   7:  can remove
+	//   8:  webdav read
+	//   9:  webdav write
+	//   10: ftp/sftp login and read
+	//   11: ftp/sftp write
 	Permission int32  `json:"permission"`
 	OtpSecret  string `json:"-"`
 	SsoID      string `json:"sso_id"` // unique by sso platform
@@ -78,43 +80,43 @@ func (u *User) SetPassword(pwd string) *User {
 }
 
 func (u *User) CanSeeHides() bool {
-	return u.IsAdmin() || u.Permission&1 == 1
+	return u.Permission&1 == 1
 }
 
 func (u *User) CanAccessWithoutPassword() bool {
-	return u.IsAdmin() || (u.Permission>>1)&1 == 1
+	return (u.Permission>>1)&1 == 1
 }
 
 func (u *User) CanAddOfflineDownloadTasks() bool {
-	return u.IsAdmin() || (u.Permission>>2)&1 == 1
+	return (u.Permission>>2)&1 == 1
 }
 
 func (u *User) CanWrite() bool {
-	return u.IsAdmin() || (u.Permission>>3)&1 == 1
+	return (u.Permission>>3)&1 == 1
 }
 
 func (u *User) CanRename() bool {
-	return u.IsAdmin() || (u.Permission>>4)&1 == 1
+	return (u.Permission>>4)&1 == 1
 }
 
 func (u *User) CanMove() bool {
-	return u.IsAdmin() || (u.Permission>>5)&1 == 1
+	return (u.Permission>>5)&1 == 1
 }
 
 func (u *User) CanCopy() bool {
-	return u.IsAdmin() || (u.Permission>>6)&1 == 1
+	return (u.Permission>>6)&1 == 1
 }
 
 func (u *User) CanRemove() bool {
-	return u.IsAdmin() || (u.Permission>>7)&1 == 1
+	return (u.Permission>>7)&1 == 1
 }
 
 func (u *User) CanWebdavRead() bool {
-	return u.IsAdmin() || (u.Permission>>8)&1 == 1
+	return (u.Permission>>8)&1 == 1
 }
 
 func (u *User) CanWebdavManage() bool {
-	return u.IsAdmin() || (u.Permission>>9)&1 == 1
+	return (u.Permission>>9)&1 == 1
 }
 
 func (u *User) CanFTPAccess() bool {
diff --git a/server/webdav.go b/server/webdav.go
index 2b5c9618b86..cdfdce7d9d3 100644
--- a/server/webdav.go
+++ b/server/webdav.go
@@ -11,7 +11,6 @@ import (
 	"github.com/alist-org/alist/v3/internal/model"
 	"github.com/alist-org/alist/v3/internal/op"
 	"github.com/alist-org/alist/v3/internal/setting"
-	"github.com/alist-org/alist/v3/pkg/utils"
 	"github.com/alist-org/alist/v3/server/webdav"
 	"github.com/gin-gonic/gin"
 	log "github.com/sirupsen/logrus"
@@ -99,12 +98,27 @@ func WebDAVAuth(c *gin.Context) {
 		c.Abort()
 		return
 	}
-	if !user.CanWebdavManage() && utils.SliceContains([]string{"PUT", "DELETE", "PROPPATCH", "MKCOL", "COPY", "MOVE"}, c.Request.Method) {
-		if c.Request.Method == "OPTIONS" {
-			c.Set("user", guest)
-			c.Next()
-			return
-		}
+	if (c.Request.Method == "PUT" || c.Request.Method == "MKCOL") && (!user.CanWebdavManage() || !user.CanWrite()) {
+		c.Status(http.StatusForbidden)
+		c.Abort()
+		return
+	}
+	if c.Request.Method == "MOVE" && (!user.CanWebdavManage() || (!user.CanMove() && !user.CanRename())) {
+		c.Status(http.StatusForbidden)
+		c.Abort()
+		return
+	}
+	if c.Request.Method == "COPY" && (!user.CanWebdavManage() || !user.CanCopy()) {
+		c.Status(http.StatusForbidden)
+		c.Abort()
+		return
+	}
+	if c.Request.Method == "DELETE" && (!user.CanWebdavManage() || !user.CanRemove()) {
+		c.Status(http.StatusForbidden)
+		c.Abort()
+		return
+	}
+	if c.Request.Method == "PROPPATCH" && !user.CanWebdavManage() {
 		c.Status(http.StatusForbidden)
 		c.Abort()
 		return
diff --git a/server/webdav/file.go b/server/webdav/file.go
index 01e96f7d223..ac8f5c1cbfb 100644
--- a/server/webdav/file.go
+++ b/server/webdav/file.go
@@ -33,6 +33,13 @@ func moveFiles(ctx context.Context, src, dst string, overwrite bool) (status int
 	dstDir := path.Dir(dst)
 	srcName := path.Base(src)
 	dstName := path.Base(dst)
+	user := ctx.Value("user").(*model.User)
+	if srcDir != dstDir && !user.CanMove() {
+		return http.StatusForbidden, nil
+	}
+	if srcName != dstName && !user.CanRename() {
+		return http.StatusForbidden, nil
+	}
 	if srcDir == dstDir {
 		err = fs.Rename(ctx, src, dstName)
 	} else {


Test patch: