async def _vlob_create(self, encryption_revision: int, entry_id: EntryID, ciphered: bytes, now: Pendulum): """ Raises: FSError FSRemoteSyncError FSBackendOfflineError FSWorkspaceInMaintenance FSBadEncryptionRevision FSWorkspaceNoAccess """ # Vlob upload rep = await self._backend_cmds("vlob_create", self.workspace_id, encryption_revision, entry_id, now, ciphered) if rep["status"] == "already_exists": raise FSRemoteSyncError(entry_id) elif rep["status"] == "not_allowed": # Seems we lost the access to the realm raise FSWorkspaceNoWriteAccess( "Cannot upload manifest: no write access") elif rep["status"] == "bad_encryption_revision": raise FSBadEncryptionRevision( f"Cannot create vlob {entry_id}: Bad encryption revision provided" ) elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( f"Cannot create vlob while the workspace is in maintenance") elif rep["status"] != "ok": raise FSError(f"Cannot create vlob {entry_id}: `{rep['status']}`")
async def _vlob_update( self, encryption_revision: int, entry_id: EntryID, ciphered: bytes, now: DateTime, version: int, ) -> None: """ Raises: FSError FSRemoteSyncError FSBackendOfflineError FSRemoteOperationError FSWorkspaceInMaintenance FSBadEncryptionRevision FSWorkspaceNoAccess """ # Vlob upload with translate_backend_cmds_errors(): rep = await self.backend_cmds.vlob_update(encryption_revision, VlobID(entry_id.uuid), version, now, ciphered) if rep["status"] == "not_found": raise FSRemoteSyncError(entry_id) elif rep["status"] == "not_allowed": # Seems we lost the access to the realm raise FSWorkspaceNoWriteAccess( "Cannot upload manifest: no write access") elif rep["status"] == "require_greater_timestamp": raise VlobRequireGreaterTimestampError( rep["strictly_greater_than"]) elif rep["status"] == "bad_version": raise FSRemoteSyncError(entry_id) elif rep["status"] == "bad_encryption_revision": raise FSBadEncryptionRevision( f"Cannot update vlob {entry_id}: Bad encryption revision provided" ) elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( "Cannot create vlob while the workspace is in maintenance") elif rep["status"] != "ok": raise FSError(f"Cannot update vlob {entry_id}: `{rep['status']}`")
async def _vlob_update( self, encryption_revision: int, entry_id: EntryID, ciphered: bytes, now: DateTime, version: int, ) -> None: """ Raises: FSError FSRemoteSyncError FSBackendOfflineError FSWorkspaceInMaintenance FSBadEncryptionRevision FSWorkspaceNoAccess """ # Vlob upload with translate_backend_cmds_errors(): rep = await self.backend_cmds.vlob_update(encryption_revision, entry_id, version, now, ciphered) if rep["status"] == "not_found": raise FSRemoteSyncError(entry_id) elif rep["status"] == "not_allowed": # Seems we lost the access to the realm raise FSWorkspaceNoWriteAccess( "Cannot upload manifest: no write access") elif rep["status"] == "bad_version": raise FSRemoteSyncError(entry_id) elif rep["status"] == "bad_timestamp": # Quick and dirty fix before a better version with a retry loop : go offline so we # don't have to deal with another client updating manifest with a later timestamp raise FSBackendOfflineError(rep) elif rep["status"] == "bad_encryption_revision": raise FSBadEncryptionRevision( f"Cannot update vlob {entry_id}: Bad encryption revision provided" ) elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( "Cannot create vlob while the workspace is in maintenance") elif rep["status"] != "ok": raise FSError(f"Cannot update vlob {entry_id}: `{rep['status']}`")
async def load_manifest( self, entry_id: EntryID, version: int = None, timestamp: Pendulum = None, expected_backend_timestamp: Pendulum = None, ) -> RemoteManifest: """ Download a manifest. Only one from version or timestamp parameters can be specified at the same time. expected_backend_timestamp enables to check a timestamp against the one returned by the backend. Raises: FSError FSBackendOfflineError FSWorkspaceInMaintenance FSRemoteManifestNotFound FSBadEncryptionRevision FSWorkspaceNoAccess """ if timestamp is not None and version is not None: raise FSError( f"Supplied both version {version} and timestamp `{timestamp}` for manifest " f"`{entry_id}`") # Download the vlob workspace_entry = self.get_workspace_entry() rep = await self._backend_cmds( "vlob_read", workspace_entry.encryption_revision, entry_id, version=version, timestamp=timestamp if version is None else None, ) if rep["status"] == "not_found": raise FSRemoteManifestNotFound(entry_id) elif rep["status"] == "not_allowed": # Seems we lost the access to the realm raise FSWorkspaceNoReadAccess( "Cannot load manifest: no read access") elif rep["status"] == "bad_version": raise FSRemoteManifestNotFoundBadVersion(entry_id) elif rep["status"] == "bad_timestamp": raise FSRemoteManifestNotFoundBadTimestamp(entry_id) elif rep["status"] == "bad_encryption_revision": raise FSBadEncryptionRevision( f"Cannot fetch vlob {entry_id}: Bad encryption revision provided" ) elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( f"Cannot download vlob while the workspace is in maintenance") elif rep["status"] != "ok": raise FSError(f"Cannot fetch vlob {entry_id}: `{rep['status']}`") expected_version = rep["version"] expected_author = rep["author"] expected_timestamp = rep["timestamp"] if version not in (None, expected_version): raise FSError( f"Backend returned invalid version for vlob {entry_id} (expecting {version}, " f"got {expected_version})") if expected_backend_timestamp and expected_backend_timestamp != expected_timestamp: raise FSError( f"Backend returned invalid expected timestamp for vlob {entry_id} at version " f"{version} (expecting {expected_backend_timestamp}, got {expected_timestamp})" ) author = await self.remote_device_manager.get_device(expected_author) try: remote_manifest = RemoteManifest.decrypt_verify_and_load( rep["blob"], key=workspace_entry.key, author_verify_key=author.verify_key, expected_author=expected_author, expected_timestamp=expected_timestamp, expected_version=expected_version, expected_id=entry_id, ) except DataError as exc: raise FSError(f"Cannot decrypt vlob: {exc}") from exc # Finally make sure author was allowed to create this manifest role_at_timestamp = await self._get_user_realm_role_at( expected_author.user_id, expected_timestamp) if role_at_timestamp is None: raise FSError( f"Manifest was created at {expected_timestamp} by `{expected_author}` " "which had no right to access the workspace at that time") elif role_at_timestamp == RealmRole.READER: raise FSError( f"Manifest was created at {expected_timestamp} by `{expected_author}` " "which had write right on the workspace at that time") return remote_manifest
async def load_manifest( self, entry_id: EntryID, version: Optional[int] = None, timestamp: Optional[DateTime] = None, expected_backend_timestamp: Optional[DateTime] = None, workspace_entry: Optional[WorkspaceEntry] = None, ) -> BaseRemoteManifest: """ Download a manifest. Only one from version or timestamp parameters can be specified at the same time. expected_backend_timestamp enables to check a timestamp against the one returned by the backend. Raises: FSError FSBackendOfflineError FSRemoteOperationError FSWorkspaceInMaintenance FSRemoteManifestNotFound FSBadEncryptionRevision FSWorkspaceNoAccess FSUserNotFoundError FSDeviceNotFoundError FSInvalidTrustchainError """ assert (timestamp is None or version is None ), "Either timestamp or version argument should be provided" # Get the current and requested workspace entry # They're usually the same, except when loading from a workspace while it's in maintenance current_workspace_entry = self.get_workspace_entry() workspace_entry = current_workspace_entry if workspace_entry is None else workspace_entry # Download the vlob with translate_backend_cmds_errors(): rep = await self.backend_cmds.vlob_read( workspace_entry.encryption_revision, VlobID(entry_id.uuid), version=version, timestamp=timestamp if version is None else None, ) # Special case for loading manifest while in maintenance. # This is done to allow users to fetch data from a workspace while it's being reencrypted. # If the workspace is in maintenance for another reason (such as garbage collection), # the recursive call to load manifest will simply also fail with an FSWorkspaceInMaintenance. if (rep["status"] == "in_maintenance" and workspace_entry.encryption_revision == current_workspace_entry.encryption_revision): # Getting the last workspace entry with the previous encryption revision # requires one or several calls to the backend, meaning the following exceptions might get raised: # - FSError # - FSBackendOfflineError # - FSWorkspaceInMaintenance # It is fine to let those exceptions bubble up as there all valid reasons for failing to load a manifest. previous_workspace_entry = await self.get_previous_workspace_entry( ) if previous_workspace_entry is not None: # Make sure we don't fall into an infinite loop because of some other bug assert (previous_workspace_entry.encryption_revision < self.get_workspace_entry().encryption_revision) # Recursive call to `load_manifest`, requiring an older encryption revision than the current one return await self.load_manifest( entry_id, version=version, timestamp=timestamp, expected_backend_timestamp=expected_backend_timestamp, workspace_entry=previous_workspace_entry, ) if rep["status"] == "not_found": raise FSRemoteManifestNotFound(entry_id) elif rep["status"] == "not_allowed": # Seems we lost the access to the realm raise FSWorkspaceNoReadAccess( "Cannot load manifest: no read access") elif rep["status"] == "bad_version": raise FSRemoteManifestNotFoundBadVersion(entry_id) elif rep["status"] == "bad_encryption_revision": raise FSBadEncryptionRevision( f"Cannot fetch vlob {entry_id}: Bad encryption revision provided" ) elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( "Cannot download vlob while the workspace is in maintenance") elif rep["status"] != "ok": raise FSError(f"Cannot fetch vlob {entry_id}: `{rep['status']}`") expected_version = rep["version"] expected_author = rep["author"] expected_timestamp = rep["timestamp"] if version not in (None, expected_version): raise FSError( f"Backend returned invalid version for vlob {entry_id} (expecting {version}, " f"got {expected_version})") if expected_backend_timestamp and expected_backend_timestamp != expected_timestamp: raise FSError( f"Backend returned invalid expected timestamp for vlob {entry_id} at version " f"{version} (expecting {expected_backend_timestamp}, got {expected_timestamp})" ) with translate_remote_devices_manager_errors(): author = await self.remote_devices_manager.get_device( expected_author) try: remote_manifest = BaseRemoteManifest.decrypt_verify_and_load( rep["blob"], key=workspace_entry.key, author_verify_key=author.verify_key, expected_author=expected_author, expected_timestamp=expected_timestamp, expected_version=expected_version, expected_id=entry_id, ) except DataError as exc: raise FSError(f"Cannot decrypt vlob: {exc}") from exc # Get the timestamp of the last role for this particular user author_last_role_granted_on = rep["author_last_role_granted_on"] # Compatibility with older backends (best effort strategy) if author_last_role_granted_on is None: author_last_role_granted_on = self.device.timestamp() # Finally make sure author was allowed to create this manifest role_at_timestamp = await self._get_user_realm_role_at( expected_author.user_id, expected_timestamp, author_last_role_granted_on) if role_at_timestamp is None: raise FSError( f"Manifest was created at {expected_timestamp} by `{expected_author}` " "which had no right to access the workspace at that time") elif role_at_timestamp == RealmRole.READER: raise FSError( f"Manifest was created at {expected_timestamp} by `{expected_author}` " "which had no right to write on the workspace at that time") return remote_manifest