|
3 | 3 | import asyncio |
4 | 4 | import json |
5 | 5 | import time |
6 | | -from collections.abc import AsyncGenerator |
| 6 | +from collections.abc import AsyncGenerator, Collection |
7 | 7 | from http import HTTPStatus |
8 | 8 | from typing import TYPE_CHECKING, Annotated, Any |
9 | 9 | from uuid import UUID, uuid4 |
|
80 | 80 | from langflow.services.database.models.user.model import User, UserRead |
81 | 81 | from langflow.services.deps import ( |
82 | 82 | get_auth_service, |
| 83 | + get_catalog_policy_service, |
83 | 84 | get_job_service, |
84 | 85 | get_memory_base_service, |
85 | 86 | get_session_service, |
@@ -180,22 +181,34 @@ async def parse_input_request_from_body(http_request: Request) -> SimplifiedAPIR |
180 | 181 |
|
181 | 182 |
|
182 | 183 | @router.get("/all") |
183 | | -async def get_all(request: Request, current_user: CurrentActiveUser): |
| 184 | +async def get_all(request: Request, current_user: CurrentActiveUser, *, include_blocked: bool = False): |
184 | 185 | """Retrieve all component types with compression for better performance. |
185 | 186 |
|
186 | 187 | Returns a compressed response containing all available component types, |
187 | 188 | with display_names translated to the locale indicated by Accept-Language. |
188 | 189 | """ |
| 190 | + if include_blocked and not current_user.is_superuser: |
| 191 | + raise HTTPException( |
| 192 | + status_code=status.HTTP_403_FORBIDDEN, |
| 193 | + detail="Only superusers can include blocked catalog components.", |
| 194 | + ) |
| 195 | + |
189 | 196 | from langflow.interface.components import get_and_cache_all_types_dict |
190 | 197 | from langflow.utils.i18n import build_component_display_names, translate_component_dict |
191 | 198 |
|
192 | 199 | try: |
| 200 | + catalog_policy_snapshot = get_catalog_policy_service().snapshot |
193 | 201 | all_types_en = await get_and_cache_all_types_dict(settings_service=get_settings_service()) |
194 | 202 | visible_types_en = _filter_component_palette_by_provider_policy( |
195 | 203 | all_types_en, |
196 | 204 | user_id=current_user.id, |
197 | 205 | attributes={"is_superuser": bool(current_user.is_superuser)}, |
198 | 206 | ) |
| 207 | + if not include_blocked: |
| 208 | + visible_types_en = _filter_component_palette_by_catalog_policy( |
| 209 | + visible_types_en, |
| 210 | + blocked_component_keys=catalog_policy_snapshot.blocked_component_keys, |
| 211 | + ) |
199 | 212 |
|
200 | 213 | locale = getattr(request.state, "locale", "en") |
201 | 214 | all_types = translate_component_dict(visible_types_en, locale) if locale != "en" else visible_types_en |
@@ -248,6 +261,28 @@ def _filter_component_palette_by_provider_policy( |
248 | 261 | } |
249 | 262 |
|
250 | 263 |
|
| 264 | +def _filter_component_palette_by_catalog_policy( |
| 265 | + all_types: dict[str, dict[str, dict]], |
| 266 | + *, |
| 267 | + blocked_component_keys: Collection[str], |
| 268 | +) -> dict[str, dict[str, dict]]: |
| 269 | + """Return shallow category copies with exact blocked component keys removed. |
| 270 | +
|
| 271 | + Catalog policy keys match the inner component-registry keys exactly and |
| 272 | + case-sensitively across every category. Category order, component order, |
| 273 | + and empty categories are preserved. Component payloads remain shared with |
| 274 | + the process-wide cache and are never mutated or deep-copied. |
| 275 | + """ |
| 276 | + return { |
| 277 | + category: { |
| 278 | + component_key: component |
| 279 | + for component_key, component in components.items() |
| 280 | + if component_key not in blocked_component_keys |
| 281 | + } |
| 282 | + for category, components in all_types.items() |
| 283 | + } |
| 284 | + |
| 285 | + |
251 | 286 | def validate_input_and_tweaks(input_request: SimplifiedAPIRequest) -> None: |
252 | 287 | # If the input_value is not None and the input_type is "chat" |
253 | 288 | # then we need to check the tweaks if the ChatInput component is present |
@@ -1670,14 +1705,27 @@ async def get_config( |
1670 | 1705 | """ |
1671 | 1706 | try: |
1672 | 1707 | settings_service: SettingsService = get_settings_service() |
| 1708 | + try: |
| 1709 | + catalog_governance_enabled = get_catalog_policy_service().enabled |
| 1710 | + except Exception as exc: # noqa: BLE001 |
| 1711 | + # Catalog governance is explicitly fail-open. A broken custom |
| 1712 | + # policy implementation must not break the public config endpoint |
| 1713 | + # or expose its internal exception text. |
| 1714 | + await logger.aexception("Catalog policy status unavailable; reporting governance disabled", exception=exc) |
| 1715 | + catalog_governance_enabled = False |
1673 | 1716 |
|
1674 | 1717 | if user is None: |
1675 | 1718 | return PublicConfigResponse.from_settings( |
1676 | 1719 | settings_service.settings, |
1677 | 1720 | settings_service.auth_settings, |
| 1721 | + catalog_governance_enabled=catalog_governance_enabled, |
1678 | 1722 | ) |
1679 | 1723 |
|
1680 | | - return ConfigResponse.from_settings(settings_service.settings, settings_service.auth_settings) |
| 1724 | + return ConfigResponse.from_settings( |
| 1725 | + settings_service.settings, |
| 1726 | + settings_service.auth_settings, |
| 1727 | + catalog_governance_enabled=catalog_governance_enabled, |
| 1728 | + ) |
1681 | 1729 |
|
1682 | 1730 | except Exception as exc: |
1683 | 1731 | raise HTTPException(status_code=500, detail=str(exc)) from exc |
0 commit comments