fix: Fail-Closed 字段级安全加固 — 堵住15+端点价格泄露
## stock.py
- _make_price_stripper(): 工厂函数,选单前缀自动剥离所有价格/成本字段
- _do_get_stock_list(permission_prefix): 每个item.to_dict()后剥离价格
- /all 端点: 非AI模式自动剥离价格字段
- /list 端点: permission_prefix='outbound_selection' 传递
## outbound.py
- /bom-match-stock: 返回前按stock_type剥离全部价格/成本字段
- /scan: result.pop('price', None)
## scrap.py
- /scan: result.pop('price', None)
- /records: 每条记录剥离 cost_at_scrap, total_loss
## transactions.py
- filter_item_by_permissions: 从空字典恢复完整20字段映射
- /borrow/stock-list: permission_prefix='op_borrow_apply' 传递
## buy.py / semi.py / product.py
- submit 成功响应剥离所有价格/成本字段(不泄露给前端)
## bom.py
- /base/list: 剥离 referencePrice
This commit is contained in:
@ -150,8 +150,11 @@ def get_all_stock():
|
||||
支持 AI 极简模式: ?ai_mode=true
|
||||
- 只返回 name / spec / availableQuantity 三个字段
|
||||
- 键名压缩为 n / s / c
|
||||
|
||||
★ Fail-Closed: 非 AI 模式自动剥离所有价格成本字段
|
||||
"""
|
||||
ai_mode = request.args.get('ai_mode', '').lower() == 'true'
|
||||
_strip = _make_price_stripper('inventory_stocktake')
|
||||
|
||||
try:
|
||||
all_items = []
|
||||
@ -170,7 +173,9 @@ def get_all_stock():
|
||||
'c': float(item.available_quantity or 0)
|
||||
})
|
||||
else:
|
||||
all_items.append(item.to_dict())
|
||||
d = item.to_dict()
|
||||
_strip(d, 'buy')
|
||||
all_items.append(d)
|
||||
|
||||
# 2. 半成品
|
||||
if StockSemi:
|
||||
@ -187,7 +192,9 @@ def get_all_stock():
|
||||
'c': float(item.available_quantity or 0)
|
||||
})
|
||||
else:
|
||||
all_items.append(item.to_dict())
|
||||
d = item.to_dict()
|
||||
_strip(d, 'semi')
|
||||
all_items.append(d)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -206,7 +213,9 @@ def get_all_stock():
|
||||
'c': float(item.available_quantity or 0)
|
||||
})
|
||||
else:
|
||||
all_items.append(item.to_dict())
|
||||
d = item.to_dict()
|
||||
_strip(d, 'product')
|
||||
all_items.append(d)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -219,9 +228,41 @@ def get_all_stock():
|
||||
# ==============================================================================
|
||||
# 分页库存查询接口(服务端分页,出库/盘点/借用模块共用)
|
||||
# ==============================================================================
|
||||
def _do_get_stock_list():
|
||||
def _make_price_stripper(permission_prefix=None):
|
||||
"""
|
||||
Fail-Closed 价格字段剥离器工厂。
|
||||
|
||||
选单/借用场景默认剥离所有价格字段;
|
||||
仅当 permission_prefix 不在已知选单前缀列表中时放行(例如 material_list 场景由调用方自行处理)。
|
||||
"""
|
||||
# 已知的选单/借用前缀 → 无条件剥离所有价格成本
|
||||
SELECTION_PREFIXES = {'outbound_selection', 'op_borrow_apply', 'inventory_stocktake'}
|
||||
|
||||
def stripper(d, stock_category):
|
||||
if permission_prefix is None or permission_prefix in SELECTION_PREFIXES:
|
||||
if stock_category == 'buy':
|
||||
for k in ('unit_price', 'post_tax_unit_price', 'total_price',
|
||||
'tax_rate', 'currency', 'exchange_rate',
|
||||
'pre_tax_unit_price', 'qty_inbound', 'in_quantity'):
|
||||
d.pop(k, None)
|
||||
elif stock_category == 'semi':
|
||||
for k in ('raw_material_cost', 'manual_cost', 'unit_total_cost',
|
||||
'total_price', 'unit_price'):
|
||||
d.pop(k, None)
|
||||
elif stock_category == 'product':
|
||||
for k in ('raw_material_cost', 'manual_cost', 'unit_total_cost',
|
||||
'sale_price', 'unit_price'):
|
||||
d.pop(k, None)
|
||||
return stripper
|
||||
|
||||
|
||||
def _do_get_stock_list(permission_prefix=None):
|
||||
"""
|
||||
分页获取库存列表(stock_quantity > 0) — 裸逻辑,供各模块复用
|
||||
|
||||
Args:
|
||||
permission_prefix: 可选权限前缀(如 'outbound_selection' / 'op_borrow_apply')。
|
||||
传入后自动剥离对应模块无权查看的价格/成本字段。
|
||||
"""
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
@ -233,6 +274,9 @@ def _do_get_stock_list():
|
||||
if pageSize < 1 or pageSize > 200:
|
||||
pageSize = 20
|
||||
|
||||
# ★ Fail-Closed: 选单/借用场景默认剥离所有价格成本字段
|
||||
_strip_price_fields = _make_price_stripper(permission_prefix)
|
||||
|
||||
all_items = []
|
||||
|
||||
# 1. 采购件
|
||||
@ -255,6 +299,7 @@ def _do_get_stock_list():
|
||||
d['name'] = d.get('material_name', d.get('name', ''))
|
||||
d['standard'] = d.get('spec_model', d.get('standard', ''))
|
||||
d['available_quantity'] = d.get('qty_available', d.get('available_quantity', 0))
|
||||
_strip_price_fields(d, 'buy')
|
||||
all_items.append(d)
|
||||
|
||||
# 2. 半成品
|
||||
@ -278,6 +323,7 @@ def _do_get_stock_list():
|
||||
d['name'] = d.get('material_name', d.get('name', ''))
|
||||
d['standard'] = d.get('spec_model', d.get('standard', ''))
|
||||
d['available_quantity'] = d.get('qty_available', d.get('available_quantity', 0))
|
||||
_strip_price_fields(d, 'semi')
|
||||
all_items.append(d)
|
||||
except Exception:
|
||||
pass
|
||||
@ -304,6 +350,7 @@ def _do_get_stock_list():
|
||||
d['name'] = d.get('material_name', d.get('name', ''))
|
||||
d['standard'] = d.get('spec_model', d.get('standard', ''))
|
||||
d['available_quantity'] = d.get('qty_available', d.get('available_quantity', 0))
|
||||
_strip_price_fields(d, 'product')
|
||||
all_items.append(d)
|
||||
|
||||
# ── 按规格+库位聚合(出库选单合并同类项)───────────────────────
|
||||
@ -360,8 +407,8 @@ def _do_get_stock_list():
|
||||
@jwt_required()
|
||||
@permission_required('outbound_selection')
|
||||
def get_stock_list():
|
||||
"""出库选单专用库存列表"""
|
||||
return _do_get_stock_list()
|
||||
"""出库选单专用库存列表 — Fail-Closed: 剥离价格字段"""
|
||||
return _do_get_stock_list(permission_prefix='outbound_selection')
|
||||
|
||||
|
||||
# --- 草稿箱接口 ---
|
||||
|
||||
Reference in New Issue
Block a user