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
This commit is contained in:
Hermes CI Fix
2026-09-14 17:39:53 +08:00
parent 8adc783501
commit 40d522cd34
39 changed files with 602 additions and 71 deletions
@@ -26,6 +26,32 @@ import urllib.request
import pymysql 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": if os.environ.get("FORCE_SNAPSHOT") != "1":
print("⛔ 这是 20260908 的历史验收快照,不是活检查;复跑结果不代表当前回归。") print("⛔ 这是 20260908 的历史验收快照,不是活检查;复跑结果不代表当前回归。")
print(" 现行活检查:python3 backend/scripts/verify_bep_threshold_fix.py") 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))) check("577 COVER_CASH yellow pending", alerts.get(577) == ("yellow", "pending"), str(alerts.get(577)))
print("== 5. bep-card 端点 ==") 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"]) card = http_json("/api/cma/dashboard/bep-card", token=login["token"])
check("available", card.get("available") is True) check("available", card.get("available") is True)
check("book red", card["book"]["cover_level"] == "red", str(card["book"])) check("book red", card["book"]["cover_level"] == "red", str(card["book"]))
@@ -18,6 +18,30 @@ import sys
import urllib.request import urllib.request
import urllib.error 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" BASE = "http://127.0.0.1:8010"
MARK = "【系统验证临时数据-验证后删除】" MARK = "【系统验证临时数据-验证后删除】"
KPI_CODE = "E2E_WT_TIER" KPI_CODE = "E2E_WT_TIER"
@@ -75,7 +99,7 @@ def main():
# ── 1. 登录(人token, entity=1) ── # ── 1. 登录(人token, entity=1) ──
st, r = http("POST", "/api/cma/auth/login", 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}") check("登录 admin entity=1", st == 200, f"http={st}")
token = r.get("token") if st == 200 else "" token = r.get("token") if st == 200 else ""
auth = {"Authorization": f"Bearer {token}"} auth = {"Authorization": f"Bearer {token}"}
@@ -11,6 +11,31 @@ import time
import urllib.request import urllib.request
import websocket 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" CDP_HTTP = "http://127.0.0.1:9224"
BASE = "https://cma.sxbh.ltd/api/cma" BASE = "https://cma.sxbh.ltd/api/cma"
@@ -78,7 +103,7 @@ def login_via_fetch(c, entity_id):
(async () => {{ (async () => {{
const r = await fetch('{BASE}/auth/login', {{ const r = await fetch('{BASE}/auth/login', {{
method: 'POST', headers: {{'Content-Type': 'application/json'}}, 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(); const j = await r.json();
if (!r.ok) return 'LOGIN_FAIL:' + JSON.stringify(j); if (!r.ok) return 'LOGIN_FAIL:' + JSON.stringify(j);
@@ -3,6 +3,31 @@
"""2026-09-07 预算版本管理UI修复 - 生产API验证""" """2026-09-07 预算版本管理UI修复 - 生产API验证"""
import json, urllib.request, ssl, sys 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" BASE = "https://cma.sxbh.ltd/api/cma"
def http(url, data=None, headers=None, method=None): 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()) return json.loads(r.read().decode())
def login(entity_id): 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") r = http(BASE + "/auth/login", data=body, headers={"Content-Type": "application/json"}, method="POST")
return r["token"] return r["token"]
+26 -1
View File
@@ -18,6 +18,31 @@ import sys
import urllib.error import urllib.error
import urllib.request 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" BASE = "http://127.0.0.1:8010"
TMP_MARK = "【系统验证临时数据-验证后删除】" TMP_MARK = "【系统验证临时数据-验证后删除】"
@@ -52,7 +77,7 @@ def main():
# 1. 登录博海账套 (admin, entity_id=2) # 1. 登录博海账套 (admin, entity_id=2)
st, r = http("POST", "/api/cma/auth/login", 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 {}), check("登录博海(entity_id=2)", st == 200 and "token" in (r or {}),
f"st={st} keys={sorted((r or {}).keys())}") f"st={st} keys={sorted((r or {}).keys())}")
token = (r or {}).get("token", "") token = (r or {}).get("token", "")
@@ -23,6 +23,30 @@ import urllib.request
import urllib.error import urllib.error
from datetime import datetime 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" BASE = "http://127.0.0.1:8010"
MARK = "【系统验证临时数据-验证后删除】" MARK = "【系统验证临时数据-验证后删除】"
PREFIX = "E2E-RD-" PREFIX = "E2E-RD-"
@@ -63,7 +87,7 @@ def main():
# ── 0. 登录(admin entity=1) ── # ── 0. 登录(admin entity=1) ──
st, r = http("POST", "/api/cma/auth/login", 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}") check("登录 admin entity=1", st == 200, f"http={st}")
token = r.get("token") if st == 200 else "" token = r.get("token") if st == 200 else ""
auth = {"Authorization": f"Bearer {token}"} auth = {"Authorization": f"Bearer {token}"}
@@ -15,8 +15,33 @@ import sys
import urllib.request import urllib.request
import urllib.error 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" BASE = "http://127.0.0.1:8010"
ADMIN = {"username": "admin", "password": "admin123"} ADMIN = {"username": ADMIN_USER, "password": ADMIN_PASS}
MARK = "【系统验证临时数据-验证后删除】" MARK = "【系统验证临时数据-验证后删除】"
TEMP_TAG = "e2e-verify-temp" TEMP_TAG = "e2e-verify-temp"
RULE_TYPE = "customer_concentration" RULE_TYPE = "customer_concentration"
+25 -1
View File
@@ -7,6 +7,30 @@ import pymysql
# 2026-09-12 库访问治理(ops):凭据从 backend/.env 读取,无明文兜底 # 2026-09-12 库访问治理(ops):凭据从 backend/.env 读取,无明文兜底
import os 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: try:
from dotenv import load_dotenv as _ld_gov from dotenv import load_dotenv as _ld_gov
_ld_gov('/root/cma-management/backend/.env') _ld_gov('/root/cma-management/backend/.env')
@@ -38,7 +62,7 @@ def get(path, token):
def main(): def main():
# 1. 登录 entity1 # 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") token = r.get("access_token") or r.get("token")
assert token, f"登录失败: {r}" assert token, f"登录失败: {r}"
print("1. 登录 entity1 OK") print("1. 登录 entity1 OK")
@@ -3,6 +3,31 @@ import json
import sys import sys
import urllib.request 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" BASE = "http://127.0.0.1:8010"
@@ -28,7 +53,7 @@ def post(path, body, token=None, method="POST"):
def main(): def main():
# 1. 登录(账套模式 entity_id=1) # 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") token = login.get("token") or login.get("access_token")
if status != 200 or not token: if status != 200 or not token:
print("FAIL login:", status, login) print("FAIL login:", status, login)
+25 -1
View File
@@ -8,6 +8,30 @@ import os
import json import json
import urllib.request 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") BASE = os.getenv("CMA_BASE", "http://127.0.0.1:8010")
@@ -24,7 +48,7 @@ def post(path, body, token=None):
def main(): def main():
# 登录(账套模式必须 entity_id) # 登录(账套模式必须 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") token = login.get("token") or login.get("access_token")
if not token: if not token:
print("❌ 登录失败:", login) print("❌ 登录失败:", login)
@@ -15,8 +15,33 @@ import sys
import urllib.request import urllib.request
import urllib.error 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" BASE = "http://127.0.0.1:8010"
ADMIN = {"username": "admin", "password": "admin123"} ADMIN = {"username": ADMIN_USER, "password": ADMIN_PASS}
MARK = "【系统验证临时数据-验证后删除】" MARK = "【系统验证临时数据-验证后删除】"
# 规则目标:酣客 → 应收账款周转天数(F_AR_DAYS, id=6,有 2026-09 预测日志) # 规则目标:酣客 → 应收账款周转天数(F_AR_DAYS, id=6,有 2026-09 预测日志)
@@ -5,6 +5,31 @@
""" """
import sys 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 = {} env = {}
with open('/root/cma-management/backend/.env') as f: with open('/root/cma-management/backend/.env') as f:
for line in f: for line in f:
@@ -85,7 +110,7 @@ def api(method, path, token=None, body=None):
return e.code, e.read().decode() return e.code, e.read().decode()
def login(eid): 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: if st != 200:
print(f" ⚠️ 登录 entity{eid} 失败 {st}: {body[:200]}") print(f" ⚠️ 登录 entity{eid} 失败 {st}: {body[:200]}")
return None return None
+26 -1
View File
@@ -10,6 +10,31 @@ import sys
import time import time
import urllib.request 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" BASE = "http://127.0.0.1:8010/api/cma"
MARK = "【系统验证临时数据-验证后删除】" MARK = "【系统验证临时数据-验证后删除】"
PASSED = [] PASSED = []
@@ -36,7 +61,7 @@ def api(method, path, token=None, body=None):
return e.code, {"detail": raw[:300]} return e.code, {"detail": raw[:300]}
def login(entity_id): 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}" assert st == 200, f"login entity{entity_id} fail: {d}"
return d["token"] return d["token"]
@@ -16,10 +16,35 @@ import time
import urllib.request import urllib.request
import websocket 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" CDP_HTTP = "http://127.0.0.1:9224"
FIXED_URL = "http://127.0.0.1:5199" FIXED_URL = "http://127.0.0.1:5199"
MULTI_URL = "http://127.0.0.1:5200" MULTI_URL = "http://127.0.0.1:5200"
USER, PWD = "admin", "admin123" USER, PWD = ADMIN_USER, ADMIN_PASS
FAILS = [] FAILS = []
PASSES = [] PASSES = []
@@ -12,6 +12,31 @@ import time
import urllib.request import urllib.request
import websocket 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" CDP_HTTP = "http://127.0.0.1:9225"
TITLE_NEEDLE = "E2E-验证 建议中心UI实证" TITLE_NEEDLE = "E2E-验证 建议中心UI实证"
@@ -80,7 +105,7 @@ def main():
(async () => { (async () => {
const d = await (await fetch('/api/cma/auth/login', {method:'POST', const d = await (await fetch('/api/cma/auth/login', {method:'POST',
headers:{'Content-Type':'application/json'}, 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(); })).json();
if (!d.token) return 'LOGIN_FAIL: ' + JSON.stringify(d).slice(0,200); if (!d.token) return 'LOGIN_FAIL: ' + JSON.stringify(d).slice(0,200);
localStorage.setItem('cma_token', d.token); localStorage.setItem('cma_token', d.token);
@@ -90,7 +115,7 @@ def main():
location.href = '/ai-suggestions'; location.href = '/ai-suggestions';
return 'LOGIN_OK entity=' + d.entity_id; 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)) print("LOGIN:", c.eval(js_login))
time.sleep(6) time.sleep(6)
print("URL:", c.eval("location.href")) print("URL:", c.eval("location.href"))
+6 -2
View File
@@ -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: def create_test_user(db: Session, **kwargs) -> User:
"""创建测试用户""" """创建测试用户"""
defaults = { defaults = {
"username": "testadmin", "username": "testadmin",
"password_hash": hashlib.sha256("admin123".encode()).hexdigest(), "password_hash": hashlib.sha256(TEST_USER_PASSWORD.encode()).hexdigest(),
"name": "测试管理员", "name": "测试管理员",
"role": "ceo", "role": "ceo",
} }
@@ -150,7 +154,7 @@ def create_test_user(db: Session, **kwargs) -> User:
return 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)""" """获取测试用户的token(账套模式:需entity_id)"""
resp = client.post("/api/cma/auth/login", json={ resp = client.post("/api/cma/auth/login", json={
"username": username, "username": username,
@@ -17,7 +17,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models import Entity, UserEntity 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"} 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: def _login(client: TestClient, entity_id: int = 1) -> str:
resp = client.post("/api/cma/auth/login", json={ 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]}" assert resp.status_code == 200, f"登录 entity{entity_id} 失败: {resp.text[:300]}"
return resp.json().get("token") return resp.json().get("token")
+4 -4
View File
@@ -4,7 +4,7 @@
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy.orm import Session 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: class TestAuth:
@@ -16,7 +16,7 @@ class TestAuth:
resp = client.post("/api/cma/auth/login", json={ resp = client.post("/api/cma/auth/login", json={
"username": "testadmin", "username": "testadmin",
"password": "admin123", "password": TEST_USER_PASSWORD,
"entity_id": 1, "entity_id": 1,
}) })
@@ -41,7 +41,7 @@ class TestAuth:
"""用户不存在""" """用户不存在"""
resp = client.post("/api/cma/auth/login", json={ resp = client.post("/api/cma/auth/login", json={
"username": "nobody", "username": "nobody",
"password": "admin123", "password": TEST_USER_PASSWORD,
}) })
assert resp.status_code == 401 assert resp.status_code == 401
@@ -89,7 +89,7 @@ class TestAuth:
resp = client.post("/api/cma/auth/register", json={ resp = client.post("/api/cma/auth/register", json={
"username": "testadmin", "username": "testadmin",
"password": "admin123", "password": TEST_USER_PASSWORD,
"name": "重复用户", "name": "重复用户",
}) })
+4 -4
View File
@@ -5,7 +5,7 @@ import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from datetime import datetime 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 from app.models import KPIAlert, KPIValue
@@ -74,7 +74,7 @@ class TestDashboard:
def test_my_kpis(self, client: TestClient, db: Session): def test_my_kpis(self, client: TestClient, db: Session):
user = create_test_user(db, username="biz_user", name="业务经理", role="business") 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") kpi = create_test_kpi(db, responsible_user="biz_user")
kpi2 = create_test_kpi(db, kpi_code="F_OTHER", kpi_name="无关KPI", responsible_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)) 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): def test_my_kpis_ceo_sees_all(self, client: TestClient, db: Session):
user = create_test_user(db, username="ceo_user", name="CEO", role="ceo") 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="张三") kpi = create_test_kpi(db, kpi_code="F_KPI_A", responsible_user="张三")
resp = client.get("/api/cma/dashboard/my-kpis", headers=auth_header(token)) resp = client.get("/api/cma/dashboard/my-kpis", headers=auth_header(token))
assert resp.status_code == 200 assert resp.status_code == 200
@@ -103,7 +103,7 @@ class TestDashboard:
def test_my_dashboard(self, client: TestClient, db: Session): def test_my_dashboard(self, client: TestClient, db: Session):
user = create_test_user(db, username="ceo_user", name="CEO", role="ceo") 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 = create_test_kpi(db)
resp = client.get("/api/cma/dashboard/my-dashboard", headers=auth_header(token)) resp = client.get("/api/cma/dashboard/my-dashboard", headers=auth_header(token))
assert resp.status_code == 200 assert resp.status_code == 200
@@ -13,7 +13,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models import KPIDefinition, KPIValue, DataSourceConfig, Entity 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" DATA = "/api/cma/data"
@@ -65,7 +65,7 @@ class TestExcelImportEntityIsolation:
k1 = self._mk_kpi(db, "F_REVENUE", 1) k1 = self._mk_kpi(db, "F_REVENUE", 1)
_ensure_entity(db, 2) _ensure_entity(db, 2)
create_test_user(db) 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() before = db.query(KPIValue).filter(KPIValue.kpi_id == k1.id).count()
df = pd.DataFrame({ df = pd.DataFrame({
@@ -91,7 +91,7 @@ class TestExcelImportEntityIsolation:
_ensure_entity(db, 2) _ensure_entity(db, 2)
k1 = self._mk_kpi(db, "F_REVENUE", 1) k1 = self._mk_kpi(db, "F_REVENUE", 1)
create_test_user(db) 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({ df = pd.DataFrame({
"kpi_code": ["F_REVENUE"], "kpi_code": ["F_REVENUE"],
@@ -156,7 +156,7 @@ class TestSourceCRUDEntityIsolation:
_ensure_entity(db, 2) _ensure_entity(db, 2)
create_test_user(db) create_test_user(db)
token1 = get_token_for_user(client) 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), r1 = client.post(f"{DATA}/sources", headers=auth_header(token1),
json={"name": "entity1数据源", "source_type": "manual"}) json={"name": "entity1数据源", "source_type": "manual"})
@@ -178,7 +178,7 @@ class TestSourceCRUDEntityIsolation:
_ensure_entity(db, 2) _ensure_entity(db, 2)
create_test_user(db) create_test_user(db)
token1 = get_token_for_user(client) 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), r1 = client.post(f"{DATA}/sources", headers=auth_header(token1),
json={"name": "entity1的源", "source_type": "manual"}) json={"name": "entity1的源", "source_type": "manual"})
+2 -2
View File
@@ -13,7 +13,7 @@ from sqlalchemy.orm import Session
from app.models import Entity, KPIValue, User, UserEntity from app.models import Entity, KPIValue, User, UserEntity
from app.utils import kpi_forecast_engine as engine 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" 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.add(UserEntity(user_id=user.id, entity_id=eid))
db.commit() db.commit()
resp = client.post("/api/cma/auth/login", json={ 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]}" assert resp.status_code == 200, f"登录 entity{eid} 失败: {resp.text[:200]}"
return resp.json()["token"] return resp.json()["token"]
+2 -1
View File
@@ -9,6 +9,7 @@ from datetime import date
from tests.conftest import ( from tests.conftest import (
create_test_user, get_token_for_user, auth_header, create_test_kpi, create_test_user, get_token_for_user, auth_header, create_test_kpi,
TEST_USER_PASSWORD,
) )
from app.models import Objective, KR, KPIDefinition, Entity from app.models import Objective, KR, KPIDefinition, Entity
@@ -253,7 +254,7 @@ class TestMultiTenant:
# 用 entity 2 登录 # 用 entity 2 登录
ent2_user = create_test_user(db, username="user2", role="ceo") ent2_user = create_test_user(db, username="user2", role="ceo")
resp2 = client.post("/api/cma/auth/login", json={ 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") token2 = resp2.json().get("token")
assert token2 assert token2
+2 -2
View File
@@ -6,7 +6,7 @@ import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models import Entity, User, UserEntity, StrategicMap, StrategicMapVersion 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: 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: def _token_for_entity(client: TestClient, entity_id: int) -> str:
resp = client.post("/api/cma/auth/login", json={ 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 assert resp.status_code == 200
data = resp.json() data = resp.json()
+2 -2
View File
@@ -5,7 +5,7 @@ import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models import Entity, User, UserEntity 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: def _create_second_entity(db: Session, user: User) -> int:
@@ -20,7 +20,7 @@ def _token_for_entity(client: TestClient, entity_id: int) -> str:
"""以指定账套登录获取token""" """以指定账套登录获取token"""
resp = client.post("/api/cma/auth/login", json={ resp = client.post("/api/cma/auth/login", json={
"username": "testadmin", "username": "testadmin",
"password": "admin123", "password": TEST_USER_PASSWORD,
"entity_id": entity_id, "entity_id": entity_id,
}) })
assert resp.status_code == 200 assert resp.status_code == 200
@@ -15,7 +15,7 @@ from sqlalchemy.orm import Session
from app.models import KPIDefinition, KPIValue, KPIAlert, ActionPlan, AISuggestion, Entity, UserEntity, ActualCost from app.models import KPIDefinition, KPIValue, KPIAlert, ActionPlan, AISuggestion, Entity, UserEntity, ActualCost
from app.api import ai_analysis 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: 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: def _token(client: TestClient, entity_id: int) -> str:
"""登录并返回指定账套的 token(get_token_for_user 固定 entity1,这里手动登录)""" """登录并返回指定账套的 token(get_token_for_user 固定 entity1,这里手动登录)"""
resp = client.post("/api/cma/auth/login", json={ 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]}" assert resp.status_code == 200, f"登录 entity{entity_id} 失败: {resp.text[:300]}"
return resp.json().get("token") return resp.json().get("token")
+2 -2
View File
@@ -14,7 +14,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models import CashForecast, ForecastAccuracy, Entity, User, UserEntity 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" 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.add(UserEntity(user_id=user.id, entity_id=eid))
db.commit() db.commit()
resp = client.post("/api/cma/auth/login", json={ 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]}" assert resp.status_code == 200, f"登录 entity{eid} 失败: {resp.text[:200]}"
return resp.json()["token"] return resp.json()["token"]
+2 -2
View File
@@ -9,7 +9,7 @@ generate(weekly/monthly/special) / history / history/{id}
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy.orm import Session 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 ( from app.models import (
KPIDefinition, KPIValue, BudgetPlan, StrategicMap, KPIAlert, ActionPlan, KPIDefinition, KPIValue, BudgetPlan, StrategicMap, KPIAlert, ActionPlan,
Subject, ReportHistory, Subject, ReportHistory,
@@ -668,7 +668,7 @@ class TestDupont:
db.add(UserEntity(user_id=user.id, entity_id=entity_id)) db.add(UserEntity(user_id=user.id, entity_id=entity_id))
db.commit() db.commit()
resp = client.post("/api/cma/auth/login", json={ 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]}" assert resp.status_code == 200, f"账套登录失败({entity_id}): {resp.status_code} {resp.text[:200]}"
return resp.json()["token"] return resp.json()["token"]
@@ -16,7 +16,7 @@ from app.models import KPIDefinition, KPIAlert, Entity
from app.models.knowledge import KnowledgeSummary, KnowledgeEvent from app.models.knowledge import KnowledgeSummary, KnowledgeEvent
from app.models.knowledge_article import KnowledgeArticle from app.models.knowledge_article import KnowledgeArticle
from app.services import knowledge_service 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( SUMMARY_FIELDS = dict(
level="daily", period_key="2026-09-01", content="测试摘要内容", 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", 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,这里参数化)""" """登录指定账套返回 token(conftest.get_token_for_user 固定 entity1,这里参数化)"""
resp = client.post("/api/cma/auth/login", json={ resp = client.post("/api/cma/auth/login", json={
"username": username, "password": password, "entity_id": entity_id, "username": username, "password": password, "entity_id": entity_id,
+9 -9
View File
@@ -13,7 +13,7 @@ from sqlalchemy.orm import Session
from app.models import KPIDefinition, KPIAlert, Entity, UserEntity from app.models import KPIDefinition, KPIAlert, Entity, UserEntity
from app.api.alert_rules import AlertRule, _eval_threshold 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"} 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") k1 = _seed_entity_kpi(db, entity_id=1, code="AR_E1")
db.commit() db.commit()
create_test_user(db) 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) headers = auth_header(token)
resp = client.post("/api/cma/alert-rules", headers=headers, json={ resp = client.post("/api/cma/alert-rules", headers=headers, json={
@@ -128,7 +128,7 @@ class TestAlertRulesIsolation:
entity_id=1)) entity_id=1))
db.commit() db.commit()
create_test_user(db) 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)) r1 = client.get(f"/api/cma/alert-rules/kpi/{k1.id}", headers=auth_header(token))
assert r1.status_code == 200 assert r1.status_code == 200
@@ -149,7 +149,7 @@ class TestReportsIsolation:
data_status="verified", entity_id=1)) data_status="verified", entity_id=1))
db.commit() db.commit()
create_test_user(db) 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)) r1 = client.get("/api/cma/reports/profit-summary?period=2026-06", headers=auth_header(token))
assert r1.status_code == 200 assert r1.status_code == 200
@@ -164,7 +164,7 @@ class TestReportsIsolation:
_seed_entity_kpi(db, entity_id=2, code="TR_E2") _seed_entity_kpi(db, entity_id=2, code="TR_E2")
db.commit() db.commit()
create_test_user(db) 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)) r1 = client.get("/api/cma/reports/kpi-trends", headers=auth_header(token))
assert r1.status_code == 200 assert r1.status_code == 200
@@ -212,7 +212,7 @@ class TestCheckGovernanceRetired:
_seed_entity_kpi(db, entity_id=1, code="GOV_KPI") _seed_entity_kpi(db, entity_id=1, code="GOV_KPI")
db.commit() db.commit()
create_test_user(db) 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)) 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]}" 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") _seed_entity_kpi(db, entity_id=1, code="GOV2_KPI")
db.commit() db.commit()
create_test_user(db) 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)) resp = client.get("/api/cma/data-quality/governance-check", headers=auth_header(token))
assert resp.status_code == 200, resp.text[:300] assert resp.status_code == 200, resp.text[:300]
@@ -242,7 +242,7 @@ class TestCrossEntityEndpointAuth:
def _login(self, client, db): def _login(self, client, db):
_ensure_entity(db, 2) _ensure_entity(db, 2)
create_test_user(db) 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 ────────────────── # ── 1. data-quality/governance-check ──────────────────
def test_governance_check_cross_entity_403(self, client, db): def test_governance_check_cross_entity_403(self, client, db):
@@ -352,7 +352,7 @@ class TestDupontSessionScope:
def _login(self, client, db): def _login(self, client, db):
_ensure_entity(db, 2) _ensure_entity(db, 2)
create_test_user(db) 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): def test_dupont_cross_entity_bohai_403(self, client, db):
"""酣客会话显式 entity=bohai(博海真实KPI)→ 403(旧豁免已撤销)""" """酣客会话显式 entity=bohai(博海真实KPI)→ 403(旧豁免已撤销)"""
+26 -1
View File
@@ -3,8 +3,33 @@
import subprocess, json, sys import subprocess, json, sys
# 登录获取token # 登录获取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( 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 shell=True, capture_output=True, text=True, timeout=10
) )
login = json.loads(r.stdout) login = json.loads(r.stdout)
+26 -1
View File
@@ -2,10 +2,35 @@
"""管理会计OS数据详情检查""" """管理会计OS数据详情检查"""
import json, urllib.request 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" BASE = "http://127.0.0.1:8010"
def login(): 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, req = urllib.request.Request(BASE + "/api/cma/auth/login", data=data,
headers={"Content-Type":"application/json"}, method="POST") headers={"Content-Type":"application/json"}, method="POST")
resp = json.loads(urllib.request.urlopen(req).read()) resp = json.loads(urllib.request.urlopen(req).read())
+26 -1
View File
@@ -2,10 +2,35 @@
"""查看原始API返回的key和样本数据""" """查看原始API返回的key和样本数据"""
import json, urllib.request 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" BASE = "http://127.0.0.1:8010"
def login(): 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, req = urllib.request.Request(BASE + "/api/cma/auth/login", data=data,
headers={"Content-Type":"application/json"}, method="POST") headers={"Content-Type":"application/json"}, method="POST")
resp = json.loads(urllib.request.urlopen(req).read()) resp = json.loads(urllib.request.urlopen(req).read())
+34 -2
View File
@@ -59,7 +59,22 @@ echo " ✓ 服务重启完成"
# ── 部署后冒烟测试 ── # ── 部署后冒烟测试 ──
echo "" 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 SMOKE_FAIL=0
if curl -sf http://127.0.0.1:8010/health > /dev/null 2>&1; then if curl -sf http://127.0.0.1:8010/health > /dev/null 2>&1; then
echo " ✅ 后端健康检查通过" echo " ✅ 后端健康检查通过"
@@ -78,9 +93,26 @@ else
echo " ❌ Schema不一致,请检查: cd /root/cma-management/backend && venv/bin/python3 scripts/schema_check.py" echo " ❌ Schema不一致,请检查: cd /root/cma-management/backend && venv/bin/python3 scripts/schema_check.py"
SMOKE_FAIL=1 SMOKE_FAIL=1
fi 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 \ TOKEN=$(curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \
-H "Content-Type: application/json" \ -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 if [ -n "$TOKEN" ]; then
echo " ✅ 登录API通过" echo " ✅ 登录API通过"
if curl -sf -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8010/api/cma/kpis > /dev/null 2>&1; then if curl -sf -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8010/api/cma/kpis > /dev/null 2>&1; then
+4 -1
View File
@@ -80,9 +80,12 @@ get_entity_id 解析链(backend/app/deps.py):
```bash ```bash
# 1. 隔离验证:登录账套A → 全系统只显示A的数据 # 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 \ curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \
-H "Content-Type: application/json" \ -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 只返回博海数据 # → token;用该token访问 /api/cma/kpis 只返回博海数据
# 2. 越权验证:未授权entity请求返回403 # 2. 越权验证:未授权entity请求返回403
+1 -1
View File
@@ -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「已批准」无按钮) 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 - **期望输出**:两个脚本末尾均输出 `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 → 「已批准」无按钮 - **已测基线**(最近一次验证日期+结果):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 → 「已批准」无按钮
@@ -53,12 +53,13 @@
## 三、curl 命令示例 ## 三、curl 命令示例
### 1. 登录拿 token(admin/admin123,账套 entity_id=1 酣客) ### 1. 登录拿 token(凭据见 600 密钥文件 CMA_ADMIN_USER / CMA_ADMIN_PASS,账套 entity_id=1 酣客)
```bash ```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 \ TOKEN=$(curl -s -X POST http://127.0.0.1:8010/api/cma/auth/login \
-H "Content-Type: application/json" \ -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'])") | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
echo "$TOKEN" # 响应字段含 token/entity_id/entity_name/user echo "$TOKEN" # 响应字段含 token/entity_id/entity_name/user
+7 -3
View File
@@ -30,7 +30,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { authApi } from '../api/index' import { authApi } from '../api/index'
@@ -39,7 +39,9 @@ import { isFixedEntity, getFixedEntityId } from '../utils/entity'
const router = useRouter() const router = useRouter()
const loading = ref(false) const loading = ref(false)
const entities = ref<any[]>([]) const entities = ref<any[]>([])
const form = reactive({ username: 'admin', password: 'admin123', entity_id: undefined as number | undefined }) // 凭据面收口(2026-09-14 cma-defaultcred-code-fix):不再预填默认口令。
// 演示包若确需"一键登录"便利,只能以构建期开关(如 VITE_DEMO_PREFILL,默认关闭)恢复,不在源码里留默认口令。
const form = reactive({ username: '', password: '', entity_id: undefined as number | undefined })
// ── 固定企业模式(Phase C):构建期锁定单一企业,隐藏下拉、登录直接带固定 entity_id ── // ── 固定企业模式(Phase C):构建期锁定单一企业,隐藏下拉、登录直接带固定 entity_id ──
const fixedMode = isFixedEntity() const fixedMode = isFixedEntity()
@@ -89,7 +91,9 @@ async function handleLogin() {
loading.value = false loading.value = false
} }
onMounted(() => loadEntities(form.username)) // 凭据面收口:不在挂载时用硬编码用户名(原 'admin')触发企业枚举
// (login-entities 是公开端点,以固定用户名预取=给匿名访客做用户名存在性探测)。
// 企业列表仅在用户输入用户名时由 onUsernameChange 加载;固定企业版展示走 fixedEntityLabel 的"企业 #id"兜底。
</script> </script>
<style scoped> <style scoped>
+26 -1
View File
@@ -2,10 +2,35 @@
"""汇总管理会计OS管理能力报告""" """汇总管理会计OS管理能力报告"""
import json, urllib.request 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" BASE = "http://127.0.0.1:8010"
def login(): 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, req = urllib.request.Request(BASE + "/api/cma/auth/login", data=data,
headers={"Content-Type":"application/json"}, method="POST") headers={"Content-Type":"application/json"}, method="POST")
resp = json.loads(urllib.request.urlopen(req).read()) resp = json.loads(urllib.request.urlopen(req).read())
+15 -1
View File
@@ -30,7 +30,21 @@ DB_PORT = 3306
DB_USER = _CMA_DB_USER DB_USER = _CMA_DB_USER
DB_PASS = _CMA_DB_PASS DB_PASS = _CMA_DB_PASS
DB_NAME = "cma" DB_NAME = "cma"
DEFAULT_PASSWORD = "admin123" # 新建用户的初始口令不再硬编码(2026-09-14 cma-defaultcred-code-fix):
# 从 $CMA_DEFAULT_USER_PASSWORD 或 600 密钥文件同源读取;缺失 → 显式报错退出(禁止静默用默认口令)。
DEFAULT_PASSWORD = _os_gov.getenv('CMA_DEFAULT_USER_PASSWORD', '')
if not DEFAULT_PASSWORD:
for _p in (_os_gov.getenv('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_gov.path.isfile(_p):
for _line in open(_p, encoding='utf-8'):
_line = _line.strip()
if _line.startswith('CMA_DEFAULT_USER_PASSWORD='):
DEFAULT_PASSWORD = _line.split('=', 1)[1].strip().strip('"').strip("'")
if not DEFAULT_PASSWORD:
raise SystemExit("[错误] 未找到新建用户初始口令:请设 $CMA_DEFAULT_USER_PASSWORD,"
"或在 600 密钥文件($CMA_ADMIN_ENV / .secrets/cma-admin.env)里定义 CMA_DEFAULT_USER_PASSWORD")
# ==== 工具函数 ==== # ==== 工具函数 ====
def mcp_call(name, arguments={}): def mcp_call(name, arguments={}):