Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update blacklist and whitelist keywords #1367

Merged
merged 4 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/source/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========
Expand Down Expand Up @@ -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.*$",
...
Expand Down
4 changes: 2 additions & 2 deletions tests/app/preheat_multiple_notebooks_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/configs/preheat/voila.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"preheat_kernel": true
},
"VoilaKernelManager": {
"preheat_blacklist": ["blacklisted.ipynb"],
"preheat_denylist": ["denylisted.ipynb"],
"kernel_pools_config": {
"default": {
"pool_size": 1
Expand Down
3 changes: 3 additions & 0 deletions voila/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmmh, Maybe we should not do that and fix the deprecation warnings instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, the DeprecationWarning is hidden by default https://docs.python.org/3/library/exceptions.html#DeprecationWarning



def _jupyter_nbextension_paths():
Expand Down
8 changes: 4 additions & 4 deletions voila/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
)
Expand Down
60 changes: 47 additions & 13 deletions voila/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"]
Comment on lines +68 to +82
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I suggest another approach for this?

@property
def file_whitelist(self):
   warn(
        "Deprecated, use VoilaConfiguration.file_allowlist instead.",
        DeprecationWarning,
        stacklevel=2,
    )
   return self.file_allowlist 


@file_whitelist.setter
def file_whitelist(self, value):
   warn(
        "Deprecated, use VoilaConfiguration.file_allowlist instead.",
        DeprecationWarning,
        stacklevel=2,
    )
   self.file_allowlist = value

That way we can get rid of the

warnings.filterwarnings("default", category=DeprecationWarning, module="traitlets")

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion applies if we consider going from a traitlet to a property is not backward incompatible

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By doing so, users can not set the allow/deny list from the CLI or the voila.json file

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point 👍🏽


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,
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions voila/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions voila/server_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)),
},
),
Expand Down
20 changes: 10 additions & 10 deletions voila/static_file_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 2 additions & 2 deletions voila/tornado/treehandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
46 changes: 23 additions & 23 deletions voila/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__,
Expand Down Expand Up @@ -110,38 +110,38 @@ 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


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:
Expand All @@ -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
]


Expand Down
Loading
Loading