예제 #1
0
    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()
예제 #2
0
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",
        )
예제 #3
0
 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)
예제 #4
0
    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
예제 #5
0
    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)
예제 #6
0
    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,
            )
예제 #7
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,
        )
예제 #8
0
    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())
예제 #9
0
    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
예제 #10
0
    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")
예제 #11
0
    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:
예제 #12
0
    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,
                ]
            )
예제 #13
0
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(),
            ),
        )
예제 #14
0
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