Files
KCGL/inventory-backend/app/utils/field_permissions.py
yueli 3c0954598c perf: 综合安全加固 — RBAC严格映射+异步邮件+字段权限白名单+前端对齐+导入模板
本次提交包含本会话所有修改的最终统一提交

## 权限系统重构
- permission_service.py: 添加入库/采购操作元素 + ensure_default_permissions
- field_permissions.py: 严格1-to-1 Default Deny 字段映射(StockBuy/Semi/Product/MaterialBase)
- decorators.py: _expand_operation_perms 双向粒度桥接 + prevent_double_submit
- deploy_production.sql: 修复 sys_element 别名码(qty_inbound→in_quantity)

## 采购模块
- purchase.py: 权限驱动可见性 + inbound_purchase独立权限 + 价格字段过滤
- purchase_service.py: 异步邮件 + 三阶段批量模糊匹配防N+1
- purchase/index.vue: canApprove严格操作权限 + upload重复修复

## 导出/入
- base_service.py: export_excel 流式写入防OOM + get_latest_specs 优化
- import_service.py + import_api.py: Excel批量导入(模板+预览+执行)
- ImportDialog.vue: 三步骤导入弹窗

## 异步邮件
- email_service.py: send_email_async (守护线程)
- inventory_task.py: send_email→send_email_async

## 前端对齐
- product/semi/buy.vue: 列对齐in_quantity/stock_quantity/available_quantity + localStorage缓存V2
- buyOdoo.vue: 排序修复 + 导入按钮 + 移除点击展开加载
- BomManage.vue: 懒加载分组 + 导入按钮
- list.vue: 导入按钮
- Selection.vue + borrow/apply: BOM匹配修复 + 导入按钮
- outbound/create.vue: 出库类型必选
- AppMain.vue: 移除transition白屏修复
- material_base.ts, outbound.ts, bom.ts, stock.ts: 新增API函数
2026-07-17 13:07:12 +08:00

124 lines
6.9 KiB
Python

