Skip to content

Commit

Permalink
feat: add middleware for checking allowed data fields
Browse files Browse the repository at this point in the history
When calling profile any kind there should be checked to which fields
the service has access rights by using the allowed data fields.
This adds middleware and mixin class for Profile model and
VerifiedPersonalInfo models for checking that the queried fields are
allowed for the service.

Refs HP-2319
  • Loading branch information
nicobav committed May 22, 2024
1 parent 32f11f0 commit 6e28a10
Showing 1 changed file with 70 additions and 2 deletions.
72 changes: 70 additions & 2 deletions profiles/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
ProfileMustHavePrimaryEmailError,
ServiceConnectionDoesNotExist,
ServiceDoesNotExist,
ServiceNotIdentifiedError,
TokenExpiredError,
)
from open_city_profile.graphene import UUIDMultipleChoiceFilter
Expand Down Expand Up @@ -370,6 +371,46 @@ def filter_by_nin_exact(self, queryset, name, value):
return queryset.none()


class AllowedDataFieldsMixin:
"""
Mixin class for checking allowed data fields per service.
`allowed_data_fields_map` is a dictionary where the key is the `field_name` of the allowed data field
`allowed_data_fields.json` and the value is an iterable of django model's field names that the `field_name`
describes. For example, if the `field_name` is `name`, the value could be `("first_name", "last_name")`.
e.g:
allowed_data_fields_map = {
"name": ("first_name", "last_name", "nickname"),
"personalidentitycode": ("national_identification_number",),
"address": ("address", "postal_code", "city", "country_code")
}
`always_allow_fields`: Since connections are not defined in `allowed_data_fields.json` they should be
defined here. If the field is connection and the node does not inherit this mixin the data will be available
to all services.
"""

allowed_data_fields_map = {}
always_allow_fields = ["id", "service_connections"]
check_allowed_data_fields = True

@classmethod
def is_field_allowed_for_service(cls, field_name: str, service: Service):
if not service:
raise ServiceNotIdentifiedError("No service identified")

if field_name in cls.always_allow_fields:
return True

allowed_data_fields = service.allowed_data_fields.values_list(
"field_name", flat=True
)
return any(
field_name in cls.allowed_data_fields_map.get(allowed_data_field, [])
for allowed_data_field in allowed_data_fields
)


class ContactNode(DjangoObjectType):
class Meta:
model = Contact
Expand Down Expand Up @@ -447,7 +488,7 @@ class Meta:
fields = ("street_address", "additional_address", "country_code")


class VerifiedPersonalInformationNode(DjangoObjectType):
class VerifiedPersonalInformationNode(DjangoObjectType, AllowedDataFieldsMixin):
class Meta:
model = VerifiedPersonalInformation
fields = (
Expand All @@ -459,6 +500,18 @@ class Meta:
"municipality_of_residence_number",
)

allowed_data_fields_map = {
"name": ("first_name", "last_name", "given_name"),
"personalidentitycode": ("national_identification_number",),
"address": (
"municipality_of_residence",
"municipality_of_residence_number",
"permanent_address",
"temporary_address",
"permanent_foreign_address",
),
}

# Need to set the national_identification_number field explicitly as non-null
# because django-searchable-encrypted-fields SearchFields are always nullable
# and you can't change it.
Expand Down Expand Up @@ -567,14 +620,29 @@ def resolve_addresses(self: Profile, info, **kwargs):


@key(fields="id")
class ProfileNode(RestrictedProfileNode):
class ProfileNode(RestrictedProfileNode, AllowedDataFieldsMixin):
class Meta:
model = Profile
fields = ("first_name", "last_name", "nickname", "language")
interfaces = (relay.Node,)
connection_class = ProfilesConnection
filterset_class = ProfileFilter

allowed_data_fields_map = {
"name": (
"first_name",
"last_name",
"nickname",
),
"email": ("emails", "primary_email"),
"phone": ("phones", "primary_phone"),
"address": ("addresses", "primary_address"),
"personalidentitycode": ("sensitivedata",),
}
always_allow_fields = AllowedDataFieldsMixin.always_allow_fields + [
"verified_personal_information"
]

sensitivedata = graphene.Field(
SensitiveDataNode,
description="Data that is consider to be sensitive e.g. social security number",
Expand Down

0 comments on commit 6e28a10

Please sign in to comment.