Skip to content

Commit cb0d3aa

Browse files
kevin-batesscanontakluyver
committed
Fix for recursive symlink - Notebook 4670
This change also prevents permission-related exceptions from logging the file. Co-authored-by: Shane Canon <[email protected]> Co-authored-by: Thomas Kluyver <[email protected]>
1 parent ca5622a commit cb0d3aa

File tree

2 files changed

+56
-18
lines changed

2 files changed

+56
-18
lines changed

jupyter_server/services/contents/filemanager.py

+27-10
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def _dir_model(self, path, content=True):
273273
# skip over broken symlinks in listing
274274
if e.errno == errno.ENOENT:
275275
self.log.warning("%s doesn't exist", os_path)
276-
else:
276+
elif e.errno != errno.EACCES: # Don't provide clues about protected files
277277
self.log.warning("Error stat-ing %s: %s", os_path, e)
278278
continue
279279

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

286-
if self.should_list(name):
287-
if self.allow_hidden or not is_file_hidden(os_path, stat_res=st):
288-
contents.append(
286+
try:
287+
if self.should_list(name):
288+
if self.allow_hidden or not is_file_hidden(os_path, stat_res=st):
289+
contents.append(
289290
self.get(path='%s/%s' % (path, name), content=False)
291+
)
292+
except OSError as e:
293+
# ELOOP: recursive symlink, also don't show failure due to permissions
294+
if e.errno not in [errno.ELOOP, errno.EACCES]:
295+
self.log.warning(
296+
"Unknown error checking if file %r is hidden",
297+
os_path,
298+
exc_info=True,
290299
)
291300

292301
model['format'] = 'json'
293302

294303
return model
295304

296-
297305
def _file_model(self, path, content=True, format=None):
298306
"""Build a model for a file
299307
@@ -585,7 +593,7 @@ async def _dir_model(self, path, content=True):
585593
# skip over broken symlinks in listing
586594
if e.errno == errno.ENOENT:
587595
self.log.warning("%s doesn't exist", os_path)
588-
else:
596+
elif e.errno != errno.EACCES: # Don't provide clues about protected files
589597
self.log.warning("Error stat-ing %s: %s", os_path, e)
590598
continue
591599

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

598-
if self.should_list(name):
599-
if self.allow_hidden or not is_file_hidden(os_path, stat_res=st):
600-
contents.append(
601-
await self.get(path='%s/%s' % (path, name), content=False)
606+
try:
607+
if self.should_list(name):
608+
if self.allow_hidden or not is_file_hidden(os_path, stat_res=st):
609+
contents.append(
610+
await self.get(path='%s/%s' % (path, name), content=False)
611+
)
612+
except OSError as e:
613+
# ELOOP: recursive symlink, also don't show failure due to permissions
614+
if e.errno not in [errno.ELOOP, errno.EACCES]:
615+
self.log.warning(
616+
"Unknown error checking if file %r is hidden",
617+
os_path,
618+
exc_info=True,
602619
)
603620

604621
model['format'] = 'json'

jupyter_server/tests/services/contents/test_manager.py

+29-8
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22
import sys
33
import time
44
import pytest
5-
import functools
65
from traitlets import TraitError
76
from tornado.web import HTTPError
87
from itertools import combinations
98

10-
119
from nbformat import v4 as nbformat
1210

1311
from jupyter_server.services.contents.filemanager import AsyncFileContentsManager, FileContentsManager
1412
from jupyter_server.utils import ensure_async
1513
from ...utils import expected_http_error
1614

15+
1716
@pytest.fixture(params=[(FileContentsManager, True),
1817
(FileContentsManager, False),
1918
(AsyncFileContentsManager, True),
@@ -29,6 +28,7 @@ def file_contents_manager_class(request, tmp_path):
2928

3029
# -------------- Functions ----------------------------
3130

31+
3232
def _make_dir(jp_contents_manager, api_path):
3333
"""
3434
Make a directory.
@@ -99,6 +99,7 @@ async def check_populated_dir_files(jp_contents_manager, api_path):
9999

100100
# ----------------- Tests ----------------------------------
101101

102+
102103
def test_root_dir(file_contents_manager_class, tmp_path):
103104
fm = file_contents_manager_class(root_dir=str(tmp_path))
104105
assert fm.root_dir == str(tmp_path)
@@ -116,6 +117,7 @@ def test_invalid_root_dir(file_contents_manager_class, tmp_path):
116117
with pytest.raises(TraitError):
117118
file_contents_manager_class(root_dir=str(temp_file))
118119

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

148150

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

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

173171

174172
@pytest.mark.skipif(
175-
sys.platform == 'win32' and sys.version_info[0] < 3,
176-
reason="System platform is Windows, version < 3"
173+
sys.platform.startswith('win'),
174+
reason="Windows doesn't detect symlink loops"
177175
)
176+
async def test_recursive_symlink(file_contents_manager_class, tmp_path):
177+
td = str(tmp_path)
178+
179+
cm = file_contents_manager_class(root_dir=td)
180+
path = 'test recursive symlink'
181+
_make_dir(cm, path)
182+
183+
file_model = await ensure_async(cm.new_untitled(path=path, ext='.txt'))
184+
185+
# create recursive symlink
186+
symlink(cm, '%s/%s' % (path, "recursive"), '%s/%s' % (path, "recursive"))
187+
model = await ensure_async(cm.get(path))
188+
189+
contents = {
190+
content['name']: content for content in model['content']
191+
}
192+
assert 'untitled.txt' in contents
193+
assert contents['untitled.txt'] == file_model
194+
# recursive symlinks should not be shown in the contents manager
195+
assert 'recursive' not in contents
196+
197+
178198
async def test_good_symlink(file_contents_manager_class, tmp_path):
179199
td = str(tmp_path)
180200
cm = file_contents_manager_class(root_dir=td)
@@ -213,6 +233,7 @@ async def test_403(file_contents_manager_class, tmp_path):
213233
except HTTPError as e:
214234
assert e.status_code == 403
215235

236+
216237
async def test_escape_root(file_contents_manager_class, tmp_path):
217238
td = str(tmp_path)
218239
cm = file_contents_manager_class(root_dir=td)

0 commit comments

Comments
 (0)