Skip to content

Commit

Permalink
feat: add feature flag for checking allowed data fields
Browse files Browse the repository at this point in the history
Adds setting for allowed data field check named as:
ENABLE_ALLOWED_DATA_FIELDS_RESTRICTION
The setting defaults False.
When False, app sends warning message to sentry, when there is query
trying to access a field which is not in service's allowed_data_fields

Refs HP-2319
  • Loading branch information
nicobav committed May 10, 2024
1 parent 6498814 commit f4b6ca7
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 2 deletions.
31 changes: 29 additions & 2 deletions open_city_profile/graphene.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -21,6 +23,7 @@
primary_email_for_profile_loader,
primary_phone_for_profile_loader,
)
from services.models import Service


class JWTMiddleware:
Expand Down Expand Up @@ -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):
Expand All @@ -202,9 +215,23 @@ def resolve(self, next, root, info, **kwargs):
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, **kwargs)
3 changes: 3 additions & 0 deletions open_city_profile/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions open_city_profile/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
60 changes: 60 additions & 0 deletions profiles/tests/test_allowed_data_field_restriction_flag.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit f4b6ca7

Please sign in to comment.