diff --git a/application/single_app/config.py b/application/single_app/config.py index 30cb1b95..b3419b56 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.241.007" +VERSION = "0.241.008" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/functions_settings.py b/application/single_app/functions_settings.py index 0091d0a3..57af08e9 100644 --- a/application/single_app/functions_settings.py +++ b/application/single_app/functions_settings.py @@ -900,6 +900,21 @@ def apply_custom_endpoint_setting_migration(settings_item): return updated + +def _get_default_management_cloud_from_environment(): + """Map AZURE_ENVIRONMENT to the default model endpoint management cloud value.""" + environment = str(AZURE_ENVIRONMENT or "").strip().lower() + if environment in ("usgovernment", "government"): + return "government" + return "public" + + +def _is_management_cloud_user_editable(endpoint_provider, endpoint_auth_type): + """Return True only when the admin UI exposes management cloud selection.""" + provider = str(endpoint_provider or "").strip().lower() + auth_type = str(endpoint_auth_type or "").strip().lower() + return provider in ("aifoundry", "new_foundry") and auth_type == "service_principal" + def normalize_model_endpoints(endpoints): """Normalize model endpoints with stable IDs and enabled flags.""" if not isinstance(endpoints, list): @@ -907,6 +922,7 @@ def normalize_model_endpoints(endpoints): normalized = [] changed = False + default_management_cloud = _get_default_management_cloud_from_environment() for endpoint in endpoints: if not isinstance(endpoint, dict): @@ -926,6 +942,23 @@ def normalize_model_endpoints(endpoints): endpoint_copy["enabled"] = True changed = True + auth = endpoint_copy.get("auth") + if not isinstance(auth, dict): + auth = {} + endpoint_copy["auth"] = auth + changed = True + + auth_type = str(auth.get("type") or "").strip().lower() + provider = str(endpoint_copy.get("provider") or "").strip().lower() + management_cloud = str(auth.get("management_cloud") or "").strip().lower() + cloud_user_editable = _is_management_cloud_user_editable(provider, auth_type) + + # When cloud selection is not user-editable in the admin UI, do not trust + # posted defaults and always align cloud behavior with AZURE_ENVIRONMENT. + if (not cloud_user_editable and management_cloud != default_management_cloud) or not management_cloud: + auth["management_cloud"] = default_management_cloud + changed = True + models = endpoint_copy.get("models") or [] normalized_models = [] for model in models: diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index db17633d..3c606d3f 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -4,6 +4,21 @@ This page tracks notable Simple Chat releases and organizes the detailed change For feature-focused and fix-focused drill-downs by version, see [Features by Version](/explanation/features/) and [Fixes by Version](/explanation/fixes/). +### **(v0.241.008)** + +#### Bug Fixes + +* **Model Endpoint Management Cloud Default Alignment** + * Fixed model endpoint save behavior so new endpoint records no longer default `management_cloud` to `public` when cloud selection is not explicitly provided. + * Endpoint normalization now derives the default cloud from `AZURE_ENVIRONMENT`, mapping `usgovernment`/`government` to `government` and all other environments to `public`, which prevents Gov audience and token-scope mismatches during model calls. + * (Ref: `functions_settings.py`, model endpoint normalization, `AZURE_ENVIRONMENT`) + +* **Model Endpoint Cloud Enforcement for Hidden Admin Fields** + * Fixed a follow-up issue where model endpoint saves could still persist `auth.management_cloud` as `public` when the cloud selector was not exposed in the Admin Model Endpoints UI. + * Model endpoint normalization now enforces `management_cloud` from `AZURE_ENVIRONMENT` whenever cloud selection is not user-editable, preventing hidden UI defaults from causing Government token audience and scope mismatches. + * Added focused functional regression coverage for the new normalization guard. + * (Ref: `functions_settings.py`, `test_model_endpoint_management_cloud_default.py`, model endpoint normalization, `AZURE_ENVIRONMENT`) + ### **(v0.241.007)** ## New Feature diff --git a/functional_tests/test_model_endpoint_management_cloud_default.py b/functional_tests/test_model_endpoint_management_cloud_default.py new file mode 100644 index 00000000..a1dcf7d1 --- /dev/null +++ b/functional_tests/test_model_endpoint_management_cloud_default.py @@ -0,0 +1,40 @@ +# test_model_endpoint_management_cloud_default.py +""" +Functional test for model endpoint management cloud normalization. +Version: 0.241.008 +Implemented in: 0.241.008 + +This test ensures model endpoint normalization enforces management_cloud from +AZURE_ENVIRONMENT when cloud selection is not user-editable in the admin UI. +""" + +import os + + +def read_file_text(file_path): + with open(file_path, "r", encoding="utf-8") as file: + return file.read() + + +def test_management_cloud_enforced_for_non_editable_paths(): + """Ensure hidden cloud selector paths do not persist incorrect defaults.""" + print("🔍 Validating management_cloud enforcement for hidden selector paths...") + + repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + settings_path = os.path.join( + repo_root, + "application", + "single_app", + "functions_settings.py", + ) + source = read_file_text(settings_path) + + assert "def _is_management_cloud_user_editable" in source + assert "provider in (\"aifoundry\", \"new_foundry\") and auth_type == \"service_principal\"" in source + assert "if (not cloud_user_editable and management_cloud != default_management_cloud) or not management_cloud:" in source + + print("✅ Management cloud normalization enforcement is wired.") + + +if __name__ == "__main__": + test_management_cloud_enforced_for_non_editable_paths()