security: harden connector policy and add dashboard widget

This commit is contained in:
2026-07-29 14:16:29 +02:00
parent 93eea839c8
commit 5b868272b9
9 changed files with 416 additions and 32 deletions

View File

@@ -348,8 +348,8 @@ def _file_connector_policy_resource_id(scope_type: str, scope_id: str | None) ->
return f"{scope_type.strip().casefold()}:{scope_id or ''}"
async def _read_limited_upload(upload: UploadFile, *, max_bytes: int) -> bytes:
data = await upload.read(max_bytes + 1)
def _read_limited_upload(upload: UploadFile, *, max_bytes: int) -> bytes:
data = upload.file.read(max_bytes + 1)
if len(data) > max_bytes:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
@@ -358,12 +358,12 @@ async def _read_limited_upload(upload: UploadFile, *, max_bytes: int) -> bytes:
return data
async def _spool_limited_upload_to_temp(upload: UploadFile, *, max_bytes: int, suffix: str = ".upload") -> str:
def _spool_limited_upload_to_temp(upload: UploadFile, *, max_bytes: int, suffix: str = ".upload") -> str:
tmp = tempfile.NamedTemporaryFile(prefix="govoplan-upload-", suffix=suffix, delete=False)
total = 0
try:
while True:
chunk = await upload.read(1024 * 1024)
chunk = upload.file.read(1024 * 1024)
if not chunk:
break
total += len(chunk)
@@ -1205,7 +1205,7 @@ def _cursor_page_size(scope: str, cursor: str | None, explicit_page_size: int |
if explicit_page_size is not None:
return explicit_page_size
if not cursor:
return None
return DEFAULT_FILE_LIST_PAGE_SIZE
try:
values = decode_keyset_cursor(scope, cursor)
except KeysetCursorError as exc:
@@ -2094,7 +2094,7 @@ def list_files(
@router.post("/upload", response_model=FileUploadResponse)
async def upload_files(
def upload_files(
files: list[UploadFile] = FastAPIFile(...),
owner_type: Literal["user", "group"] = Form(default="user"),
owner_id: str | None = Form(default=None),
@@ -2121,7 +2121,7 @@ async def upload_files(
content_type = upload.content_type or None
upload_limit = settings.file_upload_zip_max_bytes if unpack_zip and filename.lower().endswith(".zip") else settings.file_upload_max_bytes
if unpack_zip and filename.lower().endswith(".zip"):
zip_path = await _spool_limited_upload_to_temp(upload, max_bytes=upload_limit, suffix=".zip")
zip_path = _spool_limited_upload_to_temp(upload, max_bytes=upload_limit, suffix=".zip")
try:
extracted = extract_zip_upload(
session,
@@ -2143,7 +2143,7 @@ async def upload_files(
_cleanup_temp_file(zip_path)
uploaded_assets.extend(item.asset for item in extracted)
continue
data = await _read_limited_upload(upload, max_bytes=upload_limit)
data = _read_limited_upload(upload, max_bytes=upload_limit)
stored = create_file_asset(
session,
tenant_id=principal.tenant_id,
@@ -2173,7 +2173,7 @@ async def upload_files(
@router.post("/upload-zip", response_model=FileUploadResponse)
async def upload_zip(
def upload_zip(
file: UploadFile = FastAPIFile(...),
owner_type: Literal["user", "group"] = Form(default="user"),
owner_id: str | None = Form(default=None),
@@ -2194,7 +2194,7 @@ async def upload_zip(
upload_resolutions = _conflict_resolutions([ConflictResolutionRequest(**item) for item in raw_resolutions])
_enforce_connector_policy(source_provenance_json, connector_policy_json, operation="import")
metadata = _source_metadata_from_form(source_provenance_json, source_revision)
zip_path = await _spool_limited_upload_to_temp(file, max_bytes=settings.file_upload_zip_max_bytes, suffix=".zip")
zip_path = _spool_limited_upload_to_temp(file, max_bytes=settings.file_upload_zip_max_bytes, suffix=".zip")
extracted = extract_zip_upload(
session,
tenant_id=principal.tenant_id,