From 00212ea331614882fb38c5ab6d88917a8fb23f7f Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 29 Jul 2026 14:32:54 +0200 Subject: [PATCH] fix: bound organization aggregate responses --- .../backend/api/v1/routes.py | 93 +++++++++++++++++-- webui/package.json | 2 +- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/govoplan_organizations/backend/api/v1/routes.py b/src/govoplan_organizations/backend/api/v1/routes.py index f6252fe..de6be37 100644 --- a/src/govoplan_organizations/backend/api/v1/routes.py +++ b/src/govoplan_organizations/backend/api/v1/routes.py @@ -55,6 +55,8 @@ from .schemas import ( router = APIRouter(prefix="/organizations", tags=["organizations"]) +ORGANIZATION_MODEL_COLLECTION_LIMIT = 5_000 +ORGANIZATION_MODEL_TOTAL_LIMIT = 20_000 ORG_READ_SCOPES = ( "organizations:model:read", @@ -335,15 +337,90 @@ def get_organization_model( principal: ApiPrincipal = Depends(require_any_scope(*ORG_READ_SCOPES)), ) -> OrganizationModelResponse: tenant_id = _tenant_id(principal) - return OrganizationModelResponse( - unit_types=[_item_unit_type(item) for item in session.query(OrganizationUnitType).filter(OrganizationUnitType.tenant_id == tenant_id).order_by(OrganizationUnitType.name.asc()).all()], - structures=[_item_structure(item) for item in session.query(OrganizationStructure).filter(OrganizationStructure.tenant_id == tenant_id).order_by(OrganizationStructure.name.asc()).all()], - relation_types=[_item_relation_type(item) for item in session.query(OrganizationRelationType).filter(OrganizationRelationType.tenant_id == tenant_id).order_by(OrganizationRelationType.name.asc()).all()], - units=[_item_unit(item) for item in session.query(OrganizationUnit).filter(OrganizationUnit.tenant_id == tenant_id).order_by(OrganizationUnit.name.asc()).all()], - relations=[_item_relation(item) for item in session.query(OrganizationRelation).filter(OrganizationRelation.tenant_id == tenant_id).order_by(OrganizationRelation.created_at.asc()).all()], - function_types=[_item_function_type(item) for item in session.query(OrganizationFunctionType).filter(OrganizationFunctionType.tenant_id == tenant_id).order_by(OrganizationFunctionType.name.asc()).all()], - functions=[_item_function(item) for item in session.query(OrganizationFunction).filter(OrganizationFunction.tenant_id == tenant_id).order_by(OrganizationFunction.name.asc()).all()], + unit_types = _bounded_organization_rows( + session.query(OrganizationUnitType) + .filter(OrganizationUnitType.tenant_id == tenant_id) + .order_by(OrganizationUnitType.name.asc()), + "unit types", ) + structures = _bounded_organization_rows( + session.query(OrganizationStructure) + .filter(OrganizationStructure.tenant_id == tenant_id) + .order_by(OrganizationStructure.name.asc()), + "structures", + ) + relation_types = _bounded_organization_rows( + session.query(OrganizationRelationType) + .filter(OrganizationRelationType.tenant_id == tenant_id) + .order_by(OrganizationRelationType.name.asc()), + "relation types", + ) + units = _bounded_organization_rows( + session.query(OrganizationUnit) + .filter(OrganizationUnit.tenant_id == tenant_id) + .order_by(OrganizationUnit.name.asc()), + "units", + ) + relations = _bounded_organization_rows( + session.query(OrganizationRelation) + .filter(OrganizationRelation.tenant_id == tenant_id) + .order_by(OrganizationRelation.created_at.asc()), + "relations", + ) + function_types = _bounded_organization_rows( + session.query(OrganizationFunctionType) + .filter(OrganizationFunctionType.tenant_id == tenant_id) + .order_by(OrganizationFunctionType.name.asc()), + "function types", + ) + functions = _bounded_organization_rows( + session.query(OrganizationFunction) + .filter(OrganizationFunction.tenant_id == tenant_id) + .order_by(OrganizationFunction.name.asc()), + "functions", + ) + if sum( + len(items) + for items in ( + unit_types, + structures, + relation_types, + units, + relations, + function_types, + functions, + ) + ) > ORGANIZATION_MODEL_TOTAL_LIMIT: + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail=( + "The organization model is too large for the aggregate " + "endpoint and cannot be returned as one response." + ), + ) + return OrganizationModelResponse( + unit_types=[_item_unit_type(item) for item in unit_types], + structures=[_item_structure(item) for item in structures], + relation_types=[_item_relation_type(item) for item in relation_types], + units=[_item_unit(item) for item in units], + relations=[_item_relation(item) for item in relations], + function_types=[_item_function_type(item) for item in function_types], + functions=[_item_function(item) for item in functions], + ) + + +def _bounded_organization_rows(query: Any, label: str) -> list[Any]: + rows = query.limit(ORGANIZATION_MODEL_COLLECTION_LIMIT + 1).all() + if len(rows) > ORGANIZATION_MODEL_COLLECTION_LIMIT: + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail=( + f"The organization model has more than " + f"{ORGANIZATION_MODEL_COLLECTION_LIMIT} {label} and cannot " + "be returned as one response." + ), + ) + return rows @router.post("/unit-types", response_model=OrganizationUnitTypeItem, status_code=status.HTTP_201_CREATED) diff --git a/webui/package.json b/webui/package.json index 32fc4e1..2c6d1ac 100644 --- a/webui/package.json +++ b/webui/package.json @@ -22,7 +22,7 @@ "lucide-react": "^1.23.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router-dom": "^7.1.1", + "react-router-dom": ">=7.18.2 <8", "typescript": "^5.7.2", "vite": "^6.0.6" },