This commit is contained in:
parent
93be3257b7
commit
a901bd19b4
@ -1,12 +1,17 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/db/migration"
|
||||
"git.pcux.cn/xuans/Smail/models"
|
||||
"git.pcux.cn/xuans/Smail/services/access"
|
||||
"git.pcux.cn/xuans/Smail/services/logstore"
|
||||
"git.pcux.cn/xuans/Smail/services/verification"
|
||||
"git.pcux.cn/xuans/Smail/utils/password"
|
||||
)
|
||||
@ -15,6 +20,46 @@ func TestPublicFlowEmailResetAndRoleBoundary(t *testing.T) {
|
||||
for _, role := range []access.Role{access.RoleSuperAdmin, access.RoleAdmin, access.RoleUser} {
|
||||
t.Run(string(role), func(t *testing.T) {
|
||||
s, verify, engine, user := newRecoveryFixture(t)
|
||||
var migrations []migration.Migration
|
||||
for _, item := range migration.DefaultMigrations() {
|
||||
if item.Name == "create_managed_logs" || item.Name == "expand_operation_log_actor_role" {
|
||||
migrations = append(migrations, item)
|
||||
}
|
||||
}
|
||||
runner := migration.Runner{DB: engine.DB().DB, Dialect: migration.DialectSQLite, Migrations: migrations}
|
||||
if _, err := runner.Run(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logs, err := logstore.New(engine.DB().DB, migration.DialectSQLite)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := logstore.Start(logs)
|
||||
defer runtime.Stop()
|
||||
assertAudit := func(want int, secrets ...string) {
|
||||
t.Helper()
|
||||
entries, err := logs.Query(context.Background(), logstore.Query{Kind: "operation"})
|
||||
if err != nil || len(entries) != want {
|
||||
t.Fatalf("恢复审计记录数量不符:数量=%d,预期=%d,错误=%v", len(entries), want, err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.ActorID != strconv.Itoa(user.ID) || entry.ActorRole != string(role) || entry.Action != "公共密码恢复" || entry.Result != "success" {
|
||||
t.Fatal("恢复审计的可信身份或结果不符")
|
||||
}
|
||||
var payload map[string]string
|
||||
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload["method"] != "email" || payload["actorName"] != user.Account || len(payload) != 5 {
|
||||
t.Fatal("恢复审计未使用预期的白名单字段")
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
if secret != "" && strings.Contains(string(entry.Payload), secret) {
|
||||
t.Fatal("恢复审计包含敏感信息")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
user.Role = string(role)
|
||||
if _, err := engine.ID(user.ID).Cols("role").Update(user); err != nil {
|
||||
t.Fatal(err)
|
||||
@ -29,6 +74,7 @@ func TestPublicFlowEmailResetAndRoleBoundary(t *testing.T) {
|
||||
if flow.Token != "" || len(flow.Methods) != 0 || flow.Email != "" {
|
||||
t.Fatal("super administrator received public recovery access")
|
||||
}
|
||||
assertAudit(0)
|
||||
return
|
||||
}
|
||||
if flow.Email != "a***@example.com" || flow.Role != role {
|
||||
@ -62,12 +108,14 @@ func TestPublicFlowEmailResetAndRoleBoundary(t *testing.T) {
|
||||
if err := s.CompletePublicFlow(ticket, "short"); err == nil {
|
||||
t.Fatal("invalid password accepted")
|
||||
}
|
||||
assertAudit(0)
|
||||
if err := s.CompletePublicFlow(ticket, "NewPass123"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.CompletePublicFlow(ticket, "OtherPass12"); err == nil {
|
||||
t.Fatal("ticket replay accepted")
|
||||
}
|
||||
assertAudit(1, flow.Token, ticket, code, "NewPass123", "OtherPass12", "alice@example.com")
|
||||
var updated models.User
|
||||
if _, err := engine.ID(user.ID).Get(&updated); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/models"
|
||||
"git.pcux.cn/xuans/Smail/services/access"
|
||||
"git.pcux.cn/xuans/Smail/services/auth"
|
||||
"git.pcux.cn/xuans/Smail/services/logstore"
|
||||
"git.pcux.cn/xuans/Smail/utils/password"
|
||||
)
|
||||
|
||||
@ -253,6 +257,24 @@ func (s *Service) completePublicLink(token, newPassword, recoveryCode string, fl
|
||||
_ = s.verification.Store.InvalidateAccount(user.Account)
|
||||
}
|
||||
s.invalidateSecurityState(item.UserID, user.Account)
|
||||
method := item.Method
|
||||
if !flow {
|
||||
method = "email_link"
|
||||
if user.EffectiveRole() == access.RoleAdmin {
|
||||
method = "email_link_and_security_code"
|
||||
}
|
||||
}
|
||||
payload, _ := json.Marshal(struct {
|
||||
ActorName string `json:"actorName"`
|
||||
ActorRole string `json:"actorRole"`
|
||||
Method string `json:"method"`
|
||||
Object string `json:"object"`
|
||||
Outcome string `json:"outcome"`
|
||||
}{user.Account, string(user.EffectiveRole()), method, "账号安全", "密码已重置,旧凭据已撤销"})
|
||||
logstore.Record(context.Background(), logstore.Entry{
|
||||
Kind: "operation", CreatedAt: now, ActorID: strconv.Itoa(user.ID), ActorRole: string(user.EffectiveRole()),
|
||||
Action: "公共密码恢复", Section: "账号安全", Result: "success", Payload: payload,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -67,7 +67,7 @@ export function usePage() {
|
||||
const totalPage = ref(0)
|
||||
|
||||
const updateList = async function (silent = false) {
|
||||
if (silent && selectedRows.value.length > 0) return
|
||||
if (silent && selectedRows.value.length > 0) return false
|
||||
const version = ++requestVersion
|
||||
if (!silent) {
|
||||
isRefreshing.value = true
|
||||
@ -76,7 +76,7 @@ export function usePage() {
|
||||
const res = await http.post("/api/v1/email/list", {
|
||||
tag, keyword: groupStore.keyword, current_page: currentPage.value, page_size: 15
|
||||
})
|
||||
if (version !== requestVersion) return
|
||||
if (version !== requestVersion) return false
|
||||
if (res.errorNo !== 0) throw new Error(res.errorMsg)
|
||||
const available = new Set((res.data.list || []).map(row => row.id))
|
||||
selectedRows.value.filter(row => !available.has(row.id)).forEach(row => {
|
||||
@ -86,12 +86,14 @@ export function usePage() {
|
||||
totalPage.value = res.data.total_page || 0
|
||||
if (currentPage.value > Math.max(1, totalPage.value)) {
|
||||
currentPage.value = Math.max(1, totalPage.value)
|
||||
await updateList(silent)
|
||||
return await updateList(silent)
|
||||
}
|
||||
return true
|
||||
} catch (err) {
|
||||
if (!silent && version === requestVersion) {
|
||||
ElMessage.error(err?.message || '获取邮件列表失败')
|
||||
}
|
||||
return false
|
||||
} finally {
|
||||
if (!silent && version === requestVersion) {
|
||||
isRefreshing.value = false
|
||||
@ -106,9 +108,11 @@ export function usePage() {
|
||||
throw new Error(res.errorMsg || "分组列表加载失败")
|
||||
}
|
||||
groupList.value = res.data || []
|
||||
return true
|
||||
} catch (error) {
|
||||
groupList.value = []
|
||||
ElMessage.error(error?.message || "分组列表加载失败,请稍后重试")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,10 +143,11 @@ export function usePage() {
|
||||
}
|
||||
|
||||
const manualRefresh = async () => {
|
||||
await Promise.all([
|
||||
const results = await Promise.all([
|
||||
updateList(false),
|
||||
updateGroupList()
|
||||
])
|
||||
if (!globalStatus.isLogin || !results.every(result => result === true)) return
|
||||
ElMessage.success('已刷新')
|
||||
startAutoRefreshTimer()
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user