From 7e007ea8919122c0d2e94dbc30e585258e7d30e0 Mon Sep 17 00:00:00 2001 From: smail Date: Tue, 6 Oct 2026 07:36:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=89=E5=85=A8=E7=A0=81=E8=AE=A1=E6=97=B6?= =?UTF-8?q?=E5=A4=8D=E5=AE=A1=E5=89=8D=E4=BF=9D=E5=AD=98=E6=9D=83=E9=99=90?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/本轮上线审查清单.md | 27 ++++++++++++++++++++++++++ test/backend/plugin_role_scope_test.go | 18 ++++++++++++----- test/backend/protocol_access_test.go | 10 +++++----- test/backend/role_scope_test.go | 9 ++++++--- 4 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 docs/本轮上线审查清单.md diff --git a/docs/本轮上线审查清单.md b/docs/本轮上线审查清单.md new file mode 100644 index 0000000..29b4882 --- /dev/null +++ b/docs/本轮上线审查清单.md @@ -0,0 +1,27 @@ +# 本轮上线审查清单 + +## 需求确认 + +本轮接续全项目 Linux 实机审查。当前工作区、运行状态和实际验收证据优先于旧交接记录。禁止使用 Python,不将内部接口或辅助业务脚本结果冒充网页实机验收;账号、密码、验证码和云凭据不得写入本文或提交。 + +## 任务状态 + +- [✓] 核对工作区及分支,保留原有未跟踪产物。 +- [✓] 修改前提交中文检查点,推送认证失败后重试成功。 +- [✓] 使用密码及严格主机密钥校验连接目标服务器。 +- [~] 修复旧回归测试与当前权限、协议密码验证、分组表结构不一致的问题,不放宽生产授权。 +- [ ] 执行后端全量回归,核对前端与部署回归。 +- [ ] 核对运行服务、监听、部署摘要及数据库角色认证。 +- [ ] 根据构建结果进行 Linux 全新部署;清理限于已确认属于 Smail 的旧数据和备份。 +- [ ] 页面完成三角色登录、权限、分组、中继开关与配置管理回归。 +- [ ] 页面实测安全码显示时限与关闭清理,不以常量检查替代计时。 +- [ ] 三个外部邮箱由用户手动填写验证码并完成绑定。 +- [ ] 通过实际邮件客户端和外部系统验收收发、认证、撤权、TLS 与证书维护。 +- [ ] 清理测试残留并确认唯一有效恢复方式仍可用。 +- [ ] 完成全部要求逐项核对后再放行上线。 + +## 已确认的证据与边界 + +本轮复现三个后端测试失败。协议操作用例未提供现行入口要求的本人密码;插件用例将超管专属通知设置误作普通插件设置;用户列表用例缺少账号分组表,并沿用未分组前的可见范围。修复测试时保留拒绝越权、跨账号隔离与分页断言。以上为本地回归,不代表线上验收通过。 + +前一轮前端回归 235 项通过且生产构建成功,但尚不能据此认定服务器已运行该构建,或安全码在浏览器中完成了完整计时验收。 diff --git a/test/backend/plugin_role_scope_test.go b/test/backend/plugin_role_scope_test.go index 50fff39..2f975d0 100644 --- a/test/backend/plugin_role_scope_test.go +++ b/test/backend/plugin_role_scope_test.go @@ -13,7 +13,13 @@ import ( func TestPluginRolesValidateUpdateAndPending(t *testing.T) { root := t.TempDir() - writeRuntimeFixture(t, root, validRuntimeConfig()) + fixture := validRuntimeConfig() + fixture.Plugins.Plugins["spam_block"] = PluginInstance{ + Executable: currentPluginExecutable("spam_block"), + Permissions: PluginPermissions{Capabilities: []string{"network"}}, + Settings: json.RawMessage("{\"score\":1}"), + } + writeRuntimeFixture(t, root, fixture) manager, err := NewConfigManagerWithSecurity(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}}, ConfigSecurity{Audit: &MemoryAuditWriter{}}) if err != nil { t.Fatal(err) @@ -21,7 +27,7 @@ func TestPluginRolesValidateUpdateAndPending(t *testing.T) { ctx := context.Background() admin := AuditActor{UserID: "2", Role: "admin", SessionID: "admin-session", RecentAuth: true, RecoveryCodeConfigured: true} current := manager.Snapshot() - for _, change := range []string{"executable", "permissions", "install", "remove"} { + for _, change := range []string{"executable", "permissions", "install", "remove", "settings"} { candidate := cloneRuntimeConfig(current) plugin := candidate.Plugins.Plugins["telegram_push"] switch change { @@ -31,6 +37,8 @@ func TestPluginRolesValidateUpdateAndPending(t *testing.T) { plugin.Permissions.Network[0].Host = "other.example.com" case "install": candidate.Plugins.Plugins["other"] = plugin + case "settings": + plugin.Settings = json.RawMessage("{\"timeout\":30}") } candidate.Plugins.Plugins["telegram_push"] = plugin if change == "remove" { @@ -44,10 +52,10 @@ func TestPluginRolesValidateUpdateAndPending(t *testing.T) { t.Fatalf("保存允许越权 %s:%v", change, err) } } - plugins := current.Plugins - plugin := plugins.Plugins["telegram_push"] + plugins := cloneRuntimeConfig(current).Plugins + plugin := plugins.Plugins["spam_block"] plugin.Settings = json.RawMessage(`{"displayName":"修改通知名称"}`) - plugins.Plugins["telegram_push"] = plugin + plugins.Plugins["spam_block"] = plugin payload, _ := json.Marshal(plugins) validation, err := manager.ValidateAuthorized(ctx, ValidateRequest{Section: SectionPlugins, ExpectedGeneration: current.Generation, Payload: payload}, admin) if err != nil || !validation.Valid { diff --git a/test/backend/protocol_access_test.go b/test/backend/protocol_access_test.go index 915a07a..22c375a 100644 --- a/test/backend/protocol_access_test.go +++ b/test/backend/protocol_access_test.go @@ -102,12 +102,12 @@ func TestProtocolClientHandlersIsolateEveryRole(t *testing.T) { } } for _, protocol := range credentials.SupportedProtocols() { - body := fmt.Sprintf(`{"protocol":%q}`, protocol) - malicious := fmt.Sprintf(`{"protocol":%q,"enabled":false,"user_id":%d}`, protocol, victim.ID) + body := fmt.Sprintf(`{"protocol":%q,"password":"WebOnly42"}`, protocol) + malicious := fmt.Sprintf(`{"protocol":%q,"enabled":false,"user_id":%d,"password":"WebOnly42"}`, protocol, victim.ID) if result := call(SetProtocolCredentialEnabled, http.MethodPut, malicious); result.ErrorNo == 0 { t.Fatal("接口接受了未知目标用户字段") } - mustSucceed(call(SetProtocolCredentialEnabled, http.MethodPut, fmt.Sprintf(`{"protocol":%q,"enabled":true}`, protocol))) + mustSucceed(call(SetProtocolCredentialEnabled, http.MethodPut, fmt.Sprintf(`{"protocol":%q,"enabled":true,"password":"WebOnly42"}`, protocol))) generated := call(GenerateProtocolCredential, http.MethodPost, body) mustSucceed(generated) data, ok := generated.Data.(map[string]any) @@ -139,11 +139,11 @@ func TestProtocolClientHandlersIsolateEveryRole(t *testing.T) { if err != nil || strings.Contains(string(encoded), code) { t.Fatal("状态查询重新返回了授权码明文") } - mustSucceed(call(SetProtocolCredentialEnabled, http.MethodPut, fmt.Sprintf(`{"protocol":%q,"enabled":false}`, protocol))) + mustSucceed(call(SetProtocolCredentialEnabled, http.MethodPut, fmt.Sprintf(`{"protocol":%q,"enabled":false,"password":"WebOnly42"}`, protocol))) if login(code) { t.Fatal("关闭协议后实际认证入口仍放行") } - mustSucceed(call(SetProtocolCredentialEnabled, http.MethodPut, fmt.Sprintf(`{"protocol":%q,"enabled":true}`, protocol))) + mustSucceed(call(SetProtocolCredentialEnabled, http.MethodPut, fmt.Sprintf(`{"protocol":%q,"enabled":true,"password":"WebOnly42"}`, protocol))) if !login(code) { t.Fatal("重新开启协议后有效授权码未恢复") } diff --git a/test/backend/role_scope_test.go b/test/backend/role_scope_test.go index 5fa9fd4..e150d4a 100644 --- a/test/backend/role_scope_test.go +++ b/test/backend/role_scope_test.go @@ -30,7 +30,7 @@ func TestAccountRoleScopeAndOwnProfile(t *testing.T) { previous := db.Instance db.Instance = engine t.Cleanup(func() { db.Instance = previous; _ = engine.Close() }) - if err := engine.Sync2(&models.User{}, &models.RecoveryCode{}, &models.ExternalEmail{}); err != nil { + if err := engine.Sync2(&models.User{}, &models.RecoveryCode{}, &models.ExternalEmail{}, &models.AccountGroup{}, &models.AccountGroupMember{}); err != nil { t.Fatal(err) } users := []models.User{ @@ -46,6 +46,9 @@ func TestAccountRoleScopeAndOwnProfile(t *testing.T) { t.Fatal(err) } } + if _, err := engine.Insert(&models.AccountGroup{ID: 1, Name: "隔离分组"}, &models.AccountGroupMember{UserID: users[2].ID, GroupID: 1}); err != nil { + t.Fatal(err) + } admin := &appcontext.Context{UserID: users[1].ID, Role: access.RoleAdmin} ordinary := &appcontext.Context{UserID: users[3].ID, Role: access.RoleUser} call := func(ctx *appcontext.Context, handler func(*appcontext.Context, http.ResponseWriter, *http.Request), body string) response.Response { @@ -63,7 +66,7 @@ func TestAccountRoleScopeAndOwnProfile(t *testing.T) { t.Fatalf("低权限创建账号:%+v", got) } } - for page, expected := range []string{"alice", "legacy"} { + for page, expected := range []string{"manager", "alice", "legacy"} { body, _ := json.Marshal(map[string]int{"current_page": page + 1, "page_size": 1}) got := call(admin, UserList, string(body)) if got.ErrorNo != 0 { @@ -71,7 +74,7 @@ func TestAccountRoleScopeAndOwnProfile(t *testing.T) { } data := got.Data.(map[string]any) items := data["list"].([]any) - if data["total_page"] != float64(2) || len(items) != 1 || items[0].(map[string]any)["Account"] != expected { + if data["total_page"] != float64(3) || len(items) != 1 || items[0].(map[string]any)["Account"] != expected { t.Fatalf("分页前权限过滤错误:%+v", data) } }