async def test_user_claim_ok(monkeypatch, backend, anonymous_backend_sock, coolorg, alice, mallory_invitation): user_invitation_retreived = trio.Event() vanilla_claim_user_invitation = backend.user.claim_user_invitation async def _mocked_claim_user_invitation(*args, **kwargs): ret = await vanilla_claim_user_invitation(*args, **kwargs) user_invitation_retreived.set() return ret monkeypatch.setattr(backend.user, "claim_user_invitation", _mocked_claim_user_invitation) with freeze_time(mallory_invitation.created_on): async with user_claim( anonymous_backend_sock, invited_user_id=mallory_invitation.user_id, encrypted_claim=b"<foo>", ) as prep: # `backend.user.create_user` will destroy the user invitation, # so make sure we retreived it before await user_invitation_retreived.wait() # No the user we are waiting for await backend.user.create_user( alice.organization_id, User( user_id=UserID("dummy"), user_certificate=b"<user certif>", user_certifier=alice.device_id, ), Device( device_id=DeviceID("dummy@pc1"), device_certificate=b"<device certif>", device_certifier=alice.device_id, ), ) await backend.user.create_user( alice.organization_id, User( user_id=mallory_invitation.user_id, user_certificate=b"<user certif>", user_certifier=alice.device_id, ), Device( device_id=DeviceID(f"{mallory_invitation.user_id}@pc1"), device_certificate=b"<device certif>", device_certifier=alice.device_id, ), ) assert prep[0] == { "status": "ok", "user_certificate": b"<user certif>", "device_certificate": b"<device certif>", }
async def test_device_claim_ok(monkeypatch, backend, apiv1_anonymous_backend_sock, alice, alice_nd_invitation): device_invitation_retrieved = trio.Event() vanilla_claim_device_invitation = backend.user.claim_device_invitation async def _mocked_claim_device_invitation(*args, **kwargs): ret = await vanilla_claim_device_invitation(*args, **kwargs) device_invitation_retrieved.set() return ret monkeypatch.setattr(backend.user, "claim_device_invitation", _mocked_claim_device_invitation) with freeze_time(alice_nd_invitation.created_on): async with device_claim( apiv1_anonymous_backend_sock, invited_device_id=alice_nd_invitation.device_id, encrypted_claim=b"<foo>", ) as prep: # `backend.user.create_device` will destroy the device invitation, # so make sure we retrieved it before await device_invitation_retrieved.wait() # No the device we are waiting for await backend.user.create_device( alice.organization_id, Device( device_id=alice.user_id.to_device_id("dummy"), device_label=None, device_certificate=b"<alice@dummy certificate>", redacted_device_certificate= b"<redacted alice@dummy certificate>", device_certifier=alice.device_id, ), encrypted_answer=b"<alice@dummy answer>", ) await backend.user.create_device( alice.organization_id, Device( device_id=alice_nd_invitation.device_id, device_label=None, device_certificate=b"<alice@new_device certificate>", redacted_device_certificate= b"<redacted alice@new_device certificate>", device_certifier=alice.device_id, ), encrypted_answer=b"<alice@new_device answer>", ) assert prep[0] == { "status": "ok", "encrypted_answer": b"<alice@new_device answer>", "device_certificate": b"<alice@new_device certificate>", }
async def query_get_user_with_device( conn, organization_id: OrganizationID, device_id: DeviceID) -> Tuple[User, Device]: d_row = await conn.fetchrow(_q_get_device, organization_id, device_id) u_row = await conn.fetchrow(_q_get_user, organization_id, device_id.user_id) if not u_row or not d_row: raise UserNotFoundError(device_id) device = Device( device_id=device_id, device_label=d_row["device_label"], device_certificate=d_row["device_certificate"], redacted_device_certificate=d_row["redacted_device_certificate"], device_certifier=d_row["device_certifier"], created_on=d_row["created_on"], ) user = User( user_id=device_id.user_id, profile=STR_TO_USER_PROFILE[u_row["profile"]], user_certificate=u_row["user_certificate"], redacted_user_certificate=u_row["redacted_user_certificate"], user_certifier=u_row["user_certifier"], created_on=u_row["created_on"], revoked_on=u_row["revoked_on"], revoked_user_certificate=u_row["revoked_user_certificate"], revoked_user_certifier=u_row["revoked_user_certifier"], ) return user, device
async def test_user_claim_already_exists(mock_clock, backend, apiv1_anonymous_backend_sock, alice, mallory_invitation): await backend.user.create_user( alice.organization_id, User( user_id=mallory_invitation.user_id, human_handle=None, user_certificate=b"<foo>", redacted_user_certificate=b"<foo>", user_certifier=alice.device_id, ), Device( device_id=mallory_invitation.user_id.to_device_id("pc1"), device_label=None, device_certificate=b"<bar>", redacted_device_certificate=b"<bar>", device_certifier=alice.device_id, ), ) with freeze_time(mallory_invitation.created_on): async with user_claim( apiv1_anonymous_backend_sock, invited_user_id=mallory_invitation.user_id, encrypted_claim=b"<foo>", ) as prep: pass assert prep[0] == {"status": "not_found"}
async def _get_user_devices(conn, organization_id: OrganizationID, user_id: UserID) -> Tuple[Device]: results = await conn.fetch(_q_get_user_devices, organization_id, user_id) return tuple( Device( device_id=DeviceID(row["device_id"]), device_certificate=row["device_certificate"], device_certifier=row["device_certifier"], created_on=row["created_on"], ) for row in results)
async def test_device_create_ok(backend, backend_sock_factory, alice_backend_sock, alice, alice_nd, with_labels): now = pendulum.now() device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=alice_nd.device_id, device_label=alice_nd.device_label, verify_key=alice_nd.verify_key, ) redacted_device_certificate = device_certificate.evolve(device_label=None) if not with_labels: device_certificate = redacted_device_certificate device_certificate = device_certificate.dump_and_sign(alice.signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( alice.signing_key) with backend.event_bus.listen() as spy: rep = await device_create( alice_backend_sock, device_certificate=device_certificate, redacted_device_certificate=redacted_device_certificate, ) assert rep == {"status": "ok"} # No guarantees this event occurs before the command's return await spy.wait_with_timeout( BackendEvent.DEVICE_CREATED, { "organization_id": alice_nd.organization_id, "device_id": alice_nd.device_id, "device_certificate": device_certificate, "encrypted_answer": b"", }, ) # Make sure the new device can connect now async with backend_sock_factory(backend, alice_nd) as sock: rep = await ping(sock, ping="Hello world !") assert rep == {"status": "ok", "pong": "Hello world !"} # Check the resulting data in the backend _, backend_device = await backend.user.get_user_with_device( alice_nd.organization_id, alice_nd.device_id) assert backend_device == Device( device_id=alice_nd.device_id, device_label=alice_nd.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, )
async def _get_device(conn, organization_id: OrganizationID, device_id: DeviceID) -> Device: row = await conn.fetchrow(_q_get_device, organization_id, device_id) if not row: raise UserNotFoundError(device_id) return Device( device_id=device_id, device_certificate=row["device_certificate"], device_certifier=row["device_certifier"], created_on=row["created_on"], )
async def _get_trustchain(self, conn, organization_id: OrganizationID, user: User) -> Dict[DeviceID, Device]: # TODO: it's time to do a super awesome SQL query fetching everything # in one go... devices = {} devices_to_fetch = [] if user.user_certifier: devices_to_fetch.append(user.user_certifier) for device in user.devices.values(): if device.device_certifier: devices_to_fetch.append(device.device_certifier) if device.revocation_certifier: devices_to_fetch.append(device.revocation_certifier) while devices_to_fetch: results = await conn.fetch( """ SELECT d1.device_id, d1.certified_device, ( SELECT devices.device_id FROM devices WHERE devices._id = d1.device_certifier ) AS device_certifier, d1.created_on, d1.revocated_on, d1.certified_revocation, ( SELECT devices.device_id FROM devices WHERE devices._id = d1.revocation_certifier ) as revocation_certifier FROM devices as d1 WHERE organization = ( SELECT _id from organizations WHERE organization_id = $1 ) AND device_id = any($2::text[]); """, organization_id, devices_to_fetch, ) for result in results: devices[result[0]] = Device(*result) devices_to_fetch = [] for device in devices.values(): if device.device_certifier and device.device_certifier not in devices: devices_to_fetch.append(device.device_certifier) if device.revocation_certifier and device.revocation_certifier not in devices: devices_to_fetch.append(device.revocation_certifier) return devices
async def _get_device(conn, organization_id: OrganizationID, device_id: DeviceID) -> Device: row = await conn.fetchrow(*_q_get_device( organization_id=organization_id.str, device_id=device_id.str)) if not row: raise UserNotFoundError(device_id) return Device( device_id=device_id, device_label=DeviceLabel(row["device_label"]), device_certificate=row["device_certificate"], redacted_device_certificate=row["redacted_device_certificate"], device_certifier=DeviceID(row["device_certifier"]), created_on=row["created_on"], )
async def _get_user_devices(conn, organization_id: OrganizationID, user_id: UserID) -> Tuple[Device, ...]: results = await conn.fetch(*_q_get_user_devices( organization_id=organization_id.str, user_id=user_id.str)) return tuple( Device( device_id=DeviceID(row["device_id"]), device_label=DeviceLabel(row["device_label"] ) if row["device_label"] else None, device_certificate=row["device_certificate"], redacted_device_certificate=row["redacted_device_certificate"], device_certifier=DeviceID(row["device_certifier"] ) if row["device_certifier"] else None, created_on=row["created_on"], ) for row in results)
async def query_get_user_with_device( conn, organization_id: OrganizationID, device_id: DeviceID) -> Tuple[User, Device]: d_row = await conn.fetchrow(*_q_get_device( organization_id=organization_id.str, device_id=device_id.str)) u_row = await conn.fetchrow(*_q_get_user( organization_id=organization_id.str, user_id=device_id.user_id.str)) if not u_row or not d_row: raise UserNotFoundError(device_id) human_handle = None if u_row["human_email"]: human_handle = HumanHandle(email=u_row["human_email"], label=u_row["human_label"]) device = Device( device_id=device_id, device_label=DeviceLabel(d_row["device_label"]) if d_row["device_label"] else None, device_certificate=d_row["device_certificate"], redacted_device_certificate=d_row["redacted_device_certificate"], device_certifier=DeviceID(d_row["device_certifier"]) if d_row["device_certifier"] else None, created_on=d_row["created_on"], ) user = User( user_id=device_id.user_id, human_handle=human_handle, profile=UserProfile(u_row["profile"]), user_certificate=u_row["user_certificate"], redacted_user_certificate=u_row["redacted_user_certificate"], user_certifier=DeviceID(u_row["user_certifier"]) if u_row["user_certifier"] else None, created_on=u_row["created_on"], revoked_on=u_row["revoked_on"], revoked_user_certificate=u_row["revoked_user_certificate"], revoked_user_certifier=DeviceID(u_row["revoked_user_certifier"]) if u_row["revoked_user_certifier"] else None, ) return user, device
async def test_device_claim_already_exists( mock_clock, backend, anonymous_backend_sock, alice, alice_nd_invitation ): await backend.user.create_device( alice.organization_id, Device( device_id=alice_nd_invitation.device_id, device_certificate=b"<foo>", device_certifier=alice.device_id, ), ) with freeze_time(alice_nd_invitation.created_on): async with device_claim( anonymous_backend_sock, invited_device_id=alice_nd_invitation.device_id, encrypted_claim=b"<foo>", ) as prep: pass assert prep[0] == {"status": "not_found"}
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, )
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_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 _get_user(self, conn, organization_id: OrganizationID, user_id: UserID) -> User: user_result = await conn.fetchrow( """ SELECT is_admin, certified_user, (SELECT device_id FROM devices WHERE _id = user_certifier), created_on FROM users WHERE organization = ( SELECT _id from organizations WHERE organization_id = $1 ) AND user_id = $2 """, organization_id, user_id, ) if not user_result: raise UserNotFoundError(user_id) devices_results = await conn.fetch( """ SELECT d1.device_id, d1.certified_device, ( SELECT devices.device_id FROM devices WHERE devices._id = d1.device_certifier ) AS device_certifier, d1.created_on, d1.revocated_on, d1.certified_revocation, ( SELECT devices.device_id FROM devices WHERE devices._id = d1.revocation_certifier ) as revocation_certifier FROM devices as d1 WHERE user_ = ( SELECT _id FROM users WHERE organization = ( SELECT _id from organizations WHERE organization_id = $1 ) AND user_id = $2 ); """, organization_id, user_id, ) devices = DevicesMapping(*[ Device(DeviceID(device_result[0]), *device_result[1:]) for device_result in devices_results ]) return User( user_id=UserID(user_id), is_admin=user_result[0], certified_user=user_result[1], user_certifier=user_result[2], created_on=user_result[3], devices=devices, )