diff --git a/scripts/svc.sh b/scripts/svc.sh index 9993537a..75a1d3dc 100755 --- a/scripts/svc.sh +++ b/scripts/svc.sh @@ -449,14 +449,14 @@ EOF - mock 档不支持;PG 档须有有效 DSN/dbref 五个子命令(经 scripts/svc.sh user … 转发到实例二进制): - scripts/svc.sh user create --owner [--role user|admin] --out <0600新文件> - scripts/svc.sh user initialize-account --user-id <稳定ID> --expected-state-version <版本> --expected-generation <代数> --out <0600新文件> + scripts/svc.sh user create --owner [--role user|admin] + scripts/svc.sh user initialize-account --user-id <稳定ID> --expected-state-version <版本> --expected-generation <代数> scripts/svc.sh user list scripts/svc.sh user revoke-login-key --id scripts/svc.sh user bootstrap-admin --owner --out <0600新文件> -旧 reset-password、issue-login-key 与带初始密码建号已关闭;本人恢复走管理员提案、 -目标本人已有可信密码会话确认。凭据只写新建的 0600 交付文件,不回显 stdout。 +旧 reset-password、issue-login-key 入口已关闭;账号由管理员在网页创建并下发初始密码, +首次登录后必须修改。忘记密码时由管理员生成一次性口令(POST /api/admin/users/{owner}/one-time-password)。 本帮助描述 U3 代码契约;真实账号操作与生产切换须另行授权。 更完整说明:docs/svc.md、docs/svc-deploy/u1-instance-account-binding.md EOF @@ -2451,7 +2451,7 @@ agent_do_user() { local a case "${1:-}" in reset-password|issue-login-key) - c_err "[$ENV_NAME] RECOVERY_CONTRACT_REPLACED:旧账号重置/签发入口已关闭;使用本人确认的恢复提案" + c_err "[$ENV_NAME] RECOVERY_CONTRACT_REPLACED:旧账号重置/签发入口已关闭;请联系管理员生成一次性口令(POST /api/admin/users/{owner}/one-time-password)" return 2 ;; esac for a in "$@"; do diff --git a/tests/integration/multi-instance/multi_instance_test.go b/tests/integration/multi-instance/multi_instance_test.go index 95370efc..8c21d7c6 100644 --- a/tests/integration/multi-instance/multi_instance_test.go +++ b/tests/integration/multi-instance/multi_instance_test.go @@ -39,10 +39,11 @@ import ( ) const ( - aliceOwner = "mi_alice" - alicePass = "mi-alice-pass-1" - rootPass = "mi-root-pass-1" - httpWait = 15 * time.Second + aliceOwner = "mi_alice" + aliceInitialPass = "MiAliceInitPass123!" + alicePass = "mi-alice-pass-1" + rootPass = "mi-root-pass-1" + httpWait = 15 * time.Second ) type instance struct { @@ -452,28 +453,35 @@ func (e *env) killNine(t *testing.T, inst *instance) { // ---- HTTP 客户端 ---- -// loginAlice 引导 alice 账号并发放 API key(CLI 直连测试库;key 存共享 PG, -// 对两实例同时有效——程序化验收的首选凭证,SSE 亦可用 ?key= 兜底)。 -// U3 后本地无密码创建:user create 只签发待激活账号的一次性激活 Key(凭据 -// 独占落 JSON),redeem 会话进入 grant-setup 改密设 alicePass,完成后再发 Key。 +// loginAlice 由管理员创建 alice 初始密码账号,再登录并改密后发放 API key +// (CLI 直连测试库;key 存共享 PG,对两实例同时有效——程序化验收的首选凭证, +// SSE 亦可用 ?key= 兜底)。 func (e *env) loginAlice(t *testing.T) *userClient { t.Helper() - // 超级管理员 mi-root 已由 serve 启动时的 SUPER_ADMIN_* 建立;这里只建 alice, - // 一次性激活凭据独占落临时 JSON。 - activation := filepath.Join(t.TempDir(), "activation.json") - e.cliIn(t, e.a.dataDir, "user", "create", "--dsn", e.dbDSN, "--owner", aliceOwner, "--name", "Alice", "--out", activation) - activateKey := activationSecret(t, activation) + // 超级管理员 mi-root 已由 serve 启动时的 SUPER_ADMIN_* 建立;用 root key 走 + // admin API 创建 alice(带初始密码)。 + rootKey := e.cliOut(t, "apikey", "create", "--dsn", e.dbDSN, "--owner", "mi-root", "--name", "mi-acceptance-root-create-alice") + root := &http.Client{Timeout: httpWait, Transport: bearerRT{key: rootKey}} + e.do(t, root, "POST", "/api/admin/users", map[string]any{ + "owner": aliceOwner, + "profile": map[string]any{ + "name": "Alice", + }, + "password": aliceInitialPass, + }, nil) jar, err := cookiejar.New(nil) if err != nil { t.Fatal(err) } sess := &http.Client{Timeout: httpWait, Jar: jar} - // e.uc 尚未赋值 → baseFor 走 A;redeem 会话(grant-setup)直接设 alicePass。 - e.do(t, sess, "POST", "/api/auth/login-key/redeem", map[string]any{ - "key": activateKey, + // e.uc 尚未赋值 → baseFor 走 A;初始密码登录后会话仅允许改密。 + e.do(t, sess, "POST", "/api/auth/login", map[string]any{ + "owner": aliceOwner, + "password": aliceInitialPass, }, nil) e.do(t, sess, "POST", "/api/auth/password", map[string]any{ - "next": alicePass, + "current": aliceInitialPass, + "next": alicePass, }, nil) apiKey := e.cliOut(t, "apikey", "create", "--dsn", e.dbDSN, "--owner", aliceOwner, "--name", "mi-acceptance") @@ -540,22 +548,6 @@ func (b bearerRT) RoundTrip(r *http.Request) (*http.Response, error) { return http.DefaultTransport.RoundTrip(r2) } -// activationSecret 读 CLI 凭据独占文件里的激活 Key(JSON {"kind","owner","secret"})。 -func activationSecret(t *testing.T, path string) string { - t.Helper() - b, err := os.ReadFile(path) - if err != nil { - t.Fatal(err) - } - var v struct { - Secret string `json:"secret"` - } - if err := json.Unmarshal(b, &v); err != nil || v.Secret == "" { - t.Fatalf("activation credential unreadable: %v", err) - } - return v.Secret -} - // cliOut 运行 CLI 并取 stdout 首行(apikey create 的明文仅此一次输出)。 func (e *env) cliOut(t *testing.T, args ...string) string { t.Helper()