Some checks are pending
Security Checks / security (push) Waiting to run
修复管理员用户列表成员版本缺失导致的多分组保存失败,并保留管理员不可管理分组的成员关系,补充回归测试。
170 lines
5.4 KiB
Go
170 lines
5.4 KiB
Go
package controllers
|
||
|
||
import (
|
||
"errors"
|
||
"net/http"
|
||
|
||
"git.pcux.cn/xuans/Smail/db"
|
||
"git.pcux.cn/xuans/Smail/dto/response"
|
||
"git.pcux.cn/xuans/Smail/models"
|
||
"git.pcux.cn/xuans/Smail/services/access"
|
||
"git.pcux.cn/xuans/Smail/services/accountgroup"
|
||
"git.pcux.cn/xuans/Smail/utils/context"
|
||
)
|
||
|
||
func accountWriteGuard(ctx *context.Context, target models.User) accountgroup.WriteGuard {
|
||
if ctx == nil {
|
||
return nil
|
||
}
|
||
return accountgroup.Guard(ctx.UserID, ctx.EffectiveRole(), ctx.SessionVersion, ctx.SecurityVersion, target)
|
||
}
|
||
|
||
func requireAccountScope(ctx *context.Context, w http.ResponseWriter, targetID int) bool {
|
||
if ctx == nil || !accountgroup.CanAccess(db.Instance, ctx.UserID, ctx.EffectiveRole(), targetID) {
|
||
writeSecurityError(w, response.NoAccessPrivileges, "无权查看或管理该账号")
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func GetAccountGroups(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||
if ctx == nil {
|
||
writeSecurityError(w, response.NeedLogin, "请先登录")
|
||
return
|
||
}
|
||
groups, err := accountgroup.VisibleGroups(db.Instance, ctx.UserID, ctx.EffectiveRole())
|
||
if err != nil {
|
||
writeAccountGroupError(w, err)
|
||
return
|
||
}
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
response.NewSuccessResponse(map[string]any{"groups": groups, "can_manage": ctx.EffectiveRole() == access.RoleSuperAdmin}).FPrint(w)
|
||
}
|
||
|
||
func requireAccountGroupManagement(ctx *context.Context, w http.ResponseWriter) bool {
|
||
if err := authorizeCapability(ctx, access.CapabilityAccountGroupManage, "", false); err != nil {
|
||
writeUserAuthorizationError(w, err, "无权管理账号分组")
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func requireAccountGroupMembershipManagement(ctx *context.Context, w http.ResponseWriter) bool {
|
||
if err := authorizeCapability(ctx, access.CapabilityAccountGroupMemberManage, "", false); err != nil {
|
||
writeUserAuthorizationError(w, err, "无权管理账号所属分组")
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func SaveAccountGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||
if !requireAccountGroupManagement(ctx, w) {
|
||
return
|
||
}
|
||
var input struct {
|
||
ID int `json:"id"`
|
||
Name string `json:"name"`
|
||
}
|
||
if decodeUserRequest(w, req, &input) != nil || input.ID < 0 {
|
||
writeAccountGroupError(w, accountgroup.ErrInvalid)
|
||
return
|
||
}
|
||
name, err := accountgroup.NormalizeName(input.Name)
|
||
if err != nil {
|
||
writeAccountGroupError(w, err)
|
||
return
|
||
}
|
||
if db.Instance == nil {
|
||
writeSecurityError(w, response.ServerError, "数据库不可用")
|
||
return
|
||
}
|
||
item := models.AccountGroup{ID: input.ID, Name: name}
|
||
if item.ID == 0 {
|
||
_, err = db.Instance.Insert(&item)
|
||
} else {
|
||
var found bool
|
||
found, err = db.Instance.ID(item.ID).Exist(&models.AccountGroup{})
|
||
if err == nil && !found {
|
||
err = accountgroup.ErrInvalid
|
||
}
|
||
if err == nil {
|
||
_, err = db.Instance.ID(item.ID).Cols("name").Update(&item)
|
||
}
|
||
}
|
||
if err != nil {
|
||
writeAccountGroupError(w, err)
|
||
return
|
||
}
|
||
response.NewSuccessResponse(item).FPrint(w)
|
||
}
|
||
|
||
func SetAccountGroupMembership(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||
if !requireAccountGroupMembershipManagement(ctx, w) {
|
||
return
|
||
}
|
||
var input struct {
|
||
UserID int `json:"user_id"`
|
||
GroupIDs []int `json:"group_ids"`
|
||
Version int `json:"version"`
|
||
}
|
||
if decodeUserRequest(w, req, &input) != nil || input.UserID <= 0 || input.Version < 1 || input.GroupIDs == nil {
|
||
writeAccountGroupError(w, accountgroup.ErrInvalid)
|
||
return
|
||
}
|
||
if db.Instance == nil {
|
||
writeSecurityError(w, response.ServerError, "数据库不可用")
|
||
return
|
||
}
|
||
var user models.User
|
||
found, err := db.Instance.ID(input.UserID).Get(&user)
|
||
if err != nil || !found {
|
||
writeAccountGroupError(w, accountgroup.ErrInvalid)
|
||
return
|
||
}
|
||
if input.Version != user.SecurityVersion {
|
||
writeAccountGroupError(w, accountgroup.ErrConflict)
|
||
return
|
||
}
|
||
guards := []accountgroup.WriteGuard{
|
||
accountWriteGuard(ctx, user),
|
||
accountgroup.MembershipScopeGuard(ctx.UserID, ctx.EffectiveRole(), input.GroupIDs),
|
||
}
|
||
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)
|
||
response.NewSuccessResponse(map[string]any{
|
||
"user_id": user.ID,
|
||
"membership_version": user.SecurityVersion + 1,
|
||
}).FPrint(w)
|
||
}
|
||
|
||
func writeAccountGroupError(w http.ResponseWriter, err error) {
|
||
switch {
|
||
case errors.Is(err, accountgroup.ErrDenied):
|
||
writeSecurityError(w, response.NoAccessPrivileges, "无权查看或管理该分组")
|
||
case errors.Is(err, accountgroup.ErrInvalid):
|
||
writeSecurityError(w, response.ParamsError, "分组参数无效:名称为 1–32 个字符,普通用户只能属于一个分组")
|
||
case errors.Is(err, accountgroup.ErrConflict):
|
||
writeSecurityError(w, response.ParamsError, "账号状态已变化,请刷新列表后重试")
|
||
default:
|
||
writeSecurityError(w, response.ServerError, "分组操作失败,请检查是否重名并刷新后重试")
|
||
}
|
||
}
|