Skip to content

Commit c69b39f

Browse files
krisctlrashedmyt
authored andcommitted
Add experimental support for long running MATLAB sessions.
1 parent 02908f3 commit c69b39f

5 files changed

Lines changed: 274 additions & 5 deletions

File tree

matlab_proxy/util/mw.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
# Copyright 2020-2024 The MathWorks, Inc.
1+
# Copyright 2020-2026 The MathWorks, Inc.
22

33
import asyncio
44
import os
55
import select
66
import xml.etree.ElementTree as ET
77

88
import aiohttp
9+
910
from matlab_proxy.default_configuration import config
10-
from matlab_proxy.util import mwi
1111
from matlab_proxy.settings import get_process_startup_timeout
12+
from matlab_proxy.util import mwi
13+
from matlab_proxy.util.mwi import environment_variables as mwi_env
1214
from matlab_proxy.util.mwi.exceptions import (
1315
EntitlementError,
1416
MatlabError,
@@ -146,6 +148,14 @@ async def fetch_access_token(mwa_api_endpoint, identity_token, source_id):
146148
Returns:
147149
Dict : Containing the Access token.
148150
"""
151+
# MWAJ -> Long-running session (90 days), MWAS -> Short-running session (24 hours)
152+
# It comes with the security risk of being stolen and used by an attacker for an extended period.
153+
# Hence, it is recommended to use MWAJ only when necessary.
154+
access_token_type = (
155+
"MWAJ" if mwi_env.Experimental.is_long_running_session_enabled() else "MWAS"
156+
)
157+
logger.debug(f"Requesting access token of type: {access_token_type}")
158+
149159
async with aiohttp.ClientSession(trust_env=True) as client_session:
150160
async with client_session.post(
151161
f"{mwa_api_endpoint}/tokens/access",
@@ -157,7 +167,7 @@ async def fetch_access_token(mwa_api_endpoint, identity_token, source_id):
157167
data=aiohttp.FormData(
158168
{
159169
"tokenString": identity_token,
160-
"type": "MWAS",
170+
"type": access_token_type,
161171
"sourceId": source_id,
162172
}
163173
),

matlab_proxy/util/mwi/environment_variables.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020-2025 The MathWorks, Inc.
1+
# Copyright 2020-2026 The MathWorks, Inc.
22
"""This file lists and exposes the environment variables which are used by the integration."""
33

44
import os
@@ -228,3 +228,15 @@ def use_rich_logger():
228228
def get_licmode_override():
229229
"""Returns the licmode oveerride if set"""
230230
return os.environ.get("MWI_LICMODE_OVERRIDE", None)
231+
232+
@staticmethod
233+
def get_env_name_enable_long_running_session():
234+
"""Returns the environment variable name used to enable long-running session support"""
235+
return "MWI_ENABLE_LONG_RUNNING_SESSION"
236+
237+
@staticmethod
238+
def is_long_running_session_enabled():
239+
"""Returns true if long-running session support is enabled."""
240+
return _is_env_set_to_true(
241+
Experimental.get_env_name_enable_long_running_session()
242+
)
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Copyright 2026 The MathWorks, Inc.
2+
3+
"""
4+
Integration test to verify that when MWI_ENABLE_LONG_RUNNING_SESSION is set,
5+
MATLAB receives an `MWAJ` token type instead of the default `MWAS`.
6+
"""
7+
8+
import os
9+
import re
10+
import time
11+
from pathlib import Path
12+
from urllib.parse import parse_qs, urlparse
13+
14+
import pytest
15+
16+
import matlab_proxy.settings as settings
17+
from matlab_proxy.constants import MWI_AUTH_TOKEN_NAME_FOR_HTTP
18+
from matlab_proxy.util import system
19+
from tests.integration.integration_tests_with_license.test_http_end_points import (
20+
_check_matlab_status,
21+
)
22+
from tests.integration.utils import integration_tests_utils as utils
23+
from tests.utils.logging_util import create_integ_test_logger
24+
25+
_logger = create_integ_test_logger(__name__)
26+
27+
MAX_TIMEOUT = settings.get_process_startup_timeout()
28+
29+
MATLAB_SCRIPT_TO_VERIFY_TOKEN_TYPE = (
30+
Path(__file__).parent / "verify_token_type.m"
31+
).read_text()
32+
33+
34+
class LongRunningSessionMATLABServer:
35+
"""
36+
Context Manager that launches matlab-proxy with MWI_ENABLE_LONG_RUNNING_SESSION
37+
enabled and a startup script that verifies the token type.
38+
"""
39+
40+
def __init__(self):
41+
self.proc = None
42+
self.dpipe = None
43+
self.mwi_app_port = None
44+
self.mwi_base_url = None
45+
self.headers = None
46+
self.connection_scheme = None
47+
self.url = None
48+
49+
async def __aenter__(self):
50+
_logger.info(
51+
"Setting up MATLAB Server with MWI_ENABLE_LONG_RUNNING_SESSION enabled"
52+
)
53+
54+
self.dpipe = os.pipe2(os.O_NONBLOCK) if system.is_linux() else os.pipe()
55+
self.mwi_app_port = utils.get_random_free_port()
56+
self.mwi_base_url = "/matlab-test"
57+
58+
input_env = {
59+
"MWI_APP_PORT": self.mwi_app_port,
60+
"MWI_BASE_URL": self.mwi_base_url,
61+
"MWI_ENABLE_LONG_RUNNING_SESSION": "True",
62+
"MWI_MATLAB_STARTUP_SCRIPT": MATLAB_SCRIPT_TO_VERIFY_TOKEN_TYPE,
63+
}
64+
65+
self.proc = await utils.start_matlab_proxy_app(
66+
out=self.dpipe[1], input_env=input_env
67+
)
68+
69+
utils.wait_server_info_ready(self.mwi_app_port)
70+
parsed_url = urlparse(utils.get_connection_string(self.mwi_app_port))
71+
72+
self.headers = {
73+
MWI_AUTH_TOKEN_NAME_FOR_HTTP: (
74+
parse_qs(parsed_url.query)[MWI_AUTH_TOKEN_NAME_FOR_HTTP][0]
75+
if MWI_AUTH_TOKEN_NAME_FOR_HTTP in parse_qs(parsed_url.query)
76+
else ""
77+
)
78+
}
79+
self.connection_scheme = parsed_url.scheme
80+
self.url = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path
81+
return self
82+
83+
async def __aexit__(self, exc_type, exc_value, exc_traceback):
84+
import asyncio
85+
86+
_logger.info("Tearing down MATLAB Server (long-running session test)")
87+
try:
88+
self.proc.terminate()
89+
await asyncio.wait_for(self.proc.wait(), timeout=10)
90+
except asyncio.TimeoutError:
91+
self.proc.kill()
92+
await self.proc.wait()
93+
94+
95+
@pytest.fixture
96+
async def matlab_proxy_long_running_session_fixture():
97+
"""Pytest fixture that yields a matlab-proxy server with long-running session enabled."""
98+
try:
99+
async with LongRunningSessionMATLABServer() as server:
100+
yield server
101+
except ProcessLookupError as e:
102+
_logger.debug(f"ProcessLookupError: {e}")
103+
104+
105+
async def test_long_running_session_uses_mwaj_token(
106+
matlab_proxy_long_running_session_fixture,
107+
):
108+
"""Test that when MWI_ENABLE_LONG_RUNNING_SESSION is set, MATLAB is started
109+
with an MWAJ token and can verify this via the MathWorks auth API."""
110+
fixture = matlab_proxy_long_running_session_fixture
111+
112+
status = _check_matlab_status(fixture, "up")
113+
assert status == "up", f"MATLAB did not start, status: {status}"
114+
115+
read_descriptor, write_descriptor = fixture.dpipe
116+
number_of_bytes = 4000
117+
118+
if read_descriptor:
119+
line = os.read(read_descriptor, number_of_bytes).decode("utf-8")
120+
process_logs = line.strip()
121+
122+
match = re.search(
123+
r"The results of executing MWI_MATLAB_STARTUP_SCRIPT are stored at: \s*(.*?startup_code_output\.txt)",
124+
process_logs,
125+
)
126+
assert match, (
127+
f"Could not find startup code output file path in logs. "
128+
f"Log excerpt: {process_logs[:2000]}"
129+
)
130+
131+
output_file_path = match.group(1)
132+
_logger.info(f"Startup code output file: {output_file_path}")
133+
134+
# The startup script makes a network call to the auth service,
135+
# so poll for the output file to appear.
136+
start_time = time.time()
137+
while not os.path.exists(output_file_path):
138+
if time.time() - start_time > MAX_TIMEOUT:
139+
pytest.fail(
140+
f"Startup code output file not found at {output_file_path} "
141+
f"after {MAX_TIMEOUT}s"
142+
)
143+
time.sleep(1)
144+
145+
with open(output_file_path, "r") as f:
146+
content = f.read()
147+
148+
assert (
149+
"Token Type: MWAJ" in content
150+
), f"Expected token type MWAJ but got: {content}"
151+
152+
os.close(read_descriptor)
153+
os.close(write_descriptor)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
% Copyright 2026 The MathWorks, Inc.
2+
%
3+
% verify_token_type - Verify the type of access token used to start MATLAB.
4+
%
5+
% This script queries the MathWorks authentication service to determine the
6+
% token type (e.g., MWAS or MWAJ) associated with the current MATLAB session.
7+
%
8+
% When MWI_ENABLE_LONG_RUNNING_SESSION is set, the token type should be MWAJ.
9+
% Otherwise, the default token type is MWAS.
10+
%
11+
% Output:
12+
% Prints "Token Type: <type>" to stdout, where <type> is the
13+
% loginIdentifierType returned by the authentication service.
14+
15+
% Retrieve the current session identity token from the environment variable
16+
token = getenv('MLM_WEB_USER_CRED');
17+
18+
% Query the MathWorks authentication service to inspect the token
19+
wsEnv = getenv('WS_ENV');
20+
if isempty(wsEnv) || strcmp(wsEnv, 'production')
21+
authUrl = 'https://login.mathworks.com/authenticationws/service/v4/tokens';
22+
else
23+
authUrl = sprintf('https://login-%s.mathworks.com/authenticationws/service/v4/tokens', wsEnv);
24+
end
25+
26+
payload = struct('tokenString', token, 'tokenPolicyName', 'L1');
27+
jsonBody = jsonencode(payload);
28+
29+
options = weboptions( ...
30+
'MediaType', 'application/json', ...
31+
'HeaderFields', { ...
32+
'accept', 'application/json'; ...
33+
'x_mw_ws_callerid', 'desktop-jupyter' ...
34+
}, ...
35+
'RequestMethod', 'post', ...
36+
'Timeout', 30 ...
37+
);
38+
39+
response = webwrite(authUrl, jsonBody, options);
40+
41+
fprintf('Token Type: %s', response.loginIdentifierType);

tests/unit/util/test_mw.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
# Copyright 2020-2025 The MathWorks, Inc.
1+
# Copyright 2020-2026 The MathWorks, Inc.
22

33
import datetime
4+
import os
45
import random
56
import re
67
import secrets
@@ -9,6 +10,7 @@
910
from http import HTTPStatus
1011

1112
import pytest
13+
1214
from matlab_proxy import settings
1315
from matlab_proxy.util import mw, system
1416
from matlab_proxy.util.mwi import exceptions
@@ -171,6 +173,57 @@ async def test_fetch_access_token(mwa_api_data, fetch_access_token_valid_json, m
171173
assert json_data["accessTokenString"] == res["token"]
172174

173175

176+
async def test_fetch_access_token_uses_mwas_by_default(
177+
mwa_api_data, fetch_access_token_valid_json, mocker
178+
):
179+
"""Test that fetch_access_token requests MWAS token type by default."""
180+
json_data = fetch_access_token_valid_json
181+
payload = dict(accessTokenString=json_data["accessTokenString"])
182+
mock_resp = MockResponse(payload=payload, ok=True)
183+
184+
mocked = mocker.patch("aiohttp.ClientSession.post", return_value=mock_resp)
185+
mocker.patch.dict(os.environ, {}, clear=False)
186+
os.environ.pop("MWI_ENABLE_LONG_RUNNING_SESSION", None)
187+
188+
await mw.fetch_access_token(
189+
mwa_api_data.mwa_api_endpoint,
190+
mwa_api_data.identity_token,
191+
mwa_api_data.source_id,
192+
)
193+
194+
_, _, kwargs = mocked.mock_calls[0]
195+
form_data = kwargs["data"]
196+
type_field = form_data._fields[1]
197+
assert type_field[0]["name"] == "type"
198+
assert type_field[2] == "MWAS"
199+
200+
201+
async def test_fetch_access_token_uses_mwaj_when_long_running_session_enabled(
202+
mwa_api_data, fetch_access_token_valid_json, mocker
203+
):
204+
"""Test that fetch_access_token requests MWAJ token type when long-running session is enabled."""
205+
json_data = fetch_access_token_valid_json
206+
payload = dict(accessTokenString=json_data["accessTokenString"])
207+
mock_resp = MockResponse(payload=payload, ok=True)
208+
209+
mocked = mocker.patch("aiohttp.ClientSession.post", return_value=mock_resp)
210+
mocker.patch.dict(
211+
os.environ, {"MWI_ENABLE_LONG_RUNNING_SESSION": "True"}, clear=False
212+
)
213+
214+
await mw.fetch_access_token(
215+
mwa_api_data.mwa_api_endpoint,
216+
mwa_api_data.identity_token,
217+
mwa_api_data.source_id,
218+
)
219+
220+
_, _, kwargs = mocked.mock_calls[0]
221+
form_data = kwargs["data"]
222+
type_field = form_data._fields[1]
223+
assert type_field[0]["name"] == "type"
224+
assert type_field[2] == "MWAJ"
225+
226+
174227
async def test_fetch_access_token_licensing_error(mwa_api_data, mocker):
175228
"""Test to check mw.fetch_access_token() method raises a mwi_exceptions.OnlineLicensingError.
176229

0 commit comments

Comments
 (0)