Skip to content

Commit bdf95c1

Browse files
committed
Elaborate the grid_sftp sharelink access handling for a better and more
consistent client experience. Basically filter certain operations like `chattr` and `stat`/`lstat` more carefully in order to let read-only and write-only shares behave. For read-only shares it is necessary to allow non-changing `chattr` calls to avoid `sftp` failing in simple `get path` downloads. For write-only shares it's necessary to allow `lstat`/`stat` on given files and folders to avoid errors e.g. when sftp or sshfs mount `rm`s a file. Additional tweaks to allow non-changing `chmod`/`chown` in read-only shares are still under consideration.
1 parent a480cd3 commit bdf95c1

File tree

1 file changed

+86
-23
lines changed

1 file changed

+86
-23
lines changed

mig/server/grid_sftp.py

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,69 @@
116116

117117
configuration, logger = None, None
118118

119-
_read_ops = ['stat', 'lstat', 'list_folder', 'check', 'read', 'readlink',
119+
_read_ops = ['list_folder', 'check', 'read', 'readlink',
120120
'open+read']
121121
_write_ops = ['mkdir', 'rmdir', 'remove', 'rename', 'chmod', 'chown',
122-
'chattr', 'write', 'truncate', 'open+write']
122+
'write', 'truncate', 'open+write']
123+
_filter_ops = ['chattr', 'lstat', 'stat']
123124
_other_ops = ['close']
124125

126+
def list_attr_changes(configuration, path, attr):
127+
"""Decipher actual attribute changes requested in attr on path"""
128+
_logger = configuration.logger
129+
_logger.debug("check %s attr changes: %s" % (path, attr))
130+
changes = []
131+
for field in ('st_size', 'st_uid', 'st_gid', 'st_mode', 'st_atime',
132+
'st_mtime'):
133+
#_logger.debug("checking path %s attr %s" % (path, field))
134+
val = getattr(attr, field, None)
135+
if val is not None:
136+
_logger.debug("found %s attr %s value %s" % (path, field, val))
137+
changes.append(field)
138+
return changes
139+
140+
def filter_data_access(configuration, path, access_mode, operation, args=[]):
141+
"""Filter access to what operation is allowed to do based on path, args
142+
and access_mode.
143+
In read-write mode all operations are allowed to proceed path checks.
144+
In write-only mode the potentially changing operations are always allowed
145+
and specific stat/lstat is allowed as well to make most clients behave
146+
without conusing errors. It doesn't reveal any directory or file contents
147+
apart from metadata about paths the user already knows about.
148+
In read-only mode stat is always allowed while the potentially changing
149+
operations are rejected if they would actually modify anything.
150+
"""
151+
_logger = configuration.logger
152+
_logger.debug("filter %s operation on %s with %s args in %s session" %
153+
(operation, path, args, access_mode))
154+
if access_mode == READ_WRITE_ACCESS:
155+
return True
156+
if access_mode == READ_ONLY_ACCESS:
157+
_logger.debug("checking %s on %s with %s in %s session" %
158+
(operation, path, args, access_mode))
159+
if operation in ['lstat', 'stat']:
160+
return True
161+
# NOTE: allow non-changing chattr
162+
# TODO: allow non-changing chown and chmod, too?
163+
if operation == 'chattr' and args:
164+
if not list_attr_changes(configuration, path, args[0]):
165+
return True
166+
167+
_logger.warning("invalid %s operation on %s in %s session" %
168+
(operation, path, access_mode))
169+
return False
170+
if access_mode == WRITE_ONLY_ACCESS:
171+
if operation in ['chattr']:
172+
return True
173+
# NOTE: allow specific lstat/stat for write-only shares
174+
if operation in ['lstat', 'stat']:
175+
return True
176+
_logger.warning("invalid %s operation on %s in %s session" %
177+
(operation, path, access_mode))
178+
return False
125179

