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

Fix for recursive symlink - (port Notebook 4670) #497

Merged
merged 1 commit into from
Apr 30, 2021
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
37 changes: 27 additions & 10 deletions jupyter_server/services/contents/filemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def _dir_model(self, path, content=True):
# skip over broken symlinks in listing
if e.errno == errno.ENOENT:
self.log.warning("%s doesn't exist", os_path)
else:
elif e.errno != errno.EACCES: # Don't provide clues about protected files
self.log.warning("Error stat-ing %s: %s", os_path, e)
continue

Expand All @@ -283,17 +283,25 @@ def _dir_model(self, path, content=True):
self.log.debug("%s not a regular file", os_path)
continue

if self.should_list(name):
if self.allow_hidden or not is_file_hidden(os_path, stat_res=st):
contents.append(
try:
if self.should_list(name):
if self.allow_hidden or not is_file_hidden(os_path, stat_res=st):
contents.append(
self.get(path='%s/%s' % (path, name), content=False)
)
except OSError as e:
# ELOOP: recursive symlink, also don't show failure due to permissions
if e.errno not in [errno.ELOOP, errno.EACCES]:
self.log.warning(
"Unknown error checking if file %r is hidden",
os_path,
exc_info=True,
)

model['format'] = 'json'

return model


def _file_model(self, path, content=True, format=None):
"""Build a model for a file

Expand Down Expand Up @@ -585,7 +593,7 @@ async def _dir_model(self, path, content=True):
# skip over broken symlinks in listing
if e.errno == errno.ENOENT:
self.log.warning("%s doesn't exist", os_path)
else:
elif e.errno != errno.EACCES: # Don't provide clues about protected files
self.log.warning("Error stat-ing %s: %s", os_path, e)
continue

Expand All @@ -595,10 +603,19 @@ async def _dir_model(self, path, content=True):
self.log.debug("%s not a regular file", os_path)
continue

if self.should_list(name):
if self.allow_hidden or not is_file_hidden(os_path, stat_res=st):
contents.append(
await self.get(path='%s/%s' % (path, name), content=False)
try:
if self.should_list(name):
if self.allow_hidden or not is_file_hidden(os_path, stat_res=st):
contents.append(
await self.get(path='%s/%s' % (path, name), content=False)
)
except OSError as e:
# ELOOP: recursive symlink, also don't show failure due to permissions
if e.errno not in [errno.ELOOP, errno.EACCES]:
self.log.warning(
"Unknown error checking if file %r is hidden",
os_path,
exc_info=True,
)

model['format'] = 'json'
Expand Down
37 changes: 29 additions & 8 deletions jupyter_server/tests/services/contents/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@
import sys
import time
import pytest
import functools
from traitlets import TraitError
from tornado.web import HTTPError
from itertools import combinations


from nbformat import v4 as nbformat

from jupyter_server.services.contents.filemanager import AsyncFileContentsManager, FileContentsManager
from jupyter_server.utils import ensure_async
from ...utils import expected_http_error


@pytest.fixture(params=[(FileContentsManager, True),
(FileContentsManager, False),
(AsyncFileContentsManager, True),
Expand All @@ -29,6 +28,7 @@ def file_contents_manager_class(request, tmp_path):

# -------------- Functions ----------------------------


def _make_dir(jp_contents_manager, api_path):
"""
Make a directory.
Expand Down Expand Up @@ -99,6 +99,7 @@ async def check_populated_dir_files(jp_contents_manager, api_path):

# ----------------- Tests ----------------------------------


def test_root_dir(file_contents_manager_class, tmp_path):
fm = file_contents_manager_class(root_dir=str(tmp_path))
assert fm.root_dir == str(tmp_path)
Expand All @@ -116,6 +117,7 @@ def test_invalid_root_dir(file_contents_manager_class, tmp_path):
with pytest.raises(TraitError):
file_contents_manager_class(root_dir=str(temp_file))


def test_get_os_path(file_contents_manager_class, tmp_path):
fm = file_contents_manager_class(root_dir=str(tmp_path))
path = fm._get_os_path('/path/to/notebook/test.ipynb')
Expand Down Expand Up @@ -146,10 +148,6 @@ def test_checkpoint_subdir(file_contents_manager_class, tmp_path):
assert cp_dir == os.path.join(str(tmp_path), cpm.checkpoint_dir, cp_name)


@pytest.mark.skipif(
sys.platform == 'win32' and sys.version_info[0] < 3,
reason="System platform is Windows, version < 3"
)
async def test_bad_symlink(file_contents_manager_class, tmp_path):
td = str(tmp_path)

Expand All @@ -172,9 +170,31 @@ async def test_bad_symlink(file_contents_manager_class, tmp_path):


@pytest.mark.skipif(
sys.platform == 'win32' and sys.version_info[0] < 3,
reason="System platform is Windows, version < 3"
sys.platform.startswith('win'),
reason="Windows doesn't detect symlink loops"
)
async def test_recursive_symlink(file_contents_manager_class, tmp_path):
td = str(tmp_path)

cm = file_contents_manager_class(root_dir=td)
path = 'test recursive symlink'
_make_dir(cm, path)

file_model = await ensure_async(cm.new_untitled(path=path, ext='.txt'))

# create recursive symlink
symlink(cm, '%s/%s' % (path, "recursive"), '%s/%s' % (path, "recursive"))
model = await ensure_async(cm.get(path))

contents = {
content['name']: content for content in model['content']
}
assert 'untitled.txt' in contents
assert contents['untitled.txt'] == file_model
# recursive symlinks should not be shown in the contents manager
assert 'recursive' not in contents


async def test_good_symlink(file_contents_manager_class, tmp_path):
td = str(tmp_path)
cm = file_contents_manager_class(root_dir=td)
Expand Down Expand Up @@ -213,6 +233,7 @@ async def test_403(file_contents_manager_class, tmp_path):
except HTTPError as e:
assert e.status_code == 403


async def test_escape_root(file_contents_manager_class, tmp_path):
td = str(tmp_path)
cm = file_contents_manager_class(root_dir=td)
Expand Down