mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 04:53:09 +08:00
fix(meta): enforce case-insensitive access controls
- Add an invalidated metadata snapshot for case-insensitive fallback lookups - Enforce segment-aware metadata coverage for passwords and download signatures - Add regression tests while preserving case-sensitive write authorization Co-authored-by: Codex <267193182+codex@users.noreply.github.com> Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com>
This commit is contained in:
@@ -13,6 +13,14 @@ func GetMetaByPath(path string) (*model.Meta, error) {
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func GetAllMetas() ([]model.Meta, error) {
|
||||
var metas []model.Meta
|
||||
if err := db.Find(&metas).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "failed get all metas")
|
||||
}
|
||||
return metas, nil
|
||||
}
|
||||
|
||||
func GetMetaById(id uint) (*model.Meta, error) {
|
||||
var u model.Meta
|
||||
if err := db.First(&u, id).Error; err != nil {
|
||||
|
||||
+70
-11
@@ -2,6 +2,8 @@ package op
|
||||
|
||||
import (
|
||||
stdpath "path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/db"
|
||||
@@ -15,6 +17,9 @@ import (
|
||||
)
|
||||
|
||||
var metaCache = cache.NewMemCache(cache.WithShards[*model.Meta](2))
|
||||
var metaSnapshotMu sync.Mutex
|
||||
var metaSnapshot []model.Meta
|
||||
var metaSnapshotLoaded bool
|
||||
|
||||
// metaG maybe not needed
|
||||
var metaG singleflight.Group[*model.Meta]
|
||||
@@ -23,17 +28,56 @@ func GetNearestMeta(path string) (*model.Meta, error) {
|
||||
return getNearestMeta(utils.FixAndCleanPath(path))
|
||||
}
|
||||
func getNearestMeta(path string) (*model.Meta, error) {
|
||||
meta, err := GetMetaByPath(path)
|
||||
if err == nil {
|
||||
return meta, nil
|
||||
var metas []model.Meta
|
||||
loaded := false
|
||||
for {
|
||||
meta, err := GetMetaByPath(path)
|
||||
if err == nil {
|
||||
return meta, nil
|
||||
}
|
||||
if errors.Cause(err) != errs.MetaNotFound {
|
||||
return nil, err
|
||||
}
|
||||
if !loaded {
|
||||
metas, err = getMetaSnapshot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loaded = true
|
||||
}
|
||||
var matched *model.Meta
|
||||
for i := range metas {
|
||||
if !strings.EqualFold(utils.FixAndCleanPath(metas[i].Path), path) {
|
||||
continue
|
||||
}
|
||||
if matched != nil {
|
||||
return nil, errors.Errorf("multiple metas match %q case-insensitively", path)
|
||||
}
|
||||
matched = &metas[i]
|
||||
}
|
||||
if matched != nil {
|
||||
return matched, nil
|
||||
}
|
||||
if path == "/" {
|
||||
return nil, errs.MetaNotFound
|
||||
}
|
||||
path = stdpath.Dir(path)
|
||||
}
|
||||
if errors.Cause(err) != errs.MetaNotFound {
|
||||
}
|
||||
|
||||
func getMetaSnapshot() ([]model.Meta, error) {
|
||||
metaSnapshotMu.Lock()
|
||||
defer metaSnapshotMu.Unlock()
|
||||
if metaSnapshotLoaded {
|
||||
return metaSnapshot, nil
|
||||
}
|
||||
metas, err := db.GetAllMetas()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if path == "/" {
|
||||
return nil, errs.MetaNotFound
|
||||
}
|
||||
return getNearestMeta(stdpath.Dir(path))
|
||||
metaSnapshot = metas
|
||||
metaSnapshotLoaded = true
|
||||
return metaSnapshot, nil
|
||||
}
|
||||
|
||||
func GetMetaByPath(path string) (*model.Meta, error) {
|
||||
@@ -68,7 +112,12 @@ func DeleteMetaById(id uint) error {
|
||||
return err
|
||||
}
|
||||
metaCache.Del(old.Path)
|
||||
return db.DeleteMetaById(id)
|
||||
metaSnapshotMu.Lock()
|
||||
defer metaSnapshotMu.Unlock()
|
||||
err = db.DeleteMetaById(id)
|
||||
metaSnapshot = nil
|
||||
metaSnapshotLoaded = false
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateMeta(u *model.Meta) error {
|
||||
@@ -79,13 +128,23 @@ func UpdateMeta(u *model.Meta) error {
|
||||
}
|
||||
metaCache.Del(old.Path)
|
||||
metaCache.Del(u.Path)
|
||||
return db.UpdateMeta(u)
|
||||
metaSnapshotMu.Lock()
|
||||
defer metaSnapshotMu.Unlock()
|
||||
err = db.UpdateMeta(u)
|
||||
metaSnapshot = nil
|
||||
metaSnapshotLoaded = false
|
||||
return err
|
||||
}
|
||||
|
||||
func CreateMeta(u *model.Meta) error {
|
||||
u.Path = utils.FixAndCleanPath(u.Path)
|
||||
metaCache.Del(u.Path)
|
||||
return db.CreateMeta(u)
|
||||
metaSnapshotMu.Lock()
|
||||
defer metaSnapshotMu.Unlock()
|
||||
err := db.CreateMeta(u)
|
||||
metaSnapshot = nil
|
||||
metaSnapshotLoaded = false
|
||||
return err
|
||||
}
|
||||
|
||||
func GetMetaById(id uint) (*model.Meta, error) {
|
||||
|
||||
+17
-3
@@ -44,7 +44,10 @@ func CanWriteContentBypassUserPerms(meta *model.Meta, path string) bool {
|
||||
if meta == nil || !meta.Write {
|
||||
return false
|
||||
}
|
||||
return MetaCoversPath(meta.Path, path, meta.WSub)
|
||||
if utils.PathEqual(meta.Path, path) {
|
||||
return true
|
||||
}
|
||||
return utils.IsSubPath(meta.Path, path) && meta.WSub
|
||||
}
|
||||
|
||||
func CanAccess(user *model.User, meta *model.Meta, reqPath string, password string) bool {
|
||||
@@ -78,10 +81,21 @@ func CanAccess(user *model.User, meta *model.Meta, reqPath string, password stri
|
||||
}
|
||||
|
||||
func MetaCoversPath(metaPath, reqPath string, applyToSubFolder bool) bool {
|
||||
if utils.PathEqual(metaPath, reqPath) {
|
||||
metaPath = utils.FixAndCleanPath(metaPath)
|
||||
reqPath = utils.FixAndCleanPath(reqPath)
|
||||
if strings.EqualFold(metaPath, reqPath) {
|
||||
return true
|
||||
}
|
||||
return utils.IsSubPath(metaPath, reqPath) && applyToSubFolder
|
||||
if !applyToSubFolder {
|
||||
return false
|
||||
}
|
||||
for reqPath != "/" {
|
||||
reqPath = path.Dir(reqPath)
|
||||
if strings.EqualFold(metaPath, reqPath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ShouldProxy TODO need optimize
|
||||
|
||||
@@ -84,6 +84,27 @@ func TestCoversPath(t *testing.T) {
|
||||
applySub: true,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "case-insensitive exact path match",
|
||||
metaPath: "/Folder",
|
||||
reqPath: "/folder",
|
||||
applySub: false,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "case-insensitive sub path match",
|
||||
metaPath: "/Folder",
|
||||
reqPath: "/folder/Subfolder",
|
||||
applySub: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "case-insensitive sibling prefix does not match",
|
||||
metaPath: "/Folder",
|
||||
reqPath: "/folder-name",
|
||||
applySub: true,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -166,6 +187,17 @@ func TestCanWriteContentIgnoringUserPerms(t *testing.T) {
|
||||
want: false,
|
||||
reason: "non-sub path should deny write even with WSub=true",
|
||||
},
|
||||
{
|
||||
name: "case-insensitive match does not broaden write bypass",
|
||||
meta: &model.Meta{
|
||||
Path: "/Folder",
|
||||
Write: true,
|
||||
WSub: true,
|
||||
},
|
||||
path: "/folder/subfolder",
|
||||
want: false,
|
||||
reason: "case-insensitive matching should only enforce restrictions, not grant write bypass",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -579,6 +611,23 @@ func TestCanAccessWithReadPermissions(t *testing.T) {
|
||||
want: false,
|
||||
reason: "user not in ReadUsers list should be denied",
|
||||
},
|
||||
{
|
||||
name: "case-insensitive exact path still requires password",
|
||||
user: &model.User{
|
||||
ID: 1,
|
||||
Role: model.GENERAL,
|
||||
Permission: 0,
|
||||
},
|
||||
meta: &model.Meta{
|
||||
Path: "/Folder",
|
||||
Password: "secret",
|
||||
PSub: false,
|
||||
},
|
||||
reqPath: "/folder",
|
||||
password: "wrong",
|
||||
want: false,
|
||||
reason: "changing only path casing must not bypass an exact-path password",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -60,7 +60,7 @@ func needSign(meta *model.Meta, path string) bool {
|
||||
if meta == nil || meta.Password == "" {
|
||||
return false
|
||||
}
|
||||
if !meta.PSub && path != meta.Path {
|
||||
if !meta.PSub && !common.MetaCoversPath(meta.Path, path, false) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user