126-
def check_data_access(configuration, user_id, path, access_mode, operation):
180+
def check_data_access(configuration, user_id, path, access_mode, operation,
181+
args=[]):
127182
"""A simple helper to check if user_id has access to execute operation on path
128183
under the constraints set by access_mode. Namely, check if operation
129184
could potentially modify data in a read-only session or lookup/read data
@@ -140,6 +195,11 @@ def check_data_access(configuration, user_id, path, access_mode, operation):
140195
_logger.warning("invalid %s operation for %s in %s session" %
141196
(operation, user_id, access_mode))
142197
return False
198+
elif operation in _filter_ops:
199+
_logger.debug("filter %s operation on %s with %s args for %r in %s session" %
200+
(operation, path, args, user_id, access_mode))
201+
return filter_data_access(configuration, path, access_mode, operation,
202+
args)
143203
elif operation in _other_ops:
144204
_logger.debug("ignoring %s operation for %r in %s session" %
145205
(operation, user_id, access_mode))
@@ -345,7 +405,7 @@ def _impl(self, *method_args, **method_kwargs):
345405
% endpos \
346406
+ " file endpos: %s, '%s'" % \
347407
(file_endpos, self.sftpserver._get_fs_path(
348-
path, operation=method.__name__))
408+
path, operation=method.__name__, args=[]))
349409
logger.warning(msg)
350410

351411
# close
@@ -793,13 +853,13 @@ def __gdp_log(self, operation, path, dst_path=None, flags=None,
793853

794854
# Use shared daemon fs helper functions
795855

796-
def _get_fs_path(self, sftp_path, operation=None):
856+
def _get_fs_path(self, sftp_path, operation=None, args=[]):
797857
"""Wrap helper"""
798858
# self.logger.debug("get_fs_path: check access to %s" % [sftp_path])
799859
if not check_data_access(configuration, self.user_name, sftp_path,
800-
self.access, operation):
801-
self.logger.warning("get_fs_path: refused %s %s access to %r" %
802-
(self.user_name, operation, sftp_path))
860+
self.access, operation, args):
861+
self.logger.warning("get_fs_path: refused %s %s (%s) access to %r" %
862+
(self.user_name, operation, args, sftp_path))
803863
raise AccessError("%r not allowed to %s %r in this %s session" %
804864
(self.user_name, operation, sftp_path,
805865
self.access))
@@ -846,11 +906,14 @@ def _chattr(self, path, attr, sftphandle=None):
846906
# path = force_utf8(path)
847907
# Paramiko seems to handle *native* strings correctly across py version
848908
path = force_native_str(path)
849-
# self.logger.debug("_chattr %s" % [path])
909+
self.logger.debug("_chattr %r %r" % (path, attr))
850910
try:
851-
real_path = self._get_fs_path(path, operation='chattr')
852-
except ValueError as err:
853-
self.logger.warning('chattr %s: %s' % ([path], [err]))
911+
real_path = self._get_fs_path(path, operation='chattr', args=[attr])
912+
except AccessError as aer:
913+
self.logger.warning('chattr %s: %s' % ([path], [aer]))
914+
return paramiko.SFTP_PERMISSION_DENIED
915+
except ValueError as ver:
916+
self.logger.warning('chattr %s: %s' % ([path], [ver]))
854917
return paramiko.SFTP_PERMISSION_DENIED
855918
if not os.path.exists(real_path):
856919
self.logger.warning("chattr on missing path %s :: %s" %
@@ -928,7 +991,7 @@ def _chmod(self, path, mode, sftphandle=None):
928991
# path = force_utf8(path)
929992
# self.logger.debug("_chmod %s" % path)
930993
try:
931-
real_path = self._get_fs_path(path, operation='chmod')
994+
real_path = self._get_fs_path(path, operation='chmod', args=[mode])
932995
except ValueError as err:
933996
self.logger.warning('chmod %s: %s' % (path, err))
934997
return paramiko.SFTP_PERMISSION_DENIED
@@ -985,7 +1048,7 @@ def open(self, path, flags, attr):
9851048
open_flavor = 'open+read'
9861049

9871050
try:
988-
real_path = self._get_fs_path(path, operation=open_flavor)
1051+
real_path = self._get_fs_path(path, operation=open_flavor, args=[flags])
9891052
except ValueError as err:
9901053
self.logger.warning('open %s: %s' % ([path], [err]))
9911054
return paramiko.SFTP_PERMISSION_DENIED
@@ -1059,7 +1122,7 @@ def list_folder(self, path):
10591122
"""Handle operations of same name"""
10601123
# self.logger.debug('list_folder %s' % [path])
10611124
try:
1062-
real_path = self._get_fs_path(path, operation='list_folder')
1125+
real_path = self._get_fs_path(path, operation='list_folder', args=[])
10631126
except ValueError as err:
10641127
self.logger.warning('list_folder %s: %s' % ([path], [err]))
10651128
return paramiko.SFTP_PERMISSION_DENIED
@@ -1107,7 +1170,7 @@ def stat(self, path):
11071170
# path = force_utf8(path)
11081171
# self.logger.debug('stat %s' % path)
11091172
try:
1110-
real_path = self._get_fs_path(path, operation='stat')
1173+
real_path = self._get_fs_path(path, operation='stat', args=[])
11111174
except ValueError as err:
11121175
self.logger.warning('stat %s: %s' % (path, err))
11131176
return paramiko.SFTP_PERMISSION_DENIED
@@ -1135,7 +1198,7 @@ def lstat(self, path):
11351198
# path = force_utf8(path)
11361199
# self.logger.debug('lstat %s' % path)
11371200
try:
1138-
real_path = self._get_fs_path(path, operation='lstat')
1201+
real_path = self._get_fs_path(path, operation='lstat', args=[])
11391202
except ValueError as err:
11401203
self.logger.warning('lstat %s: %s' % (path, err))
11411204
return paramiko.SFTP_PERMISSION_DENIED
@@ -1159,7 +1222,7 @@ def remove(self, path):
11591222
# path = force_utf8(path)
11601223
# self.logger.debug("remove %s" % path)
11611224
try:
1162-
real_path = self._get_fs_path(path, operation='remove')
1225+
real_path = self._get_fs_path(path, operation='remove', args=[])
11631226
except ValueError as err:
11641227
self.logger.warning('remove %s: %s' % (path, err))
11651228
return paramiko.SFTP_PERMISSION_DENIED
@@ -1205,7 +1268,7 @@ def rename(self, oldpath, newpath):
12051268
# newpath = force_utf8(newpath)
12061269
# self.logger.debug("rename %s %s" % (oldpath, newpath))
12071270
try:
1208-
real_oldpath = self._get_fs_path(oldpath, operation='rename')
1271+
real_oldpath = self._get_fs_path(oldpath, operation='rename', args=[])
12091272
except ValueError as err:
12101273
self.logger.warning('rename %s %s: %s' % (oldpath, newpath, err))
12111274
return paramiko.SFTP_PERMISSION_DENIED
@@ -1231,7 +1294,7 @@ def rename(self, oldpath, newpath):
12311294
self.logger.warning('move on read-only old path %s :: %s' %
12321295
(oldpath, real_oldpath))
12331296
return paramiko.SFTP_PERMISSION_DENIED
1234-
real_newpath = self._get_fs_path(newpath, operation='rename')
1297+
real_newpath = self._get_fs_path(newpath, operation='rename', args=[])
12351298
if not check_write_access(real_newpath, parent_dir=True):
12361299
self.logger.warning('move on read-only new path %s :: %s' %
12371300
(newpath, real_newpath))
@@ -1258,7 +1321,7 @@ def mkdir(self, path, mode):
12581321
# path = force_utf8(path)
12591322
# self.logger.debug("mkdir %s" % path)
12601323
try:
1261-
real_path = self._get_fs_path(path, operation='mkdir')
1324+
real_path = self._get_fs_path(path, operation='mkdir', args=[mode])
12621325
except ValueError as err:
12631326
self.logger.warning('mkdir %s: %s' % (path, err))
12641327
return paramiko.SFTP_PERMISSION_DENIED
@@ -1289,7 +1352,7 @@ def rmdir(self, path):
12891352
# path = force_utf8(path)
12901353
# self.logger.debug("rmdir %s" % path)
12911354
try:
1292-
real_path = self._get_fs_path(path, operation='rmdir')
1355+
real_path = self._get_fs_path(path, operation='rmdir', args=[])
12931356
except ValueError as err:
12941357
self.logger.warning('rmdir %s: %s' % (path, err))
12951358
return paramiko.SFTP_PERMISSION_DENIED
@@ -1342,7 +1405,7 @@ def readlink(self, path):
13421405
# path = force_utf8(path)
13431406
# self.logger.debug("readlink %s" % path)
13441407
try:
1345-
real_path = self._get_fs_path(path, operation='readlink')
1408+
real_path = self._get_fs_path(path, operation='readlink', args=[])
13461409
except ValueError as err:
13471410
self.logger.warning('readlink %s: %s' % (path, err))
13481411
return paramiko.SFTP_PERMISSION_DENIED

0 commit comments

Comments
 (0)