Skip to content

Commit 7d2154a

Browse files
authored
[Gateway] Fix and deprecate env whitelist handling (#979)
1 parent 8877fdb commit 7d2154a

File tree

3 files changed

+71
-16
lines changed

3 files changed

+71
-16
lines changed

jupyter_server/gateway/gateway_client.py

+48-10
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from tornado import web
1414
from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPResponse
15-
from traitlets import Bool, Float, Int, TraitError, Unicode, default, validate
15+
from traitlets import Bool, Float, Int, TraitError, Unicode, default, observe, validate
1616
from traitlets.config import SingletonConfigurable
1717

1818

@@ -282,20 +282,29 @@ def __init__(self, **kwargs):
282282
# store of cookies with store time
283283
self._cookies = {} # type: ty.Dict[str, ty.Tuple[Morsel, datetime]]
284284

285-
env_whitelist_default_value = ""
286-
env_whitelist_env = "JUPYTER_GATEWAY_ENV_WHITELIST"
287-
env_whitelist = Unicode(
288-
default_value=env_whitelist_default_value,
285+
allowed_envs_default_value = ""
286+
allowed_envs_env = "JUPYTER_GATEWAY_ALLOWED_ENVS"
287+
allowed_envs = Unicode(
288+
default_value=allowed_envs_default_value,
289289
config=True,
290290
help="""A comma-separated list of environment variable names that will be included, along with
291-
their values, in the kernel startup request. The corresponding `env_whitelist` configuration
291+
their values, in the kernel startup request. The corresponding `allowed_envs` configuration
292292
value must also be set on the Gateway server - since that configuration value indicates which
293-
environmental values to make available to the kernel. (JUPYTER_GATEWAY_ENV_WHITELIST env var)""",
293+
environmental values to make available to the kernel. (JUPYTER_GATEWAY_ALLOWED_ENVS env var)""",
294294
)
295295

296-
@default("env_whitelist")
297-
def _env_whitelist_default(self):
298-
return os.environ.get(self.env_whitelist_env, self.env_whitelist_default_value)
296+
@default("allowed_envs")
297+
def _allowed_envs_default(self):
298+
return os.environ.get(
299+
"JUPYTER_GATEWAY_ENV_WHITELIST",
300+
os.environ.get(self.allowed_envs_env, self.allowed_envs_default_value),
301+
)
302+
303+
env_whitelist = Unicode(
304+
default_value=allowed_envs_default_value,
305+
config=True,
306+
help="""Deprecated, use `GatewayClient.allowed_envs`""",
307+
)
299308

300309
gateway_retry_interval_default_value = 1.0
301310
gateway_retry_interval_env = "JUPYTER_GATEWAY_RETRY_INTERVAL"
@@ -386,6 +395,35 @@ def accept_cookies_default(self):
386395
not in ["no", "false"]
387396
)
388397

398+
_deprecated_traits = {
399+
"env_whitelist": ("allowed_envs", "2.0"),
400+
}
401+
402+
# Method copied from
403+
# https://github.com/jupyterhub/jupyterhub/blob/d1a85e53dccfc7b1dd81b0c1985d158cc6b61820/jupyterhub/auth.py#L143-L161
404+
@observe(*list(_deprecated_traits))
405+
def _deprecated_trait(self, change):
406+
"""observer for deprecated traits"""
407+
old_attr = change.name
408+
new_attr, version = self._deprecated_traits[old_attr]
409+
new_value = getattr(self, new_attr)
410+
if new_value != change.new:
411+
# only warn if different
412+
# protects backward-compatible config from warnings
413+
# if they set the same value under both names
414+
self.log.warning(
415+
(
416+
"{cls}.{old} is deprecated in jupyter_server "
417+
"{version}, use {cls}.{new} instead"
418+
).format(
419+
cls=self.__class__.__name__,
420+
old=old_attr,
421+
new=new_attr,
422+
version=version,
423+
)
424+
)
425+
setattr(self, new_attr, change.new)
426+
389427
@property
390428
def gateway_enabled(self):
391429
return bool(self.url is not None and len(self.url) > 0)

jupyter_server/gateway/managers.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -421,15 +421,16 @@ async def start_kernel(self, **kwargs):
421421
if os.environ.get("KERNEL_USERNAME") is None and GatewayClient.instance().http_user:
422422
os.environ["KERNEL_USERNAME"] = GatewayClient.instance().http_user
423423

424+
payload_envs = os.environ.copy()
425+
payload_envs.update(kwargs.get("env", {})) # Add any env entries in this request
426+
427+
# Build the actual env payload, filtering allowed_envs and those starting with 'KERNEL_'
424428
kernel_env = {
425429
k: v
426-
for (k, v) in dict(os.environ).items()
427-
if k.startswith("KERNEL_") or k in GatewayClient.instance().env_whitelist.split(",")
430+
for (k, v) in payload_envs.items()
431+
if k.startswith("KERNEL_") or k in GatewayClient.instance().allowed_envs.split(",")
428432
}
429433

430-
# Add any env entries in this request
431-
kernel_env.update(kwargs.get("env", {}))
432-
433434
# Convey the full path to where this notebook file is located.
434435
if kwargs.get("cwd") is not None and kernel_env.get("KERNEL_WORKING_DIR") is None:
435436
kernel_env["KERNEL_WORKING_DIR"] = kwargs["cwd"]

tests/test_gateway.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ async def mock_gateway_request(url, **kwargs):
104104
env = json_body.get("env")
105105
kspec_name = env.get("KERNEL_KSPEC_NAME")
106106
assert name == kspec_name # Ensure that KERNEL_ env values get propagated
107+
# Verify env propagation is well-behaved...
108+
assert "FOO" in env
109+
assert "BAR" in env
110+
assert "BAZ" not in env
107111
model = generate_model(name)
108112
running_kernels[model.get("id")] = model # Register model as a running kernel
109113
response_buf = BytesIO(json.dumps(model).encode("utf-8"))
@@ -190,6 +194,10 @@ def init_gateway(monkeypatch):
190194
monkeypatch.setenv("JUPYTER_GATEWAY_CONNECT_TIMEOUT", "44.4")
191195
monkeypatch.setenv("JUPYTER_GATEWAY_LAUNCH_TIMEOUT_PAD", "1.1")
192196
monkeypatch.setenv("JUPYTER_GATEWAY_ACCEPT_COOKIES", "false")
197+
monkeypatch.setenv("JUPYTER_GATEWAY_ENV_WHITELIST", "FOO,BAR")
198+
monkeypatch.setenv("FOO", "foo")
199+
monkeypatch.setenv("BAR", "bar")
200+
monkeypatch.setenv("BAZ", "baz")
193201
yield
194202
GatewayClient.clear_instance()
195203

@@ -204,18 +212,20 @@ async def test_gateway_env_options(init_gateway, jp_serverapp):
204212
assert jp_serverapp.gateway_config.connect_timeout == 44.4
205213
assert jp_serverapp.gateway_config.launch_timeout_pad == 1.1
206214
assert jp_serverapp.gateway_config.accept_cookies is False
215+
assert jp_serverapp.gateway_config.allowed_envs == "FOO,BAR"
207216

208217
GatewayClient.instance().init_static_args()
209218
assert GatewayClient.instance().KERNEL_LAUNCH_TIMEOUT == 43
210219

211220

212-
async def test_gateway_cli_options(jp_configurable_serverapp):
221+
async def test_gateway_cli_options(jp_configurable_serverapp, capsys):
213222
argv = [
214223
"--gateway-url=" + mock_gateway_url,
215224
"--GatewayClient.http_user=" + mock_http_user,
216225
"--GatewayClient.connect_timeout=44.4",
217226
"--GatewayClient.request_timeout=96.0",
218227
"--GatewayClient.launch_timeout_pad=5.1",
228+
"--GatewayClient.env_whitelist=FOO,BAR",
219229
]
220230

221231
GatewayClient.clear_instance()
@@ -227,6 +237,12 @@ async def test_gateway_cli_options(jp_configurable_serverapp):
227237
assert app.gateway_config.connect_timeout == 44.4
228238
assert app.gateway_config.request_timeout == 96.0
229239
assert app.gateway_config.launch_timeout_pad == 5.1
240+
assert app.gateway_config.allowed_envs == "FOO,BAR"
241+
captured = capsys.readouterr()
242+
assert (
243+
"env_whitelist is deprecated in jupyter_server 2.0, use GatewayClient.allowed_envs"
244+
in captured.err
245+
)
230246
GatewayClient.instance().init_static_args()
231247
assert (
232248
GatewayClient.instance().KERNEL_LAUNCH_TIMEOUT == 90

0 commit comments

Comments
 (0)