修复管理员分组版本校验和受限保存
Some checks are pending
Security Checks / security (push) Waiting to run

修复管理员用户列表成员版本缺失导致的多分组保存失败,并保留管理员不可管理分组的成员关系,补充回归测试。
This commit is contained in:
smail 2026-10-02 06:26:18 +08:00
parent 342d0ab0a5
commit 4cf1de87d3
4 changed files with 103 additions and 9 deletions

View File

@ -125,14 +125,27 @@ func SetAccountGroupMembership(ctx *context.Context, w http.ResponseWriter, req
writeAccountGroupError(w, accountgroup.ErrConflict)
return
}
if err := accountgroup.SetMembership(
db.Instance,
user,
input.GroupIDs,
guards := []accountgroup.WriteGuard{
accountWriteGuard(ctx, user),
accountgroup.MembershipScopeGuard(ctx.UserID, ctx.EffectiveRole(), input.GroupIDs),
); err != nil {
writeAccountGroupError(w, err)
}
var setErr error
if ctx.EffectiveRole() == access.RoleAdmin {
visibleGroups, visibleErr := accountgroup.VisibleGroups(db.Instance, ctx.UserID, ctx.EffectiveRole())
if visibleErr != nil {
writeAccountGroupError(w, visibleErr)
return
}
editableGroupIDs := make([]int, 0, len(visibleGroups))
for _, group := range visibleGroups {
editableGroupIDs = append(editableGroupIDs, group.ID)
}
setErr = accountgroup.SetScopedMembership(db.Instance, user, input.GroupIDs, editableGroupIDs, guards...)
} else {
setErr = accountgroup.SetMembership(db.Instance, user, input.GroupIDs, guards...)
}
if setErr != nil {
writeAccountGroupError(w, setErr)
return
}
InvalidateSecurityRuntimeUser(user.ID, user.Account)

View File

@ -273,7 +273,9 @@ func UserList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
}
items := make([]userListItem, 0, len(users))
memberships := map[int][]int{}
if ctx.EffectiveRole() == access.RoleSuperAdmin {
groupManager := ctx.EffectiveRole() == access.RoleSuperAdmin || ctx.EffectiveRole() == access.RoleAdmin
visibleMemberships := map[int]struct{}{}
if groupManager {
ids := make([]int, 0, len(users))
for _, user := range users {
ids = append(ids, user.ID)
@ -283,10 +285,30 @@ func UserList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
writeAccountGroupError(w, err)
return
}
if ctx.EffectiveRole() == access.RoleAdmin {
visibleGroups, visibleErr := accountgroup.VisibleGroups(db.Instance, ctx.UserID, ctx.EffectiveRole())
if visibleErr != nil {
writeAccountGroupError(w, visibleErr)
return
}
for _, group := range visibleGroups {
visibleMemberships[group.ID] = struct{}{}
}
}
}
for _, user := range users {
groupIDs := memberships[user.ID]
if ctx.EffectiveRole() == access.RoleAdmin {
filtered := make([]int, 0, len(groupIDs))
for _, groupID := range groupIDs {
if _, ok := visibleMemberships[groupID]; ok {
filtered = append(filtered, groupID)
}
}
groupIDs = filtered
}
items = append(items, userListItem{
GroupIDs: memberships[user.ID],
GroupIDs: groupIDs,
ID: user.ID,
Account: user.Account,
Name: user.Name,
@ -298,7 +320,7 @@ func UserList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
ExternalEmailEnabled: user.ExternalEmailEnabled == 1,
ExternalEmailConfigured: externalEmailConfigured[user.ID],
})
if ctx.EffectiveRole() == access.RoleSuperAdmin {
if groupManager {
items[len(items)-1].MembershipVersion = user.SecurityVersion
}
}

View File

@ -296,6 +296,15 @@ func Guard(actorID int, role access.Role, sessionVersion, securityVersion int, t
}
func SetMembership(engine *xorm.Engine, user models.User, input []int, guards ...WriteGuard) error {
return setMembership(engine, user, input, nil, guards...)
}
// SetScopedMembership 只替换可管理分组,保留目标账号在其他分组中的成员关系。
func SetScopedMembership(engine *xorm.Engine, user models.User, input []int, editableGroupIDs []int, guards ...WriteGuard) error {
return setMembership(engine, user, input, editableGroupIDs, guards...)
}
func setMembership(engine *xorm.Engine, user models.User, input []int, editableGroupIDs []int, guards ...WriteGuard) error {
if engine == nil || user.ID <= 0 {
return ErrDenied
}
@ -323,6 +332,24 @@ func SetMembership(engine *xorm.Engine, user models.User, input []int, guards ..
if err != nil {
return err
}
if editableGroupIDs != nil {
editable := make(map[int]struct{}, len(editableGroupIDs))
for _, groupID := range editableGroupIDs {
editable[groupID] = struct{}{}
}
existing := make([]models.AccountGroupMember, 0)
if err := tx.Where(`user_id = ?`, user.ID).Find(&existing); err != nil {
return err
}
preserved := make([]int, 0, len(existing)+len(ids))
for _, row := range existing {
if _, ok := editable[row.GroupID]; !ok {
preserved = append(preserved, row.GroupID)
}
}
ids = append(preserved, ids...)
sort.Ints(ids)
}
if err := ReplaceMembership(tx, user.ID, ids); err != nil {
return err
}

View File

@ -135,3 +135,35 @@ func TestValidateMembershipKeepsAdminMultiGroupAndUserSingleGroupRules(t *testin
t.Fatalf("普通用户不应允许多个分组,得到:%v", err)
}
}
func TestSetScopedMembershipPreservesGroupsOutsideAdminScope(t *testing.T) {
engine, err := xorm.NewEngine(`sqlite`, `:memory:`)
if err != nil {
t.Fatal(err)
}
defer engine.Close()
if err := engine.Sync2(new(models.User), new(models.AccountGroup), new(models.AccountGroupMember)); err != nil {
t.Fatal(err)
}
target := models.User{Account: `target`, Name: `Target`, Role: string(access.RoleUser), SessionVersion: 1, SecurityVersion: 1}
if _, err := engine.Insert(&target); err != nil {
t.Fatal(err)
}
if _, err := engine.Insert(&models.AccountGroup{ID: 1, Name: `visible`}, &models.AccountGroup{ID: 2, Name: `hidden`}); err != nil {
t.Fatal(err)
}
if _, err := engine.Insert(&models.AccountGroupMember{UserID: target.ID, GroupID: 1}, &models.AccountGroupMember{UserID: target.ID, GroupID: 2}); err != nil {
t.Fatal(err)
}
allow := func(*xorm.Session) error { return nil }
if err := SetScopedMembership(engine, target, []int{0}, []int{0, 1}, allow); err != nil {
t.Fatal(err)
}
rows := make([]models.AccountGroupMember, 0)
if err := engine.Where(`user_id = ?`, target.ID).Asc(`group_id`).Find(&rows); err != nil {
t.Fatal(err)
}
if len(rows) != 2 || rows[0].GroupID != 0 || rows[1].GroupID != 2 {
t.Fatalf(`scoped membership changed an inaccessible group: %+v`, rows)
}
}