B-64: 安全修复——随机初始密码与跨公司重复上传隔离
- 公司账号创建改为随机一次性初始密码,只在创建响应中显示一次, 密码保证不等于用户名;删除「用户名即初始密码」兼容分支,继续 强制首次登录改密。 - 跨公司相同字节文件上传只返回通用重复状态:不再返回其他公司的 原批次 ID、银行、模板、期间、交易数或诊断;同公司重复上传的 幂等摘要保持可用。 - 补充服务端回归测试,覆盖同公司与跨公司两个分支及随机密码; 完整测试 85 项全绿,node --check 通过。
This commit is contained in:
@@ -633,11 +633,13 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
if username:
|
||||
# Optionally create the company login in the same request, so
|
||||
# a new company is immediately usable without code changes.
|
||||
# Initial password equals the username (see B-40 decision) and
|
||||
# The initial password is a random one-time value shown only
|
||||
# in this creation response, never stored plaintext or logged;
|
||||
# must_change_password forces a change at first login.
|
||||
initial_password = auth.generate_initial_password(exclude=username)
|
||||
try:
|
||||
user_id = auth.create_user(
|
||||
connection, username, username, "company",
|
||||
connection, username, initial_password, "company",
|
||||
company_id=company_id, must_change_password=True,
|
||||
)
|
||||
except ValueError as exc:
|
||||
@@ -654,7 +656,7 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
)
|
||||
payload.update(
|
||||
{"user_id": user_id, "username": username,
|
||||
"initial_password": username}
|
||||
"initial_password": initial_password}
|
||||
)
|
||||
self._send_json(200, payload)
|
||||
finally:
|
||||
@@ -965,10 +967,11 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
except (TypeError, ValueError):
|
||||
self._send_json(400, {"status": "error", "message": "必须指定有效的 company_id。"})
|
||||
return
|
||||
# Product decision (B-40, 2026-08-08): the initial password equals
|
||||
# the username, and must_change_password forces a change at first
|
||||
# login. Password reset keeps a random one-time password instead.
|
||||
initial_password = username
|
||||
# The initial password is a random one-time value shown only in
|
||||
# this creation response, never stored plaintext or logged;
|
||||
# must_change_password forces a change at first login. Password
|
||||
# reset keeps a random one-time password instead.
|
||||
initial_password = auth.generate_initial_password(exclude=username)
|
||||
try:
|
||||
user_id = auth.create_user(
|
||||
connection,
|
||||
@@ -981,8 +984,8 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
except ValueError as exc:
|
||||
self._send_json(400, {"status": "error", "message": str(exc)})
|
||||
return
|
||||
# While the initial password is unchanged it equals the username,
|
||||
# so the username itself must stay out of audit details.
|
||||
# The random one-time password is never written to audit detail;
|
||||
# only the company binding is recorded.
|
||||
auth.audit(
|
||||
connection,
|
||||
"user_create",
|
||||
@@ -1142,28 +1145,34 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
}
|
||||
)
|
||||
elif result.status == "duplicate":
|
||||
sheet = connection.execute(
|
||||
"""
|
||||
SELECT bank_name, template_id, header_row, period_start, period_end,
|
||||
transaction_count, warnings
|
||||
FROM sheet_batches
|
||||
WHERE import_batch_id = ?
|
||||
ORDER BY id LIMIT 1
|
||||
""",
|
||||
(result.batch_id,),
|
||||
).fetchone()
|
||||
if sheet is not None:
|
||||
payload.update(
|
||||
{
|
||||
"bank": sheet["bank_name"],
|
||||
"template": sheet["template_id"],
|
||||
"header_row": sheet["header_row"],
|
||||
"period_start": sheet["period_start"],
|
||||
"period_end": sheet["period_end"],
|
||||
"transactions": sheet["transaction_count"],
|
||||
"warnings": json.loads(sheet["warnings"]),
|
||||
}
|
||||
)
|
||||
# Same-company duplicates keep their idempotent summary so the
|
||||
# cashier sees the reused batch's bank/template/period/count.
|
||||
# Cross-company duplicates are opaque: only the generic duplicate
|
||||
# status and the uploader's own new batch id are returned, never
|
||||
# the other company's batch id, summary or diagnostics.
|
||||
if result.duplicate_same_company:
|
||||
sheet = connection.execute(
|
||||
"""
|
||||
SELECT bank_name, template_id, header_row, period_start,
|
||||
period_end, transaction_count, warnings
|
||||
FROM sheet_batches
|
||||
WHERE import_batch_id = ?
|
||||
ORDER BY id LIMIT 1
|
||||
""",
|
||||
(result.batch_id,),
|
||||
).fetchone()
|
||||
if sheet is not None:
|
||||
payload.update(
|
||||
{
|
||||
"bank": sheet["bank_name"],
|
||||
"template": sheet["template_id"],
|
||||
"header_row": sheet["header_row"],
|
||||
"period_start": sheet["period_start"],
|
||||
"period_end": sheet["period_end"],
|
||||
"transactions": sheet["transaction_count"],
|
||||
"warnings": json.loads(sheet["warnings"]),
|
||||
}
|
||||
)
|
||||
return payload
|
||||
|
||||
def _read_upload(self) -> tuple[str, bytes, dict[str, str]]:
|
||||
|
||||
Reference in New Issue
Block a user