128 lines
3.5 KiB
Go
128 lines
3.5 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"tm/pkg/audit"
|
|
"tm/pkg/password"
|
|
"tm/pkg/ratelimit"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
var (
|
|
errInvalidUserID = errors.New("invalid id")
|
|
errAdminUserNotFound = errors.New("admin not found")
|
|
errCannotResetPassword = errors.New("cannot reset password for this user")
|
|
errResetPasswordFailed = errors.New("failed to reset password")
|
|
errResetPasswordRateHit = errors.New("too many password reset requests")
|
|
errResetPasswordForbidden = errors.New("you are not allowed to reset this password")
|
|
)
|
|
|
|
const adminPasswordResetRateLimit = 10
|
|
|
|
// AdminResetPassword generates a new password for the target admin user.
|
|
func (s *userService) AdminResetPassword(ctx context.Context, actorID, actorRole, targetID string) (*AdminResetPasswordResponse, error) {
|
|
if UserRole(actorRole) != UserRoleAdmin {
|
|
return nil, errResetPasswordForbidden
|
|
}
|
|
|
|
if _, err := bson.ObjectIDFromHex(targetID); err != nil {
|
|
return nil, errInvalidUserID
|
|
}
|
|
|
|
if err := s.checkPasswordResetRateLimit(ctx, actorID, "admin"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.repository.GetByID(ctx, targetID)
|
|
if err != nil {
|
|
if errors.Is(err, ErrUserNotFound) {
|
|
return nil, errAdminUserNotFound
|
|
}
|
|
s.logger.Error("Failed to load user for password reset", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"target_id": targetID,
|
|
})
|
|
return nil, errResetPasswordFailed
|
|
}
|
|
|
|
if user.Status != UserStatusActive {
|
|
return nil, errCannotResetPassword
|
|
}
|
|
|
|
plainPassword, err := password.GenerateSecure()
|
|
if err != nil {
|
|
s.logger.Error("Failed to generate password", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"target_id": targetID,
|
|
})
|
|
return nil, errResetPasswordFailed
|
|
}
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
s.logger.Error("Failed to hash generated password", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"target_id": targetID,
|
|
})
|
|
return nil, errResetPasswordFailed
|
|
}
|
|
|
|
if err := s.repository.UpdatePasswordReset(ctx, targetID, string(hashedPassword), actorID); err != nil {
|
|
if errors.Is(err, ErrUserNotFound) {
|
|
return nil, errAdminUserNotFound
|
|
}
|
|
s.logger.Error("Failed to persist reset password", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"target_id": targetID,
|
|
})
|
|
return nil, errResetPasswordFailed
|
|
}
|
|
|
|
if err := s.authService.InvalidateUserSessions(targetID); err != nil {
|
|
s.logger.Error("Failed to invalidate user sessions after password reset", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"target_id": targetID,
|
|
})
|
|
}
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
ActorID: actorID,
|
|
TargetType: audit.TargetTypeAdmin,
|
|
TargetID: targetID,
|
|
Action: audit.ActionPasswordReset,
|
|
})
|
|
|
|
s.logger.Info("Admin password reset completed", map[string]interface{}{
|
|
"actor_id": actorID,
|
|
"target_id": targetID,
|
|
})
|
|
|
|
return &AdminResetPasswordResponse{Password: plainPassword}, nil
|
|
}
|
|
|
|
func (s *userService) checkPasswordResetRateLimit(ctx context.Context, actorID, resource string) error {
|
|
if s.redisClient == nil {
|
|
return nil
|
|
}
|
|
|
|
key := fmt.Sprintf("ratelimit:password_reset:%s:%s", actorID, resource)
|
|
allowed, err := ratelimit.Allow(ctx, s.redisClient, key, adminPasswordResetRateLimit, time.Minute)
|
|
if err != nil {
|
|
s.logger.Warn("Password reset rate limit check failed", map[string]interface{}{
|
|
"actor_id": actorID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil
|
|
}
|
|
if !allowed {
|
|
return errResetPasswordRateHit
|
|
}
|
|
return nil
|
|
}
|