feat: handle password reset admin command

This commit is contained in:
richjohnrambo
2026-08-28 14:18:27 +08:00
parent d756aeb4c2
commit 9a46fdf43c
+59 -16
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"strconv" "strconv"
@@ -31,27 +32,29 @@ const (
adminCommandUserLogoutAll = "user.logout_all" adminCommandUserLogoutAll = "user.logout_all"
adminCommandUserCancel = "user.account.cancel" adminCommandUserCancel = "user.account.cancel"
adminCommandUserDelete = "user.account.delete" adminCommandUserDelete = "user.account.delete"
adminCommandUserPasswordReset = "user.password.reset"
adminCommandKafkaDeadReplay = "kafka.dead_letter.replay" adminCommandKafkaDeadReplay = "kafka.dead_letter.replay"
adminCommandKafkaDeadIgnore = "kafka.dead_letter.ignore" adminCommandKafkaDeadIgnore = "kafka.dead_letter.ignore"
adminCommandKafkaDeadResolve = "kafka.dead_letter.resolve" adminCommandKafkaDeadResolve = "kafka.dead_letter.resolve"
) )
type adminCommandPayload struct { type adminCommandPayload struct {
CommandID string `json:"command_id"` CommandID string `json:"command_id"`
ActorUserID string `json:"actor_user_id"` ActorUserID string `json:"actor_user_id"`
Topic string `json:"topic"` Topic string `json:"topic"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
UserIDs []string `json:"user_ids"` UserIDs []string `json:"user_ids"`
TargetID string `json:"target_user_id"` TargetID string `json:"target_user_id"`
DeviceID string `json:"device_id"` DeviceID string `json:"device_id"`
Version int `json:"version"` PasswordHash string `json:"password_hash"`
Reason string `json:"reason"` Version int `json:"version"`
DeadID string `json:"dead_id"` Reason string `json:"reason"`
Note string `json:"note"` DeadID string `json:"dead_id"`
Hard bool `json:"hard"` Note string `json:"note"`
ExpiresAt *time.Time `json:"expires_at"` Hard bool `json:"hard"`
Profile groupProfilePatch `json:"profile"` ExpiresAt *time.Time `json:"expires_at"`
Governance groupGovernancePatch `json:"governance"` Profile groupProfilePatch `json:"profile"`
Governance groupGovernancePatch `json:"governance"`
} }
type groupProfilePatch struct { type groupProfilePatch struct {
@@ -157,11 +160,15 @@ func handleKafkaAdminCommand(ctx context.Context, envelope kafkaEnvelope, _ kafk
} }
func executeKafkaAdminCommand(tenantID types.TenantID, envelope kafkaEnvelope, payload adminCommandPayload) (map[string]any, error) { func executeKafkaAdminCommand(tenantID types.TenantID, envelope kafkaEnvelope, payload adminCommandPayload) (map[string]any, error) {
eventType := strings.TrimSpace(envelope.EventType)
if eventType == adminCommandUserPasswordReset {
return executeKafkaUserPasswordResetCommand(tenantID, payload)
}
actor := parseKafkaCommandUser(payload.ActorUserID) actor := parseKafkaCommandUser(payload.ActorUserID)
if actor.IsZero() { if actor.IsZero() {
return nil, types.ErrMalformed return nil, types.ErrMalformed
} }
switch strings.TrimSpace(envelope.EventType) { switch eventType {
case adminCommandKafkaDeadReplay: case adminCommandKafkaDeadReplay:
dead, err := replayKafkaDeadLetter(context.Background(), tenantID, payload.DeadID, actor, firstNonEmpty(payload.Note, payload.Reason)) dead, err := replayKafkaDeadLetter(context.Background(), tenantID, payload.DeadID, actor, firstNonEmpty(payload.Note, payload.Reason))
if err != nil { if err != nil {
@@ -373,6 +380,40 @@ func executeKafkaUserGovernanceCommand(tenantID types.TenantID, envelope kafkaEn
return nil, errors.New("unsupported user governance command: " + eventType) return nil, errors.New("unsupported user governance command: " + eventType)
} }
func executeKafkaUserPasswordResetCommand(tenantID types.TenantID, payload adminCommandPayload) (map[string]any, error) {
target := parseKafkaCommandUser(firstNonEmpty(payload.TargetID, payload.UserID))
if target.IsZero() || payload.PasswordHash == "" {
return nil, types.ErrMalformed
}
passwordHash, err := base64.StdEncoding.DecodeString(payload.PasswordHash)
if err != nil || len(passwordHash) == 0 {
return nil, types.ErrMalformed
}
_, authLevel, _, _, err := store.Users.GetAuthRecord(tenantID, target)
if err != nil {
return nil, err
}
if err = store.Users.UpdateAuthRecord(tenantID, target, authLevel, "", passwordHash, time.Time{}); err != nil {
return nil, err
}
now := time.Now().UTC().Round(time.Millisecond)
authVersion := 0
if store.Security != nil {
version, err := store.Security.BumpAuthVersion(tenantID, target, now)
if err != nil {
return nil, err
}
authVersion = version
}
evictUserSessions(target)
return map[string]any{
"user_id": internalAdminUIDRaw(target),
"action": adminCommandUserPasswordReset,
"auth_version": authVersion,
"evicted": true,
}, nil
}
func evictUserSessions(uid types.Uid) { func evictUserSessions(uid types.Uid) {
if globals.sessionStore != nil { if globals.sessionStore != nil {
globals.sessionStore.EvictUser(uid, "") globals.sessionStore.EvictUser(uid, "")
@@ -575,6 +616,8 @@ func kafkaCommandErrorReason(err error) string {
switch { switch {
case errors.Is(err, types.ErrMalformed): case errors.Is(err, types.ErrMalformed):
return "MALFORMED_COMMAND" return "MALFORMED_COMMAND"
case errors.Is(err, types.ErrPolicy):
return "POLICY_VIOLATION"
case errors.Is(err, types.ErrNotFound): case errors.Is(err, types.ErrNotFound):
return "RESOURCE_NOT_FOUND" return "RESOURCE_NOT_FOUND"
case errors.Is(err, types.ErrPermissionDenied): case errors.Is(err, types.ErrPermissionDenied):