def test_non_admin( self, mock_get_access_permissions: MagicMock, mock_officer_for_hashed_email: MagicMock, mock_officer_for_email: MagicMock, mock_can_impersonate: MagicMock, ) -> None: def test_officer_for_hashed_email(_: Session, _hashed_email: str) -> ETLOfficer: raise OfficerDoesNotExistError def test_officer_for_email(_: Session, email: str) -> ETLOfficer: return generate_fake_officer(officer_id="test", email=email) def test_impersonate(_email: str, _other_email: str) -> bool: return False def test_get_access_permissions(_email: str) -> AccessPermissions: return AccessPermissions( can_access_case_triage=True, can_access_leadership_dashboard=False, impersonatable_state_codes=set(), ) mock_can_impersonate.side_effect = test_impersonate mock_officer_for_email.side_effect = test_officer_for_email mock_officer_for_hashed_email.side_effect = test_officer_for_hashed_email mock_get_access_permissions.side_effect = test_get_access_permissions with self.test_app.test_request_context(): # Test that if there's no given email, a non-admin falls through as if # impersonation didn't happen at all. with self.test_client.session_transaction( ) as sess: # type: ignore sess["user_info"] = { "email": "*****@*****.**", } g.user_context = UserContext("*****@*****.**", self.auth_store) response = self.test_client.get("/api/bootstrap") self.assertEqual(response.status_code, HTTPStatus.OK) # Test that if there's a given email, a non-admin also falls through as if # impersonation didn't happen at all and that key is no longer in the session. with self.test_client.session_transaction( ) as sess: # type: ignore sess["user_info"] = { "email": "*****@*****.**", } g.user_context = UserContext("*****@*****.**", self.auth_store) response = self.test_client.get( f"/api/bootstrap?{IMPERSONATED_EMAIL_KEY}={parse.quote(hash_email('*****@*****.**'))}" ) with self.test_client.session_transaction( ) as sess: # type: ignore self.assertTrue(IMPERSONATED_EMAIL_KEY not in session) self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(g.user_context.current_user.email_address, "*****@*****.**")
def test_clients_for_officer(self) -> None: officer_1 = generate_fake_officer("id_1") officer_2 = generate_fake_officer("id_2") officer_3 = generate_fake_officer("id_3") auth_store = AuthorizationStore() user_context_1 = UserContext( email=officer_1.email_address, authorization_store=auth_store, current_user=officer_1, ) user_context_2 = UserContext( email=officer_2.email_address, authorization_store=auth_store, current_user=officer_2, ) user_context_3 = UserContext( email=officer_3.email_address, authorization_store=auth_store, current_user=officer_3, ) client_1 = generate_fake_client("client_1", supervising_officer_id="id_1") client_2 = generate_fake_client("client_2", supervising_officer_id="id_1") client_3 = generate_fake_client("client_3", supervising_officer_id="id_2") with SessionFactory.using_database(self.database_key) as session: session.expire_on_commit = False session.add(officer_1) session.add(officer_2) session.add(officer_3) session.add(client_1) session.add(client_2) session.add(client_3) with SessionFactory.using_database(self.database_key, autocommit=False) as read_session: self.assertEqual( len( CaseTriageQuerier.clients_for_officer( read_session, user_context_1)), 2, ) self.assertEqual( len( CaseTriageQuerier.clients_for_officer( read_session, user_context_2)), 1, ) self.assertEqual( len( CaseTriageQuerier.clients_for_officer( read_session, user_context_3)), 0, )
def set_receiving_ssi_or_disability_income( session: Session, user_context: UserContext, client: ETLClient, mark_receiving: bool, ) -> None: insert_statement = (insert(ClientInfo).values( person_external_id=user_context.person_id(client), state_code=user_context.client_state_code(client), receiving_ssi_or_disability_income=mark_receiving, ).on_conflict_do_update( constraint="unique_person", set_={"receiving_ssi_or_disability_income": mark_receiving}, )) session.execute(insert_statement) session.commit()
def set_preferred_name( session: Session, user_context: UserContext, client: ETLClient, name: Optional[str], ) -> None: insert_statement = (insert(ClientInfo).values( person_external_id=user_context.person_id(client), state_code=user_context.client_state_code(client), preferred_name=name, ).on_conflict_do_update( constraint="unique_person", set_={"preferred_name": name}, )) session.execute(insert_statement) session.commit()
def set_preferred_contact_method( session: Session, user_context: UserContext, client: ETLClient, contact_method: PreferredContactMethod, ) -> None: insert_statement = (insert(ClientInfo).values( person_external_id=user_context.person_id(client), state_code=user_context.client_state_code(client), preferred_contact_method=contact_method.value, ).on_conflict_do_update( constraint="unique_person", set_={"preferred_contact_method": contact_method.value}, )) session.execute(insert_statement) session.commit()
def create_note(session: Session, user_context: UserContext, client: ETLClient, text: str) -> OfficerNote: """Creates a new officer note in postgres for a given client.""" officer_id = user_context.officer_id client_id = user_context.person_id(client) state_code = user_context.client_state_code(client) insert_statement = insert(OfficerNote).values( state_code=state_code, officer_external_id=officer_id, person_external_id=client_id, text=text, ) result = session.execute(insert_statement) session.commit() return session.query(OfficerNote).get(result.inserted_primary_key)
def test_unimpersonating_without_impersonating(self) -> None: auth_store = AuthorizationStore() auth_store.case_triage_admin_users = ["*****@*****.**"] with self.test_app.test_request_context(): with self.test_client.session_transaction( ) as sess: # type: ignore sess["user_info"] = {"email": "*****@*****.**"} g.user_context = UserContext.base_context_for_email( "*****@*****.**", auth_store) # Perform initial impersonation response = self.test_client.delete("/api/impersonation") self.assertEqual(response.status_code, HTTPStatus.OK)
def update_case_for_person( session: Session, user_context: UserContext, client: ETLClient, action: CaseUpdateActionType, comment: Optional[str] = None, ) -> CaseUpdate: """This method updates the case_updates table with the newly provided actions. Because the underlying table does not have foreign key constraints, independent validation must be provided before calling this method. """ action_ts = user_context.now() last_version = serialize_client_case_version(action, client).to_json() officer_id = user_context.officer_id person_external_id = user_context.person_id(client) insert_statement = (insert(CaseUpdate).values( person_external_id=person_external_id, officer_external_id=officer_id, state_code=client.state_code, action_type=action.value, action_ts=action_ts, last_version=last_version, comment=comment, ).on_conflict_do_update( constraint="unique_person_officer_action_triple", set_={ "last_version": last_version, "action_ts": action_ts, "comment": comment, }, )) session.execute(insert_statement) session.commit() return (session.query(CaseUpdate).filter( CaseUpdate.person_external_id == person_external_id, CaseUpdate.officer_external_id == officer_id, CaseUpdate.action_type == action.value, ).one())
def test_etl_client_for_officer(self) -> None: officer_1 = generate_fake_officer("officer_1") officer_2 = generate_fake_officer("officer_2") auth_store = AuthorizationStore() user_context_1 = UserContext( email=officer_1.email_address, authorization_store=auth_store, current_user=officer_1, ) user_context_2 = UserContext( email=officer_2.email_address, authorization_store=auth_store, current_user=officer_2, ) client_1 = generate_fake_client( "client_1", supervising_officer_id=officer_1.external_id) with SessionFactory.using_database(self.database_key) as session: session.expire_on_commit = False session.add(officer_1) session.add(officer_2) session.add(client_1) with SessionFactory.using_database(self.database_key, autocommit=False) as read_session: # Client does not exist at all with self.assertRaises(PersonDoesNotExistError): CaseTriageQuerier.etl_client_for_officer( read_session, user_context_1, "nonexistent") # Client does not exist for the officer with self.assertRaises(PersonDoesNotExistError): CaseTriageQuerier.etl_client_for_officer( read_session, user_context_2, "client_1") # Should not raise an error CaseTriageQuerier.etl_client_for_officer(read_session, user_context_1, "client_1")
def fetch_opportunity( session: Session, user_context: UserContext, client: ETLClient, opportunity_type: OpportunityType, ) -> Opportunity: """Fetches a given opportunity for an officer and client.""" computed_opps = ComputedOpportunity.build_all_for_client(client) if opportunity_type in computed_opps: return computed_opps[opportunity_type] if user_context.should_see_demo: opportunities = get_fixture_opportunities() for opp in opportunities: if (opp.person_external_id == client.person_external_id and opp.opportunity_type == opportunity_type.value): return opp elif user_context.current_user and client: try: return (session.query(ETLOpportunity).filter( ETLOpportunity.state_code == user_context.client_state_code(client), ETLOpportunity.supervising_officer_external_id == user_context.officer_id, ETLOpportunity.person_external_id == user_context.person_id(client), ETLOpportunity.opportunity_type == opportunity_type.value, ).one()) except sqlalchemy.orm.exc.NoResultFound as e: raise OpportunityDoesNotExistError( f"Could not find opportunity for officer: {user_context.officer_id}, " f"person: {user_context.person_id(client)}, opportunity_type: {opportunity_type}" ) from e raise OpportunityDoesNotExistError( f"No opportunity exists with type {opportunity_type} and " f"for person {user_context.person_id(client)}")
def setUp(self) -> None: self.database_key = SQLAlchemyDatabaseKey.for_schema( SchemaType.CASE_TRIAGE) local_postgres_helpers.use_on_disk_postgresql_database( self.database_key) self.mock_officer = generate_fake_officer("id_1") self.mock_client = generate_fake_client( "person_id_1", last_assessment_date=date(2021, 2, 1), ) self.mock_context = UserContext( email=self.mock_officer.email_address, authorization_store=AuthorizationStore(), current_user=self.mock_officer, )
def clients_for_officer(session: Session, user_context: UserContext) -> List[CasePresenter]: """Outputs the list of clients for a given officer in CasePresenter form.""" if user_context.should_see_demo: # Organize CaseUpdates case_updates = (session.query(CaseUpdate).filter( CaseUpdate.officer_external_id == user_context.officer_id).all()) client_ids_to_case_updates = defaultdict(list) for case_update in case_updates: client_ids_to_case_updates[ case_update.person_external_id].append(case_update) clients = get_fixture_clients() for client in clients: client.person_external_id = user_context.person_id(client) # Organize ClientInfo structs client_infos = (session.query(ClientInfo).filter( ClientInfo.person_external_id.in_( (client.person_external_id for client in clients))).all()) client_ids_to_client_info = {} for client_info in client_infos: client_ids_to_client_info[ client_info.person_external_id] = client_info # Organize OfficerNotes notes = session.query(OfficerNote).filter( OfficerNote.person_external_id.in_( (client.person_external_id for client in clients))) client_ids_to_notes = defaultdict(list) for note in notes: client_ids_to_notes[note.person_external_id].append(note) for client in clients: if client_info := client_ids_to_client_info.get( client.person_external_id): client.client_info = client_info client.notes = client_ids_to_notes[client.person_external_id] return [ CasePresenter( client, client_ids_to_case_updates[client.person_external_id]) for client in clients ]
def test_happy_path( self, mock_get_access_permissions: MagicMock, mock_officer_for_hashed_email: MagicMock, ) -> None: def test_officer_for_hashed_email(_: Session, _hashed_email: str) -> ETLOfficer: return generate_fake_officer( officer_id="test", email="*****@*****.**", state_code="US_XX", ) def test_get_access_permissions(_email: str) -> AccessPermissions: return AccessPermissions( can_access_case_triage=True, can_access_leadership_dashboard=False, impersonatable_state_codes={"US_XX"}, ) mock_officer_for_hashed_email.side_effect = test_officer_for_hashed_email mock_get_access_permissions.side_effect = test_get_access_permissions with self.test_app.test_request_context(): self.auth_store.case_triage_admin_users = [ "*****@*****.**" ] with self.test_client.session_transaction( ) as sess: # type: ignore sess["user_info"] = { "email": "*****@*****.**", } g.user_context = UserContext("*****@*****.**", self.auth_store) response = self.test_client.get( f"/api/bootstrap?{IMPERSONATED_EMAIL_KEY}={parse.quote(hash_email('*****@*****.**'))}" ) self.assertEqual(response.status_code, HTTPStatus.OK) with self.test_client.session_transaction( ) as sess: # type: ignore self.assertEqual( sess[IMPERSONATED_EMAIL_KEY], hash_email("*****@*****.**"), ) self.assertEqual(g.user_context.current_user.email_address, "*****@*****.**")
def test_no_exception_if_both_impersonated_officer_and_self_not_found( self, mock_officer_for_hashed_email: MagicMock, mock_officer_for_email: MagicMock, ) -> None: def mock_officer_for_hash(_session: Session, _hashed_email: str) -> ETLOfficer: raise OfficerDoesNotExistError def mock_officer(_session: Session, _email: str) -> ETLOfficer: raise OfficerDoesNotExistError mock_officer_for_email.side_effect = mock_officer mock_officer_for_hashed_email.side_effect = mock_officer_for_hash auth_store = AuthorizationStore() with self.test_app.test_request_context(): auth_store.case_triage_admin_users = [ "*****@*****.**" ] with self.test_client.session_transaction( ) as sess: # type: ignore sess["user_info"] = { "email": "*****@*****.**" } g.user_context = UserContext.base_context_for_email( "*****@*****.**", auth_store) # Perform initial impersonation response = self.test_client.get( f"/api/bootstrap?{IMPERSONATED_EMAIL_KEY}={parse.quote(hash_email('*****@*****.**'))}" ) # Assert we called officer_for_email twice, once for the non-existent impersonated user, once for non-existent self self.assertEqual(mock_officer_for_email.call_count, 1) self.assertEqual(mock_officer_for_hashed_email.call_count, 1) self.assertIn( hash_email("*****@*****.**"), mock_officer_for_hashed_email.call_args_list[0].args, ) self.assertIn( "*****@*****.**", mock_officer_for_email.call_args_list[0].args, ) self.assertIsNone(g.user_context.current_user) with self.test_client.session_transaction( ) as sess: # type: ignore self.assertTrue(IMPERSONATED_EMAIL_KEY not in session) self.assertEqual(response.status_code, HTTPStatus.OK)
def test_opportunities_for_officer(self) -> None: officer = generate_fake_officer("officer_1") user_context = UserContext( current_user=officer, authorization_store=AuthorizationStore(), email=officer.email_address, ) client = generate_fake_client( "client_1", supervising_officer_id=officer.external_id) etl_opp = generate_fake_etl_opportunity( officer_id=officer.external_id, person_external_id=client.person_external_id) etl_reminder = generate_fake_reminder(etl_opp) with SessionFactory.using_database(self.database_key) as session: session.expire_on_commit = False session.add(officer) session.add(client) session.add(etl_opp) session.add(etl_reminder) with SessionFactory.using_database(self.database_key) as read_session: # expect a non-etl opportunity that we want to mark as deferred reminder = generate_fake_reminder( opportunity=CaseTriageQuerier.opportunities_for_officer( read_session, user_context)[1].opportunity) with SessionFactory.using_database(self.database_key) as session: session.add(reminder) with SessionFactory.using_database(self.database_key) as read_session: queried_opps = CaseTriageQuerier.opportunities_for_officer( read_session, user_context) self.assertEqual(len(queried_opps), 2) employment_opp = queried_opps[1] self.assertEqual( employment_opp.opportunity.opportunity_type, OpportunityType.EMPLOYMENT.value, ) self.assertTrue(employment_opp.is_deferred())
def on_successful_authorization(payload: Dict[str, str], token: str) -> None: """ Memoize the user's info (email_address, picture, etc) into our session """ # Populate the session with user information; This could have changed since the last request if session.get("jwt_sub", None) != payload["sub"]: session["jwt_sub"] = payload["sub"] session["user_info"] = get_userinfo(authorization_config.domain, token) # Also pop the impersonated email key if it exists, since the request could've been an impersonation request prior. if IMPERSONATED_EMAIL_KEY in session: session.pop(IMPERSONATED_EMAIL_KEY) email = session["user_info"]["email"].lower() g.user_context = UserContext(email, authorization_store) if (not g.user_context.access_permissions.can_access_case_triage and not g. user_context.access_permissions.can_access_leadership_dashboard): raise CaseTriageAuthorizationError( code="no_case_triage_access", description="You are not authorized to access this application", )
def defer_opportunity( session: Session, user_context: UserContext, client: ETLClient, opportunity_type: OpportunityType, deferral_type: OpportunityDeferralType, defer_until: datetime, reminder_requested: bool, ) -> OpportunityDeferral: """Implements base opportunity deferral and commits back to database.""" opportunity = CaseTriageQuerier.fetch_opportunity( session, user_context, client, opportunity_type) officer_id = user_context.officer_id person_external_id = user_context.person_id(client) insert_statement = (insert(OpportunityDeferral).values( person_external_id=person_external_id, supervising_officer_external_id=officer_id, state_code=opportunity.state_code, opportunity_type=opportunity.opportunity_type, deferral_type=deferral_type.value, deferred_until=defer_until, reminder_was_requested=reminder_requested, opportunity_metadata=opportunity.opportunity_metadata, ).on_conflict_do_update( constraint="unique_person_officer_opportunity_triple", set_={ "deferral_type": deferral_type.value, "deferred_until": defer_until, "reminder_was_requested": reminder_requested, "opportunity_metadata": opportunity.opportunity_metadata, }, )) result = session.execute(insert_statement) session.commit() return session.query(OpportunityDeferral).get( result.inserted_primary_key)
def opportunities_for_officer( session: Session, user_context: UserContext) -> List[OpportunityPresenter]: """Fetches all opportunities for an officer.""" if user_context.should_see_demo: opportunity_deferrals = (session.query(OpportunityDeferral).filter( OpportunityDeferral.supervising_officer_external_id == user_context.officer_id).all()) # Map from person -> opportunity type -> optional deferral opportunity_to_deferral: Dict[str, Dict[ str, Optional[OpportunityDeferral]]] = defaultdict(dict) for deferral in opportunity_deferrals: opportunity_to_deferral[deferral.person_external_id][ deferral.opportunity_type] = deferral etl_opportunities = get_fixture_opportunities() for opportunity in etl_opportunities: opportunity.person_external_id = user_context.opportunity_id( opportunity) computed_opps_by_client = [ ComputedOpportunity.build_all_for_client( c.etl_client).values() for c in CaseTriageQuerier.clients_for_officer( session, user_context) ] opportunities: List[Opportunity] = [ *etl_opportunities, # this flattens the list of lists *chain.from_iterable(computed_opps_by_client), ] return [ OpportunityPresenter( opportunity, opportunity_to_deferral[ opportunity.person_external_id].get( opportunity.opportunity_type), ) for opportunity in opportunities ] if user_context.current_user: etl_opportunity_info = (session.query( ETLOpportunity, OpportunityDeferral ).outerjoin( OpportunityDeferral, (ETLOpportunity.person_external_id == OpportunityDeferral.person_external_id) & (ETLOpportunity.state_code == OpportunityDeferral.state_code) & (ETLOpportunity.supervising_officer_external_id == OpportunityDeferral.supervising_officer_external_id) & (ETLOpportunity.opportunity_type == OpportunityDeferral.opportunity_type), ).filter( ETLOpportunity.supervising_officer_external_id == user_context.officer_id, ETLOpportunity.state_code == user_context.officer_state_code, ).all()) # we'll fill this with opportunities computed on the fly based on client conditions computed_opportunity_info: List[Tuple[ ComputedOpportunity, Optional[OpportunityDeferral]]] = [] # one query to fetch all clients and their associated opportunity deferrals client_opportunities: List[Tuple[ ETLClient, Optional[OpportunityDeferral]]] = (session.query( ETLClient, OpportunityDeferral).filter( ETLClient.state_code == user_context.officer_state_code, ETLClient.supervising_officer_external_id == user_context.officer_id, ).outerjoin( OpportunityDeferral, OpportunityDeferral.person_external_id == ETLClient.person_external_id, ).order_by(ETLClient.person_external_id).all()) # deferrals are not grouped in DB result because there isn't a proper relationship # to clients; now we group them and iterate over clients to find opportunities for client, client_rows in groupby(client_opportunities, lambda row: row[0]): deferrals = [ row[1] for row in client_rows if row[1] is not None ] computed_opps = ComputedOpportunity.build_all_for_client( client).values() for opp in computed_opps: deferral = next( (d for d in deferrals if d.opportunity_type == opp.opportunity_type), None, ) computed_opportunity_info.append((opp, deferral)) return [ *[ OpportunityPresenter(*info) for info in [*etl_opportunity_info, *computed_opportunity_info] ], ] raise NoCaseloadException()
def _get_mismatch_data_for_officer( officer_email: str, ) -> List[Dict[str, str]]: """Fetches the list of supervision mismatches on an officer's caseload for display in our email templates.""" with SessionFactory.using_database( SQLAlchemyDatabaseKey.for_schema(SchemaType.CASE_TRIAGE), autocommit=False ) as session: try: officer = CaseTriageQuerier.officer_for_email(session, officer_email) except OfficerDoesNotExistError: return [] try: policy_requirements = policy_requirements_for_state( StateCode(officer.state_code) ) except Exception: # If for some reason we can't fetch the policy requirements, we should not show mismatches. return [] user_context = UserContext( email=officer_email, authorization_store=AuthorizationStore(), # empty store won't actually be leveraged current_user=officer, ) opportunities = [ opp.opportunity for opp in CaseTriageQuerier.opportunities_for_officer( session, user_context ) if not opp.is_deferred() and opp.opportunity.opportunity_type == OpportunityType.OVERDUE_DOWNGRADE.value ] mismatches: List[Dict[str, str]] = [] for opp in opportunities: client = CaseTriageQuerier.etl_client_for_officer( session, user_context, opp.person_external_id ) client_name = json.loads(client.full_name) # TODO(#7957): We shouldn't be converting to title-case because there # are many names whose preferred casing is not that. Once we figure out # how to access the original name casing, we should use that wherever possible. given_names = client_name.get("given_names", "").title() surname = client_name.get("surname", "").title() full_name = " ".join([given_names, surname]).strip() mismatches.append( { "name": full_name, "person_external_id": client.person_external_id, "last_score": opp.opportunity_metadata["assessmentScore"], "last_assessment_date": opp.opportunity_metadata[ "latestAssessmentDate" ], "current_supervision_level": policy_requirements.get_supervision_level_name( StateSupervisionLevel(client.supervision_level) ), "recommended_level": policy_requirements.get_supervision_level_name( StateSupervisionLevel( opp.opportunity_metadata["recommendedSupervisionLevel"] ) ), } ) mismatches.sort(key=lambda x: x["last_assessment_date"], reverse=True) if len(mismatches) > MAX_SUPERVISION_MISMATCHES_TO_SHOW: cutoff_date = date.today() - timedelta( days=IDEAL_SUPERVISION_MISMATCH_AGE_IN_DAYS ) cutoff_index = len(mismatches) - MAX_SUPERVISION_MISMATCHES_TO_SHOW for i in range(cutoff_index): if ( dateutil.parser.parse(mismatches[i]["last_assessment_date"]).date() <= cutoff_date ): cutoff_index = i break return mismatches[ cutoff_index : cutoff_index + MAX_SUPERVISION_MISMATCHES_TO_SHOW ] return mismatches
def is_on_caseload(client: ETLClient, user_context: UserContext) -> bool: return user_context.should_see_demo or ( user_context.current_user is not None and client.state_code == user_context.client_state_code(client) and client.supervising_officer_external_id == user_context.officer_id)