# HG changeset patch # User Goffi # Date 1519838919 -3600 # Node ID 3b67fe672206495ef79c532951764e57d6efa1ef # Parent 898b6e1fdc7a2e08a90f3ccccf8b8149f67c493d 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 diff -r 898b6e1fdc7a -r 3b67fe672206 src/memory/memory.py --- a/src/memory/memory.py Wed Feb 28 18:28:39 2018 +0100 +++ b/src/memory/memory.py Wed Feb 28 18:28:39 2018 +0100 @@ -39,6 +39,9 @@ from sat.memory.crypto import BlockCipher from sat.memory.crypto import PasswordHasher from sat.tools import config as tools_config +import shortuuid +import mimetypes +import time PresenceTuple = namedtuple("PresenceTuple", ('show', 'priority', 'statuses')) @@ -1058,6 +1061,245 @@ def setDefault(self, name, category, callback, errback=None): return self.params.setDefault(name, category, callback, errback) + ## Files ## + + def checkFilePermission(self, file_data, peer_jid, perms_to_check): + """check that an entity has the right permission on a file + + @param file_data(dict): data of one file, as returned by getFiles + @param peer_jid(jid.JID): entity trying to access the file + @param perms_to_check(tuple[unicode]): permissions to check + tuple of C.ACCESS_PERM_* + @param check_parents(bool): if True, also check all parents until root node + @raise exceptions.PermissionError: peer_jid doesn't have all permission + in perms_to_check for file_data + @raise exceptions.InternalError: perms_to_check is invalid + """ + if peer_jid is None and perms_to_check is None: + return + peer_jid = peer_jid.userhostJID() + if peer_jid == file_data['owner']: + # the owner has all rights + return + if not C.ACCESS_PERMS.issuperset(perms_to_check): + raise exceptions.InternalError(_(u'invalid permission')) + + for perm in perms_to_check: + # we check each perm and raise PermissionError as soon as one condition is not valid + # we must never return here, we only return after the loop if nothing was blocking the access + try: + perm_data = file_data[u'access'][perm] + perm_type = perm_data[u'type'] + except KeyError: + raise failure.Failure(exceptions.PermissionError()) + if perm_type == C.ACCESS_TYPE_PUBLIC: + continue + elif perm_type == C.ACCESS_TYPE_WHITELIST: + try: + jids = perm_data[u'jids'] + except KeyError: + raise failure.Failure(exceptions.PermissionError()) + if peer_jid.full() in jids: + continue + else: + raise failure.Failure(exceptions.PermissionError()) + else: + raise exceptions.InternalError(_(u'unknown access type: {type}').format(type=perm_type)) + + @defer.inlineCallbacks + def checkPermissionToRoot(self, client, file_data, peer_jid, perms_to_check): + """do checkFilePermission on file_data and all its parents until root""" + current = file_data + while True: + self.checkFilePermission(current, peer_jid, perms_to_check) + parent = current[u'parent'] + if not parent: + break + files_data = yield self.getFile(self, client, peer_jid=None, file_id=parent, perms_to_check=None) + try: + current = files_data[0] + except IndexError: + raise exceptions.DataError(u'Missing parent') + + @defer.inlineCallbacks + def _getParentDir(self, client, path, parent, namespace, owner, peer_jid, perms_to_check): + """Retrieve parent node from a path, or last existing directory + + each directory of the path will be retrieved, until the last existing one + @return (tuple[unicode, list[unicode])): parent, remaining path elements: + - parent is the id of the last retrieved directory (or u'' for root) + - remaining path elements are the directories which have not been retrieved + (i.e. which don't exist) + """ + # if path is set, we have to retrieve parent directory of the file(s) from it + if parent is not None: + raise exceptions.ConflictError(_(u"You can't use path and parent at the same time")) + path_elts = filter(None, path.split(u'/')) + if {u'..', u'.'}.intersection(path_elts): + raise ValueError(_(u'".." or "." can\'t be used in path')) + + # we retrieve all directories from path until we get the parent container + # non existing directories will be created + parent = u'' + for idx, path_elt in enumerate(path_elts): + directories = yield self.storage.getFiles(client, parent=parent, type_=C.FILE_TYPE_DIRECTORY, + name=path_elt, namespace=namespace, owner=owner) + if not directories: + defer.returnValue((parent, path_elts[idx:])) + # from this point, directories don't exist anymore, we have to create them + elif len(directories) > 1: + raise exceptions.InternalError(_(u"Several directories found, this should not happen")) + else: + directory = directories[0] + self.checkFilePermission(directory, peer_jid, perms_to_check) + parent = directory[u'id'] + defer.returnValue((parent, [])) + + @defer.inlineCallbacks + def getFiles(self, client, peer_jid, file_id=None, version=None, parent=None, path=None, type_=None, + file_hash=None, hash_algo=None, name=None, namespace=None, mime_type=None, + owner=None, access=None, projection=None, unique=False, perms_to_check=(C.ACCESS_PERM_READ,)): + """retrieve files with with given filters + + @param peer_jid(jid.JID, None): jid trying to access the file + needed to check permission. + Use None to ignore permission (perms_to_check must be None too) + @param file_id(unicode, None): id of the file + None to ignore + @param version(unicode, None): version of the file + None to ignore + empty string to look for current version + @param parent(unicode, None): id of the directory containing the files + None to ignore + empty string to look for root files/directories + @param projection(list[unicode], None): name of columns to retrieve + None to retrieve all + @param unique(bool): if True will remove duplicates + @param perms_to_check(tuple[unicode],None): permission to check + must be a tuple of C.ACCESS_PERM_* or None + if None, permission will no be checked (peer_jid must be None too in this case) + other params are the same as for [setFile] + @return (list[dict]): files corresponding to filters + @raise exceptions.NotFound: parent directory not found (when path is specified) + @raise exceptions.PermissionError: peer_jid can't use perms_to_check for one of the file + on the path + """ + if peer_jid is None and perms_to_check or perms_to_check is None and peer_jid: + raise exceptions.InternalError('if you want to disable permission check, both peer_jid and perms_to_check must be None') + if path is not None: + # permission are checked by _getParentDir + parent, remaining_path_elts = yield self._getParentDir(client, path, parent, namespace, owner, peer_jid, perms_to_check) + if remaining_path_elts: + # if we have remaining path elements, + # the parent directory is not found + raise failure.Failure(exceptions.NotFound()) + if parent and peer_jid: + # if parent is given directly and permission check is need, + # we need to check all the parents + parent_data = yield self.storage.getFiles(client, file_id=parent) + try: + parent_data = parent_data[0] + except IndexError: + raise exceptions.DataError(u'mising parent') + yield self.checkPermissionToRoot(client, parent_data, peer_jid, perms_to_check) + + files = yield self.storage.getFiles(client, file_id=file_id, version=version, parent=parent, type_=type_, + file_hash=file_hash, hash_algo=hash_algo, name=name, namespace=namespace, + mime_type=mime_type, owner=owner, access=access, + projection=projection, unique=unique) + + if peer_jid: + # if permission are checked, we must remove all file tha use can't access + to_remove = [] + for file_data in files: + try: + self.checkFilePermission(file_data, peer_jid, perms_to_check) + except exceptions.PermissionError: + to_remove.append(file_data) + for file_data in to_remove: + files.remove(file_data) + defer.returnValue(files) + + @defer.inlineCallbacks + def setFile(self, client, name, file_id=None, version=u'', parent=None, path=None, + type_=C.FILE_TYPE_FILE, file_hash=None, hash_algo=None, size=None, namespace=None, + mime_type=None, created=None, modified=None, owner=None, access=None, extra=None, + peer_jid = None, perms_to_check=(C.ACCESS_PERM_WRITE,)): + """set a file metadata + + @param name(unicode): basename of the file + @param file_id(unicode): unique id of the file + @param version(unicode): version of this file + empty string for current version or when there is no versioning + @param parent(unicode, None): id of the directory containing the files + @param path(unicode, None): virtual path of the file in the namespace + if set, parent must be None. All intermediate directories will be created if needed, + using current access. + @param file_hash(unicode): unique hash of the payload + @param hash_algo(unicode): algorithm used for hashing the file (usually sha-256) + @param size(int): size in bytes + @param namespace(unicode, None): identifier (human readable is better) to group files + for instance, namespace could be used to group files in a specific photo album + @param mime_type(unicode): MIME type of the file, or None if not known/guessed + @param created(int): UNIX time of creation + @param modified(int,None): UNIX time of last modification, or None to use created date + @param owner(jid.JID, None): jid of the owner of the file (mainly useful for component) + will be used to check permission. + Use None to ignore permission (perms_to_check must be None too) + @param access(dict, None): serialisable dictionary with access rules. + None (or empty dict) to use private access, i.e. allow only profile's jid to access the file + key can be on on C.ACCESS_PERM_*, + then a sub dictionary with a type key is used (one of C.ACCESS_TYPE_*). + According to type, extra keys can be used: + - C.ACCESS_TYPE_PUBLIC: the permission is granted for everybody + - C.ACCESS_TYPE_WHITELIST: the permission is granted for jids (as unicode) in the 'jids' key + will be encoded to json in database + @param extra(dict, None): serialisable dictionary of any extra data + will be encoded to json in database + @param perms_to_check(tuple[unicode],None): permission to check + must be a tuple of C.ACCESS_PERM_* or None + if None, permission will no be checked (peer_jid must be None too in this case) + @param profile(unicode): profile owning the file + """ + if '/' in name: + raise ValueError('name must not contain a slash ("/")') + if file_id is None: + file_id = shortuuid.uuid() + if file_hash is not None and hash_algo is None or hash_algo is not None and file_hash is None: + raise ValueError('file_hash and hash_algo must be set at the same time') + if mime_type is None: + mime_type, file_encoding = mimetypes.guess_type(name) + if created is None: + created = time.time() + if namespace is not None: + namespace = namespace.strip() or None + if type_ == C.FILE_TYPE_DIRECTORY: + if any(version, file_hash, size, mime_type): + raise ValueError(u"version, file_hash, size and mime_type can't be set for a directory") + + if path is not None: + if peer_jid is None: + peer_jid = owner + # _getParentDir will check permissions if peer_jid is set + parent, remaining_path_elts = self._getParentDir(client, path, parent, namespace, owner, owner, perms_to_check) + # if remaining directories don't exist, we have to create them + for new_dir in remaining_path_elts: + new_dir_id = shortuuid.uuid() + yield self.storage.setFile(client, name=new_dir, file_id=new_dir_id, version=u'', parent=parent, + type_=C.FILE_TYPE_DIRECTORY, namespace=namespace, + created=time.time(), + owner=owner.userhostJID() if owner else None, + access=access, extra={}) + parent = new_dir_id + elif parent is None: + parent = u'' + + yield self.storage.setFile(client, file_id=file_id, version=version, parent=parent, type_=type_, + file_hash=file_hash, hash_algo=hash_algo, name=name, size=size, + namespace=namespace, mime_type=mime_type, created=created, modified=modified, + owner=owner.userhostJID() if owner else None, + access=access, extra=extra) + ## Misc ## def isEntityAvailable(self, entity_jid, profile_key):