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"])
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)
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
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
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)
def test_wrap_user_empty(): user = {} wrapped_user = graph.wrap_user(user) assert wrapped_user["logName"] == utils.mask_phone("unknown")
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, }))
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}", ] ), } ) )