async def test_remove_role_idempotent( backend, alice, bob, alice_backend_sock, realm, start_with_existing_role ): if start_with_existing_role: with freeze_time("2000-01-03"): rep = await _realm_generate_certif_and_update_roles_or_fail( alice_backend_sock, alice, realm, bob.user_id, RealmRole.MANAGER ) assert rep == {"status": "ok"} with freeze_time("2000-01-04"): rep = await _realm_generate_certif_and_update_roles_or_fail( alice_backend_sock, alice, realm, bob.user_id, None ) if start_with_existing_role: assert rep == {"status": "ok"} else: assert rep == {"status": "already_granted"} with freeze_time("2000-01-05"): rep = await _realm_generate_certif_and_update_roles_or_fail( alice_backend_sock, alice, realm, bob.user_id, None ) assert rep == {"status": "already_granted"} certifs = await _realm_get_clear_role_certifs(alice_backend_sock, realm) expected_certifs = [ RealmRoleCertificateContent( author=alice.device_id, timestamp=datetime(2000, 1, 2), realm_id=realm, user_id=alice.user_id, role=RealmRole.OWNER, ) ] if start_with_existing_role: expected_certifs += [ RealmRoleCertificateContent( author=alice.device_id, timestamp=datetime(2000, 1, 3), realm_id=realm, user_id=bob.user_id, role=RealmRole.MANAGER, ), RealmRoleCertificateContent( author=alice.device_id, timestamp=datetime(2000, 1, 4), realm_id=realm, user_id=bob.user_id, role=None, ), ] assert certifs == expected_certifs
async def test_realm_create_not_allowed_for_outsider(backend, alice, alice_backend_sock): realm_id = UUID("C0000000000000000000000000000000") certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm_id ).dump_and_sign(alice.signing_key) rep = await realm_create(alice_backend_sock, certif, check_rep=False) assert rep == {"status": "not_allowed", "reason": "Outsider user cannot create realm"}
async def test_create_invalid_certif(backend, alice, bob, alice_backend_sock): realm_id = UUID("C0000000000000000000000000000000") certif = RealmRoleCertificateContent.build_realm_root_certif( author=bob.device_id, timestamp=pendulum.now(), realm_id=realm_id ).dump_and_sign(bob.signing_key) rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "invalid_certification", "reason": "Invalid certification data (Signature was forged or corrupt).", }
async def _realm_generate_certif_and_update_roles_or_fail( backend_sock, author, realm_id, user_id, role): certif = RealmRoleCertificateContent( author=author.device_id, timestamp=pendulum_now(), realm_id=realm_id, user_id=user_id, role=role, ).dump_and_sign(author.signing_key) return await realm_update_roles(backend_sock, certif, check_rep=False)
async def test_create_ok(backend, alice, alice_backend_sock): await events_subscribe(alice_backend_sock) realm_id = UUID("C0000000000000000000000000000000") certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm_id ).dump_and_sign(alice.signing_key) with backend.event_bus.listen() as spy: rep = await realm_create(alice_backend_sock, certif) assert rep == {"status": "ok"} await spy.wait_with_timeout(BackendEvent.REALM_ROLES_UPDATED)
async def test_create_certif_too_old(backend, alice, alice_backend_sock): realm_id = UUID("C0000000000000000000000000000000") now = pendulum.now() certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=now, realm_id=realm_id ).dump_and_sign(alice.signing_key) with freeze_time(now.add(seconds=TIMESTAMP_MAX_DT)): rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "invalid_certification", "reason": "Invalid timestamp in certification.", }
async def test_create_certif_role_not_owner(backend, alice, bob, alice_backend_sock): realm_id = UUID("C0000000000000000000000000000000") certif = RealmRoleCertificateContent( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm_id, user_id=alice.user_id, role=RealmRole.MANAGER, ).dump_and_sign(alice.signing_key) rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "invalid_data", "reason": "Initial realm role certificate must set OWNER role.", }
async def _backend_realm_generate_certif_and_update_roles(backend, author, realm_id, user_id, role): now = pendulum_now() certif = RealmRoleCertificateContent( author=author.device_id, timestamp=now, realm_id=realm_id, user_id=user_id, role=role ).dump_and_sign(author.signing_key) await backend.realm.update_roles( author.organization_id, RealmGrantedRole( certificate=certif, realm_id=realm_id, user_id=user_id, role=role, granted_by=author.device_id, granted_on=now, ), ) return certif
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 _realm_factory(backend, author, realm_id=None, now=None): realm_id = realm_id or uuid4() now = now or pendulum_now() certif = RealmRoleCertificateContent.build_realm_root_certif( author=author.device_id, timestamp=now, realm_id=realm_id).dump_and_sign(author.signing_key) with backend.event_bus.listen() as spy: await backend.realm.create( organization_id=author.organization_id, self_granted_role=RealmGrantedRole( realm_id=realm_id, user_id=author.user_id, certificate=certif, role=RealmRole.OWNER, granted_by=author.device_id, granted_on=now, ), ) await spy.wait_with_timeout(BackendEvent.REALM_ROLES_UPDATED) return realm_id
async def _give_role(self, author_sock, author, recipient, role): author_sock = await self.get_sock(author) certif = RealmRoleCertificateContent( author=author.device_id, timestamp=pendulum_now(), realm_id=self.realm_id, user_id=recipient.user_id, role=role, ).dump_and_sign(author.signing_key) rep = await realm_update_roles(author_sock, certif, check_rep=False) if author.user_id == recipient.user_id: assert rep == { "status": "invalid_data", "reason": "Realm role certificate cannot be self-signed.", } else: owner_only = (RealmRole.OWNER, ) owner_or_manager = (RealmRole.OWNER, RealmRole.MANAGER) existing_recipient_role = self.current_roles[recipient.user_id] if existing_recipient_role in owner_or_manager or role in owner_or_manager: allowed_roles = owner_only else: allowed_roles = owner_or_manager if self.current_roles[author.user_id] in allowed_roles: # print(f"+ {author.user_id} -{role.value}-> {recipient.user_id}") if existing_recipient_role != role: assert rep == {"status": "ok"} self.current_roles[recipient.user_id] = role self.certifs.append(certif) else: assert rep == {"status": "already_granted"} else: # print(f"- {author.user_id} -{role.value}-> {recipient.user_id}") assert rep == {"status": "not_allowed"} return rep["status"] == "ok"
async def _update_role(self, author, user, role=RealmRole.MANAGER): now = pendulum_now() certif = RealmRoleCertificateContent( author=author.device_id, timestamp=now, realm_id=self.wid, user_id=user.user_id, role=role, ).dump_and_sign(author.signing_key) await self.backend.realm.update_roles( author.organization_id, RealmGrantedRole( certificate=certif, realm_id=self.wid, user_id=user.user_id, role=role, granted_by=author.device_id, granted_on=now, ), ) return certif
async def _update_role_and_check_events(role): with backend.event_bus.listen() as spy: certif = RealmRoleCertificateContent( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm, user_id=bob.user_id, role=role, ).dump_and_sign(alice.signing_key) rep = await realm_update_roles(alice_backend_sock, certif, check_rep=False) assert rep == {"status": "ok"} await spy.wait_with_timeout( BackendEvent.REALM_ROLES_UPDATED, { "organization_id": alice.organization_id, "author": alice.device_id, "realm_id": realm, "user": bob.user_id, "role": role, }, ) # Check events propagated to the client rep = await events_listen_nowait(bob_backend_sock) assert rep == { "status": "ok", "event": APIEvent.REALM_ROLES_UPDATED, "realm_id": realm, "role": role, } rep = await events_listen_nowait(bob_backend_sock) assert rep == {"status": "no_events"}
async def _create_realm_and_first_vlob(self, device): manifest = initial_user_manifest_state.get_user_manifest_v1_for_backend( device) if manifest.author == device.device_id: author = device else: author = self.get_device(device.organization_id, manifest.author) realm_id = author.user_manifest_id with self.backend.event_bus.listen() as spy: await self.backend.realm.create( organization_id=author.organization_id, self_granted_role=RealmGrantedRole( realm_id=realm_id, user_id=author.user_id, certificate=RealmRoleCertificateContent( author=author.device_id, timestamp=manifest.timestamp, realm_id=realm_id, user_id=author.user_id, role=RealmRole.OWNER, ).dump_and_sign(author.signing_key), role=RealmRole.OWNER, granted_by=author.device_id, granted_on=manifest.timestamp, ), ) await self.backend.vlob.create( organization_id=author.organization_id, author=author.device_id, realm_id=realm_id, encryption_revision=1, vlob_id=author.user_manifest_id, timestamp=manifest.timestamp, blob=manifest.dump_sign_and_encrypt( author_signkey=author.signing_key, key=author.user_manifest_key), ) # Avoid possible race condition in tests listening for events await spy.wait_multiple_with_timeout([ ( BackendEvent.REALM_ROLES_UPDATED, { "organization_id": author.organization_id, "author": author.device_id, "realm_id": author.user_manifest_id, "user": author.user_id, "role": RealmRole.OWNER, }, ), ( BackendEvent.REALM_VLOBS_UPDATED, { "organization_id": author.organization_id, "author": author.device_id, "realm_id": author.user_manifest_id, "checkpoint": 1, "src_id": author.user_manifest_id, "src_version": 1, }, ), ])
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: Dict[UserID, RealmRole] = {} owner_only = (RealmRole.OWNER, ) owner_or_manager = (RealmRole.OWNER, RealmRole.MANAGER) # Now verify each certif for unsecure_certif, raw_certif in unsecure_certifs: with translate_remote_devices_manager_errors(): author = await self.remote_devices_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: Tuple[Optional[RealmRole], ...] = (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 # TODO: typing, author is optional in base.py but it seems that manifests always have an author (no RVK) if (current_roles.get( cast(DeviceID, 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 given we have valided it items return [c for c, _ in unsecure_certifs], current_roles
async def _realm_get_clear_role_certifs(sock, realm_id): rep = await realm_get_role_certificates(sock, realm_id) assert rep["status"] == "ok" cooked = [RealmRoleCertificateContent.unsecure_load(certif) for certif in rep["certificates"]] return [item for item in sorted(cooked, key=lambda x: x.timestamp)]
async def _outbound_sync_inner(self) -> bool: base_um = self.get_user_manifest() if not base_um.need_sync: return True # Make sure the corresponding realm has been created in the backend if base_um.is_placeholder: certif = RealmRoleCertificateContent.build_realm_root_certif( author=self.device.device_id, timestamp=pendulum_now(), realm_id=self.device.user_manifest_id, ).dump_and_sign(self.device.signing_key) try: rep = await self.backend_cmds.realm_create(certif) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError( f"Cannot create user manifest's realm in backend: {exc}" ) from exc 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. pass elif rep["status"] != "ok": raise FSError( f"Cannot create user manifest's realm in backend: {rep}") # Sync placeholders for w in base_um.workspaces: await self._workspace_minimal_sync(w) # Build vlob now = pendulum_now() to_sync_um = base_um.to_remote(author=self.device.device_id, timestamp=now) ciphered = to_sync_um.dump_sign_and_encrypt( author_signkey=self.device.signing_key, key=self.device.user_manifest_key) # Sync the vlob with backend try: # Note encryption_revision is always 1 given we never reencrypt # the user manifest's realm if to_sync_um.version == 1: rep = await self.backend_cmds.vlob_create( self.user_manifest_id, 1, self.user_manifest_id, now, ciphered) else: rep = await self.backend_cmds.vlob_update( 1, self.user_manifest_id, to_sync_um.version, now, ciphered) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError(f"Cannot sync user manifest: {exc}") from exc if rep["status"] in ("already_exists", "bad_version"): # Concurrency error (handled by the caller) return False elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( f"Cannot modify workspace data while it is in maintenance: {rep}" ) elif rep["status"] != "ok": raise FSError(f"Cannot sync user manifest: {rep}") # Merge back the manifest in local async with self._update_user_manifest_lock: diverged_um = self.get_user_manifest() # Final merge could have been achieved by a concurrent operation if to_sync_um.version > diverged_um.base_version: merged_um = merge_local_user_manifests(diverged_um, to_sync_um) await self.set_user_manifest(merged_um) self.event_bus.send(ClientEvent.FS_ENTRY_SYNCED, id=self.user_manifest_id) return True
async def workspace_share(self, workspace_id: EntryID, recipient: UserID, role: Optional[WorkspaceRole]) -> None: """ Raises: FSError FSWorkspaceNotFoundError FSBackendOfflineError FSSharingNotAllowedError """ if self.device.user_id == recipient: raise FSError("Cannot share to oneself") 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}`") # Make sure the workspace is not a placeholder await self._workspace_minimal_sync(workspace_entry) # Retrieve the user recipient_user, revoked_recipient_user = await self.remote_loader.get_user( recipient) if revoked_recipient_user: raise FSError(f"User {recipient} revoked") # Note we don't bother to check workspace's access roles given they # could be outdated (and backend will do the check anyway) now = pendulum_now() # Build the sharing message try: if role is not None: recipient_message: Union[ SharingGrantedMessageContent, SharingRevokedMessageContent] = SharingGrantedMessageContent( author=self.device.device_id, timestamp=now, name=workspace_entry.name, id=workspace_entry.id, encryption_revision=workspace_entry. encryption_revision, encrypted_on=workspace_entry.encrypted_on, key=workspace_entry.key, ) else: recipient_message = SharingRevokedMessageContent( author=self.device.device_id, timestamp=now, id=workspace_entry.id) ciphered_recipient_message = recipient_message.dump_sign_and_encrypt_for( author_signkey=self.device.signing_key, recipient_pubkey=recipient_user.public_key) except DataError as exc: raise FSError( f"Cannot create sharing message for `{recipient}`: {exc}" ) from exc # Build role certificate role_certificate = RealmRoleCertificateContent( author=self.device.device_id, timestamp=now, realm_id=workspace_id, user_id=recipient, role=role, ).dump_and_sign(self.device.signing_key) # Actually send the command to the backend try: rep = await self.backend_cmds.realm_update_roles( role_certificate, ciphered_recipient_message) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError( f"Error while trying to set vlob group roles in backend: {exc}" ) from exc if rep["status"] == "not_allowed": raise FSSharingNotAllowedError( f"Must be Owner or Manager on the workspace is mandatory to share it: {rep}" ) elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( f"Cannot share workspace while it is in maintenance: {rep}") elif rep["status"] == "already_granted": # Stay idempotent return elif rep["status"] != "ok": raise FSError( f"Error while trying to set vlob group roles in backend: {rep}" )
async def test_create_realm_already_exists(backend, alice, alice_backend_sock, realm): certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm ).dump_and_sign(alice.signing_key) rep = await realm_create(alice_backend_sock, certif) assert rep == {"status": "already_exists"}
async def api_realm_create(self, client_ctx, msg): if client_ctx.profile == UserProfile.OUTSIDER: return { "status": "not_allowed", "reason": "Outsider user cannot create realm" } msg = realm_create_serializer.req_load(msg) try: data = RealmRoleCertificateContent.verify_and_load( msg["role_certificate"], author_verify_key=client_ctx.verify_key, expected_author=client_ctx.device_id, ) except DataError as exc: return { "status": "invalid_certification", "reason": f"Invalid certification data ({exc}).", } now = pendulum.now() if not timestamps_in_the_ballpark(data.timestamp, now): return { "status": "invalid_certification", "reason": "Invalid timestamp in certification.", } granted_role = RealmGrantedRole( certificate=msg["role_certificate"], realm_id=data.realm_id, user_id=data.user_id, role=data.role, granted_by=data.author, granted_on=data.timestamp, ) if granted_role.granted_by.user_id != granted_role.user_id: return { "status": "invalid_data", "reason": "Initial realm role certificate must be self-signed.", } if granted_role.role != RealmRole.OWNER: return { "status": "invalid_data", "reason": "Initial realm role certificate must set OWNER role.", } try: await self.create(client_ctx.organization_id, granted_role) except RealmNotFoundError as exc: return realm_create_serializer.rep_dump({ "status": "not_found", "reason": str(exc) }) except RealmAlreadyExistsError: return realm_create_serializer.rep_dump( {"status": "already_exists"}) return realm_create_serializer.rep_dump({"status": "ok"})
async def api_realm_update_roles(self, client_ctx, msg): msg = realm_update_roles_serializer.req_load(msg) try: data = RealmRoleCertificateContent.verify_and_load( msg["role_certificate"], author_verify_key=client_ctx.verify_key, expected_author=client_ctx.device_id, ) except DataError as exc: return { "status": "invalid_certification", "reason": f"Invalid certification data ({exc}).", } now = pendulum.now() if not timestamps_in_the_ballpark(data.timestamp, now): return { "status": "invalid_certification", "reason": "Invalid timestamp in certification.", } granted_role = RealmGrantedRole( certificate=msg["role_certificate"], realm_id=data.realm_id, user_id=data.user_id, role=data.role, granted_by=data.author, granted_on=data.timestamp, ) if granted_role.granted_by.user_id == granted_role.user_id: return { "status": "invalid_data", "reason": "Realm role certificate cannot be self-signed.", } try: await self.update_roles(client_ctx.organization_id, granted_role, msg["recipient_message"]) except RealmRoleAlreadyGranted: return realm_update_roles_serializer.rep_dump( {"status": "already_granted"}) except RealmAccessError: return realm_update_roles_serializer.rep_dump( {"status": "not_allowed"}) except RealmIncompatibleProfileError as exc: return realm_update_roles_serializer.rep_dump({ "status": "incompatible_profile", "reason": str(exc) }) except RealmNotFoundError as exc: return realm_update_roles_serializer.rep_dump({ "status": "not_found", "reason": str(exc) }) except RealmInMaintenanceError: return realm_update_roles_serializer.rep_dump( {"status": "in_maintenance"}) return realm_update_roles_serializer.rep_dump({"status": "ok"})