"""
入库模块字段级权限严格映射 (Default Deny)
规则:
- None = 基础字段,始终保留
- 字符串 = 需要用户拥有该精确权限码,否则移除
- 不在映射中的字段 → 立即删除 (Default Deny)
"""
STOCK_FIELD_RBAC_MAPPING = {
"MaterialBase": {
"id": None, "isEnabled": None, "visibilityLevel": None,
"name": "material_list:name", "commonName": "material_list:commonName",
"category": "material_list:category", "type": "material_list:type",
"spec": "material_list:spec", "unit": "material_list:unit",
"companyName": "material_list:companyName", "isInspectionRequired": "material_list:isInspectionRequired",
"generalImage": "material_list:files", "generalManual": "material_list:files",
"productImageRemark": "material_list:productImageRemark",
"manualLinkRemark": "material_list:manualLinkRemark",
"referencePrice": "material_list:referencePrice",
"inventoryCount": "material_list:inventoryCount",
"availableCount": "material_list:availableCount",
},
"StockBuy": {
"id": None, "request_id": None, "request_no": None,
"in_quantity": None, "stock_quantity": None, "available_quantity": None,
"warehouse_loc": None, "status": None, "global_print_id": None,
"company_name": "material_list:companyName", "material_name": "material_list:name",
"spec_model": "material_list:spec", "category": "material_list:category",
"unit": "material_list:unit", "material_type": "material_list:type",
"isInspectionRequired": "material_list:isInspectionRequired",
"sku": "inbound_buy:sku", "inbound_date": "inbound_buy:inbound_date",
"barcode": "inbound_buy:barcode", "serial_number": "inbound_buy:sn_bn",
"batch_number": "inbound_buy:sn_bn", "inspection_status": "inbound_buy:inspection_status",
"unit_price": "inbound_buy:unit_price", "post_tax_unit_price": "inbound_buy:post_tax_unit_price",
"total_price": "inbound_buy:total_price", "tax_rate": "inbound_buy:tax_rate",
"currency": "inbound_buy:currency", "exchange_rate": "inbound_buy:exchange_rate",
"supplier_name": "inbound_buy:supplier_name", "purchaser": "inbound_buy:purchaser",
"purchaser_email": "inbound_buy:purchaser_email", "source_link": "inbound_buy:source_link",
"detail_link": "inbound_buy:detail_link", "arrival_photo": "inbound_buy:arrival_photo",
"inspection_report": "inbound_buy:inspection_report",
},
"StockSemi": {
"id": None, "in_quantity": None, "stock_quantity": None, "available_quantity": None,
"warehouse_loc": None, "status": None, "global_print_id": None,
"company_name": "material_list:companyName", "material_name": "material_list:name",
"spec_model": "material_list:spec", "category": "material_list:category",
"unit": "material_list:unit", "material_type": "material_list:type",
"sku": "inbound_semi:sku", "inbound_date": "inbound_semi:inbound_date",
"barcode": "inbound_semi:barcode", "serial_number": "inbound_semi:sn_bn",
"batch_number": "inbound_semi:sn_bn", "bom_code": "inbound_semi:bom_code",
"bom_version": "inbound_semi:bom_version", "work_order_code": "inbound_semi:work_order_code",
"raw_material_cost": "inbound_semi:raw_material_cost", "manual_cost": "inbound_semi:manual_cost",
"unit_total_cost": "inbound_semi:unit_total_cost", "total_price": "inbound_semi:total_price",
"production_manager": "inbound_semi:production_manager",
"production_time_range": "inbound_semi:production_time_range",
"production_start_time": "inbound_semi:production_start_time",
"production_end_time": "inbound_semi:production_end_time",
"quality_status": "inbound_semi:quality_status", "quality_report_link": "inbound_semi:quality_report_link",
"arrival_photo": "inbound_semi:arrival_photo", "remark": "inbound_semi:remark",
"detail_link": "inbound_semi:detail_link",
},
"StockProduct": {
"id": None, "in_quantity": None, "stock_quantity": None, "available_quantity": None,
"warehouse_loc": None, "status": None, "global_print_id": None,
"company_name": "material_list:companyName", "material_name": "material_list:name",
"spec_model": "material_list:spec", "category": "material_list:category",
"unit": "material_list:unit", "material_type": "material_list:type",
"sku": "inbound_product:sku", "inbound_date": "inbound_product:inbound_date",
"barcode": "inbound_product:barcode", "serial_number": "inbound_product:serial_number",
"bom_code": "inbound_product:bom_code", "bom_version": "inbound_product:bom_version",
"work_order_code": "inbound_product:work_order_code",
"raw_material_cost": "inbound_product:raw_material_cost", "manual_cost": "inbound_product:manual_cost",
"unit_total_cost": "inbound_product:unit_total_cost",
"production_manager": "inbound_product:production_manager",
"production_time_range": "inbound_product:production_time_range",
"quality_status": "inbound_product:quality_status",
"quality_report_link": "inbound_product:quality_report_link",
"inspection_report_link": "inbound_product:inspection_report_link",
"sale_price": "inbound_product:sale_price", "order_id": "inbound_product:order_id",
"product_photo": "inbound_product:product_photo", "remark": "inbound_product:remark",
"detail_link": "inbound_product:detail_link",
},
}
def _is_super_admin(user_permissions: list) -> bool:
"""判断是否为超级管理员(支持 * 和 module:* 两种通配符)"""
return '*' in user_permissions or any(p.endswith(':*') for p in user_permissions)
def apply_strict_rbac(item_dict: dict, table_name: str, user_permissions: list) -> dict:
"""
Default Deny 字段过滤器:
1. 不在映射中的 key → 立即删除
2. 在映射中但需要权限且用户没有 → 删除
3. 在映射中且为 None 或用户有权限 → 保留
"""
mapping = STOCK_FIELD_RBAC_MAPPING.get(table_name)
if not mapping:
return item_dict # 未知表不做过滤
# 超级管理员放行(支持 * / module:* 通配符)
if _is_super_admin(user_permissions):
for key in list(item_dict.keys()):
if key not in mapping:
del item_dict[key]
return item_dict
for key in list(item_dict.keys()):
if key not in mapping:
del item_dict[key] # Default Deny
else:
perm_code = mapping[key]
if perm_code is not None and perm_code not in user_permissions:
if isinstance(item_dict[key], (int, float)):
item_dict[key] = 0
elif isinstance(item_dict[key], bool):
item_dict[key] = False
else:
item_dict[key] = None
return item_dict