Example #1
0
async def test_find_user_by_phone(user):
    """Test that the phone number from the user
    created in the setup method has the correct id
    """

    res = await graph.find_user_by_phone(user["phone_number"], select="id")
    assert res["id"] == user["id"]
    assert res["displayName"] == user["phone_number"]
    assert res["logName"] == utils.mask_phone(user["phone_number"])
Example #2
0
async def revoke_and_delete(phone_number):
    masked_number = mask_phone(phone_number)
    user = await graph.find_user_by_phone(phone_number)
    if user is None:
        raise web.HTTPError(404,
                            f"No user found associated with {masked_number}")

    # store consent-revoked marker on user
    await graph.store_consent_revoked(user)
    # delete devices from iothub so they won't be able to publish data anymore
    await graph.process_user_deletion(user)
Example #3
0
async def find_user(phone_number, timeout=3):
    """Lookup user by phone number"""
    masked_number = mask_phone(phone_number)
    try:
        user = await exponential_backoff(
            partial(graph.find_user_by_phone, phone_number),
            fail_message=f"Number not found: {masked_number}",
            timeout=timeout,
            start_wait=1,
        )
    except TimeoutError:
        raise web.HTTPError(400,
                            f"No user found associated with {masked_number}")
    return user
Example #4
0
 async def find_user(self, phone_number, timeout=3):
     """Lookup user by phone number"""
     masked_number = mask_phone(phone_number)
     try:
         user = await exponential_backoff(
             partial(graph.find_user_by_phone, phone_number),
             fail_message=f"Number not found: {masked_number}",
             timeout=timeout,
             start_wait=1,
         )
     except TimeoutError:
         # is this 400? This shouldn't happen, so maybe it ought to be 500
         raise web.HTTPError(400, f"No user found associated with {masked_number}")
     return user
from tornado.httpclient import HTTPRequest

import corona_backend.handlers
import corona_backend.onboarding.app
from corona_backend import devices, graph
from corona_backend import middleware as mw
from corona_backend import sql
from corona_backend import test as test_utils
from corona_backend import testsql, utils
from corona_backend.handlers import common_endpoints

from .conftest import TEST_DEVICE_ID, TEST_DEVICE_KEY

TEST_PHONE_NUMBER = f"+00{random.randint(1,9999):06}"
TEST_PHONE_NUMBER = "+00001234"
MASKED_TEST_PHONE_NUMBER = utils.mask_phone(TEST_PHONE_NUMBER)

TEST_TOKEN = "secret"
CONSECUTIVE_FAILURE_LIMIT = 2


@pytest.fixture(scope="module", autouse=True)
async def testsetup(setup_testdb):
    """Setup for handler tests.
    Creates testdb and tears down testdb when tests complete."""


