matlab-proxy 0.5.3__py3-none-any.whl → 0.30.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. matlab_proxy/app.py +578 -205
  2. matlab_proxy/app_state.py +1061 -431
  3. matlab_proxy/constants.py +37 -0
  4. matlab_proxy/default_configuration.py +39 -4
  5. matlab_proxy/devel.py +18 -22
  6. matlab_proxy/gui/index.html +20 -1
  7. matlab_proxy/gui/static/css/index.BedVwcEg.css +10 -0
  8. matlab_proxy/gui/static/js/index.pQwV1obF.js +64 -0
  9. matlab_proxy/gui/static/media/MATLAB-env-blur.NupTbPv_.png +0 -0
  10. matlab_proxy/matlab/evaluateUserMatlabCode.m +51 -0
  11. matlab_proxy/matlab/startup.m +3 -28
  12. matlab_proxy/settings.py +543 -112
  13. matlab_proxy/util/__init__.py +187 -59
  14. matlab_proxy/util/cookie_jar.py +72 -0
  15. matlab_proxy/util/event_loop.py +28 -10
  16. matlab_proxy/util/list_servers.py +71 -26
  17. matlab_proxy/util/mw.py +16 -15
  18. matlab_proxy/util/mwi/download.py +136 -0
  19. matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
  20. matlab_proxy/util/mwi/embedded_connector/helpers.py +12 -4
  21. matlab_proxy/util/mwi/embedded_connector/request.py +78 -12
  22. matlab_proxy/util/mwi/environment_variables.py +120 -27
  23. matlab_proxy/util/mwi/exceptions.py +63 -9
  24. matlab_proxy/util/mwi/logger.py +141 -27
  25. matlab_proxy/util/mwi/session_name.py +28 -0
  26. matlab_proxy/util/mwi/token_auth.py +264 -121
  27. matlab_proxy/util/mwi/validators.py +231 -88
  28. matlab_proxy/util/system.py +9 -0
  29. matlab_proxy/util/windows.py +32 -6
  30. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/METADATA +94 -49
  31. matlab_proxy-0.30.1.dist-info/RECORD +88 -0
  32. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/WHEEL +1 -2
  33. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/entry_points.txt +1 -1
  34. matlab_proxy_manager/README.md +85 -0
  35. matlab_proxy_manager/__init__.py +6 -0
  36. matlab_proxy_manager/lib/README.md +53 -0
  37. matlab_proxy_manager/lib/__init__.py +1 -0
  38. matlab_proxy_manager/lib/api.py +419 -0
  39. matlab_proxy_manager/storage/README.md +54 -0
  40. matlab_proxy_manager/storage/__init__.py +1 -0
  41. matlab_proxy_manager/storage/file_repository.py +144 -0
  42. matlab_proxy_manager/storage/interface.py +62 -0
  43. matlab_proxy_manager/storage/server.py +172 -0
  44. matlab_proxy_manager/utils/__init__.py +1 -0
  45. matlab_proxy_manager/utils/auth.py +77 -0
  46. matlab_proxy_manager/utils/constants.py +8 -0
  47. matlab_proxy_manager/utils/decorators.py +37 -0
  48. matlab_proxy_manager/utils/environment_variables.py +51 -0
  49. matlab_proxy_manager/utils/exceptions.py +45 -0
  50. matlab_proxy_manager/utils/helpers.py +314 -0
  51. matlab_proxy_manager/utils/logger.py +76 -0
  52. matlab_proxy_manager/web/README.md +37 -0
  53. matlab_proxy_manager/web/__init__.py +1 -0
  54. matlab_proxy_manager/web/app.py +536 -0
  55. matlab_proxy_manager/web/monitor.py +45 -0
  56. matlab_proxy_manager/web/watcher.py +65 -0
  57. matlab_proxy/gui/asset-manifest.json +0 -23
  58. matlab_proxy/gui/authorization.html +0 -115
  59. matlab_proxy/gui/bootstrap.3.4.1.min.css +0 -6
  60. matlab_proxy/gui/navbar.css +0 -8
  61. matlab_proxy/gui/signin.css +0 -42
  62. matlab_proxy/gui/static/css/main.d890078a.chunk.css +0 -13
  63. matlab_proxy/gui/static/css/main.d890078a.chunk.css.map +0 -1
  64. matlab_proxy/gui/static/js/2.13be6544.chunk.js +0 -3
  65. matlab_proxy/gui/static/js/2.13be6544.chunk.js.LICENSE.txt +0 -59
  66. matlab_proxy/gui/static/js/2.13be6544.chunk.js.map +0 -1
  67. matlab_proxy/gui/static/js/main.c311d854.chunk.js +0 -2
  68. matlab_proxy/gui/static/js/main.c311d854.chunk.js.map +0 -1
  69. matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js +0 -2
  70. matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js.map +0 -1
  71. matlab_proxy/gui/static/media/arrow.0c2968b9.svg +0 -4
  72. matlab_proxy/gui/static/media/feedback.6e8d50eb.svg +0 -1
  73. matlab_proxy/gui/static/media/gripper.9defbc5e.svg +0 -1
  74. matlab_proxy/gui/static/media/help.15e5bfab.svg +0 -1
  75. matlab_proxy/gui/static/media/ico-header-contact-hover.0958c442.svg +0 -17
  76. matlab_proxy/gui/static/media/ico-header-contact.ae9169c8.svg +0 -17
  77. matlab_proxy/gui/static/media/restart.7987508a.svg +0 -1
  78. matlab_proxy/gui/static/media/sign-out.08356b67.svg +0 -1
  79. matlab_proxy/gui/static/media/start.50c4596f.svg +0 -1
  80. matlab_proxy/gui/static/media/stop.30c9a9ab.svg +0 -1
  81. matlab_proxy/gui/static/media/terminate.7ea1363e.svg +0 -1
  82. matlab_proxy/gui/token.html +0 -123
  83. matlab_proxy-0.5.3.dist-info/RECORD +0 -84
  84. matlab_proxy-0.5.3.dist-info/top_level.txt +0 -1
  85. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.82b1212e.woff → glyphicons-halflings-regular.BKjkU69z.woff} +0 -0
  86. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.5be1347c.eot → glyphicons-halflings-regular.BUJKDMgK.eot} +0 -0
  87. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.060b2710.svg → glyphicons-halflings-regular.CSehLiBc.svg} +0 -0
  88. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.4692b9ec.ttf → glyphicons-halflings-regular.DrwTMapi.ttf} +0 -0
  89. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.be810be3.woff2 → glyphicons-halflings-regular.DzqM6ju8.woff2} +0 -0
  90. /matlab_proxy/gui/static/media/{ico-header-account-hover.89438e91.svg → ico-header-account-hover.-jQHo6Wx.svg} +0 -0
  91. /matlab_proxy/gui/static/media/{ico-header-account.86b10d7b.svg → ico-header-account.CJCFoo5a.svg} +0 -0
  92. /matlab_proxy/gui/static/media/{ico-sprite.cbdb66c0.png → ico-sprite.DXGLgzq9.png} +0 -0
  93. /matlab_proxy/gui/static/media/{mathworks-eps.4d20e0ee.ttf → mathworks-eps.CGNQALa9.ttf} +0 -0
  94. /matlab_proxy/gui/static/media/{mathworks-eps.df1428df.svg → mathworks-eps.DrkCtQtG.svg} +0 -0
  95. /matlab_proxy/gui/static/media/{mathworks-eps.e5c41e84.woff → mathworks-eps.Ds7lQbql.woff} +0 -0
  96. /matlab_proxy/gui/static/media/{mathworks-pictograms.3fc6513a.woff → mathworks-pictograms.BdqxEfBR.woff} +0 -0
  97. /matlab_proxy/gui/static/media/{mathworks-pictograms.f6f087b0.svg → mathworks-pictograms.CCLweoD4.svg} +0 -0
  98. /matlab_proxy/gui/static/media/{mathworks-pictograms.6e128c0e.ttf → mathworks-pictograms.DZhFdRSm.ttf} +0 -0
  99. /matlab_proxy/gui/static/media/{mathworks.80a3218e.svg → mathworks.C-qsbhDy.svg} +0 -0
  100. /matlab_proxy/gui/static/media/{mathworks.c422935b.ttf → mathworks.Ceplx86V.ttf} +0 -0
  101. /matlab_proxy/gui/static/media/{mathworks.37a563ef.woff → mathworks.D08X1Vp8.woff} +0 -0
  102. /matlab_proxy/gui/static/media/{trigger-error.3f1c4ef2.svg → trigger-error.QEdsGL-m.svg} +0 -0
  103. /matlab_proxy/gui/static/media/{trigger-ok.7b9c238b.svg → trigger-ok.Dzg8OIrk.svg} +0 -0
  104. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info/licenses}/LICENSE.md +0 -0
