async def post(self): body = self.get_json_body(allow_empty=True) or {} phone_number = self.current_user["_phonenumber"] user = await self.lookup_user(phone_number=phone_number) device_ids = await self.get_device_ids(user=user, phone_number=phone_number) self.validate_claims() await self.audit_log( phone_numbers=[phone_number], person_name="{sub_given_name} {sub_middle_name} {sub_family_name}". format(**self.current_user), person_id=self.current_user["sub"], person_organization="", ) response = await base.response_from_gps_events( device_ids=device_ids, phone_number=phone_number, page_number=max(1, body.get("page_number", 1)), per_page=max(0, min(body.get("per_page", 30), 100)), time_from=isoformat(body.get("time_from")), time_to=isoformat(body.get("time_to")), caller="HN", ) self.write(json.dumps(response))
def get_gps_events(db, device_ids, page_number=1, per_page=30, time_from=None, time_to=None): """Retrieve gps events from the database for one or more devices Returns gps events as a list of dicts """ if isinstance(device_ids, str): device_ids = [device_ids] if time_from is None: time_from = utils.now_at_utc() - datetime.timedelta(days=90) if time_to is None: time_to = utils.now_at_utc() + datetime.timedelta(hours=1) events = [] total = 0 for ( plaform, osversion, appversion, model, time_from, time_to, latitude, longitude, accuracy, speed, speed_accuracy, altitude, altitude_accuracy, total, ) in db.execute( "{CALL getdatabyUUIDList (?,?,?,?,?)}", (",".join(device_ids), time_from, time_to, page_number, per_page), ).fetchall(): # from schema, *all* fields are nullable # so make sure that any casting handles None event = { "time_from": utils.isoformat(time_from), "time_to": utils.isoformat(time_to), "latitude": cast_nullable_float(latitude), "longitude": cast_nullable_float(longitude), "accuracy": accuracy, "speed": speed, "speed_accuracy": speed_accuracy, "altitude": altitude, "altitude_accuracy": altitude_accuracy, } events.append(event) return events, total
def get_pin_codes(db, phone_number): pin_codes = [] for (pin_code, created_at,) in db.execute( r"SELECT pin, created_at FROM dbo.getPinCodesByPhoneNumber(?)", phone_number ).fetchall(): pin_codes.append( {"pin_code": pin_code, "created_at": utils.isoformat(created_at)} ) return pin_codes
async def post(self): body = self.get_json_body() phone_number = body.get("phone_number") person_name = body.get("person_name") legal_means = body.get("legal_means") bad_fields = [] if person_name == "" or person_name is None: bad_fields.append("person_name") if legal_means == "" or legal_means is None: bad_fields.append("legal_means") if len(bad_fields) > 0: raise web.HTTPError( 400, "Field(s): {} must be non-empty".format(", ".join(bad_fields))) user = await self.lookup_user(phone_number=phone_number) device_ids = await self.get_device_ids(user=user, phone_number=phone_number) await self.audit_log( phone_numbers=[phone_number], person_name=body.get("person_name"), legal_means=body.get("legal_means"), ) response = await response_from_gps_events( device_ids=device_ids, phone_number=phone_number, page_number=max(1, body.get("page_number", 1)), per_page=max(0, min(body.get("per_page", 30), 100)), time_from=utils.isoformat(body.get("time_from")), time_to=utils.isoformat(body.get("time_to")), caller="FHI", ) self.write(json.dumps(response))
def get_access_log( db, phone_number, person_name="", person_id="", person_organization="", organization="<none>", page_number=1, per_page=30, ): """Fetch the access log for a given phone number""" total = group_total = 0 app_log.info(f"Getting access log for {utils.mask_phone(phone_number)}") events = [] for ( timestamp, number, p_name, p_id, p_org, technical_org, legal_means, group_total, total, ) in db.execute( "{CALL applogfetch(?, ?, ?, ?, ?, ?, ?)}", phone_number, person_name, person_id, person_organization, organization, page_number, per_page, ).fetchall(): person_fields = {} if not p_id and p_name.isdigit(): p_id = p_name p_name = "" app_log.warning( f"Including extra match from old person id record: {timestamp} {p_id}" ) elif p_org is None: try: person_fields = json.loads(p_name) except ValueError: pass else: app_log.warning( f"Including extra match from old JSON record: {timestamp} {person_fields}" ) event = { "timestamp": utils.isoformat(timestamp), "phone_number": phone_number, "person_name": p_name or "", "person_organization": p_org or "", "person_id": p_id or "", "technical_organization": technical_org or "Norsk Helsenett", "legal_means": legal_means, "count": group_total, } event.update(person_fields) events.append(event) return events, total
async def post(self): body = self.get_json_body() phone_number = body["phone_number"] user = await self.lookup_user(phone_number) await self.audit_log(phone_numbers=[phone_number]) phone_number = user["displayName"] mask_number = user["logName"] device_ids = [] request_id = str(uuid.uuid4()) app_log.info( f"Submitting analysis jobs for {mask_number}: {request_id}") request_info = f"lookup:{request_id}:info" # create job queue in redis db = get_redis() result_keys = [] async for device_id in graph.device_ids_for_user(user): device_ids.append(device_id) result_key = f"lookup:{request_id}:result:{device_id}" result_keys.append(result_key) app_log.info(f"Submitting analysis job for {device_id}") job = { "request_id": request_id, "device_id": device_id, "result_key": result_key, "time_from": utils.isoformat(body.get("time_from")), "time_to": utils.isoformat(body.get("time_to")), "expiry": LOOKUP_RESULT_EXPIRY, } db.rpush(REDIS_JOBQUEUE_NAME, json.dumps(job).encode("utf8")) # push device id onto job queue if not device_ids: app_log.info(f"Phone number {mask_number} has no devices") self.set_status(404) self.write( json.dumps({ "phone_number": phone_number, "found_in_system": False })) return app_log.info(f"Storing request info for {request_id}") db.set( request_info, json.dumps({ "phone_number": phone_number, "result_keys": result_keys, "device_ids": device_ids, }), ex=LOOKUP_RESULT_EXPIRY, ) self.set_status(202) self.write( json.dumps({ "request_id": request_id, # todo: figure out how to get this right? # /fhi/ prefix isn't available from APIM # but it's wrong when not behind APIM "result_url": f"https://{self.request.host}/fhi/lookup/{request_id}", "result_expires": utils.isoformat(utils.now_at_utc() + datetime.timedelta( seconds=LOOKUP_RESULT_EXPIRY)), }))
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, }))