68 lines
2.7 KiB
Python
68 lines
2.7 KiB
Python
"""证件附件授权:仅接受当前租户有数据权限的 MinIO 登记对象。"""
|
|
from urllib.parse import unquote, urlsplit
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from app.core.config import settings
|
|
from app.models.crm_models import SysOss
|
|
from app.services.pl_data_scope import assert_pl_row_in_scope
|
|
|
|
|
|
def storage_base_url():
|
|
if settings.MINIO_BASE_URL:
|
|
return settings.MINIO_BASE_URL.rstrip('/')
|
|
scheme = 'https' if settings.MINIO_SECURE else 'http'
|
|
return f'{scheme}://{settings.MINIO_ENDPOINT}/{settings.MINIO_BUCKET}'
|
|
|
|
|
|
def object_key(value, base_url=None):
|
|
raw = (value or '').strip()
|
|
base = (base_url or storage_base_url()).rstrip('/')
|
|
if raw.startswith(base + '/'):
|
|
raw = raw[len(base) + 1:]
|
|
parsed = urlsplit(raw)
|
|
if (not raw or parsed.scheme or parsed.netloc or parsed.query or parsed.fragment
|
|
or raw.startswith('/') or '\\' in raw or ',' in raw
|
|
or any(part in ('', '.', '..') for part in unquote(raw).split('/'))
|
|
or unquote(raw) != raw or any(ord(c) < 32 for c in raw)):
|
|
raise HTTPException(400, '附件必须是本系统上传的MinIO对象')
|
|
return raw
|
|
|
|
|
|
def authorize_attachment(db, user, value, base_url=None):
|
|
base = (base_url or storage_base_url()).rstrip('/')
|
|
key = object_key(value, base)
|
|
row = db.query(SysOss).filter(
|
|
SysOss.tenant_no == user.tenant_no, SysOss.del_flag == 0,
|
|
SysOss.service == 'minio', SysOss.url.in_((key, base + '/' + key)),
|
|
).first()
|
|
if row is None:
|
|
raise HTTPException(403, '附件不存在或不属于当前租户')
|
|
# 统一上传的历史记录未写创建部门;本人上传是明确的对象授权依据。
|
|
# 其他人的文件仍需全角色数据范围,租户与有效状态已在查询中强制限定。
|
|
if not user.id or row.create_by != user.id:
|
|
assert_pl_row_in_scope(db, row, SysOss, user, user.tenant_no)
|
|
return key
|
|
|
|
|
|
def read_image(db, user, value):
|
|
from app.core.storage import MinIOStorage, get_storage
|
|
from app.services.pl_quota_recognition import MAX_IMAGE_BYTES
|
|
key = authorize_attachment(db, user, value)
|
|
store = get_storage()
|
|
if not isinstance(store, MinIOStorage):
|
|
raise HTTPException(503, '证件识别需要MinIO存储')
|
|
response = None
|
|
try:
|
|
response = store.client.get_object(store.bucket_name, key)
|
|
content = response.read(MAX_IMAGE_BYTES + 1)
|
|
except Exception:
|
|
raise HTTPException(502, '读取证件图片失败,请重试或手工录入') from None
|
|
finally:
|
|
if response is not None:
|
|
response.close()
|
|
response.release_conn()
|
|
if len(content) > MAX_IMAGE_BYTES:
|
|
raise HTTPException(400, '证件图片过大')
|
|
return content
|