补齐成员接口安全边界回归并同步真实整改进度
This commit is contained in:
parent
3857dac5c8
commit
9f83c63257
129
.claude/docs/ai/project-members/api-handoff.md
Normal file
129
.claude/docs/ai/project-members/api-handoff.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# Scert 项目成员接口交接
|
||||||
|
|
||||||
|
## 业务范围与交付边界
|
||||||
|
|
||||||
|
项目成员关系用于管理同一项目内的六类角色,与用户分组独立。普通用户可以担任项目管理员,普通全局管理员不因登录角色或同组身份自动取得项目权限。接口允许当前有效超级管理员或目标项目的项目管理员管理成员。
|
||||||
|
|
||||||
|
本批提供成员列表、授予与撤销接口,不提供项目列表/创建入口或成员页面,也不代表域名、证书、服务器、DNS 等全部业务资源已经接入项目授权。源码专项、实库与构建证据分别记录于《构建版本与数据库支持矩阵》第二十三节,不据此声明生产验收通过。
|
||||||
|
|
||||||
|
## 通用约定
|
||||||
|
|
||||||
|
- 前缀:`/api/v1/projects/:projectID/members`,路径必须使用实际项目标识。
|
||||||
|
- 使用正式登录接口建立的会话 Cookie;浏览器请求同源发送。服务器验证提供的 `Origin`/`Referer`,来源拒绝返回 403。
|
||||||
|
- 不传 `actor_id`、全局角色或项目权限;操作者仅取本次服务端验证的会话,仓储执行时重新核对账户状态和成员关系。
|
||||||
|
- 项目、用户及游标输入仅允许 1~128 位 ASCII 字母、数字、连字符、下划线;空游标例外。已有项目实体本身仍遵守其更严格的命名规则,此接口不创建项目。
|
||||||
|
- 响应头 `X-Scert-Request-ID` 可用于关联诊断。错误响应为 `{"error":"中文提示","request_id":"请求编号"}`,不能仅按文本匹配决定权限。
|
||||||
|
- 成功授权/撤销和成功审计在同一事务提交;失败时不得乐观更新界面。成功变更后应重新获取第一页,清空旧游标栈。
|
||||||
|
|
||||||
|
## 接口
|
||||||
|
|
||||||
|
### GET:读取成员列表
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/projects/default/members?page_size=10&after=user-001
|
||||||
|
```
|
||||||
|
|
||||||
|
| 查询参数 | 类型 | 约束 |
|
||||||
|
|---|---|---|
|
||||||
|
| `page_size` | 整数 | 默认 10;只接受 5、10、15、30 |
|
||||||
|
| `after` | 字符串 | 默认空,从第一页读取;下一页使用上一响应的 `next_cursor` |
|
||||||
|
|
||||||
|
重复参数、未知参数、非法转义、无效页大小或游标返回 400。没有页码或总人数参数。
|
||||||
|
|
||||||
|
成功返回 200:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"user_id": "user-002",
|
||||||
|
"username": "测试成员",
|
||||||
|
"status": "active",
|
||||||
|
"roles": ["dns_admin", "read_only"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page_size": 10,
|
||||||
|
"has_more": false,
|
||||||
|
"next_cursor": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 按用户 ID 升序的游标分页,不按角色关系计数,同一账户的角色合并到一个成员项中。
|
||||||
|
- `items` 始终为数组;合法空项目和末尾空页为 `[]`,不是未授权。`has_more=false` 时 `next_cursor` 为空。
|
||||||
|
- 列表包含仍有成员关系的停用账户,便于撤销其角色;停用账户自身无法操作接口。
|
||||||
|
- 不返回邮箱、密码摘要、用户分组或全局角色。角色顺序由接口稳定排序,界面应按角色值映射中文名称。
|
||||||
|
- 超级管理员不隐式出现在成员列表中;只有显式成员关系才列出。空默认项目不会自动增加成员。
|
||||||
|
- 游标不是快照。翻页期间发生授权变更时,应允许用户刷新并回到第一页,不承诺跨请求一致性。
|
||||||
|
|
||||||
|
### POST:授予角色
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/projects/default/members
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "user-002",
|
||||||
|
"role": "dns_admin"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 目标必须为真实有效账户,不能凭用户名或邮箱替代 `user_id`。
|
||||||
|
- 正文只接受 `user_id`、`role`,拒绝未知字段、多段 JSON 和超过 64 KiB 的正文;不接受查询参数。
|
||||||
|
- 成功为 201:`{"message":"项目角色已授予"}`。
|
||||||
|
- 已存在相同项目/账户/角色关系返回 409,不生成重复关系;其他角色可另行授予。
|
||||||
|
- 同一账户可在多个项目持有多个角色,但该规则不改变普通用户只能属于一个用户分组的限制。
|
||||||
|
|
||||||
|
### DELETE:撤销角色
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /api/v1/projects/default/members/user-002/roles/dns_admin
|
||||||
|
```
|
||||||
|
|
||||||
|
- 不接受请求体或查询参数,空白请求体也拒绝。不能使用 DELETE 正文冒充操作者。
|
||||||
|
- 成功为 204,无响应体;客户端不要尝试解析成功响应 JSON。
|
||||||
|
- 不存在的目标关系与权限不足均返回 403,不作为幂等成功。
|
||||||
|
- 允许项目管理员撤销自己的项目管理员角色。成功后应退出成员管理状态或处理重新读取产生的 403,不能重试为自己恢复权限;有效超级管理员仍可恢复授权。
|
||||||
|
|
||||||
|
## 角色枚举与显示名称
|
||||||
|
|
||||||
|
| 值 | 中文名称 |
|
||||||
|
|---|---|
|
||||||
|
| `project_admin` | 项目管理员 |
|
||||||
|
| `certificate_admin` | 证书管理员 |
|
||||||
|
| `deployment_admin` | 部署管理员 |
|
||||||
|
| `dns_admin` | DNS 管理员 |
|
||||||
|
| `audit_viewer` | 审计查看者 |
|
||||||
|
| `read_only` | 只读用户 |
|
||||||
|
|
||||||
|
`super_admin` 不是可授予的项目角色。角色能力矩阵见《项目资源关联模型》9.1 节;不能因为名称相近就授予未接入资源的权限。普通账户不能读取完整私钥的上限不因项目管理员角色而解除。
|
||||||
|
|
||||||
|
## 错误与审计边界
|
||||||
|
|
||||||
|
| HTTP 状态 | 含义与界面处理 |
|
||||||
|
|---|---|
|
||||||
|
| 400 | 输入无效;显示中文提示,不重试相同请求 |
|
||||||
|
| 401 | 未登录、会话失效或账户已停用;重新认证 |
|
||||||
|
| 403 | 来源拒绝、无项目权限、项目不存在、目标账户/关系不满足条件;不区分这些情况枚举他人资源 |
|
||||||
|
| 409 | 项目成员关系已存在,或系统尚未初始化;以当前流程和错误响应区分,不当作成功 |
|
||||||
|
| 500 | 操作或事务审计失败;不展示底层数据库错误,不乐观认为授权已生效 |
|
||||||
|
| 503 | 成员服务未接入或不可用;禁止将空白页面解释为没有成员 |
|
||||||
|
|
||||||
|
会话验证后、仓储执行前发生撤权或停用仍会拒绝。失败审计保留本次已验证身份;未认证请求不归到客户端声明的账户。失败审计独立于回滚事务,记录固定动作、HTTP 状态、目标及请求编号,不保存请求正文或任意底层错误。请求取消后审计仍有独立的三秒上限;审计存储自身失败时只写稳定服务器诊断日志,原失败响应保持不变,不能承诺事件已持久化。
|
||||||
|
|
||||||
|
## 前端集成与验收场景
|
||||||
|
|
||||||
|
1. 按账户分页并显示中文角色标签;用户可选择 5、10、15、30 条,切换页大小回到第一页。
|
||||||
|
2. 保存各页起始游标用于上一页;不将 `next_cursor` 当作页码,不伪造总人数。
|
||||||
|
3. 授权提交后等待 201,再刷新列表;重复授权 409 显示冲突并刷新,不生成本地重复项。
|
||||||
|
4. 撤销前确认目标与角色,自我撤权单独提示;204 后不解析 JSON,失去权限时停止继续操作。
|
||||||
|
5. 分别验证普通全局管理员、只读项目成员、其他项目管理员均无法管理本项目成员。
|
||||||
|
6. 测试空列表、末页、停用成员、多角色账户和翻页期间关系变更;不显示邮箱或其他未返回字段。
|
||||||
|
7. 401/403/500/503 分别显示正确状态,不能统一伪装为空列表;诊断信息展示请求编号而非数据库原文。
|
||||||
|
|
||||||
|
## 待完成
|
||||||
|
|
||||||
|
- [ ] 项目列表/创建及入口选择、授权目标账户选择、成员页面与浏览器验收。
|
||||||
|
- [ ] 全部业务资源与排队任务执行/取消/重试的权限重判。
|
||||||
|
- [ ] PostgreSQL 当前实库、Linux 原生及完整生产部署验收;已通过的局部测试不替代这些门禁。
|
||||||
158
backend/internal/httpapi/project_members_boundary_test.go
Normal file
158
backend/internal/httpapi/project_members_boundary_test.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||||||
|
"git.pcux.cn/xuans/Scert/backend/internal/logging"
|
||||||
|
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 在会话验证之后、真实仓储执行之前改变权限,避免仅验证顺序请求的撤权场景。
|
||||||
|
type projectMemberBoundaryRepository struct {
|
||||||
|
repository.ProjectMemberRepository
|
||||||
|
before func()
|
||||||
|
audit func(context.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r projectMemberBoundaryRepository) ListProjectMembers(ctx context.Context, actor, project, after string, size int) (repository.ProjectMemberPage, error) {
|
||||||
|
r.before()
|
||||||
|
return r.ProjectMemberRepository.ListProjectMembers(ctx, actor, project, after, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r projectMemberBoundaryRepository) GrantProjectRole(ctx context.Context, actor, project, user string, role domain.ProjectRole) error {
|
||||||
|
r.before()
|
||||||
|
return r.ProjectMemberRepository.GrantProjectRole(ctx, actor, project, user, role)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r projectMemberBoundaryRepository) RevokeProjectRole(ctx context.Context, actor, project, user string, role domain.ProjectRole) error {
|
||||||
|
r.before()
|
||||||
|
return r.ProjectMemberRepository.RevokeProjectRole(ctx, actor, project, user, role)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r projectMemberBoundaryRepository) RecordProjectMemberFailure(ctx context.Context, failure repository.ProjectMemberFailure) error {
|
||||||
|
if r.audit != nil {
|
||||||
|
r.audit(ctx)
|
||||||
|
}
|
||||||
|
return r.ProjectMemberRepository.RecordProjectMemberFailure(ctx, failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProjectMemberAPIDeleteRejectsPayload(t *testing.T) {
|
||||||
|
f := newProjectMemberAPIFixture(t)
|
||||||
|
path := memberTestPath + "/manager/roles/project_admin"
|
||||||
|
for _, tc := range []struct{ name, suffix, body string }{
|
||||||
|
{"json", "", `{"actor_id":"primary-super-admin"}`},
|
||||||
|
{"whitespace", "", " "},
|
||||||
|
{"large_body", "", strings.Repeat("x", 70*1024)},
|
||||||
|
{"query", "?actor_id=primary-super-admin", ""},
|
||||||
|
{"unknown_query", "?ignored=1", ""},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
response := serveUserRequest(f.server, f.cookies["manager"], http.MethodDelete, path+tc.suffix, tc.body)
|
||||||
|
log := f.lastAudit(t)
|
||||||
|
if response.Code != http.StatusBadRequest || log.Status != "failed" || log.OperatorID != "manager" {
|
||||||
|
t.Fatalf("撤销角色未拒绝额外输入或审计身份错误:%d,%+v", response.Code, log)
|
||||||
|
}
|
||||||
|
if _, err := f.members.ListProjectMembers(t.Context(), "manager", "project-a", "", 5); err != nil {
|
||||||
|
t.Fatalf("拒绝的请求仍撤销了角色:%v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProjectMemberAPIRechecksAfterAuthentication(t *testing.T) {
|
||||||
|
for _, change := range []string{"revoke", "disable"} {
|
||||||
|
for _, method := range []string{http.MethodGet, http.MethodPost, http.MethodDelete} {
|
||||||
|
t.Run(change+"/"+method, func(t *testing.T) {
|
||||||
|
f := newProjectMemberAPIFixture(t)
|
||||||
|
called := false
|
||||||
|
f.server.projectMembers = projectMemberBoundaryRepository{
|
||||||
|
ProjectMemberRepository: f.members,
|
||||||
|
before: func() {
|
||||||
|
called = true
|
||||||
|
query := "DELETE FROM project_members WHERE user_id='manager'"
|
||||||
|
if change == "disable" {
|
||||||
|
query = "UPDATE users SET status='disabled' WHERE id='manager'"
|
||||||
|
}
|
||||||
|
if _, err := f.db.ExecContext(t.Context(), query); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
path, body := memberTestPath, ""
|
||||||
|
if method == http.MethodPost {
|
||||||
|
body = `{"user_id":"target","role":"dns_admin"}`
|
||||||
|
} else if method == http.MethodDelete {
|
||||||
|
path += "/viewer/roles/read_only"
|
||||||
|
}
|
||||||
|
response := serveUserRequest(f.server, f.cookies["manager"], method, path, body)
|
||||||
|
log := f.lastAudit(t)
|
||||||
|
if !called || response.Code != http.StatusForbidden || log.OperatorID != "manager" || log.Status != "failed" {
|
||||||
|
t.Fatalf("会话验证后的撤权未拒绝,或丢失已验证身份:%t,%d,%+v", called, response.Code, log)
|
||||||
|
}
|
||||||
|
var target, viewer int
|
||||||
|
if err := f.db.QueryRowContext(t.Context(), "SELECT COUNT(*) FROM project_members WHERE user_id='target'").Scan(&target); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := f.db.QueryRowContext(t.Context(), "SELECT COUNT(*) FROM project_members WHERE user_id='viewer'").Scan(&viewer); err != nil || target != 0 || viewer != 1 {
|
||||||
|
t.Fatalf("撤权后仍修改了成员关系:%d,%d,%v", target, viewer, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProjectMemberAPICanceledRequestKeepsBoundedFailureAudit(t *testing.T) {
|
||||||
|
f := newProjectMemberAPIFixture(t)
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
defer cancel()
|
||||||
|
audited := false
|
||||||
|
f.server.projectMembers = projectMemberBoundaryRepository{
|
||||||
|
ProjectMemberRepository: f.members,
|
||||||
|
before: cancel,
|
||||||
|
audit: func(auditContext context.Context) {
|
||||||
|
audited = true
|
||||||
|
deadline, ok := auditContext.Deadline()
|
||||||
|
if ctx.Err() == nil || auditContext.Err() != nil || !ok || time.Until(deadline) <= 0 || time.Until(deadline) > 3*time.Second {
|
||||||
|
t.Fatalf("失败审计未脱离取消请求或等待无界:%v,%v,%v", ctx.Err(), auditContext.Err(), deadline)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, memberTestPath, nil).WithContext(ctx)
|
||||||
|
request.AddCookie(f.cookies["manager"])
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
f.server.Handler().ServeHTTP(response, request)
|
||||||
|
log := f.lastAudit(t)
|
||||||
|
if !audited || response.Code != http.StatusInternalServerError || log.Status != "failed" || log.OperatorID != "manager" {
|
||||||
|
t.Fatalf("取消请求导致失败审计丢失:%t,%d,%+v", audited, response.Code, log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProjectMemberAPIFailureAuditStorageUnavailable(t *testing.T) {
|
||||||
|
f := newProjectMemberAPIFixture(t)
|
||||||
|
var output bytes.Buffer
|
||||||
|
f.server.logger = &logging.Logger{Logger: slog.New(slog.NewJSONHandler(&output, nil))}
|
||||||
|
previous := f.lastAudit(t)
|
||||||
|
if _, err := f.db.ExecContext(t.Context(), `CREATE TRIGGER reject_failed_member_audit BEFORE INSERT ON audit_logs
|
||||||
|
WHEN NEW.resource_type='project_member' AND NEW.status='failed'
|
||||||
|
BEGIN SELECT RAISE(ABORT, 'sensitive-audit-storage-marker'); END`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
response := serveUserRequest(f.server, f.cookies["viewer"], http.MethodGet, memberTestPath, "")
|
||||||
|
if response.Code != http.StatusForbidden || f.lastAudit(t).ID != previous.ID {
|
||||||
|
t.Fatalf("审计存储失败改变了原拒绝响应或伪造了审计:%d", response.Code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output.String(), "项目成员失败审计未保存") || !strings.Contains(output.String(), response.Header().Get(requestIDHeader)) {
|
||||||
|
t.Fatalf("审计存储失败没有稳定可关联日志:%s", output.String())
|
||||||
|
}
|
||||||
|
if strings.Contains(output.String()+response.Body.String(), "sensitive-audit-storage-marker") {
|
||||||
|
t.Fatal("审计存储失败泄露底层错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
47
backend/internal/repository/project_member_errors_test.go
Normal file
47
backend/internal/repository/project_member_errors_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
type memberSQLiteCodeError int
|
||||||
|
|
||||||
|
func (e memberSQLiteCodeError) Error() string { return "测试数据库错误" }
|
||||||
|
func (e memberSQLiteCodeError) Code() int { return int(e) }
|
||||||
|
|
||||||
|
func TestProjectMemberUniqueViolationClassification(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"nil", nil, false},
|
||||||
|
{"untyped_constraint", errors.New("constraint failed"), false},
|
||||||
|
{"sqlite_primary", memberSQLiteCodeError(1555), true},
|
||||||
|
{"sqlite_unique", memberSQLiteCodeError(2067), true},
|
||||||
|
{"sqlite_trigger", memberSQLiteCodeError(1811), false},
|
||||||
|
{"sqlite_foreign_key", memberSQLiteCodeError(787), false},
|
||||||
|
{"sqlite_check", memberSQLiteCodeError(275), false},
|
||||||
|
{"sqlite_not_null", memberSQLiteCodeError(1299), false},
|
||||||
|
{"mysql_unique", &mysql.MySQLError{Number: 1062}, true},
|
||||||
|
{"mysql_trigger", &mysql.MySQLError{Number: 1644}, false},
|
||||||
|
{"mysql_foreign_key", &mysql.MySQLError{Number: 1452}, false},
|
||||||
|
{"postgres_unique", &pgconn.PgError{Code: "23505"}, true},
|
||||||
|
{"postgres_foreign_key", &pgconn.PgError{Code: "23503"}, false},
|
||||||
|
{"postgres_trigger", &pgconn.PgError{Code: "P0001"}, false},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := projectMemberUniqueViolation(tc.err); got != tc.want {
|
||||||
|
t.Fatalf("成员冲突错误分类不准确:%t,期望 %t", got, tc.want)
|
||||||
|
}
|
||||||
|
if tc.err != nil && projectMemberUniqueViolation(fmt.Errorf("包装错误:%w", tc.err)) != tc.want {
|
||||||
|
t.Fatal("包装错误后的分类不一致")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
# Scert 开发任务清单
|
# Scert 开发任务清单
|
||||||
|
|
||||||
|
> 最新批次(2026-09-28 19:10):成员列表账户分页、授予/撤销接口、应用接线与失败审计已实现;修复审计错误误报 409,并补充验证会话认证后撤权/停用、请求取消、审计存储故障和 DELETE 输入拒绝。当前专项、双版本 MySQL 三轮、完整后端 1679 个叶子与双平台构建通过;成员三轮竞态 186 个叶子通过、9 条外部 MySQL 条件跳过,退出码 0。详细证据见支持矩阵第二十三节。成员页面、项目入口及完整资源/任务授权仍待完成,原始 44 项总状态不升级。下方各批次均为历史记录。
|
||||||
|
|
||||||
> 当前批次(2026-09-28):成功成员变更与审计同事务写入已实现,审计失败回滚;源码检查点不是正式发布。SQLite 候选为 v1.59.0,最后源码取消查询/WAL 专项、完整后端、双版本 MySQL 三轮组合及双平台构建通过;原 MySQL 索引异常根因未关闭。最新任务状态、最终竞态和证据见执行清单首节与支持矩阵第二十二节,不复用旧批次数字。下一开发重点为成员 Web/API、分页、失败审计、业务资源入口与排队任务重新判权。
|
> 当前批次(2026-09-28):成功成员变更与审计同事务写入已实现,审计失败回滚;源码检查点不是正式发布。SQLite 候选为 v1.59.0,最后源码取消查询/WAL 专项、完整后端、双版本 MySQL 三轮组合及双平台构建通过;原 MySQL 索引异常根因未关闭。最新任务状态、最终竞态和证据见执行清单首节与支持矩阵第二十二节,不复用旧批次数字。下一开发重点为成员 Web/API、分页、失败审计、业务资源入口与排队任务重新判权。
|
||||||
|
|
||||||
> 当前批次(模式 14):项目成员多角色、实时判权、同语句授予/撤销及三库迁移已实现,SQLite/MySQL 双版本与独立 Windows 程序成员备份恢复通过。后续仍需成员管理 Web/API 与审计、全部资源入口和排队任务重判;本批基础模块不等于业务隔离完成。最新证据见执行清单首节与支持矩阵第二十一节。原始 44 项不缩减,P0-05/P0-06/P0-07 继续 `[~]`;下方为历史记录。
|
> 当前批次(模式 14):项目成员多角色、实时判权、同语句授予/撤销及三库迁移已实现,SQLite/MySQL 双版本与独立 Windows 程序成员备份恢复通过。后续仍需成员管理 Web/API 与审计、全部资源入口和排队任务重判;本批基础模块不等于业务隔离完成。最新证据见执行清单首节与支持矩阵第二十一节。原始 44 项不缩减,P0-05/P0-06/P0-07 继续 `[~]`;下方为历史记录。
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
# Scert 整改任务执行清单
|
# Scert 整改任务执行清单
|
||||||
|
|
||||||
## 2026 年 9 月 28 日:项目成员接口与分页(18:55 同步,修复待复测)
|
## 2026 年 9 月 28 日:项目成员接口与分页(19:10 同步,本批回归通过,等待复审)
|
||||||
|
|
||||||
本批于 18:42 进入实施,接续 `bf10eb5`,不再重复数据库驱动选型;原始 44 项及验收标准不缩减。源码修改前检查点 `4618670` 已提交;新增接口、审计与修复仍未提交。本次仅同步文档,不修改代码或重新执行测试。
|
本批于 18:42 进入实施,接续 `bf10eb5`,不再重复数据库驱动选型;原始 44 项及验收标准不缩减。源码修改前检查点为 `4618670`,接口实现与修复候选已随 `3857dac` 提交推送,随后增加正式边界测试并重跑回归。不清理业务库,不新增辅助验收脚本。
|
||||||
|
|
||||||
- [x] 确认需求:接入现有成员事务仓储,操作者只取服务端验证的会话;用户分组和全局管理员角色不隐式授予项目权限。
|
- [x] 确认需求:接入现有成员事务仓储,操作者只取服务端验证的会话;用户分组和全局管理员角色不隐式授予项目权限。
|
||||||
- [~] 增加成员列表分页与授予/撤销接口;同一用户的多个项目角色应合并显示,列表只返回必要身份字段,不返回邮箱、密码摘要或分组。
|
- [x] 增加成员列表分页与授予/撤销接口;同一用户的多个项目角色合并显示,列表不返回邮箱、密码摘要或分组,应用实际接线测试通过。
|
||||||
- [~] 成员查询在读取数据的 SQL 内判权,拒绝跨项目、撤权后访问和停用账户;空列表不能与未授权混淆。实现与用例已加入,最终源码待复测。
|
- [x] 成员查询在读取数据的 SQL 内判权;专项验证跨项目、会话验证后撤权/停用均拒绝,合法空列表与未授权区分处理。
|
||||||
- [~] 成功变更沿用事务审计,失败请求补充有界审计;未验证身份不得归给请求提供的账户,不记录原始请求体或底层数据库错误。实现已加入,失败边界继续验证。
|
- [x] 成功变更沿用事务审计,失败请求补充有界审计;验证取消请求仍记录失败、审计存储故障不伪报持久化,不记录请求正文或原始数据库错误。
|
||||||
- [~] 正式仓储、HTTP 和应用接线测试已加入。首轮 `focused-first.jsonl` 于 18:49 记录审计失败回滚用例期望 500、实际 409;精确唯一键冲突分类修复已写入,尚无修复后通过证据。
|
- [x] 首轮审计失败误报 409 已通过精确唯一键冲突分类修复;最后专项 62 个叶子通过、3 条实库条件跳过,退出码 0,首轮失败证据保留。
|
||||||
- [ ] 双版本 MySQL、完整后端、竞态、静态检查及构建按最终源码验证,上一批结果不代表当前新增接口通过。
|
- [x] MySQL 5.7/8.0 三轮成员组合各 171 个叶子通过、0 失败、0 跳过;含 SQLite 和错误分类用例,两个版本依次执行。
|
||||||
- [x] 同步总清单顶部、P0-07 最新进度及本执行清单,明确实现中、失败和待复测边界,原始 44 项总状态不升级。
|
- [x] 完整后端 1679 个叶子通过、53 条条件跳过;Windows 独立程序历史备份/升级/成员恢复路径、静态检查、模块校验与双平台构建通过,顺序进程退出码 0。
|
||||||
- [ ] 补齐模型和正式接口文档。成员管理页面、项目入口、全部业务资源授权及排队任务重判继续后续交付;本批接口通过不等于完整权限隔离或生产验收。
|
- [x] 仓储、HTTP 与应用接线三轮成员专项竞态 186 个叶子通过、0 失败、9 条外部 MySQL 条件跳过,退出码 0;不称全仓库或实库竞态通过。
|
||||||
|
- [x] 同步总清单、模型、接口交接及支持矩阵第二十三节,说明错误、分页和授权边界;Smail 参考接口文档保持原样。
|
||||||
|
- [ ] 成员管理页面、项目入口、全部业务资源授权及排队任务重判继续后续交付;本批接口通过不等于完整权限隔离或生产验收。
|
||||||
|
|
||||||
## 2026 年 9 月 28 日:数据库最终候选(18:36,回归完成,等待复审)
|
## 2026 年 9 月 28 日:数据库最终候选(18:36,回归完成,等待复审)
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
# Scert 整改、合并与优化清单
|
# Scert 整改、合并与优化清单
|
||||||
|
|
||||||
## 当前进度(2026 年 9 月 28 日 18:55,成员接口已进入实现,修复待复测)
|
## 当前进度(2026 年 9 月 28 日 19:10,成员接口本批回归通过,等待复审)
|
||||||
|
|
||||||
本次核对总清单、执行清单、工作区源码与专项日志,补齐总清单遗漏的 18:42 成员接口批次。上一批数据库回归文档已随 `bf10eb5` 提交,成员接口修改前检查点为 `4618670`;本节所述新增接口与修复仍在工作区,未提交,也未完成最终源码回归。本次仅同步文档,不修改业务代码、不重启测试、不清理数据库、不进行远端部署。
|
本批继续推进成员接口、分页及失败审计;实现与错误分类修复已保存到检查点 `3857dac`,随后增加正式边界测试。已重新执行当前源码专项、双版本 MySQL、完整后端及构建,不用上一批数据库候选结果替代本批证据。本批不清理现有业务库、不进行浏览器或远端部署测试。
|
||||||
|
|
||||||
### 本批任务状态
|
### 本批任务状态
|
||||||
|
|
||||||
- [x] 确认进度同步范围:保留原始 44 项、验收标准和历史证据,按实际实现与验证结果更新,不以测试数量折算完成率。
|
- [x] 确认进度同步范围:保留原始 44 项、验收标准和历史证据,按实际实现与验证结果更新,不以测试数量折算完成率。
|
||||||
- [~] 成员列表、授予及撤销接口已加入工作区并接入应用依赖;列表按用户账户分页,接口允许每页 5、10、15、30 位成员,同一账户的多个角色不拆页。尚未通过本批完整回归,不能视为已经交付。
|
- [x] 成员列表、授予及撤销接口已接入应用依赖;列表按用户账户分页,每页支持 5、10、15、30 位成员,同一账户的多个角色不拆页。实际应用登录及读取默认项目的接线测试通过,等待复审。
|
||||||
- [~] 列表查询内实时判权、服务端会话身份和失败请求审计已有实现;成功成员变更继续使用事务审计。相关专项已加入,但最终源码边界仍待复测。
|
- [x] 列表查询内实时判权、服务端会话身份和失败请求审计已通过专项;补齐会话验证后撤权/停用、取消请求独立审计、审计存储失败、DELETE 额外输入拒绝等边界。成功变更继续使用事务审计。
|
||||||
- [~] 首轮专项 `artifacts/project-member-api-20260928/focused-first.jsonl` 于 18:49 记录失败:成功审计写入失败回滚用例期望 HTTP 500,实际返回 409。工作区已加入精确成员唯一键冲突分类修复;截至本次核对,证据目录只有首轮日志,没有修复后通过证据。
|
- [x] 首轮 `focused-first.jsonl` 的审计错误误报 409 已修复;精确区分三类驱动的唯一键冲突与触发器等其他错误。修复后 `focused-boundaries.jsonl` 为 62 个叶子通过、3 条实库条件跳过,退出码 0;失败日志保留。
|
||||||
- [ ] 重新验证修复后的仓储、HTTP 和应用实际接线测试,再按版本依次执行 MySQL 5.7/8.0、完整后端、竞态、静态检查和构建;不将上一批数据库候选通过结果复用为本批新代码结果。
|
- [x] MySQL 5.7/8.0 依次完成三轮成员组合,各 171 个叶子通过、0 失败、0 跳过,退出码均为 0;组合含 SQLite 和错误分类用例,不称纯 MySQL 用例。
|
||||||
|
- [x] 当前源码完整后端 1679 个叶子通过、0 失败、53 条条件跳过;Windows 独立程序历史备份/升级与成员恢复路径、静态检查、模块校验、Windows 构建及 Linux 交叉构建通过,顺序进程退出码 0。跳过与交叉构建不作为实机验收。
|
||||||
|
- [x] 仓储、HTTP 和实际应用接线三轮竞态共 186 个叶子通过、0 失败、9 条外部 MySQL 条件跳过,退出码 0;这是成员专项竞态,不是全仓库或实库竞态验证。
|
||||||
|
- [x] 已补充成员接口交接与模型边界,保留 Smail 参考接口文档不混写。详细命令、结果与范围见《构建版本与数据库支持矩阵》第二十三节。
|
||||||
- [ ] 成员管理页面、项目入口、全部业务资源授权及排队任务重判继续后续交付;PostgreSQL 当前实库、Linux 原生、完整发行包和生产验收仍未完成。
|
- [ ] 成员管理页面、项目入口、全部业务资源授权及排队任务重判继续后续交付;PostgreSQL 当前实库、Linux 原生、完整发行包和生产验收仍未完成。
|
||||||
|
|
||||||
### 原始 44 项状态汇总
|
### 原始 44 项状态汇总
|
||||||
@ -360,7 +363,7 @@ backup_records 备份记录
|
|||||||
|
|
||||||
**状态:** `[~] 进行中`
|
**状态:** `[~] 进行中`
|
||||||
|
|
||||||
**最新进度(2026-09-28 18:55):** 仓储层已有六类项目角色、实时判权、单语句授权变更与事务审计。本批成员列表分页、授予/撤销 API、应用接线和失败请求审计已加入工作区,正式测试同步增加;首轮专项存在审计写入失败被误分类为成员冲突的问题,修复已写入但尚未复测,不能记为接口交付。成员管理页面、项目入口、全部资源入口与排队任务执行/取消/重试重判仍未完成;上一批仓储通过不等于本批接口或对外权限隔离完成。
|
**最新进度(2026-09-28 19:10):** 成员列表分页、授予/撤销 API、实际应用接线和失败审计已实现,审计失败误报冲突问题修复后专项、双版本 MySQL 三轮、完整后端、成员专项三轮竞态与双平台构建通过,等待复审。完整证据及跳过项见支持矩阵第二十三节。成员管理页面、项目入口、全部资源入口与排队任务执行/取消/重试重判仍未完成,P0-07 不升级为完成;成员接口通过不等于完整项目隔离或生产验收通过。
|
||||||
|
|
||||||
建议角色:
|
建议角色:
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
# 全面审查、测试与优化记录
|
# 全面审查、测试与优化记录
|
||||||
|
|
||||||
|
> 最新成员接口批次(2026-09-28 19:10):首轮审计触发器失败被误映射为 409,原失败日志保留;修复后增加 DELETE 输入拒绝、认证后撤权/停用、取消请求审计、审计存储故障和三驱动错误分类正式测试。专项 62 个叶子通过、3 条实库条件跳过;MySQL 5.7/8.0 三轮各 171 个叶子通过且无失败跳过,包含 SQLite 用例;完整后端 1679 个叶子通过、53 条条件跳过,静态检查、模块校验和双平台构建通过。成员三轮竞态 186 个叶子通过、9 条外部 MySQL 条件跳过,退出码 0;完整证据见支持矩阵第二十三节,不复用下方历史竞态数字。本批没有浏览器或远端部署验收。
|
||||||
|
|
||||||
> 最终候选结果同步(2026-09-28 18:36):完整后端 1635 个叶子通过、52 条条件跳过;MySQL 5.7/8.0 三轮组合各 531 个叶子通过且无失败跳过(包含 SQLite 用例);最后源码取消查询/WAL 专项共 30 个叶子通过,服务并发关闭另 30 次通过;最终三轮竞态 831 个叶子通过、15 条条件跳过。上述进程退出码均为 0。Windows 独立程序历史备份/升级/成员恢复、静态检查、模块校验及双平台构建通过,完整证据见支持矩阵第二十二节。旧索引异常根因仍未关闭,跳过与交叉构建不作为实机验收通过。
|
> 最终候选结果同步(2026-09-28 18:36):完整后端 1635 个叶子通过、52 条条件跳过;MySQL 5.7/8.0 三轮组合各 531 个叶子通过且无失败跳过(包含 SQLite 用例);最后源码取消查询/WAL 专项共 30 个叶子通过,服务并发关闭另 30 次通过;最终三轮竞态 831 个叶子通过、15 条条件跳过。上述进程退出码均为 0。Windows 独立程序历史备份/升级/成员恢复、静态检查、模块校验及双平台构建通过,完整证据见支持矩阵第二十二节。旧索引异常根因仍未关闭,跳过与交叉构建不作为实机验收通过。
|
||||||
|
|
||||||
> 2026-09-28 运行时与迁移批次:SQLite 取消查询遗留文件占用在旧驱动复现;v1.40.1 中间候选专项和完整后端曾通过,但其 3.50.4 引擎仍处于官方 WAL-reset 缺陷影响范围,不能作为最终放行。当前改用 v1.59.0/3.53.4,并新增 WAL 并发事务持久化完整性回归。MySQL 两次资源分配失败已核实,旧索引异常仍未关闭。最终候选执行结果、命令和未覆盖范围见支持矩阵第二十二节;历史失败日志完整保留,不称生产验收通过。
|
> 2026-09-28 运行时与迁移批次:SQLite 取消查询遗留文件占用在旧驱动复现;v1.40.1 中间候选专项和完整后端曾通过,但其 3.50.4 引擎仍处于官方 WAL-reset 缺陷影响范围,不能作为最终放行。当前改用 v1.59.0/3.53.4,并新增 WAL 并发事务持久化完整性回归。MySQL 两次资源分配失败已核实,旧索引异常仍未关闭。最终候选执行结果、命令和未覆盖范围见支持矩阵第二十二节;历史失败日志完整保留,不称生产验收通过。
|
||||||
|
|||||||
@ -171,7 +171,7 @@ P0-06 与 P0-07 均维持 `[~]`,六阶段中的隔离、生产和最终验收
|
|||||||
- 整改内容:资源增加项目、所有者、创建人及更新人;制定系统管理员、项目管理员、证书/DNS/部署管理员、审计与只读角色矩阵;列表、详情、写操作、导出与后台任务均检查项目归属,普通用户不能读完整私钥。
|
- 整改内容:资源增加项目、所有者、创建人及更新人;制定系统管理员、项目管理员、证书/DNS/部署管理员、审计与只读角色矩阵;列表、详情、写操作、导出与后台任务均检查项目归属,普通用户不能读完整私钥。
|
||||||
- 验证命令:V07、V06、V03;补跨项目 ID 枚举、伪造归属、任务访问与私钥导出拒绝测试。
|
- 验证命令:V07、V06、V03;补跨项目 ID 枚举、伪造归属、任务访问与私钥导出拒绝测试。
|
||||||
- 实机验收步骤:建立两项目和最小权限成员;分别测试证书、服务器、目标、DNS、任务、审计的跨项目读写、删除与回滚;撤权后现有会话及排队任务必须重新判权。
|
- 实机验收步骤:建立两项目和最小权限成员;分别测试证书、服务器、目标、DNS、任务、审计的跨项目读写、删除与回滚;撤权后现有会话及排队任务必须重新判权。
|
||||||
- 验收证据:域名/服务器归属和 HTTP 拒绝伪造归属已覆盖;六类项目角色、按实际成员和账户状态判权、成功成员变更与审计同事务已有仓储实现及 SQLite/MySQL 隔离证据,当前候选结果见支持矩阵第二十二节。成员 Web/API、失败请求审计、全部资源跨项目读写和排队任务撤权重判仍待补,不能称项目隔离已完成。
|
- 验收证据:域名/服务器归属和 HTTP 拒绝伪造归属已覆盖;六类角色、实时判权及成功审计同事务已有仓储证据。本批成员分页、授予/撤销 API、实际应用接线和失败审计已通过专项及双版本 MySQL,完整后端、三轮成员专项竞态和构建通过,跳过与验证范围见支持矩阵第二十三节。成员页面、项目入口、全部资源跨项目读写和排队任务撤权重判仍待补,不能称项目隔离已完成。
|
||||||
- 复审结果:未完成。
|
- 复审结果:未完成。
|
||||||
|
|
||||||
## P0-08 完善凭据和主密钥生命周期
|
## P0-08 完善凭据和主密钥生命周期
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
# 构建版本与数据库支持矩阵
|
# 构建版本与数据库支持矩阵
|
||||||
|
|
||||||
首次核查日期:2026 年 9 月 27 日;最近结果同步:2026 年 9 月 28 日 18:36(本机时间),见第二十二节。本文区分代码支持、隔离测试和正式生产准入,不把驱动存在视为兼容性验收。
|
首次核查日期:2026 年 9 月 27 日;成员接口批次的最新结果见第二十三节。本文区分代码支持、隔离测试和正式生产准入,不把驱动存在视为兼容性验收。
|
||||||
|
|
||||||
当前运行时候选及最后验证以第二十二节为准;第十五至二十一节保留各自批次的源码、依赖和环境证据,不据此推断新驱动已完成相同范围的验证。
|
运行时候选选型依据见第二十二节,新增成员接口后的验证另列第二十三节;此前各节保留各自批次的源码、依赖和环境证据,不据此推断新代码已完成相同范围的验证。
|
||||||
|
|
||||||
## 一、工具链与依赖
|
## 一、工具链与依赖
|
||||||
|
|
||||||
@ -1018,3 +1018,89 @@ go test -race -p 1 -json -count=3 -timeout=12m -run 'SQLite|ProjectMember|Histor
|
|||||||
- [ ] 大规模数据和长时间资源稳定性;有限次数回归不代表长期泄漏或并发风险全部消除。
|
- [ ] 大规模数据和长时间资源稳定性;有限次数回归不代表长期泄漏或并发风险全部消除。
|
||||||
|
|
||||||
P0-04/P0-05/P0-06/P0-07/P1-09 总项继续 `[~]`,原始 44 项和最终验收标准不缩减。
|
P0-04/P0-05/P0-06/P0-07/P1-09 总项继续 `[~]`,原始 44 项和最终验收标准不缩减。
|
||||||
|
|
||||||
|
## 二十三、项目成员 HTTP 接口、分页与失败审计(2026 年 9 月 28 日)
|
||||||
|
|
||||||
|
### 23.1 实现与范围
|
||||||
|
|
||||||
|
- 检查点 `3857dac` 保存成员列表、授予/撤销、应用依赖接线和失败审计候选;随后增加正式边界测试。源码检查点不是发布或生产放行。
|
||||||
|
- 按用户账户分页,接口只允许每页 5、10、15、30 位成员,多角色不拆页;列表与变更均从真实账户和项目成员关系判权,不信任客户端声明或会话缓存的角色。
|
||||||
|
- 失败请求使用已验证身份并独立记录审计;请求取消不取消失败审计,有三秒等待上限。存储失败只写稳定诊断,不伪报持久化。
|
||||||
|
- 本批没有修改数据库模式、没有创建辅助验收脚本、没有清理现有业务库,也没有浏览器或远端部署测试。正式 Go 测试使用新的临时 SQLite 库和专用 MySQL 隔离实例。
|
||||||
|
|
||||||
|
### 23.2 已核对证据
|
||||||
|
|
||||||
|
证据目录为 `artifacts/project-member-api-20260928/`;首轮失败记录保留,不覆盖。
|
||||||
|
|
||||||
|
| 证据 | 结果 | 边界 |
|
||||||
|
|---|---|---|
|
||||||
|
| `focused-first.jsonl` | 首轮失败 | 审计写入失败回滚用例期望 HTTP 500,实际为 409;不能计为通过 |
|
||||||
|
| `focused-after.jsonl` | 35 个叶子通过,3 条条件跳过,退出码 0 | 修复精确唯一键错误分类,并包含实际应用接线 |
|
||||||
|
| `focused-boundaries.jsonl` | 62 个叶子通过,3 条条件跳过,退出码 0 | 新增 DELETE 额外输入拒绝、会话验证后撤权/停用、取消请求审计、审计存储故障及多驱动错误分类 |
|
||||||
|
| `mysql57.jsonl` | 三轮共 171 个叶子通过,0 失败、0 跳过,退出码 0 | 原有成员变更与新分页/失败审计组合,含 SQLite 和错误分类用例,不称纯 MySQL 用例 |
|
||||||
|
| `mysql80.jsonl` | 三轮共 171 个叶子通过,0 失败、0 跳过,退出码 0 | 与 5.7 同范围,两个版本依次执行,不并行占用隔离数据库资源 |
|
||||||
|
|
||||||
|
两份本地专项的三个条件跳过是未设置实库环境的成员变更、列表和角色 MySQL 用例,不能计为通过。实库结果由后两份证据单独覆盖。PostgreSQL 错误分类测试使用驱动错误类型,不是 PostgreSQL 原生实库验收。
|
||||||
|
|
||||||
|
完整后端及构建顺序进程于 19:06 结束,退出码为 0:`backend.jsonl` 为 1679 个叶子通过、0 失败、53 条条件跳过;Windows 独立程序历史备份/升级/成员恢复路径通过,`go vet -p 1 ./...`、`go mod verify`、Windows 构建和 Linux 交叉构建均通过。条件跳过包括 Linux 专属、符号链接/权限、外部数据库、Named Pipe 与远程部署测试,不计作通过;相应实机门禁仍保留。
|
||||||
|
|
||||||
|
`race.jsonl` 三轮成员专项竞态共 186 个叶子通过、0 失败、9 条条件跳过,19:08 结束,已取回退出码 0。三个未配置外部 MySQL 的条件测试各跳过三次,不将普通实库测试当作 MySQL 竞态通过。Windows/Linux 候选二进制仅用于定位此批构建,不代表完整发行包:
|
||||||
|
|
||||||
|
| 目标 | SHA256 |
|
||||||
|
|---|---|
|
||||||
|
| Windows amd64 | `0FE034F129BFA23E59A951FDE79C28331AFF512FE37311BC861FA5C1EFC8370B` |
|
||||||
|
| Linux amd64 | `D0C129BB463A17FD9D7E3CFBA8CE4D899C4E059F7F715177CB22666B77E8DE0A` |
|
||||||
|
|
||||||
|
### 23.3 已执行命令
|
||||||
|
|
||||||
|
以下均在 `backend/` 执行。PowerShell 重定向日志按 Unicode 读取;日志、隔离数据库与候选二进制均在既有忽略目录,不提交到仓库。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:GOMAXPROCS = '2'
|
||||||
|
$env:CGO_ENABLED = '0'
|
||||||
|
go test -p 1 -json -count=1 -timeout=6m -run 'ProjectMember' ./internal/repository ./internal/httpapi ./internal/app
|
||||||
|
|
||||||
|
$env:SCERT_MYSQL_TEST_BIN = 'E:\DB\5.7\bin'
|
||||||
|
$env:SCERT_MYSQL_TEST_ROOT = 'D:\Desktop\Scert\artifacts\project-member-api-20260928\mysql57-isolated'
|
||||||
|
go test -p 1 -json -count=3 -timeout=10m -run 'ProjectMember' ./internal/repository
|
||||||
|
$env:SCERT_MYSQL_TEST_BIN = 'E:\DB\8.0\bin'
|
||||||
|
$env:SCERT_MYSQL_TEST_ROOT = 'D:\Desktop\Scert\artifacts\project-member-api-20260928\mysql80-isolated'
|
||||||
|
go test -p 1 -json -count=3 -timeout=10m -run 'ProjectMember' ./internal/repository
|
||||||
|
```
|
||||||
|
|
||||||
|
完整回归使用另一个命令进程,未设置外部 MySQL 程序目录;每一步检查退出码,失败则停止后续步骤。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:GOMAXPROCS = '2'
|
||||||
|
$env:CGO_ENABLED = '0'
|
||||||
|
$env:GOOS = 'windows'
|
||||||
|
$env:GOARCH = 'amd64'
|
||||||
|
go build -p 1 -o ../build/review-scert-members-api-windows-amd64.exe ./cmd/scert
|
||||||
|
$env:SCERT_SQLITE_TEST_CLI = 'D:\Desktop\Scert\build\review-scert-members-api-windows-amd64.exe'
|
||||||
|
go test -p 1 -json -count=1 -timeout=12m ./... > ../artifacts/project-member-api-20260928/backend.jsonl
|
||||||
|
go vet -p 1 ./...
|
||||||
|
go mod verify
|
||||||
|
$env:GOOS = 'linux'
|
||||||
|
go build -p 1 -trimpath -o ../build/review-scert-members-api-linux-amd64 ./cmd/scert
|
||||||
|
```
|
||||||
|
|
||||||
|
### 23.4 竞态范围
|
||||||
|
|
||||||
|
本批三轮竞态单独执行,范围为成员仓储、HTTP 及应用实际接线,不将其称为全仓库或实库竞态:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:GOMAXPROCS = '2'
|
||||||
|
$env:CGO_ENABLED = '1'
|
||||||
|
$env:GOOS = 'windows'
|
||||||
|
$env:GOARCH = 'amd64'
|
||||||
|
go test -race -p 1 -json -count=3 -timeout=12m -run 'ProjectMember|RequireAuthenticated' ./internal/repository ./internal/httpapi ./internal/app > ../artifacts/project-member-api-20260928/race.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
### 23.5 尚未完成
|
||||||
|
|
||||||
|
- [ ] 成员管理页面、项目入口与授权目标选择;接口契约见 [成员接口交接](../.claude/docs/ai/project-members/api-handoff.md)。Smail 参考接口文档保持原样。
|
||||||
|
- [ ] 所有资源入口的项目权限、排队任务执行/取消/重试重判,以及相应两项目端到端拒绝测试。
|
||||||
|
- [!] 旧 MySQL Error 1280 的根因未关闭,新成员专项通过不替代旧异常根因分析。
|
||||||
|
- [ ] PostgreSQL 当前实库、Linux 原生、三库完整恢复、完整发行包和十二层生产门禁。
|
||||||
|
|
||||||
|
P0-07 与其他未满足原始验收标准的总项仍为 `[~]`,不按新增测试数量折算整体完成率。
|
||||||
|
|||||||
@ -126,7 +126,7 @@ SQLite 与 PostgreSQL 在迁移事务内回填,失败回滚;MySQL DDL 可能
|
|||||||
- `GrantProjectRole`、`RevokeProjectRole` 仅允许有效超级管理员或目标项目的项目管理员;授权条件与成员写入在同一条 SQL 中执行,防止应用层先查后写的间隙。MySQL 撤销使用自连接,不依赖不兼容的同表删除子查询。
|
- `GrantProjectRole`、`RevokeProjectRole` 仅允许有效超级管理员或目标项目的项目管理员;授权条件与成员写入在同一条 SQL 中执行,防止应用层先查后写的间隙。MySQL 撤销使用自连接,不依赖不兼容的同表删除子查询。
|
||||||
- 未知角色、无效目标、禁用操作者及越权变更拒绝;重复授予不会产生重复关系。项目管理员可撤销自己的角色,之后不能自行恢复;有效超级管理员保留恢复能力。
|
- 未知角色、无效目标、禁用操作者及越权变更拒绝;重复授予不会产生重复关系。项目管理员可撤销自己的角色,之后不能自行恢复;有效超级管理员保留恢复能力。
|
||||||
- 判权结果不是长期许可。业务入口仍须从实际资源解析项目,并在执行副作用前重新判权;任务提交、排队、取消、重试及撤权竞态的完整接入尚未完成。
|
- 判权结果不是长期许可。业务入口仍须从实际资源解析项目,并在执行副作用前重新判权;任务提交、排队、取消、重试及撤权竞态的完整接入尚未完成。
|
||||||
- 成员成功变更与审计已使用同一事务,具体边界见 9.4。成员管理接口、界面、分页、失败请求审计尚未交付。当前没有新开放项目 HTTP 路由;仓储方法不能直接作为无需会话验证的外部入口。
|
- 成员成功变更与审计已使用同一事务,具体边界见 9.4。成员列表、授予/撤销 HTTP 路由、账户分页与失败请求审计已接入,使用正式会话并在仓储内重判权限,见 9.5。成员管理界面及全部业务资源授权仍未完成;仓储方法不能直接作为无需会话验证的外部入口。
|
||||||
|
|
||||||
### 9.3 迁移与恢复边界
|
### 9.3 迁移与恢复边界
|
||||||
|
|
||||||
@ -143,4 +143,15 @@ Windows 独立产品程序已验证升级前的历史备份以及含两个项目
|
|||||||
- 成员影响行数不是 1、审计写入失败、触发器忽略插入或提交失败均不得返回成功。审计插入未实际写入时回滚已发生的成员变更。
|
- 成员影响行数不是 1、审计写入失败、触发器忽略插入或提交失败均不得返回成功。审计插入未实际写入时回滚已发生的成员变更。
|
||||||
- MySQL 预先检查 `project_members` 与 `audit_logs` 都是 InnoDB;不以 BeginTx 返回成功推断非事务表也能回滚,不擅自修改历史引擎。
|
- MySQL 预先检查 `project_members` 与 `audit_logs` 都是 InnoDB;不以 BeginTx 返回成功推断非事务表也能回滚,不擅自修改历史引擎。
|
||||||
- 单连接 SQLite、审计故障、取消上下文、重复操作、自我撤权、历史审计保留及双版本 MySQL 隔离用例是当前验证范围;PostgreSQL 当前实库和生产端到端尚未完成。
|
- 单连接 SQLite、审计故障、取消上下文、重复操作、自我撤权、历史审计保留及双版本 MySQL 隔离用例是当前验证范围;PostgreSQL 当前实库和生产端到端尚未完成。
|
||||||
- 成功审计不能替代失败请求审计。拒绝请求、Web/API 身份来源与所有业务入口及排队任务重判仍是后续任务,不能由这次仓储提交宣称完成。
|
- 成功审计不能替代失败请求审计。成员接口的拒绝请求与身份来源现已接入 9.5 所述处理,全部业务入口及排队任务重判仍是后续任务,不能由成员仓储或接口提交宣称完成。
|
||||||
|
|
||||||
|
### 9.5 成员接口、分页与失败审计
|
||||||
|
|
||||||
|
- `GET /api/v1/projects/:projectID/members` 按账户游标分页,每页允许 5、10、15、30 位成员;同一账户的全部角色合并返回,只包含账户 ID、用户名、状态和角色,不含邮箱、分组或密码摘要。
|
||||||
|
- `POST` 同路径授予角色;`DELETE /api/v1/projects/:projectID/members/:userID/roles/:role` 撤销角色。真实超级管理员或本项目管理员可操作,普通全局管理员和同组成员不隐式获权。
|
||||||
|
- 成员查询在同一 SQL 内完成权限门与分页读取;合法空项目返回空数组,无权限和项目不存在统一拒绝。超级管理员不自动产生列表成员。
|
||||||
|
- 请求身份来自已验证会话,不接受 `actor_id`。会话验证后发生撤权或停用时,仓储仍重新判定;身份快照仅用于本次失败审计,不缓存权限。
|
||||||
|
- 成功事务失败必须回滚,只有确定的成员唯一键冲突映射为 409;审计触发器等其他失败保持 500,不将宽泛约束错误误报为重复授权。
|
||||||
|
- 失败审计使用真实项目解析范围,不存在项目记录为未知;不记录请求体或原始数据库错误。独立取消上下文有三秒上限;审计存储失败只写稳定诊断,不伪报已落库。
|
||||||
|
- [接口交接说明](../.claude/docs/ai/project-members/api-handoff.md) 记录完整请求、响应、错误与前端处理。`doc/后端接口说明文档.md` 是 Smail 参考,不混入本项目接口。
|
||||||
|
- 本批回归与实库证据见《构建版本与数据库支持矩阵》第二十三节;接口通过不是成员页面、所有资源项目隔离或生产验收通过。
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user