Skip to content

Commit 00120cb

Browse files
feat: enhance permission control and label management (#9215)
* 标签管理 * pr检查优化 * feat(role): Implement role management functionality - Add role management routes in `server/router.go` for listing, getting, creating, updating, and deleting roles - Introduce `initRoles()` in `internal/bootstrap/data/data.go` for initializing roles during bootstrap - Create `internal/op/role.go` to handle role operations including caching and singleflight - Implement role handler functions in `server/handles/role.go` for API responses - Define database operations for roles in `internal/db/role.go` - Extend `internal/db/db.go` for role model auto-migration - Design `internal/model/role.go` to represent role structure with ID, name, description, base path, and permissions - Initialize default roles (`admin` and `guest`) in `internal/bootstrap/data/role.go` during startup * refactor(user roles): Support multiple roles for users - Change the `Role` field type from `int` to `[]int` in `drivers/alist_v3/types.go` and `drivers/quqi/types.go`. - Update the `Role` field in `internal/model/user.go` to use a new `Roles` type with JSON and database support. - Modify `IsGuest` and `IsAdmin` methods to check for roles using `Contains` method. - Update `GetUserByRole` method in `internal/db/user.go` to handle multiple roles. - Add `roles.go` to define a new `Roles` type with JSON marshalling and scanning capabilities. - Adjust code in `server/handles/user.go` to compare roles with `utils.SliceEqual`. - Change role initialization for users in `internal/bootstrap/data/dev.go` and `internal/bootstrap/data/user.go`. - Update `Role` handling in `server/handles/task.go`, `server/handles/ssologin.go`, and `server/handles/ldap_login.go`. * feat(user/role): Add path limit check for user and role permissions - Add new permission bit for checking path limits in `user.go` - Implement `CheckPathLimit` method in `User` struct to validate path access - Modify `JoinPath` method in `User` to enforce path limit checks - Update `role.go` to include path limit logic in `Role` struct - Document new permission bit in `Role` and `User` comments for clarity * feat(permission): Add role-based permission handling - Introduce `role_perm.go` for managing user permissions based on roles. - Implement `HasPermission` and `MergeRolePermissions` functions. - Update `webdav.go` to utilize role-based permissions instead of direct user checks. - Modify `fsup.go` to integrate `CanAccessWithRoles` function. - Refactor `fsread.go` to use `common.HasPermission` for permission validation. - Adjust `fsmanage.go` for role-based access control checks. - Enhance `ftp.go` and `sftp.go` to manage FTP access via roles. - Update `fsbatch.go` to employ `MergeRolePermissions` for batch operations. - Replace direct user permission checks with role-based permission handling across various modules. * refactor(user): Replace integer role values with role IDs - Change `GetAdmin()` and `GetGuest()` functions to retrieve role by name and use role ID. - Add patch for version `v3.45.2` to convert legacy integer roles to role IDs. - Update `dev.go` and `user.go` to use role IDs instead of integer values for roles. - Remove redundant code in `role.go` related to guest role creation. - Modify `ssologin.go` and `ldap_login.go` to set user roles to nil instead of using integer roles. - Introduce `convert_roles.go` to handle conversion of legacy roles and ensure role existence in the database. * feat(role_perm): implement support for multiple base paths for roles - Modify role permission checks to support multiple base paths - Update role creation and update functions to handle multiple base paths - Add migration script to convert old base_path to base_paths - Define new Paths type for handling multiple paths in the model - Adjust role model to replace BasePath with BasePaths - Update existing patches to handle roles with multiple base paths - Update bootstrap data to reflect the new base_paths field * feat(role): Restrict modifications to default roles (admin and guest) - Add validation to prevent changes to "admin" and "guest" roles in `UpdateRole` and `DeleteRole` functions. - Introduce `ErrChangeDefaultRole` error in `internal/errs/role.go` to standardize error messaging. - Update role-related API handlers in `server/handles/role.go` to enforce the new restriction. - Enhance comments in `internal/bootstrap/data/role.go` to clarify the significance of default roles. - Ensure consistent error responses for unauthorized role modifications across the application. * 🔄 **refactor(role): Enhance role permission handling** - Replaced `BasePaths` with `PermissionPaths` in `Role` struct for better permission granularity. - Introduced JSON serialization for `PermissionPaths` using `RawPermission` field in `Role` struct. - Implemented `BeforeSave` and `AfterFind` GORM hooks for handling `PermissionPaths` serialization. - Refactored permission calculation logic in `role_perm.go` to work with `PermissionPaths`. - Updated role creation logic to initialize `PermissionPaths` for `admin` and `guest` roles. - Removed deprecated `CheckPathLimit` method from `Role` struct. * fix(model/user/role): update permission settings for admin and role - Change `RawPermission` field in `role.go` to hide JSON representation - Update `Permission` field in `user.go` to `0xFFFF` for full access - Modify `PermissionScopes` in `role.go` to `0xFFFF` for enhanced permissions * 🔒 feat(role-permissions): Enhance role-based access control - Introduce `canReadPathByRole` function in `role_perm.go` to verify path access based on user roles - Modify `CanAccessWithRoles` to include role-based path read check - Add `RoleNames` and `Permissions` to `UserResp` struct in `auth.go` for enhanced user role and permission details - Implement role details aggregation in `auth.go` to populate `RoleNames` and `Permissions` - Update `User` struct in `user.go` to include `RolesDetail` for more detailed role information - Enhance middleware in `auth.go` to load and verify detailed role information for users - Move `guest` user initialization logic in `user.go` to improve code organization and avoid repetition * 🔒 fix(permissions): Add permission checks for archive operations - Add `MergeRolePermissions` and `HasPermission` checks to validate user access for reading archives - Ensure users have `PermReadArchives` before proceeding with `GetNearestMeta` in specific archive paths - Implement permission checks for decompress operations, requiring `PermDecompress` for source paths - Return `PermissionDenied` errors with 403 status if user lacks necessary permissions * 🔒 fix(server): Add permission check for offline download - Add permission merging logic for user roles - Check user has permission for offline download addition - Return error response with "permission denied" if check fails * ✨ feat(role-permission): Implement path-based role permission checks - Add `CheckPathLimitWithRoles` function to validate access based on `PermPathLimit` permission. - Integrate `CheckPathLimitWithRoles` in `offline_download` to enforce path-based access control. - Apply `CheckPathLimitWithRoles` across file system management operations (e.g., creation, movement, deletion). - Ensure `CheckPathLimitWithRoles` is invoked for batch operations and archive-related actions. - Update error handling to return `PermissionDenied` if the path validation fails. - Import `errs` package in `offline_download` for consistent error responses. * ✨ feat(role-permission): Implement path-based role permission checks - Add `CheckPathLimitWithRoles` function to validate access based on `PermPathLimit` permission. - Integrate `CheckPathLimitWithRoles` in `offline_download` to enforce path-based access control. - Apply `CheckPathLimitWithRoles` across file system management operations (e.g., creation, movement, deletion). - Ensure `CheckPathLimitWithRoles` is invoked for batch operations and archive-related actions. - Update error handling to return `PermissionDenied` if the path validation fails. - Import `errs` package in `offline_download` for consistent error responses. * ♻️ refactor(access-control): Update access control logic to use role-based checks - Remove deprecated logic from `CanAccess` function in `check.go`, replacing it with `CanAccessWithRoles` for improved role-based access control. - Modify calls in `search.go` to use `CanAccessWithRoles` for more precise handling of permissions. - Update `fsread.go` to utilize `CanAccessWithRoles`, ensuring accurate access validation based on user roles. - Simplify import statements in `check.go` by removing unused packages to clean up the codebase. * ✨ feat(fs): Improve visibility logic for hidden files - Import `server/common` package to handle permissions more robustly - Update `whetherHide` function to use `MergeRolePermissions` for user-specific path permissions - Replace direct user checks with `HasPermission` for `PermSeeHides` - Enhance logic to ensure `nil` user cases are handled explicitly * 标签管理 * feat(db/auth/user): Enhance role handling and clean permission paths - Comment out role modification checks in `server/handles/user.go` to allow flexible role changes. - Improve permission path handling in `server/handles/auth.go` by normalizing and deduplicating paths. - Introduce `addedPaths` map in `CurrentUser` to prevent duplicate permissions. * feat(storage/db): Implement role permissions path prefix update - Add `UpdateRolePermissionsPathPrefix` function in `role.go` to update role permissions paths. - Modify `storage.go` to call the new function when the mount path is renamed. - Introduce path cleaning and prefix matching logic for accurate path updates. - Ensure roles are updated only if their permission scopes are modified. - Handle potential errors with informative messages during database operations. * feat(role-migration): Implement role conversion and introduce NEWGENERAL role - Add `NEWGENERAL` to the roles enumeration in `user.go` - Create new file `convert_role.go` for migrating legacy roles to new model - Implement `ConvertLegacyRoles` function to handle role conversion with permission scopes - Add `convert_role.go` patch to `all.go` under version `v3.46.0` * feat(role/auth): Add role retrieval by user ID and update path prefixes - Add `GetRolesByUserID` function for efficient role retrieval by user ID - Implement `UpdateUserBasePathPrefix` to update user base paths - Modify `UpdateRolePermissionsPathPrefix` to return modified role IDs - Update `auth.go` middleware to use the new role retrieval function - Refresh role and user caches upon path prefix updates to maintain consistency --------- Co-authored-by: Leslie-Xy <540049476@qq.com>
1 parent 5e15a36 commit 00120cb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1762
-183
lines changed

drivers/alist_v3/driver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (d *AListV3) Init(ctx context.Context) error {
5656
if err != nil {
5757
return err
5858
}
59-
if resp.Data.Role == model.GUEST {
59+
if utils.SliceContains(resp.Data.Role, model.GUEST) {
6060
u := d.Address + "/api/public/settings"
6161
res, err := base.RestyClient.R().Get(u)
6262
if err != nil {

drivers/alist_v3/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ type MeResp struct {
7676
Username string `json:"username"`
7777
Password string `json:"password"`
7878
BasePath string `json:"base_path"`
79-
Role int `json:"role"`
79+
Role []int `json:"role"`
8080
Disabled bool `json:"disabled"`
8181
Permission int `json:"permission"`
8282
SsoId string `json:"sso_id"`

drivers/quqi/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ type Group struct {
8383
Type int `json:"type"`
8484
Name string `json:"name"`
8585
IsAdministrator int `json:"is_administrator"`
86-
Role int `json:"role"`
86+
Role []int `json:"role"`
8787
Avatar string `json:"avatar_url"`
8888
IsStick int `json:"is_stick"`
8989
Nickname string `json:"nickname"`

internal/bootstrap/data/data.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package data
33
import "github.com/alist-org/alist/v3/cmd/flags"
44

55
func InitData() {
6+
initRoles()
67
initUser()
78
initSettings()
89
initTasks()

internal/bootstrap/data/dev.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func initDevData() {
2626
Username: "Noah",
2727
Password: "hsu",
2828
BasePath: "/data",
29-
Role: 0,
29+
Role: nil,
3030
Permission: 512,
3131
})
3232
if err != nil {

internal/bootstrap/data/role.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package data
2+
3+
// initRoles creates the default admin and guest roles if missing.
4+
// These roles are essential and must not be modified or removed.
5+
6+
import (
7+
"github.com/alist-org/alist/v3/internal/model"
8+
"github.com/alist-org/alist/v3/internal/op"
9+
"github.com/alist-org/alist/v3/pkg/utils"
10+
"github.com/pkg/errors"
11+
"gorm.io/gorm"
12+
)
13+
14+
func initRoles() {
15+
guestRole, err := op.GetRoleByName("guest")
16+
if err != nil {
17+
if errors.Is(err, gorm.ErrRecordNotFound) {
18+
guestRole = &model.Role{
19+
ID: uint(model.GUEST),
20+
Name: "guest",
21+
Description: "Guest",
22+
PermissionScopes: []model.PermissionEntry{
23+
{Path: "/", Permission: 0},
24+
},
25+
}
26+
if err := op.CreateRole(guestRole); err != nil {
27+
utils.Log.Fatalf("[init role] Failed to create guest role: %v", err)
28+
}
29+
} else {
30+
utils.Log.Fatalf("[init role] Failed to get guest role: %v", err)
31+
}
32+
}
33+
34+
_, err = op.GetRoleByName("admin")
35+
if err != nil {
36+
if errors.Is(err, gorm.ErrRecordNotFound) {
37+
adminRole := &model.Role{
38+
ID: uint(model.ADMIN),
39+
Name: "admin",
40+
Description: "Administrator",
41+
PermissionScopes: []model.PermissionEntry{
42+
{Path: "/", Permission: 0xFFFF},
43+
},
44+
}
45+
if err := op.CreateRole(adminRole); err != nil {
46+
utils.Log.Fatalf("[init role] Failed to create admin role: %v", err)
47+
}
48+
} else {
49+
utils.Log.Fatalf("[init role] Failed to get admin role: %v", err)
50+
}
51+
}
52+
}

internal/bootstrap/data/user.go

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package data
22

33
import (
4+
"github.com/alist-org/alist/v3/internal/db"
45
"os"
56

67
"github.com/alist-org/alist/v3/cmd/flags"
7-
"github.com/alist-org/alist/v3/internal/db"
88
"github.com/alist-org/alist/v3/internal/model"
99
"github.com/alist-org/alist/v3/internal/op"
1010
"github.com/alist-org/alist/v3/pkg/utils"
@@ -14,6 +14,28 @@ import (
1414
)
1515

1616
func initUser() {
17+
guest, err := op.GetGuest()
18+
if err != nil {
19+
if errors.Is(err, gorm.ErrRecordNotFound) {
20+
salt := random.String(16)
21+
guestRole, _ := op.GetRoleByName("guest")
22+
guest = &model.User{
23+
Username: "guest",
24+
PwdHash: model.TwoHashPwd("guest", salt),
25+
Salt: salt,
26+
Role: model.Roles{int(guestRole.ID)},
27+
BasePath: "/",
28+
Permission: 0,
29+
Disabled: true,
30+
Authn: "[]",
31+
}
32+
if err := db.CreateUser(guest); err != nil {
33+
utils.Log.Fatalf("[init user] Failed to create guest user: %v", err)
34+
}
35+
} else {
36+
utils.Log.Fatalf("[init user] Failed to get guest user: %v", err)
37+
}
38+
}
1739
admin, err := op.GetAdmin()
1840
adminPassword := random.String(8)
1941
envpass := os.Getenv("ALIST_ADMIN_PASSWORD")
@@ -25,15 +47,16 @@ func initUser() {
2547
if err != nil {
2648
if errors.Is(err, gorm.ErrRecordNotFound) {
2749
salt := random.String(16)
50+
adminRole, _ := op.GetRoleByName("admin")
2851
admin = &model.User{
2952
Username: "admin",
3053
Salt: salt,
3154
PwdHash: model.TwoHashPwd(adminPassword, salt),
32-
Role: model.ADMIN,
55+
Role: model.Roles{int(adminRole.ID)},
3356
BasePath: "/",
3457
Authn: "[]",
3558
// 0(can see hidden) - 7(can remove) & 12(can read archives) - 13(can decompress archives)
36-
Permission: 0x30FF,
59+
Permission: 0xFFFF,
3760
}
3861
if err := op.CreateUser(admin); err != nil {
3962
panic(err)
@@ -44,25 +67,4 @@ func initUser() {
4467
utils.Log.Fatalf("[init user] Failed to get admin user: %v", err)
4568
}
4669
}
47-
guest, err := op.GetGuest()
48-
if err != nil {
49-
if errors.Is(err, gorm.ErrRecordNotFound) {
50-
salt := random.String(16)
51-
guest = &model.User{
52-
Username: "guest",
53-
PwdHash: model.TwoHashPwd("guest", salt),
54-
Salt: salt,
55-
Role: model.GUEST,
56-
BasePath: "/",
57-
Permission: 0,
58-
Disabled: true,
59-
Authn: "[]",
60-
}
61-
if err := db.CreateUser(guest); err != nil {
62-
utils.Log.Fatalf("[init user] Failed to create guest user: %v", err)
63-
}
64-
} else {
65-
utils.Log.Fatalf("[init user] Failed to get guest user: %v", err)
66-
}
67-
}
6870
}

internal/bootstrap/patch/all.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_24_0"
55
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_32_0"
66
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_41_0"
7+
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0"
78
)
89

910
type VersionPatches struct {
@@ -32,4 +33,10 @@ var UpgradePatches = []VersionPatches{
3233
v3_41_0.GrantAdminPermissions,
3334
},
3435
},
36+
{
37+
Version: "v3.46.0",
38+
Patches: []func(){
39+
v3_46_0.ConvertLegacyRoles,
40+
},
41+
},
3542
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package v3_46_0
2+
3+
import (
4+
"errors"
5+
"github.com/alist-org/alist/v3/internal/db"
6+
"github.com/alist-org/alist/v3/internal/model"
7+
"github.com/alist-org/alist/v3/internal/op"
8+
"github.com/alist-org/alist/v3/pkg/utils"
9+
"gorm.io/gorm"
10+
)
11+
12+
// ConvertLegacyRoles migrates old integer role values to a new role model with permission scopes.
13+
func ConvertLegacyRoles() {
14+
guestRole, err := op.GetRoleByName("guest")
15+
if err != nil {
16+
if errors.Is(err, gorm.ErrRecordNotFound) {
17+
guestRole = &model.Role{
18+
ID: uint(model.GUEST),
19+
Name: "guest",
20+
Description: "Guest",
21+
PermissionScopes: []model.PermissionEntry{
22+
{
23+
Path: "/",
24+
Permission: 0,
25+
},
26+
},
27+
}
28+
if err = op.CreateRole(guestRole); err != nil {
29+
utils.Log.Errorf("[convert roles] failed to create guest role: %v", err)
30+
return
31+
}
32+
} else {
33+
utils.Log.Errorf("[convert roles] failed to get guest role: %v", err)
34+
return
35+
}
36+
}
37+
38+
adminRole, err := op.GetRoleByName("admin")
39+
if err != nil {
40+
if errors.Is(err, gorm.ErrRecordNotFound) {
41+
adminRole = &model.Role{
42+
ID: uint(model.ADMIN),
43+
Name: "admin",
44+
Description: "Administrator",
45+
PermissionScopes: []model.PermissionEntry{
46+
{
47+
Path: "/",
48+
Permission: 0x33FF,
49+
},
50+
},
51+
}
52+
if err = op.CreateRole(adminRole); err != nil {
53+
utils.Log.Errorf("[convert roles] failed to create admin role: %v", err)
54+
return
55+
}
56+
} else {
57+
utils.Log.Errorf("[convert roles] failed to get admin role: %v", err)
58+
return
59+
}
60+
}
61+
62+
generalRole, err := op.GetRoleByName("general")
63+
if err != nil {
64+
if errors.Is(err, gorm.ErrRecordNotFound) {
65+
generalRole = &model.Role{
66+
ID: uint(model.NEWGENERAL),
67+
Name: "general",
68+
Description: "General User",
69+
PermissionScopes: []model.PermissionEntry{
70+
{
71+
Path: "/",
72+
Permission: 0,
73+
},
74+
},
75+
}
76+
if err = op.CreateRole(generalRole); err != nil {
77+
utils.Log.Errorf("[convert roles] failed create general role: %v", err)
78+
return
79+
}
80+
} else {
81+
utils.Log.Errorf("[convert roles] failed get general role: %v", err)
82+
return
83+
}
84+
}
85+
86+
users, _, err := op.GetUsers(1, -1)
87+
if err != nil {
88+
utils.Log.Errorf("[convert roles] failed to get users: %v", err)
89+
return
90+
}
91+
92+
for i := range users {
93+
user := users[i]
94+
if user.Role == nil {
95+
continue
96+
}
97+
changed := false
98+
var roles model.Roles
99+
for _, r := range user.Role {
100+
switch r {
101+
case model.ADMIN:
102+
roles = append(roles, int(adminRole.ID))
103+
if int(adminRole.ID) != r {
104+
changed = true
105+
}
106+
case model.GUEST:
107+
roles = append(roles, int(guestRole.ID))
108+
if int(guestRole.ID) != r {
109+
changed = true
110+
}
111+
case model.GENERAL:
112+
roles = append(roles, int(generalRole.ID))
113+
if int(generalRole.ID) != r {
114+
changed = true
115+
}
116+
default:
117+
roles = append(roles, r)
118+
}
119+
}
120+
if changed {
121+
user.Role = roles
122+
if err := db.UpdateUser(&user); err != nil {
123+
utils.Log.Errorf("[convert roles] failed to update user %s: %v", user.Username, err)
124+
}
125+
}
126+
}
127+
128+
utils.Log.Infof("[convert roles] completed role conversion for %d users", len(users))
129+
}

internal/db/db.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ var db *gorm.DB
1212

1313
func Init(d *gorm.DB) {
1414
db = d
15-
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey))
15+
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.Role), new(model.Label), new(model.LabelFileBinDing), new(model.ObjFile))
1616
if err != nil {
1717
log.Fatalf("failed migrate database: %s", err.Error())
1818
}

0 commit comments

Comments
 (0)