From 40d522cd34f339d01768a7f42d5301442342bec2 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Mon, 14 Sep 2026 17:39:53 +0800 Subject: [PATCH] fix(security): retire default-credential surface (yanxue-20260914-cma-defaultcred-code-fix) - Login.vue: no prefilled default creds; no anonymous entity enumeration on mount - deploy.sh: readiness polling (0.5s/30s cap) replaces fixed sleep 3; login smoke reads same-source 600 secret file, missing file = explicit abort - 19 verify/check/gen_report scripts: shared _admin_creds() loader (CMA_ADMIN_ENV -> ops .secrets -> profile copy); no hardcoded creds - tests: single TEST_USER_PASSWORD fixture constant; docs: pointer to 600 secret file; removed 4 legacy .bak + pycache residue --- ...ify_bep_threshold_fix.snapshot-20260908.py | 28 ++++++++++++++- .../scripts/verify_bot_write_tiering_e2e.py | 26 +++++++++++++- .../verify_budget_versions_ui_browser.py | 27 +++++++++++++- .../scripts/verify_budget_versions_ui_fix.py | 27 +++++++++++++- backend/scripts/verify_cashflow_structure.py | 27 +++++++++++++- .../verify_cost_research_dimension_e2e.py | 26 +++++++++++++- .../verify_customer_concentration_rule_e2e.py | 27 +++++++++++++- backend/scripts/verify_dashboard_isolation.py | 26 +++++++++++++- .../scripts/verify_decompose_dialog_review.py | 27 +++++++++++++- .../scripts/verify_decompose_idempotent.py | 26 +++++++++++++- .../verify_forecast_deviation_rules_e2e.py | 27 +++++++++++++- .../scripts/verify_knowledge_partition_e2e.py | 27 +++++++++++++- backend/scripts/verify_map_versions_e2e.py | 27 +++++++++++++- .../verify_phase_c_fixed_entity_e2e.py | 27 +++++++++++++- .../verify_suggestioncenter_unapplied_ui.py | 29 +++++++++++++-- backend/tests/conftest.py | 8 +++-- .../tests/test_api_auth_surface_20260914.py | 4 +-- backend/tests/test_auth.py | 8 ++--- backend/tests/test_dashboard.py | 8 ++--- .../test_data_entity_isolation_20260908.py | 10 +++--- backend/tests/test_kpi_forecast.py | 4 +-- backend/tests/test_kr_full_fix.py | 3 +- backend/tests/test_map_versions.py | 4 +-- backend/tests/test_maps.py | 4 +-- backend/tests/test_multitenant_ai_20260907.py | 4 +-- backend/tests/test_predict.py | 4 +-- backend/tests/test_reports.py | 4 +-- .../test_security_knowledge_partition.py | 4 +-- backend/tests/test_security_multitenant.py | 18 +++++----- check_cma.py | 27 +++++++++++++- check_cma_detail.py | 27 +++++++++++++- check_raw.py | 27 +++++++++++++- deploy.sh | 36 +++++++++++++++++-- docs/account-mode-dev-spec.md | 5 ++- docs/manuals/budget-version.md | 2 +- docs/财务Bot网银流水自助入库流程.md | 5 +-- frontend/src/views/Login.vue | 10 ++++-- gen_report.py | 27 +++++++++++++- scripts/sync_wecom_users.py | 16 ++++++++- 39 files changed, 602 insertions(+), 71 deletions(-) diff --git a/backend/scripts/_archive/verify_bep_threshold_fix.snapshot-20260908.py b/backend/scripts/_archive/verify_bep_threshold_fix.snapshot-20260908.py index 0a94600c..b905963e 100644 --- a/backend/scripts/_archive/verify_bep_threshold_fix.snapshot-20260908.py +++ b/backend/scripts/_archive/verify_bep_threshold_fix.snapshot-20260908.py @@ -26,6 +26,32 @@ import urllib.request import pymysql +# 2026-09-14 凭据面收口(cma-defaultcred-code-fix):原快照里的硬编码默认账号/口令改为同源加载, +# 快照的期/阈值断言一字未动(历史原文仍在 git 历史里)。 +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + if os.environ.get("FORCE_SNAPSHOT") != "1": print("⛔ 这是 20260908 的历史验收快照,不是活检查;复跑结果不代表当前回归。") print(" 现行活检查:python3 backend/scripts/verify_bep_threshold_fix.py") @@ -120,7 +146,7 @@ check("576 GAP red pending", alerts.get(576) == ("red", "pending"), str(alerts.g check("577 COVER_CASH yellow pending", alerts.get(577) == ("yellow", "pending"), str(alerts.get(577))) print("== 5. bep-card 端点 ==") -login = http_json("/api/cma/auth/login", "POST", {"username": "admin", "password": "admin123", "entity_id": 1}) +login = http_json("/api/cma/auth/login", "POST", {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1}) card = http_json("/api/cma/dashboard/bep-card", token=login["token"]) check("available", card.get("available") is True) check("book red", card["book"]["cover_level"] == "red", str(card["book"])) diff --git a/backend/scripts/verify_bot_write_tiering_e2e.py b/backend/scripts/verify_bot_write_tiering_e2e.py index 68d2bf39..4aacb600 100644 --- a/backend/scripts/verify_bot_write_tiering_e2e.py +++ b/backend/scripts/verify_bot_write_tiering_e2e.py @@ -18,6 +18,30 @@ import sys import urllib.request import urllib.error +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = "http://127.0.0.1:8010" MARK = "【系统验证临时数据-验证后删除】" KPI_CODE = "E2E_WT_TIER" @@ -75,7 +99,7 @@ def main(): # ── 1. 登录(人token, entity=1) ── st, r = http("POST", "/api/cma/auth/login", - {"username": "admin", "password": "admin123", "entity_id": 1}) + {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1}) check("登录 admin entity=1", st == 200, f"http={st}") token = r.get("token") if st == 200 else "" auth = {"Authorization": f"Bearer {token}"} diff --git a/backend/scripts/verify_budget_versions_ui_browser.py b/backend/scripts/verify_budget_versions_ui_browser.py index 6c671897..42f8da4a 100644 --- a/backend/scripts/verify_budget_versions_ui_browser.py +++ b/backend/scripts/verify_budget_versions_ui_browser.py @@ -11,6 +11,31 @@ import time import urllib.request import websocket +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + CDP_HTTP = "http://127.0.0.1:9224" BASE = "https://cma.sxbh.ltd/api/cma" @@ -78,7 +103,7 @@ def login_via_fetch(c, entity_id): (async () => {{ const r = await fetch('{BASE}/auth/login', {{ method: 'POST', headers: {{'Content-Type': 'application/json'}}, - body: JSON.stringify({{username:'admin', password:'admin123', entity_id:{entity_id}}}) + body: JSON.stringify({{username:'{ADMIN_USER}', password:'{ADMIN_PASS}', entity_id:{entity_id}}}) }}); const j = await r.json(); if (!r.ok) return 'LOGIN_FAIL:' + JSON.stringify(j); diff --git a/backend/scripts/verify_budget_versions_ui_fix.py b/backend/scripts/verify_budget_versions_ui_fix.py index 37b5505b..6a10ab97 100644 --- a/backend/scripts/verify_budget_versions_ui_fix.py +++ b/backend/scripts/verify_budget_versions_ui_fix.py @@ -3,6 +3,31 @@ """2026-09-07 预算版本管理UI修复 - 生产API验证""" import json, urllib.request, ssl, sys +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = "https://cma.sxbh.ltd/api/cma" def http(url, data=None, headers=None, method=None): @@ -14,7 +39,7 @@ def http(url, data=None, headers=None, method=None): return json.loads(r.read().decode()) def login(entity_id): - body = json.dumps({"username": "admin", "password": "admin123", "entity_id": entity_id}).encode() + body = json.dumps({"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": entity_id}).encode() r = http(BASE + "/auth/login", data=body, headers={"Content-Type": "application/json"}, method="POST") return r["token"] diff --git a/backend/scripts/verify_cashflow_structure.py b/backend/scripts/verify_cashflow_structure.py index f724ca3b..edd3cb0a 100644 --- a/backend/scripts/verify_cashflow_structure.py +++ b/backend/scripts/verify_cashflow_structure.py @@ -18,6 +18,31 @@ import sys import urllib.error import urllib.request +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = "http://127.0.0.1:8010" TMP_MARK = "【系统验证临时数据-验证后删除】" @@ -52,7 +77,7 @@ def main(): # 1. 登录博海账套 (admin, entity_id=2) st, r = http("POST", "/api/cma/auth/login", - {"username": "admin", "password": "admin123", "entity_id": 2}) + {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 2}) check("登录博海(entity_id=2)", st == 200 and "token" in (r or {}), f"st={st} keys={sorted((r or {}).keys())}") token = (r or {}).get("token", "") diff --git a/backend/scripts/verify_cost_research_dimension_e2e.py b/backend/scripts/verify_cost_research_dimension_e2e.py index d8cd25bb..8f0afd18 100644 --- a/backend/scripts/verify_cost_research_dimension_e2e.py +++ b/backend/scripts/verify_cost_research_dimension_e2e.py @@ -23,6 +23,30 @@ import urllib.request import urllib.error from datetime import datetime +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = "http://127.0.0.1:8010" MARK = "【系统验证临时数据-验证后删除】" PREFIX = "E2E-RD-" @@ -63,7 +87,7 @@ def main(): # ── 0. 登录(admin entity=1) ── st, r = http("POST", "/api/cma/auth/login", - {"username": "admin", "password": "admin123", "entity_id": 1}) + {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1}) check("登录 admin entity=1", st == 200, f"http={st}") token = r.get("token") if st == 200 else "" auth = {"Authorization": f"Bearer {token}"} diff --git a/backend/scripts/verify_customer_concentration_rule_e2e.py b/backend/scripts/verify_customer_concentration_rule_e2e.py index 425cda58..35d79dd3 100644 --- a/backend/scripts/verify_customer_concentration_rule_e2e.py +++ b/backend/scripts/verify_customer_concentration_rule_e2e.py @@ -15,8 +15,33 @@ import sys import urllib.request import urllib.error +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = "http://127.0.0.1:8010" -ADMIN = {"username": "admin", "password": "admin123"} +ADMIN = {"username": ADMIN_USER, "password": ADMIN_PASS} MARK = "【系统验证临时数据-验证后删除】" TEMP_TAG = "e2e-verify-temp" RULE_TYPE = "customer_concentration" diff --git a/backend/scripts/verify_dashboard_isolation.py b/backend/scripts/verify_dashboard_isolation.py index a3d9be1b..663ebe8f 100644 --- a/backend/scripts/verify_dashboard_isolation.py +++ b/backend/scripts/verify_dashboard_isolation.py @@ -7,6 +7,30 @@ import pymysql # 2026-09-12 库访问治理(ops):凭据从 backend/.env 读取,无明文兜底 import os +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + try: from dotenv import load_dotenv as _ld_gov _ld_gov('/root/cma-management/backend/.env') @@ -38,7 +62,7 @@ def get(path, token): def main(): # 1. 登录 entity1 - r = post("/api/cma/auth/login", {"username": "admin", "password": "admin123", "entity_id": 1}) + r = post("/api/cma/auth/login", {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1}) token = r.get("access_token") or r.get("token") assert token, f"登录失败: {r}" print("1. 登录 entity1 OK") diff --git a/backend/scripts/verify_decompose_dialog_review.py b/backend/scripts/verify_decompose_dialog_review.py index fe25fd8f..1c7fdda1 100644 --- a/backend/scripts/verify_decompose_dialog_review.py +++ b/backend/scripts/verify_decompose_dialog_review.py @@ -3,6 +3,31 @@ import json import sys import urllib.request +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = "http://127.0.0.1:8010" @@ -28,7 +53,7 @@ def post(path, body, token=None, method="POST"): def main(): # 1. 登录(账套模式 entity_id=1) - status, login = post("/api/cma/auth/login", {"username": "admin", "password": "admin123", "entity_id": 1}) + status, login = post("/api/cma/auth/login", {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1}) token = login.get("token") or login.get("access_token") if status != 200 or not token: print("FAIL login:", status, login) diff --git a/backend/scripts/verify_decompose_idempotent.py b/backend/scripts/verify_decompose_idempotent.py index ae6853f5..9379ba98 100644 --- a/backend/scripts/verify_decompose_idempotent.py +++ b/backend/scripts/verify_decompose_idempotent.py @@ -8,6 +8,30 @@ import os import json import urllib.request +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = os.getenv("CMA_BASE", "http://127.0.0.1:8010") @@ -24,7 +48,7 @@ def post(path, body, token=None): def main(): # 登录(账套模式必须 entity_id) - login = post("/api/cma/auth/login", {"username": "admin", "password": "admin123", "entity_id": 1}) + login = post("/api/cma/auth/login", {"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": 1}) token = login.get("token") or login.get("access_token") if not token: print("❌ 登录失败:", login) diff --git a/backend/scripts/verify_forecast_deviation_rules_e2e.py b/backend/scripts/verify_forecast_deviation_rules_e2e.py index c479a807..f5e1cfde 100644 --- a/backend/scripts/verify_forecast_deviation_rules_e2e.py +++ b/backend/scripts/verify_forecast_deviation_rules_e2e.py @@ -15,8 +15,33 @@ import sys import urllib.request import urllib.error +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = "http://127.0.0.1:8010" -ADMIN = {"username": "admin", "password": "admin123"} +ADMIN = {"username": ADMIN_USER, "password": ADMIN_PASS} MARK = "【系统验证临时数据-验证后删除】" # 规则目标:酣客 → 应收账款周转天数(F_AR_DAYS, id=6,有 2026-09 预测日志) diff --git a/backend/scripts/verify_knowledge_partition_e2e.py b/backend/scripts/verify_knowledge_partition_e2e.py index 9eac0162..1b66afc2 100644 --- a/backend/scripts/verify_knowledge_partition_e2e.py +++ b/backend/scripts/verify_knowledge_partition_e2e.py @@ -5,6 +5,31 @@ """ import sys +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + env = {} with open('/root/cma-management/backend/.env') as f: for line in f: @@ -85,7 +110,7 @@ def api(method, path, token=None, body=None): return e.code, e.read().decode() def login(eid): - st, body = api("POST", "/api/cma/auth/login", body={"username": "admin", "password": "admin123", "entity_id": eid}) + st, body = api("POST", "/api/cma/auth/login", body={"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": eid}) if st != 200: print(f" ⚠️ 登录 entity{eid} 失败 {st}: {body[:200]}") return None diff --git a/backend/scripts/verify_map_versions_e2e.py b/backend/scripts/verify_map_versions_e2e.py index 76b7102f..59b63a96 100644 --- a/backend/scripts/verify_map_versions_e2e.py +++ b/backend/scripts/verify_map_versions_e2e.py @@ -10,6 +10,31 @@ import sys import time import urllib.request +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = "http://127.0.0.1:8010/api/cma" MARK = "【系统验证临时数据-验证后删除】" PASSED = [] @@ -36,7 +61,7 @@ def api(method, path, token=None, body=None): return e.code, {"detail": raw[:300]} def login(entity_id): - st, d = api("POST", "/auth/login", body={"username": "admin", "password": "admin123", "entity_id": entity_id}) + st, d = api("POST", "/auth/login", body={"username": ADMIN_USER, "password": ADMIN_PASS, "entity_id": entity_id}) assert st == 200, f"login entity{entity_id} fail: {d}" return d["token"] diff --git a/backend/scripts/verify_phase_c_fixed_entity_e2e.py b/backend/scripts/verify_phase_c_fixed_entity_e2e.py index b03c004f..3ef99061 100644 --- a/backend/scripts/verify_phase_c_fixed_entity_e2e.py +++ b/backend/scripts/verify_phase_c_fixed_entity_e2e.py @@ -16,10 +16,35 @@ import time import urllib.request import websocket +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + CDP_HTTP = "http://127.0.0.1:9224" FIXED_URL = "http://127.0.0.1:5199" MULTI_URL = "http://127.0.0.1:5200" -USER, PWD = "admin", "admin123" +USER, PWD = ADMIN_USER, ADMIN_PASS FAILS = [] PASSES = [] diff --git a/backend/scripts/verify_suggestioncenter_unapplied_ui.py b/backend/scripts/verify_suggestioncenter_unapplied_ui.py index 64f32e62..42f4c042 100644 --- a/backend/scripts/verify_suggestioncenter_unapplied_ui.py +++ b/backend/scripts/verify_suggestioncenter_unapplied_ui.py @@ -12,6 +12,31 @@ import time import urllib.request import websocket +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + CDP_HTTP = "http://127.0.0.1:9225" TITLE_NEEDLE = "E2E-验证 建议中心UI实证" @@ -80,7 +105,7 @@ def main(): (async () => { const d = await (await fetch('/api/cma/auth/login', {method:'POST', headers:{'Content-Type':'application/json'}, - body: JSON.stringify({username:'admin', password:'admin123', entity_id:1}) + body: JSON.stringify({username:'__CMA_ADMIN_USER__', password:'__CMA_ADMIN_PASS__', entity_id:1}) })).json(); if (!d.token) return 'LOGIN_FAIL: ' + JSON.stringify(d).slice(0,200); localStorage.setItem('cma_token', d.token); @@ -90,7 +115,7 @@ def main(): location.href = '/ai-suggestions'; return 'LOGIN_OK entity=' + d.entity_id; })() - """ + """.replace("__CMA_ADMIN_USER__", ADMIN_USER).replace("__CMA_ADMIN_PASS__", ADMIN_PASS) print("LOGIN:", c.eval(js_login)) time.sleep(6) print("URL:", c.eval("location.href")) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index c0d0195e..39c4fe21 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -134,11 +134,15 @@ def client(db) -> Generator[TestClient, None, None]: # ── 测试数据工厂 ── +# 测试夹具口令(仅内存 SQLite 测试库使用;不是生产默认口令——生产凭据只存 600 密钥文件 +# .secrets/cma-admin.env)。可用 $CMA_TEST_USER_PASSWORD 覆盖;用例一律从这里导入,禁止写字面量。 +TEST_USER_PASSWORD = os.environ.get("CMA_TEST_USER_PASSWORD", "test-fixture-pass") + def create_test_user(db: Session, **kwargs) -> User: """创建测试用户""" defaults = { "username": "testadmin", - "password_hash": hashlib.sha256("admin123".encode()).hexdigest(), + "password_hash": hashlib.sha256(TEST_USER_PASSWORD.encode()).hexdigest(), "name": "测试管理员", "role": "ceo", } @@ -150,7 +154,7 @@ def create_test_user(db: Session, **kwargs) -> User: return user -def get_token_for_user(client: TestClient, username: str = "testadmin", password: str = "admin123") -> str: +def get_token_for_user(client: TestClient, username: str = "testadmin", password: str = TEST_USER_PASSWORD) -> str: """获取测试用户的token(账套模式:需entity_id)""" resp = client.post("/api/cma/auth/login", json={ "username": username, diff --git a/backend/tests/test_api_auth_surface_20260914.py b/backend/tests/test_api_auth_surface_20260914.py index 603e3603..4798c109 100644 --- a/backend/tests/test_api_auth_surface_20260914.py +++ b/backend/tests/test_api_auth_surface_20260914.py @@ -17,7 +17,7 @@ from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models import Entity, UserEntity -from tests.conftest import create_test_user, auth_header +from tests.conftest import create_test_user, auth_header, TEST_USER_PASSWORD BOT_KEY = {"X-BOT-KEY": "cma-bot-finance-2026"} @@ -93,7 +93,7 @@ def _ensure_entity(db: Session, eid: int) -> Entity: def _login(client: TestClient, entity_id: int = 1) -> str: resp = client.post("/api/cma/auth/login", json={ - "username": "testadmin", "password": "admin123", "entity_id": entity_id, + "username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": entity_id, }) assert resp.status_code == 200, f"登录 entity{entity_id} 失败: {resp.text[:300]}" return resp.json().get("token") diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 703e440b..e91bcbce 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -4,7 +4,7 @@ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from tests.conftest import create_test_user, get_token_for_user, auth_header +from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD class TestAuth: @@ -16,7 +16,7 @@ class TestAuth: resp = client.post("/api/cma/auth/login", json={ "username": "testadmin", - "password": "admin123", + "password": TEST_USER_PASSWORD, "entity_id": 1, }) @@ -41,7 +41,7 @@ class TestAuth: """用户不存在""" resp = client.post("/api/cma/auth/login", json={ "username": "nobody", - "password": "admin123", + "password": TEST_USER_PASSWORD, }) assert resp.status_code == 401 @@ -89,7 +89,7 @@ class TestAuth: resp = client.post("/api/cma/auth/register", json={ "username": "testadmin", - "password": "admin123", + "password": TEST_USER_PASSWORD, "name": "重复用户", }) diff --git a/backend/tests/test_dashboard.py b/backend/tests/test_dashboard.py index c5ebd7cf..0cccd32d 100644 --- a/backend/tests/test_dashboard.py +++ b/backend/tests/test_dashboard.py @@ -5,7 +5,7 @@ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from datetime import datetime -from tests.conftest import create_test_user, get_token_for_user, auth_header, create_test_kpi +from tests.conftest import create_test_user, get_token_for_user, auth_header, create_test_kpi, TEST_USER_PASSWORD from app.models import KPIAlert, KPIValue @@ -74,7 +74,7 @@ class TestDashboard: def test_my_kpis(self, client: TestClient, db: Session): user = create_test_user(db, username="biz_user", name="业务经理", role="business") - token = get_token_for_user(client, username="biz_user", password="admin123") + token = get_token_for_user(client, username="biz_user", password=TEST_USER_PASSWORD) kpi = create_test_kpi(db, responsible_user="biz_user") kpi2 = create_test_kpi(db, kpi_code="F_OTHER", kpi_name="无关KPI", responsible_user="其他人") resp = client.get("/api/cma/dashboard/my-kpis", headers=auth_header(token)) @@ -82,7 +82,7 @@ class TestDashboard: def test_my_kpis_ceo_sees_all(self, client: TestClient, db: Session): user = create_test_user(db, username="ceo_user", name="CEO", role="ceo") - token = get_token_for_user(client, username="ceo_user", password="admin123") + token = get_token_for_user(client, username="ceo_user", password=TEST_USER_PASSWORD) kpi = create_test_kpi(db, kpi_code="F_KPI_A", responsible_user="张三") resp = client.get("/api/cma/dashboard/my-kpis", headers=auth_header(token)) assert resp.status_code == 200 @@ -103,7 +103,7 @@ class TestDashboard: def test_my_dashboard(self, client: TestClient, db: Session): user = create_test_user(db, username="ceo_user", name="CEO", role="ceo") - token = get_token_for_user(client, username="ceo_user", password="admin123") + token = get_token_for_user(client, username="ceo_user", password=TEST_USER_PASSWORD) kpi = create_test_kpi(db) resp = client.get("/api/cma/dashboard/my-dashboard", headers=auth_header(token)) assert resp.status_code == 200 diff --git a/backend/tests/test_data_entity_isolation_20260908.py b/backend/tests/test_data_entity_isolation_20260908.py index 630f931b..548b6791 100644 --- a/backend/tests/test_data_entity_isolation_20260908.py +++ b/backend/tests/test_data_entity_isolation_20260908.py @@ -13,7 +13,7 @@ from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models import KPIDefinition, KPIValue, DataSourceConfig, Entity -from tests.conftest import create_test_user, get_token_for_user, auth_header +from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD DATA = "/api/cma/data" @@ -65,7 +65,7 @@ class TestExcelImportEntityIsolation: k1 = self._mk_kpi(db, "F_REVENUE", 1) _ensure_entity(db, 2) create_test_user(db) - token2 = _login(client, "testadmin", "admin123", entity_id=2) + token2 = _login(client, "testadmin", TEST_USER_PASSWORD, entity_id=2) before = db.query(KPIValue).filter(KPIValue.kpi_id == k1.id).count() df = pd.DataFrame({ @@ -91,7 +91,7 @@ class TestExcelImportEntityIsolation: _ensure_entity(db, 2) k1 = self._mk_kpi(db, "F_REVENUE", 1) create_test_user(db) - token2 = _login(client, "testadmin", "admin123", entity_id=2) + token2 = _login(client, "testadmin", TEST_USER_PASSWORD, entity_id=2) df = pd.DataFrame({ "kpi_code": ["F_REVENUE"], @@ -156,7 +156,7 @@ class TestSourceCRUDEntityIsolation: _ensure_entity(db, 2) create_test_user(db) token1 = get_token_for_user(client) - token2 = _login(client, "testadmin", "admin123", entity_id=2) + token2 = _login(client, "testadmin", TEST_USER_PASSWORD, entity_id=2) r1 = client.post(f"{DATA}/sources", headers=auth_header(token1), json={"name": "entity1数据源", "source_type": "manual"}) @@ -178,7 +178,7 @@ class TestSourceCRUDEntityIsolation: _ensure_entity(db, 2) create_test_user(db) token1 = get_token_for_user(client) - token2 = _login(client, "testadmin", "admin123", entity_id=2) + token2 = _login(client, "testadmin", TEST_USER_PASSWORD, entity_id=2) r1 = client.post(f"{DATA}/sources", headers=auth_header(token1), json={"name": "entity1的源", "source_type": "manual"}) diff --git a/backend/tests/test_kpi_forecast.py b/backend/tests/test_kpi_forecast.py index 625022c6..1e162fae 100644 --- a/backend/tests/test_kpi_forecast.py +++ b/backend/tests/test_kpi_forecast.py @@ -13,7 +13,7 @@ from sqlalchemy.orm import Session from app.models import Entity, KPIValue, User, UserEntity from app.utils import kpi_forecast_engine as engine -from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header +from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD BASE = "/api/cma/predict" @@ -34,7 +34,7 @@ def _token_for_entity(client: TestClient, db: Session, eid: int) -> str: db.add(UserEntity(user_id=user.id, entity_id=eid)) db.commit() resp = client.post("/api/cma/auth/login", json={ - "username": "testadmin", "password": "admin123", "entity_id": eid}) + "username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": eid}) assert resp.status_code == 200, f"登录 entity{eid} 失败: {resp.text[:200]}" return resp.json()["token"] diff --git a/backend/tests/test_kr_full_fix.py b/backend/tests/test_kr_full_fix.py index 5a2383d5..0a5daceb 100644 --- a/backend/tests/test_kr_full_fix.py +++ b/backend/tests/test_kr_full_fix.py @@ -9,6 +9,7 @@ from datetime import date from tests.conftest import ( create_test_user, get_token_for_user, auth_header, create_test_kpi, + TEST_USER_PASSWORD, ) from app.models import Objective, KR, KPIDefinition, Entity @@ -253,7 +254,7 @@ class TestMultiTenant: # 用 entity 2 登录 ent2_user = create_test_user(db, username="user2", role="ceo") resp2 = client.post("/api/cma/auth/login", json={ - "username": "user2", "password": "admin123", "entity_id": 2}) + "username": "user2", "password": TEST_USER_PASSWORD, "entity_id": 2}) token2 = resp2.json().get("token") assert token2 diff --git a/backend/tests/test_map_versions.py b/backend/tests/test_map_versions.py index 44743df3..01efdc28 100644 --- a/backend/tests/test_map_versions.py +++ b/backend/tests/test_map_versions.py @@ -6,7 +6,7 @@ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models import Entity, User, UserEntity, StrategicMap, StrategicMapVersion -from tests.conftest import create_test_user, get_token_for_user, auth_header +from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD def _create_second_entity(db: Session, user: User) -> int: @@ -18,7 +18,7 @@ def _create_second_entity(db: Session, user: User) -> int: def _token_for_entity(client: TestClient, entity_id: int) -> str: resp = client.post("/api/cma/auth/login", json={ - "username": "testadmin", "password": "admin123", "entity_id": entity_id, + "username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": entity_id, }) assert resp.status_code == 200 data = resp.json() diff --git a/backend/tests/test_maps.py b/backend/tests/test_maps.py index 6b6ec78d..bffe8cd4 100644 --- a/backend/tests/test_maps.py +++ b/backend/tests/test_maps.py @@ -5,7 +5,7 @@ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models import Entity, User, UserEntity -from tests.conftest import create_test_user, get_token_for_user, auth_header +from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD def _create_second_entity(db: Session, user: User) -> int: @@ -20,7 +20,7 @@ def _token_for_entity(client: TestClient, entity_id: int) -> str: """以指定账套登录获取token""" resp = client.post("/api/cma/auth/login", json={ "username": "testadmin", - "password": "admin123", + "password": TEST_USER_PASSWORD, "entity_id": entity_id, }) assert resp.status_code == 200 diff --git a/backend/tests/test_multitenant_ai_20260907.py b/backend/tests/test_multitenant_ai_20260907.py index 789e5929..d0a1025c 100644 --- a/backend/tests/test_multitenant_ai_20260907.py +++ b/backend/tests/test_multitenant_ai_20260907.py @@ -15,7 +15,7 @@ from sqlalchemy.orm import Session from app.models import KPIDefinition, KPIValue, KPIAlert, ActionPlan, AISuggestion, Entity, UserEntity, ActualCost from app.api import ai_analysis -from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header +from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD def _ensure_entity(db: Session, eid: int) -> Entity: @@ -44,7 +44,7 @@ def _seed_user(db: Session, entity_id: int): def _token(client: TestClient, entity_id: int) -> str: """登录并返回指定账套的 token(get_token_for_user 固定 entity1,这里手动登录)""" resp = client.post("/api/cma/auth/login", json={ - "username": "testadmin", "password": "admin123", "entity_id": entity_id, + "username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": entity_id, }) assert resp.status_code == 200, f"登录 entity{entity_id} 失败: {resp.text[:300]}" return resp.json().get("token") diff --git a/backend/tests/test_predict.py b/backend/tests/test_predict.py index a73c1435..dd2b1dbe 100644 --- a/backend/tests/test_predict.py +++ b/backend/tests/test_predict.py @@ -14,7 +14,7 @@ from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models import CashForecast, ForecastAccuracy, Entity, User, UserEntity -from tests.conftest import create_test_user, get_token_for_user, auth_header +from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD BASE = "/api/cma/predict" @@ -35,7 +35,7 @@ def _token_for_entity(client: TestClient, db: Session, eid: int) -> str: db.add(UserEntity(user_id=user.id, entity_id=eid)) db.commit() resp = client.post("/api/cma/auth/login", json={ - "username": "testadmin", "password": "admin123", "entity_id": eid}) + "username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": eid}) assert resp.status_code == 200, f"登录 entity{eid} 失败: {resp.text[:200]}" return resp.json()["token"] diff --git a/backend/tests/test_reports.py b/backend/tests/test_reports.py index 59f5f29a..fe6176d0 100644 --- a/backend/tests/test_reports.py +++ b/backend/tests/test_reports.py @@ -9,7 +9,7 @@ generate(weekly/monthly/special) / history / history/{id} import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from tests.conftest import create_test_user, get_token_for_user, auth_header +from tests.conftest import create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD from app.models import ( KPIDefinition, KPIValue, BudgetPlan, StrategicMap, KPIAlert, ActionPlan, Subject, ReportHistory, @@ -668,7 +668,7 @@ class TestDupont: db.add(UserEntity(user_id=user.id, entity_id=entity_id)) db.commit() resp = client.post("/api/cma/auth/login", json={ - "username": "testadmin", "password": "admin123", "entity_id": entity_id, + "username": "testadmin", "password": TEST_USER_PASSWORD, "entity_id": entity_id, }) assert resp.status_code == 200, f"账套登录失败({entity_id}): {resp.status_code} {resp.text[:200]}" return resp.json()["token"] diff --git a/backend/tests/test_security_knowledge_partition.py b/backend/tests/test_security_knowledge_partition.py index ecbc7d6a..20df7dfe 100644 --- a/backend/tests/test_security_knowledge_partition.py +++ b/backend/tests/test_security_knowledge_partition.py @@ -16,7 +16,7 @@ from app.models import KPIDefinition, KPIAlert, Entity from app.models.knowledge import KnowledgeSummary, KnowledgeEvent from app.models.knowledge_article import KnowledgeArticle from app.services import knowledge_service -from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header +from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD SUMMARY_FIELDS = dict( level="daily", period_key="2026-09-01", content="测试摘要内容", @@ -35,7 +35,7 @@ def _ensure_entity(db: Session, eid: int, name: str = None) -> Entity: def _login_token(client: TestClient, username: str = "testadmin", - password: str = "admin123", entity_id: int = 1) -> str: + password: str = TEST_USER_PASSWORD, entity_id: int = 1) -> str: """登录指定账套返回 token(conftest.get_token_for_user 固定 entity1,这里参数化)""" resp = client.post("/api/cma/auth/login", json={ "username": username, "password": password, "entity_id": entity_id, diff --git a/backend/tests/test_security_multitenant.py b/backend/tests/test_security_multitenant.py index fa47540c..c18b1d79 100644 --- a/backend/tests/test_security_multitenant.py +++ b/backend/tests/test_security_multitenant.py @@ -13,7 +13,7 @@ from sqlalchemy.orm import Session from app.models import KPIDefinition, KPIAlert, Entity, UserEntity from app.api.alert_rules import AlertRule, _eval_threshold -from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header +from tests.conftest import create_test_kpi, create_test_user, get_token_for_user, auth_header, TEST_USER_PASSWORD BOT_KEY = {"X-BOT-KEY": "cma-bot-finance-2026"} @@ -103,7 +103,7 @@ class TestAlertRulesIsolation: k1 = _seed_entity_kpi(db, entity_id=1, code="AR_E1") db.commit() create_test_user(db) - token = get_token_for_user(client, "testadmin", "admin123") + token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD) headers = auth_header(token) resp = client.post("/api/cma/alert-rules", headers=headers, json={ @@ -128,7 +128,7 @@ class TestAlertRulesIsolation: entity_id=1)) db.commit() create_test_user(db) - token = get_token_for_user(client, "testadmin", "admin123") + token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD) r1 = client.get(f"/api/cma/alert-rules/kpi/{k1.id}", headers=auth_header(token)) assert r1.status_code == 200 @@ -149,7 +149,7 @@ class TestReportsIsolation: data_status="verified", entity_id=1)) db.commit() create_test_user(db) - token = get_token_for_user(client, "testadmin", "admin123") + token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD) r1 = client.get("/api/cma/reports/profit-summary?period=2026-06", headers=auth_header(token)) assert r1.status_code == 200 @@ -164,7 +164,7 @@ class TestReportsIsolation: _seed_entity_kpi(db, entity_id=2, code="TR_E2") db.commit() create_test_user(db) - token = get_token_for_user(client, "testadmin", "admin123") + token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD) r1 = client.get("/api/cma/reports/kpi-trends", headers=auth_header(token)) assert r1.status_code == 200 @@ -212,7 +212,7 @@ class TestCheckGovernanceRetired: _seed_entity_kpi(db, entity_id=1, code="GOV_KPI") db.commit() create_test_user(db) - token = get_token_for_user(client, "testadmin", "admin123") + token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD) resp = client.get("/api/cma/data-quality/check-governance", headers=auth_header(token)) assert resp.status_code == 404, f"应 404,实际 {resp.status_code}: {resp.text[:200]}" @@ -225,7 +225,7 @@ class TestCheckGovernanceRetired: _seed_entity_kpi(db, entity_id=1, code="GOV2_KPI") db.commit() create_test_user(db) - token = get_token_for_user(client, "testadmin", "admin123") + token = get_token_for_user(client, "testadmin", TEST_USER_PASSWORD) resp = client.get("/api/cma/data-quality/governance-check", headers=auth_header(token)) assert resp.status_code == 200, resp.text[:300] @@ -242,7 +242,7 @@ class TestCrossEntityEndpointAuth: def _login(self, client, db): _ensure_entity(db, 2) create_test_user(db) - return get_token_for_user(client, "testadmin", "admin123") # 登录账套 entity_id=1 + return get_token_for_user(client, "testadmin", TEST_USER_PASSWORD) # 登录账套 entity_id=1 # ── 1. data-quality/governance-check ────────────────── def test_governance_check_cross_entity_403(self, client, db): @@ -352,7 +352,7 @@ class TestDupontSessionScope: def _login(self, client, db): _ensure_entity(db, 2) create_test_user(db) - return get_token_for_user(client, "testadmin", "admin123") # 登录账套 entity_id=1 + return get_token_for_user(client, "testadmin", TEST_USER_PASSWORD) # 登录账套 entity_id=1 def test_dupont_cross_entity_bohai_403(self, client, db): """酣客会话显式 entity=bohai(博海真实KPI)→ 403(旧豁免已撤销)""" diff --git a/check_cma.py b/check_cma.py index 05256fe5..261375a7 100644 --- a/check_cma.py +++ b/check_cma.py @@ -3,8 +3,33 @@ import subprocess, json, sys # 登录获取token +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + r = subprocess.run( - "curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"admin123\"}'", + f"curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login -H 'Content-Type: application/json' -d '{{\"username\":\"{ADMIN_USER}\",\"password\":\"{ADMIN_PASS}\"}}'", shell=True, capture_output=True, text=True, timeout=10 ) login = json.loads(r.stdout) diff --git a/check_cma_detail.py b/check_cma_detail.py index c8c85a6a..b763d1dd 100644 --- a/check_cma_detail.py +++ b/check_cma_detail.py @@ -2,10 +2,35 @@ """管理会计OS数据详情检查""" import json, urllib.request +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = "http://127.0.0.1:8010" def login(): - data = json.dumps({"username":"admin","password":"admin123"}).encode() + data = json.dumps({"username":ADMIN_USER,"password":ADMIN_PASS}).encode() req = urllib.request.Request(BASE + "/api/cma/auth/login", data=data, headers={"Content-Type":"application/json"}, method="POST") resp = json.loads(urllib.request.urlopen(req).read()) diff --git a/check_raw.py b/check_raw.py index 2eb7a484..77bf165a 100644 --- a/check_raw.py +++ b/check_raw.py @@ -2,10 +2,35 @@ """查看原始API返回的key和样本数据""" import json, urllib.request +import os # noqa: E402 (凭据加载器需要) +# ---- 管理员凭据同源加载(2026-09-14 cma-defaultcred-code-fix)---- +# 生产凭据只存 600 密钥文件;禁止在脚本/文档/日志里硬编码口令。 +# 顺序:$CMA_ADMIN_ENV → wecom-ops 主副本(ops 轮换落点,权威) → 本 profile 副本。 +def _admin_creds(): + """返回 (user, password, 来源文件);未找到 → 显式报错退出(不静默退回默认口令)""" + for p in (os.environ.get("CMA_ADMIN_ENV"), + "/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env", + "/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env"): + if p and os.path.isfile(p): + kv = {} + for line in open(p, encoding="utf-8"): + line = line.strip() + if "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + kv[k.strip()] = v.strip().strip('"').strip("'") + if kv.get("CMA_ADMIN_USER") and kv.get("CMA_ADMIN_PASS"): + return kv["CMA_ADMIN_USER"], kv["CMA_ADMIN_PASS"], p + raise SystemExit("[错误] 未找到 CMA 管理员凭据:请设 $CMA_ADMIN_ENV,或准备 600 权限密钥文件 " + ".secrets/cma-admin.env(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS)") + + +ADMIN_USER, ADMIN_PASS, CRED_SRC = _admin_creds() +print(f"[creds] 来源={CRED_SRC} 账号={ADMIN_USER}(口令不回显)") + BASE = "http://127.0.0.1:8010" def login(): - data = json.dumps({"username":"admin","password":"admin123"}).encode() + data = json.dumps({"username":ADMIN_USER,"password":ADMIN_PASS}).encode() req = urllib.request.Request(BASE + "/api/cma/auth/login", data=data, headers={"Content-Type":"application/json"}, method="POST") resp = json.loads(urllib.request.urlopen(req).read()) diff --git a/deploy.sh b/deploy.sh index 71d56425..9ebe4f05 100755 --- a/deploy.sh +++ b/deploy.sh @@ -59,7 +59,22 @@ echo " ✓ 服务重启完成" # ── 部署后冒烟测试 ── echo "" echo "===== 冒烟测试 =====" -sleep 3 +# 就绪轮询(2026-09-14 cma-defaultcred-code-fix):journalctl 实测冷启动就绪间隔 2/2/3/3/5s, +# 固定 sleep 3 会让贴边/超时的冷启动误判为冒烟红;改为每 0.5s 探一次 /health,命中即进入判据。 +# 轮询用尽仍不健康 → 下面的健康判据照常判 ❌(不静默放行)。 +# 探测上限可由 SMOKE_READY_MAX 覆盖(离线演练用短上限,生产不设即 30s)。 +SMOKE_READY_MAX="${SMOKE_READY_MAX:-30}" +HEALTH_URL="http://127.0.0.1:8010/health" +READY_TRIES=$(( SMOKE_READY_MAX * 2 )) +READY_TRY=0 +READY_T0=$(date +%s) +while [ "$READY_TRY" -lt "$READY_TRIES" ]; do + if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then break; fi + sleep 0.5 + READY_TRY=$((READY_TRY + 1)) +done +READY_ELAPSED=$(( $(date +%s) - READY_T0 )) +echo " ⏱ 就绪探测: 耗时 ${READY_ELAPSED}s / 轮询 ${READY_TRY} 次(上限 ${SMOKE_READY_MAX}s,命中健康即停)" SMOKE_FAIL=0 if curl -sf http://127.0.0.1:8010/health > /dev/null 2>&1; then echo " ✅ 后端健康检查通过" @@ -78,9 +93,26 @@ else echo " ❌ Schema不一致,请检查: cd /root/cma-management/backend && venv/bin/python3 scripts/schema_check.py" SMOKE_FAIL=1 fi +# 凭据同源(2026-09-14 cma-defaultcred-code-fix):登录冒烟不再硬编码默认账号/口令 —— 轮换后必须自动跟随。 +# 落点:$CMA_ADMIN_ENV 或 600 密钥文件(默认 wecom-ops 主副本);缺失/缺键 → 显式报错并中止,禁止静默退回默认口令。 +CMA_ADMIN_ENV_FILE="${CMA_ADMIN_ENV:-/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env}" +if [ ! -f "$CMA_ADMIN_ENV_FILE" ]; then + echo " ❌ 管理员凭据文件缺失: $CMA_ADMIN_ENV_FILE" + echo " 请准备 600 权限密钥文件(键名 CMA_ADMIN_USER / CMA_ADMIN_PASS),或用 CMA_ADMIN_ENV 指定路径" + exit 1 +fi +set -a +# shellcheck disable=SC1090 +. "$CMA_ADMIN_ENV_FILE" +set +a +if [ -z "${CMA_ADMIN_USER:-}" ] || [ -z "${CMA_ADMIN_PASS:-}" ]; then + echo " ❌ 凭据文件缺少 CMA_ADMIN_USER / CMA_ADMIN_PASS: $CMA_ADMIN_ENV_FILE" + exit 1 +fi +echo " ℹ️ 登录冒烟凭据来源: $CMA_ADMIN_ENV_FILE(账号 ${CMA_ADMIN_USER},口令不回显)" TOKEN=$(curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \ -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123","entity_id":1}' | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null) + -d "{\"username\":\"${CMA_ADMIN_USER}\",\"password\":\"${CMA_ADMIN_PASS}\",\"entity_id\":1}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null) if [ -n "$TOKEN" ]; then echo " ✅ 登录API通过" if curl -sf -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8010/api/cma/kpis > /dev/null 2>&1; then diff --git a/docs/account-mode-dev-spec.md b/docs/account-mode-dev-spec.md index e77ec17b..93624ac0 100644 --- a/docs/account-mode-dev-spec.md +++ b/docs/account-mode-dev-spec.md @@ -80,9 +80,12 @@ get_entity_id 解析链(backend/app/deps.py): ```bash # 1. 隔离验证:登录账套A → 全系统只显示A的数据 +# 凭据见 600 密钥文件(CMA_ADMIN_USER / CMA_ADMIN_PASS),默认落点 +# /root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env +set -a; . "${CMA_ADMIN_ENV:-/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env}"; set +a curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \ -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123","entity_id":2}' + -d "{\"username\":\"$CMA_ADMIN_USER\",\"password\":\"$CMA_ADMIN_PASS\",\"entity_id\":2}" # → token;用该token访问 /api/cma/kpis 只返回博海数据 # 2. 越权验证:未授权entity请求返回403 diff --git a/docs/manuals/budget-version.md b/docs/manuals/budget-version.md index ae87d8ca..3eb10bd8 100644 --- a/docs/manuals/budget-version.md +++ b/docs/manuals/budget-version.md @@ -28,7 +28,7 @@ ## ③ 验证路径 - **自动脚本**: - 1. 生产 API 双账套断言:`/root/cma-management/backend/scripts/verify_budget_versions_ui_fix.py`(`cd backend && venv/bin/python3 scripts/verify_budget_versions_ui_fix.py`;用 admin/admin123 分别登录 entity1/entity2,断言新字段存在 + active→draft 映射 + plan_count==kpi_count + last_updated==updated_at) + 1. 生产 API 双账套断言:`/root/cma-management/backend/scripts/verify_budget_versions_ui_fix.py`(`cd backend && venv/bin/python3 scripts/verify_budget_versions_ui_fix.py`;凭据见 600 密钥文件 `CMA_ADMIN_USER` / `CMA_ADMIN_PASS`(脚本同源读取:`$CMA_ADMIN_ENV` 或 `.secrets/cma-admin.env`)分别登录 entity1/entity2,断言新字段存在 + active→draft 映射 + plan_count==kpi_count + last_updated==updated_at) 2. 浏览器页面 DOM 实测:`/root/cma-management/backend/scripts/verify_budget_versions_ui_browser.py`(需先起 Chrome CDP 9224,命令见 cma-fullstack-ops 技能 references/budget-version-ui-field-state-fix-20260907.md 的 Chrome 起法;登录博海 entity2 断言 v2026H2 行=草稿/25条/462/最后更新非空/「提交审批」按钮,切酣客 entity1 断言 5 个 active 行有提交按钮 + 2026H2 approved「已批准」无按钮) - **期望输出**:两个脚本末尾均输出 `ALL_PASS` 且 exit 0 - **已测基线**(最近一次验证日期+结果):2026-09-07 复跑双脚本 ALL_PASS。实测数据:博海 entity2 `v2026H2 | status=active | approval_status=draft | plan_count=25 | total=462.0 | last_updated='2026-08-30 11:24'`;酣客 entity1 共 6 版本,5 个 active(v1.0/v2.0/incremental/flexible/zero_based-20260826)→ 草稿+「提交审批」按钮,2026H2 approved → 「已批准」无按钮 diff --git a/docs/财务Bot网银流水自助入库流程.md b/docs/财务Bot网银流水自助入库流程.md index 9d0fb5a8..589c7d75 100644 --- a/docs/财务Bot网银流水自助入库流程.md +++ b/docs/财务Bot网银流水自助入库流程.md @@ -53,12 +53,13 @@ ## 三、curl 命令示例 -### 1. 登录拿 token(admin/admin123,账套 entity_id=1 酣客) +### 1. 登录拿 token(凭据见 600 密钥文件 CMA_ADMIN_USER / CMA_ADMIN_PASS,账套 entity_id=1 酣客) ```bash +set -a; . "${CMA_ADMIN_ENV:-/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env}"; set +a TOKEN=$(curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \ -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123","entity_id":1}' \ + -d "{\"username\":\"$CMA_ADMIN_USER\",\"password\":\"$CMA_ADMIN_PASS\",\"entity_id\":1}" \ | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") echo "$TOKEN" # 响应字段含 token/entity_id/entity_name/user diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index e5c7ac47..82d1f213 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -30,7 +30,7 @@