mirror of https://github.com/Xhofe/alist
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
parent
aea3ba1499
commit
fcfb3369d1
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 += "/"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue