This commit is contained in:
parent
951177c841
commit
4d24bf21d5
@ -12,6 +12,13 @@ import (
|
||||
"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, "无权查看或管理该账号")
|
||||
@ -110,7 +117,7 @@ func SetAccountGroupMembership(ctx *context.Context, w http.ResponseWriter, req
|
||||
writeAccountGroupError(w, accountgroup.ErrConflict)
|
||||
return
|
||||
}
|
||||
if err := accountgroup.SetMembership(db.Instance, user, input.GroupIDs); err != nil {
|
||||
if err := accountgroup.SetMembership(db.Instance, user, input.GroupIDs, accountWriteGuard(ctx, user)); err != nil {
|
||||
writeAccountGroupError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ func SetNotificationAuthorization(ctx *context.Context, w http.ResponseWriter, r
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err := notification.SetAuthorization(db.Instance, *user, input.Version, *input.Enabled, input.MinuteLimit, input.DayLimit)
|
||||
err := notification.SetAuthorization(db.Instance, *user, input.Version, *input.Enabled, input.MinuteLimit, input.DayLimit, accountWriteGuard(ctx, *user))
|
||||
recordNotificationManagement(ctx, user.ID, "设置系统通知授权", err, map[string]any{"enabled": *input.Enabled, "minute_limit": input.MinuteLimit, "day_limit": input.DayLimit})
|
||||
if err != nil {
|
||||
writeNotificationError(w, err)
|
||||
@ -191,7 +191,7 @@ func RotateNotificationCredential(ctx *context.Context, w http.ResponseWriter, r
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
code, err := notification.Rotate(db.Instance, *user, input.Version)
|
||||
code, err := notification.Rotate(db.Instance, *user, input.Version, accountWriteGuard(ctx, *user))
|
||||
recordNotificationManagement(ctx, user.ID, "重置系统通知凭据", err, nil)
|
||||
if err != nil {
|
||||
writeNotificationError(w, err)
|
||||
@ -212,6 +212,10 @@ func recordNotificationManagement(ctx *context.Context, userID int, action strin
|
||||
}
|
||||
|
||||
func writeNotificationError(w http.ResponseWriter, err error) {
|
||||
if errors.Is(err, accountgroup.ErrDenied) || errors.Is(err, accountgroup.ErrConflict) {
|
||||
writeAccountGroupError(w, err)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, notification.ErrConflict):
|
||||
writeSecurityError(w, response.ParamsError, "授权状态已变化,请刷新后重试")
|
||||
|
||||
@ -320,6 +320,7 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
targetRole := user.EffectiveRole()
|
||||
writeGuard := accountWriteGuard(ctx, user)
|
||||
if !requireAccountScope(ctx, w, user.ID) {
|
||||
return
|
||||
}
|
||||
@ -421,6 +422,10 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}()
|
||||
updateColumns := []string{"name", "disabled", "role", "is_admin"}
|
||||
if err := accountgroup.CheckWrite(tx, writeGuard); err != nil {
|
||||
writeAccountGroupError(w, err)
|
||||
return
|
||||
}
|
||||
if reqData.Password != "" {
|
||||
updateColumns = append(updateColumns, "password", "must_change_password", "password_changed_at")
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"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/externalemail"
|
||||
"git.pcux.cn/xuans/Smail/services/verification"
|
||||
"git.pcux.cn/xuans/Smail/utils/context"
|
||||
@ -64,6 +65,14 @@ func loadManagedSecurityUser(ctx *context.Context, w http.ResponseWriter, userID
|
||||
return &target, true
|
||||
}
|
||||
|
||||
func writeManagedScopeError(w http.ResponseWriter, err error) bool {
|
||||
if errors.Is(err, accountgroup.ErrDenied) || errors.Is(err, accountgroup.ErrConflict) {
|
||||
writeAccountGroupError(w, err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func managedUserIDFromQuery(req *http.Request) int {
|
||||
if req == nil || req.URL == nil {
|
||||
return 0
|
||||
@ -112,8 +121,11 @@ func GenerateManagedRecoveryCodes(ctx *context.Context, w http.ResponseWriter, r
|
||||
writeSecurityError(w, response.ServerError, "安全凭据服务不可用")
|
||||
return
|
||||
}
|
||||
codes, err := service.GenerateRecoveryCodesContext(recoveryRequestContext(req), target.ID, input.Count)
|
||||
codes, err := service.GenerateRecoveryCodesContext(recoveryRequestContext(req), target.ID, input.Count, accountWriteGuard(ctx, *target))
|
||||
if err != nil {
|
||||
if writeManagedScopeError(w, err) {
|
||||
return
|
||||
}
|
||||
if writePasswordBusy(w, err) {
|
||||
return
|
||||
}
|
||||
@ -145,7 +157,10 @@ func RevokeManagedRecoveryCodes(ctx *context.Context, w http.ResponseWriter, req
|
||||
writeSecurityError(w, response.ServerError, "安全凭据服务不可用")
|
||||
return
|
||||
}
|
||||
if err := service.RevokeRecoveryCodes(target.ID); err != nil {
|
||||
if err := service.RevokeRecoveryCodes(target.ID, accountWriteGuard(ctx, *target)); err != nil {
|
||||
if writeManagedScopeError(w, err) {
|
||||
return
|
||||
}
|
||||
log.WithContext(ctx).WithError(err).Error("revoke managed recovery codes failed")
|
||||
writeSecurityError(w, response.ServerError, "撤销安全码失败")
|
||||
return
|
||||
@ -230,8 +245,11 @@ func ChangeManagedExternalEmail(ctx *context.Context, w http.ResponseWriter, req
|
||||
UserID: target.ID,
|
||||
Email: normalizedEmail,
|
||||
Type: emailType,
|
||||
})
|
||||
}, accountWriteGuard(ctx, *target))
|
||||
if err != nil {
|
||||
if writeManagedScopeError(w, err) {
|
||||
return
|
||||
}
|
||||
if errors.Is(err, externalemail.ErrBindingExists) {
|
||||
writeSecurityError(w, response.ParamsError, "该外部邮箱已经绑定或正在被其他账号使用")
|
||||
return
|
||||
@ -295,7 +313,10 @@ func VerifyManagedExternalEmail(ctx *context.Context, w http.ResponseWriter, req
|
||||
writeSecurityError(w, response.ParamsError, externalEmailVerificationCheckMessage(err))
|
||||
return
|
||||
}
|
||||
if err := runtime.external.VerifyReplacement(target.ID, input.BindingID); err != nil {
|
||||
if err := runtime.external.VerifyReplacement(target.ID, input.BindingID, accountWriteGuard(ctx, *target)); err != nil {
|
||||
if writeManagedScopeError(w, err) {
|
||||
return
|
||||
}
|
||||
log.WithContext(ctx).WithError(err).Error("verify managed external email replacement failed")
|
||||
writeSecurityError(w, response.ParamsError, "外部邮箱替换失败")
|
||||
return
|
||||
|
||||
@ -187,6 +187,8 @@ func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
} else {
|
||||
ctx.UserID = currentUser.ID
|
||||
ctx.SessionVersion = currentUser.SessionVersion
|
||||
ctx.SecurityVersion = currentUser.SecurityVersion
|
||||
ctx.UserName = currentUser.Name
|
||||
ctx.UserAccount = currentUser.Account
|
||||
ctx.Role = currentUser.EffectiveRole()
|
||||
|
||||
@ -184,7 +184,63 @@ func ReplaceMembership(tx *xorm.Session, userID int, ids []int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func SetMembership(engine *xorm.Engine, user models.User, input []int) error {
|
||||
type WriteGuard func(*xorm.Session) error
|
||||
|
||||
func CheckWrite(tx *xorm.Session, guards ...WriteGuard) error {
|
||||
for _, guard := range guards {
|
||||
if guard == nil {
|
||||
return ErrDenied
|
||||
}
|
||||
if err := guard(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Guard(actorID int, role access.Role, sessionVersion, securityVersion int, target models.User) WriteGuard {
|
||||
return func(tx *xorm.Session) error {
|
||||
if tx == nil || actorID <= 0 || target.ID <= 0 || (role != access.RoleAdmin && role != access.RoleSuperAdmin) {
|
||||
return ErrDenied
|
||||
}
|
||||
ids := []int{actorID}
|
||||
if target.ID != actorID {
|
||||
ids = append(ids, target.ID)
|
||||
}
|
||||
sort.Ints(ids)
|
||||
for _, id := range ids {
|
||||
if _, err := tx.ID(id).SetExpr("session_version", "session_version").Update(&models.User{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var actor models.User
|
||||
found, err := tx.ID(actorID).Get(&actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || actor.Disabled != 0 || actor.MustChangePassword != 0 || actor.EffectiveRole() != role ||
|
||||
actor.SessionVersion != sessionVersion || actor.SecurityVersion != securityVersion {
|
||||
return ErrDenied
|
||||
}
|
||||
var current models.User
|
||||
query := tx.ID(target.ID)
|
||||
ApplyScope(query, actorID, role)
|
||||
found, err = query.Get(¤t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return ErrDenied
|
||||
}
|
||||
if current.SessionVersion != target.SessionVersion || current.SecurityVersion != target.SecurityVersion ||
|
||||
current.EffectiveRole() != target.EffectiveRole() || current.Disabled != target.Disabled || current.MustChangePassword != target.MustChangePassword {
|
||||
return ErrConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetMembership(engine *xorm.Engine, user models.User, input []int, guards ...WriteGuard) error {
|
||||
if engine == nil || user.ID <= 0 {
|
||||
return ErrDenied
|
||||
}
|
||||
@ -194,6 +250,9 @@ func SetMembership(engine *xorm.Engine, user models.User, input []int) error {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if err := CheckWrite(tx, guards...); err != nil {
|
||||
return err
|
||||
}
|
||||
updated := user
|
||||
updated.SessionVersion++
|
||||
updated.SecurityVersion++
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/models"
|
||||
"git.pcux.cn/xuans/Smail/services/accountgroup"
|
||||
"git.pcux.cn/xuans/Smail/utils/password"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
@ -146,11 +147,11 @@ func (s *Service) GenerateRecoveryCodes(userID int, count int) ([]string, error)
|
||||
|
||||
// GenerateRecoveryCodesContext 在请求取消时停止后续计算,并回滚尚未提交的恢复码轮换。
|
||||
// 单次 Argon2id 计算不可中断,因此每次计算前后检查取消,不降低原有密码强度。
|
||||
func (s *Service) GenerateRecoveryCodesContext(ctx context.Context, userID int, count int) ([]string, error) {
|
||||
return s.generateRecoveryCodes(ctx, userID, count, password.Hash)
|
||||
func (s *Service) GenerateRecoveryCodesContext(ctx context.Context, userID int, count int, guards ...accountgroup.WriteGuard) ([]string, error) {
|
||||
return s.generateRecoveryCodes(ctx, userID, count, password.Hash, guards...)
|
||||
}
|
||||
|
||||
func (s *Service) generateRecoveryCodes(ctx context.Context, userID int, count int, hashSecret func(string) (string, error)) (codes []string, resultErr error) {
|
||||
func (s *Service) generateRecoveryCodes(ctx context.Context, userID int, count int, hashSecret func(string) (string, error), guards ...accountgroup.WriteGuard) (codes []string, resultErr error) {
|
||||
if s == nil || s.engine == nil {
|
||||
return nil, ErrUnavailable
|
||||
}
|
||||
@ -209,6 +210,9 @@ func (s *Service) generateRecoveryCodes(ctx context.Context, userID int, count i
|
||||
return nil, fmt.Errorf("begin recovery code transaction: %w", err)
|
||||
}
|
||||
defer session.Rollback()
|
||||
if err := accountgroup.CheckWrite(session, guards...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := s.now()
|
||||
if _, err := session.Where("user_id = ? AND used_at IS NULL", userID).Cols("used_at").Update(&models.RecoveryCode{UsedAt: &now}); err != nil {
|
||||
_ = session.Rollback()
|
||||
@ -263,8 +267,6 @@ func (s *Service) ConsumeRecoveryCodeWithSession(session *xorm.Session, userID i
|
||||
if len(code) < 12 || len(code) > 64 {
|
||||
return ErrInvalidCode
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.consumeRecoveryCode(session, userID, code)
|
||||
}
|
||||
|
||||
@ -315,7 +317,7 @@ func (s *Service) HasValidRecoveryCode(userID int) (bool, error) {
|
||||
}
|
||||
|
||||
// RevokeRecoveryCodes 撤销用户现有的离线恢复码,但保留密保问题。
|
||||
func (s *Service) RevokeRecoveryCodes(userID int) error {
|
||||
func (s *Service) RevokeRecoveryCodes(userID int, guards ...accountgroup.WriteGuard) error {
|
||||
if s == nil || s.engine == nil {
|
||||
return ErrUnavailable
|
||||
}
|
||||
@ -325,10 +327,19 @@ func (s *Service) RevokeRecoveryCodes(userID int) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := s.now()
|
||||
if _, err := s.engine.Where("user_id = ? AND used_at IS NULL", userID).Cols("used_at").Update(&models.RecoveryCode{UsedAt: &now}); err != nil {
|
||||
session := s.engine.NewSession()
|
||||
defer session.Close()
|
||||
if err := session.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Rollback()
|
||||
if err := accountgroup.CheckWrite(session, guards...); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := session.Where("user_id = ? AND used_at IS NULL", userID).Cols("used_at").Update(&models.RecoveryCode{UsedAt: &now}); err != nil {
|
||||
return fmt.Errorf("revoke recovery codes: %w", err)
|
||||
}
|
||||
return nil
|
||||
return session.Commit()
|
||||
}
|
||||
|
||||
// RevokeAll 撤销用户的恢复码、密保问题、安全密钥及邮件授权码。
|
||||
@ -352,8 +363,6 @@ func (s *Service) RevokeAllWithSession(session *xorm.Session, userID int) error
|
||||
if userID <= 0 {
|
||||
return ErrInvalidUser
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := s.now()
|
||||
if _, err := session.Where("user_id = ? AND used_at IS NULL", userID).Cols("used_at").Update(&models.RecoveryCode{UsedAt: &now}); err != nil {
|
||||
return fmt.Errorf("revoke recovery codes: %w", err)
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/models"
|
||||
"git.pcux.cn/xuans/Smail/services/accountgroup"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@ -32,7 +33,30 @@ type BindRequest struct {
|
||||
}
|
||||
|
||||
// BeginReplacement 创建或更新待验证邮箱。旧邮箱会一直保留到新邮箱验证成功。
|
||||
func (r *Repository) BeginReplacement(request BindRequest) (*models.ExternalEmail, error) {
|
||||
func (r *Repository) BeginReplacement(request BindRequest, guards ...accountgroup.WriteGuard) (*models.ExternalEmail, error) {
|
||||
if r == nil || r.engine == nil {
|
||||
return nil, ErrInvalidBinding
|
||||
}
|
||||
tx := r.engine.NewSession()
|
||||
defer tx.Close()
|
||||
if err := tx.Begin(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if err := accountgroup.CheckWrite(tx, guards...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item, err := r.beginReplacement(tx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (r *Repository) beginReplacement(tx *xorm.Session, request BindRequest) (*models.ExternalEmail, error) {
|
||||
if r == nil || r.engine == nil || r.vault == nil || request.UserID <= 0 {
|
||||
return nil, ErrInvalidBinding
|
||||
}
|
||||
@ -53,7 +77,7 @@ func (r *Repository) BeginReplacement(request BindRequest) (*models.ExternalEmai
|
||||
}
|
||||
|
||||
var sameAddress models.ExternalEmail
|
||||
hasSameAddress, err := r.engine.Where("email_fingerprint = ?", fingerprint).Get(&sameAddress)
|
||||
hasSameAddress, err := tx.Where("email_fingerprint = ?", fingerprint).Get(&sameAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check replacement external email: %w", err)
|
||||
}
|
||||
@ -76,7 +100,7 @@ func (r *Repository) BeginReplacement(request BindRequest) (*models.ExternalEmai
|
||||
sameAddress.VerifiedAt = nil
|
||||
sameAddress.CredentialVersion++
|
||||
sameAddress.UpdatedAt = now
|
||||
updated, err := r.engine.ID(sameAddress.ID).Where("status = ?", models.ExternalEmailStatusDisabled).
|
||||
updated, err := tx.ID(sameAddress.ID).Where("status = ?", models.ExternalEmailStatusDisabled).
|
||||
Cols("email_type", "status", "verified_at", "credential_version", "updated_at").Update(&sameAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reuse disabled replacement external email: %w", err)
|
||||
@ -88,7 +112,7 @@ func (r *Repository) BeginReplacement(request BindRequest) (*models.ExternalEmai
|
||||
}
|
||||
|
||||
var pending models.ExternalEmail
|
||||
hasPending, err := r.engine.Where(
|
||||
hasPending, err := tx.Where(
|
||||
"user_id = ? AND email_type = ? AND status = ?",
|
||||
request.UserID,
|
||||
request.Type,
|
||||
@ -102,7 +126,7 @@ func (r *Repository) BeginReplacement(request BindRequest) (*models.ExternalEmai
|
||||
pending.EmailFingerprint = fingerprint
|
||||
pending.CredentialVersion++
|
||||
pending.UpdatedAt = now
|
||||
if _, err := r.engine.ID(pending.ID).Cols("email_ciphertext", "email_fingerprint", "credential_version", "updated_at").Update(&pending); err != nil {
|
||||
if _, err := tx.ID(pending.ID).Cols("email_ciphertext", "email_fingerprint", "credential_version", "updated_at").Update(&pending); err != nil {
|
||||
return nil, fmt.Errorf("update pending replacement external email: %w", err)
|
||||
}
|
||||
return &pending, nil
|
||||
@ -118,7 +142,7 @@ func (r *Repository) BeginReplacement(request BindRequest) (*models.ExternalEmai
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if _, err := r.engine.Insert(item); err != nil {
|
||||
if _, err := tx.Insert(item); err != nil {
|
||||
return nil, fmt.Errorf("create replacement external email: %w", err)
|
||||
}
|
||||
return item, nil
|
||||
@ -311,7 +335,7 @@ func (r *Repository) Verify(userID, id int) error {
|
||||
}
|
||||
|
||||
// VerifyReplacement 验证新邮箱后原子停用同用途旧邮箱,并启用新邮箱。
|
||||
func (r *Repository) VerifyReplacement(userID, id int) error {
|
||||
func (r *Repository) VerifyReplacement(userID, id int, guards ...accountgroup.WriteGuard) error {
|
||||
if r == nil || r.engine == nil || userID <= 0 || id <= 0 {
|
||||
return ErrInvalidBinding
|
||||
}
|
||||
@ -330,6 +354,9 @@ func (r *Repository) VerifyReplacement(userID, id int) error {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := accountgroup.CheckWrite(session, guards...); err != nil {
|
||||
return err
|
||||
}
|
||||
item := &models.ExternalEmail{}
|
||||
has, err := session.Where("id = ? AND user_id = ? AND status = ?", id, userID, models.ExternalEmailStatusPending).Get(item)
|
||||
if err != nil {
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/models"
|
||||
"git.pcux.cn/xuans/Smail/services/accountgroup"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@ -52,17 +53,33 @@ func Configured(item models.NotificationGrant, user models.User) bool {
|
||||
}
|
||||
|
||||
// SetAuthorization 使用版本条件避免覆盖其他管理员的修改;关闭时永久清除旧凭据。
|
||||
func SetAuthorization(engine *xorm.Engine, user models.User, expected int, enabled bool, minuteLimit, dayLimit int) error {
|
||||
func SetAuthorization(engine *xorm.Engine, user models.User, expected int, enabled bool, minuteLimit, dayLimit int, guards ...accountgroup.WriteGuard) error {
|
||||
if minuteLimit < 1 || minuteLimit > 100 || dayLimit < minuteLimit || dayLimit > 10000 {
|
||||
return ErrLimits
|
||||
}
|
||||
if enabled && (user.Disabled != 0 || user.MustChangePassword != 0) {
|
||||
return ErrDenied
|
||||
}
|
||||
item, err := Load(engine, user.ID)
|
||||
if engine == nil || user.ID <= 0 {
|
||||
return ErrUnavailable
|
||||
}
|
||||
tx := engine.NewSession()
|
||||
defer tx.Close()
|
||||
if err := tx.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if err := accountgroup.CheckWrite(tx, guards...); err != nil {
|
||||
return err
|
||||
}
|
||||
var item models.NotificationGrant
|
||||
found, err := tx.ID(user.ID).Get(&item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
item = models.NotificationGrant{UserID: user.ID}
|
||||
}
|
||||
if item.CodeVersion != expected {
|
||||
return ErrConflict
|
||||
}
|
||||
@ -75,20 +92,38 @@ func SetAuthorization(engine *xorm.Engine, user models.User, expected int, enabl
|
||||
item.MinuteLimit, item.DayLimit = minuteLimit, dayLimit
|
||||
item.CodeVersion++
|
||||
if expected == 0 {
|
||||
_, err = engine.Insert(&item)
|
||||
if _, err := tx.Insert(&item); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
affected, err := tx.ID(user.ID).Where("code_version = ?", expected).
|
||||
Cols("enabled", "code_hash", "code_version", "minute_limit", "day_limit").Update(&item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
affected, err := engine.ID(user.ID).Where("code_version = ?", expected).
|
||||
Cols("enabled", "code_hash", "code_version", "minute_limit", "day_limit").Update(&item)
|
||||
if err == nil && affected != 1 {
|
||||
if affected != 1 {
|
||||
return ErrConflict
|
||||
}
|
||||
return err
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Rotate 仅返回一次明文。随机凭据具有 256 位熵,数据库只保存 SHA-256 摘要。
|
||||
func Rotate(engine *xorm.Engine, user models.User, expected int) (string, error) {
|
||||
item, err := Load(engine, user.ID)
|
||||
func Rotate(engine *xorm.Engine, user models.User, expected int, guards ...accountgroup.WriteGuard) (string, error) {
|
||||
if engine == nil || user.ID <= 0 {
|
||||
return "", ErrUnavailable
|
||||
}
|
||||
tx := engine.NewSession()
|
||||
defer tx.Close()
|
||||
if err := tx.Begin(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if err := accountgroup.CheckWrite(tx, guards...); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var item models.NotificationGrant
|
||||
_, err := tx.ID(user.ID).Get(&item)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -106,7 +141,7 @@ func Rotate(engine *xorm.Engine, user models.User, expected int) (string, error)
|
||||
item.CodeHash = digest(code)
|
||||
item.CodeVersion++
|
||||
item.SessionVersion, item.SecurityVersion = user.SessionVersion, user.SecurityVersion
|
||||
affected, err := engine.ID(user.ID).Where("code_version = ? AND enabled = 1", expected).
|
||||
affected, err := tx.ID(user.ID).Where("code_version = ? AND enabled = 1", expected).
|
||||
Cols("code_hash", "code_version", "session_version", "security_version").Update(&item)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -114,6 +149,9 @@ func Rotate(engine *xorm.Engine, user models.User, expected int) (string, error)
|
||||
if affected != 1 {
|
||||
return "", ErrConflict
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return code, nil
|
||||
}
|
||||
|
||||
|
||||
@ -11,22 +11,24 @@ const (
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
context.Context `json:"-"`
|
||||
UserID int
|
||||
UserAccount string
|
||||
UserName string
|
||||
Role access.Role
|
||||
Values map[string]any
|
||||
Lang string
|
||||
IsAdmin bool
|
||||
MustChangePassword int
|
||||
context.Context `json:"-"`
|
||||
UserID int
|
||||
SessionVersion int
|
||||
SecurityVersion int
|
||||
UserAccount string
|
||||
UserName string
|
||||
Role access.Role
|
||||
Values map[string]any
|
||||
Lang string
|
||||
IsAdmin bool
|
||||
MustChangePassword int
|
||||
SecuritySetupAcknowledged int
|
||||
RecentAuth bool
|
||||
RecoveryCodeConfigured bool
|
||||
TargetRecoveryConfigured bool
|
||||
PluginInstanceAuthorized bool
|
||||
AuditScopeAuthorized bool
|
||||
DeploymentChannel bool
|
||||
RecentAuth bool
|
||||
RecoveryCodeConfigured bool
|
||||
TargetRecoveryConfigured bool
|
||||
PluginInstanceAuthorized bool
|
||||
AuditScopeAuthorized bool
|
||||
DeploymentChannel bool
|
||||
}
|
||||
|
||||
// HasRole 判断当前请求是否属于指定角色。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user