@@ -1,12 +1,19 @@
1
- # Copyright (c) 2020-2022 The MathWorks, Inc.
2
- """Functions to access & control the logging behavior of the app
3
- """
1
+ # Copyright 2020-2025 The MathWorks, Inc.
2
+ """Functions to access & control the logging behavior of the app"""
4
3
 
5
4
  import logging
6
5
  import os
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from rich.console import Console
11
+ from rich.table import Table
7
12
 
8
13
  from . import environment_variables as mwi_env
9
14
 
15
+ logging.getLogger("aiohttp_session").setLevel(logging.ERROR)
16
+
10
17
 
11
18
  def get(init=False):
12
19
  """Get the logger used by this application.
@@ -44,28 +51,65 @@ def __set_logging_configuration():
44
51
  Returns:
45
52
  Logger: Logger object with the set configuration.
46
53
  """
47
- # query for user specified environment variables
48
- log_level, log_file = __query_environment()
54
+ # Create the Logger for MATLABProxy
55
+ logger = __get_mw_logger()
56
+
57
+ # log_level is either set by environment or is the default value.
58
+ log_level = os.getenv(
59
+ mwi_env.get_env_name_logging_level(), __get_default_log_level()
60
+ ).upper()
61
+
62
+ if __is_invalid_log_level(log_level):
63
+ default_log_level = __get_default_log_level()
64
+ logging.warning(
65
+ f"Unknown log level '{log_level}' set. Defaulting to log level '{default_log_level}'..."
66
+ )
67
+ log_level = default_log_level
49
68
 
