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