Smail/backend/controllers/account_group.go
smail b7a1e44dc6
Some checks are pending
Security Checks / security (push) Waiting to run
修复管理员成员分组权限检查点
提交分组成员管理权限和前端入口调整,作为继续开发前的安全检查点。
2026-10-02 03:48:27 +08:00

157 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
if err := accountgroup.SetMembership(
db.Instance,
user,
input.GroupIDs,
accountWriteGuard(ctx, user),
accountgroup.MembershipScopeGuard(ctx.UserID, ctx.EffectiveRole(), input.GroupIDs),
); err != nil {
writeAccountGroupError(w, err)
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, "分组操作失败,请检查是否重名并刷新后重试")
}
}