Skip to content
31 changes: 27 additions & 4 deletions internal/fs/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package fs

import (
"context"

"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"path"
)

// List files
Expand Down Expand Up @@ -43,7 +44,29 @@ func list(ctx context.Context, path string, args *ListArgs) ([]model.Obj, error)
om.InitHideReg(meta.Hide)
}
objs := om.Merge(_objs, virtualFiles...)
return objs, nil
objs, err = filterReadableObjs(objs, user, path, meta)
return objs, err
}

func filterReadableObjs(objs []model.Obj, user *model.User, reqPath string, parentMeta *model.Meta) ([]model.Obj, error) {
var result []model.Obj
for _, obj := range objs {
var meta *model.Meta
objPath := path.Join(reqPath, obj.GetName())
if obj.IsDir() {
var err error
meta, err = op.GetNearestMeta(objPath)
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
return result, err
}
} else {
meta = parentMeta
}
if common.CanRead(user, meta, objPath) {
result = append(result, obj)
}
}
return result, nil
}

func whetherHide(user *model.User, meta *model.Meta, path string) bool {
Expand All @@ -60,7 +83,7 @@ func whetherHide(user *model.User, meta *model.Meta, path string) bool {
return false
}
// if meta doesn't apply to sub_folder, don't hide
if !utils.PathEqual(meta.Path, path) && !meta.HSub {
if !common.MetaCoversPath(meta.Path, path, meta.HSub) {
return false
}
// if is guest, hide
Expand Down
151 changes: 151 additions & 0 deletions internal/fs/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package fs

import (
"testing"

"github.com/OpenListTeam/OpenList/v4/internal/model"
)

func TestWhetherHide(t *testing.T) {
tests := []struct {
name string
user *model.User
meta *model.Meta
path string
want bool
reason string
}{
{
name: "nil user",
user: nil,
meta: &model.Meta{
Path: "/folder",
Hide: "secret",
HSub: true,
},
path: "/folder",
want: false,
reason: "nil user (treated as admin) should not hide",
},
{
name: "user with can_see_hides permission",
user: &model.User{
Role: model.GENERAL,
Permission: 1, // bit 0 set = can see hides
},
meta: &model.Meta{
Path: "/folder",
Hide: "secret",
HSub: true,
},
path: "/folder",
want: false,
reason: "user with can_see_hides permission should not hide",
},
{
name: "nil meta",
user: &model.User{
Role: model.GUEST,
},
meta: nil,
path: "/folder",
want: false,
reason: "nil meta should not hide",
},
{
name: "empty hide string",
user: &model.User{
Role: model.GUEST,
},
meta: &model.Meta{
Path: "/folder",
Hide: "",
HSub: true,
},
path: "/folder",
want: false,
reason: "empty hide string should not hide",
},
{
name: "exact path match with HSub=false",
user: &model.User{
Role: model.GUEST,
},
meta: &model.Meta{
Path: "/folder",
Hide: "secret",
HSub: false,
},
path: "/folder",
want: true,
reason: "exact path match should hide for guest",
},
{
name: "sub path with HSub=true",
user: &model.User{
Role: model.GUEST,
},
meta: &model.Meta{
Path: "/folder",
Hide: "secret",
HSub: true,
},
path: "/folder/subfolder",
want: true,
reason: "sub path with HSub=true should hide for guest",
},
{
name: "sub path with HSub=false",
user: &model.User{
Role: model.GUEST,
},
meta: &model.Meta{
Path: "/folder",
Hide: "secret",
HSub: false,
},
path: "/folder/subfolder",
want: false,
reason: "sub path with HSub=false should not hide",
},
{
name: "non-sub path with HSub=true",
user: &model.User{
Role: model.GUEST,
},
meta: &model.Meta{
Path: "/folder",
Hide: "secret",
HSub: true,
},
path: "/other",
want: false,
reason: "non-sub path should not hide even with HSub=true",
},
{
name: "user without can_see_hides permission",
user: &model.User{
Role: model.GENERAL,
Permission: 0, // bit 0 not set = cannot see hides
},
meta: &model.Meta{
Path: "/folder",
Hide: "secret",
HSub: true,
},
path: "/folder",
want: true,
reason: "user without can_see_hides permission should hide",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := whetherHide(tt.user, tt.meta, tt.path)
if got != tt.want {
t.Errorf("whetherHide() = %v, want %v\nReason: %s",
got, tt.want, tt.reason)
}
})
}
}
28 changes: 16 additions & 12 deletions internal/model/meta.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package model

type Meta struct {
ID uint `json:"id" gorm:"primaryKey"`
Path string `json:"path" gorm:"unique" binding:"required"`
Password string `json:"password"`
PSub bool `json:"p_sub"`
Write bool `json:"write"`
WSub bool `json:"w_sub"`
Hide string `json:"hide"`
HSub bool `json:"h_sub"`
Readme string `json:"readme"`
RSub bool `json:"r_sub"`
Header string `json:"header"`
HeaderSub bool `json:"header_sub"`
ID uint `json:"id" gorm:"primaryKey"`
Path string `json:"path" gorm:"unique" binding:"required"`
ReadUsers []uint `json:"read_users" gorm:"serializer:json"`
ReadUsersSub bool `json:"read_users_sub"`
WriteUsers []uint `json:"write_users" gorm:"serializer:json"`
WriteUsersSub bool `json:"write_users_sub"`
Password string `json:"password"`
PSub bool `json:"p_sub"`
Write bool `json:"write"`
WSub bool `json:"w_sub"`
Hide string `json:"hide"`
HSub bool `json:"h_sub"`
Readme string `json:"readme"`
RSub bool `json:"r_sub"`
Header string `json:"header"`
HeaderSub bool `json:"header_sub"`
}
6 changes: 3 additions & 3 deletions internal/model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@ func (u *User) CanAddOfflineDownloadTasks() bool {
return CanAddOfflineDownloadTasks(u.Permission)
}

func CanWrite(permission int32) bool {
func CanWriteContent(permission int32) bool {
return (permission>>3)&1 == 1
}

func (u *User) CanWrite() bool {
return CanWrite(u.Permission)
func (u *User) CanWriteContent() bool {
return CanWriteContent(u.Permission)
}

func CanRename(permission int32) bool {
Expand Down
42 changes: 34 additions & 8 deletions server/common/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package common

import (
"path"
"slices"
"strings"

"github.com/OpenListTeam/OpenList/v4/internal/conf"
Expand All @@ -17,31 +18,49 @@ func IsStorageSignEnabled(rawPath string) bool {
return storage != nil && storage.GetStorage().EnableSign
}

func CanWrite(meta *model.Meta, path string) bool {
if meta == nil || !meta.Write {
func CanRead(user *model.User, meta *model.Meta, path string) bool {
// nil user is treated as internal/system context and bypasses per-user read restrictions
if user == nil {
return true
}
if meta != nil && len(meta.ReadUsers) > 0 && !slices.Contains(meta.ReadUsers, user.ID) && (meta.ReadUsersSub || meta.Path == path) {
return false
}
return meta.WSub || meta.Path == path
return true
}

func IsApply(metaPath, reqPath string, applySub bool) bool {
if utils.PathEqual(metaPath, reqPath) {
func CanWrite(user *model.User, meta *model.Meta, path string) bool {
// nil user is treated as internal/system context and bypasses per-user read restrictions
if user == nil {
return true
}
return utils.IsSubPath(metaPath, reqPath) && applySub
if meta != nil && len(meta.WriteUsers) > 0 && !slices.Contains(meta.WriteUsers, user.ID) && (meta.WriteUsersSub || meta.Path == path) {
return false
}
return true
}

func CanWriteContentBypassUserPerms(meta *model.Meta, path string) bool {
if meta == nil || !meta.Write {
return false
}
return MetaCoversPath(meta.Path, path, meta.WSub)
}

func CanAccess(user *model.User, meta *model.Meta, reqPath string, password string) bool {
// if the reqPath is in hide (only can check the nearest meta) and user can't see hides, can't access
if meta != nil && !user.CanSeeHides() && meta.Hide != "" &&
IsApply(meta.Path, path.Dir(reqPath), meta.HSub) { // the meta should apply to the parent of current path
MetaCoversPath(meta.Path, path.Dir(reqPath), meta.HSub) { // the meta should apply to the parent of current path
for _, hide := range strings.Split(meta.Hide, "\n") {
re := regexp2.MustCompile(hide, regexp2.None)
if isMatch, _ := re.MatchString(path.Base(reqPath)); isMatch {
return false
}
}
}
if !CanRead(user, meta, reqPath) {
return false
}
// if is not guest and can access without password
if user.CanAccessWithoutPassword() {
return true
Expand All @@ -51,13 +70,20 @@ func CanAccess(user *model.User, meta *model.Meta, reqPath string, password stri
return true
}
// if meta doesn't apply to sub_folder, can access
if !utils.PathEqual(meta.Path, reqPath) && !meta.PSub {
if !MetaCoversPath(meta.Path, reqPath, meta.PSub) {
return true
}
// validate password
return meta.Password == password
}

func MetaCoversPath(metaPath, reqPath string, applyToSubFolder bool) bool {
if utils.PathEqual(metaPath, reqPath) {
return true
}
return utils.IsSubPath(metaPath, reqPath) && applyToSubFolder
}

// ShouldProxy TODO need optimize
// when should be proxy?
// 1. config.MustProxy()
Expand Down
Loading
Loading