From acdf2988117b08e622c8ea9031a9ec201a54f8b6 Mon Sep 17 00:00:00 2001 From: LanQin_ Date: Mon, 22 Jun 2026 14:42:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(permission):=20=E8=B0=83=E6=95=B4=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E6=9D=83=E9=99=90=E7=BB=84=E4=B8=8E=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=9D=83=E9=99=90=E7=BB=A7=E6=89=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将“普通用户”权限组纳入默认权限继承,创建和更新用户时自动合并其权限。 - 放宽“普通用户”系统组的可编辑性,但仍禁止删除;继续限制超级管理员组被修改。 - 更新管理端权限组操作状态,避免对不可删除或不可编辑项提供错误入口。 - 补充相关测试,覆盖默认权限组编辑、删除与用户权限继承场景。 --- apps/api/internal/app/admin_handlers.go | 2 +- apps/api/internal/app/app_test.go | 40 ++++++++++- .../internal/app/permission_group_handlers.go | 4 +- apps/api/internal/app/permissions.go | 71 ++++++++++++++++--- apps/web/src/pages/admin.tsx | 6 +- 5 files changed, 106 insertions(+), 17 deletions(-) diff --git a/apps/api/internal/app/admin_handlers.go b/apps/api/internal/app/admin_handlers.go index 3a8d853..3835045 100644 --- a/apps/api/internal/app/admin_handlers.go +++ b/apps/api/internal/app/admin_handlers.go @@ -235,7 +235,7 @@ func (a *App) handleUpdateUser(w http.ResponseWriter, r *http.Request) { if role == "admin" { next.Permissions = allPermissionKeys() } else { - permissions, err := a.permissionsForGroupIDs(r.Context(), nil, permissionGroupIDs) + permissions, err := a.effectivePermissionsForUserGroups(r.Context(), nil, permissionGroupIDs) if err != nil { badRequest(w, err) return diff --git a/apps/api/internal/app/app_test.go b/apps/api/internal/app/app_test.go index ae90163..afd3df5 100644 --- a/apps/api/internal/app/app_test.go +++ b/apps/api/internal/app/app_test.go @@ -938,9 +938,23 @@ func TestFixedRolesProtectAdminRoutesAndDefaultAdmin(t *testing.T) { }, &errBody); code != http.StatusForbidden { t.Fatalf("system permission group update should be forbidden code=%d body=%v", code, errBody) } + var regularGroup PermissionGroup + if code := admin.do("POST", "/api/admin/permission-groups/"+PermissionGroupRegular, map[string]any{ + "name": "普通用户", + "description": "Default permissions for regular users", + "permissions": []string{PermissionAdminOverview}, + }, ®ularGroup); code != http.StatusOK { + t.Fatalf("regular user group should be editable code=%d group=%+v", code, regularGroup) + } + if !regularGroup.System || !userHasPermission(&User{Role: "user", Permissions: regularGroup.Permissions}, PermissionAdminOverview) { + t.Fatalf("regular group update did not persist permissions=%+v", regularGroup) + } if code := admin.do("DELETE", "/api/admin/permission-groups/"+PermissionGroupSuperAdmin, nil, &errBody); code != http.StatusForbidden { t.Fatalf("system permission group delete should be forbidden code=%d body=%v", code, errBody) } + if code := admin.do("DELETE", "/api/admin/permission-groups/"+PermissionGroupRegular, nil, &errBody); code != http.StatusForbidden { + t.Fatalf("regular user group delete should be forbidden code=%d body=%v", code, errBody) + } if code := admin.do("POST", "/api/admin/users", map[string]any{ "email": "invalid-group@lanqin.local", "displayName": "Invalid Group", @@ -997,10 +1011,25 @@ func TestFixedRolesProtectAdminRoutesAndDefaultAdmin(t *testing.T) { }, &mailboxUser); code != http.StatusCreated { t.Fatalf("create mailbox admin user code=%d user=%+v", code, mailboxUser) } - if mailboxUser.Role != "user" || len(mailboxUser.PermissionGroupIDs) != 1 || mailboxUser.PermissionGroupIDs[0] != mailboxAdminGroup.ID || !userHasPermission(&mailboxUser.User, PermissionMailboxesManage) || userHasPermission(&mailboxUser.User, PermissionSystemSettings) { + if mailboxUser.Role != "user" || !containsString(mailboxUser.PermissionGroupIDs, PermissionGroupRegular) || !containsString(mailboxUser.PermissionGroupIDs, mailboxAdminGroup.ID) || !userHasPermission(&mailboxUser.User, PermissionMailboxesManage) || userHasPermission(&mailboxUser.User, PermissionSystemSettings) { t.Fatalf("mailbox admin authorization=%+v", mailboxUser.User) } + var plainUser AdminUser + if code := admin.do("POST", "/api/admin/users", map[string]any{ + "email": "plain-user@lanqin.local", + "displayName": "Plain User", + "role": "user", + "password": "Password123!", + "disabled": false, + "permissionGroupIds": []string{}, + }, &plainUser); code != http.StatusCreated { + t.Fatalf("create plain user code=%d user=%+v", code, plainUser) + } + if len(plainUser.PermissionGroupIDs) != 1 || plainUser.PermissionGroupIDs[0] != PermissionGroupRegular || !userHasPermission(&plainUser.User, PermissionAdminOverview) { + t.Fatalf("plain user should inherit regular permissions: %+v", plainUser.User) + } + var customUser AdminUser if code := admin.do("POST", "/api/admin/users", map[string]any{ "email": "mailbox-viewer@lanqin.local", @@ -1308,3 +1337,12 @@ func mustDefaultDomainID(t *testing.T, a *App) string { } return id } + +func containsString(items []string, needle string) bool { + for _, item := range items { + if item == needle { + return true + } + } + return false +} diff --git a/apps/api/internal/app/permission_group_handlers.go b/apps/api/internal/app/permission_group_handlers.go index 504eeaf..55f608b 100644 --- a/apps/api/internal/app/permission_group_handlers.go +++ b/apps/api/internal/app/permission_group_handlers.go @@ -127,8 +127,8 @@ func (a *App) handleUpdatePermissionGroup(w http.ResponseWriter, r *http.Request respondError(w, http.StatusNotFound, "permission group not found") return } - if intBool(existingSystem) { - respondError(w, http.StatusForbidden, "system permission groups cannot be edited") + if id == PermissionGroupSuperAdmin { + respondError(w, http.StatusForbidden, "super administrator group cannot be edited") return } var req struct { diff --git a/apps/api/internal/app/permissions.go b/apps/api/internal/app/permissions.go index ed62904..31aa178 100644 --- a/apps/api/internal/app/permissions.go +++ b/apps/api/internal/app/permissions.go @@ -312,10 +312,15 @@ func (a *App) ensureDefaultPermissionGroups(ctx context.Context) error { if _, err := a.db.ExecContext(ctx, `UPDATE permission_groups SET name=name || ' (' || id || ')' WHERE name=? AND id<>?`, item.Name, item.ID); err != nil { return err } - if _, err := a.db.ExecContext(ctx, `INSERT INTO permission_groups(id,name,description,permissions_json,system,created_at,updated_at) + query := `INSERT INTO permission_groups(id,name,description,permissions_json,system,created_at,updated_at) VALUES(?,?,?,?,?,?,?) - ON CONFLICT(id) DO UPDATE SET name=excluded.name, description=excluded.description, permissions_json=excluded.permissions_json, system=excluded.system, updated_at=excluded.updated_at`, - item.ID, item.Name, item.Description, encodePermissions(item.Permissions), boolInt(item.System), now, now); err != nil { + ON CONFLICT(id) DO UPDATE SET name=excluded.name, description=excluded.description, permissions_json=excluded.permissions_json, system=excluded.system, updated_at=excluded.updated_at` + if item.ID == PermissionGroupRegular { + query = `INSERT INTO permission_groups(id,name,description,permissions_json,system,created_at,updated_at) + VALUES(?,?,?,?,?,?,?) + ON CONFLICT(id) DO UPDATE SET name=excluded.name, description=excluded.description, system=excluded.system, updated_at=excluded.updated_at` + } + if _, err := a.db.ExecContext(ctx, query, item.ID, item.Name, item.Description, encodePermissions(item.Permissions), boolInt(item.System), now, now); err != nil { return err } } @@ -367,6 +372,10 @@ func (a *App) permissionsForUser(ctx context.Context, userID, role string) ([]st if role == "admin" { return allPermissionKeys(), nil } + seen := map[string]bool{} + if err := a.addRegularGroupPermissions(ctx, nil, seen); err != nil { + return nil, err + } rows, err := a.db.QueryContext(ctx, `SELECT pg.id,pg.permissions_json FROM permission_groups pg JOIN user_permission_groups upg ON upg.group_id=pg.id @@ -375,7 +384,6 @@ func (a *App) permissionsForUser(ctx context.Context, userID, role string) ([]st return nil, err } defer rows.Close() - seen := map[string]bool{} for rows.Next() { var groupID, raw string if err := rows.Scan(&groupID, &raw); err != nil { @@ -400,11 +408,58 @@ func (a *App) permissionsForUser(ctx context.Context, userID, role string) ([]st return out, nil } +func (a *App) addRegularGroupPermissions(ctx context.Context, tx *sql.Tx, seen map[string]bool) error { + var raw string + query := `SELECT permissions_json FROM permission_groups WHERE id=?` + var err error + if tx != nil { + err = tx.QueryRowContext(ctx, query, PermissionGroupRegular).Scan(&raw) + } else { + err = a.db.QueryRowContext(ctx, query, PermissionGroupRegular).Scan(&raw) + } + if errors.Is(err, sql.ErrNoRows) { + return nil + } + if err != nil { + return err + } + for _, permission := range decodeStoredPermissions(raw) { + seen[permission] = true + } + return nil +} + +func (a *App) effectivePermissionsForUserGroups(ctx context.Context, tx *sql.Tx, groupIDs []string) ([]string, error) { + seen := map[string]bool{} + if err := a.addRegularGroupPermissions(ctx, tx, seen); err != nil { + return nil, err + } + groupPermissions, err := a.permissionsForGroupIDs(ctx, tx, groupIDs) + if err != nil { + return nil, err + } + for _, permission := range groupPermissions { + seen[permission] = true + } + out := make([]string, 0, len(seen)) + for _, permission := range allPermissionKeys() { + if seen[permission] { + out = append(out, permission) + } + } + return out, nil +} + func (a *App) permissionGroupsForUser(ctx context.Context, userID, role string) ([]string, []PermissionGroupSummary, error) { if role == "admin" { group := PermissionGroupSummary{ID: PermissionGroupSuperAdmin, Name: "超级管理员"} return []string{group.ID}, []PermissionGroupSummary{group}, nil } + ids := []string{PermissionGroupRegular} + groups := []PermissionGroupSummary{{ID: PermissionGroupRegular, Name: "普通用户"}} + if err := a.db.QueryRowContext(ctx, `SELECT name FROM permission_groups WHERE id=?`, PermissionGroupRegular).Scan(&groups[0].Name); err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, nil, err + } rows, err := a.db.QueryContext(ctx, `SELECT pg.id,pg.name FROM permission_groups pg JOIN user_permission_groups upg ON upg.group_id=pg.id @@ -414,8 +469,6 @@ func (a *App) permissionGroupsForUser(ctx context.Context, userID, role string) } defer rows.Close() order := permissionGroupOrder() - ids := []string{} - groups := []PermissionGroupSummary{} for rows.Next() { var group PermissionGroupSummary if err := rows.Scan(&group.ID, &group.Name); err != nil { @@ -452,10 +505,6 @@ func (a *App) permissionGroupsForUser(ctx context.Context, userID, role string) } return ids[i] < ids[j] }) - if len(groups) == 0 { - group := PermissionGroupSummary{ID: PermissionGroupRegular, Name: "普通用户"} - return []string{group.ID}, []PermissionGroupSummary{group}, nil - } return ids, groups, nil } @@ -582,7 +631,7 @@ func (a *App) setUserPermissionGroups(ctx context.Context, tx *sql.Tx, userID st return fmt.Errorf("permission group not assignable: %s", groupID) } } - groupPermissions, err := a.permissionsForGroupIDs(ctx, tx, groupIDs) + groupPermissions, err := a.effectivePermissionsForUserGroups(ctx, tx, groupIDs) if err != nil { return err } diff --git a/apps/web/src/pages/admin.tsx b/apps/web/src/pages/admin.tsx index 06f8fd2..edca358 100644 --- a/apps/web/src/pages/admin.tsx +++ b/apps/web/src/pages/admin.tsx @@ -305,6 +305,8 @@ function PermissionGroupsSection({ groups, catalog }: { groups: PermissionGroup[ if (!keyword) return true return [group.name, group.description, ...group.permissions].some((value) => value.toLowerCase().includes(keyword)) }) + const isEditable = (group: PermissionGroup) => group.id !== "pg_super_admin" + const isDeletable = (group: PermissionGroup) => !group.system && group.userCount === 0 return ( @@ -334,11 +336,11 @@ function PermissionGroupsSection({ groups, catalog }: { groups: PermissionGroup[ {(canUpdate || canDelete) && - setEditing(group)}>编辑权限组 + setEditing(group)}>编辑权限组 0 || !canDelete} + disabled={!isDeletable(group) || !canDelete} onSelect={() => setPendingConfirm({ title: "删除权限组?", description: `${group.name} 删除后不能再分配给用户。`, confirmText: "删除权限组", onConfirm: () => remove.mutate(group.id) })} > 删除权限组