From f61d13d4330348493e4fa64a3b595999e9b6b100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E7=9F=B3?= Date: Sat, 26 Jul 2025 15:20:08 +0800 Subject: [PATCH 1/4] refactor(convert_role): Improve role conversion logic for legacy formats (#9219) - Add new imports: `database/sql`, `encoding/json`, and `conf` package in `convert_role.go`. - Simplify permission entry initialization by removing redundant struct formatting. - Update error logging messages for better clarity. - Replace `op.GetUsers` with direct database access for fetching user roles. - Implement role update logic using `rawDb` and handle legacy int role conversion. - Count the number of users whose roles are updated and log completion. - Introduce `IsLegacyRoleDetected` function to check for legacy role formats. - Modify `cmd/common.go` to invoke role conversion if legacy format is detected. --- cmd/common.go | 7 ++ .../bootstrap/patch/v3_46_0/convert_role.go | 107 ++++++++++++++---- 2 files changed, 89 insertions(+), 25 deletions(-) diff --git a/cmd/common.go b/cmd/common.go index 8a73f9b0..d88a86eb 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0" "os" "path/filepath" "strconv" @@ -16,6 +17,12 @@ func Init() { bootstrap.InitConfig() bootstrap.Log() bootstrap.InitDB() + + if v3_46_0.IsLegacyRoleDetected() { + utils.Log.Warnf("Detected legacy role format, executing ConvertLegacyRoles patch early...") + v3_46_0.ConvertLegacyRoles() + } + data.InitData() bootstrap.InitStreamLimit() bootstrap.InitIndex() diff --git a/internal/bootstrap/patch/v3_46_0/convert_role.go b/internal/bootstrap/patch/v3_46_0/convert_role.go index 43799485..3aac95b6 100644 --- a/internal/bootstrap/patch/v3_46_0/convert_role.go +++ b/internal/bootstrap/patch/v3_46_0/convert_role.go @@ -1,7 +1,10 @@ package v3_46_0 import ( + "database/sql" + "encoding/json" "errors" + "github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/db" "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/op" @@ -83,47 +86,101 @@ func ConvertLegacyRoles() { } } - users, _, err := op.GetUsers(1, -1) + rawDb := db.GetDb() + table := conf.Conf.Database.TablePrefix + "users" + rows, err := rawDb.Table(table).Select("id, username, role").Rows() if err != nil { utils.Log.Errorf("[convert roles] failed to get users: %v", err) return } + defer rows.Close() - for i := range users { - user := users[i] - if user.Role == nil { + var updatedCount int + for rows.Next() { + var id uint + var username string + var rawRole []byte + + if err := rows.Scan(&id, &username, &rawRole); err != nil { + utils.Log.Warnf("[convert roles] skip user scan err: %v", err) continue } - changed := false - var roles model.Roles - for _, r := range user.Role { + + utils.Log.Debugf("[convert roles] user: %s raw role: %s", username, string(rawRole)) + + if len(rawRole) == 0 { + continue + } + + var oldRoles []int + wasSingleInt := false + if err := json.Unmarshal(rawRole, &oldRoles); err != nil { + var single int + if err := json.Unmarshal(rawRole, &single); err != nil { + utils.Log.Warnf("[convert roles] user %s has invalid role: %s", username, string(rawRole)) + continue + } + oldRoles = []int{single} + wasSingleInt = true + } + + var newRoles model.Roles + for _, r := range oldRoles { switch r { case model.ADMIN: - roles = append(roles, int(adminRole.ID)) - if int(adminRole.ID) != r { - changed = true - } + newRoles = append(newRoles, int(adminRole.ID)) case model.GUEST: - roles = append(roles, int(guestRole.ID)) - if int(guestRole.ID) != r { - changed = true - } + newRoles = append(newRoles, int(guestRole.ID)) case model.GENERAL: - roles = append(roles, int(generalRole.ID)) - if int(generalRole.ID) != r { - changed = true - } + newRoles = append(newRoles, int(generalRole.ID)) default: - roles = append(roles, r) + newRoles = append(newRoles, r) } } - if changed { - user.Role = roles - if err := db.UpdateUser(&user); err != nil { - utils.Log.Errorf("[convert roles] failed to update user %s: %v", user.Username, err) + + if wasSingleInt { + err := rawDb.Table(table).Where("id = ?", id).Update("role", newRoles).Error + if err != nil { + utils.Log.Errorf("[convert roles] failed to update user %s: %v", username, err) + } else { + updatedCount++ + utils.Log.Infof("[convert roles] updated user %s: %v → %v", username, oldRoles, newRoles) } } } - utils.Log.Infof("[convert roles] completed role conversion for %d users", len(users)) + utils.Log.Infof("[convert roles] completed role conversion for %d users", updatedCount) +} + +func IsLegacyRoleDetected() bool { + rawDb := db.GetDb() + table := conf.Conf.Database.TablePrefix + "users" + rows, err := rawDb.Table(table).Select("role").Rows() + if err != nil { + utils.Log.Errorf("[role check] failed to scan user roles: %v", err) + return false + } + defer rows.Close() + + for rows.Next() { + var raw sql.RawBytes + if err := rows.Scan(&raw); err != nil { + continue + } + if len(raw) == 0 { + continue + } + + var roles []int + if err := json.Unmarshal(raw, &roles); err == nil { + continue + } + + var single int + if err := json.Unmarshal(raw, &single); err == nil { + utils.Log.Infof("[role check] detected legacy int role: %d", single) + return true + } + } + return false } From 91cc7529a035d7f78d332c4cf54f501ffb65c2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E7=9F=B3?= Date: Sun, 27 Jul 2025 22:25:45 +0800 Subject: [PATCH 2/4] feat(user/role/storage): enhance user and storage operations with additional validations (#9223) - Update `CreateUser` to adjust `BasePath` based on user roles and clean paths. - Modify `UpdateUser` to incorporate role-based path changes. - Add validation in `CreateStorage` and `UpdateStorage` to prevent root mount path. - Prevent changes to admin user's role and username in user handler. - Update `UpdateRole` to modify user base paths when role paths change, and clear user cache accordingly. - Import `errors` package to handle error messages. --- internal/op/role.go | 15 +++++++++++++++ internal/op/storage.go | 8 ++++++++ internal/op/user.go | 31 ++++++++++++++++++++++++++++++- server/handles/user.go | 17 +++++++++++++---- 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/internal/op/role.go b/internal/op/role.go index 64502f98..e0f2dc75 100644 --- a/internal/op/role.go +++ b/internal/op/role.go @@ -2,6 +2,7 @@ package op import ( "fmt" + "github.com/pkg/errors" "time" "github.com/Xhofe/go-cache" @@ -102,6 +103,20 @@ func UpdateRole(r *model.Role) error { for i := range r.PermissionScopes { r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path) } + if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 && + old.PermissionScopes[0].Path != r.PermissionScopes[0].Path { + + oldPath := old.PermissionScopes[0].Path + newPath := r.PermissionScopes[0].Path + modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath) + if err != nil { + return errors.WithMessage(err, "failed to update user base path when role updated") + } + + for _, name := range modifiedUsernames { + userCache.Del(name) + } + } roleCache.Del(fmt.Sprint(r.ID)) roleCache.Del(r.Name) return db.UpdateRole(r) diff --git a/internal/op/storage.go b/internal/op/storage.go index 2ec68aae..2833afa8 100644 --- a/internal/op/storage.go +++ b/internal/op/storage.go @@ -46,6 +46,11 @@ func GetStorageByMountPath(mountPath string) (driver.Driver, error) { func CreateStorage(ctx context.Context, storage model.Storage) (uint, error) { storage.Modified = time.Now() storage.MountPath = utils.FixAndCleanPath(storage.MountPath) + + if storage.MountPath == "/" { + return 0, errors.New("Mount path cannot be '/'") + } + var err error // check driver first driverName := storage.Driver @@ -205,6 +210,9 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error { } storage.Modified = time.Now() storage.MountPath = utils.FixAndCleanPath(storage.MountPath) + if storage.MountPath == "/" { + return errors.New("Mount path cannot be '/'") + } err = db.UpdateStorage(&storage) if err != nil { return errors.WithMessage(err, "failed update storage in database") diff --git a/internal/op/user.go b/internal/op/user.go index e775df63..30b9d0e6 100644 --- a/internal/op/user.go +++ b/internal/op/user.go @@ -78,7 +78,25 @@ func GetUsers(pageIndex, pageSize int) (users []model.User, count int64, err err func CreateUser(u *model.User) error { u.BasePath = utils.FixAndCleanPath(u.BasePath) - return db.CreateUser(u) + + err := db.CreateUser(u) + if err != nil { + return err + } + + roles, err := GetRolesByUserID(u.ID) + if err == nil { + for _, role := range roles { + if len(role.PermissionScopes) > 0 { + u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path) + break + } + } + _ = db.UpdateUser(u) + userCache.Del(u.Username) + } + + return nil } func DeleteUserById(id uint) error { @@ -106,6 +124,17 @@ func UpdateUser(u *model.User) error { } userCache.Del(old.Username) u.BasePath = utils.FixAndCleanPath(u.BasePath) + if len(u.Role) > 0 { + roles, err := GetRolesByUserID(u.ID) + if err == nil { + for _, role := range roles { + if len(role.PermissionScopes) > 0 { + u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path) + break + } + } + } + } return db.UpdateUser(u) } diff --git a/server/handles/user.go b/server/handles/user.go index 50eaf969..b729d117 100644 --- a/server/handles/user.go +++ b/server/handles/user.go @@ -1,6 +1,7 @@ package handles import ( + "github.com/alist-org/alist/v3/pkg/utils" "strconv" "github.com/alist-org/alist/v3/internal/model" @@ -60,10 +61,18 @@ func UpdateUser(c *gin.Context) { common.ErrorResp(c, err, 500) return } - //if !utils.SliceEqual(user.Role, req.Role) { - // common.ErrorStrResp(c, "role can not be changed", 400) - // return - //} + + if user.Username == "admin" { + if !utils.SliceEqual(user.Role, req.Role) { + common.ErrorStrResp(c, "cannot change role of admin user", 403) + return + } + if user.Username != req.Username { + common.ErrorStrResp(c, "cannot change username of admin user", 403) + return + } + } + if req.Password == "" { req.PwdHash = user.PwdHash req.Salt = user.Salt From 4d7c2a09ce4716cecc541d9e2518d1fdf1d23172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E7=9F=B3?= Date: Tue, 29 Jul 2025 09:42:34 +0800 Subject: [PATCH 3/4] docs(README): Add API documentation links across multiple languages (#9225) - Add API documentation section to `README.md` with link to Apifox - Add API documentation section to `README_ja.md` with Japanese translation and link to Apifox - Add API documentation section to `README_cn.md` with Chinese translation and link to Apifox --- README.md | 4 ++++ README_cn.md | 4 ++++ README_ja.md | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 2bd7e812..5a93997f 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,10 @@ English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing +## API Documentation (via Apifox): + + + ## Demo diff --git a/README_cn.md b/README_cn.md index 9052e79b..79ed864b 100644 --- a/README_cn.md +++ b/README_cn.md @@ -99,6 +99,10 @@ +## API 文档(通过 Apifox 提供) + + + ## Demo diff --git a/README_ja.md b/README_ja.md index 4dcdfd20..9291b2ac 100644 --- a/README_ja.md +++ b/README_ja.md @@ -100,6 +100,10 @@ +## APIドキュメント(Apifox 提供) + + + ## デモ From 33530554821085e069cf165083e0d7862879cb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E7=9F=B3?= Date: Tue, 29 Jul 2025 18:35:47 +0800 Subject: [PATCH 4/4] Update Dockerfile.ci (#9230) chore(docker): Update base image from alpine:edge to alpine:3.20.7 in Dockerfile.ci --- Dockerfile.ci | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.ci b/Dockerfile.ci index a17aae9f..6075acc6 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1,4 +1,4 @@ -FROM alpine:edge +FROM alpine:3.20.7 ARG TARGETPLATFORM ARG INSTALL_FFMPEG=false @@ -31,4 +31,4 @@ RUN /entrypoint.sh version ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2} VOLUME /opt/alist/data/ EXPOSE 5244 5245 -CMD [ "/entrypoint.sh" ] \ No newline at end of file +CMD [ "/entrypoint.sh" ]