Repository: dask/distributed

Issue:
[Feature] Let allow_websocket_origin to be overridden
When distributed creates the Bokeh server for the dashboard it passes `allow_websocket_origin=["*"]` ([here](https://github.com/dask/distributed/blob/cc4fc7d8fe42c41b328ade1db54dd88da904244a/distributed/dashboard/core.py#L36)).

That's not great from a security standpoint: any webpage on the internet can run Javascript to interact with your dashboard, exploit any security holes there might be in the installed version of Tornado, exfiltrate information about your cluster etc. Changing the default would break compatibility, but it would be nice if I could at least override it for my apps. Unfortunately, passing something through service_kwargs doesn't work, because then the argument is passed twice.

I'd like to suggest changing the way the constructor is called so that first a dictionary is constructed with the internal args, then update it with server_kwargs, then `**` that dict into the call. That way the user kwargs can override the other arguments. Apologies for not providing a PR (yet) - I'm about to go on holiday and wanted to file this before I forgot.


Discussion:
@bmerry , assuming you are still around, do you think you would have the time to write this PR eventually, after you are back?
I'm back from holiday. I should have time to write a PR in the next week or so.

Resolving patch:
diff --git a/distributed/dashboard/core.py b/distributed/dashboard/core.py
--- a/distributed/dashboard/core.py
+++ b/distributed/dashboard/core.py
@@ -28,16 +28,16 @@ def listen(self, addr):
             ip = None
         for i in range(5):
             try:
-                self.server = Server(
-                    self.apps,
+                server_kwargs = dict(
                     port=port,
                     address=ip,
                     check_unused_sessions_milliseconds=500,
                     allow_websocket_origin=["*"],
                     use_index=False,
                     extra_patterns=[(r"/", web.RedirectHandler, {"url": "/status"})],
-                    **self.server_kwargs
                 )
+                server_kwargs.update(self.server_kwargs)
+                self.server = Server(self.apps, **server_kwargs)
                 self.server.start()
 
                 handlers = [


Test patch:
diff --git a/distributed/dashboard/tests/test_scheduler_bokeh_html.py b/distributed/dashboard/tests/test_scheduler_bokeh_html.py
--- a/distributed/dashboard/tests/test_scheduler_bokeh_html.py
+++ b/distributed/dashboard/tests/test_scheduler_bokeh_html.py
@@ -6,7 +6,8 @@
 pytest.importorskip("bokeh")
 
 from tornado.escape import url_escape
-from tornado.httpclient import AsyncHTTPClient, HTTPClientError
+from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPRequest
+from tornado.websocket import websocket_connect
 
 from dask.sizeof import sizeof
 from distributed.utils import is_valid_xml
@@ -153,3 +154,26 @@ def test_task_page(c, s, a, b):
     assert "int" in body
     assert a.address in body
     assert "memory" in body
+
+
+@gen_cluster(
+    client=True,
+    scheduler_kwargs={
+        "services": {
+            ("dashboard", 0): (
+                BokehScheduler,
+                {"allow_websocket_origin": ["good.invalid"]},
+            )
+        }
+    },
+)
+def test_allow_websocket_origin(c, s, a, b):
+    url = (
+        "ws://localhost:%d/status/ws?bokeh-protocol-version=1.0&bokeh-session-id=1"
+        % s.services["dashboard"].port
+    )
+    with pytest.raises(HTTPClientError) as err:
+        yield websocket_connect(
+            HTTPRequest(url, headers={"Origin": "http://evil.invalid"})
+        )
+    assert err.value.code == 403