diff --git a/docs/source/customize.rst b/docs/source/customize.rst index e11b844d8..057a0d13f 100755 --- a/docs/source/customize.rst +++ b/docs/source/customize.rst @@ -363,17 +363,17 @@ Serving static files Unlike JupyterLab or the classic notebook server, ``voila`` does not serve all files that are present in the directory of the notebook. Only files that -match one of the whitelists and none of the blacklist regular expression are +match one of the allowlist and none of the denylist regular expression are served by VoilĂ :: - voila mydir --VoilaConfiguration.file_whitelist="['.*']" \ - --VoilaConfiguration.file_blacklist="['private.*', '.*\.(ipynb)']" + voila mydir --VoilaConfiguration.file_allowlist="['.*']" \ + --VoilaConfiguration.file_denylist="['private.*', '.*\.(ipynb)']" Which will serve all files, except anything starting with private, or notebook files:: - voila mydir --VoilaConfiguration.file_whitelist="['.*\.(png|jpg|gif|svg|mp4|avi|ogg)']" + voila mydir --VoilaConfiguration.file_allowlist="['.*\.(png|jpg|gif|svg|mp4|avi|ogg)']" -Will serve many media files, and also never serve notebook files (which is the default blacklist). +Will serve many media files, and also never serve notebook files (which is the default denylist). Run scripts =========== @@ -466,7 +466,7 @@ Here is an example of settings with explanations for preheating kernel option. }, "VoilaKernelManager": { # A list of notebook name or regex patterns to exclude notebooks from using preheat kernel. - "preheat_blacklist": [ + "preheat_denylist": [ "notebook-does-not-need-preheat.ipynb", "^.*foo.*$", ... diff --git a/tests/app/preheat_multiple_notebooks_test.py b/tests/app/preheat_multiple_notebooks_test.py index 9682616a3..8e34f6131 100644 --- a/tests/app/preheat_multiple_notebooks_test.py +++ b/tests/app/preheat_multiple_notebooks_test.py @@ -46,12 +46,12 @@ async def test_render_notebook_with_heated_kernel(http_server_client, base_url): await asyncio.sleep(NOTEBOOK_EXECUTION_TIME + 1) -async def test_render_blacklisted_notebook_with_nornal_kernel( +async def test_render_denylisted_notebook_with_nornal_kernel( http_server_client, base_url ): await asyncio.sleep(NUMBER_PREHEATED_KERNEL * NOTEBOOK_EXECUTION_TIME + 1) time, text = await send_request( - sc=http_server_client, url=f"{base_url}voila/render/blacklisted.ipynb" + sc=http_server_client, url=f"{base_url}voila/render/denylisted.ipynb" ) assert "hello world" in text diff --git a/tests/configs/preheat/voila.json b/tests/configs/preheat/voila.json index 86c41c895..f2b816215 100644 --- a/tests/configs/preheat/voila.json +++ b/tests/configs/preheat/voila.json @@ -3,7 +3,7 @@ "preheat_kernel": true }, "VoilaKernelManager": { - "preheat_blacklist": ["blacklisted.ipynb"], + "preheat_denylist": ["denylisted.ipynb"], "kernel_pools_config": { "default": { "pool_size": 1 diff --git a/tests/notebooks/preheat/blacklisted.ipynb b/tests/notebooks/preheat/denylisted.ipynb similarity index 100% rename from tests/notebooks/preheat/blacklisted.ipynb rename to tests/notebooks/preheat/denylisted.ipynb diff --git a/voila/__init__.py b/voila/__init__.py index a790f7070..73d8004f0 100644 --- a/voila/__init__.py +++ b/voila/__init__.py @@ -10,6 +10,9 @@ from ._version import __version__ # noqa from .server_extension import _load_jupyter_server_extension # noqa from .server_extension import load_jupyter_server_extension # noqa +import warnings + +warnings.filterwarnings("default", category=DeprecationWarning, module="traitlets") def _jupyter_nbextension_paths(): diff --git a/voila/app.py b/voila/app.py index 3f1febddf..f57eb1b4b 100644 --- a/voila/app.py +++ b/voila/app.py @@ -71,9 +71,9 @@ from .request_info_handler import RequestInfoSocketHandler from .shutdown_kernel_handler import VoilaShutdownKernelHandler from .static_file_handler import ( + AllowListFileHandler, MultiStaticFileHandler, TemplateStaticFileHandler, - WhiteListFileHandler, ) from .tornado.handler import TornadoVoilaHandler from .tornado.treehandler import TornadoVoilaTreeHandler @@ -701,10 +701,10 @@ def init_handlers(self) -> List: handlers.append( ( url_path_join(self.server_url, r"/voila/files/(.*)"), - WhiteListFileHandler, + AllowListFileHandler, { - "whitelist": self.voila_configuration.file_whitelist, - "blacklist": self.voila_configuration.file_blacklist, + "allowlist": self.voila_configuration.file_allowlist, + "denylist": self.voila_configuration.file_denylist, "path": self.root_dir, }, ) diff --git a/voila/configuration.py b/voila/configuration.py index 5ab4caf7b..6747ef64f 100644 --- a/voila/configuration.py +++ b/voila/configuration.py @@ -8,7 +8,9 @@ ############################################################################# import traitlets.config -from traitlets import Bool, Dict, Enum, Int, List, Type, Unicode +from traitlets import Bool, Dict, Enum, Int, List, Type, Unicode, validate + +from warnings import warn class VoilaConfiguration(traitlets.config.Configurable): @@ -52,30 +54,62 @@ class VoilaConfiguration(traitlets.config.Configurable): ) strip_sources = Bool(True, config=True, help="Strip sources from rendered html") - file_whitelist = List( + file_allowlist = List( Unicode(), [r".*\.(png|jpg|gif|svg)"], config=True, help=r""" List of regular expressions for controlling which static files are served. - All files that are served should at least match 1 whitelist rule, and no blacklist rule - Example: --VoilaConfiguration.file_whitelist="['.*\.(png|jpg|gif|svg)', 'public.*']" + All files that are served should at least match 1 allowlist rule, and no denylist rule + Example: --VoilaConfiguration.file_allowlist="['.*\.(png|jpg|gif|svg)', 'public.*']" """, ) - file_blacklist = List( + file_whitelist = List( + Unicode(), + [r".*\.(png|jpg|gif|svg)"], + config=True, + help="""Deprecated, use `file_allowlist`""", + ) + + @validate("file_whitelist") + def _valid_file_whitelist(self, proposal): + warn( + "Deprecated, use VoilaConfiguration.file_allowlist instead.", + DeprecationWarning, + stacklevel=2, + ) + return proposal["value"] + + file_denylist = List( Unicode(), [r".*\.(ipynb|py)"], config=True, help=r""" - List of regular expressions for controlling which static files are forbidden to be served. - All files that are served should at least match 1 whitelist rule, and no blacklist rule - Example: - --VoilaConfiguration.file_whitelist="['.*']" # all files - --VoilaConfiguration.file_blacklist="['private.*', '.*\.(ipynb)']" # except files in the private dir and notebook files + List of regular expressions for controlling which static files are forbidden to be served. + All files that are served should at least match 1 allowlist rule, and no denylist rule + Example: + --VoilaConfiguration.file_allowlist="['.*']" # all files + --VoilaConfiguration.file_denylist="['private.*', '.*\.(ipynb)']" # except files in the private dir and notebook files """, ) + file_blacklist = List( + Unicode(), + [r".*\.(ipynb|py)"], + config=True, + help="""Deprecated, use `file_denylist`""", + ) + + @validate("file_blacklist") + def _valid_file_blacklist(self, proposal): + warn( + "Deprecated, use VoilaConfiguration.file_denylist instead.", + DeprecationWarning, + stacklevel=2, + ) + return proposal["value"] + language_kernel_mapping = Dict( {}, config=True, @@ -141,16 +175,16 @@ class VoilaConfiguration(traitlets.config.Configurable): """, ) - extension_whitelist = List( + extension_allowlist = List( None, allow_none=True, config=True, help="""The list of enabled JupyterLab extensions, if `None`, all extensions are loaded. - This setting has higher priority than the `extension_blacklist` + This setting has higher priority than the `extension_denylist` """, ) - extension_blacklist = List( + extension_denylist = List( None, allow_none=True, config=True, diff --git a/voila/handler.py b/voila/handler.py index f0a668fad..753787c4e 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -19,6 +19,8 @@ from tornado.httputil import split_host_and_port from traitlets.traitlets import Bool +from .configuration import VoilaConfiguration + from ._version import __version__ from .notebook_renderer import NotebookRenderer from .request_info_handler import RequestInfoSocketHandler @@ -68,7 +70,7 @@ def initialize(self, **kwargs): self.notebook_path = kwargs.pop("notebook_path", []) # should it be [] self.template_paths = kwargs.pop("template_paths", []) self.traitlet_config = kwargs.pop("config", None) - self.voila_configuration = kwargs["voila_configuration"] + self.voila_configuration: VoilaConfiguration = kwargs["voila_configuration"] self.prelaunch_hook = kwargs.get("prelaunch_hook", None) # we want to avoid starting multiple kernels due to template mistakes self.kernel_started = False @@ -198,8 +200,8 @@ async def get_generator(self, path=None): base_url=self.base_url, settings=self.settings, log=self.log, - extension_whitelist=self.voila_configuration.extension_whitelist, - extension_blacklist=self.voila_configuration.extension_blacklist, + extension_allowlist=self.voila_configuration.extension_allowlist, + extension_denylist=self.voila_configuration.extension_denylist, ), mathjax_config=mathjax_config, mathjax_url=mathjax_url, diff --git a/voila/server_extension.py b/voila/server_extension.py index 239b6b2a4..6c18e5336 100644 --- a/voila/server_extension.py +++ b/voila/server_extension.py @@ -22,7 +22,7 @@ from .static_file_handler import ( MultiStaticFileHandler, TemplateStaticFileHandler, - WhiteListFileHandler, + AllowListFileHandler, ) from .tornado.treehandler import TornadoVoilaTreeHandler from .utils import get_data_dir, get_server_root_dir, pjoin @@ -110,10 +110,10 @@ def _load_jupyter_server_extension(server_app): ), ( url_path_join(base_url, r"/voila/files/(.*)"), - WhiteListFileHandler, + AllowListFileHandler, { - "whitelist": voila_configuration.file_whitelist, - "blacklist": voila_configuration.file_blacklist, + "allowlist": voila_configuration.file_allowlist, + "denylist": voila_configuration.file_denylist, "path": os.path.expanduser(get_server_root_dir(web_app.settings)), }, ), diff --git a/voila/static_file_handler.py b/voila/static_file_handler.py index 390d70415..800ad1fe9 100644 --- a/voila/static_file_handler.py +++ b/voila/static_file_handler.py @@ -95,19 +95,19 @@ def get_absolute_path(self, root, path): return abspath -class WhiteListFileHandler(tornado.web.StaticFileHandler): - def initialize(self, whitelist=[], blacklist=[], **kwargs): - self.whitelist = whitelist - self.blacklist = blacklist +class AllowListFileHandler(tornado.web.StaticFileHandler): + def initialize(self, allowlist=[], denylist=[], **kwargs): + self.allowlist = allowlist + self.denylist = denylist super().initialize(**kwargs) def get_absolute_path(self, root, path): # StaticFileHandler.get always calls this method first, so we use this as the # place to check the path. Note that now the path separator is os dependent (\\ on windows) - whitelisted = any(re.fullmatch(pattern, path) for pattern in self.whitelist) - blacklisted = any(re.fullmatch(pattern, path) for pattern in self.blacklist) - if not whitelisted: - raise tornado.web.HTTPError(403, "File not whitelisted") - if blacklisted: - raise tornado.web.HTTPError(403, "File blacklisted") + allowlisted = any(re.fullmatch(pattern, path) for pattern in self.allowlist) + denylisted = any(re.fullmatch(pattern, path) for pattern in self.denylist) + if not allowlisted: + raise tornado.web.HTTPError(403, "File not allowlisted") + if denylisted: + raise tornado.web.HTTPError(403, "File denylisted") return super().get_absolute_path(root, path) diff --git a/voila/tornado/treehandler.py b/voila/tornado/treehandler.py index 5c79c6079..14348e108 100644 --- a/voila/tornado/treehandler.py +++ b/voila/tornado/treehandler.py @@ -49,8 +49,8 @@ def allowed_content(content): base_url=self.base_url, settings=self.settings, log=self.log, - extension_whitelist=self.voila_configuration.extension_whitelist, - extension_blacklist=self.voila_configuration.extension_blacklist, + extension_allowlist=self.voila_configuration.extension_allowlist, + extension_denylist=self.voila_configuration.extension_denylist, ) page_config["jupyterLabTheme"] = theme_arg diff --git a/voila/utils.py b/voila/utils.py index 8115e14be..43bd95c1c 100644 --- a/voila/utils.py +++ b/voila/utils.py @@ -80,8 +80,8 @@ def get_page_config( base_url, settings, log, - extension_whitelist: List[str] = [], - extension_blacklist: List[str] = [], + extension_allowlist: List[str] = [], + extension_denylist: List[str] = [], ): page_config = { "appVersion": __version__, @@ -110,15 +110,15 @@ def get_page_config( "@voila-dashboards/jupyterlab-preview", "@jupyter/collaboration-extension", ] - must_have_extensions = ["@jupyter-widgets/jupyterlab-manager"] + required_extensions = ["@jupyter-widgets/jupyterlab-manager"] federated_extensions = deepcopy(page_config["federated_extensions"]) page_config["federated_extensions"] = filter_extension( federated_extensions=federated_extensions, disabled_extensions=disabled_extensions, - must_have_extensions=must_have_extensions, - extension_whitelist=extension_whitelist, - extension_blacklist=extension_blacklist, + required_extensions=required_extensions, + extension_allowlist=extension_allowlist, + extension_denylist=extension_denylist, ) return page_config @@ -126,22 +126,22 @@ def get_page_config( def filter_extension( federated_extensions: List[Dict], disabled_extensions: List[str] = [], - must_have_extensions: List[str] = [], - extension_whitelist: List[str] = [], - extension_blacklist: List[str] = [], + required_extensions: List[str] = [], + extension_allowlist: List[str] = [], + extension_denylist: List[str] = [], ) -> List[Dict]: """Create a list of extension to be loaded from available extensions and the - black/white list configuration. + allow/deny list configuration. Args: - federated_extensions (List[Dict]): List of available extension - disabled_extensions (List[str], optional): List of extension disabled by default. Defaults to []. - - must_have_extensions (List[str], optional): List of extension must be enabled. + - required_extensions (List[str], optional): List of required extensions. Defaults to []. - - extension_whitelist (List[str], optional): The white listed extensions. + - extension_allowlist (List[str], optional): The allowlisted extensions. Defaults to []. - - extension_blacklist (List[str], optional): The black listed extensions. + - extension_denylist (List[str], optional): The denylisted extensions. Defaults to []. Returns: @@ -150,31 +150,31 @@ def filter_extension( filtered_extensions = [ x for x in federated_extensions if x["name"] not in disabled_extensions ] - if len(extension_blacklist) == 0: - if len(extension_whitelist) == 0: - # No white and black list, return all + if len(extension_denylist) == 0: + if len(extension_allowlist) == 0: + # No allow and deny list, return all return filtered_extensions - # White list is not empty, return white listed only + # Allow list is not empty, return allow listed only return [ x for x in filtered_extensions - if x["name"] in must_have_extensions or x["name"] in extension_whitelist + if x["name"] in required_extensions or x["name"] in extension_allowlist ] - if len(extension_whitelist) == 0: - # No white list, return non black listed only + if len(extension_allowlist) == 0: + # No allow list, return non deny listed only return [ x for x in filtered_extensions - if x["name"] in must_have_extensions or x["name"] not in extension_blacklist + if x["name"] in required_extensions or x["name"] not in extension_denylist ] - # Have both black and white list, use only white list + # Have both allow and deny list, use only allow list return [ x for x in filtered_extensions - if x["name"] in must_have_extensions or x["name"] in extension_whitelist + if x["name"] in required_extensions or x["name"] in extension_allowlist ] diff --git a/voila/voila_kernel_manager.py b/voila/voila_kernel_manager.py index 62bef73b8..49a0bbda1 100644 --- a/voila/voila_kernel_manager.py +++ b/voila/voila_kernel_manager.py @@ -16,8 +16,10 @@ from typing import Dict as TypeDict from typing import List as TypeList from typing import Tuple, Type, TypeVar, Union +from warnings import warn from jupyter_core.utils import ensure_async +from traitlets import validate from traitlets.traitlets import Dict, Float, List, default from .notebook_renderer import NotebookRenderer @@ -86,12 +88,27 @@ class VoilaKernelManager(base_class): """, ) - preheat_blacklist = List( + preheat_denylist = List( [], config=True, help="List of notebooks which do not use pre-heated kernel.", ) + preheat_blacklist = List( + [], + config=True, + help="Deprecated, use `preheat_denylist`.", + ) + + @validate("preheat_blacklist") + def _valid_preheat_blacklist(self, proposal): + warn( + "Deprecated, use preheat_denylist instead.", + DeprecationWarning, + stacklevel=2, + ) + return proposal["value"] + fill_delay = Float( 1, config=True, @@ -383,27 +400,27 @@ def _notebook_renderer_factory( base_url=self.parent.base_url, settings=self.parent.app.settings, log=self.parent.log, - extension_whitelist=voila_configuration.extension_whitelist, - extension_blacklist=voila_configuration.extension_blacklist, + extension_allowlist=voila_configuration.extension_allowlist, + extension_denylist=voila_configuration.extension_denylist, ), mathjax_config=mathjax_config, mathjax_url=mathjax_url, ) def _notebook_filter(self, nb_path: Path) -> bool: - """Helper to filter blacklisted notebooks. + """Helper to filter denylisted notebooks. Args: nb_path (Path): Path to notebook Returns: bool: return `False` if notebook is in `ipynb_checkpoints` folder or - is blacklisted, `True` otherwise. + is denylisted, `True` otherwise. """ nb_name = str(nb_path) if ".ipynb_checkpoints" in nb_name: return False - for nb_pattern in self.preheat_blacklist: + for nb_pattern in self.preheat_denylist: pattern = re.compile(nb_pattern) if (nb_pattern in nb_name) or bool(pattern.match(nb_name)): return False