diff --git a/demos/demo_multiple_classes/jupyter_server_config.py b/demos/demo_multiple_classes/jupyter_server_config.py new file mode 100644 index 000000000..603335e1b --- /dev/null +++ b/demos/demo_multiple_classes/jupyter_server_config.py @@ -0,0 +1,6 @@ +c = get_config() + +c.ServerApp.tornado_settings = {} +c.ServerApp.tornado_settings["headers"] = { + "Content-Security-Policy": "frame-ancestors 'self'" +} diff --git a/demos/demo_one_class_multiple_graders/jupyter_server_config.py b/demos/demo_one_class_multiple_graders/jupyter_server_config.py new file mode 100644 index 000000000..603335e1b --- /dev/null +++ b/demos/demo_one_class_multiple_graders/jupyter_server_config.py @@ -0,0 +1,6 @@ +c = get_config() + +c.ServerApp.tornado_settings = {} +c.ServerApp.tornado_settings["headers"] = { + "Content-Security-Policy": "frame-ancestors 'self'" +} diff --git a/demos/demo_one_class_one_grader/jupyter_server_config.py b/demos/demo_one_class_one_grader/jupyter_server_config.py new file mode 100644 index 000000000..603335e1b --- /dev/null +++ b/demos/demo_one_class_one_grader/jupyter_server_config.py @@ -0,0 +1,6 @@ +c = get_config() + +c.ServerApp.tornado_settings = {} +c.ServerApp.tornado_settings["headers"] = { + "Content-Security-Policy": "frame-ancestors 'self'" +} diff --git a/demos/utils.sh b/demos/utils.sh index e80019105..5476eea99 100644 --- a/demos/utils.sh +++ b/demos/utils.sh @@ -58,6 +58,7 @@ setup_jupyterhub () { # Copy config file. cp jupyterhub_config.py "${jupyterhub_root}/jupyterhub_config.py" + cp jupyter_server_config.py /usr/local/etc/jupyter/jupyter_server_config.py } enable_create_assignment () { diff --git a/nbgrader/docs/source/configuration/images/jupyterhub_4.1_iframe.png b/nbgrader/docs/source/configuration/images/jupyterhub_4.1_iframe.png new file mode 100644 index 000000000..f65566c8e Binary files /dev/null and b/nbgrader/docs/source/configuration/images/jupyterhub_4.1_iframe.png differ diff --git a/nbgrader/docs/source/configuration/jupyterhub_config.rst b/nbgrader/docs/source/configuration/jupyterhub_config.rst index e750bb9f6..ec472a124 100644 --- a/nbgrader/docs/source/configuration/jupyterhub_config.rst +++ b/nbgrader/docs/source/configuration/jupyterhub_config.rst @@ -20,6 +20,20 @@ Using nbgrader with JupyterHub much required reading if you want to integrate the formgrader with JupyterHub. +.. warning:: + + For security reasons, ``iframe`` are not allowed with JupyterHub from version 4.1. The + documentation about this security change is at + `mitigating-same-origin-deployments `_. + + In the current version of nbgrader, the ``formgrader`` UI is embedded in an ``iframe``, to + be available in a new tab of Jupyterlab or Notebook. Therefore, the ``formgrader`` UI can't + be loaded when using ``jupyterhub>=4.1``, and shows a blank panel instead. + + There are several ways to use the ``formgrader`` with ``jupyterhub>=4.1``, see details + at :ref:`jupyterhub-4.1`. + + For instructors running a class with JupyterHub, nbgrader offers several tools that optimize and enrich the instructors' and students' experience of sharing the same system. By integrating with JupyterHub, nbgrader streamlines the @@ -331,3 +345,55 @@ API .. automethod:: add_student_to_course .. automethod:: remove_student_from_course + + +.. _jupyterhub-4.1: + +Formgrader with ``jupyterhub>=4.1`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As explained above in the warning, ``jupyterhub>=4.1`` does not allow iframe for security +reasons, which lead to blank panel instead of the ``formgrader`` UI. + +Below are different ways to use the ``formgrader`` UI with ``jupyterhub>=4.1``. + +Opening the ``formgrader`` UI in a new browser tab +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Web browsers are able to open iframes in a new browser tab, which allows using the +``formgrader`` without any additional setting on the jupyterhub server. +For example with Firefox, right clicking on the iframe shows a context menu to open the +contents in a new browser tab. + +.. image:: images/jupyterhub_4.1_iframe.png + +Although this solution isn't the most practical, it does allow to use ```formgrader`` +without having to update the configuration and without adding vulnerabilities to the application. + +Enabling JupyterHub subdomains +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Enabling per-user and per-service subdomains with ``JupyterHub.enable_subdomains = True`` +allows to securely use iframes with JupyterHub. +With subdomains enabled, `frame-ancestors 'self'` allows embedding the iframe only on pages +served by the user's own server. + +In this case, the ``"frame-ancestor 'self'"`` can be restored in the application: + +.. code:: python + + c.ServerApp.tornado_settings = {} + c.ServerApp.tornado_settings["headers"] = { + "Content-Security-Policy": "frame-ancestors 'self'" + } + +in e.g. ``/usr/local/etc/jupyter/jupyter_server_config.py``. + +Trusting users (less secure) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you trust users and are aware of the security vulnerability, it is also possible to +enable the iframe with the same configuration as above, without subdomains. + +This is the solution used in the JupyterHub docker +`demo `_. diff --git a/nbgrader/server_extensions/formgrader/base.py b/nbgrader/server_extensions/formgrader/base.py index 7877b96f7..37254d137 100644 --- a/nbgrader/server_extensions/formgrader/base.py +++ b/nbgrader/server_extensions/formgrader/base.py @@ -60,10 +60,6 @@ def api(self): api.log_level = level return api - def initialize(self): - super().initialize() - self.set_header("Content-Security-Policy", "frame-ancestors 'self'") - def render(self, name, **ns): template = self.settings['nbgrader_jinja2_env'].get_template(name) return template.render(**ns)