comparison src/memory/memory.py @ 2501:3b67fe672206

core (memory): file metadata handling methods: - checkFilePermission check that requested permission(s) are granted for a file (without checking the parents) - checkPermissionToRoot check that requested permission(s) are granted for a file and all its parents - getFiles retrieve file(s) according to filter, and check permissions by default - setFile create a file (and its parents if needed) and check permissions by default
author Goffi <goffi@goffi.org>
date Wed, 28 Feb 2018 18:28:39 +0100
parents 0046283a285d
children 4e5cc45e2be7
comparison
equal deleted inserted replaced
2500:898b6e1fdc7a 2501:3b67fe672206
37 from sat.memory.params import Params 37 from sat.memory.params import Params
38 from sat.memory.disco import Discovery 38 from sat.memory.disco import Discovery
39 from sat.memory.crypto import BlockCipher 39 from sat.memory.crypto import BlockCipher
40 from sat.memory.crypto import PasswordHasher 40 from sat.memory.crypto import PasswordHasher
41 from sat.tools import config as tools_config 41 from sat.tools import config as tools_config
42 import shortuuid
43 import mimetypes
44 import time
42 45
43 46
44 PresenceTuple = namedtuple("PresenceTuple", ('show', 'priority', 'statuses')) 47 PresenceTuple = namedtuple("PresenceTuple", ('show', 'priority', 'statuses'))
45 MSG_NO_SESSION = "Session id doesn't exist or is finished" 48 MSG_NO_SESSION = "Session id doesn't exist or is finished"
46 49
1056 return self.params.paramsRegisterApp(xml, security_limit, app) 1059 return self.params.paramsRegisterApp(xml, security_limit, app)
1057 1060
1058 def setDefault(self, name, category, callback, errback=None): 1061 def setDefault(self, name, category, callback, errback=None):
1059 return self.params.setDefault(name, category, callback, errback) 1062 return self.params.setDefault(name, category, callback, errback)
1060 1063
1064 ## Files ##
1065
1066 def checkFilePermission(self, file_data, peer_jid, perms_to_check):
1067 """check that an entity has the right permission on a file
1068
1069 @param file_data(dict): data of one file, as returned by getFiles
1070 @param peer_jid(jid.JID): entity trying to access the file
1071 @param perms_to_check(tuple[unicode]): permissions to check
1072 tuple of C.ACCESS_PERM_*
1073 @param check_parents(bool): if True, also check all parents until root node
1074 @raise exceptions.PermissionError: peer_jid doesn't have all permission
1075 in perms_to_check for file_data
1076 @raise exceptions.InternalError: perms_to_check is invalid
1077 """
1078 if peer_jid is None and perms_to_check is None:
1079 return
1080 peer_jid = peer_jid.userhostJID()
1081 if peer_jid == file_data['owner']:
1082 # the owner has all rights
1083 return
1084 if not C.ACCESS_PERMS.issuperset(perms_to_check):
1085 raise exceptions.InternalError(_(u'invalid permission'))
1086
1087 for perm in perms_to_check:
1088 # we check each perm and raise PermissionError as soon as one condition is not valid
1089 # we must never return here, we only return after the loop if nothing was blocking the access
1090 try:
1091 perm_data = file_data[u'access'][perm]
1092 perm_type = perm_data[u'type']
1093 except KeyError:
1094 raise failure.Failure(exceptions.PermissionError())
1095 if perm_type == C.ACCESS_TYPE_PUBLIC:
1096 continue
1097 elif perm_type == C.ACCESS_TYPE_WHITELIST:
1098 try:
1099 jids = perm_data[u'jids']
1100 except KeyError:
1101 raise failure.Failure(exceptions.PermissionError())
1102 if peer_jid.full() in jids:
1103 continue
1104 else:
1105 raise failure.Failure(exceptions.PermissionError())
1106 else:
1107 raise exceptions.InternalError(_(u'unknown access type: {type}').format(type=perm_type))
1108
1109 @defer.inlineCallbacks
1110 def checkPermissionToRoot(self, client, file_data, peer_jid, perms_to_check):
1111 """do checkFilePermission on file_data and all its parents until root"""
1112 current = file_data
1113 while True:
1114 self.checkFilePermission(current, peer_jid, perms_to_check)
1115 parent = current[u'parent']
1116 if not parent:
1117 break
1118 files_data = yield self.getFile(self, client, peer_jid=None, file_id=parent, perms_to_check=None)
1119 try:
1120 current = files_data[0]
1121 except IndexError:
1122 raise exceptions.DataError(u'Missing parent')
1123
1124 @defer.inlineCallbacks
1125 def _getParentDir(self, client, path, parent, namespace, owner, peer_jid, perms_to_check):
1126 """Retrieve parent node from a path, or last existing directory
1127
1128 each directory of the path will be retrieved, until the last existing one
1129 @return (tuple[unicode, list[unicode])): parent, remaining path elements:
1130 - parent is the id of the last retrieved directory (or u'' for root)
1131 - remaining path elements are the directories which have not been retrieved
1132 (i.e. which don't exist)
1133 """
1134 # if path is set, we have to retrieve parent directory of the file(s) from it
1135 if parent is not None:
1136 raise exceptions.ConflictError(_(u"You can't use path and parent at the same time"))
1137 path_elts = filter(None, path.split(u'/'))
1138 if {u'..', u'.'}.intersection(path_elts):
1139 raise ValueError(_(u'".." or "." can\'t be used in path'))
1140
1141 # we retrieve all directories from path until we get the parent container
1142 # non existing directories will be created
1143 parent = u''
1144 for idx, path_elt in enumerate(path_elts):
1145 directories = yield self.storage.getFiles(client, parent=parent, type_=C.FILE_TYPE_DIRECTORY,
1146 name=path_elt, namespace=namespace, owner=owner)
1147 if not directories:
1148 defer.returnValue((parent, path_elts[idx:]))
1149 # from this point, directories don't exist anymore, we have to create them
1150 elif len(directories) > 1:
1151 raise exceptions.InternalError(_(u"Several directories found, this should not happen"))
1152 else:
1153 directory = directories[0]
1154 self.checkFilePermission(directory, peer_jid, perms_to_check)
1155 parent = directory[u'id']
1156 defer.returnValue((parent, []))
1157
1158 @defer.inlineCallbacks
1159 def getFiles(self, client, peer_jid, file_id=None, version=None, parent=None, path=None, type_=None,
1160 file_hash=None, hash_algo=None, name=None, namespace=None, mime_type=None,
1161 owner=None, access=None, projection=None, unique=False, perms_to_check=(C.ACCESS_PERM_READ,)):
1162 """retrieve files with with given filters
1163
1164 @param peer_jid(jid.JID, None): jid trying to access the file
1165 needed to check permission.
1166 Use None to ignore permission (perms_to_check must be None too)
1167 @param file_id(unicode, None): id of the file
1168 None to ignore
1169 @param version(unicode, None): version of the file
1170 None to ignore
1171 empty string to look for current version
1172 @param parent(unicode, None): id of the directory containing the files
1173 None to ignore
1174 empty string to look for root files/directories
1175 @param projection(list[unicode], None): name of columns to retrieve
1176 None to retrieve all
1177 @param unique(bool): if True will remove duplicates
1178 @param perms_to_check(tuple[unicode],None): permission to check
1179 must be a tuple of C.ACCESS_PERM_* or None
1180 if None, permission will no be checked (peer_jid must be None too in this case)
1181 other params are the same as for [setFile]
1182 @return (list[dict]): files corresponding to filters
1183 @raise exceptions.NotFound: parent directory not found (when path is specified)
1184 @raise exceptions.PermissionError: peer_jid can't use perms_to_check for one of the file
1185 on the path
1186 """
1187 if peer_jid is None and perms_to_check or perms_to_check is None and peer_jid:
1188 raise exceptions.InternalError('if you want to disable permission check, both peer_jid and perms_to_check must be None')
1189 if path is not None:
1190 # permission are checked by _getParentDir
1191 parent, remaining_path_elts = yield self._getParentDir(client, path, parent, namespace, owner, peer_jid, perms_to_check)
1192 if remaining_path_elts:
1193 # if we have remaining path elements,
1194 # the parent directory is not found
1195 raise failure.Failure(exceptions.NotFound())
1196 if parent and peer_jid:
1197 # if parent is given directly and permission check is need,
1198 # we need to check all the parents
1199 parent_data = yield self.storage.getFiles(client, file_id=parent)
1200 try:
1201 parent_data = parent_data[0]
1202 except IndexError:
1203 raise exceptions.DataError(u'mising parent')
1204 yield self.checkPermissionToRoot(client, parent_data, peer_jid, perms_to_check)
1205
1206 files = yield self.storage.getFiles(client, file_id=file_id, version=version, parent=parent, type_=type_,
1207 file_hash=file_hash, hash_algo=hash_algo, name=name, namespace=namespace,
1208 mime_type=mime_type, owner=owner, access=access,
1209 projection=projection, unique=unique)
1210
1211 if peer_jid:
1212 # if permission are checked, we must remove all file tha use can't access
1213 to_remove = []
1214 for file_data in files:
1215 try:
1216 self.checkFilePermission(file_data, peer_jid, perms_to_check)
1217 except exceptions.PermissionError:
1218 to_remove.append(file_data)
1219 for file_data in to_remove:
1220 files.remove(file_data)
1221 defer.returnValue(files)
1222
1223 @defer.inlineCallbacks
1224 def setFile(self, client, name, file_id=None, version=u'', parent=None, path=None,
1225 type_=C.FILE_TYPE_FILE, file_hash=None, hash_algo=None, size=None, namespace=None,
1226 mime_type=None, created=None, modified=None, owner=None, access=None, extra=None,
1227 peer_jid = None, perms_to_check=(C.ACCESS_PERM_WRITE,)):
1228 """set a file metadata
1229
1230 @param name(unicode): basename of the file
1231 @param file_id(unicode): unique id of the file
1232 @param version(unicode): version of this file
1233 empty string for current version or when there is no versioning
1234 @param parent(unicode, None): id of the directory containing the files
1235 @param path(unicode, None): virtual path of the file in the namespace
1236 if set, parent must be None. All intermediate directories will be created if needed,
1237 using current access.
1238 @param file_hash(unicode): unique hash of the payload
1239 @param hash_algo(unicode): algorithm used for hashing the file (usually sha-256)
1240 @param size(int): size in bytes
1241 @param namespace(unicode, None): identifier (human readable is better) to group files
1242 for instance, namespace could be used to group files in a specific photo album
1243 @param mime_type(unicode): MIME type of the file, or None if not known/guessed
1244 @param created(int): UNIX time of creation
1245 @param modified(int,None): UNIX time of last modification, or None to use created date
1246 @param owner(jid.JID, None): jid of the owner of the file (mainly useful for component)
1247 will be used to check permission.
1248 Use None to ignore permission (perms_to_check must be None too)
1249 @param access(dict, None): serialisable dictionary with access rules.
1250 None (or empty dict) to use private access, i.e. allow only profile's jid to access the file
1251 key can be on on C.ACCESS_PERM_*,
1252 then a sub dictionary with a type key is used (one of C.ACCESS_TYPE_*).
1253 According to type, extra keys can be used:
1254 - C.ACCESS_TYPE_PUBLIC: the permission is granted for everybody
1255 - C.ACCESS_TYPE_WHITELIST: the permission is granted for jids (as unicode) in the 'jids' key
1256 will be encoded to json in database
1257 @param extra(dict, None): serialisable dictionary of any extra data
1258 will be encoded to json in database
1259 @param perms_to_check(tuple[unicode],None): permission to check
1260 must be a tuple of C.ACCESS_PERM_* or None
1261 if None, permission will no be checked (peer_jid must be None too in this case)
1262 @param profile(unicode): profile owning the file
1263 """
1264 if '/' in name:
1265 raise ValueError('name must not contain a slash ("/")')
1266 if file_id is None:
1267 file_id = shortuuid.uuid()
1268 if file_hash is not None and hash_algo is None or hash_algo is not None and file_hash is None:
1269 raise ValueError('file_hash and hash_algo must be set at the same time')
1270 if mime_type is None:
1271 mime_type, file_encoding = mimetypes.guess_type(name)
1272 if created is None:
1273 created = time.time()
1274 if namespace is not None:
1275 namespace = namespace.strip() or None
1276 if type_ == C.FILE_TYPE_DIRECTORY:
1277 if any(version, file_hash, size, mime_type):
1278 raise ValueError(u"version, file_hash, size and mime_type can't be set for a directory")
1279
1280 if path is not None:
1281 if peer_jid is None:
1282 peer_jid = owner
1283 # _getParentDir will check permissions if peer_jid is set
1284 parent, remaining_path_elts = self._getParentDir(client, path, parent, namespace, owner, owner, perms_to_check)
1285 # if remaining directories don't exist, we have to create them
1286 for new_dir in remaining_path_elts:
1287 new_dir_id = shortuuid.uuid()
1288 yield self.storage.setFile(client, name=new_dir, file_id=new_dir_id, version=u'', parent=parent,
1289 type_=C.FILE_TYPE_DIRECTORY, namespace=namespace,
1290 created=time.time(),
1291 owner=owner.userhostJID() if owner else None,
1292 access=access, extra={})
1293 parent = new_dir_id
1294 elif parent is None:
1295 parent = u''
1296
1297 yield self.storage.setFile(client, file_id=file_id, version=version, parent=parent, type_=type_,
1298 file_hash=file_hash, hash_algo=hash_algo, name=name, size=size,
1299 namespace=namespace, mime_type=mime_type, created=created, modified=modified,
1300 owner=owner.userhostJID() if owner else None,
1301 access=access, extra=extra)
1302
1061 ## Misc ## 1303 ## Misc ##
1062 1304
1063 def isEntityAvailable(self, entity_jid, profile_key): 1305 def isEntityAvailable(self, entity_jid, profile_key):
1064 """Tell from the presence information if the given entity is available. 1306 """Tell from the presence information if the given entity is available.
1065 1307