def setUp(self) -> None: self.get_local_patcher = mock.patch( "recidiviz.case_triage.authorization.get_local_file", new=_test_get_local_file, ) self.get_local_patcher.start() self.auth_store = AuthorizationStore() self.auth_store.refresh()
class TestAuthorizationStore(TestCase): """Class to test AuthorizationStore""" def setUp(self) -> None: self.get_local_patcher = mock.patch( "recidiviz.case_triage.authorization.get_local_file", new=_test_get_local_file, ) self.get_local_patcher.start() self.auth_store = AuthorizationStore() self.auth_store.refresh() def tearDown(self) -> None: self.get_local_patcher.stop() @parameterized.expand( [ ("*****@*****.**", False, False), ("*****@*****.**", True, False), ("*****@*****.**", True, True), ("*****@*****.**", True, True), ] ) def test_basic_auth(self, email: str, is_allowed: bool, can_see_demo: bool) -> None: self.assertEqual(email in self.auth_store.case_triage_allowed_users, is_allowed) self.assertEqual(self.auth_store.can_see_demo_data(email), can_see_demo) @parameterized.expand( [ ("*****@*****.**", None, None), ("*****@*****.**", None, "in-experiment"), ("*****@*****.**", "in-experiment", "in-experiment"), ("*****@*****.**", "second-variant", "second-variant"), ] ) @freeze_time("2021-01-01 00:00:00") def test_feature_gating( self, email: str, current_variant: Optional[str], future_variant: Optional[str] ) -> None: feature = "can-see-test-feature" self.assertEqual( self.auth_store.get_feature_variant(feature, email), current_variant, msg="Incorrect variant returned for current date", ) self.assertEqual( self.auth_store.get_feature_variant( feature, email, on_date=date(2022, 2, 2) ), future_variant, msg="Incorrect variant returned for future date", )
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 setUp(self) -> None: self.metadata_patcher = mock.patch( "recidiviz.utils.metadata.project_id") self.metadata_patcher.start().return_value = "recidiviz-456" self.auth_store = AuthorizationStore() def no_op() -> str: return "" self.test_app = Flask(__name__) self.test_app.secret_key = "NOT-A-SECRET" self.test_app.add_url_rule("/", view_func=no_op) self.test_app.add_url_rule( "/impersonate_user", view_func=ImpersonateUser.as_view( "impersonate_user", redirect_url="/", authorization_store=self.auth_store, ), ) self.test_client = self.test_app.test_client() @self.test_app.errorhandler(FlaskException) def _handle_auth_error(ex: FlaskException) -> Response: response = jsonify({ "code": ex.code, "description": ex.description, }) response.status_code = ex.status_code return response
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_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 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 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 setUp(self) -> None: self.metadata_patcher = mock.patch( "recidiviz.utils.metadata.project_id") self.metadata_patcher.start().return_value = "recidiviz-456" self.auth_store = AuthorizationStore() self.officer = generate_fake_officer("officer_id_1", "*****@*****.**") self.test_app = Flask(__name__) self.test_app.secret_key = "NOT-A-SECRET" self.helpers = CaseTriageTestHelpers.from_test(self, self.test_app) self.test_client = self.helpers.test_client @self.test_app.errorhandler(FlaskException) def _handle_auth_error(ex: FlaskException) -> Response: response = jsonify({ "code": ex.code, "description": ex.description, }) response.status_code = ex.status_code return response
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")
if email not in authorization_store.allowed_users: raise CaseTriageAuthorizationError( code="unauthorized", description="You are not authorized to access this application", ) if email in authorization_store.admin_users: session[SESSION_ADMIN_KEY] = True auth0_configuration = get_local_secret("case_triage_auth0") if not auth0_configuration: raise ValueError("Missing Case Triage Auth0 configuration secret") authorization_store = AuthorizationStore() authorization_config = Auth0Config(json.loads(auth0_configuration)) requires_authorization = build_auth0_authorization_decorator( authorization_config, on_successful_authorization) store_refresh = RepeatedTimer(15 * 60, authorization_store.refresh, run_immediately=True) if not in_test(): store_refresh.start() # Security headers @app.after_request def set_headers(response: Response) -> Response:
def setUp(self) -> None: self.get_local_patcher = mock.patch( "recidiviz.case_triage.authorization.get_local_file", new=_test_get_local_file, ) self.get_local_patcher.start() self.auth_store = AuthorizationStore() self.auth_store.refresh() self.database_key = SQLAlchemyDatabaseKey.for_schema(SchemaType.CASE_TRIAGE) local_postgres_helpers.use_on_disk_postgresql_database(self.database_key) self.case_triage_user = generate_fake_user_restrictions( "US_XX", "*****@*****.**", can_access_leadership_dashboard=False, can_access_case_triage=True, ) self.dashboard_user = generate_fake_user_restrictions( "US_XX", "*****@*****.**", can_access_leadership_dashboard=True, can_access_case_triage=False, ) self.both_user = generate_fake_user_restrictions( "US_XX", "*****@*****.**", can_access_leadership_dashboard=True, can_access_case_triage=True, ) self.overridden_user = generate_fake_user_restrictions( "US_XX", "*****@*****.**", can_access_leadership_dashboard=True, can_access_case_triage=False, ) self.both_user_different_state = generate_fake_user_restrictions( "US_YY", "*****@*****.**", can_access_leadership_dashboard=True, can_access_case_triage=True, ) self.officer = generate_fake_officer( "test", "*****@*****.**", state_code="US_XX" ) with SessionFactory.using_database(self.database_key) as session: session.expire_on_commit = False session.add_all( [ self.case_triage_user, self.dashboard_user, self.both_user, self.overridden_user, self.both_user_different_state, self.officer, ] )
class TestAccessPermissions(TestCase): """Implements tests for the authorization store that make use of the database as a secondary store of permissions to check for frontend app access.""" # Stores the location of the postgres DB for this test run temp_db_dir: Optional[str] @classmethod def setUpClass(cls) -> None: cls.temp_db_dir = local_postgres_helpers.start_on_disk_postgresql_database() @classmethod def tearDownClass(cls) -> None: local_postgres_helpers.stop_and_clear_on_disk_postgresql_database( cls.temp_db_dir ) def setUp(self) -> None: self.get_local_patcher = mock.patch( "recidiviz.case_triage.authorization.get_local_file", new=_test_get_local_file, ) self.get_local_patcher.start() self.auth_store = AuthorizationStore() self.auth_store.refresh() self.database_key = SQLAlchemyDatabaseKey.for_schema(SchemaType.CASE_TRIAGE) local_postgres_helpers.use_on_disk_postgresql_database(self.database_key) self.case_triage_user = generate_fake_user_restrictions( "US_XX", "*****@*****.**", can_access_leadership_dashboard=False, can_access_case_triage=True, ) self.dashboard_user = generate_fake_user_restrictions( "US_XX", "*****@*****.**", can_access_leadership_dashboard=True, can_access_case_triage=False, ) self.both_user = generate_fake_user_restrictions( "US_XX", "*****@*****.**", can_access_leadership_dashboard=True, can_access_case_triage=True, ) self.overridden_user = generate_fake_user_restrictions( "US_XX", "*****@*****.**", can_access_leadership_dashboard=True, can_access_case_triage=False, ) self.both_user_different_state = generate_fake_user_restrictions( "US_YY", "*****@*****.**", can_access_leadership_dashboard=True, can_access_case_triage=True, ) self.officer = generate_fake_officer( "test", "*****@*****.**", state_code="US_XX" ) with SessionFactory.using_database(self.database_key) as session: session.expire_on_commit = False session.add_all( [ self.case_triage_user, self.dashboard_user, self.both_user, self.overridden_user, self.both_user_different_state, self.officer, ] ) def tearDown(self) -> None: self.get_local_patcher.stop() local_postgres_helpers.teardown_on_disk_postgresql_database(self.database_key) def assert_email_has_permissions( self, email: str, *, can_access_case_triage: bool, can_access_leadership_dashboard: bool, impersonatable_state_codes: Set[str] ) -> None: self.assertEqual( self.auth_store.get_access_permissions(email), AccessPermissions( can_access_case_triage=can_access_case_triage, can_access_leadership_dashboard=can_access_leadership_dashboard, impersonatable_state_codes=impersonatable_state_codes, ), ) def test_basic_db_permissions(self) -> None: self.assert_email_has_permissions( self.case_triage_user.restricted_user_email, can_access_case_triage=True, can_access_leadership_dashboard=False, impersonatable_state_codes=set(), ) self.assert_email_has_permissions( self.dashboard_user.restricted_user_email, can_access_case_triage=False, can_access_leadership_dashboard=True, impersonatable_state_codes={"US_XX"}, ) self.assert_email_has_permissions( self.both_user.restricted_user_email, can_access_case_triage=True, can_access_leadership_dashboard=True, impersonatable_state_codes={"US_XX"}, ) self.assert_email_has_permissions( "*****@*****.**", can_access_case_triage=False, can_access_leadership_dashboard=False, impersonatable_state_codes=set(), ) self.assert_email_has_permissions( self.both_user_different_state.restricted_user_email, can_access_case_triage=True, can_access_leadership_dashboard=True, impersonatable_state_codes={"US_YY"}, ) def test_allowlist_override_succeeds(self) -> None: # User does not have case triage in database, but is in allowlist_v2.json self.assert_email_has_permissions( self.overridden_user.restricted_user_email, can_access_case_triage=True, can_access_leadership_dashboard=True, impersonatable_state_codes={"US_XX"}, ) @parameterized.expand( [ ("*****@*****.**", False), ("*****@*****.**", True), ("*****@*****.**", True), ("*****@*****.**", True), ("*****@*****.**", False), ("*****@*****.**", False), ] ) def test_can_impersonate_US_XX(self, email: str, can_impersonate: bool) -> None: self.assertEqual( self.auth_store.can_impersonate( email, generate_fake_officer( officer_id="test", email="*****@*****.**", state_code="US_XX", ), ), can_impersonate, ) def test_admin_can_impersonate_all_states(self) -> None: self.assertTrue( self.auth_store.can_impersonate( "*****@*****.**", generate_fake_officer( officer_id="test", email="*****@*****.**", ), ) ) def test_cannot_impersonate_own_self(self) -> None: self.assertFalse( self.auth_store.can_impersonate( "*****@*****.**", generate_fake_officer( officer_id="test", email="*****@*****.**", state_code="US_XX", ), ) ) def test_still_can_access_if_not_in_database(self) -> None: with SessionFactory.using_database(self.database_key) as session: with self.assertRaises(NoResultFound): session.query(DashboardUserRestrictions).filter( DashboardUserRestrictions.restricted_user_email == "*****@*****.**" ).one() self.assert_email_has_permissions( "*****@*****.**", can_access_case_triage=True, can_access_leadership_dashboard=False, impersonatable_state_codes=set(), ) def test_recidiviz_employees_can_access(self) -> None: self.assertEqual( self.auth_store.get_access_permissions("*****@*****.**"), AccessPermissions( can_access_case_triage=True, can_access_leadership_dashboard=True, impersonatable_state_codes=set(), ), )
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