From 3a42c0859015e361c8390afef100eb31fdde9cd3 Mon Sep 17 00:00:00 2001
From: Karel van Klink <karel.vanklink@geant.org>
Date: Tue, 10 Dec 2024 11:49:43 +0100
Subject: [PATCH] Sanitize all extra vars that are sent to LSO

---
 gso/services/lso_client.py       |  9 +++++++--
 requirements.txt                 |  1 +
 setup.py                         |  1 +
 test/services/test_lso_client.py | 28 ++++++++++++++++++++++++++++
 4 files changed, 37 insertions(+), 2 deletions(-)
 create mode 100644 test/services/test_lso_client.py

diff --git a/gso/services/lso_client.py b/gso/services/lso_client.py
index 1529ca1f..9280cbc4 100644
--- a/gso/services/lso_client.py
+++ b/gso/services/lso_client.py
@@ -17,6 +17,7 @@ from orchestrator.workflow import Step, StepList, begin, callback_step, conditio
 from pydantic import ConfigDict
 from pydantic_forms.types import FormGenerator
 from pydantic_forms.validators import Label, LongText, ReadOnlyField
+from unidecode import unidecode
 
 from gso import settings
 
@@ -42,6 +43,9 @@ def _send_request(parameters: dict, callback_route: str) -> None:
         parameters: JSON body for the request, which will almost always at least consist of a subscription object, and a
             boolean value to indicate a dry run.
         callback_route: The callback route that should be used to resume the workflow.
+
+    Raises:
+        HTTPError: When receiving a non-successful status code from LSO.
     """
     oss = settings.load_oss_params()
     params = oss.PROVISIONING_PROXY
@@ -106,12 +110,13 @@ def _execute_playbook(
         callback_route: The endpoint at which GSO expects a callback to continue the workflow executing this step.
         inventory: An inventory of machines at which the playbook is targeted. Must be in YAML-compatible format.
         extra_vars: Any extra variables that the playbook relies on. This can include a subscription object, a boolean
-            value indicating a dry run, a commit comment, etc.
+            value indicating a dry run, a commit comment, etc.  All unicode character values are decoded to prevent
+            sending special characters to remote machines that don't support this.
     """
     parameters = {
         "playbook_name": playbook_name,
         "inventory": inventory,
-        "extra_vars": extra_vars,
+        "extra_vars": json.loads(unidecode(json.dumps(extra_vars, ensure_ascii=False))),
     }
 
     _send_request(parameters, callback_route)
diff --git a/requirements.txt b/requirements.txt
index 2488de13..97fd1fed 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,6 +8,7 @@ celery==5.3.6
 azure-identity==1.16.0
 msgraph-sdk==1.2.0
 ping3==4.0.8
+unidecode==1.3.8
 
 # Test and linting dependencies
 celery-stubs==0.1.3
diff --git a/setup.py b/setup.py
index fa21476f..bce5bfc2 100644
--- a/setup.py
+++ b/setup.py
@@ -21,6 +21,7 @@ setup(
         "azure-identity==1.16.0",
         "msgraph-sdk==1.2.0",
         "ping3==4.0.8",
+        "unidecode==1.3.8",
     ],
     include_package_data=True,
 )
diff --git a/test/services/test_lso_client.py b/test/services/test_lso_client.py
new file mode 100644
index 00000000..2f52072a
--- /dev/null
+++ b/test/services/test_lso_client.py
@@ -0,0 +1,28 @@
+from unittest.mock import patch
+
+from gso.services.lso_client import _execute_playbook
+
+
+@patch("gso.services.lso_client.requests.post")
+def test_replace_unicode_in_lso_call_success(mock_post):
+    extra_vars = {
+        "deployment_description": "I am going to deploy the best GÉANT service EVER!!",
+        "email": "goat@géant.org",
+        "translations": {"ja": "ジェアントのスゴイなサービスをデプロイする"},
+    }
+
+    expected_parameters = {
+        "playbook_name": "playbook.yaml",
+        "inventory": {},
+        "callback": "http://gso-api:9000/api/callback_route",
+        "extra_vars": {
+            "deployment_description": "I am going to deploy the best GEANT service EVER!!",
+            "email": "goat@geant.org",
+            "translations": {"ja": "zieantonosugoinasa-bisuwodepuroisuru"},
+        },
+    }
+
+    execute_playbook = _execute_playbook.__wrapped__
+    execute_playbook("playbook.yaml", "/api/callback_route", {}, extra_vars)
+
+    mock_post.assert_called_once_with("https://localhost:44444/api/playbook", json=expected_parameters, timeout=10)
-- 
GitLab