def get_test_payload(phonenumber=TEST_PHONE_NUMBER):
    return {
        "_access_token": TEST_TOKEN,
        "_phonenumber": phonenumber,
def test_mask_phone(phone_nr, masked_phone_nr):
    assert utils.mask_phone(phone_nr) == masked_phone_nr
Example #7
0
def test_wrap_user_with_logName():
    phone_number = "+001234567"
    u = {"displayName": phone_number}
    wrapped_user = graph.wrap_user(u)
    assert wrapped_user["logName"] == utils.mask_phone(phone_number)
Example #8
0
def test_wrap_user_empty():
    user = {}
    wrapped_user = graph.wrap_user(user)
    assert wrapped_user["logName"] == utils.mask_phone("unknown")
Example #9
0
    async def get(self, request_id):
        app_log.info(f"Looking up result for {request_id}")
        request_info = f"lookup:{request_id}:info"
        db = get_redis()
        item = db.get(request_info)
        if not item:
            raise web.HTTPError(404, "No such request")

        info = json.loads(item.decode("utf8"))
        device_ids = info["device_ids"]
        phone_number = info["phone_number"]

        # TODO: is it worth logging retrieval in audit log separately request?
        # without separate auth, this isn't useful
        mask_number = utils.mask_phone(phone_number)
        result_keys = info["result_keys"]
        num_ready = db.exists(*result_keys)
        progress = f"{num_ready}/{len(result_keys)}"
        if num_ready < len(info["result_keys"]):
            app_log.info(
                f"Lookup request {request_id} not ready yet: {progress}")
            self.set_status(202)
            self.write(
                json.dumps({
                    "message":
                    f"Not finished processing (completed {progress} tasks)"
                }))
            return

        app_log.info(f"Lookup request {request_id} complete: {progress}")

        # we are done! Collect and return the report
        results = [
            json.loads(item.decode("utf8")) for item in db.mget(*result_keys)
        ]
        contacts = []

        for result in results:
            if result["status"] != "success":
                app_log.error(
                    f"Error processing {result['device_id']}: {result['message']}"
                )
                raise web.HTTPError(
                    500,
                    "Error in analysis pipeline. Please report the input parameters to the analysis team.",
                )
            if not result["result"]:
                app_log.info(f"Empty result for {result['device_id']}")
                continue
            device_result = result["result"]
            contact = {}
            for device_id, contact_info in device_result.items():
                contact_number = await graph.phone_number_for_device_id(
                    device_id)
                if contact_number:
                    if contact_number == phone_number:
                        app_log.warning(
                            f"Omitting contact with self for {mask_number}")
                    else:
                        await self.audit_log(phone_numbers=[contact_number])
                        if pin.PIN_ENABLED:
                            pin_code = await fetch_pin(
                                phone_number=contact_number)
                            contact_info["pin_code"] = pin_code
                        contact[contact_number] = contact_info
                else:
                    app_log.warning(
                        f"Omitting contact for {device_id} with no phone number"
                    )
            if contact:
                contacts.append(contact)

        app_log.info(f"Checking {len(device_ids)} devices for activity")
        last_activity = None
        for device_id in device_ids:
            # use get_device here, not get_devices
            # because get_devices doesn't report last activity accurately
            try:
                device = await devices.get_device(device_id)
            except Exception as e:
                if "not found" in str(e).lower():
                    app_log.warning(f"Device {device_id} not in IoTHub")
                    continue
                else:
                    raise
            device_last_activity = parse_date(device["lastActivityTime"])
            if device_last_activity < devices.before_times:
                app_log.info(
                    f"Device {device['deviceId']} appears to have no activity")
                continue
            if last_activity is None or device_last_activity >= last_activity:
                last_activity = device_last_activity

        self.write(
            json.dumps({
                "phone_number": phone_number,
                "found_in_system": True,
                "last_activity": utils.isoformat(last_activity),
                "contacts": contacts,
            }))
Example #10
0
    async def post(self):
        phone_number = self.current_user["_phonenumber"]

        is_test = False
        TESTER_NUMBERS = set(os.environ.get("TESTER_NUMBERS", "").split(","))
        if phone_number in TESTER_NUMBERS and self.request.headers.get("Test-Number"):
            is_test = True
            tester_number = phone_number
            phone_number = self.request.headers["Test-Number"]

        masked_number = mask_phone(phone_number)
        user = await handlers.find_user(phone_number)

        if is_test:
            if user.get(graph.extension_attr_name("testCredentials")):
                app_log.info(
                    f"Tester {tester_number} is impersonating test user {phone_number}"
                )
            else:
                app_log.error(
                    f"Tester {tester_number} attempted to impersonate non-test user {phone_number}"
                )
                raise web.HTTPError(403, f"{phone_number} is not a test user")

        # TODO: check and unset consent revocation on new registration
        if user.get(graph.extension_attr_name("consentRevoked")):
            consent_revoked_date = user.get(
                graph.extension_attr_name("consentRevokedDate")
            )
            app_log.warning(
                f"Phone number {masked_number} had previously revoked consent on {consent_revoked_date}."
                " Resetting for new device registration."
            )
            await graph.reset_consent(user)

        existing_device_id = user.get(graph.extension_attr_name("deviceId"))
        if existing_device_id:
            app_log.warning(
                f"Phone number {masked_number} is already associated with device id {existing_device_id}. Registering new device."
            )

        device_future = asyncio.ensure_future(devices.create_new_device())
        tic = time.perf_counter()
        try:
            await asyncio.wait_for(device_future, timeout=PROVISIONING_TIMEOUT)
        except asyncio.TimeoutError:
            self.settings["consecutive_failures"] += 1
            app_log.error(
                "Timeout registering device ({consecutive_failures}/{consecutive_failure_limit} before abort)".format(
                    **self.settings
                )
            )
            if (
                self.settings["consecutive_failures"]
                >= self.settings["consecutive_failure_limit"]
            ):
                app_log.critical("Aborting due to consecutive failure limit!")
                loop = asyncio.get_event_loop()
                loop.call_later(2, loop.stop)
            raise web.HTTPError(500, "Timeout registering device")
        else:
            self.settings["consecutive_failures"] = 0
            toc = time.perf_counter()
            app_log.info(f"Registered device in {int(1000 * (toc-tic))}ms")
        device = await device_future
        device_id = device["deviceId"]
        device_key = device["authentication"]["symmetricKey"]["primaryKey"]
        iothub_hostname = devices.iothub_hostname

        # store device id on user in AD
        try:
            await graph.store_device_id(user, device_id)
        except Exception:
            # failed to associated user with device
            # delete the device from IoTHub since nobody is going to use it
            await devices.delete_devices(device_id)
            raise

        self.write(
            json.dumps(
                {
                    "DeviceId": device_id,
                    "PhoneNumber": phone_number,
                    "HostName": iothub_hostname,
                    "SharedAccessKey": device_key,
                    "ConnectionString": ";".join(
                        [
                            f"HostName={iothub_hostname}",
                            f"DeviceId={device_id}",
                            f"SharedAccessKey={device_key}",
                        ]
                    ),
                }
            )
        )