安全码计时复审前保存权限回归修正
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-10-06 07:36:46 +08:00
parent 7401d96a17
commit 7e007ea891
4 changed files with 51 additions and 13 deletions

View File

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

View File

@ -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 {

View File

@ -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("重新开启协议后有效授权码未恢复")
}

View File

@ -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)
}
}