async def load_block(self, access: BlockAccess) -> None: """ Raises: FSError FSRemoteBlockNotFound FSBackendOfflineError FSWorkspaceInMaintenance FSWorkspaceNoAccess """ # Download rep = await self._backend_cmds("block_read", access.id) if rep["status"] == "not_found": raise FSRemoteBlockNotFound(access) elif rep["status"] == "not_allowed": # Seems we lost the access to the realm raise FSWorkspaceNoReadAccess("Cannot load block: no read access") elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( f"Cannot download block while the workspace in maintenance") elif rep["status"] != "ok": raise FSError(f"Cannot download block: `{rep['status']}`") # Decryption try: block = access.key.decrypt(rep["block"]) # Decryption error except CryptoError as exc: raise FSError(f"Cannot decrypt block: {exc}") from exc # TODO: let encryption manager do the digest check ? assert HashDigest.from_data(block) == access.digest, access await self.local_storage.set_clean_block(access.id, block)
async def list_versions( self, entry_id: EntryID) -> Dict[int, Tuple[DateTime, DeviceID]]: """ Raises: FSError FSRemoteOperationError FSBackendOfflineError FSWorkspaceInMaintenance FSRemoteManifestNotFound """ with translate_backend_cmds_errors(): rep = await self.backend_cmds.vlob_list_versions( VlobID(entry_id.uuid)) if rep["status"] == "not_allowed": # Seems we lost the access to the realm raise FSWorkspaceNoReadAccess( "Cannot load manifest: no read access") elif rep["status"] == "not_found": raise FSRemoteManifestNotFound(entry_id) 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']}`") return rep["versions"]
async def _load_realm_role_certificates(self, realm_id: Optional[EntryID] = None ): rep = await self._backend_cmds("realm_get_role_certificates", realm_id or self.workspace_id) if rep["status"] == "not_allowed": # Seems we lost the access to the realm raise FSWorkspaceNoReadAccess( "Cannot get workspace roles: no read access") elif rep["status"] != "ok": raise FSError( f"Cannot retrieve workspace roles: `{rep['status']}`") try: # Must read unverified certificates to access metadata unsecure_certifs = sorted( [(RealmRoleCertificateContent.unsecure_load(uv_role), uv_role) for uv_role in rep["certificates"]], key=lambda x: x[0].timestamp, ) current_roles = {} owner_only = (RealmRole.OWNER, ) owner_or_manager = (RealmRole.OWNER, RealmRole.MANAGER) # Now verify each certif for unsecure_certif, raw_certif in unsecure_certifs: author = await self.remote_device_manager.get_device( unsecure_certif.author) RealmRoleCertificateContent.verify_and_load( raw_certif, author_verify_key=author.verify_key, expected_author=author.device_id, ) # Make sure author had the right to do this existing_user_role = current_roles.get(unsecure_certif.user_id) if not current_roles and unsecure_certif.user_id == author.device_id.user_id: # First user is autosigned needed_roles = (None, ) elif (existing_user_role in owner_or_manager or unsecure_certif.role in owner_or_manager): needed_roles = owner_only else: needed_roles = owner_or_manager if current_roles.get( unsecure_certif.author.user_id) not in needed_roles: raise FSError( f"Invalid realm role certificates: " f"{unsecure_certif.author} has not right to give " f"{unsecure_certif.role} role to {unsecure_certif.user_id} " f"on {unsecure_certif.timestamp}") if unsecure_certif.role is None: current_roles.pop(unsecure_certif.user_id, None) else: current_roles[ unsecure_certif.user_id] = unsecure_certif.role # Decryption error except DataError as exc: raise FSError(f"Invalid realm role certificates: {exc}") from exc # Now unsecure_certifs is no longer unsecure we have valided it items return [c for c, _ in unsecure_certifs], current_roles
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