Some checks are pending
Security Checks / security (push) Waiting to run
修复管理员用户列表成员版本缺失导致的多分组保存失败,并保留管理员不可管理分组的成员关系,补充回归测试。
604 lines
21 KiB
Go
604 lines
21 KiB
Go
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"git.pcux.cn/xuans/Smail/config"
|
|
"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/services/auth"
|
|
"git.pcux.cn/xuans/Smail/session"
|
|
"git.pcux.cn/xuans/Smail/utils/array"
|
|
"git.pcux.cn/xuans/Smail/utils/context"
|
|
"git.pcux.cn/xuans/Smail/utils/csrf"
|
|
"git.pcux.cn/xuans/Smail/utils/password"
|
|
"git.pcux.cn/xuans/Smail/utils/validation"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cast"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const maxUserRequestBytes int64 = 64 << 10
|
|
|
|
func decodeUserRequest(w http.ResponseWriter, req *http.Request, dst any) error {
|
|
req.Body = http.MaxBytesReader(w, req.Body, maxUserRequestBytes)
|
|
decoder := json.NewDecoder(req.Body)
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(dst); err != nil {
|
|
return err
|
|
}
|
|
var trailing any
|
|
if err := decoder.Decode(&trailing); err != io.EOF {
|
|
if err == nil {
|
|
return errors.New("multiple json values")
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type userCreateRequest struct {
|
|
GroupIDs *[]int `json:"group_ids"`
|
|
Id int `json:"id"`
|
|
Account string `json:"account"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
Disabled int `json:"disabled"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
type userEditRequest struct {
|
|
userCreateRequest
|
|
GroupIDs *[]int `json:"group_ids"`
|
|
MembershipVersion int `json:"membership_version"`
|
|
}
|
|
|
|
func CreateUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
|
targetRole := access.RoleUser
|
|
var reqData userCreateRequest
|
|
if err := decodeUserRequest(w, req, &reqData); err != nil {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
if reqData.Role != "" {
|
|
targetRole = access.Role(reqData.Role)
|
|
}
|
|
if err := authorizeCapability(ctx, access.CapabilityUserCreate, targetRole, false); err != nil {
|
|
response.NewErrorResponse(response.NoAccessPrivileges, "无权访问", "").FPrint(w)
|
|
return
|
|
}
|
|
if reqData.Username == "" || reqData.Password == "" || reqData.Account == "" {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
if reqData.Disabled != 0 && reqData.Disabled != 1 {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
|
|
account, err := validation.NormalizeAccount(reqData.Account)
|
|
if err != nil {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
username, err := validation.NormalizeDisplayName(reqData.Username)
|
|
if err != nil {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
user := models.User{Name: username, Account: account, Disabled: reqData.Disabled, SessionVersion: 1, SecurityVersion: 1}
|
|
user.ApplyRole(targetRole)
|
|
groupIDs := []int{0}
|
|
if reqData.GroupIDs != nil {
|
|
groupIDs = *reqData.GroupIDs
|
|
}
|
|
if len(groupIDs) > 0 && (len(groupIDs) != 1 || groupIDs[0] != 0) {
|
|
if !requireAccountGroupMembershipManagement(ctx, w) {
|
|
return
|
|
}
|
|
}
|
|
if err := auth.SetPassword(&user, reqData.Password); err != nil {
|
|
if writePasswordBusy(w, err) {
|
|
return
|
|
}
|
|
if errors.Is(err, password.ErrInvalidLength) {
|
|
response.NewErrorResponse(response.ParamsError, "密码长度必须为 8-12 字节", "").FPrint(w)
|
|
return
|
|
}
|
|
log.WithContext(ctx).WithError(err).Error("password hash failed")
|
|
response.NewErrorResponse(response.ServerError, "密码设置失败", "").FPrint(w)
|
|
return
|
|
}
|
|
if db.Instance == nil {
|
|
writeAccountGroupError(w, accountgroup.ErrDenied)
|
|
return
|
|
}
|
|
tx := db.Instance.NewSession()
|
|
defer tx.Close()
|
|
if err := tx.Begin(); err != nil {
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
actor := models.User{ID: ctx.UserID, SessionVersion: ctx.SessionVersion, SecurityVersion: ctx.SecurityVersion}
|
|
actor.ApplyRole(ctx.EffectiveRole())
|
|
if err := accountgroup.CheckWrite(tx, accountWriteGuard(ctx, actor)); err != nil {
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
if err := accountgroup.CheckWrite(tx, accountgroup.MembershipScopeGuard(ctx.UserID, ctx.EffectiveRole(), groupIDs)); err != nil {
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
groupIDs, err = accountgroup.ValidateMembership(tx, user.EffectiveRole(), groupIDs)
|
|
if err != nil {
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
if _, err := tx.Insert(&user); err != nil {
|
|
log.WithContext(ctx).WithError(err).Error("create user failed")
|
|
response.NewErrorResponse(response.ServerError, "创建用户失败", "").FPrint(w)
|
|
return
|
|
}
|
|
if err := accountgroup.ReplaceMembership(tx, user.ID, groupIDs); err != nil {
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
response.NewSuccessResponse(userListItem{
|
|
ID: user.ID, Account: user.Account, Name: user.Name,
|
|
Disabled: user.Disabled, Role: user.EffectiveRole(), IsAdmin: user.IsAdmin,
|
|
GroupIDs: groupIDs, MembershipVersion: user.SecurityVersion,
|
|
MustChangePassword: user.MustChangePassword,
|
|
ExternalEmailEnabled: user.ExternalEmailEnabled == 1,
|
|
}).FPrint(w)
|
|
}
|
|
|
|
type userListRequest struct {
|
|
UserID int `json:"user_id"`
|
|
GroupID *int `json:"group_id"`
|
|
CurrentPage int `json:"current_page"`
|
|
PageSize int `json:"page_size"`
|
|
}
|
|
|
|
type userListItem struct {
|
|
GroupIDs []int `json:"GroupIDs,omitempty"`
|
|
MembershipVersion int `json:"MembershipVersion,omitempty"`
|
|
ID int `json:"ID"`
|
|
Account string `json:"Account"`
|
|
Name string `json:"Name"`
|
|
Disabled int `json:"Disabled"`
|
|
Role access.Role `json:"Role"`
|
|
IsAdmin int `json:"IsAdmin"`
|
|
MustChangePassword int `json:"must_change_password"`
|
|
RecoveryCodeConfigured bool `json:"RecoveryCodeConfigured"`
|
|
ExternalEmailEnabled bool `json:"ExternalEmailEnabled"`
|
|
ExternalEmailConfigured bool `json:"ExternalEmailConfigured"`
|
|
}
|
|
|
|
func loadUserSecurityStatus(users []models.User) (map[int]bool, map[int]bool, error) {
|
|
recoveryConfigured := make(map[int]bool, len(users))
|
|
externalEmailConfigured := make(map[int]bool, len(users))
|
|
if len(users) == 0 {
|
|
return recoveryConfigured, externalEmailConfigured, nil
|
|
}
|
|
|
|
userIDs := make([]int, 0, len(users))
|
|
for _, user := range users {
|
|
userIDs = append(userIDs, user.ID)
|
|
}
|
|
|
|
var recoveryCodes []models.RecoveryCode
|
|
if err := db.Instance.Cols("user_id").In("user_id", userIDs).Where("used_at IS NULL").Find(&recoveryCodes); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, recoveryCode := range recoveryCodes {
|
|
recoveryConfigured[recoveryCode.UserID] = true
|
|
}
|
|
|
|
var externalEmails []models.ExternalEmail
|
|
if err := db.Instance.Cols("user_id").In("user_id", userIDs).Where("status = ?", models.ExternalEmailStatusVerified).Find(&externalEmails); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, externalEmail := range externalEmails {
|
|
externalEmailConfigured[externalEmail.UserID] = true
|
|
}
|
|
|
|
return recoveryConfigured, externalEmailConfigured, nil
|
|
}
|
|
|
|
func UserList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
|
if err := authorizeCapability(ctx, access.CapabilityUserRead, "", false); err != nil {
|
|
response.NewErrorResponse(response.NoAccessPrivileges, "无权访问", "").FPrint(w)
|
|
return
|
|
}
|
|
var reqData userListRequest
|
|
if err := decodeUserRequest(w, req, &reqData); err != nil {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
if reqData.PageSize <= 0 || reqData.PageSize > 100 {
|
|
reqData.PageSize = 15
|
|
}
|
|
offset := 0
|
|
if reqData.CurrentPage >= 1 {
|
|
offset = (reqData.CurrentPage - 1) * reqData.PageSize
|
|
}
|
|
|
|
var users []models.User
|
|
query := db.Instance.Table(&models.User{})
|
|
defer query.Close()
|
|
accountgroup.ApplyScope(query, ctx.UserID, ctx.EffectiveRole())
|
|
if reqData.UserID != 0 {
|
|
query.And("id = ?", reqData.UserID)
|
|
}
|
|
if reqData.GroupID != nil {
|
|
groups, err := accountgroup.VisibleGroups(db.Instance, ctx.UserID, ctx.EffectiveRole())
|
|
if err != nil {
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
allowed := false
|
|
for _, group := range groups {
|
|
if group.ID == *reqData.GroupID {
|
|
allowed = true
|
|
}
|
|
}
|
|
if !allowed {
|
|
writeAccountGroupError(w, accountgroup.ErrDenied)
|
|
return
|
|
}
|
|
accountgroup.ApplyGroup(query, *reqData.GroupID)
|
|
}
|
|
totalNum, err := query.Asc("id").Limit(reqData.PageSize, offset).FindAndCount(&users)
|
|
if err != nil {
|
|
log.WithContext(ctx).Errorf("SQL Error: %+v", err)
|
|
response.NewErrorResponse(response.ServerError, "查询失败", "").FPrint(w)
|
|
return
|
|
}
|
|
recoveryConfigured, externalEmailConfigured, err := loadUserSecurityStatus(users)
|
|
if err != nil {
|
|
log.WithContext(ctx).WithError(err).Error("load user security status failed")
|
|
response.NewErrorResponse(response.ServerError, "读取用户安全状态失败", "").FPrint(w)
|
|
return
|
|
}
|
|
items := make([]userListItem, 0, len(users))
|
|
memberships := map[int][]int{}
|
|
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)
|
|
}
|
|
memberships, err = accountgroup.Memberships(db.Instance, ids)
|
|
if err != nil {
|
|
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: groupIDs,
|
|
ID: user.ID,
|
|
Account: user.Account,
|
|
Name: user.Name,
|
|
Disabled: user.Disabled,
|
|
Role: user.EffectiveRole(),
|
|
IsAdmin: user.IsAdmin,
|
|
MustChangePassword: user.MustChangePassword,
|
|
RecoveryCodeConfigured: recoveryConfigured[user.ID],
|
|
ExternalEmailEnabled: user.ExternalEmailEnabled == 1,
|
|
ExternalEmailConfigured: externalEmailConfigured[user.ID],
|
|
})
|
|
if groupManager {
|
|
items[len(items)-1].MembershipVersion = user.SecurityVersion
|
|
}
|
|
}
|
|
response.NewSuccessResponse(map[string]any{
|
|
"current_page": reqData.CurrentPage,
|
|
"total_page": cast.ToInt(math.Ceil(cast.ToFloat64(totalNum) / cast.ToFloat64(reqData.PageSize))),
|
|
"list": items,
|
|
}).FPrint(w)
|
|
}
|
|
|
|
func Info(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
|
if _, err := csrf.Ensure(w, req, !config.DevelopmentMode); err != nil {
|
|
log.WithContext(ctx).WithError(err).Error("restore csrf token failed")
|
|
response.NewErrorResponse(response.ServerError, "登录状态恢复失败", "").FPrint(w)
|
|
return
|
|
}
|
|
mailDomains := config.CurrentMailDomains()
|
|
if mailDomains.Primary == "" {
|
|
response.NewErrorResponse(response.ServerError, "服务配置不可用", "").FPrint(w)
|
|
return
|
|
}
|
|
domains := array.Difference(mailDomains.Accepted, []string{mailDomains.Primary})
|
|
domains = append([]string{mailDomains.Primary}, domains...)
|
|
result := map[string]any{
|
|
"id": ctx.UserID,
|
|
"account": ctx.UserAccount,
|
|
"name": ctx.UserName,
|
|
"role": ctx.EffectiveRole(),
|
|
"capabilities": access.Capabilities(ctx.EffectiveRole()),
|
|
"is_admin": ctx.IsAdmin,
|
|
"must_change_password": ctx.MustChangePassword,
|
|
"domains": domains,
|
|
}
|
|
for key, value := range loginSecurityFields(&models.User{ID: ctx.UserID, Role: string(ctx.EffectiveRole())}) {
|
|
result[key] = value
|
|
}
|
|
if result["securitySetupRequired"] != true && session.Instance != nil && session.Instance.GetBool(req.Context(), session.SecuritySetupDismissedKey) {
|
|
result["showSecuritySetupPrompt"] = false
|
|
result["showRecoverySetupPrompt"] = false
|
|
}
|
|
response.NewSuccessResponse(result).FPrint(w)
|
|
}
|
|
|
|
func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
|
var reqData userEditRequest
|
|
if err := decodeUserRequest(w, req, &reqData); err != nil {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
if reqData.Id == 0 && reqData.Account == "" {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
if reqData.Disabled != 0 && reqData.Disabled != 1 {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
|
|
var user models.User
|
|
var found bool
|
|
var err error
|
|
if reqData.Id != 0 {
|
|
found, err = db.Instance.Where("id=?", reqData.Id).Get(&user)
|
|
} else {
|
|
account, validationErr := validation.NormalizeAccount(reqData.Account)
|
|
if validationErr != nil {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
found, err = db.Instance.Where("account=?", account).Get(&user)
|
|
}
|
|
if err != nil {
|
|
log.WithContext(ctx).Errorf("SQL Error: %+v", err)
|
|
response.NewErrorResponse(response.ServerError, "查询用户失败", "").FPrint(w)
|
|
return
|
|
}
|
|
if !found || user.ID == 0 {
|
|
response.NewErrorResponse(response.ParamsError, "用户不存在", "").FPrint(w)
|
|
return
|
|
}
|
|
targetRole := user.EffectiveRole()
|
|
writeGuard := accountWriteGuard(ctx, user)
|
|
if reqData.GroupIDs != nil {
|
|
if !requireAccountGroupMembershipManagement(ctx, w) {
|
|
return
|
|
}
|
|
if reqData.MembershipVersion < 1 || reqData.MembershipVersion != user.SecurityVersion {
|
|
writeAccountGroupError(w, accountgroup.ErrConflict)
|
|
return
|
|
}
|
|
}
|
|
if !requireAccountScope(ctx, w, user.ID) {
|
|
return
|
|
}
|
|
if err := authorizeCapability(ctx, access.CapabilityUserManage, targetRole, false); err != nil {
|
|
response.NewErrorResponse(response.NoAccessPrivileges, "无权管理该用户", "").FPrint(w)
|
|
return
|
|
}
|
|
if reqData.Password != "" {
|
|
if err := authorizeCapability(ctx, access.CapabilityPasswordReset, targetRole, false); err != nil {
|
|
writeUserAuthorizationError(w, err, "密码重置需要为当前管理员配置有效安全码")
|
|
return
|
|
}
|
|
}
|
|
originalDisabled := user.Disabled
|
|
originalRole := user.EffectiveRole()
|
|
originalSessionVersion := user.SessionVersion
|
|
originalSecurityVersion := user.SecurityVersion
|
|
if reqData.Role != "" && access.Role(reqData.Role) != targetRole {
|
|
newRole := access.Role(reqData.Role)
|
|
if !newRole.Valid() {
|
|
response.NewErrorResponse(response.ParamsError, "角色参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
ctx.TargetRecoveryConfigured = false
|
|
if credentialService := credentialRuntime(); credentialService != nil {
|
|
configured, checkErr := credentialService.HasValidRecoveryCode(user.ID)
|
|
if checkErr != nil {
|
|
log.WithContext(ctx).WithError(checkErr).Error("load target recovery code status failed")
|
|
response.NewErrorResponse(response.ServerError, "读取目标用户恢复码状态失败", "").FPrint(w)
|
|
return
|
|
}
|
|
ctx.TargetRecoveryConfigured = configured
|
|
}
|
|
if err := authorizeCapabilityWithNewRole(ctx, access.CapabilityRoleManage, targetRole, newRole, false); err != nil {
|
|
writeUserAuthorizationError(w, err, "更改权限级别前,请先为该用户配置有效安全码")
|
|
return
|
|
}
|
|
user.ApplyRole(newRole)
|
|
}
|
|
if user.IsAdmin == 1 && reqData.Disabled != 0 {
|
|
response.NewErrorResponse(response.ParamsError, "不允许禁用管理员账户", "").FPrint(w)
|
|
return
|
|
}
|
|
if user.ID == ctx.UserID && reqData.Password != "" {
|
|
response.NewErrorResponse(response.ParamsError, "请通过安全设置修改当前账户密码", "").FPrint(w)
|
|
return
|
|
}
|
|
if reqData.Username != "" {
|
|
username, validationErr := validation.NormalizeDisplayName(reqData.Username)
|
|
if validationErr != nil {
|
|
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
|
return
|
|
}
|
|
user.Name = username
|
|
}
|
|
user.Disabled = reqData.Disabled
|
|
if reqData.Password != "" {
|
|
if err := auth.SetPassword(&user, reqData.Password); err != nil {
|
|
if writePasswordBusy(w, err) {
|
|
return
|
|
}
|
|
if errors.Is(err, password.ErrInvalidLength) {
|
|
response.NewErrorResponse(response.ParamsError, "密码长度必须为 8-12 字节", "").FPrint(w)
|
|
return
|
|
}
|
|
log.WithContext(ctx).WithError(err).Error("password hash failed")
|
|
response.NewErrorResponse(response.ServerError, "密码设置失败", "").FPrint(w)
|
|
return
|
|
}
|
|
user.MustChangePassword = 1
|
|
now := time.Now().UTC()
|
|
user.PasswordChangedAt = &now
|
|
}
|
|
if reqData.Password != "" || user.Disabled != originalDisabled || user.EffectiveRole() != originalRole || reqData.GroupIDs != nil {
|
|
user.SessionVersion = originalSessionVersion + 1
|
|
user.SecurityVersion = originalSecurityVersion + 1
|
|
}
|
|
credentialService := credentialRuntime()
|
|
if reqData.Password != "" && credentialService == nil {
|
|
log.WithContext(ctx).Error("credential service unavailable during user password reset")
|
|
response.NewErrorResponse(response.ServerError, "安全凭据服务不可用", "").FPrint(w)
|
|
return
|
|
}
|
|
tx := db.Instance.NewSession()
|
|
if tx == nil {
|
|
response.NewErrorResponse(response.ServerError, "更新用户失败", "").FPrint(w)
|
|
return
|
|
}
|
|
defer tx.Close()
|
|
if err := tx.Begin(); err != nil {
|
|
log.WithContext(ctx).WithError(err).Error("begin user update transaction failed")
|
|
response.NewErrorResponse(response.ServerError, "更新用户失败", "").FPrint(w)
|
|
return
|
|
}
|
|
committed := false
|
|
defer func() {
|
|
if !committed {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
updateColumns := []string{"name", "disabled", "role", "is_admin"}
|
|
if err := accountgroup.CheckWrite(tx, writeGuard); err != nil {
|
|
_ = tx.Rollback()
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
if reqData.GroupIDs != nil {
|
|
if err := accountgroup.CheckWrite(tx, accountgroup.MembershipScopeGuard(ctx.UserID, ctx.EffectiveRole(), *reqData.GroupIDs)); err != nil {
|
|
_ = tx.Rollback()
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
}
|
|
if reqData.Password != "" {
|
|
updateColumns = append(updateColumns, "password", "must_change_password", "password_changed_at")
|
|
}
|
|
if reqData.Password != "" || user.Disabled != originalDisabled || user.EffectiveRole() != originalRole || reqData.GroupIDs != nil {
|
|
updateColumns = append(updateColumns, "session_version", "security_version")
|
|
}
|
|
update := tx.ID(user.ID).Where("session_version = ? AND security_version = ?", originalSessionVersion, originalSecurityVersion).Cols(updateColumns...)
|
|
accountgroup.ApplyScope(update, ctx.UserID, ctx.EffectiveRole())
|
|
if num, err := update.Update(&user); err != nil {
|
|
_ = tx.Rollback()
|
|
log.WithContext(ctx).WithError(err).Error("update user failed")
|
|
response.NewErrorResponse(response.ServerError, "更新用户失败", "").FPrint(w)
|
|
return
|
|
} else if num == 0 {
|
|
_ = tx.Rollback()
|
|
response.NewErrorResponse(response.ServerError, "没有可更新的数据", "").FPrint(w)
|
|
return
|
|
}
|
|
if reqData.Password != "" {
|
|
if err := credentialService.RevokeAllWithSession(tx, user.ID); err != nil {
|
|
_ = tx.Rollback()
|
|
log.WithContext(ctx).WithError(err).Error("revoke target credentials failed")
|
|
response.NewErrorResponse(response.ServerError, "撤销目标用户安全凭据失败", "").FPrint(w)
|
|
return
|
|
}
|
|
}
|
|
// 角色和分组在同一事务中校验与更新,任何失败均不能留下部分修改。
|
|
if reqData.GroupIDs != nil {
|
|
ids, err := accountgroup.ValidateMembership(tx, user.EffectiveRole(), *reqData.GroupIDs)
|
|
if err == nil {
|
|
err = accountgroup.ReplaceMembership(tx, user.ID, ids)
|
|
}
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
}
|
|
if user.EffectiveRole() != access.RoleAdmin {
|
|
count, err := tx.Where("user_id = ?", user.ID).Count(&models.AccountGroupMember{})
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
writeAccountGroupError(w, err)
|
|
return
|
|
}
|
|
if count > 1 {
|
|
_ = tx.Rollback()
|
|
writeSecurityError(w, response.ParamsError, "请先将账号调整为一个分组,再更改为普通用户")
|
|
return
|
|
}
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
_ = tx.Rollback()
|
|
log.WithContext(ctx).WithError(err).Error("commit user update failed")
|
|
response.NewErrorResponse(response.ServerError, "更新用户失败", "").FPrint(w)
|
|
return
|
|
}
|
|
committed = true
|
|
if user.SessionVersion != originalSessionVersion {
|
|
InvalidateSecurityRuntimeUser(user.ID, user.Account)
|
|
}
|
|
response.NewSuccessResponse(user).FPrint(w)
|
|
}
|
|
|
|
// writeUserAuthorizationError 保留授权限制,并让界面按错误类型继续身份确认。
|
|
func writeUserAuthorizationError(w http.ResponseWriter, err error, recoveryMessage string) {
|
|
if access.IsDenied(err, access.CodeRecentAuthRequired) {
|
|
writeSecurityErrorWithCode(w, response.NoAccessPrivileges, "请确认当前密码后继续操作", string(access.CodeRecentAuthRequired))
|
|
return
|
|
}
|
|
if access.IsDenied(err, access.CodeRecoveryCodeRequired) {
|
|
writeSecurityErrorWithCode(w, response.NoAccessPrivileges, recoveryMessage, string(access.CodeRecoveryCodeRequired))
|
|
return
|
|
}
|
|
writeSecurityError(w, response.NoAccessPrivileges, "无权执行该用户管理操作")
|
|
}
|