From fcfb3369d17150e6b09ed13f1da1b70caa0b3e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E7=9F=B3?= Date: Fri, 15 Aug 2025 08:10:55 -0700 Subject: [PATCH] fix: webdav error location (#9266) * feat: improve WebDAV permission handling and user role fetching - Added logic to handle root permissions in WebDAV requests. - Improved the user role fetching mechanism. - Enhanced path checks and permission scopes in role_perm.go. - Set FetchRole function to avoid import cycles between modules. * fix(webdav): resolve connection reset issue by encoding paths - Adjust path encoding in webdav.go to prevent connection reset. - Utilize utils.EncodePath for correct path formatting. - Ensure proper handling of directory paths with trailing slash. * fix(webdav): resolve connection reset issue by encoding paths - Adjust path encoding in webdav.go to prevent connection reset. - Utilize utils.FixAndCleanPath for correct path formatting. - Ensure proper handling of directory paths with trailing slash. * fix: resolve webdav handshake error in permission checks - Updated role permission logic to handle bidirectional subpaths. - This adjustment fixes the issue where remote host terminates the handshake due to improper path matching. * fix: resolve webdav handshake error in permission checks (fix/fix-webdav-error) - Updated role permission logic to handle bidirectional subpaths, fixing handshake termination by remote host due to path mismatch. - Refactored function naming for consistency and clarity. - Enhanced filtering of objects based on user permissions. * fix: resolve webdav handshake error in permission checks - Updated role permission logic to handle bidirectional subpaths, fixing handshake termination by remote host due to path mismatch. - Refactored function naming for consistency and clarity. - Enhanced filtering of objects based on user permissions. --- internal/model/user.go | 18 ++++++- internal/op/role.go | 4 ++ server/common/role_perm.go | 41 ++++++++++++--- server/handles/fsread.go | 18 ++++++- server/webdav.go | 6 ++- server/webdav/file.go | 4 ++ server/webdav/webdav.go | 101 ++++++++++++++++++++++++++++++++++++- 7 files changed, 180 insertions(+), 12 deletions(-) diff --git a/internal/model/user.go b/internal/model/user.go index 221747a4..8ea1ef1a 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -145,12 +145,15 @@ func (u *User) CheckPathLimit() bool { } func (u *User) JoinPath(reqPath string) (string, error) { + if reqPath == "/" { + return utils.FixAndCleanPath(u.BasePath), nil + } path, err := utils.JoinBasePath(u.BasePath, reqPath) if err != nil { return "", err } - if u.CheckPathLimit() { + if path != "/" && u.CheckPathLimit() { basePaths := GetAllBasePathsFromRoles(u) match := false for _, base := range basePaths { @@ -206,12 +209,23 @@ func (u *User) WebAuthnIcon() string { return "https://alistgo.com/logo.svg" } +// FetchRole is used to load role details by id. It should be set by the op package +// to avoid an import cycle between model and op. +var FetchRole func(uint) (*Role, error) + // GetAllBasePathsFromRoles returns all permission paths from user's roles func GetAllBasePathsFromRoles(u *User) []string { basePaths := make([]string, 0) seen := make(map[string]struct{}) - for _, role := range u.RolesDetail { + for _, rid := range u.Role { + if FetchRole == nil { + continue + } + role, err := FetchRole(uint(rid)) + if err != nil || role == nil { + continue + } for _, entry := range role.PermissionScopes { if entry.Path == "" { continue diff --git a/internal/op/role.go b/internal/op/role.go index e24ba5ab..4d187506 100644 --- a/internal/op/role.go +++ b/internal/op/role.go @@ -15,6 +15,10 @@ import ( var roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2)) var roleG singleflight.Group[*model.Role] +func init() { + model.FetchRole = GetRole +} + func GetRole(id uint) (*model.Role, error) { key := fmt.Sprint(id) if r, ok := roleCache.Get(key); ok { diff --git a/server/common/role_perm.go b/server/common/role_perm.go index 1e539d96..36dedf98 100644 --- a/server/common/role_perm.go +++ b/server/common/role_perm.go @@ -43,17 +43,23 @@ func MergeRolePermissions(u *model.User, reqPath string) int32 { if err != nil { continue } - for _, entry := range role.PermissionScopes { - if utils.IsSubPath(entry.Path, reqPath) { + if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) { + for _, entry := range role.PermissionScopes { perm |= entry.Permission } + } else { + for _, entry := range role.PermissionScopes { + if utils.IsSubPath(entry.Path, reqPath) { + perm |= entry.Permission + } + } } } return perm } func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password string) bool { - if !canReadPathByRole(u, reqPath) { + if !CanReadPathByRole(u, reqPath) { return false } perm := MergeRolePermissions(u, reqPath) @@ -78,7 +84,30 @@ func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password strin return meta.Password == password } -func canReadPathByRole(u *model.User, reqPath string) bool { +func CanReadPathByRole(u *model.User, reqPath string) bool { + if u == nil { + return false + } + if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) { + return len(u.Role) > 0 + } + for _, rid := range u.Role { + role, err := op.GetRole(uint(rid)) + if err != nil { + continue + } + for _, entry := range role.PermissionScopes { + if utils.PathEqual(entry.Path, reqPath) || utils.IsSubPath(entry.Path, reqPath) || utils.IsSubPath(reqPath, entry.Path) { + return true + } + } + } + return false +} + +// HasChildPermission checks whether any child path under reqPath grants the +// specified permission bit. +func HasChildPermission(u *model.User, reqPath string, bit uint) bool { if u == nil { return false } @@ -88,7 +117,7 @@ func canReadPathByRole(u *model.User, reqPath string) bool { continue } for _, entry := range role.PermissionScopes { - if utils.IsSubPath(entry.Path, reqPath) { + if utils.IsSubPath(reqPath, entry.Path) && HasPermission(entry.Permission, bit) { return true } } @@ -102,7 +131,7 @@ func canReadPathByRole(u *model.User, reqPath string) bool { func CheckPathLimitWithRoles(u *model.User, reqPath string) bool { perm := MergeRolePermissions(u, reqPath) if HasPermission(perm, PermPathLimit) { - return canReadPathByRole(u, reqPath) + return CanReadPathByRole(u, reqPath) } return true } diff --git a/server/handles/fsread.go b/server/handles/fsread.go index cc403c4a..8cf3c9b0 100644 --- a/server/handles/fsread.go +++ b/server/handles/fsread.go @@ -107,7 +107,14 @@ func FsList(c *gin.Context) { common.ErrorResp(c, err, 500) return } - total, objs := pagination(objs, &req.PageReq) + filtered := make([]model.Obj, 0, len(objs)) + for _, obj := range objs { + childPath := stdpath.Join(reqPath, obj.GetName()) + if common.CanReadPathByRole(user, childPath) { + filtered = append(filtered, obj) + } + } + total, objs := pagination(filtered, &req.PageReq) provider := "unknown" storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{}) if err == nil { @@ -161,7 +168,14 @@ func FsDirs(c *gin.Context) { common.ErrorResp(c, err, 500) return } - dirs := filterDirs(objs) + visible := make([]model.Obj, 0, len(objs)) + for _, obj := range objs { + childPath := stdpath.Join(reqPath, obj.GetName()) + if common.CanReadPathByRole(user, childPath) { + visible = append(visible, obj) + } + } + dirs := filterDirs(visible) common.SuccessResp(c, dirs) } diff --git a/server/webdav.go b/server/webdav.go index a65896df..582c469d 100644 --- a/server/webdav.go +++ b/server/webdav.go @@ -95,6 +95,9 @@ func WebDAVAuth(c *gin.Context) { c.Abort() return } + if roles, err := op.GetRolesByUserID(user.ID); err == nil { + user.RolesDetail = roles + } reqPath := c.Param("path") if reqPath == "" { reqPath = "/" @@ -107,7 +110,8 @@ func WebDAVAuth(c *gin.Context) { return } perm := common.MergeRolePermissions(user, reqPath) - if user.Disabled || !common.HasPermission(perm, common.PermWebdavRead) { + webdavRead := common.HasPermission(perm, common.PermWebdavRead) + if user.Disabled || (!webdavRead && (c.Request.Method != "PROPFIND" || !common.HasChildPermission(user, reqPath, common.PermWebdavRead))) { if c.Request.Method == "OPTIONS" { c.Set("user", guest) c.Next() diff --git a/server/webdav/file.go b/server/webdav/file.go index ab78d261..419c7b07 100644 --- a/server/webdav/file.go +++ b/server/webdav/file.go @@ -94,6 +94,7 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn depth = 0 } meta, _ := op.GetNearestMeta(name) + user := ctx.Value("user").(*model.User) // Read directory names. objs, err := fs.List(context.WithValue(ctx, "meta", meta), name, &fs.ListArgs{}) //f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0) @@ -108,6 +109,9 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn for _, fileInfo := range objs { filename := path.Join(name, fileInfo.GetName()) + if !common.CanReadPathByRole(user, filename) { + continue + } if err != nil { if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { return err diff --git a/server/webdav/webdav.go b/server/webdav/webdav.go index f22e15aa..dde73559 100644 --- a/server/webdav/webdav.go +++ b/server/webdav/webdav.go @@ -648,6 +648,98 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status mw := multistatusWriter{w: w} + if utils.PathEqual(reqPath, user.BasePath) { + hasRootPerm := false + for _, role := range user.RolesDetail { + for _, entry := range role.PermissionScopes { + if utils.PathEqual(entry.Path, user.BasePath) { + hasRootPerm = true + break + } + } + if hasRootPerm { + break + } + } + if !hasRootPerm { + basePaths := model.GetAllBasePathsFromRoles(user) + type infoItem struct { + path string + info model.Obj + } + infos := []infoItem{{reqPath, fi}} + seen := make(map[string]struct{}) + for _, p := range basePaths { + if !utils.IsSubPath(user.BasePath, p) { + continue + } + rel := strings.TrimPrefix( + strings.TrimPrefix( + utils.FixAndCleanPath(p), + utils.FixAndCleanPath(user.BasePath), + ), + "/", + ) + dir := strings.Split(rel, "/")[0] + if dir == "" { + continue + } + if _, ok := seen[dir]; ok { + continue + } + seen[dir] = struct{}{} + sp := utils.FixAndCleanPath(path.Join(user.BasePath, dir)) + info, err := fs.Get(ctx, sp, &fs.GetArgs{}) + if err != nil { + continue + } + infos = append(infos, infoItem{sp, info}) + } + for _, item := range infos { + var pstats []Propstat + if pf.Propname != nil { + pnames, err := propnames(ctx, h.LockSystem, item.info) + if err != nil { + return http.StatusInternalServerError, err + } + pstat := Propstat{Status: http.StatusOK} + for _, xmlname := range pnames { + pstat.Props = append(pstat.Props, Property{XMLName: xmlname}) + } + pstats = append(pstats, pstat) + } else if pf.Allprop != nil { + pstats, err = allprop(ctx, h.LockSystem, item.info, pf.Prop) + if err != nil { + return http.StatusInternalServerError, err + } + } else { + pstats, err = props(ctx, h.LockSystem, item.info, pf.Prop) + if err != nil { + return http.StatusInternalServerError, err + } + } + rel := strings.TrimPrefix( + strings.TrimPrefix( + utils.FixAndCleanPath(item.path), + utils.FixAndCleanPath(user.BasePath), + ), + "/", + ) + href := utils.EncodePath(path.Join("/", h.Prefix, rel), true) + if href != "/" && item.info.IsDir() { + href += "/" + } + if err := mw.write(makePropstatResponse(href, pstats)); err != nil { + return http.StatusInternalServerError, err + } + } + if err := mw.close(); err != nil { + return http.StatusInternalServerError, err + } + return 0, nil + } + } + walkFn := func(reqPath string, info model.Obj, err error) error { if err != nil { return err @@ -671,7 +763,14 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status if err != nil { return err } - href := path.Join(h.Prefix, strings.TrimPrefix(reqPath, user.BasePath)) + rel := strings.TrimPrefix( + strings.TrimPrefix( + utils.FixAndCleanPath(reqPath), + utils.FixAndCleanPath(user.BasePath), + ), + "/", + ) + href := utils.EncodePath(path.Join("/", h.Prefix, rel), true) if href != "/" && info.IsDir() { href += "/" }