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 workspace_continue_reencryption(self, workspace_id: EntryID) -> ReencryptionJob: """ Raises: FSError FSBackendOfflineError FSWorkspaceNoAccess FSWorkspaceNotFoundError """ user_manifest = self.get_user_manifest() workspace_entry = user_manifest.get_workspace_entry(workspace_id) if not workspace_entry: raise FSWorkspaceNotFoundError(f"Unknown workspace `{workspace_id}`") # First make sure the workspace is under maintenance try: rep = await self.backend_cmds.realm_status(workspace_entry.id) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError( f"Cannot continue maintenance on workspace {workspace_id}: {exc}" ) from exc if rep["status"] == "not_allowed": raise FSWorkspaceNoAccess(f"Not allowed to access workspace {workspace_id}: {rep}") elif rep["status"] != "ok": raise FSError(f"Error while getting status for workspace {workspace_id}: {rep}") if not rep["in_maintenance"] or rep["maintenance_type"] != MaintenanceType.REENCRYPTION: raise FSWorkspaceNotInMaintenance("Not in reencryption maintenance") current_encryption_revision = rep["encryption_revision"] if rep["encryption_revision"] != workspace_entry.encryption_revision: raise FSError("Bad encryption revision") # Must retrieve the previous encryption revision's key version_to_fetch = None while True: previous_user_manifest = await self._fetch_remote_user_manifest( version=version_to_fetch ) previous_workspace_entry = previous_user_manifest.get_workspace_entry( workspace_entry.id ) if not previous_workspace_entry: raise FSError( f"Never had access to encryption revision {current_encryption_revision - 1}" ) if previous_workspace_entry.encryption_revision == current_encryption_revision - 1: break else: version_to_fetch = previous_user_manifest.version - 1 return ReencryptionJob(self.backend_cmds, workspace_entry, previous_workspace_entry)
async def process_last_messages(self) -> Sequence[Tuple[int, Exception]]: """ Raises: FSError FSBackendOfflineError FSSharingNotAllowedError """ errors = [] # Concurrent message processing is totally pointless async with self._process_messages_lock: user_manifest = self.get_user_manifest() initial_last_processed_message = user_manifest.last_processed_message try: rep = await self.backend_cmds.message_get( offset=initial_last_processed_message) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError(f"Cannot retrieve user messages: {exc}") from exc if rep["status"] != "ok": raise FSError(f"Cannot retrieve user messages: {rep}") new_last_processed_message = initial_last_processed_message for msg in rep["messages"]: try: await self._process_message(msg["sender"], msg["timestamp"], msg["body"]) new_last_processed_message = msg["count"] except FSBackendOfflineError: raise except FSError as exc: logger.warning("Invalid message", reason=exc, sender=msg["sender"], count=msg["count"]) errors.append((msg["count"], exc)) # Update message offset in user manifest async with self._update_user_manifest_lock: user_manifest = self.get_user_manifest() if user_manifest.last_processed_message < new_last_processed_message: user_manifest = user_manifest.evolve_and_mark_updated( last_processed_message=new_last_processed_message, timestamp=self.device.timestamp(), ) await self.set_user_manifest(user_manifest) self.event_bus.send(CoreEvent.FS_ENTRY_UPDATED, id=self.user_manifest_id) return errors
async def _fetch_remote_user_manifest(self, version: int = None) -> UserManifest: """ Raises: FSError FSWorkspaceInMaintenance FSBackendOfflineError """ try: # Note encryption_revision is always 1 given we never reencrypt # the user manifest's realm rep = await self.backend_cmds.vlob_read(1, self.user_manifest_id, version) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc if rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( "Cannot access workspace data while it is in maintenance") elif rep["status"] != "ok": raise FSError(f"Cannot fetch user manifest from backend: {rep}") expected_author = rep["author"] expected_timestamp = rep["timestamp"] expected_version = rep["version"] blob = rep["blob"] try: author = await self.remote_devices_manager.get_device( expected_author) except RemoteDevicesManagerBackendOfflineError as exc: raise FSBackendOfflineError(str(exc)) from exc except RemoteDevicesManagerError as exc: raise FSError(f"Cannot retrieve author public key: {exc}") from exc try: manifest = UserManifest.decrypt_verify_and_load( blob, key=self.device.user_manifest_key, author_verify_key=author.verify_key, expected_id=self.device.user_manifest_id, expected_author=expected_author, expected_timestamp=expected_timestamp, expected_version=version if version is not None else expected_version, ) except DataError as exc: raise FSError(f"Invalid user manifest: {exc}") from exc return manifest
def translate_backend_cmds_errors() -> Iterator[None]: try: yield except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError(str(exc)) from exc
async def _retreive_participants(self, workspace_id): """ Raises: FSError FSBackendOfflineError FSWorkspaceNoAccess """ # First retrieve workspace participants list roles = await self.remote_loader.load_realm_current_roles(workspace_id) # Then retrieve each participant user data try: users = [] for user_id in roles.keys(): user, revoked_user = await self.remote_devices_manager.get_user(user_id) if not revoked_user: users.append(user) except RemoteDevicesManagerBackendOfflineError as exc: raise FSBackendOfflineError(str(exc)) from exc except RemoteDevicesManagerError as exc: raise FSError(f"Cannot retrieve workspace {workspace_id} participants: {exc}") from exc return users
def _generate_reencryption_messages(self, new_workspace_entry, users, now: Pendulum): """ Raises: FSError """ msg = SharingReencryptedMessageContent( author=self.device.device_id, timestamp=now, name=new_workspace_entry.name, id=new_workspace_entry.id, encryption_revision=new_workspace_entry.encryption_revision, encrypted_on=new_workspace_entry.encrypted_on, key=new_workspace_entry.key, ) per_user_ciphered_msgs = {} for user in users: try: ciphered = msg.dump_sign_and_encrypt_for( author_signkey=self.device.signing_key, recipient_pubkey=user.public_key ) per_user_ciphered_msgs[user.user_id] = ciphered except DataError as exc: raise FSError( f"Cannot create reencryption message for `{user.user_id}`: {exc}" ) from exc return per_user_ciphered_msgs
async def upload_manifest(self, entry_id: EntryID, manifest: RemoteManifest): """ Raises: FSError FSRemoteSyncError FSBackendOfflineError FSWorkspaceInMaintenance FSBadEncryptionRevision """ assert manifest.author == self.device.device_id assert timestamps_in_the_ballpark(manifest.timestamp, pendulum_now()) workspace_entry = self.get_workspace_entry() try: ciphered = manifest.dump_sign_and_encrypt( key=workspace_entry.key, author_signkey=self.device.signing_key) except DataError as exc: raise FSError(f"Cannot encrypt vlob: {exc}") from exc # Upload the vlob if manifest.version == 1: await self._vlob_create(workspace_entry.encryption_revision, entry_id, ciphered, manifest.timestamp) else: await self._vlob_update( workspace_entry.encryption_revision, entry_id, ciphered, manifest.timestamp, manifest.version, )
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 _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 _process_message( self, sender_id: DeviceID, expected_timestamp: DateTime, ciphered: bytes ) -> None: """ Raises: FSError FSBackendOfflineError FSSharingNotAllowedError """ # Retrieve the sender sender = await self.remote_loader.get_device(sender_id) # Decrypt&verify message try: msg = BaseMessageContent.decrypt_verify_and_load_for( ciphered, recipient_privkey=self.device.private_key, author_verify_key=sender.verify_key, expected_author=sender_id, expected_timestamp=expected_timestamp, ) except DataError as exc: raise FSError(f"Cannot decrypt&validate message from `{sender_id}`: {exc}") from exc if isinstance(msg, (SharingGrantedMessageContent, SharingReencryptedMessageContent)): await self._process_message_sharing_granted(msg) elif isinstance(msg, SharingRevokedMessageContent): await self._process_message_sharing_revoked(msg) elif isinstance(msg, PingMessageContent): self.event_bus.send(CoreEvent.MESSAGE_PINGED, ping=msg.ping)
async def _get_previous_workspace_entry( self, workspace_entry: WorkspaceEntry) -> WorkspaceEntry: """ Return the most recent workspace entry using the previous encryption revision. Raises: FSError FSBackendOfflineError FSWorkspaceInMaintenance """ # Must retrieve the previous encryption revision's key version_to_fetch = None current_encryption_revision = workspace_entry.encryption_revision while True: previous_user_manifest = await self._fetch_remote_user_manifest( version=version_to_fetch) previous_workspace_entry = previous_user_manifest.get_workspace_entry( workspace_entry.id) if not previous_workspace_entry: raise FSError( f"Never had access to encryption revision {current_encryption_revision - 1}" ) if previous_workspace_entry.encryption_revision == current_encryption_revision - 1: return previous_workspace_entry else: version_to_fetch = previous_user_manifest.version - 1
async def upload_manifest( self, entry_id: EntryID, manifest: BaseRemoteManifest, timestamp_greater_than: Optional[DateTime] = None, ) -> BaseRemoteManifest: raise FSError( "Cannot upload manifest through a timestamped remote loader")
async def get_reencryption_need(self) -> ReencryptionNeed: """ Raises: FSError FSBackendOfflineError FSWorkspaceNoAccess """ wentry = self.get_workspace_entry() try: workspace_manifest = await self.local_storage.get_manifest( self.workspace_id) if workspace_manifest.is_placeholder: return ReencryptionNeed(user_revoked=(), role_revoked=(), reencryption_already_in_progress=False) except FSLocalMissError: pass try: rep = await self.backend_cmds.realm_status(self.workspace_id) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError( f"Cannot retreive remote status for workspace {self.workspace_id}: {exc}" ) from exc reencryption_already_in_progress = (rep["in_maintenance"] and rep["maintenance_type"] == MaintenanceType.REENCRYPTION) certificates = await self.remote_loader.load_realm_role_certificates() has_role = set() role_revoked = set() for certif in certificates: if certif.role is None: if certif.timestamp > wentry.encrypted_on: role_revoked.add(certif.user_id) has_role.discard(certif.user_id) else: role_revoked.discard(certif.user_id) has_role.add(certif.user_id) user_revoked = [] for user_id in has_role: _, revoked_user = await self.remote_loader.get_user(user_id, no_cache=True) if revoked_user and revoked_user.timestamp > wentry.encrypted_on: user_revoked.append(user_id) return ReencryptionNeed( user_revoked=tuple(user_revoked), role_revoked=tuple(role_revoked), reencryption_already_in_progress=reencryption_already_in_progress, )
async def _process_message(self, sender_id: DeviceID, expected_timestamp: Pendulum, ciphered: bytes): """ Raises: FSError FSBackendOfflineError FSSharingNotAllowedError """ # Retrieve the sender try: sender = await self.remote_devices_manager.get_device(sender_id) except RemoteDevicesManagerBackendOfflineError as exc: raise FSBackendOfflineError(str(exc)) from exc except RemoteDevicesManagerError as exc: raise FSError( f"Cannot retrieve message sender `{sender_id}`: {exc}" ) from exc # Decrypt&verify message try: msg = MessageContent.decrypt_verify_and_load_for( ciphered, recipient_privkey=self.device.private_key, author_verify_key=sender.verify_key, expected_author=sender_id, expected_timestamp=expected_timestamp, ) except DataError as exc: raise FSError( f"Cannot decrypt&validate message from `{sender_id}`: {exc}" ) from exc if isinstance( msg, (SharingGrantedMessageContent, SharingReencryptedMessageContent)): await self._process_message_sharing_granted(msg) elif isinstance(msg, SharingRevokedMessageContent): await self._process_message_sharing_revoked(msg) elif isinstance(msg, PingMessageContent): self.event_bus.send("pinged", ping=msg.ping)
async def _vlob_update( self, encryption_revision: int, entry_id: EntryID, ciphered: bytes, now: DateTime, version: int, ) -> None: raise FSError("Cannot update vlob through a timestamped remote loader")
async def _send_start_reencryption_cmd( self, workspace_id: EntryID, encryption_revision: int, timestamp: DateTime, per_user_ciphered_msgs: Dict[UserID, bytes], ) -> bool: """ Raises: FSError FSBackendOfflineError FSWorkspaceNoAccess BackendCmdsParticipantsMismatchError """ # Finally send command to the backend try: rep = await self.backend_cmds.realm_start_reencryption_maintenance( RealmID(workspace_id.uuid), encryption_revision, timestamp, per_user_ciphered_msgs) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError( f"Cannot start maintenance on workspace {workspace_id}: {exc}" ) from exc if rep["status"] == "participants_mismatch": # Catched by caller return False elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( f"Workspace {workspace_id} already in maintenance: {rep}") elif rep["status"] == "not_allowed": raise FSWorkspaceNoAccess( f"Not allowed to start maintenance on workspace {workspace_id}: {rep}" ) elif rep["status"] != "ok": raise FSError( f"Cannot start maintenance on workspace {workspace_id}: {rep}") return True
async def _backend_cmds(self, cmd, *args, **kwargs): try: return await getattr(self.backend_cmds, cmd)(*args, **kwargs) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError( f"`{cmd}` request has failed due to connection error `{exc}`" ) from exc
async def upload_manifest( self, entry_id: EntryID, manifest: BaseRemoteManifest, timestamp_greater_than: Optional[DateTime] = None, ) -> BaseRemoteManifest: """ Raises: FSError FSRemoteSyncError FSBackendOfflineError FSWorkspaceInMaintenance FSBadEncryptionRevision """ assert manifest.author == self.device.device_id # Restamp the manifest before uploading timestamp = self.device.timestamp() if timestamp_greater_than is not None: timestamp = max( timestamp, timestamp_greater_than.add( microseconds=MANIFEST_STAMP_AHEAD_US)) manifest = manifest.evolve(timestamp=timestamp) workspace_entry = self.get_workspace_entry() try: ciphered = manifest.dump_sign_and_encrypt( key=workspace_entry.key, author_signkey=self.device.signing_key) except DataError as exc: raise FSError(f"Cannot encrypt vlob: {exc}") from exc # Upload the vlob try: if manifest.version == 1: await self._vlob_create(workspace_entry.encryption_revision, entry_id, ciphered, manifest.timestamp) else: await self._vlob_update( workspace_entry.encryption_revision, entry_id, ciphered, manifest.timestamp, manifest.version, ) # The backend notified us that some restamping is required except VlobRequireGreaterTimestampError as exc: return await self.upload_manifest(entry_id, manifest, exc.strictly_greater_than) else: return manifest
async def upload_block(self, access: BlockAccess, data: bytes) -> None: """ Raises: FSError FSBackendOfflineError FSRemoteOperationError FSWorkspaceInMaintenance FSWorkspaceNoAccess """ # Encryption try: ciphered = access.key.encrypt(data) # Encryption error except CryptoError as exc: raise FSError(f"Cannot encrypt block: {exc}") from exc # Upload block with translate_backend_cmds_errors(): rep = await self.backend_cmds.block_create( access.id, RealmID(self.workspace_id.uuid), ciphered) if rep["status"] == "already_exists": # Ignore exception if the block has already been uploaded # This might happen when a failure occurs before the local storage is updated pass elif rep["status"] == "not_allowed": # Seems we lost the access to the realm raise FSWorkspaceNoWriteAccess( "Cannot upload block: no write access") elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( "Cannot upload block while the workspace in maintenance") elif rep["status"] != "ok": raise FSError(f"Cannot upload block: {rep}") # Update local storage await self.local_storage.set_clean_block(access.id, data) await self.local_storage.clear_chunk(ChunkID(access.id.uuid), miss_ok=True)
async def get_reencryption_need(self) -> ReencryptionNeed: """ Raises: FSError FSBackendOfflineError FSWorkspaceNoAccess """ wentry = self.get_workspace_entry() try: workspace_manifest = await self.local_storage.get_manifest( self.workspace_id) if workspace_manifest.is_placeholder: return ReencryptionNeed() except FSLocalMissError: pass certificates = await self.remote_loader.load_realm_role_certificates() has_role = set() role_revoked = set() for certif in certificates: if certif.role is None: if certif.timestamp > wentry.encrypted_on: role_revoked.add(certif.user_id) has_role.discard(certif.user_id) else: role_revoked.discard(certif.user_id) has_role.add(certif.user_id) user_revoked = [] try: for user_id in has_role: _, revoked_user = await self.remote_device_manager.get_user( user_id, no_cache=True) if revoked_user and revoked_user.timestamp > wentry.encrypted_on: user_revoked.append(user_id) except RemoteDevicesManagerBackendOfflineError as exc: raise FSBackendOfflineError(str(exc)) from exc except RemoteDevicesManagerError as exc: raise FSError( f"Cannot retrieve workspace participant {user_id}: {exc}" ) from exc return ReencryptionNeed(user_revoked=tuple(user_revoked), role_revoked=tuple(role_revoked))
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 create_realm(self, realm_id: EntryID): """ Raises: FSError FSBackendOfflineError """ certif = RealmRoleCertificateContent.build_realm_root_certif( author=self.device.device_id, timestamp=pendulum_now(), realm_id=realm_id).dump_and_sign(self.device.signing_key) rep = await self._backend_cmds("realm_create", certif) if rep["status"] == "already_exists": # It's possible a previous attempt to create this realm # succeeded but we didn't receive the confirmation, hence # we play idempotent here. return elif rep["status"] != "ok": raise FSError(f"Cannot create realm {realm_id}: `{rep['status']}`")
async def create_realm(self, realm_id: EntryID) -> None: """ Raises: FSError FSRemoteOperationError FSBackendOfflineError """ timestamp = self.device.timestamp() certif = RealmRoleCertificateContent.build_realm_root_certif( author=self.device.device_id, timestamp=timestamp, realm_id=RealmID(realm_id.uuid)).dump_and_sign( self.device.signing_key) with translate_backend_cmds_errors(): rep = await self.backend_cmds.realm_create(certif) if rep["status"] == "already_exists": # It's possible a previous attempt to create this realm # succeeded but we didn't receive the confirmation, hence # we play idempotent here. return elif rep["status"] != "ok": raise FSError(f"Cannot create realm {realm_id}: `{rep['status']}`")
def _throw_permission_error(self) -> NoReturn: raise FSError("Not implemented : WorkspaceStorage is timestamped")
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 _vlob_update(self, *e, **ke): raise FSError( f"Cannot update vlob through a timestamped remote loader")
async def upload_manifest(self, *e, **ke): raise FSError( f"Cannot upload manifest through a timestamped remote loader")
async def upload_block(self, *e, **ke): raise FSError( f"Cannot upload block through a timestamped remote loader")