B-64: 安全修复——随机初始密码与跨公司重复上传隔离
- 公司账号创建改为随机一次性初始密码,只在创建响应中显示一次, 密码保证不等于用户名;删除「用户名即初始密码」兼容分支,继续 强制首次登录改密。 - 跨公司相同字节文件上传只返回通用重复状态:不再返回其他公司的 原批次 ID、银行、模板、期间、交易数或诊断;同公司重复上传的 幂等摘要保持可用。 - 补充服务端回归测试,覆盖同公司与跨公司两个分支及随机密码; 完整测试 85 项全绿,node --check 通过。
This commit is contained in:
@@ -53,8 +53,13 @@ def verify_password(password: str, stored: str) -> bool:
|
||||
return hmac.compare_digest(digest, expected)
|
||||
|
||||
|
||||
def generate_initial_password() -> str:
|
||||
"""Generate a 12-char initial password with upper, lower and digit chars."""
|
||||
def generate_initial_password(exclude: str | None = None) -> str:
|
||||
"""Generate a 12-char initial password with upper, lower and digit chars.
|
||||
|
||||
When ``exclude`` is given, the result is guaranteed to differ from it
|
||||
(case-insensitive) so a fresh account never starts with a password equal
|
||||
to its own username.
|
||||
"""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
while True:
|
||||
password = "".join(
|
||||
@@ -64,6 +69,7 @@ def generate_initial_password() -> str:
|
||||
any(char.isupper() for char in password)
|
||||
and any(char.islower() for char in password)
|
||||
and any(char.isdigit() for char in password)
|
||||
and (exclude is None or password.lower() != exclude.lower())
|
||||
):
|
||||
return password
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class ImportResult:
|
||||
source_file_id: int
|
||||
batches: tuple[StatementBatch, ...] = ()
|
||||
message: str | None = None
|
||||
duplicate_same_company: bool = False # only meaningful when status == 'duplicate'
|
||||
|
||||
|
||||
def import_statement(
|
||||
@@ -96,7 +97,7 @@ def _record_duplicate(
|
||||
) -> ImportResult:
|
||||
original = connection.execute(
|
||||
"""
|
||||
SELECT id FROM import_batches
|
||||
SELECT id, company_id FROM import_batches
|
||||
WHERE source_file_id = ? AND status = 'parsed'
|
||||
ORDER BY id LIMIT 1
|
||||
""",
|
||||
@@ -105,7 +106,7 @@ def _record_duplicate(
|
||||
if original is None:
|
||||
original = connection.execute(
|
||||
"""
|
||||
SELECT id FROM import_batches
|
||||
SELECT id, company_id FROM import_batches
|
||||
WHERE source_file_id = ? AND status != 'duplicate'
|
||||
ORDER BY id LIMIT 1
|
||||
""",
|
||||
@@ -113,7 +114,7 @@ def _record_duplicate(
|
||||
).fetchone()
|
||||
now = utc_now()
|
||||
with connection:
|
||||
connection.execute(
|
||||
cursor = connection.execute(
|
||||
"""
|
||||
INSERT INTO import_batches
|
||||
(source_file_id, status, duplicate_of_id, company_id, diagnostics, created_at, updated_at)
|
||||
@@ -128,12 +129,22 @@ def _record_duplicate(
|
||||
now,
|
||||
),
|
||||
)
|
||||
# A duplicate is only "same company" when the uploader belongs to the
|
||||
# same tenant that owns the original batch. Cross-company duplicates
|
||||
# must stay opaque: the caller gets the new duplicate batch id (which
|
||||
# belongs to its own company) but never the other company's batch id.
|
||||
same_company = original["company_id"] == company_id
|
||||
if same_company:
|
||||
batch_id = original["id"]
|
||||
else:
|
||||
batch_id = int(cursor.lastrowid)
|
||||
return ImportResult(
|
||||
original["id"],
|
||||
batch_id,
|
||||
"duplicate",
|
||||
sha256,
|
||||
source_file_id,
|
||||
message="相同内容的文件已导入,本次按重复上传处理。",
|
||||
duplicate_same_company=same_company,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user