Skip to content

Commit 549050f

Browse files
diningPhilosopher64Prabhakar Kumar
authored andcommitted
Fixes a bug which prevents MATLAB from exiting cleanly when token authentication is enabled.
1 parent 4b43091 commit 549050f

File tree

6 files changed

+147
-33
lines changed

6 files changed

+147
-33
lines changed

matlab_proxy/app_state.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88
import time
99
from collections import deque
1010
from datetime import datetime, timedelta, timezone
11-
from typing import Final
11+
from typing import Final, Optional
1212

1313
from matlab_proxy import util
1414
from matlab_proxy.settings import (
1515
get_process_startup_timeout,
16-
get_default_mwi_log_file_path,
1716
)
1817
from matlab_proxy.constants import (
1918
CONNECTOR_SECUREPORT_FILENAME,
@@ -293,7 +292,24 @@ def _are_required_processes_ready(
293292

294293
return True
295294

296-
async def _get_matlab_connector_status(self):
295+
def _get_token_auth_headers(self) -> Optional[dict]:
296+
"""Returns token info as headers if authentication is enabled.
297+
298+
Returns:
299+
[Dict | None]: Returns token authentication headers if any.
300+
"""
301+
return (
302+
{self.settings["mwi_auth_token_name"]: self.settings["mwi_auth_token_hash"]}
303+
if self.settings["mwi_is_token_auth_enabled"]
304+
else None
305+
)
306+
307+
async def _get_matlab_connector_status(self) -> str:
308+
"""Returns the status of MATLABs Embedded Connector.
309+
310+
Returns:
311+
str: Returns any of "up", "down" or "starting" indicating the status of Embedded Connector.
312+
"""
297313
if not self.matlab_session_files["matlab_ready_file"].exists():
298314
return "starting"
299315

@@ -303,11 +319,7 @@ async def _get_matlab_connector_status(self):
303319
# As the matlab_view is now a protected endpoint, we need to pass token information through headers.
304320

305321
# Include token information into the headers if authentication is enabled.
306-
headers = (
307-
{self.settings["mwi_auth_token_name"]: self.settings["mwi_auth_token_hash"]}
308-
if self.settings["mwi_is_token_auth_enabled"]
309-
else None
310-
)
322+
headers = self._get_token_auth_headers()
311323

312324
embedded_connector_status = await mwi.embedded_connector.request.get_state(
313325
mwi_server_url=self.settings["mwi_server_url"],
@@ -683,10 +695,13 @@ async def __setup_env_for_matlab(self) -> dict:
683695
else:
684696
# On windows stdout is not supported yet.
685697
# So, use the default log file for MATLAB logs
686-
default_matlab_logs_file = get_default_mwi_log_file_path()
698+
matlab_logs_file = self.mwi_logs_dir / MATLAB_LOGS_FILE_NAME
687699
# Write MATLAB logs
688-
matlab_env["MW_DIAGNOSTIC_DEST"] = f"file={default_matlab_logs_file}"
700+
matlab_env["MW_DIAGNOSTIC_DEST"] = f"file={matlab_logs_file}"
689701

702+
logger.info(
703+
f"Writing MATLAB process logs to: {matlab_env['MW_DIAGNOSTIC_DEST']}"
704+
)
690705
matlab_env[
691706
"MW_DIAGNOSTIC_SPEC"
692707
] = "connector::http::server=all;connector::lifecycle=all"
@@ -1069,12 +1084,16 @@ async def __send_stop_request_to_matlab(self):
10691084

10701085
try:
10711086
data = mwi.embedded_connector.helpers.get_data_to_eval_mcode("exit")
1087+
headers = self._get_token_auth_headers()
10721088
url = mwi.embedded_connector.helpers.get_mvm_endpoint(
10731089
self.settings["mwi_server_url"]
10741090
)
10751091

10761092
resp_json = await mwi.embedded_connector.send_request(
1077-
url=url, method="POST", data=data, headers=None
1093+
url=url,
1094+
method="POST",
1095+
data=data,
1096+
headers=headers,
10781097
)
10791098

10801099
if resp_json["messages"]["EvalResponse"][0]["isError"]:

matlab_proxy/settings.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,6 @@ def get_mwi_config_folder(dev=False):
150150
return config_folder_path
151151

152152

153-
def get_default_mwi_log_file_path():
154-
return get_mwi_config_folder() / constants.MATLAB_LOGS_FILE_NAME
155-
156-
157153
def get_mwi_logs_root_dir(dev=False):
158154
return get_mwi_config_folder(dev) / "ports"
159155

matlab_proxy/util/mwi/embedded_connector/request.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2020-2022 The MathWorks, Inc.
1+
# Copyright 2020-2023 The MathWorks, Inc.
22

33
"""
44
This file contains the methods to communicate with the embedded connector.

matlab_proxy/util/mwi/token_auth.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2020-2023 The MathWorks, Inc.
1+
# Copyright 2020-2023 The MathWorks, Inc.
22

33
# This file contains functions required to enable token based authentication in the server.
44

@@ -135,6 +135,8 @@ async def protect_endpoint(request):
135135

136136

137137
## Module Private Methods:
138+
139+
138140
async def _get_token_name(request):
139141
"""Gets the name of the token from settings.
140142

tests/unit/test_app_state.py

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from matlab_proxy.app_state import AppState
1212
from matlab_proxy.util.mwi.exceptions import LicensingError, MatlabError
13+
from tests.unit.util import MockResponse
1314

1415

1516
@pytest.fixture
@@ -24,6 +25,36 @@ def app_state_fixture():
2425
return app_state
2526

2627

28+
@pytest.fixture
29+
def sample_token_headers_fixture():
30+
return {"mwi_auth_token_name": "asdf"}
31+
32+
33+
@pytest.fixture
34+
def app_state_with_token_auth_fixture(
35+
app_state_fixture, sample_token_headers_fixture, tmp_path
36+
):
37+
"""Pytest fixture which returns AppState instance with token authentication enabled.
38+
39+
Args:
40+
app_state_fixture (AppState): Pytest fixture
41+
tmp_path (str): Built-in pytest fixture
42+
43+
Returns:
44+
(AppState, dict): Instance of the AppState class with token authentication enabled and token headers
45+
"""
46+
tmp_matlab_ready_file = Path(tmp_path) / "tmp_file.txt"
47+
tmp_matlab_ready_file.touch()
48+
((mwi_auth_token_name, mwi_auth_token_hash),) = sample_token_headers_fixture.items()
49+
app_state_fixture.matlab_session_files["matlab_ready_file"] = tmp_matlab_ready_file
50+
app_state_fixture.settings["mwi_is_token_auth_enabled"] = True
51+
app_state_fixture.settings["mwi_auth_token_name"] = mwi_auth_token_name
52+
app_state_fixture.settings["mwi_auth_token_hash"] = mwi_auth_token_hash
53+
app_state_fixture.settings["mwi_server_url"] = "http://localhost:8888"
54+
55+
return app_state_fixture
56+
57+
2758
@pytest.fixture
2859
def mocker_os_patching_fixture(mocker, platform):
2960
"""A pytest fixture which patches the is_* functions in system.py module
@@ -38,12 +69,20 @@ def mocker_os_patching_fixture(mocker, platform):
3869
mocker.patch("matlab_proxy.app_state.system.is_linux", return_value=False)
3970
mocker.patch("matlab_proxy.app_state.system.is_windows", return_value=False)
4071
mocker.patch("matlab_proxy.app_state.system.is_mac", return_value=False)
72+
mocker.patch("matlab_proxy.app_state.system.is_posix", return_value=False)
73+
4174
if platform == "linux":
4275
mocker.patch("matlab_proxy.app_state.system.is_linux", return_value=True)
76+
mocker.patch("matlab_proxy.app_state.system.is_posix", return_value=True)
77+
4378
elif platform == "windows":
4479
mocker.patch("matlab_proxy.app_state.system.is_windows", return_value=True)
80+
mocker.patch("matlab_proxy.app_state.system.is_posix", return_value=False)
81+
4582
else:
4683
mocker.patch("matlab_proxy.app_state.system.is_mac", return_value=True)
84+
mocker.patch("matlab_proxy.app_state.system.is_posix", return_value=True)
85+
4786
return mocker
4887

4988

@@ -397,9 +436,12 @@ def test_env_variables_filtration_for_xvfb_process(
397436
assert filtered_env_vars.get(env_var) == is_filtered
398437

399438

400-
@pytest.mark.parametrize("platform", [("linux"), ("windows"), ("mac")])
439+
@pytest.mark.parametrize(
440+
"platform, expected_output",
441+
[("linux", "stdout"), ("windows", "file"), ("mac", "stdout")],
442+
)
401443
async def test_setup_env_for_matlab(
402-
mocker_os_patching_fixture, platform, app_state_fixture, tmp_path
444+
mocker_os_patching_fixture, platform, expected_output, app_state_fixture, tmp_path
403445
):
404446
"""Test to check MW_DIAGNOSTIC_DEST is set appropriately for posix and non-posix systems
405447
@@ -411,7 +453,6 @@ async def test_setup_env_for_matlab(
411453
"""
412454

413455
# Arrange
414-
expected_log_file_path = tmp_path / "matlab_logs.txt"
415456
app_state_fixture.licensing = {"type": "existing_license"}
416457
app_state_fixture.settings = {"mwapikey": None, "matlab_display": ":1"}
417458
app_state_fixture.mwi_logs_dir = tmp_path
@@ -423,7 +464,47 @@ async def test_setup_env_for_matlab(
423464
matlab_env = await app_state_fixture._AppState__setup_env_for_matlab()
424465

425466
# Assert
426-
if "linux" or "mac" in platform:
427-
assert matlab_env["MW_DIAGNOSTIC_DEST"] == "stdout"
428-
else:
429-
assert matlab_env["MW_DIAGNOSTIC_DEST"] == expected_log_file_path
467+
assert expected_output in matlab_env["MW_DIAGNOSTIC_DEST"]
468+
469+
470+
@pytest.mark.parametrize(
471+
"function_to_call ,mock_response",
472+
[
473+
("_get_matlab_connector_status", MockResponse(ok=True)),
474+
(
475+
"_AppState__send_stop_request_to_matlab",
476+
MockResponse(
477+
ok=True, payload={"messages": {"EvalResponse": [{"isError": None}]}}
478+
),
479+
),
480+
],
481+
ids=["request matlab connector status", "send request to stop matlab"],
482+
)
483+
async def test_requests_sent_by_matlab_proxy_have_headers(
484+
app_state_with_token_auth_fixture,
485+
function_to_call,
486+
mock_response,
487+
mocker,
488+
sample_token_headers_fixture,
489+
):
490+
"""Test to check if token headers are included in requests sent by matlab-proxy when authentication is enabled
491+
492+
Args:
493+
app_state_fixture_with_token_auth (AppState): Instance of AppState class with token authentication enabled
494+
mocker : Built-in pytest fixture
495+
"""
496+
# Arrange
497+
mocked_request = mocker.patch(
498+
"aiohttp.ClientSession.request", return_value=mock_response
499+
)
500+
501+
# Act
502+
# Call the function passed as a string
503+
method = getattr(app_state_with_token_auth_fixture, function_to_call)
504+
_ = await method()
505+
506+
# Assert
507+
connector_status_request_headers = list(mocked_request.call_args_list)[0].kwargs[
508+
"headers"
509+
]
510+
assert sample_token_headers_fixture == connector_status_request_headers
Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2022 The MathWorks, Inc.
1+
# Copyright 2022-2023 The MathWorks, Inc.
22

33
import pytest
44
from matlab_proxy.util import mwi
@@ -7,42 +7,58 @@
77

88

99
async def test_send_request_success(mocker):
10+
"""Test to check the happy path for send_request
11+
Args:
12+
mocker : Built in pytest fixture
13+
"""
14+
# Arrange
1015
json_data = {"hello": "world"}
11-
1216
payload = json_data
1317
mock_resp = MockResponse(payload=payload, ok=True)
18+
_ = mocker.patch("aiohttp.ClientSession.request", return_value=mock_resp)
1419

15-
mocked = mocker.patch("aiohttp.ClientSession.request", return_value=mock_resp)
20+
# Act
1621
res = await mwi.embedded_connector.send_request(
1722
url="https://localhost:3000", data=json_data, method="GET"
1823
)
1924

25+
# Assert
2026
assert json_data["hello"] == res["hello"]
2127

2228

2329
async def test_send_request_failure(mocker):
30+
"""Test to check if send_request fails when
31+
1) EC does not respond
32+
2) url or method is not supplied
33+
Args:
34+
mocker : Built in pytest fixture
35+
"""
36+
37+
# Arrange
2438
json_data = {"hello": "world"}
2539

2640
payload = json_data
2741
mock_resp = MockResponse(payload=payload, ok=False)
42+
_ = mocker.patch("aiohttp.ClientSession.request", return_value=mock_resp)
43+
44+
# Doesnt have url or data or method
45+
mock_resp = MockResponse(payload=payload, ok=False)
2846

29-
mocked = mocker.patch("aiohttp.ClientSession.request", return_value=mock_resp)
47+
# Act
3048

3149
# Failed to communicate with EmbeddedConnector
3250
with pytest.raises(EmbeddedConnectorError):
33-
res = await mwi.embedded_connector.send_request(
51+
_ = await mwi.embedded_connector.send_request(
3452
url="https://localhost:3000", method="GET", data=json_data
3553
)
3654

37-
# Doesnt have url or data or method
38-
mock_resp = MockResponse(payload=payload, ok=False)
39-
4055
for key in ["url", "method"]:
4156
options = {
4257
"url": "https://localhost:3000",
4358
"data": json_data,
4459
"method": "GET",
4560
}
4661
options[key] = ""
62+
# Assert
4763
with pytest.raises(EmbeddedConnectorError):
48-
res = await mwi.embedded_connector.send_request(**options)
64+
_ = await mwi.embedded_connector.send_request(**options)

0 commit comments

Comments
 (0)