This commit is contained in:
parent
7401d96a17
commit
7e007ea891
27
docs/本轮上线审查清单.md
Normal file
27
docs/本轮上线审查清单.md
Normal file
@ -0,0 +1,27 @@
|
||||
# 本轮上线审查清单
|
||||
|
||||
## 需求确认
|
||||
|
||||
本轮接续全项目 Linux 实机审查。当前工作区、运行状态和实际验收证据优先于旧交接记录。禁止使用 Python,不将内部接口或辅助业务脚本结果冒充网页实机验收;账号、密码、验证码和云凭据不得写入本文或提交。
|
||||
|
||||
## 任务状态
|
||||
|
||||
- [✓] 核对工作区及分支,保留原有未跟踪产物。
|
||||
- [✓] 修改前提交中文检查点,推送认证失败后重试成功。
|
||||
- [✓] 使用密码及严格主机密钥校验连接目标服务器。
|
||||
- [~] 修复旧回归测试与当前权限、协议密码验证、分组表结构不一致的问题,不放宽生产授权。
|
||||
- [ ] 执行后端全量回归,核对前端与部署回归。
|
||||
- [ ] 核对运行服务、监听、部署摘要及数据库角色认证。
|
||||
- [ ] 根据构建结果进行 Linux 全新部署;清理限于已确认属于 Smail 的旧数据和备份。
|
||||
- [ ] 页面完成三角色登录、权限、分组、中继开关与配置管理回归。
|
||||
- [ ] 页面实测安全码显示时限与关闭清理,不以常量检查替代计时。
|
||||
- [ ] 三个外部邮箱由用户手动填写验证码并完成绑定。
|
||||
- [ ] 通过实际邮件客户端和外部系统验收收发、认证、撤权、TLS 与证书维护。
|
||||
- [ ] 清理测试残留并确认唯一有效恢复方式仍可用。
|
||||
- [ ] 完成全部要求逐项核对后再放行上线。
|
||||
|
||||
## 已确认的证据与边界
|
||||
|
||||
本轮复现三个后端测试失败。协议操作用例未提供现行入口要求的本人密码;插件用例将超管专属通知设置误作普通插件设置;用户列表用例缺少账号分组表,并沿用未分组前的可见范围。修复测试时保留拒绝越权、跨账号隔离与分页断言。以上为本地回归,不代表线上验收通过。
|
||||
|
||||
前一轮前端回归 235 项通过且生产构建成功,但尚不能据此认定服务器已运行该构建,或安全码在浏览器中完成了完整计时验收。
|
||||
@ -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 {
|
||||
|
||||
@ -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("重新开启协议后有效授权码未恢复")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user