diff --git a/open_city_profile/graphene.py b/open_city_profile/graphene.py index 5fe6ddaf..b6446597 100644 --- a/open_city_profile/graphene.py +++ b/open_city_profile/graphene.py @@ -1,7 +1,9 @@ import uuid from functools import partial +from typing import Optional import graphene +import sentry_sdk from django.conf import settings from django.forms import MultipleChoiceField from django_filters import MultipleChoiceFilter @@ -21,6 +23,7 @@ primary_email_for_profile_loader, primary_phone_for_profile_loader, ) +from services.models import Service class JWTMiddleware: @@ -182,6 +185,16 @@ def __init_subclass_with_meta__( ) +def send_allowed_data_field_sentry_warning( + message: str, service: Optional[Service], field_name: str +): + with sentry_sdk.push_scope() as scope: + scope.set_extra("service", getattr(service, "name", "Service not identified")) + scope.set_extra("field_name", field_name) + + sentry_sdk.capture_message(message, level="warning") + + class AllowedDataFieldsMiddleware: def _is_profile_query(self, info): @@ -202,9 +215,23 @@ def resolve(self, next, root, info, **args): field_name = to_snake_case(getattr(info, "field_name", "")) if not getattr(info.context, "service", False): - raise ServiceNotIdentifiedError("Service not identified") + if settings.ENABLE_ALLOWED_DATA_FIELDS_RESTRICTION: + raise ServiceNotIdentifiedError("Service not identified") + + send_allowed_data_field_sentry_warning( + "Allowed data field exception would occur: Service not identified", + None, + field_name, + ) if not node.is_field_allowed_for_service(field_name, info.context.service): - raise FieldNotAllowedError("Field is not allowed for service.") + if settings.ENABLE_ALLOWED_DATA_FIELDS_RESTRICTION: + raise FieldNotAllowedError("Field is not allowed for service.") + + send_allowed_data_field_sentry_warning( + "Allowed data field exception would occur. Field is not allowed for service.", + info.context.service, + field_name, + ) return next(root, info, **args) diff --git a/open_city_profile/settings.py b/open_city_profile/settings.py index 620af35d..af7057ac 100644 --- a/open_city_profile/settings.py +++ b/open_city_profile/settings.py @@ -50,6 +50,7 @@ AUDIT_LOG_LOGGER_FILENAME=(str, ""), AUDIT_LOG_TO_DB_ENABLED=(bool, False), OPEN_CITY_PROFILE_LOG_LEVEL=(str, None), + ENABLE_ALLOWED_DATA_FIELDS_RESTRICTION=(bool, False), ENABLE_GRAPHIQL=(bool, False), ENABLE_GRAPHQL_INTROSPECTION=(bool, False), GRAPHQL_QUERY_DEPTH_LIMIT=(int, 12), @@ -169,6 +170,8 @@ ENABLE_GRAPHQL_INTROSPECTION = env("ENABLE_GRAPHQL_INTROSPECTION") GRAPHQL_QUERY_DEPTH_LIMIT = env("GRAPHQL_QUERY_DEPTH_LIMIT") +ENABLE_ALLOWED_DATA_FIELDS_RESTRICTION = env("ENABLE_ALLOWED_DATA_FIELDS_RESTRICTION") + INSTALLED_APPS = [ "helusers.apps.HelusersConfig", "open_city_profile.apps.OpenCityProfileAdminConfig", diff --git a/open_city_profile/tests/conftest.py b/open_city_profile/tests/conftest.py index b3849328..ae9745c6 100644 --- a/open_city_profile/tests/conftest.py +++ b/open_city_profile/tests/conftest.py @@ -209,3 +209,8 @@ def unix_timestamp_now(): @pytest.fixture(params=[None, ""]) def empty_string_value(request): return request.param + + +@pytest.fixture(autouse=True) +def enable_allowed_data_fields_restriction(settings): + settings.ENABLE_ALLOWED_DATA_FIELDS_RESTRICTION = True diff --git a/profiles/tests/test_allowed_data_field_restriction_flag.py b/profiles/tests/test_allowed_data_field_restriction_flag.py new file mode 100644 index 00000000..1fa941ac --- /dev/null +++ b/profiles/tests/test_allowed_data_field_restriction_flag.py @@ -0,0 +1,60 @@ +from unittest import mock + +from profiles.tests.factories import ProfileFactory, SensitiveDataFactory +from services.tests.factories import AllowedDataFieldFactory, ServiceConnectionFactory + + +@mock.patch("open_city_profile.graphene.send_allowed_data_field_sentry_warning") +def test_enable_allowed_data_fields_restriction_flag_false_shows_data( + sentry_mock, user_gql_client, service, settings +): + settings.ENABLE_ALLOWED_DATA_FIELDS_RESTRICTION = False + profile = ProfileFactory(user=user_gql_client.user) + ServiceConnectionFactory(profile=profile, service=service) + service.allowed_data_fields.add(AllowedDataFieldFactory(field_name="name")) + SensitiveDataFactory(profile=profile) + + query = """ + { + myProfile { + firstName + sensitivedata { + ssn + } + } + } + """ + + executed = user_gql_client.execute(query, service=service) + + assert executed["data"]["myProfile"]["firstName"] == profile.first_name + assert ( + executed["data"]["myProfile"]["sensitivedata"]["ssn"] + == profile.sensitivedata.ssn + ) + + +@mock.patch("open_city_profile.graphene.send_allowed_data_field_sentry_warning") +def test_enable_allowed_data_fields_restriction_flag_false_sends_warning_to_sentry_if_access_to_restricted_field( + sentry_mock, user_gql_client, service, settings +): + settings.ENABLE_ALLOWED_DATA_FIELDS_RESTRICTION = False + profile = ProfileFactory(user=user_gql_client.user) + ServiceConnectionFactory(profile=profile, service=service) + service.allowed_data_fields.add(AllowedDataFieldFactory(field_name="name")) + SensitiveDataFactory(profile=profile) + + query = """ + { + myProfile { + firstName + sensitivedata { + ssn + } + } + } + """ + + user_gql_client.execute(query, service=service) + + assert sentry_mock.call_count == 1