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.
pull/8491/merge v3.50.0
千石 2025-08-15 08:10:55 -07:00 committed by GitHub
parent aea3ba1499
commit fcfb3369d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 180 additions and 12 deletions

View File

@ -145,12 +145,15 @@ func (u *User) CheckPathLimit() bool {
} }
func (u *User) JoinPath(reqPath string) (string, error) { func (u *User) JoinPath(reqPath string) (string, error) {
if reqPath == "/" {
return utils.FixAndCleanPath(u.BasePath), nil
}
path, err := utils.JoinBasePath(u.BasePath, reqPath) path, err := utils.JoinBasePath(u.BasePath, reqPath)
if err != nil { if err != nil {
return "", err return "", err
} }
if u.CheckPathLimit() { if path != "/" && u.CheckPathLimit() {
basePaths := GetAllBasePathsFromRoles(u) basePaths := GetAllBasePathsFromRoles(u)
match := false match := false
for _, base := range basePaths { for _, base := range basePaths {
@ -206,12 +209,23 @@ func (u *User) WebAuthnIcon() string {
return "https://alistgo.com/logo.svg" 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 // GetAllBasePathsFromRoles returns all permission paths from user's roles
func GetAllBasePathsFromRoles(u *User) []string { func GetAllBasePathsFromRoles(u *User) []string {
basePaths := make([]string, 0) basePaths := make([]string, 0)
seen := make(map[string]struct{}) 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 { for _, entry := range role.PermissionScopes {
if entry.Path == "" { if entry.Path == "" {
continue continue

View File

@ -15,6 +15,10 @@ import (
var roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2)) var roleCache = cache.NewMemCache[*model.Role](cache.WithShards[*model.Role](2))
var roleG singleflight.Group[*model.Role] var roleG singleflight.Group[*model.Role]
func init() {
model.FetchRole = GetRole
}
func GetRole(id uint) (*model.Role, error) { func GetRole(id uint) (*model.Role, error) {
key := fmt.Sprint(id) key := fmt.Sprint(id)
if r, ok := roleCache.Get(key); ok { if r, ok := roleCache.Get(key); ok {

View File

@ -43,17 +43,23 @@ func MergeRolePermissions(u *model.User, reqPath string) int32 {
if err != nil { if err != nil {
continue continue
} }
for _, entry := range role.PermissionScopes { if reqPath == "/" || utils.PathEqual(reqPath, u.BasePath) {
if utils.IsSubPath(entry.Path, reqPath) { for _, entry := range role.PermissionScopes {
perm |= entry.Permission perm |= entry.Permission
} }
} else {
for _, entry := range role.PermissionScopes {
if utils.IsSubPath(entry.Path, reqPath) {
perm |= entry.Permission
}
}
} }
} }
return perm return perm
} }
func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password string) bool { func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password string) bool {
if !canReadPathByRole(u, reqPath) { if !CanReadPathByRole(u, reqPath) {
return false return false
} }
perm := MergeRolePermissions(u, reqPath) perm := MergeRolePermissions(u, reqPath)
@ -78,7 +84,30 @@ func CanAccessWithRoles(u *model.User, meta *model.Meta, reqPath, password strin
return meta.Password == password 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 { if u == nil {
return false return false
} }
@ -88,7 +117,7 @@ func canReadPathByRole(u *model.User, reqPath string) bool {
continue continue
} }
for _, entry := range role.PermissionScopes { for _, entry := range role.PermissionScopes {
if utils.IsSubPath(entry.Path, reqPath) { if utils.IsSubPath(reqPath, entry.Path) && HasPermission(entry.Permission, bit) {
return true return true
} }
} }
@ -102,7 +131,7 @@ func canReadPathByRole(u *model.User, reqPath string) bool {
func CheckPathLimitWithRoles(u *model.User, reqPath string) bool { func CheckPathLimitWithRoles(u *model.User, reqPath string) bool {
perm := MergeRolePermissions(u, reqPath) perm := MergeRolePermissions(u, reqPath)
if HasPermission(perm, PermPathLimit) { if HasPermission(perm, PermPathLimit) {
return canReadPathByRole(u, reqPath) return CanReadPathByRole(u, reqPath)
} }
return true return true
} }

View File

@ -107,7 +107,14 @@ func FsList(c *gin.Context) {
common.ErrorResp(c, err, 500) common.ErrorResp(c, err, 500)
return 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" provider := "unknown"
storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{}) storage, err := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
if err == nil { if err == nil {
@ -161,7 +168,14 @@ func FsDirs(c *gin.Context) {
common.ErrorResp(c, err, 500) common.ErrorResp(c, err, 500)
return 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) common.SuccessResp(c, dirs)
} }

View File

@ -95,6 +95,9 @@ func WebDAVAuth(c *gin.Context) {
c.Abort() c.Abort()
return return
} }
if roles, err := op.GetRolesByUserID(user.ID); err == nil {
user.RolesDetail = roles
}
reqPath := c.Param("path") reqPath := c.Param("path")
if reqPath == "" { if reqPath == "" {
reqPath = "/" reqPath = "/"
@ -107,7 +110,8 @@ func WebDAVAuth(c *gin.Context) {
return return
} }
perm := common.MergeRolePermissions(user, reqPath) 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" { if c.Request.Method == "OPTIONS" {
c.Set("user", guest) c.Set("user", guest)
c.Next() c.Next()

View File

@ -94,6 +94,7 @@ func walkFS(ctx context.Context, depth int, name string, info model.Obj, walkFn
depth = 0 depth = 0
} }
meta, _ := op.GetNearestMeta(name) meta, _ := op.GetNearestMeta(name)
user := ctx.Value("user").(*model.User)
// Read directory names. // Read directory names.
objs, err := fs.List(context.WithValue(ctx, "meta", meta), name, &fs.ListArgs{}) objs, err := fs.List(context.WithValue(ctx, "meta", meta), name, &fs.ListArgs{})
//f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0) //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 { for _, fileInfo := range objs {
filename := path.Join(name, fileInfo.GetName()) filename := path.Join(name, fileInfo.GetName())
if !common.CanReadPathByRole(user, filename) {
continue
}
if err != nil { if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
return err return err

View File

@ -648,6 +648,98 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
mw := multistatusWriter{w: w} 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 { walkFn := func(reqPath string, info model.Obj, err error) error {
if err != nil { if err != nil {
return err return err
@ -671,7 +763,14 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
if err != nil { if err != nil {
return err 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() { if href != "/" && info.IsDir() {
href += "/" href += "/"
} }