50
69
  ## Set logging object
51
- logger = __get_mw_logger()
52
- if log_file is not None:
53
- logger.info(f"Initializing logger with log_file:{log_file}")
54
- file_handler = logging.FileHandler(filename=log_file)
55
- formatter = logging.Formatter(
56
- fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
70
+ if mwi_env.Experimental.use_rich_logger():
71
+ from rich.logging import RichHandler
72
+
73
+ rich_handler = RichHandler(
74
+ keywords=[__get_mw_logger_name()],
57
75
  )
58
- file_handler.setFormatter(formatter)
59
- file_handler.setLevel(log_level)
60
- logger.addHandler(file_handler)
76
+ rich_handler.setFormatter(logging.Formatter("%(name)s %(message)s"))
77
+ logger.addHandler(rich_handler)
78
+ else:
79
+ colored_formatter = _ColoredFormatter(
80
+ "%(color)s[%(levelname)1.1s %(asctime)s %(name)s]%(end_color)s %(message)s"
81
+ )
82
+ stream_handler = logging.StreamHandler()
83
+ stream_handler.setFormatter(colored_formatter)
84
+ logger.addHandler(stream_handler)
61
85
 
62
- # log_level is either set by environment or is the default value.
63
- logger.info(f"Initializing logger with log_level: {log_level}")
64
86
  logger.setLevel(log_level)
65
87
 
66
- # Allow other libraries used by this integration to
67
- # also print their logs at the specified level
68
- logging.basicConfig(level=log_level)
88
+ log_file = os.getenv(mwi_env.get_env_name_log_file(), None)
89
+ if log_file:
90
+ try:
91
+ log_file = Path(log_file)
92
+ # Need to create the file if it doesn't exist or else logging.FileHandler
93
+ # would open it in 'write' mode instead of 'append' mode.
94
+ log_file.touch(exist_ok=True)
95
+ logger.info(f"Initializing logger with log file:{log_file}")
96
+ file_handler = logging.FileHandler(filename=log_file, mode="a")
97
+ formatter = logging.Formatter(
98
+ fmt="[%(levelname)s %(asctime)s %(name)s] %(message)s"
99
+ )
100
+ file_handler.setFormatter(formatter)
101
+ file_handler.setLevel(log_level)
102
+ logger.addHandler(file_handler)
103
+
104
+ except PermissionError:
105
+ print(
106
+ f"PermissionError: Permission denied to create log file at: {log_file}"
107
+ )
108
+ sys.exit(1)
109
+
110
+ except Exception as err:
111
+ print(f"Failed to use log file: {log_file} with error: {err}")
112
+ sys.exit(1)
69
113
 
70
114
  return logger
71
115
 
@@ -91,14 +135,84 @@ def __get_default_log_level():
91
135
  return "INFO"
92
136
 
93
137
 
94
- def __query_environment():
95
- """Private function to query environment variables
96
- to control logging
138
+ def __is_invalid_log_level(log_level):
139
+ """Helper to check if the log level is valid.
97
140
 
98
141
  Returns:
99
- tuple: Log level & Log file as found in the environment
142
+ Boolean: Whether log level exists
100
143
  """
101
- env_var_log_level, env_var_log_file = get_environment_variable_names()
102
- log_level = os.environ.get(env_var_log_level, __get_default_log_level())
103
- log_file = os.environ.get(env_var_log_file)
104
- return log_level, log_file
144
+
145
+ return not hasattr(logging, log_level)
146
+
147
+
148
+ def log_startup_info(title=None, matlab_urls=[]):
149
+ """Logs the startup information to the console and log file if specified."""
150
+ logger = __get_mw_logger()
151
+ print_as_table = False
152
+ header_info = "Access MATLAB at:"
153
+
154
+ if sys.stdout.isatty():
155
+ # Width cannot be determined in non-interactive sessions
156
+ console = Console()
157
+ # Number of additional characters used by the table
158
+ padding = 4
159
+ print_as_table = all(len(url) + padding <= console.width for url in matlab_urls)
160
+
161
+ if print_as_table:
162
+ table = Table(
163
+ caption=title,
164
+ show_header=False,
165
+ show_lines=True,
166
+ show_edge=True,
167
+ highlight=True,
168
+ expand=True,
169
+ )
170
+ table.add_column(overflow="fold", style="bold green", justify="center")
171
+ table.add_row(header_info)
172
+ for url in matlab_urls:
173
+ table.add_row(url)
174
+ console.print(table)
175
+
176
+ if os.getenv(mwi_env.get_env_name_log_file(), None) or not print_as_table:
177
+ urls = "\n\t".join(matlab_urls)
178
+ logger.critical(f"{header_info}\n\t{urls}")
179
+
180
+
181
+ class _ColoredFormatter(logging.Formatter):
182
+ """Custom formatter to add colors based on log level and modify time format."""
183
+
184
+ def format(self, record):
185
+ # Example: Add 'color' and 'end_color' attributes based on log level
186
+ if record.levelno == logging.INFO:
187
+ record.color = "\033[32m" # Green
188
+ record.end_color = "\033[0m"
189
+ elif record.levelno == logging.DEBUG:
190
+ record.color = "\033[94m" # Blue
191
+ record.end_color = "\033[0m"
192
+ elif record.levelno == logging.WARNING:
193
+ record.color = "\033[93m" # Yellow
194
+ record.end_color = "\033[0m"
195
+ elif record.levelno == logging.ERROR:
196
+ record.color = "\033[91m" # Red
197
+ record.end_color = "\033[0m"
198
+ elif record.levelno == logging.CRITICAL:
199
+ record.color = "\033[35m" # Magenta
200
+ record.end_color = "\033[0m"
201
+ else:
202
+ record.color = ""
203
+ record.end_color = ""
204
+
205
+ # Call the original format method
206
+ return super().format(record)
207
+
208
+ def formatTime(self, record, datefmt=None):
209
+ # Default behavior of formatTime
210
+ ct = self.converter(record.created)
211
+ if datefmt:
212
+ s = time.strftime(datefmt, ct)
213
+ else:
214
+ t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
215
+ s = "%s,%03d" % (t, record.msecs)
216
+
217
+ # Replace the comma with a period
218
+ return s.replace(",", ".")
@@ -0,0 +1,28 @@
1
+ # Copyright 2025 The MathWorks, Inc.
2
+ """This file provides functions to set a session name for the MATLAB Proxy instance."""
3
+
4
+ import os
5
+ from matlab_proxy.util.mwi import environment_variables as mwi_env
6
+ from matlab_proxy.util.mwi import logger as mwi_logger
7
+
8
+ logger = mwi_logger.get()
9
+
10
+
11
+ def _get_session_name():
12
+ """Get the session name for the MATLAB Proxy instance.
13
+
14
+ Returns:
15
+ str: returns the user-defined session name if set, otherwise returns None.
16
+ """
17
+ return os.getenv(mwi_env.get_env_name_session_name(), None)
18
+
19
+
20
+ def get_browser_title(matlab_version) -> str:
21
+ """Get the browser title for the MATLAB Proxy instance."""
22
+
23
+ browser_title = "MATLAB " + (matlab_version or "")
24
+ session_name = _get_session_name()
25
+ if session_name:
26
+ browser_title = session_name + " - " + browser_title
27
+ logger.info("Session Name set to : %s", session_name)
28
+ return browser_title
@@ -1,65 +1,65 @@
1
- # Copyright (c) 2020-2022 The MathWorks, Inc.
1
+ # Copyright 2020-2025 The MathWorks, Inc.
2
2
 
3
3
  # This file contains functions required to enable token based authentication in the server.
4
4
 
5
5
  import os
6
6
  import secrets
7
+ from hashlib import sha256
8
+ from hmac import compare_digest
9
+ from urllib.parse import parse_qs
7
10
 
8
11
  from aiohttp import web
9
- from aiohttp_session import get_session, new_session, setup
10
- from aiohttp_session.cookie_storage import EncryptedCookieStorage
11
- from matlab_proxy.util.mwi import environment_variables as mwi_env
12
+ from aiohttp_session import get_session, new_session
12
13
 
13
- from . import logger as mwi_logger
14
+ from matlab_proxy.util.mwi import environment_variables as mwi_env
15
+ from matlab_proxy.util.mwi import logger as mwi_logger
14
16
 
15
17
  logger = mwi_logger.get()
16
18
 
19
+ ## Module Public Methods:
17
20
 
18
- def decorator_authenticate_access(endpoint):
19
- """Decorates any endpoint function with token authentication checks."""
20
- logger.debug("inside decorator_authenticate_access")
21
21
 
22
- async def authenticate_access(request):
23
- """
24
- If Authentication is enabled, this function expects the token to be present either in
25
- the URL or in the session cookie.
26
- If token is provided and matches the expected secret, then the request is considered authentic,
27
- and the token is saved into the session cookie.
28
- """
29
- logger.debug(f" inside authenticate_access for request:{request}")
30
- app_settings = request.app["settings"]
31
- base_url = app_settings["base_url"]
22
+ def generate_mwi_auth_token_and_hash():
23
+ """
24
+ Generate the MWI Token and a hash for that token to be used by the server,
25
+ based on the environment variables that control it.
32
26
 
33
- if await authenticate_request(request):
34
- logger.debug(
35
- f" Request is authenticated, proceed to endpoint:{endpoint}{request}"
36
- )
37
- return await endpoint(request)
38
- else:
39
- # This branch intends to redirect users to the error page asking for TOKEN.
40
- # However, we need to tell this page which page to redirect to on successful validation.
27
+ Token Auth is enabled by default, unless MWI_ENABLE_TOKEN_AUTH is explicitly set to False.
41
28
 
42
- attempted_url = str(request.rel_url)
29
+ If MWI_ENABLE_TOKEN_AUTH is set, and MWI_AUTH_TOKEN is unset, then generate a token
30
+ else if MWI_AUTH_TOKEN is set, use that token for authentication.
43
31
 
44
- # Strip URL paramters
45
- attempted_url = attempted_url.split("?", 1)[0]
46
- logger.debug(f"attempted_url: {attempted_url}")
47
- return web.HTTPFound(
48
- f"{base_url}/authorization.html?attempted_url={request.rel_url}"
49
- )
32
+ Returns the Token and its hash to be used for authentication if enabled.
33
+ Returns None, if Token-Based Authentication is not enabled by user.
34
+ """
35
+ env_name_enable_mwi_token_auth = mwi_env.get_env_name_enable_mwi_auth_token()
36
+ env_name_mwi_auth_token = mwi_env.get_env_name_mwi_auth_token()
37
+ enable_token_auth = os.getenv(env_name_enable_mwi_token_auth, "").lower()
38
+ auth_token = os.getenv(env_name_mwi_auth_token, "")
50
39
 
51
- return authenticate_access
40
+ if enable_token_auth == "false":
41
+ if auth_token:
42
+ logger.warning(
43
+ f"Ignoring {env_name_mwi_auth_token}, as {env_name_enable_mwi_token_auth} explicitly set to false"
44
+ )
45
+ return _format_token_as_dictionary(None)
52
46
 
47
+ if auth_token:
48
+ auth_token = auth_token.strip()
49
+ logger.debug(f"Using provided {env_name_mwi_auth_token}.")
50
+ return _format_token_as_dictionary(auth_token)
53
51
 
54
- def is_mwi_token_auth_enabled(app_settings):
55
- """Returns True/False based on whether the mwi_auth_token_auth is enabled."""
56
- return app_settings["mwi_is_mwi_token_auth_enabled"]
52
+ # default catch-all for when the env variables are not set or above conditions are not met.
53
+ # This path will be executed if MWI_ENABLE_TOKEN_AUTH is set to true/Any value (default workflow)
54
+ generated_token = secrets.token_urlsafe()
55
+ logger.debug("Using auto-generated token.")
56
+ return _format_token_as_dictionary(generated_token)
57
57
 
58
58
 
59
59
  def get_mwi_auth_token_access_str(app_settings):
60
60
  """Returns formatted string with mwi token for use with server URL"""
61
- if is_mwi_token_auth_enabled(app_settings):
62
- mwi_auth_token_name = app_settings["mwi_auth_token_name"]
61
+ if app_settings["mwi_is_token_auth_enabled"]:
62
+ mwi_auth_token_name = app_settings["mwi_auth_token_name_for_http"]
63
63
  mwi_auth_token = app_settings["mwi_auth_token"]
64
64
  return f"?{mwi_auth_token_name}={mwi_auth_token}"
65
65
 
@@ -68,93 +68,236 @@ def get_mwi_auth_token_access_str(app_settings):
68
68
 
69
69
 
70
70
  async def authenticate_request(request):
71
- """Returns True/False based on whether the server is authenticated."""
71
+ """Authenticates incoming request by verifying whether the expected token is in the request.
72
72
 
73
- logger.debug(f" inside authenticate_request for request:{request}")
74
- # Get information from APP
75
- app_settings = request.app["settings"]
76
- # Verify that the request contains the authorization token
77
- if is_mwi_token_auth_enabled(app_settings):
78
- logger.debug(" Token Authentication is Enabled!!")
79
- the_secret_token = app_settings["mwi_auth_token"]
80
- token_name = app_settings["mwi_auth_token_name"]
81
- base_url = app_settings["base_url"]
82
-
83
- # get token if present in URL
84
- parsed_url_token = request.rel_url.query.get(token_name, None)
85
-
86
- if parsed_url_token is None:
87
- logger.debug("No Token found in URL. Checking session cookie...")
88
-
89
- # Check to see if there are cookies?
90
- session = await get_session(request)
91
- logger.debug(f"Got session cookie : {session}")
92
-
93
- if token_name in session:
94
- stored_session_token = session[token_name]
95
- logger.debug(f"Found token with value: {stored_session_token}")
96
- # Verify that token contains expected value
97
- if stored_session_token == the_secret_token:
98
- logger.debug("Token validation success!")
99
- return True
100
- else:
101
- logger.info("Invalid Token found in session!")
102
- logger.debug(f"Expected: {the_secret_token} ")
103
- logger.debug(f"Actual : {stored_session_token}")
104
- return False
105
- else:
106
- logger.info(f"{token_name} not found in session cookie.")
107
- return False
73
+ The mwi_auth_token must be present either in:
74
+ a. session cookie
75
+ b. request's query parameters
76
+ c. or the request's headers.
77
+
78
+ Returns True/False based on whether the request is authenticated.
79
+ Returns True when authentication is disabled.
80
+ """
81
+
82
+ logger.debug(f"<======== Authenticate request: {request}")
83
+
84
+ if _is_mwi_token_auth_enabled(request):
85
+ logger.debug("Authentication is Enabled.")
86
+ is_authenticated = (
87
+ await _is_valid_token_in_session_cookie(request)
88
+ or await _is_valid_token_in_headers(request)
89
+ or await _is_valid_token_in_url_query(request)
90
+ )
91
+ if is_authenticated:
92
+ logger.debug("Authentication successful. ========>")
108
93
  else:
109
- logger.debug(f"Token found in URL with value: {parsed_url_token}")
110
- # Token is being provided, check it and stash it.
111
- if parsed_url_token == the_secret_token:
112
- logger.debug("Token validation success!")
113
- # Stash token in session for other endpoints
114
- # Always use `new_session` during login to guard against
115
- # Session Fixation. See aiohttp-session#281
116
- session = await new_session(request)
117
- session[token_name] = the_secret_token
118
- logger.debug(f"Created session : {session} and saved cookie")
119
- return True
120
- else:
121
- logger.info("Invalid Token found in URL!")
122
- logger.debug(f"Expected: {the_secret_token} ")
123
- logger.debug(f"Actual : {parsed_url_token}")
124
- return False
125
- else:
126
- # Token Authentication is not enabled
127
- logger.debug(" Token Authentication is NOT Enabled!!")
128
- return True
129
-
130
-
131
- def generate_mwi_auth_token():
94
+ logger.error("Token Authentication failed. ========>")
95
+
96
+ return is_authenticated
97
+
98
+ logger.debug("Token Authentication disabled.========>")
99
+ return True
100
+
101
+
102
+ def authenticate_access_decorator(endpoint):
103
+ """This decorator verifies that the request to an endpoint exposed by matlab-proxy
104
+ contains the correct MWI_AUTH_TOKEN before servicing an endpoint."""
105
+
106
+ async def protect_endpoint(request):
107
+ """Passes request to the endpoint after validating the token
108
+
109
+ Args:
110
+ request (HTTPRequest) : Web Request to endpoint
111
+
112
+ Raises:
113
+ web.HTTPForbidden: Thrown when validation of token fails
114
+ """
115
+ if await authenticate_request(request):
116
+ # request is authentic, proceed to execute the endpoint
117
+ return await endpoint(request)
118
+ else:
119
+ raise web.HTTPForbidden(reason="Unauthorized access to matlab-proxy.")
120
+
121
+ return protect_endpoint
122
+
123
+
124
+ ## Module Private Methods:
125
+
126
+
127
+ async def _get_token_name(request):
128
+ """Gets the name of the token from settings.
129
+
130
+ Args:
131
+ request (HTTPRequest) : Used to get to app settings
132
+
133
+ Returns:
134
+ str : token name
132
135
  """
133
- Generate the MWI Token to be used by the server,
134
- based on the environment variables that control it.
136
+ app_settings = request.app["settings"]
137
+ return app_settings["mwi_auth_token_name_for_env"]
135
138
 
136
- Returns None, if Token-Based Authentication is not enabled by user.
139
+
140
+ def _get_token_name_for_http(request):
141
+ """Gets the name of the token from settings.
142
+
143
+ Args:
144
+ request (HTTPRequest) : Used to get to app settings
145
+
146
+ Returns:
147
+ str : token name
148
+ """
149
+ app_settings = request.app["settings"]
150
+ return app_settings["mwi_auth_token_name_for_http"]
151
+
152
+
153
+ async def _get_token(request):
154
+ """Gets the value of secret token from settings.
155
+
156
+ Args:
157
+ request (HTTPRequest) : Used to get to app settings
158
+
159
+ Returns:
160
+ str : token value
161
+ """
162
+ app_settings = request.app["settings"]
163
+ return app_settings[await _get_token_name(request)]
164
+
165
+
166
+ async def _get_token_hash(request):
167
+ """Gets the hashed value of secret token from settings.
168
+
169
+ Args:
170
+ request (HTTPRequest) : Used to get to app settings
171
+
172
+ Returns:
173
+ str : token hash
174
+ """
175
+ app_settings = request.app["settings"]
176
+ return app_settings["mwi_auth_token_hash"]
177
+
178
+
179
+ async def _store_token_hash_into_session(request):
180
+ """Stores the token hash into the session cookie."""
181
+ # Always use `new_session` during login to guard against
182
+ # Session Fixation. See aiohttp-session#281
183
+ session = await new_session(request)
184
+
185
+ # Stash token hash in session for other endpoints
186
+ session[await _get_token_name(request)] = await _get_token_hash(request)
187
+ logger.debug(f"Created session and saved cookie.")
188
+
189
+
190
+ def _is_mwi_token_auth_enabled(request):
191
+ """Returns True/False based on whether the mwi_auth_token_auth is enabled in app settings
192
+
193
+ Args:
194
+ request (HTTPRequest) : Used to get access to app settings
195
+ """
196
+ app_settings = request.app["settings"]
197
+ return app_settings["mwi_is_token_auth_enabled"]
198
+
199
+
200
+ async def _is_valid_token(token, request):
201
+ """Checks if token contains expected value.
202
+
203
+ Args:
204
+ token (str): Token string to validate
205
+ request (HTTPRequest) : Used to access app settings
206
+
207
+ Returns:
208
+ _type_: True is token is valid, false otherwise.
137
209
  """
138
- is_mwi_auth_token_enabled = (
139
- os.getenv(mwi_env.get_env_name_enable_mwi_auth_token(), "false").lower()
140
- == "true"
210
+ # Check if the token provided in the request matches the hash or the original token
211
+ # equivalent to a == b, but protects against timing attacks
212
+ is_valid = compare_digest(token, await _get_token_hash(request)) or compare_digest(
213
+ token, await _get_token(request)
141
214
  )
142
- mwi_auth_token = os.getenv(mwi_env.get_env_name_mwi_auth_token(), None)
215
+ logger.debug("Token validation " + ("successful." if is_valid else "failed."))
216
+ return is_valid
217
+
218
+
219
+ async def _is_valid_token_in_session_cookie(request):
220
+ """Checks the session cookie for auth token
221
+
222
+ Args:
223
+ request (HTTPRequest) : Used to access app settings
224
+
225
+ Returns:
226
+ Boolean : True if valid token is found
227
+ """
228
+ logger.debug("Checking for token in session cookie...")
229
+ session = await get_session(request)
230
+ logger.debug(f"Got session cookie.")
231
+ token_name = await _get_token_name(request)
232
+ if token_name in session:
233
+ stored_session_token = session[token_name]
234
+ logger.debug(f"Found token in session cookie, validating...")
235
+ return await _is_valid_token(stored_session_token, request)
236
+
237
+ logger.debug("Token not found in session cookie.")
238
+ return False
239
+
240
+
241
+ async def _is_valid_token_in_url_query(request):
242
+ """Checks the url_query parameter for auth token
243
+
244
+ Args:
245
+ request (HTTPRequest) : Used to access app settings
246
+
247
+ Returns:
248
+ Boolean : True if valid token is found
249
+ """
250
+ logger.debug("Checking for token in url query...")
251
+ query_string = request.query_string
252
+ logger.debug(f"url query parameters found:{query_string}")
253
+ if query_string:
254
+ token_name = _get_token_name_for_http(request)
255
+ parsed_token = parse_qs(request.query_string).get(token_name)
256
+ if parsed_token:
257
+ parsed_token = parsed_token[0]
258
+ logger.debug("parsed_token from url query string.")
259
+ return await _is_valid_token(parsed_token, request)
260
+
261
+ logger.debug("Token not found in url query.")
262
+ return False
263
+
264
+
265
+ async def _is_valid_token_in_headers(request):
266
+ """Checks the request headers for auth token
267
+ Additionally, save token into session cookie when a token is found.
268
+ This is done to avoid the front end from having to send the token in every header.
269
+
270
+ Args:
271
+ request (HTTPRequest) : Used to access app settings
272
+
273
+ Returns:
274
+ Boolean : True if valid token is found
275
+ """
276
+ logger.debug("Checking for token in request headers...")
277
+ headers = request.headers
278
+ token_name = _get_token_name_for_http(request)
279
+ if token_name in headers:
280
+ logger.debug(f"Token found in headers: {token_name}")
281
+ is_valid_token = await _is_valid_token(headers[token_name], request)
282
+ if is_valid_token:
283
+ await _store_token_hash_into_session(request)
284
+ return is_valid_token
285
+
286
+ logger.debug("Token not found in request headers.")
287
+ return False
288
+
289
+
290
+ def _generate_hash(message):
291
+ """Util function to generate a sha256 hash for a message
292
+
293
+ Args:
294
+ message (str): message to be hashed
295
+
296
+ Returns:
297
+ str: sha256 hash for a given message
298
+ """
299
+ return sha256(message.encode()).hexdigest() if message is not None else None
143
300
 
144
- if is_mwi_auth_token_enabled:
145
- if mwi_auth_token and mwi_auth_token.strip():
146
- # Use the provided MWI token, after stripping leading and trailing whitespaces
147
- mwi_auth_token = mwi_auth_token.strip()
148
- logger.debug(f"Using provided mwi_auth_token: {mwi_auth_token.strip()}")
149
- else:
150
- # Generate a token
151
- mwi_auth_token = secrets.token_urlsafe()
152
- else:
153
- # Token Authentication must be enabled to provide custom tokens.
154
- if mwi_auth_token is not None:
155
- logger.error(
156
- "Ignoring MWI_AUTH_TOKEN. To enable, set MWI_ENABLE_TOKEN_AUTH to True !!!"
157
- )
158
- mwi_auth_token = None
159
301
 
160
- return mwi_auth_token
302
+ def _format_token_as_dictionary(token):
303
+ return {"token": token, "token_hash": _generate_hash(token)}