def _trustchain_data_factory(todo_devices, todo_users): data = TrustchainData(coolorg.organization_id, coolorg.root_verify_key) def _get_certifier_id_and_key(certifier): if not certifier: # Certified by root certifier_id = None certifier_key = coolorg.root_signing_key else: certifier_ld = data.get_local_device(certifier) if not certifier_ld: raise RuntimeError( f"Missing `{certifier}` to sign creation of `{todo_device}`" ) certifier_id = certifier_ld.device_id certifier_key = certifier_ld.signing_key return certifier_id, certifier_key # First create all the devices for todo_device in todo_devices: local_device = local_device_factory(todo_device["id"], org=coolorg) data.add_local_device(local_device) # Generate device certificate certifier_id, certifier_key = _get_certifier_id_and_key( todo_device.get("certifier")) created_on = todo_device.get("created_on", now) device_certificate = DeviceCertificateContent( author=certifier_id, timestamp=created_on, device_id=local_device.device_id, verify_key=local_device.verify_key, ) data.add_device_certif( device_certificate, device_certificate.dump_and_sign(certifier_key)) # Now deal with the users for todo_user in todo_users: local_user = next((u for u in data.local_devices.values() if str(u.user_id) == todo_user["id"]), None) if not local_user: raise RuntimeError( f"Missing device for user `{todo_user['id']}`") # Generate user certificate certifier_id, certifier_key = _get_certifier_id_and_key( todo_user.get("certifier")) created_on = todo_user.get("created_on", now) user_certif = UserCertificateContent( author=certifier_id, timestamp=created_on, user_id=local_user.user_id, public_key=local_user.public_key, is_admin=todo_user.get("is_admin", False), ) data.add_user_certif(user_certif, user_certif.dump_and_sign(certifier_key)) # Generate user revocation certificate if needed revoker = todo_user.get("revoker", None) if revoker: revoked_on = todo_user.get("revoked_on", now) revoker_ld = data.get_local_device(revoker) if not revoker_ld: raise RuntimeError( f"Missing `{revoker}` to sign revocation of `{todo_user['id']}`" ) revoked_user_certificate = RevokedUserCertificateContent( author=revoker_ld.device_id, timestamp=revoked_on, user_id=local_user.user_id) data.add_revoked_user_certif( revoked_user_certificate, revoked_user_certificate.dump_and_sign( revoker_ld.signing_key), ) return data
def load_trustchain( self, users: List[bytes] = (), revoked_users: List[bytes] = (), devices: List[bytes] = (), now: Pendulum = None, ) -> Tuple[List[UserCertificateContent], List[RevokedUserCertificateContent], List[DeviceCertificateContent], ]: now = now or pendulum_now() users_states = {} devices_states = {} revoked_users_states = {} # Deserialize the certificates and filter the ones we already have in cache try: for certif in devices: unverified_device = DeviceCertificateContent.unsecure_load( certif) verified_device = self.get_device(unverified_device.device_id, now) if verified_device: devices_states[verified_device.device_id] = CertifState( certif, verified_device, True) else: devices_states[unverified_device.device_id] = CertifState( certif, unverified_device, False) for certif in users: unverified_user = UserCertificateContent.unsecure_load(certif) verified_user = self.get_user(unverified_user.user_id, now) if verified_user: users_states[verified_user.user_id] = CertifState( certif, verified_user, True) else: users_states[unverified_user.user_id] = CertifState( certif, unverified_user, False) for certif in revoked_users: unverified_revoked_user = RevokedUserCertificateContent.unsecure_load( certif) verified_revoked_user = self.get_revoked_user( unverified_revoked_user.user_id, now) if verified_revoked_user: revoked_users_states[ verified_revoked_user.user_id] = CertifState( certif, verified_revoked_user, True) else: revoked_users_states[ unverified_revoked_user.user_id] = CertifState( certif, unverified_revoked_user, False) except DataError as exc: raise TrustchainError(f"Invalid certificate: {exc}") from exc def _get_eventually_verified_user(user_id): try: return users_states[user_id].content except KeyError: return None def _get_eventually_verified_revoked_user(user_id): try: return revoked_users_states[user_id].content except KeyError: return None def _verify_created_by_root(certif, certif_cls, sign_chain): try: return certif_cls.verify_and_load( certif, author_verify_key=self.root_verify_key, expected_author=None) except DataError as exc: path = _build_signature_path(*sign_chain, "<Root Key>") raise TrustchainError( f"{path}: Invalid certificate: {exc}") from exc def _verify_created_by_device(certif, certif_cls, author_id, sign_chain): author_device = _recursive_verify_device(author_id, sign_chain) try: verified = certif_cls.verify_and_load( certif, author_verify_key=author_device.verify_key, expected_author=author_device.device_id, ) except DataError as exc: path = _build_signature_path(*sign_chain, author_id) raise TrustchainError( f"{path}: Invalid certificate: {exc}") from exc # Author is either admin or signing one of it own devices verified_user_id = (verified.device_id.user_id if isinstance( verified, DeviceCertificateContent) else verified.user_id) if author_device.device_id.user_id != verified_user_id: author_user = _get_eventually_verified_user(author_id.user_id) if not author_user: path = _build_signature_path(*sign_chain, author_id) raise TrustchainError( f"{path}: Missing user certificate for {author_id.user_id}" ) elif not author_user.is_admin: path = _build_signature_path(*sign_chain, author_id) raise TrustchainError( f"{path}: Invalid signature given {author_user.user_id} is not admin" ) # Also make sure author wasn't revoked at creation time author_revoked_user = _get_eventually_verified_revoked_user( author_id.user_id) if author_revoked_user and verified.timestamp > author_revoked_user.timestamp: path = _build_signature_path(*sign_chain, author_id) raise TrustchainError( f"{path}: Signature ({verified.timestamp}) is posterior " f"to user revocation ({author_revoked_user.timestamp})") return verified def _recursive_verify_device(device_id, signed_children=()): if device_id in signed_children: path = _build_signature_path(*signed_children, device_id) raise TrustchainError( f"{path}: Invalid signature loop detected") try: state = devices_states[device_id] except KeyError: path = _build_signature_path(*signed_children, device_id) raise TrustchainError( f"{path}: Missing device certificate for {device_id}") author = state.content.author if author is None: verified_device = _verify_created_by_root( state.certif, DeviceCertificateContent, sign_chain=(*signed_children, device_id)) else: verified_device = _verify_created_by_device( state.certif, DeviceCertificateContent, author, sign_chain=(*signed_children, device_id), ) return verified_device def _verify_user(unverified_content, certif): author = unverified_content.author user_id = unverified_content.user_id if author is None: verified_user = _verify_created_by_root( certif, UserCertificateContent, sign_chain=(f"{user_id}'s creation", )) elif author.user_id == user_id: raise TrustchainError( f"{user_id}: Invalid self-signed user certificate") else: verified_user = _verify_created_by_device( certif, UserCertificateContent, author, sign_chain=(f"{user_id}'s creation", )) return verified_user def _verify_revoked_user(unverified_content, certif): author = unverified_content.author user_id = unverified_content.user_id if author is None: verified_revoked_user = _verify_created_by_root( certif, RevokedUserCertificateContent, sign_chain=(f"{user_id}'s revocation", )) elif author.user_id == user_id: raise TrustchainError( f"{user_id}: Invalid self-signed user revocation certificate" ) else: verified_revoked_user = _verify_created_by_device( certif, RevokedUserCertificateContent, author, sign_chain=(f"{user_id}'s revocation", ), ) return verified_revoked_user # Verified what need to be and populate the cache with them for certif_state in devices_states.values(): if not certif_state.verified: certif_state.content = _recursive_verify_device( certif_state.content.device_id) for certif_state in users_states.values(): if not certif_state.verified: certif_state.content = _verify_user(certif_state.content, certif_state.certif) for certif_state in revoked_users_states.values(): if not certif_state.verified: certif_state.content = _verify_revoked_user( certif_state.content, certif_state.certif) # Finally populate the cache for certif_state in devices_states.values(): if not certif_state.verified: self._devices_cache[certif_state.content.device_id] = ( now, certif_state.content) for certif_state in users_states.values(): if not certif_state.verified: self._users_cache[certif_state.content.user_id] = ( now, certif_state.content) for certif_state in revoked_users_states.values(): if not certif_state.verified: self._revoked_users_cache[certif_state.content.user_id] = ( now, certif_state.content, ) return ( [state.content for state in users_states.values()], [state.content for state in revoked_users_states.values()], [state.content for state in devices_states.values()], )
async def api_organization_bootstrap(self, client_ctx, msg): msg = organization_bootstrap_serializer.req_load(msg) root_verify_key = msg["root_verify_key"] try: u_data = UserCertificateContent.verify_and_load( msg["user_certificate"], author_verify_key=root_verify_key, expected_author=None) d_data = DeviceCertificateContent.verify_and_load( msg["device_certificate"], author_verify_key=root_verify_key, expected_author=None) except DataError as exc: return { "status": "invalid_certification", "reason": f"Invalid certification data ({exc}).", } if u_data.timestamp != d_data.timestamp: return { "status": "invalid_data", "reason": "Device and user certificates must have the same timestamp.", } if u_data.user_id != d_data.device_id.user_id: return { "status": "invalid_data", "reason": "Device and user must have the same user ID.", } now = pendulum.now() if not timestamps_in_the_ballpark(u_data.timestamp, now): return { "status": "invalid_certification", "reason": f"Invalid timestamp in certification.", } user, first_device = new_user_factory( device_id=d_data.device_id, human_handle=u_data.human_handle, is_admin=True, certifier=None, user_certificate=msg["user_certificate"], device_certificate=msg["device_certificate"], ) try: await self.bootstrap( client_ctx.organization_id, user, first_device, msg["bootstrap_token"], root_verify_key, ) except OrganizationAlreadyBootstrappedError: return {"status": "already_bootstrapped"} except (OrganizationNotFoundError, OrganizationInvalidBootstrapTokenError): return {"status": "not_found"} # Note: we let OrganizationFirstUserCreationError bobbles up given # it should not occurs under normal circumstances return organization_bootstrap_serializer.rep_dump({"status": "ok"})
async def api_organization_bootstrap(self, client_ctx, msg): msg = apiv1_organization_bootstrap_serializer.req_load(msg) bootstrap_token = msg["bootstrap_token"] root_verify_key = msg["root_verify_key"] try: u_data = UserCertificateContent.verify_and_load( msg["user_certificate"], author_verify_key=root_verify_key, expected_author=None) d_data = DeviceCertificateContent.verify_and_load( msg["device_certificate"], author_verify_key=root_verify_key, expected_author=None) ru_data = rd_data = None if "redacted_user_certificate" in msg: ru_data = UserCertificateContent.verify_and_load( msg["redacted_user_certificate"], author_verify_key=root_verify_key, expected_author=None, ) if "redacted_device_certificate" in msg: rd_data = DeviceCertificateContent.verify_and_load( msg["redacted_device_certificate"], author_verify_key=root_verify_key, expected_author=None, ) except DataError as exc: return { "status": "invalid_certification", "reason": f"Invalid certification data ({exc}).", } if u_data.profile != UserProfile.ADMIN: return { "status": "invalid_data", "reason": "Bootstrapping user must have admin profile.", } if u_data.timestamp != d_data.timestamp: return { "status": "invalid_data", "reason": "Device and user certificates must have the same timestamp.", } if u_data.user_id != d_data.device_id.user_id: return { "status": "invalid_data", "reason": "Device and user must have the same user ID.", } now = pendulum.now() if not timestamps_in_the_ballpark(u_data.timestamp, now): return { "status": "invalid_certification", "reason": f"Invalid timestamp in certification.", } if ru_data: if ru_data.evolve(human_handle=u_data.human_handle) != u_data: return { "status": "invalid_data", "reason": "Redacted User certificate differs from User certificate.", } if ru_data.human_handle: return { "status": "invalid_data", "reason": "Redacted User certificate must not contain a human_handle field.", } if rd_data: if rd_data.evolve(device_label=d_data.device_label) != d_data: return { "status": "invalid_data", "reason": "Redacted Device certificate differs from Device certificate.", } if rd_data.device_label: return { "status": "invalid_data", "reason": "Redacted Device certificate must not contain a device_label field.", } if (rd_data and not ru_data) or (ru_data and not rd_data): return { "status": "invalid_data", "reason": "Redacted user&device certificate muste be provided together", } user = User( user_id=u_data.user_id, human_handle=u_data.human_handle, profile=u_data.profile, user_certificate=msg["user_certificate"], redacted_user_certificate=msg.get("redacted_user_certificate", msg["user_certificate"]), user_certifier=u_data.author, created_on=u_data.timestamp, ) first_device = Device( device_id=d_data.device_id, device_label=d_data.device_label, device_certificate=msg["device_certificate"], redacted_device_certificate=msg.get("redacted_device_certificate", msg["device_certificate"]), device_certifier=d_data.author, created_on=d_data.timestamp, ) try: await self.bootstrap(client_ctx.organization_id, user, first_device, bootstrap_token, root_verify_key) except OrganizationAlreadyBootstrappedError: return {"status": "already_bootstrapped"} except (OrganizationNotFoundError, OrganizationInvalidBootstrapTokenError): return {"status": "not_found"} # Note: we let OrganizationFirstUserCreationError bobbles up given # it should not occurs under normal circumstances # Finally notify webhook await self.webhooks.on_organization_bootstrap( organization_id=client_ctx.organization_id, device_id=first_device.device_id, device_label=first_device.device_label, human_email=user.human_handle.email if user.human_handle else None, human_label=user.human_handle.label if user.human_handle else None, ) return apiv1_organization_bootstrap_serializer.rep_dump( {"status": "ok"})
async def test_organization_bootstrap_bad_data( apiv1_backend_sock_factory, organization_factory, local_device_factory, backend, coolorg ): neworg = organization_factory("NewOrg") newalice = local_device_factory("alice@dev1", neworg) await backend.organization.create( id=neworg.organization_id, bootstrap_token=neworg.bootstrap_token ) bad_organization_id = coolorg.organization_id good_organization_id = neworg.organization_id root_signing_key = neworg.root_signing_key bad_root_signing_key = coolorg.root_signing_key good_bootstrap_token = neworg.bootstrap_token bad_bootstrap_token = coolorg.bootstrap_token good_rvk = neworg.root_verify_key bad_rvk = coolorg.root_verify_key good_device_id = newalice.device_id good_user_id = newalice.user_id bad_user_id = UserID("dummy") public_key = newalice.public_key verify_key = newalice.verify_key now = pendulum.now() bad_now = now.subtract(seconds=1) good_cu = UserCertificateContent( author=None, timestamp=now, user_id=good_user_id, public_key=public_key, profile=UserProfile.ADMIN, human_handle=newalice.human_handle, ) good_redacted_cu = good_cu.evolve(human_handle=None) good_cd = DeviceCertificateContent( author=None, timestamp=now, device_id=good_device_id, device_label=newalice.device_label, verify_key=verify_key, ) good_redacted_cd = good_cd.evolve(device_label=None) bad_now_cu = good_cu.evolve(timestamp=bad_now) bad_now_cd = good_cd.evolve(timestamp=bad_now) bad_now_redacted_cu = good_redacted_cu.evolve(timestamp=bad_now) bad_now_redacted_cd = good_redacted_cd.evolve(timestamp=bad_now) bad_id_cu = good_cu.evolve(user_id=bad_user_id) bad_not_admin_cu = good_cu.evolve(profile=UserProfile.STANDARD) bad_key_cu = good_cu.dump_and_sign(bad_root_signing_key) bad_key_cd = good_cd.dump_and_sign(bad_root_signing_key) good_cu = good_cu.dump_and_sign(root_signing_key) good_redacted_cu = good_redacted_cu.dump_and_sign(root_signing_key) good_cd = good_cd.dump_and_sign(root_signing_key) good_redacted_cd = good_redacted_cd.dump_and_sign(root_signing_key) bad_now_cu = bad_now_cu.dump_and_sign(root_signing_key) bad_now_cd = bad_now_cd.dump_and_sign(root_signing_key) bad_now_redacted_cu = bad_now_redacted_cu.dump_and_sign(root_signing_key) bad_now_redacted_cd = bad_now_redacted_cd.dump_and_sign(root_signing_key) bad_id_cu = bad_id_cu.dump_and_sign(root_signing_key) bad_not_admin_cu = bad_not_admin_cu.dump_and_sign(root_signing_key) for i, (status, organization_id, *params) in enumerate( [ ("not_found", good_organization_id, bad_bootstrap_token, good_cu, good_cd, good_rvk), ( "already_bootstrapped", bad_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, ), ( "invalid_certification", good_organization_id, good_bootstrap_token, good_cu, good_cd, bad_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, bad_now_cu, good_cd, good_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, bad_id_cu, good_cd, good_rvk, ), ( "invalid_certification", good_organization_id, good_bootstrap_token, bad_key_cu, good_cd, good_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, bad_now_cd, good_rvk, ), ( "invalid_certification", good_organization_id, good_bootstrap_token, good_cu, bad_key_cd, good_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, bad_not_admin_cu, good_cd, good_rvk, ), # Tests with redacted certificates ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, good_cu, # Not redacted ! good_redacted_cd, ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, good_redacted_cu, good_cd, # Not redacted ! ), ( "bad_message", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, None, # None not allowed good_redacted_cd, ), ( "bad_message", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, good_redacted_cu, None, # None not allowed ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, bad_now_redacted_cu, good_redacted_cd, ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, good_redacted_cu, bad_now_redacted_cd, ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, good_redacted_cu, _missing, # Must proved redacted_device if redacted user is present ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, _missing, # Must proved redacted_device if redacted user is present good_redacted_cd, ), ] ): print(f"sub test {i}") async with apiv1_backend_sock_factory(backend, organization_id) as sock: rep = await organization_bootstrap(sock, *params) assert rep["status"] == status # Finally cheap test to make sure our "good" data were really good async with apiv1_backend_sock_factory(backend, good_organization_id) as sock: rep = await organization_bootstrap( sock, good_bootstrap_token, good_cu, good_cd, good_rvk, good_redacted_cu, good_redacted_cd, ) assert rep["status"] == "ok"
async def test_user_create_ok( backend, backend_sock_factory, alice_backend_sock, alice, mallory, profile, with_labels ): now = pendulum.now() user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=mallory.user_id, human_handle=mallory.human_handle, public_key=mallory.public_key, profile=profile, ) redacted_user_certificate = user_certificate.evolve(human_handle=None) device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=mallory.device_id, device_label=mallory.device_label, verify_key=mallory.verify_key, ) redacted_device_certificate = device_certificate.evolve(device_label=None) if not with_labels: user_certificate = redacted_user_certificate device_certificate = redacted_device_certificate user_certificate = user_certificate.dump_and_sign(alice.signing_key) device_certificate = device_certificate.dump_and_sign(alice.signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign(alice.signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign(alice.signing_key) rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=redacted_device_certificate, ) assert rep == {"status": "ok"} # Make sure mallory can connect now async with backend_sock_factory(backend, mallory) as sock: rep = await user_get(sock, user_id=mallory.user_id) assert rep["status"] == "ok" # Check the resulting data in the backend backend_user, backend_device = await backend.user.get_user_with_device( mallory.organization_id, mallory.device_id ) assert backend_user == User( user_id=mallory.user_id, human_handle=mallory.human_handle if with_labels else None, profile=profile, user_certificate=user_certificate, redacted_user_certificate=redacted_user_certificate, user_certifier=alice.device_id, created_on=now, ) assert backend_device == Device( device_id=mallory.device_id, device_label=mallory.device_label if with_labels else None, device_certificate=device_certificate, redacted_device_certificate=redacted_device_certificate, device_certifier=alice.device_id, created_on=now, )
def validate_new_user_certificates( expected_author: DeviceID, author_verify_key: VerifyKey, user_certificate: bytes, device_certificate: bytes, redacted_user_certificate: bytes, redacted_device_certificate: bytes, ) -> Tuple[User, Device]: """ Raises: CertificateValidationError UserInvalidCertificationError """ try: d_data = DeviceCertificateContent.verify_and_load( device_certificate, author_verify_key=author_verify_key, expected_author=expected_author) u_data = UserCertificateContent.verify_and_load( user_certificate, author_verify_key=author_verify_key, expected_author=expected_author) ru_data = UserCertificateContent.verify_and_load( redacted_user_certificate, author_verify_key=author_verify_key, expected_author=expected_author, ) rd_data = DeviceCertificateContent.verify_and_load( redacted_device_certificate, author_verify_key=author_verify_key, expected_author=expected_author, ) except DataError as exc: raise CertificateValidationError( "invalid_certification", f"Invalid certification data ({exc}).") if u_data.timestamp != d_data.timestamp: raise CertificateValidationError( "invalid_data", "Device and User certificates must have the same timestamp.") now = pendulum.now() if not timestamps_in_the_ballpark(u_data.timestamp, now): raise CertificateValidationError("invalid_certification", "Invalid timestamp in certificate.") if u_data.user_id != d_data.device_id.user_id: raise CertificateValidationError( "invalid_data", "Device and User must have the same user ID.") if ru_data.evolve(human_handle=u_data.human_handle) != u_data: raise CertificateValidationError( "invalid_data", "Redacted User certificate differs from User certificate.") if ru_data.human_handle: raise CertificateValidationError( "invalid_data", "Redacted User certificate must not contain a human_handle field.") if rd_data.evolve(device_label=d_data.device_label) != d_data: raise CertificateValidationError( "invalid_data", "Redacted Device certificate differs from Device certificate.") if rd_data.device_label: raise CertificateValidationError( "invalid_data", "Redacted Device certificate must not contain a device_label field." ) user = User( user_id=u_data.user_id, human_handle=u_data.human_handle, profile=u_data.profile, user_certificate=user_certificate, redacted_user_certificate=redacted_user_certificate, user_certifier=u_data.author, created_on=u_data.timestamp, ) first_device = Device( device_id=d_data.device_id, device_label=d_data.device_label, device_certificate=device_certificate, redacted_device_certificate=redacted_device_certificate, device_certifier=d_data.author, created_on=d_data.timestamp, ) return user, first_device
async def test_user_claim_invalid_returned_certificates( running_backend, backend, alice, bob, backend_claim_response_hook): hooks = backend_claim_response_hook device_count = 0 async def _do_test(): nonlocal device_count device_count += 1 new_device_id = DeviceID(f"user{device_count}@dev") token = generate_invitation_token() exception_occured = False async def _from_alice(): await invite_and_create_user(alice, new_device_id.user_id, token=token, is_admin=False) async def _from_new_device(): nonlocal exception_occured with pytest.raises(InviteClaimCryptoError): await claim_user(alice.organization_addr, new_device_id, token=token) exception_occured = True await _invite_and_claim(running_backend, _from_alice, _from_new_device) assert exception_occured # Invalid data hooks["user_certificate"] = lambda x: b"dummy" hooks["device_certificate"] = None await _do_test() hooks["user_certificate"] = None hooks["device_certificate"] = lambda x: b"dummy" await _do_test() # Certificate author differs from invitation creator def bob_sign(certif): return certif.evolve(author=bob.device_id).dump_and_sign( author_signkey=bob.signing_key) hooks["user_certificate"] = lambda raw: bob_sign( UserCertificateContent.unsecure_load(raw)) hooks["device_certificate"] = None await _do_test() hooks["user_certificate"] = None hooks["device_certificate"] = lambda raw: bob_sign( DeviceCertificateContent.unsecure_load(raw)) await _do_test() # Certificate info doesn't correspond to created user hooks["user_certificate"] = ( lambda raw: UserCertificateContent.unsecure_load(raw).evolve( user_id=bob.user_id).dump_and_sign(author_signkey=alice.signing_key )) hooks["device_certificate"] = None await _do_test() hooks["user_certificate"] = None hooks["device_certificate"] = ( lambda raw: DeviceCertificateContent.unsecure_load(raw).evolve( device_id=bob.device_id).dump_and_sign(author_signkey=alice. signing_key)) await _do_test()
async def do_create_new_user( self, author: LocalDevice, device_label: Optional[str], human_handle: Optional[HumanHandle], profile: UserProfile, ) -> None: device_id = DeviceID.new() try: now = pendulum_now() user_certificate = UserCertificateContent( author=author.device_id, timestamp=now, user_id=device_id.user_id, human_handle=human_handle, public_key=self._public_key, profile=profile, ) redacted_user_certificate = user_certificate.evolve( human_handle=None) device_certificate = DeviceCertificateContent( author=author.device_id, timestamp=now, device_id=device_id, verify_key=self._verify_key, device_label=device_label, ) redacted_device_certificate = device_certificate.evolve( device_label=None) user_certificate = user_certificate.dump_and_sign( author.signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign( author.signing_key) device_certificate = device_certificate.dump_and_sign( author.signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( author.signing_key) except DataError as exc: raise InviteError( f"Cannot generate device certificate: {exc}") from exc rep = await self._cmds.user_create( user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=redacted_device_certificate, ) if rep["status"] != "ok": raise InviteError(f"Cannot create device: {rep}") try: payload = InviteUserConfirmation( device_id=device_id, device_label=device_label, human_handle=human_handle, profile=profile, root_verify_key=author.root_verify_key, ).dump_and_encrypt(key=self._shared_secret_key) except DataError as exc: raise InviteError( "Cannot generate InviteUserConfirmation payload") from exc rep = await self._cmds.invite_4_greeter_communicate(token=self.token, payload=payload) if rep["status"] in ("not_found", "already_deleted"): raise InviteNotAvailableError() elif rep["status"] == "invalid_state": raise InvitePeerResetError() elif rep["status"] != "ok": raise InviteError( f"Backend error during step 4 (confirmation exchange): {rep}") await self._cmds.invite_delete(token=self.token, reason=InvitationDeletedReason.FINISHED)
async def api_user_create(self, client_ctx, msg): if not client_ctx.is_admin: return { "status": "not_allowed", "reason": f"User `{client_ctx.device_id.user_id}` is not admin", } msg = user_create_serializer.req_load(msg) try: d_data = DeviceCertificateContent.verify_and_load( msg["device_certificate"], author_verify_key=client_ctx.verify_key, expected_author=client_ctx.device_id, ) u_data = UserCertificateContent.verify_and_load( msg["user_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}).", } if u_data.timestamp != d_data.timestamp: return { "status": "invalid_data", "reason": "Device and User certifications must have the same timestamp.", } now = pendulum.now() if not timestamps_in_the_ballpark(u_data.timestamp, now): return { "status": "invalid_certification", "reason": f"Invalid timestamp in certification.", } if u_data.user_id != d_data.device_id.user_id: return { "status": "invalid_data", "reason": "Device and User must have the same user ID.", } try: user = User( user_id=u_data.user_id, human_handle=u_data.human_handle, is_admin=u_data.is_admin, user_certificate=msg["user_certificate"], user_certifier=u_data.author, created_on=u_data.timestamp, ) first_devices = Device( device_id=d_data.device_id, device_certificate=msg["device_certificate"], device_certifier=d_data.author, created_on=d_data.timestamp, ) await self.create_user(client_ctx.organization_id, user, first_devices) except UserAlreadyExistsError as exc: return {"status": "already_exists", "reason": str(exc)} return user_create_serializer.rep_dump({"status": "ok"})
async def _do_bootstrap_organization( config_dir, password: str, password_check: str, user_id: str, device_name: str, bootstrap_addr: BackendOrganizationBootstrapAddr, ): if password != password_check: raise JobResultError("password-mismatch") if len(password) < 8: raise JobResultError("password-size") try: device_id = DeviceID(f"{user_id}@{device_name}") except ValueError as exc: raise JobResultError("bad-device_name") from exc root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = bootstrap_addr.generate_organization_addr( root_verify_key) try: device = generate_new_device(device_id, organization_addr, is_admin=True) save_device_with_password(config_dir, device, password) except LocalDeviceAlreadyExistsError as exc: raise JobResultError("user-exists") from exc now = pendulum.now() user_certificate = UserCertificateContent( author=None, timestamp=now, user_id=device.user_id, public_key=device.public_key, is_admin=device.is_admin, ).dump_and_sign(root_signing_key) device_certificate = DeviceCertificateContent( author=None, timestamp=now, device_id=device_id, verify_key=device.verify_key).dump_and_sign(root_signing_key) try: async with backend_anonymous_cmds_factory(bootstrap_addr) as cmds: rep = await cmds.organization_bootstrap( bootstrap_addr.organization_id, bootstrap_addr.token, root_verify_key, user_certificate, device_certificate, ) if rep["status"] == "already_bootstrapped": raise JobResultError("already-bootstrapped", info=str(rep)) elif rep["status"] == "not_found": raise JobResultError("invalid-url", info=str(rep)) elif rep["status"] != "ok": raise JobResultError("refused-by-backend", info=str(rep)) return device, password except BackendConnectionRefused as exc: raise JobResultError("invalid-url", info=str(exc)) from exc except BackendNotAvailable as exc: raise JobResultError("backend-offline", info=str(exc)) from exc except BackendConnectionError as exc: raise JobResultError("refused-by-backend", info=str(exc)) from exc
async def test_user_create_invalid_certificate(alice_backend_sock, alice, bob, mallory): now = pendulum.now() good_user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=mallory.user_id, public_key=mallory.public_key, profile=UserProfile.STANDARD, ).dump_and_sign(alice.signing_key) good_device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=mallory.device_id, verify_key=mallory.verify_key, ).dump_and_sign(alice.signing_key) bad_user_certificate = UserCertificateContent( author=bob.device_id, timestamp=now, user_id=mallory.user_id, public_key=mallory.public_key, profile=UserProfile.STANDARD, ).dump_and_sign(bob.signing_key) bad_device_certificate = DeviceCertificateContent( author=bob.device_id, timestamp=now, device_id=mallory.device_id, verify_key=mallory.verify_key, ).dump_and_sign(bob.signing_key) for cu, cd in [ (good_user_certificate, bad_device_certificate), (bad_user_certificate, good_device_certificate), (bad_user_certificate, bad_device_certificate), ]: rep = await user_create( alice_backend_sock, user_certificate=cu, device_certificate=cd, redacted_user_certificate=good_user_certificate, redacted_device_certificate=good_device_certificate, ) assert rep == { "status": "invalid_certification", "reason": "Invalid certification data (Signature was forged or corrupt).", } # Same thing for the redacted part for cu, cd in [ (good_user_certificate, bad_device_certificate), (bad_user_certificate, good_device_certificate), (bad_user_certificate, bad_device_certificate), ]: rep = await user_create( alice_backend_sock, user_certificate=good_user_certificate, device_certificate=good_device_certificate, redacted_user_certificate=cu, redacted_device_certificate=cd, ) assert rep == { "status": "invalid_certification", "reason": "Invalid certification data (Signature was forged or corrupt).", }
async def test_user_create_bad_redacted_user_certificate( alice_backend_sock, alice, mallory): now = pendulum.now() device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=mallory.device_id, verify_key=mallory.verify_key, ).dump_and_sign(alice.signing_key) user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=mallory.user_id, human_handle=mallory.human_handle, public_key=mallory.public_key, profile=UserProfile.STANDARD, ) good_redacted_user_certificate = user_certificate.evolve(human_handle=None) user_certificate = user_certificate.dump_and_sign(alice.signing_key) for bad_redacted_user_certificate in ( good_redacted_user_certificate.evolve(timestamp=now.add( seconds=1)), good_redacted_user_certificate.evolve(user_id=alice.user_id), good_redacted_user_certificate.evolve(public_key=alice.public_key), good_redacted_user_certificate.evolve( profile=UserProfile.OUTSIDER), ): rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=bad_redacted_user_certificate. dump_and_sign(alice.signing_key), redacted_device_certificate=device_certificate, ) assert rep == { "status": "invalid_data", "reason": "Redacted User certificate differs from User certificate.", } # Missing redacted certificate is not allowed as well rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=None, redacted_device_certificate=device_certificate, ) assert rep == { "status": "bad_message", "reason": "Invalid message.", "errors": { "redacted_user_certificate": ["Missing data for required field."] }, } # Finally just make sure good was really good rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=good_redacted_user_certificate.dump_and_sign( alice.signing_key), redacted_device_certificate=device_certificate, ) assert rep == {"status": "ok"}
async def invite_and_create_user( device: LocalDevice, user_id: UserID, token: str, is_admin: bool, keepalive: Optional[int] = None, ) -> DeviceID: """ Raises: InviteClaimError InviteClaimBackendOfflineError InviteClaimValidationError InviteClaimPackingError InviteClaimCryptoError InviteClaimInvalidTokenError """ try: async with backend_authenticated_cmds_factory( device.organization_addr, device.device_id, device.signing_key, keepalive=keepalive) as cmds: try: rep = await cmds.user_invite(user_id) if rep["status"] == "timeout": raise InviteClaimTimeoutError() elif rep["status"] != "ok": raise InviteClaimError(f"Cannot invite user: {rep}") try: claim = UserClaimContent.decrypt_and_load_for( rep["encrypted_claim"], recipient_privkey=device.private_key) except DataError as exc: raise InviteClaimCryptoError( f"Cannot decrypt user claim info: {exc}") from exc if claim.token != token: raise InviteClaimInvalidTokenError( f"Invalid claim token provided by peer: `{claim.token}`" f" (was expecting `{token}`)") device_id = claim.device_id now = pendulum.now() try: user_certificate = UserCertificateContent( author=device.device_id, timestamp=now, user_id=device_id.user_id, public_key=claim.public_key, is_admin=is_admin, ).dump_and_sign(device.signing_key) device_certificate = DeviceCertificateContent( author=device.device_id, timestamp=now, device_id=device_id, verify_key=claim.verify_key, ).dump_and_sign(device.signing_key) except DataError as exc: raise InviteClaimError( f"Cannot generate user&first device certificates: {exc}" ) from exc except: # Cancel the invitation to prevent the claiming peer from # waiting us until timeout deadline = trio.current_time() + CANCEL_INVITATION_MAX_WAIT with trio.CancelScope(shield=True, deadline=deadline): try: await cmds.user_cancel_invitation(user_id) except BackendConnectionError: pass raise rep = await cmds.user_create(user_certificate, device_certificate) if rep["status"] != "ok": raise InviteClaimError(f"Cannot create user: {rep}") except BackendNotAvailable as exc: raise InviteClaimBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise InviteClaimError(f"Cannot create user: {exc}") from exc return device_id
async def test_user_create_ok(backend, apiv1_backend_sock_factory, alice, mallory, profile): now = pendulum.now() user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=mallory.user_id, human_handle=None, public_key=mallory.public_key, profile=profile, ).dump_and_sign(alice.signing_key) device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=mallory.device_id, device_label=None, verify_key=mallory.verify_key, ).dump_and_sign(alice.signing_key) with backend.event_bus.listen() as spy: async with apiv1_backend_sock_factory(backend, alice) as sock: rep = await user_create(sock, user_certificate=user_certificate, device_certificate=device_certificate) assert rep == {"status": "ok"} # No guarantees this event occurs before the command's return await spy.wait_with_timeout( BackendEvent.USER_CREATED, { "organization_id": alice.organization_id, "user_id": mallory.user_id, "first_device_id": mallory.device_id, "user_certificate": user_certificate, "first_device_certificate": device_certificate, }, ) # Make sure mallory can connect now async with apiv1_backend_sock_factory(backend, mallory) as sock: rep = await user_get(sock, user_id=mallory.user_id) assert rep["status"] == "ok" # Check the resulting data in the backend backend_user, backend_device = await backend.user.get_user_with_device( mallory.organization_id, mallory.device_id) assert backend_user == User( user_id=mallory.user_id, human_handle=None, profile=profile, user_certificate=user_certificate, redacted_user_certificate=user_certificate, user_certifier=alice.device_id, created_on=now, ) assert backend_device == Device( device_id=mallory.device_id, device_label=None, device_certificate=device_certificate, redacted_device_certificate=device_certificate, device_certifier=alice.device_id, created_on=now, )
async def claim_user( organization_addr: BackendOrganizationAddr, new_device_id: DeviceID, token: str, keepalive: Optional[int] = None, ) -> LocalDevice: """ Raises: InviteClaimError InviteClaimBackendOfflineError InviteClaimValidationError InviteClaimPackingError InviteClaimCryptoError """ new_device = generate_new_device(new_device_id, organization_addr) try: async with backend_anonymous_cmds_factory(organization_addr, keepalive=keepalive) as cmds: # 1) Retrieve invitation creator try: invitation_creator_user, invitation_creator_device = await get_user_invitation_creator( cmds, new_device.root_verify_key, new_device.user_id) except RemoteDevicesManagerBackendOfflineError as exc: raise InviteClaimBackendOfflineError(str(exc)) from exc except RemoteDevicesManagerError as exc: raise InviteClaimError( f"Cannot retrieve invitation creator: {exc}") from exc # 2) Generate claim info for invitation creator try: encrypted_claim = UserClaimContent( device_id=new_device_id, token=token, public_key=new_device.public_key, verify_key=new_device.verify_key, ).dump_and_encrypt_for( recipient_pubkey=invitation_creator_user.public_key) except DataError as exc: raise InviteClaimError( f"Cannot generate user claim message: {exc}") from exc # 3) Send claim rep = await cmds.user_claim(new_device_id.user_id, encrypted_claim) if rep["status"] != "ok": raise InviteClaimError(f"Cannot claim user: {rep}") # 4) Verify user&device certificates and check admin status try: user = UserCertificateContent.verify_and_load( rep["user_certificate"], author_verify_key=invitation_creator_device.verify_key, expected_author=invitation_creator_device.device_id, expected_user=new_device_id.user_id, ) DeviceCertificateContent.verify_and_load( rep["device_certificate"], author_verify_key=invitation_creator_device.verify_key, expected_author=invitation_creator_device.device_id, expected_device=new_device_id, ) new_device = new_device.evolve(is_admin=user.is_admin) except DataError as exc: raise InviteClaimCryptoError(str(exc)) from exc except BackendNotAvailable as exc: raise InviteClaimBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise InviteClaimError(f"Cannot claim user: {exc}") from exc return new_device
def public_key(self) -> PublicKey: return UserCertificateContent.unsecure_load( self.user_certificate).public_key
def test_unsecure_read_user_certificate_bad_data(): with pytest.raises(DataError): UserCertificateContent.unsecure_load(b"dummy")
async def _api_user_create(self, client_ctx, msg): try: d_data = DeviceCertificateContent.verify_and_load( msg["device_certificate"], author_verify_key=client_ctx.verify_key, expected_author=client_ctx.device_id, ) u_data = UserCertificateContent.verify_and_load( msg["user_certificate"], author_verify_key=client_ctx.verify_key, expected_author=client_ctx.device_id, ) ru_data = UserCertificateContent.verify_and_load( msg["redacted_user_certificate"], author_verify_key=client_ctx.verify_key, expected_author=client_ctx.device_id, ) rd_data = DeviceCertificateContent.verify_and_load( msg["redacted_device_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}).", } if u_data.timestamp != d_data.timestamp: return { "status": "invalid_data", "reason": "Device and User certificates must have the same timestamp.", } now = pendulum.now() if not timestamps_in_the_ballpark(u_data.timestamp, now): return { "status": "invalid_certification", "reason": f"Invalid timestamp in certificate.", } if u_data.user_id != d_data.device_id.user_id: return { "status": "invalid_data", "reason": "Device and User must have the same user ID.", } if ru_data.evolve(human_handle=u_data.human_handle) != u_data: return { "status": "invalid_data", "reason": "Redacted User certificate differs from User certificate.", } if ru_data.human_handle: return { "status": "invalid_data", "reason": "Redacted User certificate must not contain a human_handle field.", } if rd_data.evolve(device_label=d_data.device_label) != d_data: return { "status": "invalid_data", "reason": "Redacted Device certificate differs from Device certificate.", } if rd_data.device_label: return { "status": "invalid_data", "reason": "Redacted Device certificate must not contain a device_label field.", } try: user = User( user_id=u_data.user_id, human_handle=u_data.human_handle, profile=u_data.profile, user_certificate=msg["user_certificate"], redacted_user_certificate=msg["redacted_user_certificate"] or msg["user_certificate"], user_certifier=u_data.author, created_on=u_data.timestamp, ) first_device = Device( device_id=d_data.device_id, device_label=d_data.device_label, device_certificate=msg["device_certificate"], redacted_device_certificate=msg["redacted_device_certificate"] or msg["device_certificate"], device_certifier=d_data.author, created_on=d_data.timestamp, ) await self.create_user(client_ctx.organization_id, user, first_device) except UserAlreadyExistsError as exc: return {"status": "already_exists", "reason": str(exc)} return {"status": "ok"}
async def test_organization_bootstrap_bad_data( backend_data_binder, backend_sock_factory, organization_factory, local_device_factory, backend, coolorg, alice, ): neworg = organization_factory("NewOrg") newalice = local_device_factory("alice@dev1", neworg) await backend_data_binder.bind_organization(neworg) bad_organization_id = coolorg.organization_id good_organization_id = neworg.organization_id root_signing_key = neworg.root_signing_key bad_root_signing_key = coolorg.root_signing_key good_bootstrap_token = neworg.bootstrap_token bad_bootstrap_token = coolorg.bootstrap_token good_rvk = neworg.root_verify_key bad_rvk = coolorg.root_verify_key good_device_id = newalice.device_id good_user_id = newalice.user_id bad_user_id = UserID("dummy") public_key = newalice.public_key verify_key = newalice.verify_key now = pendulum.now() good_cu = UserCertificateContent( author=None, timestamp=now, user_id=good_user_id, public_key=public_key, is_admin=False).dump_and_sign(root_signing_key) good_cd = DeviceCertificateContent( author=None, timestamp=now, device_id=good_device_id, verify_key=verify_key).dump_and_sign(root_signing_key) bad_now = now - pendulum.interval(seconds=1) bad_now_cu = UserCertificateContent( author=None, timestamp=bad_now, user_id=good_user_id, public_key=public_key, is_admin=False).dump_and_sign(root_signing_key) bad_now_cd = DeviceCertificateContent( author=None, timestamp=bad_now, device_id=good_device_id, verify_key=verify_key).dump_and_sign(root_signing_key) bad_id_cu = UserCertificateContent( author=None, timestamp=now, user_id=bad_user_id, public_key=public_key, is_admin=False).dump_and_sign(root_signing_key) bad_key_cu = UserCertificateContent( author=None, timestamp=now, user_id=good_user_id, public_key=public_key, is_admin=False).dump_and_sign(bad_root_signing_key) bad_key_cd = DeviceCertificateContent( author=None, timestamp=now, device_id=good_device_id, verify_key=verify_key).dump_and_sign(bad_root_signing_key) for i, (status, organization_id, *params) in enumerate([ ("not_found", good_organization_id, bad_bootstrap_token, good_cu, good_cd, good_rvk), ( "already_bootstrapped", bad_organization_id, bad_bootstrap_token, bad_key_cu, bad_key_cd, bad_rvk, ), ( "invalid_certification", good_organization_id, good_bootstrap_token, good_cu, good_cd, bad_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, bad_now_cu, good_cd, good_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, bad_id_cu, good_cd, good_rvk, ), ( "invalid_certification", good_organization_id, good_bootstrap_token, bad_key_cu, good_cd, good_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, bad_now_cd, good_rvk, ), ( "invalid_certification", good_organization_id, good_bootstrap_token, good_cu, bad_key_cd, good_rvk, ), ]): async with backend_sock_factory(backend, organization_id) as sock: rep = await organization_bootstrap(sock, *params) assert rep["status"] == status # Finally cheap test to make sure our "good" data were really good async with backend_sock_factory(backend, good_organization_id) as sock: rep = await organization_bootstrap(sock, good_bootstrap_token, good_cu, good_cd, good_rvk) assert rep["status"] == "ok"
async def initialize_test_organization( backend_address, organization_id, alice_device_id, bob_device_id, other_device_name, alice_workspace, bob_workspace, password, administration_token, force, ): configure_logging("WARNING") config_dir = get_default_config_dir(os.environ) alice_slugid = f"{organization_id}:{alice_device_id}" bob_slugid = f"{organization_id}:{bob_device_id}" # Create organization async with backend_administration_cmds_factory( backend_address, administration_token) as cmds: rep = await cmds.organization_create(organization_id) assert rep["status"] == "ok" bootstrap_token = rep["bootstrap_token"] organization_bootstrap_addr = BackendOrganizationBootstrapAddr.build( backend_address, organization_id, bootstrap_token) # Bootstrap organization and Alice user async with backend_anonymous_cmds_factory( organization_bootstrap_addr) as cmds: root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = organization_bootstrap_addr.generate_organization_addr( root_verify_key) alice_device = generate_new_device(alice_device_id, organization_addr, True) save_device_with_password(config_dir, alice_device, password, force=force) now = pendulum.now() user_certificate = UserCertificateContent( author=None, timestamp=now, user_id=alice_device.user_id, public_key=alice_device.public_key, is_admin=True, ).dump_and_sign(author_signkey=root_signing_key) device_certificate = DeviceCertificateContent( author=None, timestamp=now, device_id=alice_device.device_id, verify_key=alice_device.verify_key, ).dump_and_sign(author_signkey=root_signing_key) rep = await cmds.organization_bootstrap( organization_bootstrap_addr.organization_id, organization_bootstrap_addr.token, root_verify_key, user_certificate, device_certificate, ) assert rep["status"] == "ok" # Create a workspace for Alice config = load_config(config_dir, debug="DEBUG" in os.environ) async with logged_core_factory(config, alice_device) as core: alice_ws_id = await core.user_fs.workspace_create(f"{alice_workspace}") await core.user_fs.sync() # Register a new device for Alice token = generate_invitation_token() other_alice_device_id = DeviceID( f"{alice_device.user_id}@{other_device_name}") other_alice_slugid = f"{organization_id}:{other_alice_device_id}" async def invite_task(): await invite_and_create_device(alice_device, other_device_name, token) other_alice_device = None async def claim_task(): nonlocal other_alice_device other_alice_device = await retry_claim(claim_device, alice_device.organization_addr, other_alice_device_id, token) save_device_with_password(config_dir, other_alice_device, password, force=force) async with trio.open_service_nursery() as nursery: nursery.start_soon(invite_task) nursery.start_soon(claim_task) # Invite Bob in token = generate_invitation_token() bob_device = None async def invite_task(): await invite_and_create_user(alice_device, bob_device_id.user_id, token, is_admin=False) async def claim_task(): nonlocal bob_device bob_device = await retry_claim(claim_user, alice_device.organization_addr, bob_device_id, token) save_device_with_password(config_dir, bob_device, password, force=force) async with trio.open_service_nursery() as nursery: nursery.start_soon(invite_task) nursery.start_soon(claim_task) # Create bob workspace and share with Alice async with logged_core_factory(config, bob_device) as core: bob_ws_id = await core.user_fs.workspace_create(f"{bob_workspace}") await core.user_fs.workspace_share(bob_ws_id, alice_device_id.user_id, WorkspaceRole.MANAGER) # Share Alice workspace with bob async with logged_core_factory(config, alice_device) as core: await core.user_fs.workspace_share(alice_ws_id, bob_device_id.user_id, WorkspaceRole.MANAGER) # Synchronize every device for device in (alice_device, other_alice_device, bob_device): async with logged_core_factory(config, device) as core: await core.user_fs.process_last_messages() await core.user_fs.sync() return alice_slugid, other_alice_slugid, bob_slugid